
简介面向电商数据采集与毕业设计场景这份拼多多商品及评论爬虫项目提供可运行的完整工程。基于Python与Selenium技术栈涵盖代理池、验证码处理、数据库存储、线程池调度等关键模块配有详细文档与源码注释适合计算机相关专业学生用于课程设计、毕业设计或爬虫学习进阶。资源包共31个文件以17个Python脚本为主另有Scrapy配置、依赖清单、状态文件和说明文档其中README提供使用说明requirements.txt列出依赖环境整体仅49KB轻量易部署。已有50人学习浏览项目代码通过测试运行评审得分95分。读者可从中掌握商品评论采集、反爬应对、数据落库的工程化写法并了解完整爬虫流程的实现思路便于在此基础上二次开发扩展更多电商平台功能也可替换为目标商品、评论维度或数据存储方式。1. 拼多多商品评论爬虫的边界能拿到什么、卡点在哪拼多多的商品评论不在首屏 HTML 里而是由前端异步请求一个 JSON 接口拿到的。直接拿 requests 请求商品详情页解析结果里看不到任何评论正文——这一步就劝退了不少刚开始写爬虫的人。真正决定一个评论爬虫能不能跑起来的是接口定位、签名参数和会话状态这三件事拿到就通拿不到就卡在原地。标题里说的“资料齐全、详细文档”落到执行层面其实是一份分层笔记抓包记录一份、签名处理一份、解析入库一份、排错决策一份。这篇文章就按这个顺序展开覆盖从定位接口到断点续抓的完整链路。适合已经会写 requests 爬虫、想啃带签名参数的电商评论场景的工程师对 5 年以上的人来说第 4、5 章的字段去重、状态落盘和风控决策表也值得过一遍。整篇按『抓包定位接口 → 签名处理 → 拉取与分页 → 解析入库 → 限速与排错』推进每个章节都有可直接抄的参数表和代码。先把链路摸清楚再动手写第一行代码。2. 先拆拼多多评论数据链路接口、签名与请求头2.1 用开发者工具定位评论列表接口先别写代码。打开无痕窗口访问https://mobile.yangkeduo.com/goods.html?goods_id你的商品ID把页面滚到评论区。按 F12 打开开发者工具切到 Network 面板把筛选条件设为 Fetch/XHR。点击“查看更多”或翻页触发一次新的评论加载观察新增的请求。请求列表里能看到一个明确的候选路径https://mobile.yangkeduo.com/proxy/api/api/reviews/list部分线上环境路径带版本前缀以你抓到的为准。这个接口返回 JSON里面按分页结构装着评论正文、评分、SKU 规格、图片列表和标签。与直接解析 HTML 相比接口方案字段干净、分页明确是公开项目里最常见的做法也是我一般会走的路线。有一点要提醒接口路径和参数名在历次迭代里调整过。复制网上的旧代码前先用自己抓到的请求核对一遍 Query String拿接口返回的 JSON 和页面评论逐条比对字段对应关系确认无误再进入下一步。2.2 anti_content 签名参数与两条实现路线请求的 URL 里除了goods_id、page、size、sort_type还有一个一长串不可读的参数anti_content。这个参数把关键业务参数和时间信息做变换后生成签名串主要作用是把高频爬取和异常调用挡在风控之外。第一次接触时不要试图一次性还原完整算法先选路线。路线 A从页面源码里把生成 anti_content 的 JS 函数抽出来本地用 PyExecJS 或 Node 子进程调用跟随平台算法更新缺点是每次都要维护一份 JS 文件。路线 B抓包时把签名串固定下来先跑通全流程用短线数据验证字段解析和入库逻辑之后再回头处理签名时效。第一版建议走 B把精力放在数据管道上。签名串过期是明确的报错特征不会干扰排错。注意 anti_content 有明显时效性通常几分钟到几十分钟内有效。常见做法是把它和 Cookie 一起从浏览器会话里搬运而不是每次请求都现场生成等整体流程跑通后再用 execjs 把签名函数脚本化。在开发者工具的 Sources 面板里全局搜索anti_content能直接看到是哪个 JS 方法在拼这个串。2.3 最少请求头与 Cookie 字段说明裸 requests 直接访问 reviews/list大概率返回 403 或者空列表。下面是一组最小请求头配合浏览器里正常访问评论区的会话 Cookie 使用User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1 Referer: https://mobile.yangkeduo.com/goods.html?goods_id你的商品ID Accept: application/json, text/plain, */*Cookie 里真正起决定作用的是下面几个字段Cookie 字段作用失效特征api_uid会话标识直接 403pdd_user_id用户身份评论接口返回空_nano_fp设备指纹风控概率上升anti_content接口签名校验失败这张表列的是公开资料里出现频次最高的字段并不是全部。更稳妥的做法是在浏览器 Application 面板里把当前域名下的 Cookie 整体导出成 header 字符串或者直接复制到代码里减少遗漏。提示签名参数和 Cookie 都取自你自己浏览器会话仅用于个人学习与数据分析不要做商业化采集或高频抓取。3. 用 requests 爬虫拉取拼多多评论数据最小代码与参数表3.1 先用 curl 验证 Cookie 与接口连通性写 Python 之前先用 curl 验证一遍环境。把商品 ID、Cookie、anti_content 替换成自己抓到的值curl https://mobile.yangkeduo.com/proxy/api/api/reviews/list?goods_id123456page1size10sort_type2anti_contentYOUR_ANTI_CONTENT \ -H User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) \ -H Referer: https://mobile.yangkeduo.com/goods.html?goods_id123456 \ -H Cookie: api_uidxxxx; pdd_user_idxxxx \ --compressed如果返回 JSON 且包含评论数组说明 Cookie 有效、签名还在时效内返回error_code或空数组直接换一组新抓的 Cookie 和 anti_content。这一步把环境问题前置排掉后面调 Python 时只面对业务逻辑。--compressed让 curl 接受 gzip 响应不加在某些环境下会拿到乱码或报错。实际响应里评论列表常见字段是reviews_list同时带total之类的统计字段。确认能拿到数据后把这个请求参数完整复制下来作为 Python 版本的对照基准。3.2 fetch_reviews 最小实现与五个参数说明下面是可运行的 requests 版本签名先用固定字符串占位跑通后再替换import requests REVIEW_API https://mobile.yangkeduo.com/proxy/api/api/reviews/list HEADERS { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15, Referer: https://mobile.yangkeduo.com/goods.html, Accept: application/json, text/plain, */*, } COOKIES { api_uid: 你的 api_uid, pdd_user_id: 你的 pdd_user_id, _nano_fp: 你的设备指纹, } def fetch_reviews(goods_id, page, size10, sort_type2, anti_content固定串): 抓取单页评论返回 (评论列表, 总条数) params { goods_id: goods_id, # 商品 ID page: page, # 页码从 1 开始 size: size, # 单页条数建议 10~20 sort_type: sort_type, # 1 综合 2 有图优先 3 最新 anti_content: anti_content, # 签名串 } resp requests.get( REVIEW_API, paramsparams, headersHEADERS, cookiesCOOKIES, timeout10 ) resp.raise_for_status() payload resp.json() return payload.get(reviews_list, []), payload.get(total, 0)五个参数里goods_id从商品页 URL 里截取page从 1 开始递增size传 10 到 20 之间比较常见传超大值接口会直接忽略或返回空sort_type控制排序具体枚举以你抓包时下拉框切换产生的请求为准anti_content是决定了这个请求能不能过校验的签名串。函数返回评论列表和总数两个值总数用于后面计算是否还有下一页。把requests.get换成 Session 的session.get更接近真实浏览器行为连接池和 Cookie 由 Session 内部维护后面并发场景还能减少握手开销。3.3 分页循环的退出条件与节流写法import time def crawl_all_reviews(goods_id, max_page50, sleep_sec3): collected [] for page in range(1, max_page 1): items, total fetch_reviews(goods_id, page) if not items: print(f[stop] page{page} 返回空列表) break collected.extend(items) if len(collected) total: break time.sleep(sleep_sec) # 固定间隔先观察风控反应 return collected退出条件有两个返回空列表说明没有更多数据或已被风控拦截已抓数量达到 total 说明分页收口。max_page是安全上限防止接口异常时无限翻页。第一版用固定sleep_sec即可后面再改成随机延时。抓取过程中每页打印 page 号和累计条数连续三页返回空就优先怀疑 anti_content 过期而不是商品没有更多评论。4. 拼多多爬虫商品数据字段解析、去重与入库4.1 评论字段与页面展示的对应关系接口返回的 JSON 数组里每条评论是reviews_list里的一个字典。公开项目里最常见的字段映射如下返回字段页面含义类型review_id评论 ID字符串review评论正文字符串rating评分整数 1-5goodsSpec购买的 SKU 规格字符串nickName用户昵称字符串avatar头像地址字符串tagList评论标签数组reviewTime评论时间字符串images图片列表数组不同时期接口字段名有差异有的版本用comment_id而不是review_id有的把图片数量单独放在imageNum字段。写解析函数前先json.dumps(item, ensure_asciiFalse, indent2)打印一条完整记录看一遍真实结构再写映射不要凭空猜字段名。4.2 解析与清洗函数import json import time def parse_review(item, goods_id): 把接口原始记录转成入库用字典 tags ,.join(t.get(name, ) for t in (item.get(tagList) or [])) images [] for img in (item.get(images) or []): url img if isinstance(img, str) else img.get(url, ) if url: images.append(url) return { goods_id: goods_id, review_id: str(item.get(review_id) or item.get(comment_id) or ), rating: item.get(rating), review: (item.get(review) or ).strip(), goods_spec: item.get(goodsSpec) or , nickname: item.get(nickName) or , tags: tags, image_count: len(images), images: json.dumps(images, ensure_asciiFalse), review_time: item.get(reviewTime), crawl_time: time.strftime(%Y-%m-%d %H:%M:%S), }清洗要点评论正文可能携带换行和多余空格统一striptagList 里的标签结构不确定用get兜底后拼成逗号分隔images 里的元素有时候是字符串有时候是字典两层都兼容。review_id同时兼容两个命名接口字段调整时解析不会直接崩掉。rating 在部分评论下可能缺失保留 None 入库统计时AVG(rating)会自然跳过。4.3 以 goods_id review_id 去重并落 MySQL评论翻页本身不会重复但断点续抓、多轮补数据时同一批数据会重复入库。以goods_id review_id作为唯一键去重是开销最小也最准确的方案。先建表CREATE TABLE pdd_reviews ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id VARCHAR(32) NOT NULL, review_id VARCHAR(64) NOT NULL, rating TINYINT DEFAULT NULL, review TEXT, goods_spec VARCHAR(255) DEFAULT , nickname VARCHAR(64) DEFAULT , tags VARCHAR(255) DEFAULT , image_count INT DEFAULT 0, images TEXT, review_time VARCHAR(32) DEFAULT , crawl_time DATETIME, UNIQUE KEY uk_review (goods_id, review_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一键放在(goods_id, review_id)上因为不同商品的评论 ID 理论上有碰撞可能。评论正文用 TEXT图片较多时单条 JSON 可能超长需要就改成 MEDIUMTEXT。utf8mb4是必须的评论里大量出现 emoji 和特殊符号用 utf8 会直接写入报错。插入用INSERT ... ON DUPLICATE KEY UPDATE做幂等def save_reviews(conn, rows): sql INSERT INTO pdd_reviews (goods_id, review_id, rating, review, goods_spec, nickname, tags, image_count, images, review_time, crawl_time) VALUES (%(goods_id)s, %(review_id)s, %(rating)s, %(review)s, %(goods_spec)s, %(nickname)s, %(tags)s, %(image_count)s, %(images)s, %(review_time)s, %(crawl_time)s) ON DUPLICATE KEY UPDATE rating VALUES(rating), tags VALUES(tags), images VALUES(images) with conn.cursor() as cur: cur.executemany(sql, rows) conn.commit()executemany批量提交100 条一批比较平衡。重复记录只更新评分、标签和图片评论正文保持首次抓取原貌这样追评后的标签变化也能覆盖进来。用 pymysql 连接时记得带charsetutf8mb4否则连接层就把字符集卡住了。4.4 CSV 导出的兜底方案不是所有场景都需要数据库。临时分析或演示环境直接用 pandas 落 CSV 更快import pandas as pd def export_csv(rows, pathpdd_reviews.csv): df pd.DataFrame(rows) df.to_csv(path, indexFalse, encodingutf-8-sig)utf-8-sig是为了 Excel 打开时中文不乱码。CSV 方案做不了增量去重每次全量导出即可。不想引入 pandas 就用 csv 模块的 DictWriter字段顺序按解析函数 return 的 key 对齐。5. 拼多多爬虫排错签名过期、限流与断点续抓5.1 四类错误响应决策表跑一段时间后遇到的错误基本集中在四类。对照响应特征快速定位现象最可能原因处理动作返回 error_code 或校验失败anti_content 过期重新抓包或重新生成签名HTTP 403Cookie 失效从浏览器重新导出 Cookie连续空列表单商品评论已抓完核对 total 是否达成前面正常突然全部失败触发频率限制拉大延时并停止 10~30 分钟频率限制的特征是前面一切正常突然连续几页返回空或 403换签名也没用。唯一有效动作是停。休息后从断点继续而不是从头开始。5.2 断点续抓抓取状态落盘import json, os STATE_FILE crawl_state.json def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, encodingutf-8) as f: return json.load(f) return {} def save_state(state): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def crawl_with_resume(goods_id): state load_state() page state.get(goods_id, 0) 1 while True: items, total fetch_reviews(goods_id, page) if not items: break save_reviews(conn, [parse_review(x, goods_id) for x in items]) state[goods_id] page save_state(state) page 1每成功一页就写一次状态文件进程崩溃或限流杀掉进程后从page 1继续不重头扫。先入库后写状态避免评论存了但状态没更新的中间态。多商品并行时用goods_id作为字典 key状态互不覆盖。5.3 三个检查验证抓取结果抓完一批后用三个角度验证数据可信度SELECT COUNT(*) FROM pdd_reviews WHERE goods_id ...与页面展示的评论总数对比差异在翻页边界内可接受随机抽 5 条评论正文和页面逐字对比确认解析映射没偏再查MIN(review_time)和MAX(review_time)的时间跨度判断翻页是否覆盖了期望范围防止中途跳页漏数据。三关都过这批数据才能交给下游分析任何一关不过先回对应章节排查不要带着脏数据继续翻页。本文还有配套的精品资源点击获取