news 2026/9/26 1:13:52

电商数据采集系统实战:从爬虫脚本到稳定数据管道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
电商数据采集系统实战:从爬虫脚本到稳定数据管道

1. 从零搭建电商数据采集系统的整体思路

电商数据采集这件事,表面上看是"写个爬虫抓数据",但真正落到生产环境里,你会发现抓取只是整条链路里最不值钱的一环。我做过好几个电商数据项目,从最早的单机 requests 脚本,到后来几十台机器跑的分布式采集,再到现在的"采集 + 清洗 + 入库 + 监控"一体化管道,踩过的坑足够写一本书。这篇文章就把我这些年攒下来的实战经验完整拆一遍,重点讲怎么把一堆零散的爬虫脚本,变成一个能长期稳定跑下去的数据管道。

先说清楚这套系统到底解决什么问题。电商场景下的数据需求通常有这么几类:商品详情(标题、价格、SKU、库存)、评论数据(评分、内容、时间)、销量与榜单、店铺信息、类目结构。这些数据的特点是量大、更新频繁、页面结构经常变、反爬策略持续升级。如果你只是偶尔抓一次,写个脚本跑完就完事;但如果你要做价格监控、竞品分析、选品决策,那就必须有一套能每天自动跑、出错能自愈、数据能直接给分析师用的管道。

适合谁来参考这套方案?我的判断是:有 Python 基础、写过简单爬虫、但没做过完整数据管道的同学收益最大。如果你完全零基础,建议先把 requests 和 Scrapy 的基础过一遍再来看,不然很多设计取舍你会看不懂为什么这么做。整套方案的技术栈我选的是Scrapy + Playwright + PostgreSQL/ClickHouse + Airflow(或轻量调度)+ Prometheus 监控,下面会逐个解释为什么这么选。

整体架构我习惯分成四层,这个分层不是拍脑袋定的,而是根据"故障隔离"原则来的——任何一层挂掉,不应该影响其他层的正常运行:

层级职责核心技术故障影响范围
采集层页面抓取、反爬对抗Scrapy、Playwright只影响新数据获取
解析清洗层结构化提取、字段标准化Parsel、Pydantic影响数据质量
存储层原始数据与业务数据分离存储PostgreSQL、ClickHouse影响查询与分析
调度监控层任务编排、告警、重试Airflow、Prometheus影响运维效率

这个分层最大的好处是可替换。比如你哪天觉得 Scrapy 不够用了想换 Playwright 集群,只要采集层的输出接口不变,上层完全不用动。我见过太多项目把抓取、解析、入库全塞在一个脚本里,结果页面一改版整个流程全崩,改起来牵一发动全身。

2. 采集层设计:Scrapy 与动态渲染的取舍

2.1 为什么主力选 Scrapy 而不是纯 requests

很多人入门爬虫是从 requests + BeautifulSoup 开始的,简单直接。但一旦你要抓几万、几十万个页面,requests 的短板就暴露了:没有内置的并发控制、没有自动重试、没有去重、没有中间件机制。你得自己造轮子,造到最后发现造出来的东西就是半个 Scrapy。

Scrapy 的核心优势在于它把爬虫的通用问题都抽象好了:调度器负责 URL 队列和去重,下载器负责并发和重试,中间件负责请求/响应的统一处理,Item Pipeline 负责数据流转。你只需要写解析逻辑,其他都是配置。我实测下来,单机 Scrapy 配合合理的并发配置(CONCURRENT_REQUESTS 设为 16,DOWNLOAD_DELAY 设为 0.5),抓取速度能稳定在每秒 20-30 个页面,比手写的多线程 requests 稳定得多。

但 Scrapy 有个硬伤:它默认不执行 JavaScript。而现在的电商页面,尤其是淘宝、京东、抖音这类,大量内容是通过 JS 动态渲染的,你直接请求 HTML 拿到的往往是空壳。这就引出了下一个关键决策。

2.2 动态渲染方案:Playwright 集成进 Scrapy

