news 2026/9/14 13:04:12

Scrapy采集京东商品:解析、渲染与并发调优全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy采集京东商品:解析、渲染与并发调优全攻略

简介:基于Scrapy框架的京东商品数据爬虫项目,代码精简、文档齐全,适合爬虫入门者、高校学生用于课程设计、毕业设计或快速搭建电商数据采集原型。项目经过完整测试并获导师认可,可直接运行或二次开发。资源包含27个文件,以9个Python源码为核心,覆盖items、pipelines、middlewares、spiders等Scrapy关键模块,另有pyc编译文件、xml工程配置、scrapy.cfg及README说明文档,整体压缩包仅27KB,便于学习阅读。目前已有44人浏览学习。对希望掌握Scrapy框架、理解爬虫目录结构的学习者而言,这套资源提供了从请求发送、数据解析到管道存储的完整示例,配合文档可快速上手,也能作为课程设计或毕业设计的起始模板,在此基础上便捷扩展其他功能。内容来源于网络分享,版权归原作者所有,下载后请合理使用。

1. 京东页面对 Scrapy 的考验不只是反爬,还有状态管理

大多数 Scrapy 入门教程喜欢拿静态页面练手,而京东恰恰是“看起来像静态、实际上拆成四五次请求”的典型:列表页首屏数据藏在内嵌 JSON 里,价格走单独接口,部分字段要等 JS 渲染后才出现在 DOM 中。直接把response.xpath打在商品标题上,往往能取到结果,但稍微换一个品类或翻页参数,字段就大面积缺失。这个标题要解决的,是在 Scrapy 的规则调度框架内,把京东商品页的数据链路完整接住:列表页解析、详情页补全、动态渲染、并发控制、数据管道。内容面向已经跑通过基本 Scrapy 项目、想把它推到“可维护的工程资料”程度的开发者。适合的人群是把爬虫当数据结构问题处理的人,而不是只想抓一次数据就扔掉的脚本党。抓下来只是第一步,字段稳定、请求可追溯、失败可重放,才是这个项目真正的价值所在。

2. 列表页、详情页与价格接口:用 Scrapy 回调链拆解数据源

京东商品搜索页虽然返回的是 HTML,但有效数据集中在<script>标签里的__INITIAL_STATE__变量中。这个变量不是标准 JSON,而是 JSBridge 渲染前写入页面的状态快照。相比直接解析 HTML 节点,从内嵌 JSON 里取值的好处是结构稳定,不随页面改版而频繁变动;坏处是字段层级深,且某些值是null或省略的。常见做法是先用 XPath 取出 script 文本,再按 JSON 子串做解析,最后兜底从 DOM 提取标题。

2.1 列表页解析优先读内嵌 JSON,DOM 解析作为兜底

import json def parse_list(self, response): # 取出包含 __INITIAL_STATE__ 的 script 标签文本 script = response.xpath( '//script[contains(text(), "__INITIAL_STATE__")]/text()' ).get() if not script: self.logger.warning("no init state: %s", response.url) return start = script.index("{") # 去掉变量名、等号和结尾分号,得到完整的 JSON 字符串 state = json.loads(script[start:].strip().rstrip(";")) # 这个路径对应商品列表数据,京东不同页面结构略有差异 items = state["goods"]["list"] for item in items: product = { "sku_id": item.get("skuId"), "title": item.get("title"), "price_url": "https://p.3.cn/prices/mgets?skuIds=J_%s" % item.get("skuId"), "detail_url": "https://item.jd.com/%s.html" % item.get("skuId"), } # 详情页一旦通过,价格回调会把数据并回 product yield scrapy.Request( url=product["detail_url"], meta={"product": product}, callback=self.parse_detail, )

这段代码的关键是拆两步走:先解析列表页拿到 skuId,再用 skuId 拼接详情页和价格接口。meta参数的作用在于把当前请求的数据传递到下一个回调函数中,避免重新解析一遍列表页。很多初学者会在parse_detail里再次请求列表页拿数据,这既浪费流量又增加被封风险。script[start:].strip().rstrip(";")这行是为了去掉 JS 变量声明后的分号,因为有些环境下原始文本末尾会残留;

