资讯动态

项目书爬数据全流程:工具选型、采集解析与常见坑

发布时间:2026/9/8 10:45:01 来源:尧图企业网站定制
1. 为什么“项目书爬数据”值得单独写一篇先把这个场景讲清楚。项目书这个事情在招投标、科研申报、投资尽调、政府补贴申请这些领域里太常见了。很多人手里积压了一堆PDF、Word、网页端公示的项目申报书、中标通知书、立项名单、结题报告格式五花八门来源七零八落。真要用的时候——比如做行业分析、竞争对手监控、政策复盘、数据报表——全靠人工一份份打开、复制、粘贴几十份还能忍几百份就是灾难。这个需求跟普通爬虫有个明显区别不是“爬一个网站”而是“围绕项目清单批量抓取结构化信息”。它往往涉及多个站点、多种文件格式、混合页面结构而且数据字段高度定制——项目名称、申报单位、金额、时间、负责人、区域、行业分类每个字段都对应你的业务口径。通用的爬虫教程很少会把这些讲透网上能搜到的大多是“怎么爬Boss直聘”或者“怎么爬美团商家”用到项目书这种场景总觉得差半口气。我最早接触这个需求是做区域产业政策梳理。领导丢给我一个名单说“这里面五百家企业每家近三年的省市级项目都查出来”我当时如果手动去点估计一个月就没了。后来把流程拆成三块——清单整理、网页采集、字段抽取——每块用最简单的工具解决最后整个链路跑下来不到半天。这篇文章就是把那套流程完整拆开把我踩过的坑、试过的方案、最后稳定的配置全部写清楚给正在做同类事情的人一条能直接走的路。文章会从工具选型讲起然后是单页采集和字段抽取的完整实操再到并发提速、反爬应对最后是常见的异常场景和排查思路。整体偏工程落地中间每一段核心代码都可以直接搬走改改用。2. 动手前先把思路和工具链理清楚2.1 拿到项目书数据前要想清楚的五件事很多人一上来就写代码结果写到一半发现数据源不对、字段要重来、网站结构变了。我个人的习惯是任何事情开始前先花二十分钟回答五个问题第一个数据从哪来。项目书的来源通常有三类公开的政府项目公示页面、招标采购平台的项目公告、行业侧的信息聚合站点。每一类的页面结构差异非常大公示页一般是表格或列表公告页可能是长文章聚合站点则常常有列表页加详情页两层结构。明确来源后才能判断要写几套解析逻辑。第二个要抓到什么粒度。同样一份项目书数据有的人只需要项目名称和金额有的人还要申报时间、所属领域、承担单位地址、负责人联系方式。这个粒度直接决定了解析字段的数量和难度也决定了后续清洗的工作量。我的建议是宁可一开始多定两个字段也不要后面返工再爬一次。第三个更新频率是多少。是只做一次性打包抓取还是每周定时更新如果是一次性的脚本写直白点跑完就算如果要持续监控某个网站的政策更新就得加上增量抓取、去重、状态管理复杂度和前者完全不是一个量级。第四个规模和频率约束。目标是一百家单位的五百条项目还是全行业几万条明细目标越小越可以用串行加礼貌延时尽量不给目标网站压力目标越大越要在流程设计上考虑并发和容错。第五个数据保存成什么格式。大多数业务场景Excel就够了但如果你后续要做分析或者对接BICSV、SQLite、数据库都可能成为选项。我自己的习惯是统一落CSV作为中间态方便预览和检查最后再转成业务需要的格式。这五个问题不需要全部精确回答但必须在动手前有大致答案。否则很容易出现“爬了两天发现字段太粗或太细”的尴尬境地。2.2 最小可用工具组合Anaconda加requests加pandas工具选型这件事我见过太多人一上来就上Scrapy、上Selenium东西是能跑但调试周期长、依赖复杂最后改起来也麻烦。对于“项目书爬数据”这个场景我的建议是直接用最小可用组合Anaconda管理Python环境requests负责拿页面BeautifulSoup或者lxml负责解析HTMLpandas负责整理数据和输出。为什么要Anaconda因为爬虫任务往往涉及解析库、HTTP库、数据处理库的混合安装Anaconda把Python环境、包管理、常用科学计算库一次性装好省去了很多环境层面的折腾。尤其是新手用Anaconda开一个独立环境就算装坏了重来也就几分钟的事。用Anaconda建环境的命令很简单conda create -n spider python3.10 -y conda activate spider接下来把需要的库装上pip install requests beautifulsoup4 lxml pandasrequests没什么好说的Python生态里最成熟的HTTP客户端。BeautifulSoup4加lxml做解析前者写起来直观、适合快速开发后者解析速度快、适合跑大批量。pandas则是数据清洗和导出的利器DataFrame一行就能生成CSV。为什么不推荐直接上Scrapy原因有两个一是项目书类任务通常要适配多个不同站点Scrapy的Spider抽象在跨站场景下有优势但配置成本和学习成本都高二是如果需求是“简单直接拿一批数据”Scrapy的异步框架和中间件机制属于过度设计。等哪天真遇到了需要大规模、多层级、长时间跑的项目再迁移到Scrapy也不迟。这个组合的好处是每块都能单独替换出错时能快速定位到具体模块。比如requests被反爬拦截了可以直接换成curl或者httpx不影响后面的解析和导出逻辑。2.3 值得关注的几个“爬数据”姿势和误区网上关于爬数据的热搜词比如“boss直聘python数据爬取”“如何使用anaconda爬取网络数据”“如何py爬取美团数据”其实方向都很正但有几个坑大家容易踩。第一个误区是把别人网站的焦点都放在“反爬”上。实际上很多项目书类网站根本没有严格的反爬它们真正难搞的是页面结构不统一、历史数据年份久远导致链接失效、公告附件是扫描版PDF这些“低级问题”。反爬策略不是不重要但把百分之八十的精力放在应对反爬上是典型的资源错置。第二个误区是看到网页就用Selenium模拟浏览器。Selenium适合的是页面动态渲染、内容由JavaScript异步加载的场景。但如果你目标站的表格内容是直接渲染在HTML里的用requests拿下来解析就行性能快好几个量级。判断标准很简单浏览器里右键“查看网页源代码”如果数据直接在里面就用requests如果源代码里看不到数据、只有一堆JS脚本再考虑Selenium或者直接找接口。第三个误区是不做数据校验。爬下来的数据里经常出现空值、乱码、重复项直接进Excel往往要到很晚才发现问题。养成爬完就做校验的习惯能省下很多返工时间。3. 工具链装配完成后核心实操与完整流程3.1 第一步从单页请求开始构建稳定请求头很多爬虫教程忽视了请求头但对于项目书类网站这是能不能拿到数据的分水岭。目标网站一般会检查User-Agent和Referer如果裸奔访问轻则返回错误页重则IP被临时封禁。一个平衡了隐蔽性和稳定性的请求头配置长这样import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Connection: keep-alive, }这里有几个细节值得注意。User-Agent尽量保持和主流浏览器一致不要用那种很长很奇怪的UA容易被识别为脚本。Accept-Language指定成中文避免部分网站在中英文之间做重定向。Connection用keep-alive在批量请求时能复用TCP连接速度会明显提升。请求头写好后先不要写解析逻辑单独跑一次请求测试看返回的HTML是否包含目标数据。我一般会打印状态码和页面前几百个字符用这两项判断是正常页面、登录跳转还是拦截页。这一步确认通过才继续往下走。url https://example.com/project/notice/list?page1 resp requests.get(url, headersheaders, timeout10) print(resp.status_code) print(resp.text[:500])另外建议在请求函数里加上超时控制。很多列表页系统偶尔会慢不加timeout的话请求会一直挂在那里批量跑的时候等于白白浪费线程。3.2 第二步解析列表页拿全每一行的核心字段列表页是所有项目书数据的入口里面通常直接展示着项目名称、发布时间、状态等基础字段。用BeautifulSoup解析列表页的表格是老本行但有几个常见的页面套路要提前识别。第一种是HTML表格标签规整每个tr对应一条项目记录。这种直接用find_all定位tr再逐行提取td就可以了最简单。第二种是列表加详情页结构列表页只有标题和链接详细字段藏在详情页里。这种情况必须先抓列表页拿到链接然后每个链接再发一次请求。很多新手栽在这里列表页解析完了就直接存数据结果字段全是残缺的。第三种是JavaScript渲染的列表源码中没有列表数据只有一长串JS逻辑。遇到这种页面优先开F12看Network面板在XHR请求里找到返回JSON数据的接口。通常这个接口可以直接用requests访问返回的数据比HTML还好解析。下面是一个典型的表格解析示例from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) rows soup.select(table tbody tr) for row in rows: cells row.find_all(td) if len(cells) 3: continue project_name cells[0].get_text(stripTrue) publish_date cells[1].get_text(stripTrue) link cells[0].find(a)[href] if cells[0].find(a) else print(project_name, publish_date, link)这里要注意get_text()默认会把子标签的文本全部拼进来加上stripTrue可以去掉首尾空格和换行避免后面清洗数据的时候被隐形字符坑到。if len(cells) 3这个判断不是多余的很多表格的底部有合并行、提示行不加判断会把噪音数据混进来。3.3 第三步详情页采集把完整信息抽成结构化字段列表页拿到只是开头大部分业务需求需要用详情页的完整字段。详情页的解析逻辑不复杂难在每个页面结构可能略有不同——有的用表格展示字段有的用“字段名字段值”的方式平铺。一个通用性较强的做法是先按字段名定位再取相邻节点。比如def parse_detail(text): soup BeautifulSoup(text, lxml) result {} # 方式一表格结构 for tr in soup.select(table tr): tds tr.find_all(td) if len(tds) 2: key tds[0].get_text(stripTrue) value tds[1].get_text(stripTrue) result[key] value # 方式二平铺结构 for item in soup.select(.info-item): key item.select_one(.label) value item.select_one(.value) if key and value: result[key.get_text(stripTrue)] value.get_text(stripTrue) return result解析出来的字段最好立刻统一键名。比如详情页里“申报单位”“申请单位”“承担单位”可能表达同一个概念在输出前就要统一成unit。这一步叫字段归一化直接决定最后的数据能不能用来横向对比。还经常遇到金额字段页面上可能是“500万元”“500万”“5000000元”三种写法保险起见用正则提取数字部分再统一单位import re def parse_amount(text): if not text: return None match re.search(r([\d,.])\s*万元, text) if match: return float(match.group(1).replace(,, ).replace(, )) match re.search(r([\d,.])\s*元, text) if match: return float(match.group(1).replace(,, ).replace(, )) / 10000 return None这种细节如果不在解析阶段处理后面Excel里就全是“500万元”和“5,000,000元”混着做汇总统计的时候非常头疼。3.4 第四步把数据用pandas整理输出当所有页面跑完拿到一批字典列表记录后用pandas一次性转成结构化表格import pandas as pd data [] # data是每个详情页解析后得到的字段字典列表 df pd.DataFrame(data) df df.drop_duplicates() # 去重 df df.fillna() # 空值填充 df.to_csv(projects.csv, indexFalse, encodingutf-8-sig)这里要特别提醒encodingutf-8-sig。直接用utf-8存CSVExcel打开大概率乱码。utf-8-sig会在文件头加BOMExcel对带BOM的UTF-8识别率最高这个细节我当年栽过一次之后再也没有忘过。去重的时候建议先用关键字段组合判断重复。比如“项目名称单位名称发布时间”三个字段完全一致几乎可以断定是同一条记录如果只有项目名称一致可能是不同年度的同名项目不要误删。字段导出前我还习惯顺手打印一行df.info()看看有没有字段变成全NaN有没有类型不对。这一步检查花费两分钟能帮你少说十句“怎么跑完数据不对”的烦恼。3.5 第五步用并发把速度提上去单线程跑几十条需求还能忍跑到几百条、上千条的时候速度就是硬伤。项目书网站的响应时间通常在0.3到1秒之间单线程抓一千条要十到十五分钟并发后能压缩到两三分钟。但并发也会放大被封的风险所以必须控制节奏。最稳妥的做法是用ThreadPoolExecutor加自定义限速from concurrent.futures import ThreadPoolExecutor, as_completed import time def fetch_detail(url): time.sleep(0.3) # 控制整体请求频率 resp requests.get(url, headersheaders, timeout10) return parse_detail(resp.text) urls [...] # 需要抓取的详情页链接列表 results [] with ThreadPoolExecutor(max_workers5) as executor: future_map {executor.submit(fetch_detail, url): url for url in urls} for future in as_completed(future_map): try: results.append(future.result()) except Exception as e: print(f抓取失败: {future_map[future]}错误: {e})max_workers5是我个人比较常用的值对大多数中小网站都算礼貌。如果目标站响应速度很快、且你确认没有严格的反爬可以适当加大到8或10如果遇到429或者503立刻降回3并且每次请求的sleep时间加到0.5秒以上。这里想说一句经验之谈快速失败和重试机制有时候比并发数更重要。一个稳定的爬虫不应该是“尽量多抓”而是“抓到了的部分保证不丢没抓到的部分能记录重试”。所以在合适的位置加异常捕获和日志输出能让整个任务的可维护性上一个台阶。3.6 第六步断点续跑与增量更新跑了几百条之后遇到网络波动或者代码异常是家常便饭。如果脚本设计成“从头跑起”已经抓到的数据就白费了。所以从第一天起就要考虑断点续跑。最简单的实现是每成功解析一条记录就把链接写入一个已完成的清单文件下次启动时先加载这个清单跳过已经完成的链接。这样即使中断了重复执行脚本就能从断点处继续。done_urls set() if os.path.exists(done.txt): with open(done.txt, r) as f: done_urls set(f.read().splitlines()) for url in urls: if url in done_urls: continue # 抓取、解析、保存 ... with open(done.txt, a) as f: f.write(url \n)增量更新的思路也一样只不过“已完成的链接”变成“已经入库的项目的唯一标识”。每次跑之前查一下库把新的项目链接追加到任务列表里。这个办法虽然土但在没有引入数据库和调度框架的情况下已经足够管理大多数项目书采集任务。4. 常见问题与坑位排查实录4.1 案例一页面返回200但解析出来是空数据这个问题排在所有爬虫问题的最前面。你的请求明明成功了状态码200也拿到了但用BeautifulSoup一找目标表格是空的。排查步骤其实有规律可循。第一看页面是否加载在iframe里。很多项目公示网站使用iframe嵌入内容外层HTML里只有一个iframe src...标签数据在子页面里。遇到这种先用resp.text搜一下iframe关键字拿到子页面的地址后再发一次请求获取真正的内容页。第二看页面是不是用了JS动态渲染。判断方法前面说过打开网页源码看目标数据在不在里面。如果源码里找不到数据说明是异步接口去Network面板找XHR请求。有些接口返回的是JSON解析起来比HTML简单有些返回的是JS代码片段需要用正则提取。第三看是不是登录后才可见。这类情况在招投标网站特别多列表页可以匿名访问详情页却必须登录。遇到这种最常见的方法是携带Cookie访问。先用浏览器登录一次在DevTools里复制Cookie字符串放到请求头里。注意Cookie有有效期过期了就需要重新获取。4.2 案例二数据量一大耗时指数级上升一百条数据跑得挺快一千条就开始像蜗牛。这时候先检查是不是每个请求都重新建立了TCP连接。如果没有用Sessionrequests默认会在每个请求上新建连接耗时和资源开销都比较可惜。用Session统一管理session requests.Session() session.headers.update(headers) def fetch(url): resp session.get(url, timeout10) return resp.text同一个Session会复用底层的TCP连接速度提升非常明显。再配合连接池参数session requests.Session() adapter requests.adapters.HTTPAdapter(pool_connections10, pool_maxsize10) session.mount(http://, adapter) session.mount(https://, adapter)这两个配置改完之后一千条数据的耗时经常能缩短三分之一到一半。当然并发也能省时间但并发要谨慎Session复用是无风险优化。4.3 案例三被目标网站拦了IP或封了账号小规模抓取一般不会遇到封禁但如果你目标网站比较敏感或者你的请求频率确实高就可能遇到403、429、验证码这些情况。应对思路从轻到重排列第一步降低频率。把请求之间的sleep调大比如从0.3秒调到1秒多数情况下问题就解决了。很多“被封”其实是请求频率超过了对方服务器的合理阈值。第二步设置重试。临时性的429或503等几秒再重试往往就过了。注意不要无限重试一般三次就够了。第三步更换请求头。除了User-Agent还可以补充一些浏览器常用的Header比如Accept-Encoding、Sec-Fetch-Site、Sec-Fetch-Mode。虽然部分网站在真实性校验上做得不严格但多几项总比裸奔强。第四步检查是不是走了登录态。有些站点的反爬策略是“未登录用户请求频率超过N次就封IP”但登录后频率阈值会高很多。这种情况挂上Cookie往往比换代理更有效。我自己遇到最麻烦的一次是对方设置了滑动验证码而项目数据又必须登录后才能拿到全字段。最后我的方案是把这个网站的采集频率降到“每分钟不超过10次”用几个不同账号轮流登录加上每次请求之间随机延时3到5秒跑了整整一天慢是慢了点但数据完整拿下来了。我的体会是不要太相信工具的“妖艳技巧”合规和节奏才是发不出来最大的保障。4.4 常见问题速查表现象可能原因解决方案状态码200但无数据iframe嵌套或JS动态加载查找iframe子页面或找XHR接口返回403请求头缺少关键字段补充Referer、User-Agent、Accept-Language返回429请求频率过高增大延时降低并发数加重试机制中文乱码编码识别错误先resp.encoding resp.apparent_encoding再取文本Excel打开乱码编码不是utf-8-sigto_csv时改encodingutf-8-sig字段混入换行符未使用strip统一get_text(stripTrue)金额格式不统一页面单位不同解析阶段用正则统一换算单位数据重复多次运行未去重用关键字段组合做去重链接在下一页分页加载解析下一页链接或page参数迭代这个表是我实际排查中归纳出来的高发问题几乎每个新项目都会撞上其中两三条。4.5 两个容易被忽略的稳健性习惯第一个习惯是把不合法的HTML修正好再解析。某些老旧的政务网站HTML标签不够规范BeautifulSoup对畸形标签的容忍度比lxml高但lxml速度快。稳妥的搭配是BeautifulSoup加lxml解析器遇到明显异常可以把HTML片段用html5lib再处理一次。代价是html5lib速度很慢只在异常时用。第二个习惯是给请求加上随机的User-Agent或延时。不要每次都一模一样稍微加一点随机性不仅减少被封概率也让日志看起来更接近真人操作。比如import random time.sleep(random.uniform(0.5, 1.5))注意这里随机延时不是要你伪装成什么只是模拟人的操作节奏。合规采集本来就是公开数据节奏合理大家相安无事。5. 合规边界与工程化进阶思考5.1 爬数据要守住的基本准则讲完技术必须聊几句合规。项目书数据属于公开信息抓取公开数据本身没有问题但有几个基本原则要守住第一只抓公开可见的数据不突破登录授权和验证码机制。网站要求登录才能看的信息最好走正规接口或者人工确认不要在技术上强行绕过。第二控制请求频率不给目标网站造成压力。公开数据也是别人运营出来的爬取的时候保持基本的礼貌尽量模拟正常访问节奏。第三获取的数据不用于侵权、不正当竞争、泄露他人隐私等目的。项目书数据中有时会包含联系人、联系电话、地址使用时要特别注意范围。技术本身是中性的但使用技术的人要有边界感。我见过一些人把爬虫写得很野气一天抓几十万条最后对方直接上了验证码大家都没得玩。与其那样不如一开始就温和一点。5.2 从一次性脚本到半自动小工具当脚本稳定后可以往“小工具”的方向迭代。我的做法是增加一个参数配置文件把目标URL、请求间隔、字段映射关系、输出路径都放到配置里这样别人用或者自己换个站点时只需要改配置不用改代码。import json with open(config.json, r) as f: config json.load(f) url config[list_url] interval config[interval] fields config[fields]这算半个工程化但收益很明显。尤其是“字段映射”这个设计把同一个网站上不同页面的字段名统一到一个标准上后续清洗数据的成本会再次下降。再往前走一步就是用定时任务让脚本自动跑。Windows的任务计划程序或者Linux的crontab都能做设定每月或者每周定点运行一次输出文件按日期命名基本就是一个可用的小型数据服务了。5.3 如果数据量继续膨胀怎么升级如果项目书数据量涨到几万条、几十万条CSV文件读写开始变慢Excel直接打不开那时候就要考虑升级到数据库。SQLite是最轻量的过渡方案无需安装服务一个文件就是一个库pandas可以无缝读写非常适合中期过渡。import sqlite3 conn sqlite3.connect(projects.db) df.to_sql(projects, conn, if_existsreplace, indexFalse)再往后数据量更大、需要多人协作或者需要对外提供查询接口时再引入MySQL或PostgreSQL也不迟。原则是不为将来的规模过度设计每一步都用当下够用的方案。6. 关于“工作量和工具选择”的一些个人建议最后再分享一点实操中的体会。项目书爬数据这件事真正的工作量大头不是“写爬虫”而是“清洗数据”和“字段对齐”。代码可能三个小时就写完了但不同来源的项目数据格式差异非常大有的叫“项目名称”有的叫“课题名称”有的金额写在标题里有的金额在附件里有的日期格式是“2024-03-01”有的是“2024.3.1”。这些数据不统一拿到业务手里就是一堆废数据。所以我建议所有做同类事情的人在写代码之前先问一句拿到数据之后谁来用用来做什么字段口径是什么。把这个问题想清楚再动笔写代码效率会高很多。工具方面再强调一点requests加pandas这套组合适合绝大多数情况但如果你要抓的页面大量依赖JavaScript渲染或者目标网站有严格的登录和会话管理不用硬撑着用requestsSelenium或者Playwright都是更合理的选择。工具没有高下只有合适与否。按这套流程我帮不同行业的朋友处理过招投标文件清单、科研项目立项数据、政府补贴公示记录基本上从拿到需求到交付数据小规模一两天大规模一周内都能搞定。希望这篇文章能让你少走弯路把时间花在真正有价值的数据分析上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价