处理 JS 渲染页面,常见方案有三种:Selenium、Splash、Playwright。我最终选 Playwright,理由很实在:

  • Selenium太老了,驱动管理麻烦,速度慢,多标签页支持差
  • Splash是 Scrapy 官方推荐的,但它是独立服务,部署复杂,而且对现代前端框架支持一般
  • Playwright是微软出的,API 现代,支持 Chromium/Firefox/WebKit,自动等待机制好用,还能拦截网络请求

集成方式我推荐用scrapy-playwright这个库,它把 Playwright 封装成了 Scrapy 的下载器中间件,你只需要在 Spider 里给 Request 加个meta={'playwright': True}就能触发浏览器渲染,非常丝滑。

# settings.py 关键配置 DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor" PLAYWRIGHT_BROWSER_TYPE = "chromium" PLAYWRIGHT_LAUNCH_OPTIONS = { "headless": True, "args": ["--disable-blink-features=AutomationControlled"], }

这里有个坑我必须提醒:不要所有请求都走 Playwright。浏览器渲染的开销是普通请求的 10-20 倍,内存占用也大。我的做法是先用普通请求试探,如果解析出来关键字段为空,再降级到 Playwright 重试。这样能把 80% 的请求走轻量通道,整体吞吐量提升非常明显。

2.3 动态 iframe 的处理技巧

电商页面里 iframe 特别常见,尤其是支付、登录、评论模块。Scrapy-Playwright 处理 iframe 有个细节:默认情况下它只返回主 frame 的内容,iframe 里的内容需要显式指定。我一般用playwright_page_methods配合frame_locator来定位:

yield scrapy.Request( url, meta={ "playwright": True, "playwright_page_methods": [ PageMethod("wait_for_selector", "iframe#comment-frame"), ], "playwright_include_page": True, }, callback=self.parse_with_iframe, )

然后在回调里通过page.frame_locator()拿到 iframe 内容。实测下来,如果 iframe 是跨域的,Playwright 依然能访问(因为它是在浏览器上下文里操作),这点比纯 HTTP 请求强太多。但要注意及时关闭 page,否则浏览器实例会越积越多,内存直接爆掉。

2.4 反爬对抗的实战心得

反爬这块我不展开讲具体绕过手段(合规红线),只讲工程层面的稳定性设计。核心思路是降低请求特征、控制请求频率、分散请求来源。

  • User-Agent 池:不要用固定的 UA,准备 50-100 个真实浏览器 UA 轮换
  • 请求间隔随机化:DOWNLOAD_DELAY配合RANDOMIZE_DOWNLOAD_DELAY=True,让间隔在 0.5-1.5 倍之间浮动
  • Cookie 管理:用 Scrapy 的 CookiesMiddleware,但要注意有些站点会绑定 IP 和 Cookie,这时候需要配合代理池
  • 失败重试策略:对 403、429 这类状态码,不要立即重试,而是指数退避,RETRY_TIMES=3,RETRY_HTTP_CODES=[500, 502, 503, 504, 408, 429]

提示:反爬对抗是持续博弈,不要指望一套配置吃一辈子。建议把反爬相关的参数全部做成可配置项,方便随时调整而不用改代码。

3. 数据解析与清洗:从脏 HTML 到干净结构化数据

3.1 解析器的选择与字段标准化

Scrapy 默认用 Parsel(底层是 lxml),支持 CSS 和 XPath 两种选择器。我的经验是:结构稳定的页面用 CSS,结构混乱或需要复杂条件判断的用 XPath。比如提取商品价格,CSS 写法是.price::text,但如果价格分成了整数部分和小数部分两个标签,就得用 XPath 拼接。

解析出来只是第一步,真正麻烦的是字段标准化。同一个"价格"字段,不同平台可能叫price、sale_price、current_price,单位可能是元也可能是分,可能带货币符号也可能不带。我的做法是定义一个统一的 Pydantic 模型,所有平台的解析结果都往这个模型里塞:

