1. 项目概述:这不是“爬虫教程”,而是Web Scraping的底层思维框架
你点开这篇内容,大概率不是为了找一段能跑通的Python代码,也不是想抄个现成的XPath表达式——你真正卡住的地方,往往是:该不该爬?能不能爬?从哪下手?爬下来的数据怎么用才不白费功夫?为什么昨天还正常的脚本今天就全挂了?这些问题,和你写的第1行
import requests
无关,而和你对Web Scraping这件事本身的认知深度直接相关。
The 5 W’s and H of Web Scraping
,说白了,就是用新闻编辑室最基础的六个提问法(Who、What、When、Where、Why、How),给整个网络数据采集行为做一次系统性“体检”。它不教你怎么写
BeautifulSoup.find_all()
,但它决定了你写这行代码之前,是否已经想清楚了目标网站的反爬策略类型、数据更新频率、结构稳定性、法律边界、以及你自己的业务目标到底需要什么颗粒度的数据。我做过37个不同行业的爬虫项目,从电商比价、招聘趋势分析、到学术文献追踪、本地生活服务监测,踩过最多坑的,从来不是技术实现,而是前期没把这六个问题问透。比如,一个做竞品价格监控的客户,最初只要求“每天抓一次首页价格”,结果上线两周后发现,对手把价格藏在AJAX接口里,且接口带时间戳签名;另一个做舆情分析的团队,花两周写了完美解析微博正文的脚本,却忽略了一个关键事实:微博PC端早已下线,移动端API返回结构完全不同——这就是典型的“What”没定义清,“Where”没确认准。所以,这篇文章不是给你一个工具箱,而是帮你校准罗盘。接下来每一节,我都用真实项目中的决策现场还原,告诉你这六个问题怎么问、问谁、问到什么程度才算过关。
2. 核心需求解析与场景化拆解
2.1 Why:动机决定技术方案的生死线
Why是所有决策的起点,但也是最容易被跳过的环节。很多人一上来就想“怎么抓”,却没想“为什么抓”。可恰恰是这个“为什么”,直接锁定了你的技术选型、资源投入和风险等级。我把它分成三个递进层级:
第一层是业务动因。比如,某跨境电商公司要做“海外小众品牌入驻可行性分析”,核心Why是“降低选品试错成本”。这意味着他们不需要实时数据,但需要历史价格波动曲线、用户评论情感倾向、以及竞品上架时间线。这种需求天然排斥高并发、低延迟的方案,反而适合用分布式调度+离线解析+数据库归档的组合。相反,一个做期货套利的量化团队,Why是“捕捉商品页面秒级价格异动”,这就要求毫秒级响应、WebSocket长连接、甚至浏览器自动化渲染——因为价格可能藏在JS动态计算中,静态请求根本拿不到。
第二层是合规动因。这是国内从业者最容易踩雷的点。很多团队以为“只要不商用就没事”,但现实是:《反不正当竞争法》第十二条明确将“妨碍、破坏其他经营者合法提供的网络产品或者服务正常运行”的行为列为不正当竞争;《数据安全法》第四十五条也规定,非法获取、使用他人数据可能面临高额罚款。去年我们帮一家教育机构做课程信息聚合,目标网站robots.txt明确禁止
/course/
路径,且页面底部有“数据受版权保护”声明。我们当时立刻叫停,转而建议他们通过官方API申请合作——虽然周期长,但避免了后续法律风险。这个决策依据,就来自对Why的再追问:“我们采集这些课程标题和简介,是为了生成招生简章,还是为了训练推荐模型?”前者属于合理使用范畴,后者则涉及数据二次加工,风险陡增。
第三层是可持续动因。很多脚本跑一周就失效,根本原因不是技术差,而是没想清楚“这个数据源能稳定支撑我多久”。我见过最典型的案例,是一家做本地餐饮点评的创业公司,初期用Selenium模拟点击翻页,效果很好。但三个月后,平台上线了基于Canvas指纹的设备识别,Selenium默认配置直接被标记为机器人。如果他们在Why阶段就问一句:“这个数据源的前端技术栈升级频率如何?是否有公开的技术博客或招聘信息透露其反爬团队规模?”答案可能是“每月至少一次JS混淆更新”,那他们一开始就会选择更轻量、更易维护的方案,比如优先尝试逆向分析XHR接口,而非重依赖浏览器渲染。
提示:每次启动新项目前,强制自己写下三句话:
- 这个数据最终要驱动哪个具体业务动作?(例:自动生成日报/触发库存预警/训练NLP模型)
- 如果这个数据源明天关闭,我的业务会停摆吗?有没有备选方案?(例:是否有官方API/第三方数据市场/人工抽样替代)
- 我的采集行为,是否会让目标网站的普通用户感受到性能下降?(例:单IP每秒请求数是否超过其CDN限流阈值)
2.2 What:数据本质决定解析路径的复杂度
What回答的是“你真正要的是什么”,而不是“网页上显示的是什么”。这是区分新手和老手的关键分水岭。新手看到一个商品页,本能地想“把整个页面HTML存下来”;老手会先问:“我要的到底是‘当前售价’这个数字,还是‘历史最低价’这个字段,抑或是‘价格变动趋势’这个时间序列?”
我们以招聘网站职位信息采集为例。表面看,What是“职位名称、公司、薪资、要求”,但深入拆解会发现:
-
职位名称 :看似简单,但实际包含结构化陷阱。比如“Java高级开发工程师(大数据方向)”,括号内是关键技能标签,但有些网站会把“大数据方向”放在单独的
<span class="tag">里,有些则混在主标题文本中。如果你的下游系统需要按技能标签分类,就必须在What阶段就定义清楚:是否需要提取括号内容?是否需要标准化为“大数据”、“Hadoop”、“Spark”等原子标签? -
薪资 :这是最典型的“表里不一”字段。网页显示“20K-30K”,但后台API可能返回
{"min_salary": 20000, "max_salary": 30000, "unit": "monthly", "currency": "CNY"}。前者需要正则提取+单位推断,后者直接JSON解析。更麻烦的是,有些网站用图片展示薪资(防爬手段),或者用Unicode零宽空格混淆数字(如20K-30K),这时候What就变成了“能否接受OCR识别的误差率?误差率超过5%是否影响业务判断?” -
要求 :文本类字段的坑在于非结构化。一条“3年以上Java开发经验,熟悉Spring Boot、MyBatis,有高并发系统设计经验”中,隐含了年限、技术栈、架构能力三个维度。如果你的What定义是“提取所有技术关键词”,那就要处理同义词(如“SpringBoot”和“Spring Boot”)、缩写(如“MQ”和“Message Queue”)、以及上下文依赖(如“熟悉Redis”是技能,“用Redis做缓存”是应用场景)。我们曾为一家猎头公司做简历匹配,他们最初的What是“提取JD里的技术词”,结果爬下来一堆“Java”、“Python”,但漏掉了“JVM调优”、“GC算法”这类高阶能力词——因为原始HTML里这些词被包裹在
<p>段落中,而非加粗的<strong>标签里。后来我们把What重新定义为“提取所有与技术能力相关的名词短语,并保留其出现频次和上下文位置”,才真正满足业务需求。
注意:What必须用“可验证的输出格式”来定义。例如,不要写“获取公司信息”,而要写“输出JSON对象,包含company_name(字符串,长度≤50)、company_size(枚举:small/mid/large)、industry(三级分类,参考GB/T 4754-2017)”。这样,后续的解析逻辑才能被测试用例覆盖,避免“我以为爬到了,其实字段为空”。
2.3 Who:目标网站的技术画像决定你的对抗策略
Who不是指“网站是谁运营的”,而是指“这个网站的技术特征是什么”。这直接决定了你该用requests还是Playwright,该逆向API还是模拟登录。我总结了六类典型网站画像,每类都对应一套预设的应对策略:
| 网站类型 | 技术特征 | 典型反爬手段 | 推荐采集策略 | 实操心得 |
|---|---|---|---|---|
| 静态资讯站 (如政府公报、学校官网) | HTML结构稳定,无JS渲染,CSS类名规范 | robots.txt限制、IP频率限制 | requests + BeautifulSoup,配合Session复用和User-Agent轮换 | 这类网站最友好,但要注意其CDN可能有地域限制,需在目标区域服务器部署节点 |
| 传统电商 (如早期淘宝、京东PC端) | 商品列表页静态,详情页部分JS加载,URL参数规律 | Referer校验、Cookie会话绑定、验证码(低频) | requests + 正则解析,关键字段用XPath兜底;登录态用Selenium预热后导出Cookie复用 | 切忌全程用Selenium,90%的列表页数据完全可用静态请求获取,否则资源浪费严重 |
| 单页应用 (如Vue/React构建的管理后台、新型招聘平台) | 页面骨架由JS动态注入,URL为hash路由,数据走GraphQL或RESTful API | Token时效性(<15分钟)、请求头签名、设备指纹 | 浏览器自动化(Playwright首选)+ Network面板抓包,优先复用前端调用的真实API | 不要试图自己实现签名算法,耗时且易失效;直接Hook前端JS获取token生成逻辑更可靠 |
| 内容社区 (如知乎、小红书) | 用户生成内容为主,无限滚动,评论懒加载,大量图片/视频 | 图片防盗链、评论接口需登录态+CSRF token、用户主页有访问频次限制 | 分布式任务队列(Celery)+ Playwright集群,每个Worker绑定独立账号和代理IP | 社区类数据价值在于关系链(关注/点赞/收藏),单纯抓帖子文本意义不大,需同步采集互动行为元数据 |
| 金融数据平台 (如雪球、东方财富) | 数据敏感度高,实时性要求强,图表多用ECharts渲染 | WebSocket加密推送、Canvas指纹、行为轨迹检测(鼠标移动、键盘敲击) | 定制化浏览器环境(禁用WebDriver标志、模拟真实人机行为)、结合Frida Hook解密WebSocket消息 | 这类平台反爬投入最大,建议优先评估官方数据接口价格,自研成本往往高于采购费用 |
| 本地生活服务 (如大众点评、美团) | 商户信息分散,地理位置权重高,用户评价含大量UGC图片 | 地理位置参数校验(GPS坐标、IP属地)、图片Base64编码、评论折叠需点击展开 | 地理围栏代理池(按城市分配IP)+ Playwright模拟点击展开操作 + OCR识别图片内文字 | 最大坑是“商户ID”不等于“页面URL中的ID”,很多商户页URL含哈希值,需从列表页的XHR响应中提取真实ID |
你可能会问:“怎么快速判断一个网站属于哪一类?”我的方法是:打开Chrome开发者工具,切到Network标签页,然后刷新页面,观察第一个HTML请求之后,哪些资源是JS/CSS,哪些是XHR/Fetch。如果XHR请求返回的是大量JSON数据,且URL含
/api/
或
/graphql
,基本就是单页应用;如果只有几个JS文件,且没有XHR,那就是静态站。这个动作30秒就能完成,但能帮你省下三天的试错时间。
2.4 Where:数据落点决定整个架构的扩展性
Where回答的是“数据最终存在哪里、怎么被消费”,这常常被忽视,却直接影响你的架构选型。很多团队把数据爬下来就往MySQL里一塞,结果半年后发现:查询慢、备份大、无法支持全文检索、更别说做实时分析了。Where必须从三个维度定义:
首先是 存储介质 。不是所有数据都适合存在关系型数据库。比如,你爬取的是10万条微博评论,每条评论平均200字,还带用户头像URL和转发数。如果存MySQL,光是text字段就占几十GB,而你90%的查询只是“查某个关键词出现频次”,这时Elasticsearch的倒排索引效率高出百倍。我们曾帮一家舆情公司重构数据管道,他们原来用PostgreSQL存原始HTML,查询一条“华为mate60预售评论”要8秒;迁移到ES后,同样查询0.3秒返回,且支持中文分词、同义词扩展、情感极性标注。
其次是
数据形态
。是存原始HTML、解析后的结构化JSON、还是清洗后的业务对象?我的经验是:
必须同时保留原始HTML和结构化数据,但用不同的存储策略
。原始HTML存对象存储(如MinIO或阿里云OSS),按日期+网站+任务ID分目录,设置生命周期自动删除(如30天后转低频存储);结构化数据存数据库,但字段设计要预留扩展性。比如,一个“职位要求”字段,不要设计成
requirement TEXT
,而要设计成
requirement_json JSON
,这样未来可以轻松增加“编程语言”、“框架”、“数据库”等子字段,无需改表结构。
最后是 消费路径 。数据不是采完就结束,而是要被下游系统调用。Where必须明确“谁在什么时候用什么方式调用”。比如,一个实时价格监控系统,Where是“Kafka消息队列,每5分钟推送一次价格变更事件,消费者是Flink实时计算作业”;而一个学术研究项目,Where可能是“每日生成CSV快照,上传至Google Drive共享文件夹,供研究员手动下载”。前者要求高吞吐、低延迟、Exactly-Once语义;后者只要求文件完整性校验(如MD5)和版本管理。我们有个客户,最初把Where定为“存MongoDB供BI工具直连”,结果BI工具频繁全表扫描,拖垮数据库。后来改成“每日凌晨ETL到ClickHouse,BI只查汇总视图”,问题迎刃而解。
实操心得:在项目启动时,画一张简单的数据流向图,标出每个环节的SLA(服务等级协议)。例如:“原始HTML存储:99.9%可用性,单次读取延迟<100ms”、“结构化数据入库:写入延迟<5s,错误率<0.1%”。这张图会逼你提前思考容错机制,比如当Kafka积压时,是否要降级到本地磁盘暂存?
2.5 When:时间维度决定调度策略与数据鲜度
When不只是“什么时候开始爬”,更是“数据的时间属性如何定义”。很多项目失败,是因为把“采集时间”和“业务时间”混为一谈。比如,你每天上午9点爬一次招聘网站,拿到的数据时间戳是“2024-05-20 09:00:00”,但这不代表这些职位是今天发布的——可能其中80%是上周发布的,只是还没下线。真正的业务时间,是“职位发布时间”,它藏在HTML某个
<time datetime="2024-05-15">
标签里,或者API返回的
publish_time
字段中。
When要拆解为三个时间点:
-
采集触发时间(Trigger Time) :由业务需求驱动。比如,电商大促期间,价格变化频繁,Trigger Time可能是“每15分钟一次”;而企业年报数据,Trigger Time就是“每年4月30日之后,每天检查一次”。
-
数据业务时间(Business Time) :即数据本身代表的时间。这是最难准确获取的,尤其对于历史数据。我们做过一个房地产项目,目标是“近一年各城市二手房挂牌均价”。表面看,When是“每天采集”,但实际业务时间是“每条数据的挂牌日期”。而很多房产网站不显示具体挂牌日,只显示“3天前”、“一周内”。这时,我们必须用采集时间减去相对时间,估算业务时间。比如,今天是5月20日,页面显示“3天前”,那业务时间就是5月17日。但要注意,这种估算有误差,需在数据中标记
business_time_estimated: true,供下游系统判断可信度。 -
数据有效时间(Valid Time) :即数据多久后会过期。这决定了你的缓存策略和重采频率。比如,天气预报数据,Valid Time可能是2小时;而上市公司基本信息,Valid Time可能是1年。我们有个客户爬取工商注册信息,最初设为“每周采集”,结果发现企业股权变更平均3个月发生一次,于是调整为“每月采集+变更事件监听(通过天眼查API订阅)”,既保证数据新鲜度,又节省资源。
关键技巧:为每条数据打上三个时间戳:
trigger_time(采集任务启动时间)、business_time(数据本身的时间)、valid_until(数据有效期截止时间)。这三个字段必须作为元数据,和业务数据一起存储。我在一个金融项目中,就靠valid_until字段实现了自动化的“过期数据归档”,避免了人工清理的疏漏。
2.6 How:技术实现不是终点,而是风险控制的起点
How是大家最熟悉的环节,但恰恰是最容易陷入“技术炫技”的陷阱。很多人觉得“用Scrapy就比requests高级”、“用Playwright就比Selenium先进”,但真实项目中,How的选择永远服务于前面五个W的答案。我用一个真实案例说明:
项目背景:为一家连锁药店做竞品药品价格监控,目标网站是某大型医药电商平台。
- Why:降低采购成本,需精准比价 → 要求数据100%准确,不能有缺失。
- What:药品通用名、规格、厂家、当前售价、促销价、库存状态 → 都是结构化字段,但库存状态是动态的,需实时判断。
- Who:单页应用,数据走GraphQL API,有Token签名 → 必须走浏览器自动化。
- Where:数据存入内部ERP系统,要求事务一致性 → 需要支持回滚的存储方案。
- When:每小时采集一次,业务时间=采集时间(因价格随时变动)→ 调度需高可靠性。
基于这五个W,How的决策就非常清晰: 不用Scrapy(太重,不支持JS渲染),不用纯requests(搞不定GraphQL签名),也不用Selenium(维护成本高) 。最终方案是:Playwright + Python,但做了关键定制:
-
签名绕过 :不自己实现签名算法,而是用Playwright启动浏览器后,执行一段JS,从页面内存中直接读取GraphQL请求所需的
x-signatureheader值,然后用requests复用这个签名发批量请求。这样既利用了浏览器的JS执行能力,又避免了全程浏览器渲染的性能损耗。 -
库存状态双校验 :页面显示“有货”,但API返回
in_stock: false。我们设定规则:以API为准,但当两者冲突时,记录告警日志并触发人工复核流程。这比单纯“相信页面”或“相信API”都更稳妥。 -
事务保障 :每批次采集100个药品,用PostgreSQL的
INSERT ... ON CONFLICT DO UPDATE语法,确保价格更新的原子性。同时,采集任务本身用Celery的acks_late=True,确保Worker崩溃时任务不会丢失。
你看,这个How方案里,没有一个技术是“为了用而用”的。每一个选择,都是对前面五个W的回应。这才是专业和业余的根本区别。
3. 核心技术点拆解与实操细节
3.1 反爬策略识别:从HTTP响应头开始的侦探工作
很多人一遇到403就慌,以为被封了,其实HTTP响应头里藏着大量线索。我养成的习惯是:每次调试,第一件事就是看Response Headers,重点关注这四个字段:
-
X-Powered-By:暴露后端技术栈。如果是PHP/7.4.33,说明可能用Nginx+PHP-FPM,反爬逻辑大概率在PHP层,可针对性构造Cookie;如果是Express,那Node.js中间件可能做了Referer校验。 -
Server:显示Web服务器类型。nginx常见于静态资源,cloudflare意味着你面对的是CDN层防护,此时IP封禁、JS挑战、验证码都可能发生;Apache则更可能在.htaccess里做了规则限制。 -
Set-Cookie:这是最关键的线索。比如,你看到Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure,说明网站用了会话机制,后续请求必须带上这个Cookie。更隐蔽的是Set-Cookie: _cf_bm=xxx; Path=/; Domain=.example.com; Expires=...,这是Cloudflare的Bot Management Cookie,表明你已通过JS挑战,但Cookie有时效性,需及时更新。 -
X-RateLimit-*系列 :X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset,这是最友好的反爬提示。比如X-RateLimit-Remaining: 0,说明你已用完配额,X-RateLimit-Reset: 1716201600(Unix时间戳),换算成北京时间是2024-05-20 16:00:00,那就知道要等到那时再试。
实战案例:我们爬某旅游平台时,一直返回403。看Headers发现
Server: cloudflare
,但
Set-Cookie
里没有
_cf_bm
。这说明JS挑战没过。我们用Playwright打开页面,等待
document.readyState == 'complete'
后,再检查
document.cookie
,果然发现
_cf_bm
已设置。于是,在requests中,我们先用Playwright获取一次Cookie,再用这个Cookie发后续请求,成功率从0%提升到99%。
注意:不要迷信
robots.txt。它只是君子协定,很多网站根本不遵守。我见过最离谱的,是robots.txt里写着Disallow: /,但实际所有数据都在/api/下,且API完全开放。所以,robots.txt只能作为参考,不能作为决策依据。
3.2 动态渲染页面的高效解析:Playwright的正确打开方式
Playwright常被当成“Selenium替代品”,但它的真正价值在于
可控的渲染粒度
。很多人用它,就是开个浏览器,
page.goto()
,然后
page.content()
拿HTML,这和Selenium没区别,还更慢。正确的用法,是让它只做它最擅长的事:执行JS、模拟交互、获取动态数据。
核心技巧有三个:
第一,禁用非必要资源加载 。90%的爬虫不需要图片、字体、样式表。在Playwright中,你可以这样配置:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(
# 禁用图片,节省70%加载时间
java_script_enabled=True,
bypass_csp=True,
# 只加载关键资源
ignore_https_errors=True,
)
page = context.new_page()
# 设置资源拦截
page.route("**/*.{png,jpg,gif,woff,woff2}", lambda route: route.abort())
这段代码让Playwright跳过所有图片和字体请求,页面加载速度提升3倍以上,且不影响JS执行。
第二,用
page.evaluate()
直接读取内存数据
。很多网站的数据并不在DOM里,而是在JS变量中。比如,一个Vue应用,商品数据可能存放在
window.__INITIAL_STATE__.product
里。你不必等DOM渲染完再用XPath去扒,直接:
# 等待Vue实例挂载完成
page.wait_for_function("window.__INITIAL_STATE__ && window.__INITIAL_STATE__.product")
# 直接从内存读取
product_data = page.evaluate("window.__INITIAL_STATE__.product")
这比解析HTML快一个数量级,且100%准确,不受CSS类名变更影响。
第三,精准控制等待时机
。别用
time.sleep(3)
,那是反模式。Playwright提供了强大的等待API:
-
page.wait_for_selector("#price"):等某个元素出现 -
page.wait_for_function("() => document.querySelector('#price').innerText !== ''"):等元素内容不为空 -
page.wait_for_response(lambda response: "api/price" in response.url):等特定API响应完成
我们有个项目,要等一个动态加载的评论列表。用
wait_for_selector(".comment-item")
经常超时,因为列表是懒加载的。后来改成
wait_for_response
,监听评论API,拿到JSON响应后直接解析,稳定性和速度都大幅提升。
实操心得:Playwright的
page.screenshot()不仅是调试工具,更是反爬检测的“照妖镜”。当你怀疑被识别为机器人时,截个图,看看页面是否显示“检测到异常流量”。如果是,说明你的User-Agent、屏幕分辨率、字体列表等特征暴露了。这时,用context.add_init_script()注入一段JS,修改navigator.webdriver为false,并设置合理的viewport,往往就能绕过。
3.3 数据清洗与标准化:让脏数据变成金矿
爬下来的数据,90%是脏的。What定义得再清晰,也挡不住网站前端的随意发挥。数据清洗不是“锦上添花”,而是“生死攸关”。我总结了一套四步清洗法:
第一步:格式归一化 。把各种表示方式统一成标准格式。比如价格:
- “¥2999” → “2999”
- “2,999元” → “2999”
- “2999.00” → “2999”
-
“面议” →
None
用正则很容易:
import re
def normalize_price(text):
if not text or "面议" in text:
return None
# 提取所有数字和小数点
match = re.search(r'[\d,]+\.?\d*', text)
if match:
# 去掉逗号,转为整数
return int(float(match.group().replace(',', '')))
return None
第二步:语义标准化 。把同义词、缩写映射到标准术语。比如技术栈:
- “SpringBoot” → “Spring Boot”
- “MQ” → “Message Queue”
- “JVM tuning” → “JVM Tuning”
我们维护了一个YAML文件,存着这样的映射:
spring_boot:
- springboot
- spring boot
- spring-boot
message_queue:
- mq
- message queue
- messaging
清洗时,遍历所有候选词,用模糊匹配(如
fuzzywuzzy
)找到最接近的标准词。
第三步:置信度标注
。不是所有数据都一样可信。比如,一个职位要求里,“3年经验”是从
<li>3年Java开发经验</li>
里提取的,置信度100%;而“熟悉Docker”是从
<p>会用Docker部署</p>
里推断的,置信度可能只有80%。我们在数据结构里加一个
confidence_score
字段,下游系统可以根据这个分数决定是否采用该字段。
第四步:异常值检测 。用统计学方法揪出明显错误。比如,爬取的房价数据,大部分在1万-10万元/平米,突然出现一条“1000万元/平米”,大概率是爬错了(可能是把总价当单价了)。我们用IQR(四分位距)法:
import numpy as np
def detect_outliers(data, multiplier=1.5):
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower_bound = q1 - multiplier * iqr
upper_bound = q3 + multiplier * iqr
return [x for x in data if x < lower_bound or x > upper_bound]
这套方法,让我们在一个房产项目中,自动识别并隔离了3.7%的异常数据,避免了错误结论。
提示:清洗逻辑一定要单元测试覆盖。为每个清洗函数写测试用例,输入各种脏数据,断言输出是否符合预期。这是保证数据质量的唯一可靠方式。
3.4 分布式采集架构:从单机到集群的平滑演进
当数据量超过单机处理能力,就必须上分布式。但很多团队一上来就搞Kubernetes+Kafka+Redis,结果运维成本远超业务价值。我的建议是: 按数据量阶梯式演进 。
-
< 1万条/天 :单机+定时任务(cron)。用APScheduler,简单可靠。
-
1万-10万条/天 :单机+任务队列(Celery + Redis)。把采集任务拆成小任务,比如“采集100个商品ID”,Celery Worker并发执行。Redis做Broker,任务状态存MySQL。
-
10万-100万条/天 :多机+分布式调度(Celery + RabbitMQ + Flower)。RabbitMQ比Redis更稳定,Flower提供可视化监控。此时,需要考虑Worker的负载均衡和故障转移。
-
> 100万条/天 :微服务架构(FastAPI + Kafka + ClickHouse)。每个采集任务是一个独立服务,Kafka做消息总线,ClickHouse做实时分析。
关键设计原则有两个:
第一,任务幂等性
。无论一个任务执行一次还是十次,结果都一样。这是分布式系统的基石。实现方法很简单:每个任务带一个唯一ID(如
task_id = f"{url}_{timestamp}"
),在执行前,先查数据库,如果该ID的任务已完成,直接跳过。
第二,状态外置 。不要把任务状态存在内存里。所有状态——任务是否开始、是否成功、错误日志、耗时——都存到数据库或Redis。这样,Worker挂了,新Worker可以无缝接管。
我们有个电商项目,峰值要采集50万商品,最初用单机Celery,结果Worker内存爆满。后来拆成“URL发现”和“URL采集”两个服务:一个服务专门从搜索页提取商品URL,存到Redis List;另一个服务从List里取URL,用Playwright采集。两个服务可以独立扩缩容,系统稳定性大幅提升。
实操心得:分布式最大的坑是“时间不同步”。当多个Worker在不同服务器上运行,它们的系统时间可能差几秒。这会导致任务调度错乱。解决方案是:所有时间戳都用UTC,并从NTP服务器同步;任务调度用绝对时间(如
eta=datetime(2024,5,20,9,0,0, tzinfo=timezone.utc)),而不是相对时间(如countdown=3600)。
4. 实操过程与完整项目复现
4.1 项目背景与目标定义
我们以一个真实项目为例: 为某省级文旅局建设“全省景区实时客流监测系统” 。这不是一个商业项目,而是政务信息化需求,因此对合规性、稳定性、数据准确性要求极高。
- Why :辅助政府进行节假日客流疏导决策,避免景区拥堵踩踏事件。核心动因是公共安全,不是商业利益。
- What :全省127家4A级以上景区的实时客流数、最大承载量、当前承载率、入园人数趋势(每15分钟一个点)。
- Who :各景区官网技术差异极大。有62家是静态HTML,38家是WordPress,27家是自建系统(其中15家为Vue单页应用)。
- Where :数据需实时推送到政务云的Kafka集群,供大屏系统消费;同时每日生成PDF报告,存入OA系统。
- When :采集触发时间为每天8:00-22:00,每15分钟一次;业务时间为采集时刻;数据有效时间为15分钟(因客流变化快)。
- How :混合架构——静态站用requests,WordPress站用其REST API,单页应用用Playwright。
这个项目,完美覆盖了5W1H的所有维度,是我们复现的最佳样本。
4.2 环境准备与依赖安装
我们采用Python 3.11,所有依赖都通过
requirements.txt
管理,确保环境可复现:
playwright==1.43.0
requests==2.31.0
beautifulsoup4==4.12.2
lxml==4.9.3
celery==5.3.6
redis==4.6.0
kafka-python==2.0.2
pandas==2.0.3
numpy==1.24.3
特别注意Playwright的安装。它不是pip install完就完事,还需要下载浏览器二进制:
# 安装Playwright
pip install playwright
# 下载Chromium(国内网络,用清华源)
playwright install chromium --with-deps
# 或者,如果网络受限,可以下载离线包
# wget https://npmmirror.com/mirrors/playwright/chromium-1228.zip
# unzip chromium-1228.zip -d ~/.cache/ms-playwright/
我们用Docker Compose编排整个环境,
docker-compose.yml
如下:
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
kafka:
image: bitnami/kafka:3.5
ports:
- "9092:9092"
environment:
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
scraper:
build: .
depends_on:
- redis
- kafka
environment:
- REDIS_URL=redis://redis:6379/0
- KAFKA_BOOTSTRAP_SERVERS=kafka:9092
这样,任何开发者拉下代码,
docker-compose up -d
,就能获得一个开箱即用的开发环境。
4.3 核心采集逻辑实现
我们把采集逻辑拆成三个模块:
url_discovery
(发现URL)、
data_fetcher
(获取数据)、
data_processor
(处理数据)。
url_discovery.py
:负责从文旅局公布的景区名录Excel中,提取每个景区的官网URL,并根据域名后缀(`.gov

9099

被折叠的 条评论
为什么被折叠?



