ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scrapy实战:京东商品数据采集、反爬与价格监控

Scrapy实战:京东商品数据采集、反爬与价格监控 简介基于Scrapy框架的京东爬虫项目源码包包含完整可运行的爬虫模块与详实说明文档适合正在学习Python爬虫、准备课程设计或毕业设计的计算机专业学生参考。压缩包共27个文件核心为9个Python源码文件覆盖spiders爬虫、items数据模型、pipelines管道处理、middlewares中间件及settings配置另有scrapy.cfg配置文件、README.md说明文档以及IDE相关的xml/iml文件整体仅27KB轻量易用。目前已有44人学习下载项目曾获导师指导认可答辩评分达到95分代码经过实际运行验证可直接启动或在其基础上二次开发。通过阅读该项目的目录结构与代码实现可清晰理解Scrapy框架的请求调度、数据解析与存储流程对快速上手爬虫开发、完成类似电商数据采集任务具有实用价值。1. 为什么选Scrapy京东爬虫栽过跟头后才理解的事有人用 requests 写京东爬虫前 500 个商品可能很顺然后 IP 被风控response 页面里开始混入验证码过一会儿连列表页都进不去。真正要把京东数据稳定跑起来要解决页面结构变化、异步价格、Cookie 失效、限速、断点续爬这五个问题。Scrapy 的引擎调度、中间件和 Pipeline 正好把这些变成配置项和独立模块列表页崩了不影响价格队列被风控的请求经过重试逻辑自动回到队列尾部限速和代理统一挂在下载中间件上。这个标题里的“完整资料详细文档”不是一份源码包而是一套可维护的打法Item 怎么定、爬虫怎么写、反爬参数怎么调、动态价格怎么补。下面按一线采集工程里常见的做法从字段设计开始讲。2. 京东爬虫的 Item 建模商品、价格、评论先各归各表先讲一个会返工的坑把京东所有字段塞进一张表。商品属性、价格快照、评论内容的变化频率完全不同商品标题可能一个月改一次价格一天变十次评论持续追加。如果全写在一个表里一次全量采集就会产生大量重复行清洗时还得理清哪个是最新值。常见做法是先拆成商品主表、价格快照表、评论表三类再在 Item 层面对应三组字段。2.1 商品与 SKU 的边界怎么定京东的“商品 ID”和“SKU”通常是同一个数字典型形态是item.jd.com/100012043978.html里的 100012043978。但同一个商详页里经常挂多个规格颜色、存储版本不同实际成交价也不一样。如果把“商品”当主键同一页面多价格就会互相覆盖把 sku_id 当主键规格属性作为同一条记录里的普通字段再用单独的价格快照表记录每次抓到的价格下游做销量分析或价格变动 join 起来就非常直接。商品主表字段没有官方标准我一般这样定字段类型说明sku_idvarchar商详页 URL 中的数字 ID主键titlevarchar商品标题可能包含营销词shop_namevarchar店铺名称categoryvarchar类目路径用/分隔brandvarchar品牌从商详页品牌链接解析urlvarchar商详页 URL排错时回放用ext_timedatetime本次采集时间戳这里有两个容易忽略的点sku_id 统一存字符串JSON 接口经常把它当数字返回直接用整型入库某些数据库驱动在高位数上可能丢失精度ext_time 统一用采集时间不要用页面上的更新时间否则你无法判断这条记录到底是“当时看到的状态”还是“后来被改过的状态”。2.2 评论表与价格表的字段差异评论表在采集时最容易出现重复。京东评论接口翻页时偶尔会把同一条评论返回两次所以表里要把 comment_id 设成唯一键写入用INSERT ... ON DUPLICATE KEY UPDATE。评论字段一般包括 comment_id、sku_id、content、score、comment_time、useful_count。content 里经常带 HTML 标签和表情符落库前要统一清洗。价格表更关注“采集这个时刻的价格”字段上是 sku_id、price、origin_price、activity_price、ext_time。price 取当前价origin_price 取划线价activity_price 只有参加促销活动时才出现很多情况下必须等异步接口返回。价格表的设计要能支撑两类查询某 SKU 的历史价格曲线以及某一时刻全站商品的价格分布。所以 ext_time 一定要建索引查询别用全表扫。2.3 用 Item 约束字段形状即文档# items.py from scrapy import Item, Field class JdGoodsItem(Item): sku_id Field() # 商详页 URL 中的数字 ID统一字符串 title Field() shop_name Field() category Field() brand Field() url Field() ext_time Field() # 价格字段由价格接口回填允许为空 price Field() origin_price Field() class JdPriceItem(Item): sku_id Field() price Field() origin_price Field() activity_price Field() ext_time Field() class JdCommentItem(Item): comment_id Field() # 评论去重键 sku_id Field() content Field() score Field() comment_time Field() useful_count Field()Item 是 Scrapy 的字段容器核心用处不在类型校验而在“看到字段名就知道哪个阶段要填什么”。JdGoodsItem里的 price 允许为空由价格接口回填JdCommentItem的 comment_id 就是给去重用的。Pipeline 里做字段完整性检查时漏了哪个字段可以直接报出来不用等到落库后才发现字段名拼错。3. 从搜索页到商详页搭一个可运行的 Scrapy 京东爬虫骨架Item 定好之后要把它接进完整链路。京东爬虫的入口有两种按关键词搜索或者按类目列表。按关键词的优点是结构稳定、翻页规律统一适合监控指定品类的价格按类目列表覆盖更全适合全量数据采集但入口更复杂。下面的骨架按关键词搜索来写链路是搜索列表页 → 商详页 → 价格接口。3.1 创建工程与第一个 Spiderscrapy startproject jd_spider cd jd_spider scrapy genspider jd_product item.jd.comgenspider生成的只是带 allowed_domains 的壳京东搜索入口在 search.jd.com所以生成后要改 allowed_domains。一个容易漏掉的地方是价格接口在 p.3.cn 域名下allowed_domains 里只留jd.com会导致价格请求被过滤怎么等都不会回调。把它改成[jd.com, 3.cn]商详页和价格请求都能正常走。3.2 解析搜索列表与翻页参数搜索页 URL 格式是https://search.jd.com/Search?keyword机械键盘encutf-8。列表里的每个商品在一个li.gl-item里># spiders/jd_search.py import scrapy class JdSearchSpider(scrapy.Spider): name jd_search allowed_domains [jd.com, 3.cn] def start_requests(self): keyword getattr(self, keyword, 机械键盘) url https://search.jd.com/Search?keyword{}encutf-8.format(keyword) yield scrapy.Request(url, dont_filterTrue) def parse(self, response): # 先判断是不是验证码页面是就直接放弃 if verify in response.url or 滑块 in response.text: self.logger.warning(risk control page: %s, response.url) return for li in response.css(li.gl-item): sku li.css(::attr(data-sku)).get() if sku: detail_url https://item.jd.com/{}.html.format(sku) yield scrapy.Request( detail_url, callbackself.parse_detail, meta{sku_id: sku} ) # 翻页 page response.meta.get(page, 1) if page 20: next_page page 1 next_url ( https://search.jd.com/Search?keyword{} page{}s{}.format(self.keyword, next_page, (next_page - 1) * 60 1) ) yield scrapy.Request( next_url, callbackself.parse, meta{page: next_page}, dont_filterTrue )翻页参数里s是商品偏移量常见写法是(page-1)*601。京东调过几次列表页的每页条数如果第二页抓回来还是第一页内容把 60 改成 30 再验证一次。dont_filterTrue用在搜索入口页和s参数不同的翻页 URL 上避免调度器把相似 URL 当成重复请求丢弃。page 上限设 20 是保守值超过之后风控概率会明显上升要靠下面一章的中间件来兜底。3.3 商详页解析与价格接口回调商详页里可以直接解析标题、店铺名价格却经常是空占位符。常见的做法是把商品 Item 放在 meta 里传给价格请求等价格接口返回后一起 yield这样同一个 SKU 只产出一次有效记录。def parse_detail(self, response): sku_id response.meta.get(sku_id) item JdGoodsItem() item[sku_id] sku_id item[url] response.url item[title] response.css(.sku-name::text).get() item[shop_name] response.css(.contact a::text).get() item[ext_time] datetime.datetime.now() price_url https://p.3.cn/prices/mgets?skuIdsJ_{}.format(sku_id) yield scrapy.Request( price_url, callbackself.parse_price, headers{Referer: response.url}, meta{item: item}, dont_filterTrue ) def parse_price(self, response): item response.meta[item] try: data response.json() except ValueError: self.logger.warning(parse price json failed, sku%s, item[sku_id]) yield item return if data and data[0].get(p): item[price] data[0][p] item[origin_price] data[0].get(op) else: self.logger.warning(empty price data, sku%s, item[sku_id]) yield item这段代码的逻辑是商详页先组装商品主表数据再发一个价格请求价格回调里把 price、origin_price 回填后统一 yield。Referer 头必须带着价格接口对 Referer 比较敏感不带 Referer 时通常会返回空数组。注意dont_filterTrue在这里不能省同一个 SKU 的商详页可能被多个关键词入口重复访问第二次发起的价格请求如果被去重价格就永远回填不了。3.4 settings 关键参数速查参数建议值说明ROBOTSTXT_OBEYFalse默认开启时搜索页进不去是否关闭要在目标站点允许的范围内自行权衡CONCURRENT_REQUESTS4单机单 IP 同时请求数先压住再放开DOWNLOAD_DELAY1.2每个下载请求之间的基础间隔单位秒COOKIES_ENABLEDTrue维持会话降低被识别成脚本的概率RETRY_TIMES2网络异常重试次数配合第 4 章中间件使用DOWNLOAD_TIMEOUT20超过 20 秒判定失败DOWNLOAD_DELAY 只设一个固定值还不够下一章会在中间件里加随机区间让请求间隔不要呈现均匀节拍。4. 京东反爬应对随机 UA、Cookie、延时、代理与重试的中间件写法4.1 风控不是立刻拒绝京东风控的常见表现不是直接拒绝而是给你返回一个验证码页面或者把价格接口返回清空。判断是否被风控可以用两个信号response.url里出现verify相关参数或者页面文本里有“滑块”“拖动”等关键词。另一个隐蔽信号是商详页正常返回但价格接口连续返回[]。这时候优先考虑 Cookie 失效或 IP 被限而不是接口变更。所以第 3 章里已经写了一句风控判断正式工程中应该把它抽成一个独立中间件而不是在每个回调里重复写。4.2 随机 UA 和 Cookie 轮换中间件# middlewares.py import random class JdHeaderMiddleware: def process_request(self, request, spider): ua_list spider.settings.get(USER_AGENT_LIST) if ua_list: request.headers[User-Agent] random.choice(ua_list) cookie spider.settings.get(JD_COOKIE) if cookie and Cookie not in request.headers: request.headers[Cookie] cookie return None这里把 UA 列表放在 settings.py 里维护不要写死在中间件里。spider.settings.get在 Spider 初始化后就能拿到配置中间件里调用不会产生额外依赖。JD_COOKIE 建议从浏览器复制一份当前登录态的 Cookie尤其是带pt_key、pt_pin的那一段没有登录态也能抓但出现验证码页的概率会高不少。Cookie 会失效一般每隔几小时或出现大量价格空返回时手动替换一次。4.3 代理切换与重试中间件单个 IP 抓几千个商品后不管 Cookie 多干净还是会被盯上。常见做法是挂代理池请求进来时从池子里随机取一个代理放到request.meta[proxy]。代理失效和触发验证码要分开处理前者靠 Scrapy 自带重试后者需要换代理后重新入队。class JdProxyRetryMiddleware: def process_response(self, request, response, spider): if response.status ! 200: return response # 命中风控页面换代理重放 if 滑块 in response.text or verify.jd.com in response.url: spider.logger.warning(risk control, switch proxy: %s, request.url) new_request request.copy() new_request.dont_filter True new_request.meta[proxy] spider.proxy_pool.get() return new_request return responserequest.copy()会保留原请求的 headers、meta 和 body确保重放时字段不回丢。dont_filterTrue在这里是关键否则调度器会把重放请求当成重复 URL 丢掉。代理池对象在 Spider 里初始化成简单的列表即可生产环境可以换成 Redis 队列多个节点共享同一份代理资源。代理池接入请求的写法同样在中间件里class JdProxyMiddleware: def process_request(self, request, spider): if hasattr(spider, proxy_pool): request.meta[proxy] spider.proxy_pool.get()参数建议值说明CONCURRENT_REQUESTS4~8每个代理 IP 的建议并发上限不超过 8DOWNLOAD_DELAY1.0~2.5随机区间避免固定节拍RETRY_TIMES2代理连接失败时的重试次数RETRY_HTTP_CODES[403, 500, 502, 503, 504]除 403 外500 系错误也值得重试DOWNLOAD_TIMEOUT20慢代理直接超时换下一个这些参数在启动时可以先跑 200 个商品做压力测试。观察指标不是抓取速度而是“价格接口空返回率”和“验证码出现次数”。两者如果明显上升就把 CONCURRENT_REQUESTS 再降一档。5. 动态价格与数据校验京东爬虫的最后一公里5.1 价格接口的坑Referer 与空返回第 3 章的价格接口写法能用但要注意两个场景。第一页面上的到手价可能包含优惠券、满减等叠加逻辑p.3.cn接口只返回基础价和划线价这时候要做的是把接口返回值和页面表现分开存不要直接把p当最终售价。第二价格接口并发拉太高时经常返回空数组。遇到空返回不要立刻重试等 3 到 5 秒再单独补采连续三次空返回就换代理。5.2 需要渲染时再开 Playwright有些促销信息只在商详页动态渲染后才出现在 DOM 里常见做法是用 scrapy-playwright 桥接。在 settings 里启用# settings.py 片段 DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor需要渲染的请求在 meta 里带上开关yield scrapy.Request( url, meta{ playwright: True, playwright_page_goto_kwargs: { wait_until: domcontentloaded, timeout: 15000 } } )Playwright 的代价是慢和吃内存所以只对拿不到价格字段的 SKU 开渲染正常情况下保持普通请求。渲染后可以执行response.css(.p-price::text).get()读取最终环节的到手价和价格接口的值做对照差值超过 0.01 就以页面渲染值为准。5.3 数据校验的三个动作落库前在 Pipeline 里做三道校验sku_id 非空且是纯数字字符串price 大于 0 小于 1000000超出范围说明解析错位ext_time 不能比当前时间晚超过 5 分钟。价格快照表按 sku_id ext_time 建唯一索引同一秒内的重复写入直接忽略。最后在日志里输出成功率指标用“价格成功回填数 / 商品总数”低于 95% 时触发一次补采。这套校验做完这套 Scrapy 京东爬虫才算真正可交接。本文还有配套的精品资源点击获取
返回列表