
简介一款基于Scrapy框架的京东商品爬虫完整项目资料面向爬虫初学者、高校在校生及需要快速搭建项目演示的工程师重点解决电商数据采集、请求调度、管道存储等常见问题可直接作为毕业设计或课程设计参考。压缩包共二十七份文件含九份py源码、七份pyc编译缓存、四份zbak备份配置、四份xml工程配置、一份iml模块文件、一份cfg环境配置以及一份md说明文档包体仅约二十七KB整体结构精简便于对照Scrapy工作流逐模块学习。目前已有四十四人学习浏览。代码均经过测试运行成功并配有详细中文文档清晰展示项目结构、配置方式与扩展思路。阅读源码可掌握Spider编写、Item定义、Pipeline数据处理及下载中间件应用等核心技能也能通过修改settings、redis相关接入逻辑实现分布式抓取或增量更新适合在原有基础上二次开发快速完成课程作业或初期项目立项演示。1. 拆一个能支撑毕设答辩的 Scrapy 京东爬虫工程这个源码包里没有花哨的框架但把电商爬虫最核心的链路完整搭了出来add_category_to_redis.py负责把京东全部分类写入 Redis 任务队列jingDong/spiders从队列取任务、解析列表页和详情页pipelines.py只做清洗与入库middlewares.py管请求头轮换和失败重试。整套代码按 Scrapy 标准工程组织分类、商品、价格、评论路径各司其职。真正完整跑一遍之后你会发现它比网上只贴一个get_price.py的“京东爬虫教程”可用得多拿来做课程设计、毕业设计是能顶住导师提问的。下面按我拆这套代码的路径把每一层为何存在、参数怎么设、坑在哪讲清楚。2. 工程骨架拆解Scrapy 四层结构与各文件职责2.1 为什么是 Scrapy和 requests 脚本的差距在哪里很多人拿到项目源码后第一反应是问直接用requests抓京东列表页不就行了确实能抓但一旦任务从“爬 100 个商品”变成“爬全部分类下的商品”需要面对的是多级页面跳转、请求失败重试、去重、入库这些横切问题。用纯脚本写这些逻辑最终会形成大量try/except和全局变量调试成本极高。Scrapy 的工程化价值在于把网络请求调度统一交给引擎管理spiders/只做页面解析请求头、重试、入库全部由中间件和管道解决模块边界非常清楚。对于有 Java 背景的读者可以直接把这套工程映射到 Spring 风格的分层思想上。下面这张表是我带人看这类项目时固定的开场表格能快速建立全局认知也方便答辩时给导师讲清楚设计思路。Scrapy 模块本工程中的职责Java Web 类比settings.py并发数、下载延迟、中间件与管道注册application.ymlitems.py商品字段定义约束每条数据的结构DTO / VOmiddlewares.py请求头随机化、失败重试、动态页面切换Filter / Interceptorpipelines.py数据清洗、幂等去重、写入 MongoDBService DAOspiders/列表页解析、详情页构造Controller Parser后面几节会按这个表格逐层展开。先读settings.py不是因为它最简单而是因为中间件、管道、爬虫的开关全部汇总在这里把所有配置看一遍整个工程的运行方式就清楚了。2.2 settings.py并发、延时、重试参数怎么设打开jingDong/settings.py重点是上面这一组参数它们直接决定爬虫对目标站点的压力以及单机吞吐量。BOT_NAME jingDong SPIDER_MODULES [jingDong.spiders] NEWSPIDER_MODULE jingDong.spiders # 并发请求数单机建议 416再大容易触发风控 CONCURRENT_REQUESTS 8 # 下载延迟单位秒配合随机化之后实际间隔为 0.51.5 倍 DOWNLOAD_DELAY 1.2 RANDOMIZE_DOWNLOAD_DELAY True # 失败重试只在对应状态码出现时才生效 RETRY_ENABLED True RETRY_TIMES 3 RETRY_HTTP_CODES [403, 429, 500, 502, 503] DEFAULT_REQUEST_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.jd.com/, }CONCURRENT_REQUESTS表示同一时刻下载器最多发出的请求数这个值不是越大越好。电商站点对短时间高频请求很敏感几百个请求用同一特征头连续打过去很容易返回 302 跳转到验证页面爬虫的任务就断在种子阶段。DOWNLOAD_DELAY配合RANDOMIZE_DOWNLOAD_DELAY会把每次请求间隔随机化到 0.61.8 秒之间看起来更像真实用户的访问节奏。RETRY_HTTP_CODES里的 403 和 429 是典型的访问受限返回值。重试时要先让请求头换一次否则重试多少次结果都一样。这也是为什么中间件和重试逻辑必须配合使用单独调大RETRY_TIMES只会把日志刷得更长。2.3 middlewares.py请求头轮换与失败重试middlewares.py是本工程中下载器之前最关键的一道关卡它挂在每个请求发出之前负责改写请求头或直接返回新的请求。import random UA_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, ] class RandomUserAgentMiddleware: def process_request(self, request, spider): request.headers[User-Agent] random.choice(UA_LIST) return Noneprocess_request是下载中间件处理请求前的标准钩子函数这里做的事情是每次请求前随机替换 UA。京东页面里的 JS 有指纹采集逻辑长期使用同一个 UA 很容易在服务端被识别为异常。加上这段之后至少能把 403 的出现频率压下去一半。提示UA 轮换只是最基本的防御手段。如果之后发现请求头层面已经做到位仍然大量 302需要检查的是 Cookie 是否需要按抓包结果动态更新以及两次请求之间的延迟是否太短。不要一上来就调大并发那是反向优化。2.4 items.py 与 pipelines.py字段定义和 MongoDB 入库items.py定义商品数据的基本结构。字段在设计时就要想清楚因为管道和页面解析都会依赖它import scrapy class JingDongItem(scrapy.Item): sku_id scrapy.Field() # 商品唯一编号整条数据的主键 title scrapy.Field() # 商品名称 price scrapy.Field() # 当前售价浮点数 shop scrapy.Field() # 店铺名称 comment_count scrapy.Field() # 评价数 category scrapy.Field() # 商品所属三级分类sku_id是京东商品的全局唯一标识同一商品多次采集不会变所以管道中的去重和更新全部围绕它展开。价格和评价数不直接从详情页 HTML 取原因在本文第 4 章单独说明。pipelines.py里最核心的方法是process_item每个 item 从 spider 产出后都会经过这里。import pymongo class MongoPipeline: def __init__(self, mongo_uri, mongo_db, mongo_collection): self.mongo_uri mongo_uri self.mongo_db mongo_db self.mongo_collection mongo_collection classmethod def from_crawler(cls, crawler): return cls( mongo_uricrawler.settings.get(MONGO_URI, mongodb://127.0.0.1:27017), mongo_dbcrawler.settings.get(MONGO_DB, jd), mongo_collectioncrawler.settings.get(MONGO_COLLECTION, goods), ) def open_spider(self, spider): self.client pymongo.MongoClient(self.mongo_uri, serverSelectionTimeoutMS3000) def process_item(self, item, spider): if item.get(sku_id) is None: return item self.client[self.mongo_db][self.mongo_collection].update_one( {sku_id: item[sku_id]}, {$set: dict(item)}, upsertTrue, ) return item def close_spider(self, spider): self.client.close()update_one配合upsertTrue是本段的核心。第一次采集某个商品时MongoDB 里没有对应文档update_one直接插入第二次再爬到同一个sku_id时$set会把新价格、新评价数写进既有文档而不是新增一条脏数据。serverSelectionTimeoutMS3000是为了防止 MongoDB 未启动时爬虫进程长时间卡在连接等待上。管道要在settings.py里注册才会生效ITEM_PIPELINES { jingDong.pipelines.MongoPipeline: 300, }数值 300 代表管道执行优先级越小越先执行。如果后续要加清洗管道把清洗逻辑写在 300 之前、存储写在 300 之后即可。3. 从分类到商品Redis 种子数据如何驱动整个爬虫3.1 add_category_to_redis.py一次把全站分类刷进队列京东商品分类非常多且层级不一把分类链接写死在爬虫代码里是非常差的做法。add_category_to_redis.py独立于 Scrapy 工程之外运行它的作用是把京东全部分类链接一次性写入 Redis爬虫启动后从队列中弹出分类链接作为起始任务。这样分类变化时只需要重跑这个脚本爬虫代码不用动。import argparse import requests import redis from lxml import etree HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: https://www.jd.com/, } def fetch_categories(): # 京东全部分类页面结构变动时需要重新抓包分析 resp requests.get(https://www.jd.com/allSort.aspx, headersHEADERS, timeout10) root etree.HTML(resp.text) urls [] # 常见结构dl 是大分类dd/em/a 是具体分类入口 for link in root.xpath(//div[classitems]/dl/dd/em/a): url link.xpath(./href)[0].strip() if url.startswith(//): url https: url urls.append(url) return urls def main(): parser argparse.ArgumentParser(description推送京东分类到 Redis) parser.add_argument(--host, default127.0.0.1) parser.add_argument(--port, typeint, default6379) parser.add_argument(--db, typeint, default0) parser.add_argument(--key, defaultjd:start_urls) args parser.parse_args() r redis.Redis(hostargs.host, portargs.port, dbargs.db, decode_responsesTrue) r.delete(args.key) urls fetch_categories() # 每个分类链接封装成一个独立任务lpush 后由爬虫侧 rpop 消费 for url in urls: r.lpush(args.key, url) print(f分类总数{len(urls)}已写入 Redis{args.key}) if __name__ __main__: main()执行方式pip install redis lxml requests python add_category_to_redis.py --key jd:start_urls脚本里的lpush/rpop构成一个最简任务队列。r.delete(args.key)会先清空旧数据再写入保证队列里不会残留上一次的半截任务。如果后续把 Redis 换成其他消息队列改动点只集中在main()的最后几行与爬虫骨架完全解耦。提示allSort.aspx的页面结构一旦改版fetch_categories里的 XPath 就要同步调整。判断方法很简单跑完后打印分类总数正常应当在 2000 以上如果只有几十条先打开页面检查https://www.jd.com/allSort.aspx的 DOM 实际层级。3.2 spider 消费队列并生成详情请求分类链接进入 Redis 之后爬虫侧需要从队列中取出链接并继续往下解析。本工程的jingDong/spiders/目录下有一个核心爬虫文件内部处理逻辑可以分为两段解析列表页得到商品 ID再为每个商品 ID 生成详情页请求。import scrapy from scrapy_redis.spiders import RedisSpider from jingDong.items import JingDongItem class JdSpider(RedisSpider): name jd # 从 Redis 的 jd:start_urls 队列里弹出一条分类链接 redis_key jd:start_urls def parse(self, response): # 列表页京东商品列表的 li 标签带>SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter REDIS_START_URLS_KEY jd:start_urlsSCHEDULER替换后所有待抓取请求都会由 Redis 保存而不是保存在内存中爬虫重启也不会丢失任务。DUPEFILTER_CLASS用 Redis 做请求去重多个节点同时运行同一分类队列时同一个商品 ID 只会被其中一个节点抓取这是分布式爬虫的核心机制。项目中add_category_to_redis.py、scrapy-redis 配置与 MongoDB 管道三者配合形成了完整的“任务生成、任务消费、结果存储”闭环。对课程设计而言能把这个闭环讲清楚已经比单纯的“一个爬虫脚本爬了一百个商品”高出几个层级。4. 动态渲染与异步接口价格和评论数不在 HTML 里4.1 价格与评价数走 JSON 接口解析成本最低很多人在parse_detail里直接尝试用response.css(span.price::text)取价格拿到的是 None。原因很简单京东详情页的商品价格、评价数都是页面加载后通过 JS 异步请求再渲染进 DOM 的Scrapy 默认下载器拿到的 HTML 里根本没有这些值。针对这种情况我一般不会先上浏览器渲染而是先抓一遍页面 XHR 接口。京东价格和评价数其实各自都有公开的 JSON 接口价格接口返回的是价格区间和原价评价接口返回的是好评率与评价总数。在 spider 内直接用scrapy.Request请求这两个接口解析成本最低速度也最快。import json def parse_price(self, sku_id): # 价格接口skuIds 一次最多传 20 个这里只传一个 price_url https://p.3.cn/prices/mgets params {skuIds: fJ_{sku_id}} yield scrapy.Request( price_url, methodGET, callbackself.parse_price_result, cb_kwargs{sku_id: sku_id}, dont_filterTrue, ) def parse_price_result(self, response, sku_id): data json.loads(response.text) if data: price data[0].get(p) item JingDongItem(sku_idsku_id, priceprice) yield item注意dont_filterTrue同一商品价格接口的 URL 只有参数不同如果重复抓取最新价格必须关闭去重过滤器否则第二次请求会被RFPDupeFilter拦掉。评论数可以按同一思路处理对应的接口是commentSummaries系列返回 JSON 后解析CommentCount字段写入comment_count。4.2 iframe 与复杂交互页面用 playwright 兜底JSON 接口能解决大部分问题但总有些页面数据不是标准接口返回的而是嵌在 iframe 中动态渲染出来的。Scrapy 的response.body对 iframe 只返回标签本身不包含 iframe 内部的 DOM 树。网上讨论最多的组合是 scrapy playwright也就是把这个工程的第 4 层中间件能力继续向外延伸。# 独立的渲染辅助脚本供中间件或独立进程调用 from playwright.sync_api import sync_playwright def fetch_with_render(url, wait_selectordiv.sku-name): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untildomcontentloaded, timeout15000) # 等待目标节点真正出现在 DOM 中避免拿到半加载页面 page.wait_for_selector(wait_selector, timeout5000) html page.content() browser.close() return htmlwait_for_selector是关键参数它告诉 playwright 等到某个节点出现后再取 HTML而不是固定 sleep 3 秒。固定 sleep 在目标站点变慢时会拿到不完整页面还白白浪费时间。实际接入时不要把sync_playwright直接放进 spider 的同步方法里它会阻塞整个 Scrapy 引擎。常规做法是把渲染函数放进独立进程渲染完的 HTML 再通过 Redis 或文件回传给爬虫解析。4.3 并发与稳定性这套工程最合适的运行水位这里说“并发设计”到底哪种好取决于你的运行环境。Scrapy 的并发不是越高越好尤其当任务队列里有大量列表页和详情页混在一起时详情页请求权重更高反而更合理。下面是按这台工程常用参数整理的一组水位参考运行环境CONCURRENT_REQUESTSDOWNLOAD_DELAY预估吞吐笔记本 4 核 8G42.023 请求/秒单机 8 核 16G81.256 请求/秒分布式 3 节点每节点 81.01.51518 请求/秒实际跟踪方式可以在日志里观察item_scraped_count与downloader/request_count的比值。如果入库条数远小于请求数说明大量请求消耗在了重试或非商品页上此时要优先看中间件日志而不是继续调高并发。速率稳定跑几万条数据是这个工程经过验证的能力边界。5. 排错清单与答辩演示技巧5.1 最容易翻车的五个点这套工程在课程设计和答辩场景里最容易出问题的地方通常集中在环境与页面结构变化上。我把过去遇到的情况整理成下面这张排查表每个现象都能对到一个具体操作。现象可能原因定位方法与处理大量 302 跳转请求头不足或频率过高调大DOWNLOAD_DELAY确认 UA 轮换是否生效分类总数为 0allSort.aspx结构变动用浏览器打开分类页重新分析 XPathsku_id全部为空列表页改用其他属性标识保存一份response.body到本地检查MongoDB 连接超时mongod 未启动或 URI 错误先执行mongosh --eval db.runCommand({ping:1})Redis 连接拒绝bind 地址或密码不匹配redis-cli -h 127.0.0.1 ping验证基础连通性sku_id为空这一点值得单独强调。京东偶尔会对未登录用户的列表页返回简化版 HTML此时># 1. 确认任务队列已经被分类链接填满 redis-cli llen jd:start_urls # 2. 以前台模式运行爬虫日志里能看到请求成功率和 item 产出 scrapy crawl jd -s LOG_LEVELINFO # 3. 抓取结束后统计 MongoDB 中的商品文档数量 mongosh jd --eval db.goods.countDocuments()redis-cli llen的结果能直接回答“分类数据从哪来”scrapy crawl的日志能直观展示请求总数与成功抓取数countDocuments()则证明管道确实把数据落盘。三个命令之间用真实的返回数值衔接比任何架构图都有说服力。如果担心现场网络不稳定提前用scrapy crawl jd -s CLOSESPIDER_ITEMCOUNT50把采集量限制在 50 条配合LOG_LEVELINFO抓一份完整日志存好。答辩时即使现场页面改版也可以直接展示这份日志和 MongoDB 中已有数据。更稳妥的做法是预先用 OBS 录一段 3 分半的视频把“启动 Redis、执行种子脚本、启动爬虫、连接 MongoDB 查询”全流程录下来导师问到任何细节时定位到视频对应时间点回放比临时开终端重跑要稳得多。本文还有配套的精品资源点击获取