1. 项目缘起:为什么我们要自己动手抓评论?

大家好,我是老张,一个在数据分析和电商圈子里混了十来年的“老油条”。今天想和大家聊聊一个非常实际的问题:当你需要分析一款产品的用户口碑时,你该怎么办?是手动去商品页面一页页翻看评论,还是等着别人给你现成的数据报告?我的答案是,都不如自己动手,用Python把数据“拿”过来。

你可能觉得,抓取数据听起来是程序员干的事儿,离我们运营、市场、数据分析师有点远。但说实话,在这个数据驱动的时代,能自己获取一手数据,就像自己会做饭一样,是一种非常核心的生存技能。特别是电商平台的用户评论,这里面藏着金矿:用户为什么喜欢这个产品?最常吐槽的点是什么?竞品的评价怎么样?这些信息对于产品迭代、市场定位、甚至是广告投放策略都至关重要。

京东作为国内主要的电商平台,其商品评论的质量和数量都非常可观。但平台本身并没有提供一个非常方便的一键导出功能。这时候,Python的requests库就成了我们的“万能钥匙”。它不像Scrapy那样庞大复杂,对于新手来说,requests足够简单直接,能让我们快速上手,把精力集中在数据本身,而不是框架学习上。今天,我就带你走一遍完整的流程,从找到数据接口,到写出健壮的代码,最后把几千条评论整整齐齐地存进CSV表格里。放心,哪怕你之前只写过“Hello World”,跟着我的步骤,也一定能搞定。

2. 实战前的侦察:找到数据的“后门”

在开始写代码之前,有个至关重要的步骤,我称之为“侦察”。我们得先搞清楚,京东的评论数据藏在哪里,以及它以什么形式提供。直接去解析网页HTML代码(也就是常说的“爬网页”)是一种方法,但这种方法既笨重又脆弱,页面结构一变,代码就得重写。更聪明的方法是找到网站背后提供数据的API接口,也就是数据的“后门”。

怎么找到这个“后门”呢? 这里就要用到浏览器的“开发者工具”了。以Chrome浏览器为例,你打开任意一个京东商品页面,比如某个手机的详情页,然后按下键盘上的 F12 键。这时浏览器旁边或底部会弹出一个面板,这就是我们的“侦察兵指挥部”。

  1. 在开发者工具顶部,找到并点击 “Network” (网络)标签页。
  2. 然后,在商品页面上,找到“商品评价”区域,点击“只看当前商品评价”或者尝试切换不同的评分筛选(比如“好评”、“中评”、“差评”)。
  3. 此时,你的眼睛要紧盯“Network”面板。你会看到一大堆文件在刷屏,这些就是页面加载时请求的各种资源,比如图片、样式表、JavaScript文件,还有最重要的——数据接口。

我们的目标是找到那个名字看起来像commentproductPageComments的请求。通常,它的类型(Type)是xhrfetch。当你点击它,在右侧的“Headers”(标头)选项卡里,你能看到完整的请求网址(Request URL)。我敢打赌,你十有八九会看到一个类似这样的链接: https://club.jd.com/comment/productPageComments.action?...

这个链接,就是我们梦寐以求的API接口地址。在“Preview”(预览)或“Response”(响应)选项卡里,你能直接看到这个请求返回的数据,通常是清晰明了的JSON格式,就像下面这样的一小段:

{
  "comments": [
    {
      "nickname": "j***d",
      "score": 5,
      "content": "手机很好用,运行流畅,拍照清晰",
      "productColor": "星光色",
      "creationTime": "2023-10-27 10:30:00",
      "images": [...]
    },
    // ... 更多评论
  ],
  "maxPage": 100,
  // ... 其他信息
}

看到没?数据已经结构化地摆在这里了,我们要做的,就是写个程序,模拟浏览器去“调用”这个接口,然后把返回的JSON数据解析出来。这比从混乱的HTML标签里大海捞针要高效、稳定一万倍。这个侦察过程,是任何数据抓取项目成功的第一步,也是最能体现“实战”经验的地方。我见过太多新手一上来就对着HTML硬啃,结果事倍功半。

3. 搭建你的数据流水线:核心代码逐行拆解

侦察完毕,拿到了接口地址和参数规律,接下来就是搭建我们的自动化数据流水线了。我会把完整的代码拆解成几个功能模块,就像搭积木一样,一块块给你讲明白。

3.1 准备工具箱:导入必要的库

