ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Scrapy+Selenium实现拼多多评论数据采集:登录态、翻页与反爬解析

Scrapy+Selenium实现拼多多评论数据采集:登录态、翻页与反爬解析 简介一套基于拼多多商品评论数据的爬虫项目源码面向计算机相关专业学生、教师及企业开发者尤其适合用作毕业设计、课程设计或爬虫实战入门。项目以 Selenium 与 Python 为核心覆盖动态页面加载、验证码识别、代理IP切换、多线程任务调度以及数据库存储等关键环节各功能模块独立封装代码结构清晰。压缩包共包含31个文件其中17个Python脚本是主体对应页面操作、验证码处理、代理配置、数据库操作等核心能力另有5个zbak备份文件、3个txt配置文件及状态文件整体体积仅49KB却完整呈现了从请求、解析到落库的爬虫全流程。该项目为评审95分的个人高分作品代码经测试运行无误附带有README说明文档和详细注释适合对照源码研究Selenium爬虫的工程化写法也可直接作为模板扩展到其他电商平台。目前已有50人学习下载是一份小而精的高质量参考资源。1. 拼多多评论数据从哪来先看 Spider.py 的抓取目标拼多多没有开放商品评论查询 API页面里的评论内容、评分、规格和晒图都是异步加载的。直接对首页 HTML 发请求拿不到完整评论区这也是很多爬虫项目会遇到的第一道门槛。本项目基于 Scrapy 搭建入口是 Spider.py配套 seleniumoperate.py、captcha.py、proxy.py、threadpool.py、dboperate.py把登录态、验证码、网络出口切换、并发控制这几个高频卡点都串了起来。资源内附 README、项目文档和完整源码跑通后可以输出包含评论内容、评分、购买规格、评论时间等字段的数据集。适合正在做竞品分析、商品洞察或比价监控的工程师参考也适合用 Scrapy 做综合实战训练的学习者。抓取频率建议控制在对方站点无明显感知的范围内本项目相关代码仅用于学习交流。2. Scrapy 与 Selenium 双引擎seleniumoperate.py 怎么补登录态和动态渲染2.1 为什么拼多多评论不能只用 requests评论接口走的是 POST 请求抓包里能看到请求体主要由 goods_id、page、size 和一串叫做 anti-content 的签名组成。anti-content 由前端 JS 在运行时生成和 cookies、时间戳、设备指纹都有关系。用 requests 直连也能调通但每次都要维护签名生成逻辑一旦前端加密规则更新整套代码就要跟着改。Selenium 的价值在于让真实浏览器去完成签名和渲染代码只需要等待页面加载完成再读取接口返回或 DOM 节点。这里的“双引擎”是指 Selenium 负责拿到登录后的状态Scrapy 负责批量请求和解析。项目里 seleniumoperate.py 不会参与所有页面的抓取只在登录、搜索页滚动加载这类需要渲染的场景出现进入评论分页后改用 Scrapy 的 FormRequest 直接请求评论接口这样整体速度比全程 Selenium 快很多。两套工具各管一段既避开了签名维护又保留了 Scrapy 的调度能力。2.2 seleniumoperate.py 的登录与状态保存评论接口要求登录状态seleniumoperate.py 的核心任务是打开拼多多登录页等用户扫码或短信完成后把 cookies 导出。下面是常见的实现方式# seleniumoperate.py关键逻辑简化 from selenium import webdriver from selenium.webdriver.chrome.options import Options def get_login_cookies(login_url: str) - dict: options Options() options.add_argument(user-agentMozilla/5.0 ... Chrome/120.0) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions) driver.get(login_url) input(请完成扫码或短信登录然后按回车键继续...) cookies {c[name]: c[value] for c in driver.get_cookies()} driver.quit() return cookies这段代码的作用是用 Selenium 打开登录页并等待人工登录登录完成后把浏览器 cookies 转成字典。excludeSwitches参数用于关闭浏览器右下角的“自动化测试正在控制此浏览器”提示降低被前端识别的概率user-agent建议填当前主流 Chrome 版本的完整 UA不要用默认值否则部分反爬策略会直接拦截登录请求。登录态拿到后项目里会序列化到 opt_id.txt避免每次启动都重新扫码。state.zbak 是状态文件的备份防止运行中断导致登录态损坏后无法恢复。2.3 在 Scrapy 请求中注入 cookiesSpider.py 启动后先从 opt_id.txt 读取 cookies再注入到评论接口请求中。常见做法是写一个 load_cookies_from_file 辅助函数然后用 scrapy.Request 的 cookies 参数传递。# Spider.py 片段 from scrapy import FormRequest cookies load_cookies_from_file(opt_id.txt) yield FormRequest( urlcomment_api_url, methodPOST, formdata{ goods_id: goods_id, page: 1, size: 10, }, cookiescookies, callbackself.parse_comments, meta{goods_id: goods_id, page: 1}, dont_filterTrue, )这里使用 FormRequest 而不是 Request因为评论接口是表单提交方式cookies 参数会由 Scrapy 自动拼到请求头里不需要手动维护 Cookie 字符串。dont_filterTrue 是在需要跳过 URL 去重时使用例如同一个商品多次抓取或不同 cookies 条件下重复请求。如果不加这个参数Scrapy 的调度器会认为 URL 已经请求过而直接丢弃后续请求导致翻页看起来正常但数据不更新。meta 里的 goods_id 和 page 会原样传递给回调函数用于翻页时拼接下一次请求。下面整理一下项目内核心文件的职责后续章节会分别展开。文件职责关键产出seleniumoperate.py登录、页面渲染、滚动加载登录 cookies、渲染后的 HTMLSpider.py请求调度、回调解析清洗后的评论 itemcaptcha.py验证码截图、识别和人工输入验证码答案proxy.py网络出口存活检测与切换可用出口地址threadpool.py多线程并发控制并发任务调度结果dboperate.py数据写入与去重MySQL 记录或 CSV 文件这个表可以作为后续改代码时的索引。实际运行中seleniumoperate.py 只需要成功一次其他模块负责把抓取效率提上去。第一次运行时不要直接上高并发先用单个请求确认 cookies 和接口参数都正确。3. 评论列表页的解析与翻页Spider.py 的完整回调链3.1 从搜索词到商品 ID 的入口设计Spider.py 的 start_requests 一般从关键词文件读取搜索词例如“蓝牙耳机”“充电宝”这类商品类目词。搜索页由 Selenium 渲染完成后Spider 拿到页面源码再用 XPath 定位商品链接。拼多多商品链接中带有 goods_id 参数格式通常是 /goods.html?goods_idxxx所以用正则就能提取 ID。下面是一个简化版本# Spider.py 的搜索解析部分 import re def parse_search(self, response): goods_links response.xpath( //a[contains(href, goods_id)]/href ).getall() for link in goods_links: match re.search(rgoods_id(\d), link) if match: goods_id match.group(1) yield FormRequest( urlself.comment_api_url, methodPOST, formdata{goods_id: goods_id, page: 1, size: 10}, callbackself.parse_comments, meta{goods_id: goods_id, page: 1}, cookiesself.cookies, dont_filterTrue, )这段代码的关键点是 XPath 只提取包含 goods_id 的链接正则负责把参数值取出来。contains(href, goods_id)会比//a/href更精确避免误抓底部分类或推荐位链接。商品 ID 统一转成字符串传给 formdata因为在 POST 表单里数字字符串是更稳定的传输方式。如果搜索页是异步加载更多商品需要在 seleniumoperate.py 里模拟滚动到底部等待新商品出现后再提取否则只能拿到第一屏的商品链接。3.2 评论接口的翻页参数与终止条件进入评论回调解析时响应实际是 JSON而不是 HTML。对接 Scrapy 时可以把 response.text 直接 json.loads再遍历评论列表。翻页逻辑如下import json def parse_comments(self, response): data json.loads(response.text) reviews data.get(reviews, []) if not reviews: return for item in reviews: yield self.build_item(item, response.meta[goods_id]) next_page response.meta[page] 1 yield FormRequest( urlself.comment_api_url, methodPOST, formdata{ goods_id: str(response.meta[goods_id]), page: str(next_page), size: 10, }, callbackself.parse_comments, meta{goods_id: response.meta[goods_id], page: next_page}, cookiesself.cookies, dont_filterTrue, )当返回的 reviews 为空列表时说明当前商品评论已经翻到底直接 return 终止。每一次回调都从 meta 里取当前 page加 1 后构造下一次 FormRequestsize 参数控制每页条数常见值是 10 或 20需要和前端抓包结果保持一致。这里不能把 page 保存在 Spider 实例变量中否则多商品并发时会相互覆盖导致评论页错乱。用 meta 传递是 Scrapy 里可靠的标准做法。3.3 评论字段的提取与清理评论的原始字段里通常包含用户昵称、评分、时间、评论文案、购买规格、图片列表等。为了后续做情感分析或竞品对比字段尽量保持规范。下面是 build_item 的实现def build_item(self, item, goods_id): return { goods_id: goods_id, comment_id: item.get(comment_id), rating: item.get(rating), comment_time: item.get(time), comment_content: re.sub(r\s, , item.get(comment, )).strip(), specs: |.join(item.get(specs, [])) if item.get(specs) else 无, image_num: len(item.get(images, [])), }这里把评论内容中的连续空白符替换为单个空格并去掉首尾空白避免同一句话因为换行格式不同被拆成多条specs 是多选择字段用竖线拼接成一个字符串方便后续写入 CSV 或 MySQLimage_num 用于统计晒图率是判断评论质量的重要维度。缺字段时不要直接抛异常rating 为空可以填 0comment_time 解析失败可以保留原文等数据落库后再统一清洗。去重键建议用 goods_id 加 comment_id 拼接后取 MD5存到表里作为唯一业务键。翻页过程中偶尔会出现重复评论比如接口在最后一页仍返回前一页的部分数据去重键能有效避免最终数据集膨胀。下表是评论字段与类型的对应关系字段类型说明goods_idint商品编号关联商品维度comment_idint评论编号去重依据ratingtinyint1~5 星评分comment_timestring评论时间后续可做时间序列comment_contenttext清洗后的文本specsstring购买规格多规格用竖线分隔image_numint评论图片数量这一套回调链是整套爬虫的主路径搜索词得到商品 ID商品 ID 拉取评论分页分页数据清洗成 item。实际工程中Spider.py 还应该增加一个中间件把响应状态码 302、403 单独记录下来方便定位具体在哪一步被拦截。4. 验证码、IP 调度与线程池captcha.py / proxy.py / threadpool.py 的调度细节4.1 captcha.py验证码应对策略拼多多的验证码出现频率和请求速度强相关。从经验看单个出口每秒超过 2 次请求或连续翻页超过 30 页时就可能弹出滑块或点选验证码。captcha.py 在项目里的定位不是训练识别模型而是做“截图、识别、人工兜底”三层处理。代码逻辑如下# captcha.py import time def solve_captcha(driver, ocr_api, image_pathcaptcha.png): driver.save_screenshot(image_path) result ocr_api(open(image_path, rb)) if result.get(confidence, 0) 0.8: driver.find_element(class name, captcha_input).send_keys(result[text]) else: input(请人工确认验证码并输入然后回车继续...) time.sleep(1)这里先截图保存再用打码平台的 OCR 接口识别。confidence 阈值设成 0.8低于这个值就转人工输入宁可慢一点也不要因为识别错误反复提交。滑块类验证码通常 OCR 解决不了常见做法是切换到 Selenium 的 ActionChains 模拟拖动或者直接暂停当前任务并提示人工处理。建议在验证码处理结束后等待 3 秒以上避免刚填完答案就立刻发起下一个请求。4.2 proxy.py网络出口的存活检测与切换为了避免请求集中在一个出口地址项目中用 proxy.py 管理一组可用的网络出口。检测逻辑很简单用当前出口请求拼多多移动端首页只有返回 200 的出口才进入可用队列。# proxy.py 简化实现 import requests def get_valid_source(source_pool: list) - dict: test_url https://mobile.yangkeduo.com for source in source_pool: try: resp requests.get( test_url, proxies{http: source, https: source}, timeout5, ) if resp.status_code 200: return {http: source, https: source} except requests.RequestException: continue return {}检测目标不要选无关的通用站点因为某个出口对通用站点通并不代表对目标站通。timeout 设置为 5 秒超过就放弃整个轮询过程控制在几十秒内。某个出口被触发风控后要在 proxy.py 中加入黑名单机制把该地址从可用队列移除一段时间避免每轮请求都撞上同一批被封的出口。实际项目中可用出口数量至少保持 5 个低于这个阈值时应该暂停抓取而不是降速硬扛。4.3 threadpool.py并发参数怎么定Scrapy 本身是异步框架但本项目里的一些预处理任务会用到 threadpool.py把多个商品 ID 分配给多个线程并发执行。下面是用 ThreadPoolExecutor 包装的常见写法# threadpool.py from concurrent.futures import ThreadPoolExecutor, as_completed import time, random def run_with_threadpool(goods_ids, worker4, per_page10): def task(goods_id): time.sleep(random.uniform(0.5, 1.5)) return fetch_comments(goods_id, per_page) with ThreadPoolExecutor(max_workersworker) as executor: futures [executor.submit(task, gid) for gid in goods_ids] for future in as_completed(futures): yield future.result()worker 表示同时执行的线程数任务内部用 sleep 做随机延时把请求节奏打散。as_completed 会在任意一个任务完成后立即返回结果适合后续数据落库。worker 参数不建议直接设成 CPU 核数或一个很大的值接口型爬虫的瓶颈在服务端限流不在本地计算。针对拼多多的经验值是日常增量抓取设 4短时间补数据设 8超过 8 很容易触发验证码。下表是不同并发量级在单个出口下的表现worker表现建议1安全但速度慢首次验证项目逻辑4错误率低速度可接受日常增量抓取8较快偶尔出现验证码短时间补数据16大概率触发强风控不推荐单独使用如果项目最终要跑到更高并发可以配合多个网络出口把 worker 分配到不同出口上不要让同一个出口同时发起太多请求。线程池里出现的异常要捕获并记录到日志否则某几个商品 ID 异常会导致整个 future 链中断。4.4 失败请求的退避与重试在验证码和出口状态都不理想的情况下重试策略应该做指数退避而不是立即重试。比如第一次失败等 2 秒第二次等 4 秒最多重试 3 次。threadpool.py 里可以让每个线程内部维护一个 retry_count超过阈值就跳过该商品并记录到 fail_goods.txt后续再用低并发补抓。def fetch_comments_with_retry(goods_id, session, max_retry3): for attempt in range(max_retry): try: return fetch_comments(goods_id, session) except BlockedError: time.sleep(2 ** attempt) # 1s, 2s, 4s write_fail_log(goods_id) return None这里的2 ** attempt产生的时间序列是 1 秒、2 秒、4 秒BlockedError 是自定义异常用来捕获验证码拦截、状态码 403 这类情况。重试不能解决根本问题只是给风控留出恢复时间所以重试次数要有限制。失败商品单独记录比反复重试当前队列更划算。5. 数据落库与规模自检用 dboperate.py 把评论变成可用数据集5.1 评论表结构与幂等写入评论数据最终要落到 MySQL 或 CSV。dboperate.py 主要负责连接数据库和执行写入。建表时要把商品 ID 和评论 ID 组合成唯一键这是幂等写入的前提。CREATE TABLE pdd_comments ( id BIGINT AUTO_INCREMENT PRIMARY KEY, goods_id BIGINT NOT NULL, comment_id BIGINT NOT NULL, rating TINYINT, comment_time DATETIME, comment_content TEXT, specs VARCHAR(255), image_num INT DEFAULT 0, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_comment (goods_id, comment_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一键 uk_comment 确保同一个商品下的同一条评论不会重复插入。dboperate.py 里的写入语句用 ON DUPLICATE KEY UPDATE 做覆盖更新而不是先查再插。完成写入后可以用简单的分组统计检查数据质量。5.2 用评论分布判断有没有被反爬截断日志里看到请求完成并不代表数据完整。更有效的自检方式是统计每个商品的评论条数分布。先导出 CSV再运行下面的 Python 脚本import pandas as pd df pd.read_csv(pdd_comments.csv) print(df.groupby(goods_id).size().describe())重点看 50% 分位数和最大值。如果大量商品评论数都是 10、20、30 这种整十数字说明翻页在固定页数被截断可能遇到验证码或接口参数错误。正常评论分布应该是长尾形态少数爆款商品有大量评论多数商品评论较少。如果所有商品的评论数都刚好等于页面大小乘以固定页数优先检查终止条件中 reviews 是否为空的判断是否过早退出。如果需要定时增量在 dboperate.py 里加一个逻辑每天只抓取近 7 天新增评论通过 ON DUPLICATE KEY UPDATE 只更新已有评论的文本不删除旧评论。这样就能把评论数据变成可持续维护的数据集。调整并发参数时优先看每个商品的评论数分布而不是看总条数。本文还有配套的精品资源点击获取
返回列表