这个方案依赖__INITIAL_STATE__在当前京东页面版本中存在,如果后续页面改用其他变量名,XPath 的contains(text(), "__INITIAL_STATE__")就匹配不到。遇到这种情况,优先看页面源代码里还有没有其他以__开头的全局状态变量,而不是直接去改json.loads的逻辑。

2.2 详情页补全字段,而不是从零开始

列表页已经提供了标题和 skuId,但商品详情页里更有价值的信息是品牌、产地、规格参数,这部分只在详情页的规格表或页面 JSON 中存在。在parse_detail中要做的是把已有数据和新增字段合并,不要重新构造一个 product 对象。

def parse_detail(self, response): product = response.meta["product"] # 详情页规格参数通常在 <ul class="parameter2"> 里 params = {} for li in response.xpath('//ul[contains(@class, "parameter2")]/li'): text = li.xpath("string(.)").get() if text and ":" in text: key, value = text.split(":", 1) params[key.strip()] = value.strip() product["brand"] = params.get("品牌") product["origin"] = params.get("产地") product["detail_params"] = params yield product

这里要注意response.meta默认在重定向时会被丢弃,如果京东对无 Cookie 请求返回 302 到验证页面,meta就丢了。解决办法是在Request里设置dont_process_response=True或者显式带上meta={"handle_httpstatus_all": True}。我一般会在 settings 里把REDIRECT_ENABLED保持开启,但在详情页回调前给对方一个合理的 User-Agent,这样触发验证码重定向的概率会低很多。

2.3 价格接口单独请求,失败不影响商品数据落库

价格是京东页面中变动最频繁的数据,而且经常与商品详情页不在同一个域名下。专门拿p.3.cn的接口来做价格采集,可以让整个爬虫的关注点更集中。这个接口返回的是 JSON 数组,结构为[{"p": "4999.00", "m": "5999.00"}],其中p是当前售价,m是原价。

def parse_price(self, response): product = response.meta["product"] try: data = json.loads(response.text) product["current_price"] = data[0].get("p") product["original_price"] = data[0].get("m") except (IndexError, KeyError, TypeError): # 价格接口偶发空数组,保留默认值 product["current_price"] = None yield product

这个回调因为是独立请求,即使价格接口暂时不可用,商品信息已经通过meta带了过来,不会导致整条数据丢失。这正好体现 Scrapy 回调链设计的价值:数据的每个组成部分都有独立的状态和失败语义,而不是一个parse函数包打天下。在运行scrapy crawl jd_product时,日志中每个 URL 的处理时长和失败状态都可以单独观测。

字段来源失败策略
skuId列表页__INITIAL_STATE__跳过该商品
标题列表页内嵌 JSONDOM 兜底
品牌/产地详情页<ul class="parameter2">字段留空
当前价p.3.cn价格接口置 None 保留商品

3. 动态渲染与 iframe 内容:在 Scrapy 中集成 Playwright 的取舍

不是所有京东数据都藏在接口和 HTML 里。商品评价的概览、某些营销活动页的实时库存、以及登录后可见的价格区间,通常由 JS 在浏览器环境中执行后渲染。收到“低于 3000 元才展示”之类的促销逻辑,服务器返回的 HTML 里就没有那个数字,这时候要么花力气去逆向它的 XHR 接口,要么直接用浏览器渲染。接口逆向对反爬升级的抵抗力差,而浏览器渲染方案虽然重,却更稳定。

3.1 什么时候才需要 Playwright,而不是继续加解析规则

判断标准很直接:把response.text存下来直接 grep,如果发现目标数据根本不在里面,再去谈渲染。很多人一上来就在 Scrapy 里挂无头浏览器,结果只是把本来能通过接口拿到的数据拖慢了三倍。京东商品详情页 90% 以上的字段可以通过第 2 章的请求链路拿到,真正需要渲染的是“滚动加载后出现的用户评价摘要”和部分店招模块。