工欲善其事,必先利其器。我们不需要很复杂的工具,四个Python标准库就足够了。打开你的代码编辑器(比如VS Code、PyCharm或者哪怕只是记事本),新建一个Python文件,比如叫jd_comment_crawler.py,然后在文件开头写下:

import requests  # 核心武器,用于发送HTTP请求
import csv       # 数据搬运工,负责读写CSV文件
import json      # 翻译官,把接口返回的JSON字符串变成Python字典
import time      # 节奏控制器,防止请求太快被限制

这里重点说一下requests库,它不是Python自带的,需要额外安装。打开你的命令行(Windows上是CMD或PowerShell,Mac/Linux上是终端),输入下面这行命令,回车即可:

pip install requests

如果安装速度慢,可以试试国内的镜像源,比如清华的:pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

3.2 创建数据仓库:初始化CSV文件

我们要把抓下来的数据存起来,CSV格式是最通用、最方便用Excel打开查看的。这一步,我们创建文件并定义好表格的“表头”,也就是每一列叫什么名字。

# 打开(创建)一个名为‘jd_comments.csv’的文件,准备写入
# mode='w' 表示写入模式,如果文件存在会清空重写,不存在则创建
# encoding='utf-8-sig' 是关键!这样用Excel打开中文不会乱码
# newline='' 是为了避免在Windows系统下写入空行
with open('jd_comments.csv', mode='w', encoding='utf-8-sig', newline='') as f:
    # 定义我们想要保存的字段(表头)
    fieldnames = ['nickname', 'score', 'content', 'productColor', 'creationTime', 'imageCount']
    # 创建一个DictWriter对象,它知道如何把字典写成CSV的一行
    csv_writer = csv.DictWriter(f, fieldnames=fieldnames)
    # 把表头先写入文件
    csv_writer.writeheader()

注意,我用了with open(...) as f:的写法。这是Python的上下文管理器,它的好处是,无论程序正常结束还是中途出错,它都会自动帮我们关闭文件,防止数据丢失或文件损坏。这是一个非常好的编程习惯。

3.3 伪装成浏览器:设置请求头

直接用一个“裸”的Python程序去访问网站,对方服务器一眼就能看出来你不是正常用户,很可能会拒绝你的请求。所以我们需要进行简单的“伪装”,主要是设置User-Agent(用户代理),告诉服务器:“我是一个正经的Chrome浏览器”。

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

这个字符串可以从你之前打开的浏览器开发者工具的“Network”标签里,随便找一个请求,看它的Request Headers部分复制过来。记得偶尔更新一下版本号,让它看起来更“新鲜”。

3.4 组装请求参数:告诉接口你要什么

根据我们之前的侦察,京东评论接口需要一些参数。最核心的是productId(商品ID)和score(评分)。商品ID就在商品页面的网址里,比如 https://item.jd.com/100142621600.html, 那么100142621600就是它的ID。

base_url = 'https://club.jd.com/comment/productPageComments.action'

# 定义基础参数,这些是固定或循环的起点
params_template = {
    'productId': '100142621600',  # 替换成你想分析的商品ID
    'sortType': 5,  # 5表示按推荐排序,6按时间排序
    'pageSize': 10, # 每页请求多少条,最大可设30,但太大可能不稳定
    'isShadowSku': 0,
    'fold': 1
}

这里我把它定义为一个模板,因为page(页码)和score(评分)是需要我们在循环里动态变化的。

3.5 启动抓取循环:按评分和页码遍历

京东的评论可以按1-5分筛选。为了获取全面的数据,我们最好把所有评分的评论都抓下来。同时,每个评分下又有许多页。这就需要用到两层循环。

# 外层循环:遍历1星到5星评分
for score in range(1, 6):
    print(f'开始抓取 {score} 星评价...')
    # 内层循环:遍历每一页。这里先假设抓前5页,你可以根据需要调整
    for page in range(0, 5): # 注意:京东的页码通常从0开始
        print(f'    正在抓取第 {page+1} 页...')

        # 组合当前请求的参数
        params = params_template.copy() # 重要!复制模板,避免修改原模板
        params['score'] = score
        params['page'] = page

        # 发送GET请求,带上我们的伪装头和参数
        try:
            response = requests.get(base_url, params=params, headers=headers, timeout=10)
            response.raise_for_status()  # 如果状态码不是200,会抛出异常
        except requests.exceptions.RequestException as e:
            print(f'    请求失败: {e}')
            continue  # 跳过这一页,继续下一页

        # 解析返回的JSON数据
        try:
            json_data = response.json()
        except json.JSONDecodeError:
            print(f'    解析JSON失败,响应内容: {response.text[:200]}')
            continue

        # 检查数据是否有效
        if 'comments' not in json_data or not json_data['comments']:
            print(f'    第{page+1}页无评论数据,可能已抓完。')
            break  # 跳出当前评分的内层循环

        comments = json_data['comments']

