Web Scraping底层思维框架:用5W1H重构数据采集决策体系

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接口,而非重依赖浏览器渲染。

提示:每次启动新项目前,强制自己写下三句话:

  1. 这个数据最终要驱动哪个具体业务动作?(例:自动生成日报/触发库存预警/训练NLP模型)
  2. 如果这个数据源明天关闭,我的业务会停摆吗?有没有备选方案?(例:是否有官方API/第三方数据市场/人工抽样替代)
  3. 我的采集行为,是否会让目标网站的普通用户感受到性能下降?(例:单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零宽空格混淆数字(如 2​0​K​-​3​0​K ),这时候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,但做了关键定制:

  1. 签名绕过 :不自己实现签名算法,而是用Playwright启动浏览器后,执行一段JS,从页面内存中直接读取GraphQL请求所需的 x-signature header值,然后用requests复用这个签名发批量请求。这样既利用了浏览器的JS执行能力,又避免了全程浏览器渲染的性能损耗。

  2. 库存状态双校验 :页面显示“有货”,但API返回 in_stock: false 。我们设定规则:以API为准,但当两者冲突时,记录告警日志并触发人工复核流程。这比单纯“相信页面”或“相信API”都更稳妥。

  3. 事务保障 :每批次采集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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值