from pydantic import BaseModel, field_validator from decimal import Decimal class ProductItem(BaseModel): platform: str product_id: str title: str price: Decimal currency: str = "CNY" stock: int | None = None captured_at: datetime @field_validator("price", mode="before") def clean_price(cls, v): if isinstance(v, str): v = v.replace("¥", "").replace(",", "").strip() return Decimal(v)

用 Pydantic 的好处是校验和清洗一步到位,脏数据在进入管道前就被拦下来了。我见过太多项目把脏数据直接入库,后面分析师用的时候才发现一堆空值和异常值,返工成本极高。

3.2 数据去重与增量采集

电商数据采集最怕重复。同一个商品你今天抓一遍、明天抓一遍,如果不去重,数据库很快就膨胀到没法用。去重分两个层面:

URL 层面去重:Scrapy 内置了dupefilter,基于请求指纹去重。但默认是基于内存的,重启就丢。生产环境要换成基于 Redis 的RFPDupeFilter,这样多机共享去重集合。

数据层面去重:同一个商品可能通过不同 URL 访问到,或者页面改版导致 URL 变化。这时候需要在入库时做去重,我一般用(platform, product_id, captured_date)作为唯一键,配合数据库的ON CONFLICT语法实现幂等写入。

增量采集的策略我推荐基于时间戳 + 变更检测:记录每个商品的最后采集时间,只采集超过阈值(比如 24 小时)的商品;同时对比关键字段(价格、库存),只有变化的才写入历史表,没变化的只更新last_seen_at。这样能把存储量降低一个数量级。

3.3 数据质量监控

数据管道最怕的是"静默失败"——爬虫没报错,但抓回来的数据全是空的或者错的。我一般会在 Pipeline 里加一层质量检查:

检查项阈值处理方式
单批次记录数< 预期的 50%告警
关键字段空值率> 10%告警并暂停入库
价格异常值超出历史均值 ±3σ标记待人工确认
解析失败率> 5%触发页面结构变更告警

这套检查帮我抓到过好几次问题:有一次某平台改版,价格字段的 class 名变了,爬虫没报错但价格全抓成了空,质量检查第一时间就发现了。

4. 存储层设计:PostgreSQL 与 ClickHouse 的分工

4.1 为什么不用单一数据库

很多人图省事,所有数据都往 MySQL 或 PostgreSQL 里塞。小规模没问题,但电商数据一旦上量,问题就来了:明细数据写入频繁、分析查询又重,两者互相拖累。我的方案是冷热分离 + 读写分离:

  • PostgreSQL:存商品主数据、店铺信息、配置表这类需要事务和频繁更新的数据
  • ClickHouse:存价格历史、评论明细、销量快照这类只追加、需要快速聚合分析的时序数据

这个分工的核心逻辑是:PostgreSQL 擅长 OLTP(事务处理),ClickHouse 擅长 OLAP(分析查询)。把合适的数据放到合适的库里,各司其职。

4.2 ClickHouse 的建表与写入优化

ClickHouse 的建表有几个关键决策点。首先是引擎选择,电商价格历史这种场景,我用ReplacingMergeTree,它能自动去重(基于排序键),配合version字段可以保留最新版本:

CREATE TABLE price_history ( platform LowCardinality(String), product_id String, price Decimal(10, 2), captured_at DateTime, version UInt64 ) ENGINE = ReplacingMergeTree(version) PARTITION BY toYYYYMM(captured_at) ORDER BY (platform, product_id, captured_at);

分区键用月份,是因为电商数据查询通常按时间范围过滤,按月分区能让查询只扫描相关分区。排序键的顺序也很讲究:platform和product_id放前面是因为查询经常按这两个维度过滤,captured_at放最后用于时间范围扫描。

写入方面,千万不要一条一条 INSERT,ClickHouse 对单条写入性能极差。正确做法是攒批,每批 1000-10000 条,用clickhouse-driver的execute批量插入,或者用clickhouse-client的INSERT ... FORMAT CSV从文件导入。我实测过,批量写入比单条写入快 100 倍以上。

