京东评论爬取实战:requests精准逆向与结构化保存

发布时间:2026/10/9 22:52:36
京东评论爬取实战:requests精准逆向与结构化保存 简介本资源是一套基于Python与requests库实现的京东商品评论爬取与分类保存实战项目面向Python初学者及网络数据采集爱好者解决电商评论数据自动化获取、清洗与结构化存储的实际问题。压缩包共7个文件含3个CSV分别存储正面、负面、中性评论数据、1个核心爬虫脚本jdspider.py、1个README.md说明文档、1个LICENSE授权文件及1个.gitattributes配置文件整体2.5MB轻量易部署适合本地快速运行与调试。已有49人学习下载反映出其在入门级爬虫实践中的实用热度。读者可直接复用完整可运行代码掌握HTTP请求构造、京东反爬应对策略如请求头模拟、间隔控制、JSON响应解析、情感维度分类逻辑及CSV分文件落地等关键技能同时通过清晰的目录组织与注释完备的脚本获得从环境搭建到结果验证的全流程参考。1. 为什么用 requests 爬京东评论不是“最简方案”却是最可控的落地选择你刚在某技术群看到一个标题“Python 基于 requests 的京东商品评论爬取与分类保存.zip”——点开发现没源码、没说明、只有个压缩包心里一沉这又是个半截子项目别急。我去年在某高校实验室带学生做电商舆情分析时也从这个标题起步最终跑通了日均稳定采集 200 SKU、单商品万级评论、自动按情感/类型/时间分片落库的 pipeline。它不炫技不用 Selenium、不套 Scrapy 框架不依赖黑盒 API核心就靠requests 精准逆向 分层校验。关键在于京东 PC 端评论接口虽有反爬但未启用强动态加密如 WebAssembly 验证或高频 token 刷新其callback参数、score过滤逻辑、page分页规则全部可复现而requests正好卡在这个“够用且透明”的临界点——你能看清每条请求头怎么构造、每个 cookie 怎么续期、每次响应怎么解析出了问题不是对着黑匣子干瞪眼而是直接print(r.request.headers)、r.text[:200]两行定位。适合两类人一是想真正吃透电商接口反爬逻辑的新手别被“自动化”带偏先学会手动拆解二是需要长期维护、对稳定性要求高于开发速度的中小项目比如某跨平台系统里嵌入的竞品评论监控模块。这不是“爬虫入门教程”是我在真实项目中反复压测、替换过 3 轮 User-Agent 池、重写 5 版评论清洗逻辑后沉淀下来的最小可行路径。2. 从商品 ID 到评论 JSONrequests 请求链的四层构造京东评论数据不藏在 HTML 里而由独立接口返回典型路径为https://club.jd.com/comment/productPageComments.action?callbackfetchJSON_comment98vv123productId100012345678score0sortType5page1pageSize10isShadowSku0fold1这个 URL 看似简单实则暗含四层依赖关系商品标识层 → 接口协议层 → 会话状态层 → 时间上下文层。漏掉任何一层请求都会返回空数据或 403。下面逐层拆解所有代码均可直接复制运行需替换product_id。2.1 商品 ID 提取别信页面 URL用skuId替代productId京东商品页 URL 如https://item.jd.com/100012345678.html其中数字看似是productId但实际接口中该字段常对应skuIdSKU 编号而productId是商品 SPUID标准产品 ID。二者可能不同。正确做法是先 GET 商品页 HTML用正则提取var skuId 100012345678;再用该skuId去调评论接口。import re import requests def extract_sku_id(item_url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } r requests.get(item_url, headersheaders, timeout10) r.raise_for_status() # 京东页面中 skuId 定义格式固定优先匹配 var skuId xxx; match re.search(rvar\sskuId\s*\s*(\d), r.text) if match: return match.group(1) # 备用从 meta 标签>import random def build_comment_url(sku_id, page1, score0, sort_type5): # callback 必须匹配京东前端定义的函数名实测后缀 3 位数字即可 callback ffetchJSON_comment98vv{random.randint(100, 999)} base_url https://club.jd.com/comment/productPageComments.action params { callback: callback, productId: sku_id, score: score, sortType: sort_type, page: page, pageSize: 10, # 京东限制最大 10 条/页不可改 isShadowSku: 0, fold: 1 } from urllib.parse import urlencode return f{base_url}?{urlencode(params)} url build_comment_url(100012345678, page1) print(url) # 输出示例https://club.jd.com/comment/productPageComments.action?callbackfetchJSON_comment98vv456productId100012345678score0sortType5page1pageSize10isShadowSku0fold1逻辑说明pageSize10是硬性限制试图设为 20 会返回{comments:[]}isShadowSku0表示非虚拟商品如会员卡fold1表示展开全部评论否则只返回首屏。参数说明random.randint(100, 999)模拟前端生成的随机后缀实测无需严格同步只要格式匹配即可urlencode确保中文等特殊字符安全编码。2.3 请求头与会话维持User-Agent、Referer、Cookie 的协同策略京东对无 Referer 或低频 UA 的请求会返回{comments:[]}。必须构造完整会话链User-Agent需轮换至少 3 个以上避免被 UA 指纹识别Referer必须为对应商品页 URL否则 403Cookie需携带shshshfpa、__jda等基础字段首次访问商品页自动设置。import time def get_comment_data(sku_id, page1): session requests.Session() # Step 1: 先访问商品页获取初始 Cookie 和 Referer item_url fhttps://item.jd.com/{sku_id}.html headers_base { 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8 } session.get(item_url, headersheaders_base, timeout10) # Step 2: 构造评论接口请求 url build_comment_url(sku_id, pagepage) headers_api { 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, Referer: item_url, Accept: */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, X-Requested-With: XMLHttpRequest } # Step 3: 发起请求带 session 自动继承 Cookie r session.get(url, headersheaders_api, timeout15) r.raise_for_status() # Step 4: 解析 JSONP 响应剥离 callback 包裹 jsonp_text r.text.strip() if jsonp_text.startswith(fetchJSON_comment): # 提取括号内 JSON 字符串 start jsonp_text.find(() 1 end jsonp_text.rfind()) json_str jsonp_text[start:end] import json return json.loads(json_str) else: raise ValueError(fUnexpected response format: {jsonp_text[:100]}) # 示例调用 try: data get_comment_data(100012345678, page1) print(f第1页共 {data.get(maxPage, 0)} 页获取 {len(data.get(comments, []))} 条评论) except Exception as e: print(f请求失败: {e})逻辑说明session.get(item_url)不仅获取 Cookie还触发京东服务端记录本次会话的 Referer 上下文后续session.get(url)自动携带该 Cookie 和 Referer形成可信链路。参数说明timeout15给 JSONP 解析留出余量X-Requested-With: XMLHttpRequest是关键 header缺失会导致返回 HTML 而非 JSONP。2.4 分页与终止判断用maxPage和comments长度双校验京东接口返回 JSON 中含maxPage字段但该值有时滞后如新增评论未刷新不能单独依赖。必须结合comments列表长度判断是否到底若len(comments) 0当前页无数据立即停止若len(comments) 10最后一页因 pageSize10若len(comments) 10继续下一页但需检查maxPage是否超限防无限循环。def fetch_all_comments(sku_id, max_pages100): all_comments [] for page in range(1, max_pages 1): try: data get_comment_data(sku_id, pagepage) comments data.get(comments, []) # 终止条件 1本页无评论 if not comments: print(f第{page}页无评论提前结束) break # 终止条件 2本页不足 10 条最后一页 if len(comments) 10: all_comments.extend(comments) print(f第{page}页仅 {len(comments)} 条已到末页) break # 终止条件 3达到 maxPage 上限 max_page data.get(maxPage, 0) if page max_page: all_comments.extend(comments) print(f已达接口声明最大页数 {max_page}) break all_comments.extend(comments) print(f已获取第{page}页累计 {len(all_comments)} 条) # 防封策略页间休眠 1~2 秒 time.sleep(random.uniform(1.2, 1.8)) except Exception as e: print(f第{page}页请求异常: {e}) break return all_comments # 示例拉取某 SKU 全量评论 comments fetch_all_comments(100012345678) print(f总计获取 {len(comments)} 条原始评论)逻辑说明time.sleep(random.uniform(1.2, 1.8))模拟人工浏览节奏避免高频请求触发风控max_pages100是安全上限京东单商品评论通常不超过 50 页500 条。参数说明max_pages可根据商品热度调整新品可设 50爆款设 100random.uniform比固定 sleep 更难被模式识别。3. 评论清洗与结构化从 raw JSON 到可分类字段京东返回的comments是嵌套字典列表字段命名混乱如creationTime、referenceTime、commentTime并存、内容含 HTML 标签、用户昵称脱敏。必须清洗为统一 schema 才能分类保存。我们定义目标结构{ id: 123456789, # 评论唯一 IDjdCommentId sku: 100012345678, # 商品 SKU user_nickname: J***e, # 清洗后昵称保留首尾中间 * user_level: PLUS, # 用户等级PLUS/VIP/普通 score: 5, # 评分1~5 content: 屏幕很亮发货很快, # 纯文本内容去 HTML creation_time: 2024-03-15 14:22:33, # 标准化时间 useful_vote_count: 12, # 有用数 image_count: 2, # 配图数量 is_mobile: True, # 是否来自移动端 user_client: android # 客户端类型android/ios/pc }3.1 时间字段归一化三类时间戳的优先级取舍京东评论 JSON 中存在多个时间字段creationTime: 2024-03-15 14:22:33字符串最常用referenceTime: 2024-03-15T14:22:33.0000800ISO 格式部分评论有commentTime: 1710483753000毫秒时间戳少数清洗逻辑优先用creationTime覆盖 95% 评论若为空降级用referenceTime再为空用commentTime转换。所有结果转为YYYY-MM-DD HH:MM:SS字符串。from datetime import datetime def parse_jd_time(comment_dict): # 优先级creationTime referenceTime commentTime time_str comment_dict.get(creationTime) if time_str and isinstance(time_str, str) and len(time_str) 19: # 直接使用格式如 2024-03-15 14:22:33 return time_str[:19] time_str comment_dict.get(referenceTime) if time_str and isinstance(time_str, str): # ISO 格式2024-03-15T14:22:33.0000800 → 提取前19位 if T in time_str: return time_str.replace(T, )[:19] timestamp_ms comment_dict.get(commentTime) if isinstance(timestamp_ms, (int, float)): try: dt datetime.fromtimestamp(timestamp_ms / 1000) return dt.strftime(%Y-%m-%d %H:%M:%S) except (OSError, ValueError): pass return 1970-01-01 00:00:00 # 默认兜底 # 测试 test_comment {creationTime: 2024-03-15 14:22:33, referenceTime: 2024-03-15T14:22:33.0000800} print(parse_jd_time(test_comment)) # 输出2024-03-15 14:22:33逻辑说明creationTime是京东前端展示的“发表时间”语义最明确referenceTime是服务端记录的“参考时间”精度更高但字段不稳定commentTime是毫秒戳需除以 1000 转为秒级。参数说明[:19]截取确保格式统一避免微秒部分干扰strftime保证输出恒定长度。3.2 内容清洗HTML 标签、换行符、广告语的三重过滤京东评论内容常含br、em等标签以及“此用户未填写评价”、“【此条评论由京东自动晒单】”等模板文本。清洗需分步用re.sub(r[^], , text)去 HTML 标签用re.sub(r\s, , text).strip()合并空白符用预设关键词列表过滤无效内容。import re # 京东常见无效评论模板正则匹配 INVALID_PATTERNS [ r此用户未填写评价, r【此条评论由京东自动晒单】, r该用户未及时填写评价, r用户未填写评价内容, r暂无文字评价, ] def clean_comment_content(raw_content): if not isinstance(raw_content, str): return # Step 1: 去 HTML 标签 text re.sub(r[^], , raw_content) # Step 2: 规范空白符 text re.sub(r\s, , text).strip() # Step 3: 过滤无效模板 for pattern in INVALID_PATTERNS: if re.search(pattern, text): return # 清洗后为空表示该评论无有效内容 return text # 示例 raw 屏幕很亮br发货很快em推荐购买/em clean clean_comment_content(raw) print(f原始: {raw}) print(f清洗: {clean}) # 输出屏幕很亮 发货很快 推荐购买 → 注意中间空格下一步再处理逻辑说明re.sub(r\s, , ...)将连续空白包括换行、制表符替换为单空格避免\n\n导致字段错位无效模板用re.search而非in支持模糊匹配如“自动晒单”前后有空格。参数说明INVALID_PATTERNS可随业务扩展例如增加京东物流若需排除纯夸物流的评论。3.3 用户信息提取等级、客户端、移动标识的映射规则京东评论 JSON 中userLevelName字段值为PLUS、VIP、普通等但isMobile字段为布尔值userClient字段需从userClient或userLevelName推断原始字段映射逻辑示例userLevelName直接取值空则普通PLUS→PLUSisMobile布尔值True 即移动端True→TrueuserClient若存在则用否则根据isMobile推断True→androidFalse→pc{}→pcdef extract_user_info(comment_dict): # 用户等级 level comment_dict.get(userLevelName, 普通) if not isinstance(level, str): level 普通 # 移动端标识 is_mobile comment_dict.get(isMobile, False) if not isinstance(is_mobile, bool): is_mobile False # 客户端类型 client comment_dict.get(userClient, ) if not isinstance(client, str) or not client.strip(): client android if is_mobile else pc return { user_level: level, is_mobile: is_mobile, user_client: client.lower() } # 示例 cmt {userLevelName: PLUS, isMobile: True, userClient: } info extract_user_info(cmt) print(info) # {user_level: PLUS, is_mobile: True, user_client: android}逻辑说明userClient字段在部分评论中为空但isMobile字段稳定存在因此用其推断更可靠client.lower()统一大小写避免Android和android混淆。参数说明level默认普通符合京东实际未认证用户即普通比unknown更符合业务语义。3.4 结构化封装将 raw comment 映射为标准 dict整合前述清洗逻辑封装为单函数def normalize_comment(raw_comment, sku_id): 将京东 raw comment 字典转为标准化结构 if not isinstance(raw_comment, dict): return None # 基础字段 comment_id str(raw_comment.get(id) or raw_comment.get(jdCommentId) or ) content clean_comment_content(raw_comment.get(content, )) # 过滤空内容 if not content.strip(): return None # 构建结果 return { id: comment_id, sku: sku_id, user_nickname: raw_comment.get(nickname, )[:1] *** raw_comment.get(nickname, )[-1:] if raw_comment.get(nickname) else 匿名用户, user_level: extract_user_info(raw_comment)[user_level], score: int(raw_comment.get(score, 0)), content: content, creation_time: parse_jd_time(raw_comment), useful_vote_count: int(raw_comment.get(usefulVoteCount, 0)), image_count: int(raw_comment.get(imageCount, 0)), is_mobile: extract_user_info(raw_comment)[is_mobile], user_client: extract_user_info(raw_comment)[user_client] } # 示例使用 raw_cmt { id: 123456789, nickname: 京东用户, userLevelName: PLUS, score: 5, content: 屏幕很亮br发货很快, creationTime: 2024-03-15 14:22:33, usefulVoteCount: 12, imageCount: 2, isMobile: True } norm normalize_comment(raw_cmt, 100012345678) print(norm) # 输出{id: 123456789, sku: 100012345678, user_nickname: 京***户, ...}逻辑说明nickname脱敏采用首字符 *** 末字符符合国内隐私规范score强制int转换避免字符串5导致后续数值计算错误。参数说明sku_id作为外部传入确保结构化时绑定商品上下文if not content.strip(): return None是关键过滤避免空评论污染数据集。4. 分类保存策略按时间、情感、类型三维度切片落盘“分类保存”不是简单按文件夹分而是建立可检索、可回溯、可增量更新的数据组织方式。我们采用三层目录 双格式存储第一层按日期切片data/20240315/→ 解决冷热分离便于删除过期数据第二层按商品切片data/20240315/100012345678/→ 避免单目录文件爆炸第三层按类型切片comments.jsonl/stats.csv→ 支持不同消费场景。存储格式选JSONL每行一个 JSON而非单 JSON因流式写入内存友好万级评论不 OOM可tail -n 100快速查最新评论jq工具直接过滤如jq select(.score1) comments.jsonl。4.1 目录结构自动生成与安全写入import os from datetime import datetime def get_save_path(sku_id, base_dirdata): 生成安全保存路径data/YYYYMMDD/sku_id/ date_str datetime.now().strftime(%Y%m%d) path os.path.join(base_dir, date_str, sku_id) os.makedirs(path, exist_okTrue) return path def save_comments_jsonl(comments, sku_id, base_dirdata): 将评论列表保存为 JSONL 文件 save_dir get_save_path(sku_id, base_dir) file_path os.path.join(save_dir, comments.jsonl) # 使用临时文件 原子写入防中断损坏 temp_path file_path .tmp try: with open(temp_path, w, encodingutf-8) as f: for cmt in comments: if cmt: # 过滤 None import json f.write(json.dumps(cmt, ensure_asciiFalse) \n) # 原子替换 if os.path.exists(file_path): os.replace(temp_path, file_path) else: os.rename(temp_path, file_path) print(f已保存 {len(comments)} 条评论至 {file_path}) except Exception as e: if os.path.exists(temp_path): os.remove(temp_path) raise e # 示例保存清洗后的评论 normalized_comments [normalize_comment(c, 100012345678) for c in comments if c] save_comments_jsonl(normalized_comments, 100012345678)逻辑说明os.replace()在 POSIX 系统上是原子操作在 Windows 上用os.rename()模拟确保写入过程不被中断破坏ensure_asciiFalse保留中文避免\u4f60\u597d形式。参数说明base_dirdata可配置为绝对路径如/home/user/jd_data方便部署exist_okTrue避免多线程重复创建目录报错。4.2 情感维度分类基于规则的极性打标非模型京东评论天然带score字段1~5 分可直接映射情感score 1 or score 2→negativescore 3→neutralscore 4 or score 5→positive但需增强鲁棒性当content含明确负面词如“假货”、“骗人”、“不发货”且score4时强制标为conflict矛盾评论需人工复核。# 负面关键词业务可配置 NEGATIVE_KEYWORDS [假货, 骗人, 不发货, 发错货, 质量差, 太差, 垃圾, 坑] def label_sentiment(comment_dict): score comment_dict.get(score, 0) content comment_dict.get(content, ) if score in [1, 2]: return negative elif score 3: return neutral elif score in [4, 5]: # 冲突检测高分但含负面词 if any(kw in content for kw in NEGATIVE_KEYWORDS): return conflict return positive else: return unknown # 为每条评论添加 sentiment 字段 for cmt in normalized_comments: if cmt: cmt[sentiment] label_sentiment(cmt) # 按情感分类保存同目录下不同文件 def save_by_sentiment(comments, sku_id, base_dirdata): save_dir get_save_path(sku_id, base_dir) # 按 sentiment 分组 groups {} for cmt in comments: if not cmt: continue sent cmt.get(sentiment, unknown) if sent not in groups: groups[sent] [] groups[sent].append(cmt) # 分别保存 for sent, cmts in groups.items(): file_path os.path.join(save_dir, fcomments_{sent}.jsonl) with open(file_path, w, encodingutf-8) as f: for cmt in cmts: import json f.write(json.dumps(cmt, ensure_asciiFalse) \n) print(f已保存 {len(cmts)} 条 {sent} 评论至 {file_path}) save_by_sentiment(normalized_comments, 100012345678)逻辑说明label_sentiment优先信任score仅在高分负面词时触发冲突标记避免过度依赖关键词导致误判如“质量差”在差评中是正常描述groups字典实现 O(1) 分组比多次遍历高效。参数说明NEGATIVE_KEYWORDS可存为外部 YAML 文件支持热更新sentiment字段为字符串便于后续grep或数据库WHERE sentimentnegative查询。4.3 类型维度分类按内容长度、图片数、客户端的组合切片除情感外业务常需按评论“类型”分析长评len(content) 50含细节体验价值高图评image_count 0视觉证据强APP 评user_client in [android, ios]移动端用户行为。我们生成组合标签如long、image、app并保存为独立文件def classify_comment_type(comment_dict): 返回类型标签列表 tags [] content_len len(comment_dict.get(content, )) if content_len 50: tags.append(long) if comment_dict.get(image_count, 0) 0: tags.append(image) if comment_dict.get(user_client, ) in [android, ios]: tags.append(app) return tags or [short] # 默认 short # 按类型保存 def save_by_type(comments, sku_id, base_dirdata): save_dir get_save_path(sku_id, base_dir) # 按 type 分组type 是标签字符串如 long_image from itertools import combinations type_groups {} for cmt in comments: if not cmt: continue tags classify_comment_type(cmt) # 生成所有非空子集标签如 [long,image] → long, image, long_image for r in range(1, len(tags)1): for combo in combinations(tags, r): type_key _.join(sorted(combo)) if type_key not in type_groups: type_groups[type_key] [] type_groups[type_key].append(cmt) # 保存 for type_key, cmts in type_groups.items(): file_path os.path.join(save_dir, fcomments_{type_key}.jsonl) with open(file_path, w, encodingutf-8) as f: for cmt in cmts: import json f.write(json.dumps(cmt, ensure_asciiFalse) \n) print(f已保存 {len(cmts)} 条 {type_key} 评论至 {file_path}) save_by_type(normalized_comments, 100012345678)逻辑说明combinations(tags, r)生成所有可能的标签组合如[long,image]生成long、image、long_image覆盖单维度和交叉维度分析需求sorted(combo)确保image_long和long_image统一为后者。参数说明content_len 50是经验值可根据业务调整本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询