常见做法是单独为这些区块配置一个 Playwright 请求,而不是把整个站点的请求都走浏览器。Scrapy 有一个scrapy-playwright插件可以在不改变原有回调结构的前提下,把特定 URL 交给浏览器处理。配置的核心就在 settings 和 Request meta 上。

# settings.py DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
# items.py 中的爬虫代码片段 yield scrapy.Request( url=review_url, callback=self.parse_review, meta={ "playwright": True, "playwright_page_methods": [ {"method": "wait_for_selector", "selector": ".comment-item"}, ], }, )

playwright置为 True 后,该请求会走浏览器处理;playwright_page_methods里声明的是页面加载完成后需要等待的选择器,这里尤其注意不要用固定延时。无条件sleep(5)在慢网络下不充分,在快网络下浪费 4 秒,用wait_for_selector让页面自行告诉爬虫“我准备好了”。等待条件里还可以加入state="visible",确保元素真的渲染到可视区域,某些懒加载组件仅仅存在于 DOM 但未显示时也可能被误判为成功。

3.2 iframe 里的数据:切 frame 比改 URL 靠谱

京东的店铺首页和部分活动页使用 iframe 嵌入子页面。iframe 的特性是 URL 不变但内容由另一个文档承载,直接 XPath 不到任何节点。处理 iframe 的常见做法是用 Playwright 的frame_locator切换到对应的子页面,再继续等待和提取。frame_scrapy项目中常见的问题是把 iframe 的 src 单独拉出来请求,如果 src 带签名参数,单独请求会签名失效。

meta={ "playwright": True, "playwright_page_methods": [ { "method": "frame_locator", "selector": "iframe#shopInfo", "methods": [ {"method": "wait_for_selector", "selector": ".shop-name"}, {"method": "text_content", "selector": ".shop-name"}, ], }, ], }

上面这种方式适合简单的取值。我更推荐的做法是在parse_review里直接拿page对象,用 Python 原生控制 iframe,因为这样能拿到完整的上下文,调试时也能打印控制台报错。

async def parse_review(self, response): page = response.meta.get("playwright_page") if not page: self.logger.error("playwright page is None") return frame = page.frame_locator("iframe#shopInfo") shop_name = await frame.locator(".shop-name").text_content() yield {"shop_name": shop_name.strip()}

frame_locator返回的是一个FrameLocator,它的选择器范围天然限定在指定 frame 内,不会误匹配主页面里同名的元素。京东的 iframe id 可能会随页面改版变化,这个值要从response.text里先确认存在,再写进代码。直接写死选择器而不过滤id-contains会导致改版后全部失效,建议用frame_locator("iframe[src*='shop']")这类属性前缀匹配提升容错。

4. 并发调优与代理 IP 池:Scrapy 的稳定抓取要改的五个参数

Scrapy 默认的并发设置偏向于“快”,但京东的限流策略恰恰对单 IP 的请求频率最敏感。在爬取商品数据这类场景中,稳定优于速度,因为一旦触发滑块验证,重试所花的时间远超通过降速节省的时间。这里先列出一组经验起点,再逐个解释调整方向。

参数建议值说明
CONCURRENT_REQUESTS8~16同一域名下的最大并发数
DOWNLOAD_DELAY1.0~2.0同一 IP 下两个请求的时间间隔
RANDOMIZE_DOWNLOAD_DELAYTrue在 0.5~1.5 倍之间随机化间隔
AUTOTHROTTLE_ENABLEDTrue根据响应延迟动态调节频率
COOKIES_ENABLEDFalse尽量不携带持久 Cookie 请求

CONCURRENT_REQUESTS不是越高越好,当响应体体积大且下载带宽有限时,高并发反而让单请求变慢,整体吞吐下降。京东的反爬日志里经常出现同一 IP 在 5 秒内请求超过 20 次就被临时封禁的情况,把并发降到 8 并开启DOWNLOAD_DELAY后,单机跑一两个小时仍然稳定。

4.1 User-Agent 池不能只换浏览器版本,要换内核特征