4.3 ClickHouse 重启报错的排查

热词里提到了 "clickhouse 重启报错 failed to flush system log already exists",这个坑我踩过。原因是 ClickHouse 在关闭时没能正常 flush 系统日志表,重启时发现日志文件已存在,就报错了。解决方法通常是:

  1. 检查system库下的日志表(query_log、metric_log等)是否有残留的.tmp文件
  2. 手动清理残留文件,或者临时把对应的日志表配置关掉
  3. 更根本的做法是调整flush相关参数,给关闭流程留足时间

注意:ClickHouse 的系统日志表默认是开启的,生产环境建议根据实际需要选择性开启,不然日志表本身会占用大量磁盘和写入资源。

4.4 Rocky Linux 9 上的部署要点

在 Rocky Linux 9 上部署 ClickHouse,有几个和 CentOS 时代不同的地方。首先是SELinux默认是 enforcing 状态,ClickHouse 的数据目录如果不在标准路径下,会被 SELinux 拦截。要么调整 SELinux 策略,要么把数据目录放到/var/lib/clickhouse下。其次是firewalld的配置,ClickHouse 默认监听 9000(native)和 8123(HTTP)端口,需要显式放行。

安装方式我推荐用官方 RPM 仓库,比下载二进制包省心:

sudo dnf install -y clickhouse-server clickhouse-client sudo systemctl enable clickhouse-server sudo systemctl start clickhouse-server

启动后第一件事是改config.xml里的max_memory_usage和max_threads,默认值对生产环境来说太保守了。

5. 调度与监控:让管道自己跑起来

5.1 调度方案的选择

调度这块,轻量场景用crontab + 脚本就够了,但一旦任务多了、依赖复杂了,就必须上专业工具。我推荐Airflow,虽然它有点重,但 DAG 的依赖管理、重试机制、可视化界面都是刚需。

一个典型的电商采集 DAG 长这样:

fetch_product_list >> fetch_product_detail >> parse_and_clean >> load_to_db >> quality_check

每个任务失败可以单独重试,不会影响已经成功的部分。Airflow 的catchup和backfill功能也很有用,比如某天爬虫挂了,可以补跑那天的任务。

如果觉得 Airflow 太重,Prefect或Dagster是更现代的选择,API 更友好,部署更简单。我最近的项目在用 Prefect,体验不错。

5.2 监控指标与告警

监控我分三个层面:

采集层:请求成功率、平均响应时间、重试次数、被封禁次数。这些指标 Scrapy 自带统计,通过StatsCollector导出到 Prometheus 即可。

数据层:每批次入库记录数、字段空值率、数据延迟(最新数据的时间戳与当前时间的差)。这些需要在 Pipeline 里埋点。

系统层:CPU、内存、磁盘、网络。用 node_exporter 采集,Grafana 展示。

告警规则我一般设这几条:请求成功率低于 90% 持续 5 分钟、数据延迟超过 2 小时、磁盘使用率超过 85%。告警渠道用企业微信或钉钉机器人,简单直接。

5.3 故障自愈设计

管道跑久了,故障是常态。我的设计原则是能自动恢复的绝不人工介入:

  • 网络抖动:Scrapy 自动重试,配合指数退避
  • 页面结构变更:解析失败率超阈值时,自动切换到备用解析规则(如果有),同时告警
  • 数据库连接断开:连接池自动重连,写入失败进入本地队列,恢复后补写
  • 浏览器实例泄漏:定时任务扫描并清理僵尸进程

这套机制下来,大部分故障都能自愈,运维只需要处理真正需要人工判断的问题。

6. 常见问题与排查技巧实录

6.1 采集层常见问题

问题一:Scrapy 跑着跑着内存暴涨

