Python资讯爬虫工程化实战:解析、去重与反爬限速全攻略
发布时间:2026/9/3 18:57:41 作者:尧图编辑部 阅读量:1,286

简介一份面向Python爬虫学习者与舆情数据分析人员的完整实践项目基于多线程与selenium模拟浏览器技术实现对人民网领导留言板留言的高效抓取适用于动态页面采集、JS渲染处理及反爬应对等场景。压缩包内共15个文件包含3个Python爬虫脚本、11个csv留言数据文件及1个url说明txt整体体积1.8MB结构清晰可直接运行调试。目前已有811人学习下载项目覆盖大兴、通州、海淀等多个区县领导留言数据同时提供多线程任务调度、selenium自动化操作、数据落盘等关键代码便于读者理解从请求发送到数据解析入库的完整链路。通过源码研读与实测数据可快速掌握动态网页爬虫的工程化写法为政务公开数据采集与社情民意分析提供可复用的参考范式。 我拿到这个压缩包的时候最先注意到的是文件名本身。Renminwang-Message-Crawler-2.rar一眼看过去就知道这是个资讯采集类的爬虫项目。Renminwang 是目标站点的拼音标识做爬虫的人经常这么干——用站点拼音给项目命名方便团队内部一看就懂Message 说明采集对象是消息/资讯条目而不是整张页面Crawler 是爬虫类型-2 则代表这是第二个迭代版本。这类任务在现实里非常普遍某个资讯站每天更新几十上百条内容靠人工盯着页面刷新再复制粘贴效率极低漏抓、错抓、格式混乱都是常态。这个爬虫要解决的就是定时自动跑一圈把新增消息列表拿下来再进详情页把标题、发布时间、正文、来源这些字段抽出来存成结构化数据供检索、展示、统计使用。如果你正在做内容聚合、竞品资讯监控、行业新闻收集或者只是想系统学一下爬虫项目的工程化写法这个项目的思路都值得参考。它不涉及多复杂的算法难点全在工程细节解析稳定性、去重策略、异常兜底、抓取节奏控制。下面我会把这几块逐个拆开讲代码用 Python 写目标站点统一用 target-site.com 占位你可以直接替换成自己实际要采集的站点。1. 从文件名看项目本质这到底是个什么工具1.1 命名拆解每个字段都不是随便写的这种站点拼音-采集对象-技术类型-版本号的命名方式在爬虫工程里非常常见。好处是任何人拿到项目压缩包不用翻文档就能猜到任务大概是什么。Renminwang 是拼音标识比用完整域名短输入方便用 Message 而不是 News、Article说明采集重点落在一条条结构化的消息上而不是文章长文这个定位决定了后续解析字段的粒度Crawler 不用多说标明技术属性-2 说明这是第二次重构或者第二套采集任务经历过一次迭代意味着第一个版本一定踩过某些坑比如解析逻辑和抓取调度耦合在一起、改一个站点配置就要动主代码第二个版本通常会把这些拆开。另外注意打包格式是 rar。这本身是个信息项目大概率是在 Windows 环境里分发的。我在实际项目里收过很多这种压缩包里面通常是 Python 工程核心就是几个文件main.py 作为入口config.py 放目标站点配置requirements.txt 列依赖数据落在本地 SQLite 或者 output 目录下的 JSON 文件里。rar 分发包的好处是部署简单解压后装依赖就能跑不需要额外搭环境缺点是版本管理混乱如果团队协作我更建议用 Git 仓库配合标签tag管理版本而不是发压缩包但这属于工程规范问题不影响这个项目本身的价值。1.2 一个采集任务的典型场景和产出假设业务方提了个需求每天早上 9 点把目标站点过去 24 小时发布的消息全部抓回来整理成表格推给运营同学看。如果人工做需要打开页面、逐条判断发布时间、复制标题和正文、粘贴到表格几十条内容就要折腾一上午。换成爬虫后这个动作被压缩成一条命令或者一次定时任务。最终产出的结构大致是这样的{ id: 20240611-001, title: 示例消息标题, url: https://target-site.com/p/12345, published_at: 2024-06-11 08:30:00, source: 目标站点, content: 这里是完整的正文内容... }我习惯把所有字段统一成字符串或标准时间格式再入库而不是把原始 HTML 直接丢进去。原因有两个一是后续做检索、排序、导出结构化字段比一堆标签友好得多二是数据清洗放在抓取阶段完成下游使用方拿到手就是干净的不用每家都重复处理一遍。2. 整体架构与选型写爬虫前先回答五个问题在动手写代码之前我习惯先问自己五个问题目标站点是静态页面还是动态渲染列表页能不能直接拿到详情页 URL抓取频率控制在多少数据存在哪里某一步失败了怎么处理这五个问题想清楚了架构基本就定了代码只是把答案翻译成实现。2.1 数据流列表页入口、详情页拿正文绝大多数资讯站都是列表页 详情页两级结构列表页展示标题、摘要、发布时间和详情页链接正文内容在详情页里。对应到数据流就是请求列表页 → 解析出每条消息的 URL → 逐个请求详情页 → 提取字段 → 入库。def crawl(): list_urls get_list_page_urls() # 生成器避免一次加载全部 for detail_url in list_urls: item fetch_detail(detail_url) save(item)这里有个新手容易犯的错误试图在列表页直接把所有字段拿全。实际上列表页为了展示速度通常只有标题和摘要正文要么截断要么不渲染强行解析会导致字段缺失。而且列表页和详情页的 HTML 结构完全不同混在一起解析代码会变得非常别扭。分开处理各自的解析函数只管自己的页面哪个环节出问题也更容易定位。2.2 技术栈选择requests lxml 就够用了选型这件事我的原则是复杂度要匹配任务本身不要为了用框架而用框架。这个项目里我选的是 requests lxml理由有三点第一目标页面是服务端渲染的静态 HTML不需要等 JavaScript 执行requests 直接请求就能拿到完整内容第二lxml 的 XPath 表达式写起来直观、解析速度快比正则表达式维护成本低得多页面结构有调整时改一行 XPath 就能适配第三项目只有单机单任务不需要分布式引入 Scrapy 反而会带来学习成本和调度上的复杂度。如果哪一天需求变成要采集几十个站点、每天几百万条再考虑上 Scrapy Redis 也不迟。到那个时候选型的依据是任务规模而不是这个框架比较流行。2.3 抓取节奏设计限速与礼貌抓取抓取节奏是新手最容易忽略、老手最容易踩坑的地方。爬虫本质上是高频访问别人的服务器如果不控制节奏轻则 IP 被临时限制重则给对方服务器造成压力。我的经验是请求间隔最少 1 到 3 秒并且一定要加随机延迟不要写死time.sleep(2)。import time import random # 每次请求前随机等 1~3 秒 time.sleep(random.uniform(1, 3))为什么要随机而不是固定固定间隔本身就是一种机器行为特征很容易被识别随机间隔则更接近人类浏览页面的节奏。另外每一轮任务要设置总量上限防止列表页翻页逻辑写错、进入死循环时无休止地请求下去把对方服务器打挂的同时也把自己的 IP 搭进去。提示限速不是胆小而是长期稳定抓取的前提。很多站点不是不允许爬虫访问而是不允许无节制的爬虫访问。3. 核心实现从 HTML 到结构化消息的完整链路3.1 请求伪装与容错UA、超时、重试requests 默认的 User-Agent 是python-requests/x.x.x一眼就能看出来是脚本在访问很多站点会直接拒绝。我通常会把它伪装成常见浏览器的 UA同时设置超时时间。超时这件事要特别强调不设超时的话一个请求卡住整个任务就停在那里后面的全部排队等待日志看了半天也找不到原因。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor2, status_forcelist[500, 502, 503]) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp session.get(https://target-site.com/list, headersheaders, timeout10) resp.raise_for_status()这里的重试策略用的是指数退避第一次失败后等 2 秒第二次等 4 秒第三次还失败就放弃并记日志。注意不要把状态码 404 纳入重试范围404 说明页面不存在重试多少次都一样纯粹浪费资源。3.2 页面解析XPath 提取消息字段拿到 HTML 之后用 lxml 解析再通过 XPath 定位字段。这是整个项目里最需要细心的地方。from lxml import etree html etree.HTML(resp.text) title html.xpath(//h1/text())[0].strip() content_nodes html.xpath(//div[classarticle-content]//text()) content .join([node.strip() for node in content_nodes]).strip()这里有三个细节很多人处理不好。第一//h1/text()返回的是列表直接取下标前一定要先判断非空不然会 IndexError第二正文不要只取某一个节点比如//div[classarticle-content]/text()这样只能拿到第一层子节点的文本段落之间有缩进或者嵌套时会丢内容正确做法是取全部后代文本节点再拼接第三XPath 里尽量不要写死带空格的 class 名如果页面结构调整空格的细微变化会导致解析失败宁可多写一个相对路径匹配。3.3 时间与正文清洗输出干净的 JSON时间字段是重灾区。资讯站的时间格式五花八门2024-06-11 08:30、2024年6月11日 08:30、刚刚、昨天 10:00、3小时前。我一般在解析层统一转成标准格式from datetime import datetime, timedelta def parse_time(text): text text.strip() if 刚刚 in text: return datetime.now() if 小时前 in text: hours int(text.replace(小时前, )) return datetime.now() - timedelta(hourshours) if 昨天 in text: time_part text.replace(昨天, ).strip() yesterday datetime.now() - timedelta(days1) return datetime.strptime(f{yesterday.date()} {time_part}, %Y-%m-%d %H:%M) return datetime.strptime(text, %Y-%m-%d %H:%M:%S)正文清洗同样重要。页面里经常混入相关阅读点击查看广告这类噪声以及 script/style 标签里的脚本代码。script 和 style 可以在 XPath 阶段直接排除比如先取出所有//div[classarticle-content]/p再对每个段落做过滤广告类的文本我的做法是维护一个黑名单关键词列表命中就跳过。这个列表随着运行时间会越来越丰富是项目里隐形的资产。4. 增量与去重让爬虫只会拿新消息这是爬虫从能跑到能用的分水岭。没有去重的爬虫跑第二遍就会把同样的数据再次入库数据表越来越脏下游统计全部失真。4.1 为什么必须做增量而不是全量重抓全量重抓的问题显而易见第一数据重复必须配合去重逻辑才能避免脏数据那为什么不一开始就做增量第二服务器压力大目标站点同样的内容被反复请求没有任何意义第三耗时线性增长采集量大了以后全量重抓会占满整个时间窗口导致新数据来不及抓。增量采集的核心思路就是记录已经抓过哪些消息每一轮只处理新增部分。4.2 三种去重方案与我的选择方案原理优点缺点适用场景URL 去重把已抓 URL 存集合/表简单高效开销极小同一 URL 内容更新会漏抓大多数标准资讯站标题哈希去重对标题做哈希比对指纹能识别重复转载的内容标题微调会产生误判多源聚合采集URL 发布时间联合判断按时间窗口过滤逻辑清晰命中率高时间字段解析失败时失效有规律的发布时间我的建议是先用 URL 去重作为基准再把发布时间作为辅助维度。具体做法是SQLite 里建一张crawled_items表存url和published_at每轮抓列表页时先查库判断 URL 是否已存在存在就直接跳过详情页请求。这样既节省网络请求又避免重复入库。4.3 断点续抓与失败补偿假设任务抓了 500 条之后网络断了重启之后怎么办如果设计成从第一页重新开始那前面 500 条又要重新请求一遍虽然 URL 去重会跳过入库但网络请求浪费了。我习惯每成功抓取一条就立刻写入数据库同时更新一个进度游标。任务启动时读进度游标从上次停下的位置继续。这里有个取舍有人喜欢全部抓完再批量入库性能确实好一些但风险是中途一旦失败整批数据全丢而且无法断点续跑。单条入库虽然写入次数多但对 SQLite 来说完全不是瓶颈可控性却高了一个档次。在爬虫这种重网络、轻写入的场景里选可控性。5. 真实运行中的四个坑与排查过程这一部分是我跑这类采集任务时真实遇到的问题。每个坑都按现象 → 猜测 → 验证 → 根因 → 解决的链路来讲排查思路比答案本身更有复用价值。5.1 抓了几百条后突然全部超时被限流的信号现象前 200 条抓得很正常到第 201 条开始几乎每一条都请求超时重试三次也救不回来。排查过程我先怀疑是对方服务器出了问题于是手动打开浏览器访问同一个页面结果秒开。这说明问题出在爬虫这边。接着我单独拿一条 URL 用 Python 请求发现响应特别慢偶尔能通、大多数超时。最后我检查了自己的请求频率发现前面 200 条几乎没有间隔相当于短时间内高频打了几百个请求。根因请求频率过高触发了站点对单一 IP 的限流策略。解决把请求间隔从无间隔改成随机 1 到 3 秒同时程序里加了一个开关如果连续 10 次请求超时立即停止本轮任务等待 10 分钟后再继续。不要在被限流之后继续暴力重试那样只会加重限制延长封禁时间。5.2 页面改版后解析全部落空兜底与告警现象某天开始新增消息的标题和正文全部为空但日志里没有任何异常看起来一切正常。排查过程我先看单条页面 HTML发现原来匹配的//h1路径还在标题能取到再看正文的//div[classarticle-content]发现这个 class 已经不存在了被改成了article-detail。日志里之所以没有异常是因为我用html.xpath(...)[0]XPath 返回空列表后取下标抛了 IndexError被我外层一个宽泛的 try/except 吞掉了。根因站点改版导致正文 XPath 失效而宽泛的异常捕获掩盖了问题。解决解析逻辑单独收敛成一个函数函数内部对每个字段做严格检查取不到就抛出自定义异常并带上有问题的 URL 和字段名。同时接了一个简单的告警解析失败率达到一定阈值时发一条消息到群里。这样改版后几分钟内就能被发现而不是让空数据积累好几天。注意爬虫项目里看起来正常是最危险的信号。日志里全是异常不可怕可怕的是一条异常都没有、数据却全是空的。5.3 中文乱码编码声明的优先级现象抓下来的正文在终端里看正常写入文件后变成乱码或者直接在解析阶段就是乱码。排查过程我先用resp.encoding看 requests 自动猜测的编码发现是 ISO-8859-1这基本是 requests 拿不到显式编码声明时的兜底值。再看响应头里的 Content-Type发现没有 charset 参数。继续检查 HTML 的 meta 标签发现页面声明了gb2312但实际内容用的是 GBK。根因requests 的自动编码检测没生效页面编码和解析编码不一致。解决手工指定编码优先级从高到低是响应头 charset → HTML meta charset →resp.apparent_encoding。代码里直接判断如果resp.apparent_encoding是 gbk 或 gb2312就统一设置成gbk再读取resp.text。if resp.apparent_encoding.lower() in (gbk, gb2312): resp.encoding gbk text resp.text5.4 单条数据解析异常导致任务中断异常边界现象任务跑了一天日志显示中间某个 URL 上抛了AttributeError后面的全部没抓。排查过程点开日志里那个 URL发现是条特殊内容正文里没有段落只有一张图片//div[classarticle-content]//text()返回空列表后续代码对空列表做.strip()时报错。外层循环没有捕获单条异常整个任务直接退出。根因脏数据触发解析异常而异常没有在单条边界内被隔离。解决每条消息的解析都包在独立的 try/except 里失败时记录 URL 和异常信息然后continue继续下一个。循环体的异常边界一定要收敛在单条任务内不要包住整个循环。for detail_url in list_urls: try: item fetch_detail(detail_url) save(item) except Exception as e: logging.error(ffailed to parse {detail_url}: {e}) continue这也解释了为什么 4.3 里要单条入库——单条异常隔离和单条入库是一对设计前者保证任务不中断后者保证已处理的数据不丢失。6. 合规边界与后续还能怎么玩6.1 爬虫项目必须守住的合规底线爬虫技术本身是中性的但用在哪里、怎么用是必须认真对待的问题。我自己在跑采集项目时会给自己定几条硬规矩只采集公开可访问的信息不碰需要登录之后才能看到的非公开内容遵守目标站点的服务条款和 robots 协议控制请求频率不以影响对方正常服务为代价换取采集速度不把采集到的内容用于商业牟利或者侵权场景。这几条不是套话是真实踩过坑之后的教训——一个本来很正常的数据采集需求因为这些边界没守住轻则收到对方的警告重则惹上不必要的麻烦。6.2 从脚本爬虫到采集服务可扩展的方向如果这个项目要继续演进有几个明确方向可以走。一是定时调度把main.py挂到 cron 或者用 APScheduler实现每天定时自动采集二是消息推送抓完新数据之后通过 Webhook 把摘要推到工作群里运营同学打开就能看三是数据展示给 SQLite 里的数据套一层简单的 Web 页面提供检索和导出功能四是分布式等到目标站点数量成倍增加、单机采集速度确实跟不上的时候再改成 Scrapy Redis 的方案。不过我的个人建议是在确认现有方案真的撑不住之前不要过早引入分布式。把单机爬虫的稳定性、日志、告警和去重逻辑做好收益远大于拆成一堆微服务。我自己见过太多项目数据量明明几千条架构却堆了消息队列加分布式调度最后大部分时间都在修基础设施而不是在解决采集本身的问题。把一件小事做扎实比铺一个大摊子有用得多。本文还有配套的精品资源点击获取