爬虫与综合系统的边界探索:一个京东后端的实战手记
去年618大促前两周,我坐在望京某栋写字楼里,窗外是北京典型的灰蒙蒙天空,手里却攥着一个让我头皮发麻的需求:“我们需要从全网抓取竞品价格、库存和促销信息,实时同步到我们的比价系统。”
产品经理一脸诚恳地说:“就你们后端搞一下吧,技术上应该不难?”
我当时真的想把 MacBook 合上直接走人——但转念一想,这活儿要是真做成了,年底绩效说不定能多拿点奖金,通勤一小时也值了。于是咬咬牙接了下来。
为啥这事非得我们后端干?
在京东干了五年后端,经历过好几轮 618 和双 11 的流量洪峰洗礼,我对“稳”字的理解早已刻进骨子里。爬虫这种听起来像是前端或者数据团队该干的活儿,怎么落到我们这边?原因其实很现实:
- 数据时效性要求高:不是每天跑一次就行,而是要分钟级甚至秒级更新;
- 反爬机制复杂:主流电商平台的反爬策略越来越狠,光靠简单的
requests+BeautifulSoup根本扛不住; - 需要深度集成业务系统:抓回来的数据要直接写入我们的商品中心、风控系统、营销引擎,这些全是后端服务。
换句话说,这不是一个孤立的“爬点数据”任务,而是一个综合系统工程。这也正是我想在这篇文章里强调的核心观点:爬虫从来不是终点,而是整个数据链路的起点;真正的挑战,在于如何把它安全、稳定、可维护地嵌入到现有架构中。
第一版方案:天真如我
一开始,我和组里两个小伙伴(一个刚入职的校招生,一个比我早一年的老油条)用最朴素的方式搭了个原型:
import requests
from bs4 import BeautifulSoup
def scrape_product(url):
resp = requests.get(url, headers={'User-Agent': 'Mozilla/5.0...'})
soup = BeautifulSoup(resp.text, 'html.parser')
price = soup.select_one('.price').text
return {'price': price}
本地跑起来没问题,测试环境也没报错。我们甚至还在内部演示会上秀了一把,产品经理眼睛都亮了:“哇,这么快!”
结果上线第三天,凌晨两点,钉钉疯狂震动——大面积请求被封 IP,部分目标站点返回 403,还有几个页面结构变了导致解析失败。运维同事在群里幽幽地说:“你们这爬虫是不是把人家防火墙当玩具了?”
那一刻,我深刻体会到什么叫“线上一分钟,线下十年功”。
重构思路:从单点工具到综合系统
痛定思痛,我们决定彻底重构。这次不再只盯着“怎么抓”,而是思考“如何让爬虫成为可靠的数据生产者”。以下是我们的核心设计原则:
1. 调度与隔离:别把鸡蛋放一个篮子里
我们放弃了单机脚本模式,引入了 分布式任务队列(Celery + Redis),每个目标站点作为一个独立的任务类型,分配专属的代理池和 User-Agent 池。关键配置如下:
| 站点 | 并发数上限 | 代理IP轮换频率 | 请求间隔(秒) | 失败重试次数 |
|---|---|---|---|---|
| JD | 20 | 每次请求 | 1.5 | 3 |
| TMALL | 10 | 每5次请求 | 3.0 | 5 |
| PDD | 15 | 每次请求 | 2.0 | 4 |
这样即使某个站点挂了,也不会拖垮整个系统。而且通过限流和随机延迟,大大降低了被识别为机器人的概率。
2. 动态渲染支持:不是所有页面都能靠 HTML 解析
很多现代电商站(比如拼多多的部分活动页)重度依赖 JavaScript 渲染。我们尝试过 Puppeteer,但资源消耗太大,一台 8C16G 的机器只能跑 5 个实例,成本太高。
后来改用 Playwright + Docker 容器化部署,配合 Kubernetes 的 HPA(水平 Pod 自动扩缩容),在流量高峰时自动扩容渲染节点。虽然运维复杂度上升了,但稳定性显著提升。
# playwright-crawler.Dockerfile
FROM mcr.microsoft.com/playwright:latest
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
3. 数据清洗与标准化:别让脏数据污染下游
爬回来的原始数据五花八门:
- 价格字段可能是 “¥299”、“299元”、“299.00” 甚至 “即将开售”
- 库存状态有 “有货”、“仅剩3件”、“预售”、“无货” 等多种表述
我们在入库前加了一层 标准化中间件,用正则 + 规则引擎统一格式:
def normalize_price(raw: str) -> float:
match = re.search(r'[\d,]+\.?\d*', raw.replace(',', ''))
return float(match.group()) if match else 0.0
def normalize_stock(status: str) -> str:
if any(kw in status for kw in ['无', '缺', '售罄']):
return 'out_of_stock'
elif '预' in status:
return 'pre_sale'
else:
return 'in_stock'
这套逻辑后来被抽象成独立的 data-normalizer 微服务,供其他数据采集项目复用。
反爬对抗:一场永无止境的猫鼠游戏
最头疼的还是反爬。某天下午,我们发现天猫的价格接口突然全部返回验证码图片。运维查日志发现,对方启用了基于行为分析的风控(比如鼠标轨迹、点击节奏)。
我们一度想放弃,但想到 618 就在眼前,只能硬着头皮上。最终采取了“分层防御+动态策略”:
- 基础层:高质量住宅代理 IP(不是那种烂大街的数据中心 IP)
- 伪装层:模拟真实浏览器指纹(通过 Playwright 设置
deviceScaleFactor,screen,timezone等) - 行为层:加入随机滚动、悬停、点击等“人类行为”(用 Playwright 的
page.mouse.move()模拟) - 应急层:当检测到验证码时,自动切换备用策略(比如降级到移动端 API,或调用第三方打码平台)
虽然不能 100% 绕过,但成功率从 30% 提升到了 85% 以上。更重要的是,系统具备了快速切换策略的能力——这才是工程化的价值。
监控与告警:别等到用户投诉才发现问题
早期我们只监控任务是否完成,后来发现“完成了 ≠ 成功了”。比如页面结构没变,但价格区域被 A/B 测试替换了,爬虫照样跑完,数据却是错的。
于是我们加了三层监控:
- 任务级:Celery 任务失败率、执行时长
- 数据级:抓取字段缺失率、价格突变检测(比如某商品价格突然从 1000 变成 1)
- 业务级:下游系统消费延迟、比价准确率抽样校验
用 Prometheus + Grafana 做可视化,关键指标超过阈值就自动触发企业微信告警。有一次凌晨三点,系统发现某竞品站价格异常下跌 90%,我们立刻介入,发现是对方在做秒杀——及时修正了数据,避免了错误的营销决策。
可维护性:代码不是写给机器看的
作为重度 Mac 用户 + 代码洁癖患者,我坚决反对“能跑就行”的糙快猛风格。在这个项目里,我们坚持了几条原则:
- 配置外置:站点规则、选择器、重试策略全部放到 YAML 配置文件,不用改代码就能调整
- 模块解耦:下载器、解析器、存储器完全分离,方便单元测试
- 日志结构化:用 JSON 格式记录每一步的关键信息,便于 ELK 分析
举个例子,一个站点的配置长这样:
site: tmall
url_template: "https://detail.tmall.com/item.htm?id={item_id}"
selectors:
price: ".tm-price"
stock: ".stock-status"
anti_crawl:
use_playwright: true
human_behaviors:
- scroll_to_bottom
- hover_on_price
retry_policy:
max_attempts: 5
backoff_factor: 2
新来的实习生看一眼就知道怎么加新站点,再也不用翻我写的“祖传代码”。
效果与反思
这套系统在去年双 11 前正式上线,支撑了日均 2000 万+ 的商品数据采集,平均延迟 < 3 分钟,准确率 98.7%。最关键的是——整个大促期间零重大事故。运维同事终于不用半夜爬起来救火,我也能安心在家陪娃(虽然还是被叫去处理了两次 minor alarm)。
但回过头看,有几个教训值得分享:
- 不要低估法律和合规风险:我们专门法务评审过,确保只抓公开数据、遵守 robots.txt、控制频率。技术再牛,也不能踩红线。
- 爬虫不是银弹:有些数据(比如会员专享价)根本拿不到,不如和第三方数据服务商合作。
- 人力成本常被低估:维护一个高可用爬虫系统,远比写个脚本复杂。我们后来专门成立了“数据采集小组”,而不是让业务后端兼职。
写在最后
有人说,爬虫是“脏活累活”,上不了台面。但在今天的数据驱动时代,谁能高效、合规、稳定地获取外部信息,谁就掌握了竞争先机。而作为后端工程师,我们的价值不只是写 CRUD,更是构建端到端的可靠数据管道。
现在每次看到商品详情页上的“价格低于同行 X%”提示,我都会心一笑——那背后,有我和团队熬过的夜、掉过的头发,以及无数次想砸电脑又忍住的瞬间。
技术探索从来不是一蹴而就,而是在一次次“线上炸了 → 修复 → 优化 → 再炸”的循环中,慢慢打磨出一套经得起流量洪峰考验的综合系统。
如果你也在做类似的事情,别怕踩坑。记住:每一个 403 错误,都是通往 200 的必经之路。
(完)
P.S. 本文所有代码和配置均为简化示例,真实系统复杂得多。另外,求求产品经理们下次提需求前先问问“这个数据能不能通过 API 拿到”……🙏

评论 0