很多 UA 池只是把 Chrome 版本号改成120121122,但所有 UA 的 Sec-Ch-UA、Sec-Fetch-Site 等请求头完全一致,服务端通过请求头组合特征很容易识别出这是同一套库发出的请求。维护 UA 池的正确做法是让每个 UA 对应一组完整的请求头模板,至少包含User-AgentAccept-LanguageSec-Fetch-DestSec-Fetch-ModeSec-Fetch-Site

class RandomUAwareMiddleware: UA_TEMPLATES = [ { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", "Sec-Fetch-Site": "same-origin", }, { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15", "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6", "Sec-Fetch-Site": "none", }, ] def process_request(self, request, spider): template = random.choice(self.UA_TEMPLATES) for key, value in template.items(): request.headers[key] = value

这个中间件的价值在于把请求头的选择逻辑从settings.py中抽离出来,每个请求都有一致但不同的头组合。需要注意Sec-Fetch-Site的值不能乱填,它表示请求来源与目标站点的关系,京东站内请求通常是same-origin,如果填成cross-site会触发更严格的风控校验。京东有些接口还校验Referer,这种情况下要在被校验的请求上显式设置Referer为商品页 URL,而不是依赖中间件统一处理。

4.2 代理 IP 池的接入位置:放在下载中间件而不是调度器

常见做法是下载中间件的process_request里为每个请求选一个出口 IP。但要注意,频繁切换 IP 并不会提高成功率,反而可能因为 IP 段变化触发“新设备登录”校验。更稳妥的策略是:一个 IP 负责一个品类页面的前 N 个请求,完成后整体调换。用request.meta["proxy"]传 IP 地址的方式实现粒度控制,而不是在中间件里随机换。

class ProxyMiddleware: def process_request(self, request, spider): pool = spider.crawler.settings.get("PROXY_POOL") if request.meta.get("force_proxy"): # 指定代理,用于已经触发限流的请求重试 request.meta["proxy"] = request.meta["force_proxy"] else: # 同一爬虫进程内给一个固定代理,减少切换频率 if not hasattr(spider, "_current_proxy"): spider._current_proxy = pool.next() request.meta["proxy"] = spider._current_proxy

这段逻辑的关键在spider._current_proxy,它让整个爬虫进程内保持同一个出口 IP,而不是每个请求都换。当某个请求返回 403 或滑块页时,通过重试中间件把force_proxy带上,强制该请求换一个新 IP 重新执行。这样做的好处是正常请求不需要频繁切换 IP,显著降低了被判定为异常的概率。代理 IP 池的维护频率取决于抓取强度,单机低并发场景下每小时换一次池子足矣。

4.3 请求失败的语义要区分:超时、连接异常与反爬拦截

Scrapy 的RetryMiddleware默认对所有 500 系列和超时异常重试两次,但这个策略对京东并不合适。京东的接口偶尔返回 200 但内容为空 JSON,这种请求即使重试一百次也一样空。更好的做法是用自定义异常类区分失败类型,控制重试次数和重试间隔。

class JDRetryMiddleware(RetryMiddleware): def process_response(self, request, response, spider): if response.status in self.retry_http_codes: # 京东搜索页偶尔返回 302 到验证码页,重试时降速 reason = response.status spider.crawler.engine.close_spider(spider, "banned_on_%s" % reason) return response return super().process_response(request, response, spider)

这个写法不建议直接复制进生产环境,它只是表达一个思路:当检测到验证码跳转时,与其快速重试把 IP 打上更高风控标签,不如主动暂停整个爬虫。暂停比换 IP 更快,因为当前 IP 已经在风控名单里,继续请求只会扩大封禁范围。

5. 数据管道落地与追溯:字段校验和断点恢复的具体做法

Scrapy 的 Item Pipeline 是数据的出口,也是最后一道质量闸门。京东商品数据的常见问题不是“抓不到”,而是“抓到了错误的数据”。比如促销价和原价倒挂、品牌字段混入店铺名、skuId 被解析成科学计数法的数字型。这些问题必须在下游存储之前做结构化校验,否则数据进了数据库再清洗,成本和可信度都会打折扣。

