爬虫工具链选型与合规采集:从requests到Playwright的实战指南 做爬虫开发这几年我越来越觉得真正的分水岭不在于会不会写正则、能不能解析JSON而在于能不能先把“数据边界”和“工具边界”想清楚。很多人上来就问我要一个最强采集工具我反而会先问一句你要抓的东西属于什么类型数据允不允许抓是一次性项目还是长期任务这三个问题不回答清楚工具选得再好也会在后半程翻车。这篇文章想把两件事讲透一是7款我实际在生产环境里用过的采集相关工具它们各自解决哪一层问题、适合什么场景、有什么坑二是用“股票基金讨论社区”这类站点的公开话题列表作为案例从页面分析到数据落库完整走一遍最小可运行的抓取流程。适合刚入门不久、想系统化认识爬虫工具链的开发者也适合那些已经会写简单脚本但总觉得自己缺一块拼图的人。先说明一下数据口径标题里的雪球数据抓取我把它理解为“投资者社区的公开话题采集”这类需求。出于合规考虑本文不会对任何具体站点做定向、绕过或破解性演示案例里用的域名是占位符重点讲通用思路。你可以把同样的方法迁移到任何有权访问、允许采集的公开数据源上。1. 采集前先把边界想清楚这三件事比选工具更重要1.1 数据是否允许被拿我看到很多入门教程把第一步写成安装工具这其实是把顺序搞反了。真正专业的第一步是判断目标数据允不允许采集这个判断决定了整个项目的性质。怎么看先看目标站点的robots.txt。它通常放在域名根路径下例如https://example.com/robots.txt里面会声明哪些路径允许爬虫访问、哪些不允许。这是一个约定俗成的行业规范虽然不等同于法律但它能帮你快速判断站长的态度。如果某个路径明确写着Disallow: /search那你最好别去碰这个路径下的数据。然后看服务条款。很多站点在注册协议里写明了“禁止未经授权批量抓取数据”这种情况下哪怕技术上行得通法律风险也是实打实的。我的原则很简单只采集公开可见、无需登录、不涉个人隐私的数据看到登录墙、验证码、频率限制第一反应应该是停手而不是研究怎么绕过去。这不是胆小而是长期做采集的基本职业素养。1.2 你要拿的是列表、详情还是接口数据边界想清楚之后第二步是把目标数据的形态拆开。以股票讨论社区为例一次完整的数据采集往往包含三种信息话题列表页的标题和摘要、进入详情页后的正文和评论、以及页面背后可能存在的JSON接口数据。这三种形态对应的技术方案完全不同。列表页通常是静态HTML用requests加解析库就能搞定详情页数量大、字段多适合用Scrapy做调度如果站点本身就是前后端分离架构很多数据其实是接口直接返回JSON这时候连HTML解析都省了直接请求接口反而更高效。我的习惯是先用浏览器开发者工具看“网络”面板如果能在XHR请求里直接看到结构化数据就优先走接口不要死磕HTML解析。记得有一次某开发者让我帮忙看为什么抓不到话题发布时间我打开开发者工具一看那个时间字段根本不是静态页面里的文本而是接口返回的毫秒时间戳前端再用JavaScript格式化展示。搞清楚这一点之后代码量少了一半稳定性还更高了。1.3 今天跑一次还是长期跑最后一个边界问题是项目性质。如果只是做一次性的行业调研抓500条数据导出成CSV就行那没必要上框架一个小脚本足够。但如果是每天都要更新数据、持续跑几个月的项目就必须考虑增量采集、断点续爬、日志监控和请求频率控制。长期采集和一次性脚本是完全不同的两种工程。一次性脚本可以容忍跑挂了重新跑长期任务则必须设计成“挂了能自己恢复、重复数据能自动跳过、出错了能第一时间发现”。我见过太多人用一次性脚本的思路去做每日巡检结果第二天定时任务失败却完全没察觉最后导出的数据缺了一大块。所以在选工具之前先问清楚这个项目要活多久这直接决定了你要不要上框架。2. 七款采集工具的定位差异它们解决的是四层问题很多人把requests、Scrapy、Selenium放在一起比较好像它们是同类竞品。这是最大的误解。采集工具链其实分成四层请求层、解析层、框架层、浏览器自动化层。每一层解决不同的问题所谓选型本质上是搞清楚你现在缺的是哪一层。工具所属层次核心定位适合场景学习成本常见坑点requests请求层发送HTTP请求接口调用、小型脚本低不内置重试、编码需手动处理httpx请求层同步/异步HTTP客户端需要异步、HTTP/2中API与requests不完全一致BeautifulSoup解析层HTML/XML文档解析宽容度高的DOM解析低大规模场景性能一般lxml解析层高性能XML/HTML解析性能敏感的解析任务中XPath语法需要额外学习Scrapy框架层完整采集流程调度中大规模、长期项目高中间件体系复杂Selenium浏览器自动化层模拟真实浏览器老系统、需要点击交互高资源占用大、运行慢Playwright浏览器自动化层现代浏览器自动化复杂交互、自动等待中依赖浏览器内核较重2.1 请求层requests 和 httpx 怎么选requests是我用得最多的请求库它在Python社区里几乎成了标准解法。API设计得简洁直接get()、post()、Session()都很好懂普通爬虫项目用它就够了。但requests有个短板它不支持异步。如果你的项目要同时请求几百个页面纯requests逐条等响应会非常慢。httpx是requests的现代替代品同时支持同步和异步两种模式还支持HTTP/2。我在做多页面并发抓取时会优先考虑它用asyncio配合AsyncClient效率能提高不少。但需要注意httpx的API虽然刻意向requests靠拢并非100%兼容比如有些参数名字不同迁移时得逐项排查。选择建议很简单项目规模不大、同步请求就够用那就选requests生态最成熟遇到问题也好查资料项目需要高并发或已经决定用异步架构直接上httpx别等到中途再迁。2.2 解析层BeautifulSoup 与 lxml 的取舍拿到HTML之后要用解析库把它变成可以提取的数据结构。BeautifulSoup的优点是容错性极高页面标签没闭合、属性写错它都能尽量帮你兜住写起来也很直白用select()写CSS选择器非常顺手。对初学者来说它是零成本上手的首选。lxml走的是另一条路它以libxml2为底层引擎解析速度比BeautifulSoup快很多而且支持XPath。XPath在做复杂定位时表达力很强比如“选取所有 class 包含 topic 并且>import csv from urllib.parse import urljoin import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)>import pandas as pd df pd.read_csv(topics.csv) df[link] df[link].str.split(?).str[0].str.split(#).str[0] df df.drop_duplicates(subset[link], keepfirst)这里用keepfirst保留第一次出现的记录意味着重复内容里先被抓到的那份有效。如果后续要做增量采集还需要建立一个历史指纹库而不是每次都全量去重。4.2 字段清洗的常见陷阱清洗阶段常见的问题集中在四个方面空值、格式混杂、编码脏数据、超链接字符。作者字段可能因为页面渲染问题出现“”多余符号发布时间可能是“刚刚”“3小时前”这种相对时间也可能是“2025-06-01 14:30”这种绝对时间两者混在一起时直接转成datetime会报错。一个稳妥的做法是先用正则把相对时间统一映射成绝对时间或者干脆丢弃相对时间字段只保留有绝对时间的数据。具体取舍看业务需求如果是实时行情相对时间有用如果是存量数据分析绝对时间才是正确格式。4.3 增量采集的两种姿势增量采集有两种典型做法。第一种是“同步全部再合并去重”每天全量抓一遍最后在数据库里按唯一键去重适合数据量小的场景。第二种是“只抓新增”通过记录最后一页的时间戳或ID下次抓取时只请求比这个位置更新的内容适合高频率更新的场景。第二种做法的边界条件很麻烦因为列表页只展示最近N条数据如果两次采集间隔超过了列表页的覆盖范围中间的数据就永久丢了。所以我个人的建议是不要盲目追求增量先算清楚你的采集频率和目标页面保留条数之间的关系再决定要不要做增量。5. 长期跑稳定的关键礼貌频率、容错与观察5.1 频率控制为什么比速度重要很多人觉得爬虫快就是好可真实的生产项目里慢反而是美德。服务器端通常有访问频率限制快速连续请求不仅容易被封还会给目标站点造成不必要的压力。最关键的是一旦IP被封你整个项目的数据采集都要停摆损失远比慢几分钟大。控制频率最朴素的做法是每次请求后随机休眠import random import time for page in range(1, 6): fetch_page(page) time.sleep(random.uniform(1.5, 3.5))随机休眠比固定休眠更接近人类行为也能避免定时脉冲式的请求模式。这个区间数值没有标准答案要根据目标站点的承受能力和页面大小调整。基本原则是宁慢勿快以不影响对方正常服务为前提。5.2 超时、重试、退避策略网络请求一定会失败只是时间问题。代码里如果不设置超时requests会一直挂着任务卡死也没提示。所以我在所有请求里都加了timeout10这只是基础防护更重要的是重试策略。重试不能无脑重试要配合退避。第一次失败等1秒再试第二次等2秒第三次等4秒指数退避是为了避免在服务器繁忙时再添堵。用现成的Retry类可以省不少事from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter retry_strategy Retry( total3, status_forcelist[429, 500, 502, 503, 504], backoff_factor0.5, ) adapter HTTPAdapter(max_retriesretry_strategy, pool_connections10, pool_maxsize10) session requests.Session() session.mount(https://, adapter) session.mount(http://, adapter)status_forcelist里的429是“请求过多”500和5xx是服务端错误这些状态值得重试。404、403这类状态码就不是重试能解决的重试只会浪费时间。5.3 日志和进度记录长期采集的项目必须能回答一个问题现在跑到哪了上一次跑到哪了用什么记录答案是日志。不要只在出错时print()要写结构化日志。每次请求成功记一条ID和时间失败记全异常堆栈每隔一定量记一次进度汇总。这样凌晨三点日志告警时你能快速定位是哪个URL出了问题而不是两眼一抹黑。如果做每日定时任务建议把上次采集的位置存到一个单独的进度文件或数据库表里。脚本重启后先读进度再从断点继续避免每次从头跑。5.4 遇到反爬的正确处理方式这是我最想强调的一点。看到验证码、滑块、风控提示第一反应应该是考虑停手而不是去找破解方案。网络上打着“绕过”旗号的教程我从来不碰因为风险完全大于收益。反爬机制的背后是数据源方的真实技术意图它不希望你批量采集。这种情况下正确的合规替代方案是查查目标站点有没有开放官方API或者直接联系对方谈数据合作。如果两者都不可行说明这个数据源本来就不适合你碰。这句话听起来很扫兴但能帮你避开无数麻烦。6. 我踩过的几个隐蔽Bug以及排查思路6.1 乱码问题不是只有gbk才踩很多教程说中文乱码就换成gbk实际上现在的站点大多用UTF-8反而更常见的坑是“响应的头部声明的编码和实际内容编码不一致”。requests的默认行为是看响应头里的charset但不少页面写的是charsetwindows-1252内容却是UTF-8。我的排查经验是遇到乱码先打印resp.apparent_encoding看看实际推测结果和当前resp.encoding是否一致不一致就手动指定。上面案例里我把编码处理写进了fetch_html就是为了让这个问题在一开始就规避掉。6.2 CSS选择器在页面改版后的批量失效采集脚本最怕的不是新需求而是目标页面悄悄改版。class名变了、DOM层级变了、甚至原来用标签选择器的地方换成了所有节点都相同的tag都会导致选择器全军覆没。而且这种问题往往是数据写进CSV之后很久才被发现因为脚本本身不会报错只是抓到的内容全是空。排查思路是写一个最小校验函数在每次采集后统计非空字段的数量如果比历史平均值低20%立即触警。不要等人工去看CSV要在源头设置监控。6.3 缺了请求头服务器给你503用requests直接访问某些站点可能返回503但用浏览器打开却正常。原因通常是服务器启用了基础的bot防护缺了User-Agent或者Accept-Language这些常规请求头就被拦下来。这不是什么玄学只是一个很基础的指纹识别。我踩过的坑是复制浏览器的完整请求头反而更容易被拦因为一套完整的浏览器指纹里带着很多动态值脚本里如果固定了这些值一旦浏览器版本变化就会对不上。更稳妥的办法是只设置必要的几个头部字段不要贪多。上面的案例里我只设置了User-Agent和Accept-Language就是为了保持最小且稳定的请求指纹。6.4 动态渲染页面从requests到Playwright的升级路线如果页面标题和列表内容全靠JavaScript加载requests拿到的HTML里根本找不到topic-item这个节点。这时候再怎么调解析库都没用因为源头就不存在。正确判断思路是先用requests拿到的HTML做一次搜索看看关键class名是否存在。如果存在说明是静态内容继续用requests如果不存在再看页面是否调用了XHR接口有接口就改请求JSON实在不行才上Playwright。这个判断顺序能帮你少引入很多不必要的复杂度。用Playwright抓同一个列表页的骨架代码也不复杂from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://example.com/community/topics) page.wait_for_selector(div.topic-item) html page.content() browser.close()这里wait_for_selector是Playwright最实用的方法之一它会自动等待节点渲染完成比死板地time.sleep(3)可靠得多。如果你发现页面里有些内容滚动才加载还可以再加一句page.mouse.wheel或者直接调用页面滚动逻辑。说实话做采集这些年我最大的体会是技术方案从来不是最难的难的是判断什么该做、什么不该做、做到什么程度就该停。工具永远有更高级的替代品但数据边界和合规意识才是项目能不能长期走下去的分水岭。每次拿到一个新需求我都会先把这五个字过一遍值不值得抓、允不允许抓、抓了怎么存、挂了怎么续、被拒怎么办。想清楚这五个问题再打开编辑器写第一行代码也不迟。