Python爬虫实战:京东商品数据采集与反爬处理全攻略 很多人第一次接触 Python 爬虫不是被教程里那些花里胡哨的案例吸引而是被一个非常具体、非常接地气的需求拉进来的我想抓一批商品数据拿来分析价格、监控竞品、或者做数据集。而“python 爬取京东 商品数据”这个需求几乎可以算是国内爬虫学习者的必经之路。京东的商品结构复杂、反爬机制多、页面改版频繁用它当练手对象比抓那种静态博客能学到的实战经验多得多。这篇文章我会把整套方案的完整链路拆开讲清楚URL 结构分析、请求头设计、页面解析、价格字段的异步处理、数据清洗与存储、以及高频出现的反爬拦截和异常排查。代码可以直接抄但更重要的是理解每一步为什么要这么写这样京东改版了、字段变了、验证严格了你也能自己跟上节奏。1. 整体设计与技术路线1.1 明确要抓哪些字段爬京东第一步不是写代码是先想清楚你要什么。这里的“商品数据”不是一个笼统的概念它至少包含两个层次列表页数据商品标题、商品链接、图片地址、价格、评论数、店铺名、好评率。详情页数据商品参数、规格、品牌、产地、上架时间、促销信息等。对于大多数场景列表页的数据已经够用了。比如做价格监控、竞品分析、销量预估核心字段就三个标题、价格、评论数。再加个店铺名和商品链接基本能覆盖 80% 的需求。详情页数据虽然丰富但请求量会放大十倍不止反爬风险也会急剧上升前期没有必要。我建议第一版只抓列表页先把链路跑通再考虑扩展。很多人一上来就想全字段采集结果被详情页的各种校验挡住了连列表页都没跑通心态直接崩了。1.2 技术方案的选择requests 还是 Selenium京东的页面渲染方式比较特殊。列表页的 HTML 是服务端渲染出来的也就是说你用 requests 直接请求搜索页 URL是能拿到商品标题、链接、店铺名这些核心信息的。但价格字段是异步加载的HTML 源文件里只有一个占位符真正的价格是页面加载后通过额外的接口请求拿到的。这就带来了两条技术路线方案优点缺点适用场景requests 正则/解析库速度快、资源占用小、可控性强需要手动处理价格异步加载、Cookie、反爬校验大批量采集、定时任务、服务器上运行Selenium / Playwright能拿到完整渲染后的页面不用关心异步逻辑速度慢、内存占用高、容易被检测小批量采集、页面结构复杂、调试阶段我个人推荐把 requests 作为主力方案。原因很简单速度快、可控性强而且当你把异步价格的逻辑搞明白之后对 HTTP 请求的理解会上一个台阶。Selenium 不是不能用但它在并发场景下的表现很差跑个几百条数据还行上万条数据你会想哭。另外还有一个折中方案——使用 requests 获取 HTML再用 Selenium 只处理那些真正需要渲染的页面。但这是后话第一版不要搞这么复杂。1.3 整体架构流程整条采集链路的流程可以概括为构造搜索页 URL带上搜索关键词和分页参数。携带合理的请求头通过 Session 发起 GET 请求。用解析库提取 HTML 中的商品标题、链接、店铺名等静态字段。通过异步接口或页面内嵌的 JSON 数据补充价格字段。数据清洗、去重、格式化。写入 CSV / JSON / 数据库。设置延时和异常处理循环翻页。这套流程虽然简单但每一步都有坑。接下来我会一个环节一个环节地拆。2. 页面结构与字段定位2.1 列表页 URL 的结构拆解京东搜索页的 URL 长这样https://search.jd.com/Search?keyword手机encutf-8注意京东的搜索 URL 不止这一种格式。你直接在搜索框输入关键词可能会跳转到带wq参数的地址在搜索结果页翻页时URL 会变成https://search.jd.com/Search?keyword手机encutf-8page3s56log...这里的page参数是页码但你需要留意一个细节——京东的翻页是跳页式的page1和page3之间可能有内容重复或字段差异而且s参数代表起始条数。实际操作时我习惯在浏览器里手动翻几页观察 URL 的变化规律再在代码里动态拼接参数而不是死记硬背一套模板。还有一点容易被忽略keyword参数需要 URL 编码。中文关键词直接拼进去会报错或返回空结果要用urllib.parse.quote或者parse.urlencode处理。2.2 列表页 HTML 里到底有什么请求搜索页拿到的 HTML核心商品的 DOM 结构是li.gl-item。一个典型的解析思路是商品标题li.gl-item .p-name a em的文本内容。商品链接li.gl-item .p-name a的href属性。店铺名li.gl-item .p-shop a的文本。评论数li.gl-item .p-commit strong的文本。图片li.gl-item .p-img img的src或>{pid:123456,p:6999.00}但京东对这块做了加密和混淆目前比较常见的方式是页面加载后请求一个专门的价格接口传入商品 ID 列表返回对应的价格信息。这里我给一个比较实用的思路用浏览器的开发者工具切到 Network 面板刷新搜索页筛选 XHR 请求找包含price关键字的请求看它的请求地址和返回结构。一般来说这个地址会带有skuId参数返回的格式有可能是 JSON 也有可能是 JSONP 回调函数包裹的结果。我实测下来建议直接尝试通过请求https://p.3.cn/prices/mgets?skuIdsJ_商品ID来获取价格这个接口返回的 JSON 数组里包含p字段就是价格。这个接口稳定性还可以但京东随时可能调整所以我在代码里做了多层降级优先用价格接口失败则尝试从页面内嵌 JSON 中提取再不行就跳过价格字段并打日志。提示接口地址和返回结构都可能会变核心是掌握“在 Network 面板里定位异步请求并模拟它”这个思路而不是背接口地址。2.4 商品 ID 的提取方式价格接口需要一个参数商品 ID。这个 ID 实际上就在商品链接里。京东的商品链接格式是https://item.jd.com/100012345.html中间的那串数字就是skuId。所以在解析列表页时拿到商品链接后用正则提取数字即可import re sku_id re.search(r/item/(\d)\.html, url).group(1)这个 ID 不仅可以用来查价格后续如果要扩展详情页采集也是用这个 ID 拼 URL。一个字段可以串联好几个环节这是爬虫设计里常见的信息复用思路。3. 实操过程与核心代码实现3.1 请求头的设计与 Session 维持京东对请求头的校验比其他电商网站更严格其中三个字段是必须认真设置的User-Agent不能太旧也不能太新选一个主流浏览器的近几个大版本即可。Referer搜索页的请求Referer 可以设置为首页或搜索页本身。Cookie第一次请求最好带上一个浏览器里复制出来的 Cookie后续几次请求用 Session 维持。Session 的好处在于会自动保存服务端返回的 Cookie并且后续请求会自动附带。这模拟了一个“连续浏览”的浏览器行为比每次独立请求更像真人。import requests from urllib.parse import quote session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.jd.com/, })注意Cookie 不建议写死在代码里。它会有有效期而且不同账号的 Cookie 不一样。最好的做法是把 Cookie 放在配置文件里失效了更新配置即可不用改代码。3.2 搜索页请求与翻页逻辑搜索页请求本身不复杂但要注意几个细节关键词要编码、翻页参数要动态生成、每抓完一页要做一次随机延时。def fetch_search_page(session, keyword, page): params { keyword: keyword, enc: utf-8, page: page, # s: (page - 1) * 30 1, } url https://search.jd.com/Search? .join( f{k}{quote(str(v))} for k, v in params.items() ) resp session.get(url, timeout10) resp.encoding utf-8 if resp.status_code 200: return resp.text return Nonequote(str(v))这种写法是先把参数统一转字符串再编码避免中文和特殊字符出问题。这里我没有固定加s参数因为京东对这个参数的处理有点玄学有时候不加也能正常翻页。建议你在调试的时候对着浏览器实际观察一下该加的加上不该加的别乱加。3.3 页面解析从 HTML 中提取核心字段解析用lxml或者BeautifulSoup lxml解析器都行我习惯用lxml直接写 XPath速度更快。但考虑到很多读者对 BeautifulSoup 更熟下面的示例用 BeautifulSoup 展示逻辑是等价的。from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(li.gl-item): title_node li.select_one(.p-name a em) if not title_node: continue title title_node.get_text(stripTrue) link_node li.select_one(.p-name a) link https: link_node[href] if link_node else shop_node li.select_one(.p-shop a) shop shop_node.get_text(stripTrue) if shop_node else commit_node li.select_one(.p-commit strong) commit commit_node.get_text(stripTrue) if commit_node else 0 sku_id if link: m re.search(r/item/(\d)\.html, link) if m: sku_id m.group(1) items.append({ title: title, link: link, shop: shop, commit: commit, sku_id: sku_id, }) return items这里有个重要的经验li.gl-item是商品的唯一容器节点解析一定要限定在这个范围内不要直接全局搜索.p-name否则会把广告位、推荐位的数据也混进来。3.4 价格字段的补全方案价格接口返回的数据一般长这样[{p: 6999.00, op: 7999.00, id: 100012345}]p是当前售价op是原价或划线价。批量请求价格时可以一次传多个商品 ID用逗号拼接减少请求次数。def fetch_prices(session, sku_ids): if not sku_ids: return {} sku_str ,.join(fJ_{sid} for sid in sku_ids) url fhttps://p.3.cn/prices/mgets?skuIds{sku_str} headers { Referer: https://search.jd.com/, User-Agent: session.headers.get(User-Agent), } try: resp session.get(url, headersheaders, timeout8) data resp.json() except Exception as e: print(f价格接口请求失败: {e}) return {} return {item[id].replace(J_, ): item.get(p) for item in data}拿到价格之后把解析出来的商品列表和价格字典合并。items parse_list_page(html) sku_ids [item[sku_id] for item in items if item[sku_id]] price_map fetch_prices(session, sku_ids) for item in items: item[price] price_map.get(item[sku_id], )这里面的细节是sku_ids列表最好控制在 30 个以内因为京东这个接口单次支持的 sku 数量有限制超出部分可能被丢弃。如果你做了列表去重实际商品数可能会少于 30。3.5 数据清洗与存储原始解析出来的字段比较脏常见的问题有三类评论数是“2万”“5000”这种带加号或中文单位的格式。标题首尾有空白字符或中间有冗余空格。价格字段可能是字符串空值也可能是带逗号的文本。清洗逻辑可以写成一个独立的函数def clean_commit(text): if not text: return 0 text text.replace(, ).strip() if 万 in text: return int(float(text.replace(万, )) * 10000) try: return int(text) except ValueError: return 0 def clean_price(text): if not text: return None try: return float(text.replace(,, )) except ValueError: return None存储方面第一版用 CSV 就足够了。但需要注意写入文件时encodingutf-8-sig不要用utf-8。因为utf-8编码的 CSV 用 Excel 打开会乱码utf-8-sig带 BOM 头Excel 兼容性最好。import csv def save_to_csv(data, filenamejd_products.csv): if not data: return fieldnames [title, price, shop, commit, link] with open(filename, a, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if f.tell() 0: writer.writeheader() for row in data: writer.writerow({k: row.get(k, ) for k in fieldnames})3.6 完整主流程把上面的模块串起来完整的主流程代码是这样的import time import random def main(keyword手机, max_pages10): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Referer: https://www.jd.com/, }) all_items [] for page in range(1, max_pages 1): print(f正在抓取第 {page} 页...) html fetch_search_page(session, keyword, page) if not html: print(f第 {page} 页请求失败) time.sleep(random.uniform(3, 6)) continue items parse_list_page(html) if not items: print(f第 {page} 页没有解析到商品可能被反爬拦截) break sku_ids [item[sku_id] for item in items] price_map fetch_prices(session, sku_ids) for item in items: item[price] price_map.get(item[sku_id], ) all_items.extend(items) print(f第 {page} 页抓取 {len(items)} 条数据) time.sleep(random.uniform(3, 8)) save_to_csv(all_items) print(f总共抓取 {len(all_items)} 条数据)这里我留了两个随机延时的点位页面请求结束后和翻页前。延时的作用不只是礼貌更是把请求频率降到一个人手动浏览的水平。爬虫被风控拦截绝大多数时候不是因为你伪装得不够好而是因为你的请求频率太快了。3.7 关于 Selenium 的补充方案如果你的需求里包含反爬严重的关键词或者 requests 方案被验证码拦截到无法使用可以准备一个 Selenium 备选方案。核心思路是用 Selenium 打开搜索页。等待页面完全加载特别是价格渲染完成。从driver.page_source里直接解析整个渲染后的 HTML。使用无头模式Headless减少资源占用。Selenium 的缺点是慢但作为兜底方案应付小批量采集足够了。有一类情况特别适合用 Selenium——你需要采集多个筛选条件下的数据比如价格区间筛选、品牌筛选这些操作涉及 URL 参数和页面交互用 requests 模拟会比较麻烦Selenium 直接点击操作反而简单。4. 常见问题与排查技巧实录4.1 HTTP 状态码速查表爬京东过程中你会遇到各种状态码每个状态码代表的情况不一样状态码含义应对思路200正常返回直接解析301 / 302重定向检查 URL 是否正确Cookie 是否过期403拒绝访问检查 UA 和 Referer降低频率等待一段时间418我是茶壶京东风控拦截大概率是 Cookie 失效或频率过快503服务不可用请求太频繁建议暂停并拉长延时418 是很多新手会遇到的状态码看起来像开玩笑但实际上是真实的风控回应。遇到 418你要做的不是疯狂重试而是停下来看看自己的 Cookie 是否有效、请求频率是否过高、IP 是否被标记。4.2 反爬拦截的实际表现与应对很多人不知道反爬拦截是什么样的以为只有验证码才是拦截。实际上京东的风控有几个层次轻量拦截返回空列表页页面里没有商品节点或商品节点数量为 0。中等拦截返回验证码页面HTML 里出现滑块验证相关的关键词。重量拦截直接对 IP 进行短时间封禁。我之前遇到过一种比较隐蔽的拦截——请求能正常返回 200页面标题和完整的 HTML 框架都在但商品列表区域只剩一条广告推荐li.gl-item节点完全消失。这种情况如果没有日志记录很容易被误判为“解析代码写错了”。排查思路是这样把响应保存成文件检查其中是否包含商品关键词。如果商品关键词还在说明页面正常如果只剩下推荐和广告内容基本可以确定是被风控了。应对策略从轻到重依次是降低抓取频率单次请求间隔拉到 5 秒以上。更换更完整的 Cookie模拟登录状态。检查 IP 是否被限制必要时考虑更换出口 IP。4.3 请求超时和连接重置爬虫跑起来之后最常见的异常是 requests 的Timeout和ConnectionError。这不一定是你被拦截了也有可能是网络波动、DNS 解析慢、或者代理不稳定。我的建议是给所有请求都加超时参数同时写一个带重试的请求函数def request_with_retry(session, url, max_retries3, **kwargs): for i in range(max_retries): try: resp session.get(url, timeout10, **kwargs) if resp.status_code 200: return resp time.sleep(2) except (requests.ConnectionError, requests.Timeout): time.sleep(2 ** i) return None重试间隔用指数退避第一次等 1 秒、第二次等 2 秒、第三次等 4 秒。这种策略在网络抖动时很管用但也能避免在风控拦截时狂打接口。4.4 商品数量对不上的已知原因如果你抓到的商品数量比页面显示的少大概率不是代码问题广告位和推荐位混在列表里解析容器不干净。同一商品出现多种 SKU页面会以不同条目的形式展示。翻页过程中出现重复数据部分商品会在相邻页码中重复出现。针对重复问题最简单有效的办法是维护一个已采集的sku_id集合采集前做一次去重过滤seen_ids set() def is_duplicate(sku_id): if sku_id in seen_ids: return True seen_ids.add(sku_id) return False这里的seen_ids建议同时做内存去重和文件去重。如果采集任务跨天执行还需要把历史 sku_id 持久化到数据库或 JSON 文件否则第二次运行时会把前一次的数据再抓一遍。4.5 代码报错优先排查这三处每次我在群里看到有人把报错日志贴出来80% 都是同样的问题解析器未安装lxml没装BeautifulSoup 指定lxml解析器直接报错。换成标准的html.parser能跑但速度会慢一些。更建议直接安装pip install lxml。字段不存在报错.select_one()返回None再取.text就抛异常。建议先判断节点是否存在或者用get_text前加空值判断。JSON 解析失败价格接口返回的不是 JSON是被验证码页面包了一层 HTML。这种情况说明被拦截了不是数据格式变了。5. 工程化扩展与合规提醒5.1 从一次性脚本到定时任务如果你的采集需求比较“长期”比如每天跑一次价格监控那一次性脚本就不够用了。需要做三件事第一把关键词、页数上限、延时范围放到配置文件里避免每次改代码。第二加入增量采集逻辑。每天只采集价格变动的商品而不是全量重爬。增量采集的核心是维护一个商品 ID 到历史价格的映射表当天采集到新价格后和历史值对比变化超过阈值才输出。第三任务日志要完整。每次运行结束输出本次抓了多少条、成功多少条、失败多少条、耗时多少秒。这些信息在排查问题时极其重要。5.2 数据存储升级从 CSV 到 SQLite数据量达到几千条之后CSV 就不好用了。查询、去重、按价格排序都麻烦。这时候换成 SQLite 是一个性价比极高的方案无需额外安装数据库服务Python 内置支持。import sqlite3 conn sqlite3.connect(jd_products.db) conn.execute( CREATE TABLE IF NOT EXISTS products ( sku_id TEXT PRIMARY KEY, title TEXT, price REAL, shop TEXT, commit_count INTEGER, link TEXT, crawl_date TEXT ) )这里把sku_id设为主键天然实现了去重功能。重复采集时用INSERT OR REPLACE或INSERT OR IGNORE处理既不会产生重复数据又能保留历史记录。5.3 关于合规的几点提醒爬虫这个领域技术本身是中性的但使用场景需要自律。我在这里多说几句我的真实经验不要抓取用户个人信息、订单数据、评论者账号信息这类涉及隐私的数据。商品名称、价格、销量这些公开的商品数据用于学习和研究通常没问题但不能用于商业牟利、恶意竞争。控制访问频率不是示弱是自我保护。爬虫是一个长期工程不是一次性的“薅羊毛”。你设计延时、处理重试、监控日志本质上是在控制风险。很多爬虫项目死掉不是代码不行是太贪心频率压得太高最终导致 IP 被限制整个项目搁浅。遵守网站的 robots 协议尊重平台规则。遇到验证码说明对方已经明确不愿意让你继续爬了这时候的合理做法是停止而不是去研究怎么绕过验证码。5.4 这个项目还能怎么扩展爬取了京东商品数据之后能做的大方向有三个价格监控每天定时采集同一批商品的价格生成价格走势图用于判断促销时机。市场分析分析某个品类的商品价格分布、店铺份额、评价数量区间用于辅助选品决策。数据运营把商品数据入库之后结合销量预测模型分析爆款规律。这已经是一个完整的数据分析项目了。无论走哪个方向都要注意数据的时效性。商品价格、评价数、店铺活动变化很快一周前的数据可能就没有参考价值了。给每条数据打上采集时间戳是数据仓库的基本素养。写在最后我做了这么多年爬虫最大的体会是爬虫的技术难点从来不是“怎么发出请求”而是“怎么长期稳定地拿到数据”。京东是一个很好的练习场它的页面结构复杂、异步逻辑多、风控体系完整你把京东爬明白了国内大部分电商网站的采集思路都能举一反三。最后分享一个实用小技巧在写爬虫代码之前手动在浏览器里完整走一遍流程同时打开开发者工具的 Network 面板观察每一个请求的顺序和参数变化。这一步花不了 10 分钟但能让你少写 2 小时的废代码。爬虫不是猜谜游戏所有答案都在浏览器里摆着呢就看你会不会看。