这段代码有几个实战要点

  1. params_template.copy():在循环内修改字典前先复制一份。如果不复制,直接修改params_template,会导致下一次循环的参数是上一次修改后的结果,引发错误。
  2. 异常处理(try...except:网络请求充满了不确定性,服务器可能出错、网络可能中断。用try包裹起来,程序遇到错误不会崩溃,而是记录下错误并继续运行,这是编写健壮爬虫的基本素养
  3. 超时设置(timeout=10:告诉requests,如果10秒内没收到响应,就认为请求失败,避免程序无限期卡住。
  4. 数据有效性检查:解析完JSON后,先判断是否有comments字段以及它是否为空。如果为空,通常意味着这个评分下的评论已经抓取完毕,可以用break跳出当前评分的循环,提高效率。

3.6 提取与存储:从JSON到CSV行

拿到comments列表后,我们就可以遍历每一条评论,提取出我们关心的字段,并写入CSV文件。

        # 遍历当前页的每一条评论
        for comment in comments:
            # 构建要保存的数据字典
            row_data = {
                'nickname': comment.get('nickname', '匿名用户'),
                'score': comment.get('score', 0),
                'content': comment.get('content', '').replace('\n', ' ').strip(), # 清洗换行符
                'productColor': comment.get('productColor', ''),
                'creationTime': comment.get('creationTime', ''),
                'imageCount': len(comment.get('images', []))  # 计算图片列表的长度
            }
            # 将这一行数据写入CSV文件
            csv_writer.writerow(row_data)
            # 在控制台打印一下进度,让人安心
            print(f'      已保存: {row_data["nickname"][:5]}...的评价')

        # 礼貌性延迟,模拟真人操作,避免给服务器带来压力或触发反爬
        time.sleep(1.5)  # 休眠1.5秒

这里有几个细节处理

  1. 使用.get()方法comment.get('nickname', '匿名用户')的意思是,尝试从comment字典里获取nickname键的值,如果这个键不存在,就返回默认值‘匿名用户’。这能有效避免因数据字段缺失导致的程序崩溃。
  2. 数据清洗:评论内容content里可能有换行符\n,直接存入CSV会导致行错乱。我们用.replace('\n', ' ')把它替换成空格,再用.strip()去掉首尾空白字符。
  3. 计算图片数量:接口返回的images字段是一个列表,里面是图片信息。我们不需要具体图片链接(除非你要下载),只关心数量,所以用len()计算列表长度即可。
  4. 请求间隔(time.sleep:这是网络爬虫的道德和生存准则。快速连续的请求会对目标服务器造成压力,也极易被识别为恶意攻击而封禁IP。加上一个1到3秒的随机延迟,是友好且必要的。你可以用time.sleep(random.uniform(1, 3))让间隔时间更随机。

当所有循环结束,with open语句会自动关闭文件。这时,打开你的项目文件夹,就能看到一个名为jd_comments.csv的文件,用Excel或文本编辑器打开,里面就是整整齐齐的结构化评论数据了。

4. 从能用变好用:代码健壮性与功能扩展

上面的代码已经是一个可用的爬虫了。但“能用”和“好用”、“耐用”之间还有距离。在实际项目中,我们还需要考虑更多。

4.1 增强健壮性:应对各种异常

网络世界充满意外,我们的程序必须足够“皮实”。

  • 连接错误/超时:我们已经用try...except requests.exceptions.RequestException捕获了。
  • HTTP错误:我们用response.raise_for_status()处理了非200状态码。
  • JSON解析错误:用try...except json.JSONDecodeError捕获。
  • 关键字段缺失:用.get()方法提供了默认值。

但还可以更进一步,比如加入重试机制。当请求失败时,不是直接跳过,而是重试几次。

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# 创建一个带重试策略的Session对象
session = requests.Session()
retry_strategy = Retry(
    total=3,  # 总共重试3次
    backoff_factor=1,  # 重试等待时间间隔增长因子
    status_forcelist=[429, 500, 502, 503, 504],  # 遇到这些状态码才重试
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)

# 之后就用 session.get 代替 requests.get
response = session.get(base_url, params=params, headers=headers, timeout=10)

这个Retry配置会在请求失败(如遇到服务器内部错误500,或网关错误502)时,自动重试最多3次,并且每次重试的等待时间会逐渐增加(backoff_factor),避免“雪崩”式重试。

4.2 扩展数据字段:挖掘更多信息

基础的评论信息之外,接口往往还返回了更多有价值的数据,我们可以根据分析需求决定是否抓取。例如:

  • userLevelName: 用户等级(如“金牌会员”),可用于分析不同等级用户的评价倾向。
  • userClientShow: 用户购买渠道(如“来自京东APP”)。
  • afterDays / afterUserComment: 追评内容和时间,这对了解产品长期使用体验非常关键。
  • usefulVoteCount: “有用”点赞数,可以筛选出高质量评论。

你只需要在fieldnames列表和构建row_data字典时,把这些字段加进去就行。记得先用print(json_data)打印一下完整的返回数据,看看里面到底有什么宝藏。

4.3 实现自动翻页:不再限制抓取页数

我们之前的代码固定抓取前5页。但一个热销商品可能有成千上万条评论。如何实现自动翻页,直到抓完所有数据呢?关键在于解析接口返回的maxPage字段。

# 在成功解析json_data后
max_page = json_data.get('maxPage', 1)  # 获取最大页数,默认1
print(f'该评分下共有 {max_page} 页评论。')

# 然后将内层循环改为
for page in range(0, max_page):  # 从0遍历到 max_page-1
    # ... 原有的请求和解析代码 ...

这样,程序就会自动根据该评分下的总页数进行抓取,真正做到“一网打尽”。

4.4 数据清洗与预处理:让分析更高效

抓取下来的原始数据可能有些“脏”,在存入CSV前做初步清洗,能为后续分析省不少事。

  • 去重:有些接口可能在不同条件下返回重复数据。可以根据comment_id(如果接口提供)或结合nicknamecontentcreationTime生成唯一标识进行去重。
  • 文本清洗:除了处理换行符,还可以移除无意义的广告、统一表情符号(如将[微笑]统一替换为[表情_微笑])、过滤掉过短的无意义评论(如“好评”、“不错”)。
  • 格式标准化:将creationTime字符串转换为Python的datetime对象,方便后续按时间进行分析。

这些清洗步骤可以在构建row_data字典之后,写入CSV文件之前进行。

5. 让程序更“智能”:应对反爬与调度

如果你的抓取量很大,或者需要定期运行,就需要考虑更高级的策略。

5.1 应对反爬虫策略

网站为了防止数据被过度抓取,会设置反爬机制。除了我们已经做的设置User-Agent和添加延迟,还可能遇到:

  • IP限制:短时间内同一IP请求过多会被封。解决方案是使用代理IP池。你可以购买付费的代理服务,或者寻找一些免费的代理IP(但稳定性较差)。在requests.get中通过proxies参数设置。
    proxies = {
        'http': 'http://your_proxy_ip:port',
        'https': 'http://your_proxy_ip:port',
    }
    response = requests.get(url, headers=headers, proxies=proxies)
    
  • 请求签名验证:一些复杂的接口会对参数进行加密或签名。这需要你逆向分析前端JavaScript代码,找到加密算法并用Python实现。这是爬虫工程师的“深水区”,需要一定的JS逆向能力。
  • Cookie/Session验证:有些数据需要登录后才能查看。这时你需要先用requests模拟登录,获取有效的Cookie,并在后续请求中携带。可以使用requests.Session()对象来保持会话。

5.2 程序调度与日志

  • 定时运行:如果你需要每天定时抓取某商品的评论,可以使用系统的定时任务。在Linux上可以用cron,在Windows上可以用“任务计划程序”。更Pythonic的方式是使用scheduleAPScheduler这样的库在程序中实现定时。
  • 记录日志:把程序运行情况(开始时间、成功抓取多少页、遇到什么错误等)记录到文件里,而不是只打印在屏幕上,方便事后排查问题。Python自带的logging库就非常好用。
  • 增量抓取:如果你每天只关心新增的评论,可以记录上次抓取的最后一条评论的时间,下次只抓取这个时间之后的评论。这需要在代码中加入时间比对逻辑,并持久化存储这个“最后时间点”。

写一个能跑起来的爬虫脚本不难,但要让它在复杂的网络环境中稳定、可靠、长期地运行,并且友好地对待目标网站,这些“工程化”的思考和实践才是真正价值的体现。这也是我从无数个深夜调试和爬虫被封的教训中总结出来的经验。希望这些扩展思路能帮你把项目从“课堂作业”升级为“生产工具”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