这是最常见的问题,90% 的情况是 Playwright 的 page 没关闭。解决方法是给playwright_include_page=True的请求,在回调里务必await page.close()。另外可以在 settings 里设置PLAYWRIGHT_MAX_PAGES_PER_CONTEXT限制单上下文页面数。

问题二:请求全部返回 403

先检查是不是 UA 被识别了,换个 UA 池试试。如果还不行,可能是 IP 被限了,需要上代理。还有一种可能是请求头缺少关键字段(比如 Referer、Accept-Language),用浏览器开发者工具对比一下正常请求和你的请求差异。

问题三:动态内容抓不到

先确认页面是不是真的需要 JS 渲染。用curl请求一下,如果返回的 HTML 里就有数据,那说明是静态的,不需要 Playwright。如果确实是动态的,检查wait_for_selector是否等到了正确的元素,有时候需要等网络空闲(wait_until="networkidle")。

6.2 存储层常见问题

问题四:ClickHouse 写入报 "Too many parts"

这是批量写入太频繁导致的,ClickHouse 每个分区建议不要超过 300 个 part。解决方法是增大批次大小、降低写入频率,或者调整parts_to_throw_insert参数(治标不治本)。

问题五:PostgreSQL 连接池耗尽

Scrapy 是异步的,但 psycopg2 是同步的,如果直接在 Pipeline 里用同步连接,高并发下连接池很快耗尽。解决方案是用psycopg3的异步接口,或者用asyncpg,配合 Scrapy 的异步 Pipeline。

6.3 排查速查表

现象可能原因排查方向
抓取速度突然变慢被限速 / 代理质量差看响应时间分布,换代理测试
数据大量为空页面改版 / 解析规则失效对比新旧 HTML 结构
数据库写入失败连接断开 / 字段类型不匹配看数据库日志,检查 schema
任务卡住不动死锁 / 等待超时看进程栈,检查锁和超时配置
内存持续增长对象未释放 / 缓存无上限用 memory_profiler 定位

6.4 几条血泪经验

第一,永远不要在生产环境直接调试。我早期犯过这个错,改一行代码重启一次,结果把正在跑的任务全打断了。正确做法是本地复现问题,改好测试通过再上线。

第二,日志要打够,但别打太多。关键节点(请求开始、解析完成、入库成功)必须打日志,但不要在循环里打 debug 日志,不然日志文件一天能涨几十 G。

第三,配置和代码分离。反爬参数、数据库连接、并发数这些全部放配置文件或环境变量,改配置不用改代码、不用重新部署。

第四,做好数据备份。ClickHouse 的数据虽然可以从源头重新抓,但成本很高。定期备份关键表,尤其是那些历史数据。

这套系统我从最早的脚本一路演进过来,中间推翻重做了好几次。现在回头看,最大的体会是:采集系统的核心竞争力不在抓取本身,而在于稳定性和数据质量。抓取技术网上教程一大堆,但怎么让系统连续跑一年不出大问题、怎么保证数据干净可用,这些才是真正拉开差距的地方。如果你正在做类似的项目,建议一开始就把架构分层做好,别图快把所有逻辑塞一起,后面维护的时候你会感谢当初的自己。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 1:13:33

自建状态监控面板Status Deck:技术选型与全栈实施记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:12:04

服务端HTML转PDF方案:无头浏览器解决中文与表格分页

简介&#xff1a;这是一份面向前端开发者的网页转 PDF 完整源码方案&#xff0c;基于 jsPDF 与 html2canvas 实现&#xff0c;无需安装任何浏览器插件&#xff0c;即可将任意网页对象以所见即所得的矢量方式输出为 PDF&#xff0c;并完整支持中文、图片与表格。资源共 10 个文件…

作者头像 李华
网站建设 2026/9/26 1:10:58

基于xxxwww的电商采集管道:会话保持与反爬实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:46

洗碗机水泵EMC整改:高集成方案如何从源头解决辐射超标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:33

Proteus 8.17 SP2安装故障深度解析与系统级调优指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:07

多重假设检验校正:FDR、q值与Bonferroni原理及Python/R实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华