Python爬虫实战:详解京东商品价格与详情页抓取流程 聊到Python爬虫绕不开的一个经典练手项目就是电商商品信息的抓取。我这次写的是京东商品价格及详情页抓取属于一个偏入门的实战场景目标是输入一个商品链接或SKU自动拿到标题、价格、店铺、评价等基础信息再整理成结构化结果。这类项目非常适合正在学Python、刚接触爬虫但不知道从哪开头的人也适合做比价工具、价格监控、竞品分析前先练一练数据采集基本功。为什么选京东而不是淘宝或拼多多因为京东的商品页URL规则非常简单清晰价格信息又有相对稳定的接口对新手友好能让你把注意力放在“请求-解析-存储”这条主线上而不是一开始就陷进各种加密参数里。本文会以我的实际抓取过程为线索带上环境准备、请求头伪装、HTML解析、价格接口调用、批量采集以及遇到302、滑块验证码、编码乱码时的排查经验。每段都按“我当时是怎么想的后来怎么做的”来写尽量不说废话。1. 项目整体设计与思路拆解1.1 先搞清楚数据到底在哪很多新手写爬虫拿过URL就requests.get然后把HTML打印出来发现页面里怎么找不到价格这是因为现在的电商页面早就不是一次HTTP请求能拿全所有数据的结构。京东商品页里的标题、图片、店铺信息有一部分在服务端渲染的HTML里但价格、促销、配送、库存这类实时性很强的字段通常是通过额外的XHR接口异步加载再由前端JavaScript填充到页面上的。我刚开始做这个项目的时候第一件事不是写代码而是打开浏览器的开发者工具F12切到“Network”面板清空请求记录再刷新一次商品页。你会看到页面发起了几十个请求按类型过滤一下只留XHR或Fetch就能看到一堆JSON接口。其中带price、stock、promotion这类关键字的请求就是我们要重点关注的。这个思路对整个爬虫生涯都通用不管目标网站长什么样先搞清楚数据源是渲染在HTML里还是通过接口返回。搞错这一步后面全是白干。1.2 HTML解析和接口调用怎么选理论上数据从HTML里能拿从接口里也能拿但两者的稳定性和成本差别很大。HTML解析的优点是“所见即所得”浏览器里看到什么HTML里一般就有对应节点。缺点是页面结构一旦改版选择器全要重写而且服务器返回的HTML里包含大量无关内容正经解析起来繁琐。接口方案则是直接拿到结构化JSON字段清晰、性能好、更新相对稳定但前提是你得先找到接口、搞清楚参数怎么带有的接口还会校验签名和Cookie。我的建议很直接详情页的基础信息比如标题、店铺名用HTML解析来做练基本功价格这类实时数据优先找接口拿数据因为价格接口通常返回的就是干净的数字用正则或JSON解析都很方便。这两种能力以后换个平台也能复用不算白学。1.3 合规意识先讲清楚写爬虫之前一定要明白边界。本文的代码和思路只用于个人学习、技术研究和少量数据验证不要用来大规模采集商业数据更不要拿去卖钱。京东的robots协议对于目录的爬取有限制实际生产环境中的数据采集还需要评估法律风险并严格遵守目标网站的访问协议。实际操作中我会主动控制请求频率比如每抓一个商品休息1到5秒一天总量控制在几十条以内。这不是“胆小”而是对自己和对目标服务器的基本尊重。爬虫技术本身是中性的用得好是效率工具用不好就是给自己找麻烦。2. 环境准备与核心依赖安装2.1 Python环境怎么搭最省事做Python爬虫我推荐直接用Python 3.8以上版本太老的版本在处理一些第三方依赖时会有兼容性问题。如果你还没装Python去官网下载安装包安装时记得勾选“Add Python to PATH”这一步虽然基础但踩坑的人特别多。装好之后我习惯先建一个独立的虚拟环境避免把系统Python搞乱。在项目目录下执行python -m venv venv然后激活环境。Windows下是venv\Scripts\activateMac或Linux下是source venv/bin/activate激活后提示符前面会出现(venv)说明已经进入虚拟环境了。接下来安装依赖就不会污染全局。2.2 用到的库和国内镜像源本次项目会用到的Python库不多核心就四个requests发HTTP请求爬虫入门必装lxml高性能HTML/XML解析库配XPath用beautifulsoup4另一个解析HTML的选择适合习惯CSS选择器的人pandas最后把抓到的数据整理成表格导出CSV或Excel时好用安装命令是pip install requests lxml beautifulsoup4 pandas国内用户如果下载慢建议临时指定清华镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests lxml beautifulsoup4 pandas这个镜像源地址是公开的速度比默认源快很多我至今还在用。如果你在的公司或学校有内部源那就更好了。2.3 先封装一个带点“伪装”的请求模板在正式写抓取逻辑前我建议先把请求和响应的基础封装好。不要每次请求都临时拼headers你会在调试时被自己乱写的代码搞疯。下面这段是我常用的小模板把超时、UA、重试都提前考虑好import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def make_session(): session requests.Session() retry Retry( total2, backoff_factor0.5, status_forcelist[500, 502, 503, 504] ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.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, }) return session session make_session()注意这里的Retry只对连接问题和5xx错误生效4xx不会自动重试。原因很简单4xx是请求本身有问题重试一万次也没用。3. 详情页抓取实操以标题、店铺、评价数为例3.1 拆解商品URL和SKU的关系京东的商品URL非常有规律长这样https://item.jd.com/100012043978.html最后那串数字就是商品的SKU ID是商品的唯一标识。只要你能拿到SKU列表就能批量构造URL这也是京东比较好抓的原因之一。反观淘宝的链接里带着一堆spm参数一个页面一个样新手看着就头大。我习惯在代码里把SKU作为入口参数而不是直接传整个URL这样后面批量抓取时只需要传ID列表就能跑通。3.2 请求商品页并确认响应状态用前面封装好的session去请求详情页sku_id 100012043978 url fhttps://item.jd.com/{sku_id}.html resp session.get(url, timeout(3, 6)) print(resp.status_code) print(resp.encoding) print(resp.apparent_encoding)这里有个小细节timeout我习惯传元组(3, 6)意思是连接超时3秒读取超时6秒。只传一个数字的话两个阶段用同一个值不够灵活。请求返回后先看状态码是不是200再看响应编码。京东页面返回的charset通常是utf-8但保险起见我会用resp.apparent_encoding来动态判断编码或者直接手动设置为utf-8然后再打印前500个字符确认一下HTML内容正常显示。这一步看着笨但能省掉后面一堆玄学乱码问题。3.3 用XPath提取标题和店铺名称HTML拿到手后解析方案我推荐lxml性能好XPath写起来也直观。先导入库from lxml import etree html resp.text doc etree.HTML(html)接下来是关键怎么找到标题节点我不会把选择器写得死板而是给你一套定位方法。旧版京东页面的标题在classsku-name的h1标签里店铺名称在classname的a标签里类似这样title_nodes doc.xpath(//h1[contains(class, sku-name)]/text()) shop_nodes doc.xpath(//div[contains(class, name)]/a/title)但页面结构是可能变的。所以我更推荐的做法是先在浏览器里右键目标元素选“检查”看它所在的DOM层级再回头写XPath。比如标题节点如果带class就优先用class定位如果层级很深可以稍微放宽一点条件用contains匹配部分类名降低改版带来的影响。还有一个很实际的问题提取出来的文本可能带换行和大量空格。京东页面尤其如此一个标题字符串里能塞进好多\n和空格。我写了段简单的清理逻辑def clean_text(text): return .join(text.split()) if text else text.split()会按空白字符切开再重新拼接这样能一次性清掉所有多余换行和空格很实用。3.4 评价数和详情图能拿多少拿多少评价数通常在页面上表现为“100万”这种格式在源码里经常出现在带comment相关class的节点里。你可以先搜索“评论”或“评价”这几个字再找附近的数字节点。这个字段不一定稳定所以我一般把它定位为“可选字段”拿不到就跳过不影响主流程。至于详情图京东的详情页主图在经过懒加载处理后很多时候img标签的src是一个占位符真正的图片地址在>[ { id: J_100012043978, p: 4999.00, m: 5999.00, op: 4999.00 } ]不同字段含义不一样p是促销价也就是用户实际看到的价格m是市场价经常被划掉的那个op是普通售价。以哪个为准取决于你业务怎么定义“价格”。如果只是监控成交价看p就对了。4.2 构造请求并解析价格JSON接口找到了代码就简单了def fetch_price(sku_id): price_url https://p.3.cn/prices/mgets params { skuIds: fJ_{sku_id} } resp session.get(price_url, paramsparams, timeout(3, 6)) resp.raise_for_status() data resp.json() if data: return data[0] return None用params传参requests会自动把参数拼到URL后面比自己手动拼字符串安全能正确转义特殊字符。返回后直接调resp.json()解析不用手动处理字符串。这个接口胜在返回稳定、速度极快而且支持一个请求传多个SKU这就是后面并发和批量采集的基础。实测一下单个SKU的响应基本在几十毫秒量级一次请求传10个SKU也是几十毫秒效率比逐个解析HTML高一个数量级。4.3 把详情页和价格拼成一条完整记录有了详情页信息和价格接口就可以把两者合并成一条结构化数据。我用一个字典来组织def crawl_one_sku(sku_id): detail {} # 详情页解析出的标题、店铺等 price fetch_price(sku_id) record { sku_id: sku_id, url: fhttps://item.jd.com/{sku_id}.html, title: detail.get(title, ), shop: detail.get(shop, ), price: price.get(p) if price else , market_price: price.get(m) if price else , } return record字段里有英文有中文但这不是给用户看的是给后续数据处理程序看的所以保持稳定和可预测才是最重要的。后面如果存数据库这个字典的key直接当列名即可。5. 批量抓取、限速与反爬应对5.1 多商品批量抓取怎么写才靠谱批量抓取前先准备好一批SKU ID。你可以手工维护一个CSV也可以用selenium之类从列表页自动提取但那是另一个话题。我这次用一个简单的SKU列表演示sku_ids [100012043978, 100008348542, 100012129956]批量抓的循环没什么花哨的关键是节奏控制。我写了一个带随机延时的版本import time import random for sku_id in sku_ids: try: record crawl_one_sku(sku_id) print(record) except Exception as e: print(f抓取 {sku_id} 失败: {e}) time.sleep(random.uniform(1, 3))随机延时是为了避免固定间隔的机械感因为固定间隔的请求在服务端看来更像机器行为。1到3秒的间隔对学习场景足够了。5.2 并发能不能上我建议先别急很多人在掌握了单请求之后就急着上并发觉得并发越高越快。理论上Python网络IO密集型的爬虫多线程确实能提速用concurrent.futures的ThreadPoolExecutor就能做from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers5) as pool: futures {pool.submit(crawl_one_sku, sku_id): sku_id for sku_id in sku_ids} for future in as_completed(futures): record future.result() print(record)但我的经验是新手阶段并发带来的收益远没有你想象的那么大反而会增加被封风险。京东对单IP的请求频率有一定限制一旦触发风控轻则请求变慢重则直接弹滑块验证码。先老老实实把单线程流程跑通观察一段时间确认没问题再一点点增加并发数。爬虫不是越快越好是越稳越好。如果后续真要大规模抓取要考虑的不只是线程池而是更完整的调度方案比如用Redis做队列、多节点分布式采集但那已经属于分布式爬虫的范畴了等把单机版跑利索了再去了解也不迟。5.3 遇到302跳转、验证码和IP限制怎么办批量抓的时候最大的拦路虎是各种风控。最典型的情况是请求发出去返回的不是200而是302跳转到一个验证页面或者直接弹一个滑块。我建议抓之前先在浏览器里打开一次商品页手动过一遍验证让浏览器留下正常访问的Cookie再把这些Cookie抓出来放到请求头里。requests会话第一次请求时带上这个Cookie很多时候就能顺利通过验证。但要注意Cookie是有时效的过期之后需要重新获取。如果连续请求被限制最直接的应对是退一步降低频率、增加延时、减少批次。至于IP代理确实是一种常见方案但学习阶段真不建议碰一是花费高二是代理池质量参差不齐三是容易依赖这些手段而忽略了更本质的反爬规避思路。再补充一点一旦触发了滑块验证码我个人建议直接停手不要傻傻地去破解验证码。模拟点击、轨迹识别这些方案从技术上讲可以做但涉及的安全合规边界非常模糊而且成本极高对学习项目来说完全不值当。把频率降下来或者过段时间再跑往往更实际。5.4 数据存储CSV是默认选项抓到的数据如果只打印出来那就白抓了。我会先用pandas整理成DataFrame然后导出CSVimport pandas as pd records [] for sku_id in sku_ids: records.append(crawl_one_sku(sku_id)) df pd.DataFrame(records) df.to_csv(jd_products.csv, indexFalse, encodingutf-8-sig)注意编码用utf-8-sig而不是utf-8这个细节一定要记住。utf-8-sig会在文件开头写入BOM标记Excel打开时才不会乱码。我早期用utf-8导出CSV发给朋友用Excel打开全乱后来才知道是编码问题。这个小坑很典型新手基本都遇到过。6. 常见问题与排查技巧实录6.1 问题速查表我把这个项目里容易踩的坑整理成了一个速查表遇到问题可以先对着查一遍。现象可能原因解决思路返回403 Forbidden请求头太少被识别为爬虫补全UA、Referer、Accept等请求头使用Session维持会话返回302跳转触发了风控或登录校验更换Cookie降低请求频率避免短时间高频访问返回HTML但找不到价格节点价格走异步接口返回按4.1节的方法去Network里找价格接口中文字符乱码编码判断错误设置resp.encoding utf-8或使用apparent_encoding取到的标题带一堆换行和空格页面源码包含大量空白字符用 .join(text.split())清理空白图片URL取不到图片在懒加载属性里尝试data-src、data-lazy-img等属性请求偶尔超时网络波动或服务器稍慢设置timeout(3,6)增加重试逻辑批量抓取时被封IP频率太高触发反爬策略降低并发、增加随机延时必要时换IP池页面改版后选择器失效页面结构变化重新打开开发者工具定位节点更新XPath6.2 一个典型的排查过程实录有一次我抓详情页所有商品都正常只有某一个SKU返回的标题是空的。我以为是选择器写错了但其他商品都能取到说明代码本身没问题。后来我拿这个SKU在浏览器里打开发现商品已经是“已下架”状态页面结构和其他正常商品完全不同标题位置变成了一个下架提示。这就引出一个重要经验爬虫代码要始终对异常情况保持宽容。下架的、缺货的、秒杀未开始的、错误页面的各种场景都可能在抓取过程中遇到。我的做法是给缺失值一个默认结果比如标题为空字符串而不是直接让程序崩溃。同时每抓一个商品就打印一条日志能第一时间定位是哪些SKU出了问题。6.3 关于页面改版的一点心态做爬虫一定要接受一个事实选择器不是一劳永逸的。网站前端改了class你的代码就废了。这不是你写得不对而是技术环境的常态。我之前维护过一个类似的采集脚本平均每两三个月就要更新一轮页面选择器。应对方式就是模块化。把页面解析的代码单独封装成一个函数不要和请求逻辑、存储逻辑混在一起。这样页面改版后只需要改解析那一小块逻辑其他部分不用动。这种代码组织思维比多会几个库重要得多。7. 我个人实操中的一个小习惯最后分享一个我自己一直在用的习惯拿到一个目标页面后先用curl或requests把页面源码完整下载下来存成一个HTML文件然后用浏览器打开这个本地文件配合开发者工具慢慢研究节点结构。这样做的好处是后面怎么改代码都不会因为页面原始内容丢失而抓瞎也方便对比网站前后改版时的结构差异。这个项目本身还有很多可以扩展的方向比如加上定时任务做价格监控、把数据落到MySQL里做后续分析、接入更友好的通知机制每一个方向都能延展成一篇文章。不过对于刚起步的人来说先把单页抓明白、把接口找清楚、把反爬应对形成肌肉记忆就已经是很大的进步了。爬虫这条路没有太多捷径多写、多调试、多看页面源码慢慢就有感觉了。