5.1 在 Pipeline 里做价格一致性校验

价格是京东数据里最容易被业务方拿去直接做分析的值,那就在管道里给价格做规则校验。促销价不能大于原价,价格不能为负数或小数位超过两位,这些规则可以先写在本地配置里,后续调整不需要改动代码逻辑。

class PriceValidationPipeline: def process_item(self, item, spider): p = item.get("current_price") m = item.get("original_price") if p is not None and m is not None: try: p_float = float(p) m_float = float(m) except (TypeError, ValueError): spider.crawler.stats.inc_value("price_validation/not_number") return item if p_float > m_float: spider.crawler.stats.inc_value("price_validation/inverted") # 不丢弃,保留原数据并标记异常,方便人工复核 item["price_warning"] = True return item

spider.crawler.stats.inc_value是 Scrapy 内置的统计器,记录每个异常类型发生的次数。运行结束后可以在scrapy crawl jd_product -s LOG_LEVEL=INFO的输出末尾看到这些统计值,比盯着日志逐行人工排查高效得多。这个方案的价值在于管道不是简单地“收数据入库”,而是给数据附加了一条验证记录,下游人员能直接看到哪些商品触发了价格告警。

5.2 用scrapy parse做单页快速验证

开发阶段不建议每次改完 XPath 都全量跑爬虫,scrapy parse命令可以直接指定 URL 和解析函数做单页测试。

scrapy parse https://item.jd.com/100012043978.html \ --callback parse_detail \ --spider jd_product \ --meta '{"product": {"sku_id": "100012043978"}}'

命令里的--meta会被打包成response.meta提供给回调函数使用,这样不需要先把列表页跑完就能验证详情页解析逻辑是否正确。如果回调函数里依赖了parse_list产生的字段,可以在--meta里临时补一个最小字典。这个用法在多人协作时特别有用,负责详情页解析的人不需要关心列表页是否挂了,只管把自己的回调调通。

对于“基于 Scrapy 框架的京东爬虫实现完整资料”来说,Pipelines 的字段校验规则、重试中间件的故障语义、以及每个解析函数的输入输出约定,应该沉淀为项目 README 里最核心的内容。把这一次调试过程记录成docs/参数变更记录.md,下次京东改版后,可以通过比对字段变更历史快速确认影响范围,而不是重新逆向一遍页面结构。

本文还有配套的精品资源,点击获取

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

FMCW SAR成像为何必须用range-Doppler处理

简介&#xff1a;本资源是一份面向雷达信号处理初学者与SAR成像研究者的FMCW SAR Range-Doppler成像实践代码包&#xff0c;聚焦于合成孔径雷达中连续波调频体制下的距离-多普勒域图像重建原理与实现。资源核心为一个MATLAB脚本&#xff08;range_doppler.m&#xff09;&#x…

作者头像 李华
网站建设 2026/9/14 12:57:53

从RAG到智能体:WeKnora v0.8.0记忆、工具与技能落地实践

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

作者头像 李华
网站建设 2026/9/14 12:56:02

RS485现场频繁掉线怎么办?从地环路到布线的完整排查方法论

下午三点客户打来电话&#xff1a;"你们的仪表在实验室测得好好的&#xff0c;一到我们车间就掉线&#xff0c;五分钟掉一次&#xff0c;重试能恢复&#xff0c;一会儿又掉。"这种话我听了不下十次。RS485作为工业现场最古老也最顽强的通信方式&#xff0c;实验室里一…

作者头像 李华
网站建设 2026/9/14 12:55:39

影视APP源码解析:原生安卓+苹果CMS三端协同方案

简介&#xff1a;这是一套基于原生开发的七彩安卓影视APP源码&#xff0c;面向Android应用开发者与全栈工程师&#xff0c;解决多端影视平台快速搭建需求&#xff0c;支持PC网页、WAP移动端及原生Android APP三端统一对接苹果CMS后台&#xff0c;适用于中小型视频网站二次开发或…

作者头像 李华