Python爬取京东商品评论实战:从API到反爬策略全解析(附完整代码)
从API到实战:深入解析Python获取京东商品评论的技术路径与策略
最近在做一个关于消费电子产品趋势分析的项目,需要大量真实的用户反馈数据。京东作为国内最大的电商平台之一,其商品评论无疑是一座数据金矿。但当我真正开始动手时,发现事情远比想象中复杂——京东并没有像淘宝那样提供相对容易访问的公开API,各种反爬机制也相当严密。经过几周的摸索、试错和优化,我总结出了一套相对稳定可靠的采集方案,今天就来和大家详细聊聊其中的技术细节和实战经验。
这篇文章主要面向有一定Python基础,但可能在网络爬虫领域经验不算特别丰富的开发者或数据分析师。我会从最基础的请求构造讲起,逐步深入到反爬应对策略、数据清洗存储,以及在实际操作中容易踩到的那些“坑”。无论你是想为自己的数据分析项目收集素材,还是单纯想学习现代电商平台的数据采集技术,相信都能从中获得实用的参考。
1. 理解京东评论数据的获取途径与技术选择
在开始写任何代码之前,搞清楚数据从哪里来、以什么形式存在,是至关重要的一步。京东的商品评论数据主要通过几种不同的技术路径暴露给前端,每种方式都有其特点和适用场景。
1.1 官方API接口:理想但受限的途径
京东确实提供了开放平台(Open Platform),理论上开发者可以通过申请成为合作伙伴,使用官方认证的API获取数据。这听起来是最正规、最稳定的方式,但实际操作起来门槛不低。
首先,你需要注册京东开放平台账号,完成企业认证(个人开发者权限非常有限),然后申请相应的API权限。以商品评论接口为例,即使申请通过,也会面临严格的调用频率限制和配额管理。对于小规模、非商业用途的数据分析项目来说,这个流程可能显得过于繁琐。
提示:如果你所在的公司或团队有与京东的正式合作需求,走官方API路线仍然是首选。虽然前期准备复杂,但长期来看数据稳定性和合规性最有保障。
从技术实现角度看,京东开放平台的API通常采用标准的OAuth 2.0认证流程,请求参数需要按照特定规则签名。下面是一个简化的请求结构示意:
# 京东开放平台API请求的基本框架(伪代码) import hashlib import time def generate_sign(params, app_secret): """生成京东API要求的签名""" # 1. 将所有参数按key排序 sorted_params = sorted(params.items()) # 2. 拼接成字符串 sign_string = app_secret + ''.join([f'{k}{v}' for k, v in sorted_params]) + app_secret # 3. MD5加密并转为大写 return hashlib.md5(sign_string.encode()).hexdigest().upper() # 实际调用时,需要构造包含method、timestamp、format等必需参数的请求 这种签名机制主要是为了防止请求被篡改,确保API调用的安全性。对于初学者来说,理解并正确实现签名算法可能是第一个小挑战。
1.2 网页端数据接口:最常用的实战方案
在实际的爬虫项目中,更多开发者选择的是分析京东网页端加载评论时调用的数据接口。这种方式不需要官方授权,技术门槛相对较低,但需要应对更复杂的反爬机制。
通过浏览器开发者工具(按F12打开),在商品评论页面滚动时观察Network标签,你会发现一个关键接口:
https://club.jd.com/comment/productPageComments.action 这个接口接收一系列参数,返回JSONP格式的数据。所谓JSONP(JSON with Padding),是一种为了解决跨域问题而诞生的数据格式。京东的接口返回数据大概长这样:
fetchJSON_comment98({ "comments": [...], "maxPage": 100, "productCommentSummary": {...} }) 我们需要做的,就是模拟浏览器发送相同的请求,然后从返回的文本中提取出真正的JSON数据。这种方法的最大优势是直接、无需认证,但稳定性完全取决于京东是否更改接口参数或返回格式。
1.3 第三方数据服务:快速但成本较高的选择
市场上还有一些专门的数据服务提供商,比如八爪鱼、集搜客等,它们已经封装好了京东评论采集的功能。你只需要在他们的平台上配置要采集的商品ID,就能通过API获取结构化数据。
这种方式适合以下场景:
- 对编程不熟悉,但需要快速获取数据
- 项目预算充足,可以接受付费服务
- 需要长期稳定、大规模的数据采集
不过,第三方服务的价格通常不菲,而且数据获取的实时性和完整性可能不如自己编写的爬虫灵活可控。
2. 构建稳健的请求与数据处理框架
确定了数据来源后,接下来就是搭建一个既高效又稳定的爬虫框架。这个部分我会分享一些在实际操作中积累的经验,特别是如何优雅地处理各种异常情况。
2.1 请求会话管理与头部信息优化
直接使用requests.get()进行单次请求当然可以,但在需要连续采集多页评论时,使用requests.Session()会带来明显优势。Session可以自动保持cookies,在某些需要登录态的场景下(虽然京东评论不需要登录也能查看)更加方便。
更重要的是,合理的请求头(headers)设置是绕过基础反爬检测的关键。京东的服务器会检查User-Agent、Referer等字段,过于简单或明显是爬虫的头部信息很容易被识别。
import requests import random class JDCommentScraper: def __init__(self): self.session = requests.Session() # 准备一组不同的User-Agent,轮流使用 self.user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0' ] def get_random_headers(self, product_id): """生成随机的请求头部""" return { 'User-Agent': random.choice(self.user_agents), 'Referer': f'https://item.jd.com/{product_id}.html', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'X-Requested-With': 'XMLHttpRequest' } 除了User-Agent轮换,我还发现Referer字段特别重要。京东的评论接口会检查这个字段,确保请求是从正确的商品页面发起的。如果Referer缺失或格式不对,可能会直接返回空数据。
2.2 参数解析与动态调整
京东评论接口的参数看起来不少,但真正核心的只有几个:
| 参数名 | 说明 | 示例值 | 注意事项 |
|---|---|---|---|
| productId | 商品ID | 100012014970 | 必须与URL中的商品ID一致 |
| page | 页码 | 0 | 从0开始计数 |
| pageSize | 每页条数 | 10 | 最大可设为100,但过大可能被限制 |
| score | 评分筛选 | 0 | 0表示全部,1-5表示对应星级 |
| sortType | 排序方式 | 5 | 5为默认排序,6为时间排序 |
在实际采集时,我建议将pageSize设为10或20,不要贪多。一方面,单次请求数据量过大会增加被识别为爬虫的风险;另一方面,如果某次请求失败,小批量数据也更容易重试。
score参数特别有用,当你只想分析好评或差评时,可以分别采集不同评分区间的评论。比如设置score=5只获取五星好评,score=1只获取一星差评。
2.3 健壮的异常处理与重试机制
网络爬虫最怕的就是不稳定——可能因为网络波动、目标服务器临时故障、或者触发了反爬机制而导致请求失败。一个好的爬虫框架必须有完善的异常处理和重试逻辑。
import time import json from tenacity import retry, stop_after_attempt, wait_exponential class RobustJDScraper(JDCommentScraper): @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def fetch_page_with_retry(self, product_id, page): """带重试机制的页面获取""" try: headers = self.get_random_headers(product_id) params = { 'callback': 'fetchJSON_comment98', 'productId': product_id, 'score': 0, 'sortType': 5, 'page': page, 'pageSize': 10, 'isShadowSku': 0, 'fold': 1 } response = self.session.get( 'https://club.jd.com/comment/productPageComments.action', params=params, headers=headers, timeout=10 # 设置超时时间 ) response.raise_for_status() # 如果状态码不是200,抛出异常 # 处理JSONP格式 if response.text.startswith('fetchJSON_comment98('): json_str = response.text[len('fetchJSON_comment98('):-2] data = json.loads(json_str) ret