京东评论爬虫全链路:采集清洗入库到SQL可视化分析 📅 发布时间:2026/9/12 9:06:45 👁 浏览次数: 简介京东评论爬虫项目适合作为数据库课程设计参考覆盖数据采集、清洗、可视化与分析全流程。资源包共21个文件包括Python爬虫脚本、4个Jupyter Notebook分析案例、京东与淘宝两组CSV评论数据集、数据库课程设计报告docx/pdf、相关图片和字体文件整体压缩包约23.88MB目录结构清晰。项目中演示了requests与BeautifulSoup抓取评论、pandas处理缺失值与重复项、matplotlib绘制评分分布并利用SQLite/MySQL等工具进行数据存储完整呈现数据处理管道。数据分析部分还使用jieba分词和SnowNLP进行情感倾向分析可直观看到正负面评价分布代码中增加了异常处理与请求间隔兼顾采集效率与站点合规性。同时提供爬虫脚本与报告文档便于对照学习已有2653人学习该资源能帮助初学者快速上手Web数据采集与数据库应用的综合实践。1. 京东评论爬虫把“采集-清洗-可视化-分析”做成一条能答辩的数据链路京东评论爬虫听起来只是一个入门练手项目但放到数据库课程设计场景里难度并不在“爬”而在后面的链路数据抓回来容易能不能洗干净、按什么表结构入库、用哪些 SQL 挖出有用信息才真正拉开差距。京东评论走公开 JSON 接口不需要模拟登录就能抓但返回内容里模板话术、脱敏昵称、缺失字段和不同规格评价混在一起恰好把数据清洗、数据分析和数据库设计里的常见坑一次覆盖。这篇文章按课程设计最常用的技术栈组织requests 采集、Pandas 清洗、MySQL 入库、SQL 聚合与 ECharts 可视化最后给出答辩前必做的数据验证方法。适合正在找数据库课程设计项目或者想独立走通 Python 爬虫全链路的人。2. 京东评论采集接口解析、requests 拉取与翻页限速2.1 京东评论真正的数据源是 productPageComments 接口打开京东任意一个商品详情页按 F12 进入开发者工具切到 Network 面板再点击一次“商品评价”页签会看到很多异步请求其中名字带 productPageComments 的那个接口就是评论数据源。它返回 JSON 而不是 HTML意味着不需要用 XPath 或 BeautifulSoup 解析页面直接用 requests 请求 JSON 再取字段即可。这属于 requests 爬虫里典型的“接口型采集”和抓静态 HTML 的“页面解析型”相比最大优点是字段名明确后续入库省去选择器维护成本。请求参数里最需要弄明白的是 productId、score 和 sortType。productId 是商品 SKU ID决定抓哪款商品score 控制评分区间0 是全部、1 是差评、2 是中评、3 是好评、4 是追评sortType 控制排序方式5 是推荐排序6 是时间排序做时间趋势分析时建议固定用 6。page 从 0 开始pageSize 单页条数保持 10 即可不要试图调大京东接口并不支持自定义大页面。参数作用课设建议值productId商品 SKU ID纯数字不带 .html 后缀score评分区间0 全部 / 3 好评 / 1 差评sortType排序策略5 推荐 / 6 时间建议 6page页码索引从 0 开始从 0 递增到 maxPagepageSize单页评论条数10isShadowSku影子 SKU 开关0fold折叠长评开关1响应结构里comments 数组存放单页评论本体maxPage 字段是最大可翻页数productCommentSummary 里包含好评率、总评价数等汇总信息。翻页和后续完整性校验都依赖 maxPage所以第一页请求成功后先把 maxPage 打印出来观察它决定了整个采集循环的终值。提示score 参数在接口不同版本里可能有细微变化动手写代码前先在浏览器里手动切换“差评”页签看请求负载里的 score 是几再写进代码。2.2 requests 拉取第一页评论的最小可跑代码下面这个函数能直接拿到某一页评论的 JSON。把 URL 和查询参数拆开params 以字典形式传入好处是翻页时只改 page 一个值后续切换 score 或 sortType 也不用改 URL 拼接逻辑。import requests def fetch_jd_comments(product_id, page0, score0): url https://club.jd.com/comment/productPageComments.action params { productId: product_id, score: score, # 0全部 1差评 2中评 3好评 4追评 sortType: 6, # 6按时间排序5按推荐排序 page: page, pageSize: 10, isShadowSku: 0, fold: 1, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: fhttps://item.jd.com/{product_id}.html, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()这段代码的逻辑requests.get 会自动把 params 字典拼接成查询字符串避免手动拼接出错。headers 里两个字段要特别注意User-Agent 是服务端识别客户端的依据课设里用桌面浏览器常见 UA 即可不必伪装成移动端Referer 指向对应商品详情页很多接口会校验来源漏填时可能返回空 comments。timeout10 不是总超时时间而是连接和读取各自最多等 10 秒防止某个异常页卡住整个采集流程。如果返回的 JSON 里 comments 为空先别急着调低频率按顺序检查三件事productId 是否纯数字、score 是否拼错、resp.url 是否被 302 跳走。把这些信息用 print 打出来大部分参数问题一眼就能定位盲目改 headers 反而浪费时间。2.3 翻页循环、随机限速与“爬虫并发设计到底哪个好”单页跑通后翻页采集就只剩一个循环。京东评论列表不是无限加载超过 maxPage 后 comments 会返回空数组所以正确做法是先请求第 0 页拿到 maxPage再以它为循环终值而不是固定请求 20 页。import time import random def collect_all_comments(product_id, max_pages20): all_comments [] first fetch_jd_comments(product_id, page0) max_page min(first.get(maxPage, 1), max_pages) for page in range(max_page): data fetch_jd_comments(product_id, pagepage) # 风控响应可能不是完整JSON做防御性判断 if comments not in data: print(fpage {page} 返回异常停止采集) break all_comments.extend(data[comments]) # 随机间隔比固定间隔更不容易被识别出机器节奏 time.sleep(random.uniform(1.2, 3.6)) return all_comments这里有个参数取舍要说明random.uniform(1.2, 3.6) 生成的是 1.2 到 3.6 秒的随机等待时间不是固定 sleep 2 秒。随机延迟的意义在于打破固定节奏因为脚本最明显的特征就是每隔相同时间发一次请求。max_pages 参数是安全帽避免某次接口异常导致 maxPage 过大把循环拖得无限长。关于“爬虫并发设计到底哪个好”这个问题课设场景我的答案很明确不要并发。requests 加顺序 sleep 是最优解几千条评论量级下几分钟就能抓完并发带来的加速根本体现不出来。线程池或 asyncio 的真正价值在几十万条数据采集场景而那种场景下频控压力会成倍放大反而更容易触发风控。答辩时用“数据量小、控制请求频率优先”来解释设计取舍比硬说“我要展示高并发”更有说服力。遇到页面被风控拦截时响应体往往不是正常 JSON而是包含“验证”“风险”字样的 HTML或者一个缺少 comments 字段的空壳结构。在 collect_all_comments 里做一次判断就能兜住大部分异常如果返回数据里没有 comments 字段打印异常页并停止等到人工介入降低频率后再重跑。这里没有万能参数能绕开风控能做的就是提前做好失败退出机制让采集过程不产生不可恢复的脏数据。3. 数据清洗与 MySQL 入库Pandas 规则和表结构设计3.1 评论 JSON 里哪些字段值得入库采集到的是原始 JSON一条评论的大致字段包括 id、content、creationTime、score、nickname、productColor、productSize、usefulVoteCount、imageCount。课程设计阶段id、content、creationTime、score 四条无论做什么分析都得保留productColor 和 productSize 只有在你想分析“哪个规格差评多”时才需要usefulVoteCount 是这条评论被点“有用”的次数适合说明评论质量差异。字段名原始类型入库建议id字符串主键必留content字符串清洗后进入 TEXT 字段creationTime字符串转成 DATETIMEscore整数转成 TINYINTnickname字符串脱敏仅用于 user 表productColor字符串按分析需求选留productSize字符串按分析需求选留常见脏数据有三类content 为空字符串属于用户没写字只打分content 是“此用户未填写评价内容”“默认好评”这类模板话术没有任何分析价值nickname 被脱敏成 j***d 的格式无法还原真实用户。清洗不是把全部空值删掉就行而是区分“可剔除”和“可保留”空内容和模板话术直接剔除因为对词频和情感分析没有贡献脱敏昵称虽然看不出真实姓名但可以作为用户唯一标识入库用于按用户去重或统计“一个用户发了多少条评论”。3.2 Pandas 加数据清洗和处理去重、去噪、格式化Pandas 处理这种表格式 JSON 非常顺手。我习惯把清洗拆成三步去重、去噪、格式化。去重必须以评论 id 为准不能以 content 为准否则会把不同用户写的相同内容误删去噪包含模板词过滤和 HTML 标签清理格式化则是把时间、分数统一成标准类型。import pandas as pd import re df pd.DataFrame(all_comments) # 1. 按评论ID去重保留首次出现的记录 df df.drop_duplicates(subsetid, keepfirst) # 2. 过滤空内容和模板话术 df df[df[content].fillna().str.len() 2] df df[~df[content].str.contains(此用户未填写评价内容|默认好评, naFalse)] # 3. 清理HTML标签和HTML实体字符串 def clean_text(s): s re.sub(r[^], , str(s)) s re.sub(r[a-zA-Z#0-9];, , s) return s.strip() df[content_clean] df[content].map(clean_text) # 4. 时间字符串转标准datetime解析失败置为NaT df[create_time] pd.to_datetime(df[creationTime], errorscoerce) # 5. 删除无法解析时间的记录 df df[df[create_time].notna()]这套清洗规则可以直接抄但有几处要按实际数据微调。pd.to_datetime(errorscoerce) 会把解析不了的时间置为 NaT后续配合 notna() 过滤比逐行 try/except 高效得多。content 长度下限设为 2 是经验值如果你抓的商品大量出现“好”“赞”这类单字评价可以放宽到 1但会带来更多无意义噪声。第三步的正则是先删 HTML 标签再删 HTML 实体顺序不能反如果先删实体标签字符串里的 符号可能已经被替换成别的内容正则就匹配不上了。数据清洗规则不是越多越好过滤条件每一步都要能回答“为什么”测试数据集是后面验证阶段的重要据。3.3 三张表的数据库设计product、user、comment数据库课程设计最忌讳一张大表装所有字段那体现不出建模能力。建议拆成 product、user、comment 三张表一个商品有多条评论一个用户也能发多条评论把公共信息抽出来正好形成一对多关系。CREATE DATABASE IF NOT EXISTS jd_comment DEFAULT CHARSET utf8mb4; CREATE TABLE product ( sku_id BIGINT PRIMARY KEY, product_name VARCHAR(255) NOT NULL DEFAULT , category VARCHAR(64) DEFAULT , url VARCHAR(512) DEFAULT ) ENGINEInnoDB; CREATE TABLE user ( user_id BIGINT AUTO_INCREMENT PRIMARY KEY, nickname VARCHAR(128) NOT NULL ) ENGINEInnoDB; CREATE TABLE comment ( id VARCHAR(64) PRIMARY KEY, sku_id BIGINT NOT NULL, user_id BIGINT, content TEXT, score TINYINT NOT NULL, create_time DATETIME NOT NULL, useful_vote_count INT DEFAULT 0, product_color VARCHAR(64), product_size VARCHAR(64), INDEX idx_sku_score (sku_id, score), INDEX idx_create_time (create_time), CONSTRAINT fk_comment_product FOREIGN KEY (sku_id) REFERENCES product(sku_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有几个点值得在答辩时展开。第一comment.id 直接使用京东评论 ID它是字符串型主键不能自增如果换用自增主键就必须给京东 ID 单独建唯一索引否则重复爬取时无法做幂等。第二score 用 TINYINT 而不是 INT评论分数只有 1 到 5TINYINT 占 1 字节语义上也更准确。第三索引不是乱建的idx_sku_score 支撑“某个商品各评分人数”的聚合查询idx_create_time 支撑按天统计评价数的时间趋势分析这两个索引正好对应第四章的 SQL答辩时能把它们串起来讲。3.4 用 pymysql 批量写入避免逐条 insert数据落库用 pymysql 连接 MySQL执行业务用 executemany 批量插入。很多新手在这里写 for 循环逐条 insert几百条数据时看不出问题一旦过万逐条提交会明显变慢还会产生大量短事务。executemany 一次提交多行配合 ON DUPLICATE KEY UPDATE 实现幂等重跑采集任务时同一批评论不会插出重复行。import pymysql conn pymysql.connect( hostlocalhost, userroot, password123456, databasejd_comment, charsetutf8mb4 ) rows df[[id, sku_id, content_clean, score, create_time, useful_vote_count]].values.tolist() sql INSERT INTO comment (id, sku_id, content, score, create_time, useful_vote_count) VALUES (%s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE content VALUES(content), score VALUES(score) with conn.cursor() as cur: cur.executemany(sql, rows) conn.commit() conn.close()关键点在于占位符统一用 %s即使插入的是数字也这样写pymysql 会自动完成类型转换。commit 放在 executemany 之后一次性提交不要每条都 commit。如果数据量特别大可以每 5000 行分块一次既控制事务大小又能在出错时把数据损失限制在一个块内。入库后先用 SELECT COUNT(*) 和采集到的评论条数对一遍数量不一致时优先看主键冲突被 ON DUPLICATE 覆盖的记录类型而不是急着怀疑清洗逻辑。4. 数据分析与可视化SQL 聚合、Pandas 统计与 ECharts 出图4.1 先定分析目标再决定用哪张图和哪句 SQL可视化是清洗和入库后的效果出口但很多人上手就画图结果画出一堆没有结论的图表答辩被问“这说明什么”就答不上来。正确的顺序是先定义分析目标再写 SQL最后才画图。课程设计最常用的三个目标是评分分布是否健康、评论量随时间如何变化、差评集中在哪些规格。分析目标SQL 聚合维度推荐图表评分分布score 分组计数柱状图 / 饼图评论时间趋势按天计数、按天平均值折线图规格差评分布productSize 交叉 score横向柱状图 / 堆叠柱状图这张表可以直接拿去做课程设计文档里“数据分析方案”的目录它说明你理解了数据可以从哪些维度切入而不是零散地东拼西凑几张图。4.2 评分分布和时间趋势的两条核心 SQL下面的查询是整套可视化最核心的两条 SQL直接从 comment 表取数。-- 评分分布用于柱状图或饼图 SELECT score, COUNT(*) AS cnt FROM comment GROUP BY score ORDER BY score; -- 时间趋势按天聚合评论量和平均评分 SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, AVG(score) AS avg_score, COUNT(*) AS cnt FROM comment GROUP BY day ORDER BY day;第一条 SQL 用 score 分组idx_sku_score 索引的左侧前缀能加速这种查询数据量大时区别非常明显。第二条用 DATE_FORMAT 把 DATETIME 截断到天再按天聚合评论数和平均分这是时间序列可视化最常见的预处理方式。如果发现按天粒度太稀疏可以把格式串改成 %Y-%m 按月聚合折线图的曲线会更平滑。4.3 Python 把 SQL 结果转成 ECharts 需要的格式ECharts 不认识 SQL需要在 Python 侧把查询结果转成 x 轴数组和 y 轴数组。用 pd.read_sql 直接执行查询比游标循环取值更简洁返回的 DataFrame 自带列名后续转换工作量很小。import pandas as pd import json def load_sql(sql): conn pymysql.connect( hostlocalhost, userroot, password123456, databasejd_comment, charsetutf8mb4 ) df pd.read_sql(sql, conn) conn.close() return df score_df load_sql( SELECT score, COUNT(*) AS cnt FROM comment GROUP BY score ORDER BY score ) payload { x: score_df[score].astype(str).tolist(), y: score_df[cnt].tolist() } print(json.dumps(payload, ensure_asciiFalse))输出结果类似 {x: [1, 2, 3, 4, 5], y: [102, 356, 210, 988, 2345]}。为什么要把 score 转成字符串因为 ECharts 的 category 类型 x 轴要求数据是字符串直接传数字会走 value 轴横轴刻度会变成不连续的分段。这一步也是前端调试时最容易出现问题的地方打开浏览器控制台看到 x 轴为空优先回来检查 payload 里的字段名是否与前端代码完全一致。4.4 用 ECharts 画评分分布柱状图和差评词云前后端接口约定好之后前端的柱状图只需要一个 setOption。先从 ECharts 官网下载 echarts.min.js 放到页面静态目录再初始化一个 DOM 容器async function drawScoreBar() { const resp await fetch(/api/score_dist); const data await resp.json(); const chart echarts.init(document.getElementById(scoreChart)); chart.setOption({ tooltip: {}, grid: { left: 40, right: 20, top: 30, bottom: 30 }, xAxis: { type: category, data: data.x }, yAxis: { type: value }, series: [{ type: bar, data: data.y, itemStyle: { color: #e83e3a } }] }); } drawScoreBar();tooltip 打开后可以直接交互查看数量答辩演示时比静态图效果好很多。想换成饼图只需要把 series 里 type 从 bar 改成 pie并补上 radius 和 label 配置其余部分不变。如果想复刻企业级数据可视化大屏的布局效果不需要接任何大屏平台同一个 HTML 页面里初始化多个 ECharts 实例用 CSS Grid 划分区块每个图表请求各自的接口即可这是免费数据可视化大屏最常见的落地方式。差评分析还能加一张词云图。把清洗后的内容用 jieba 分词Counter 统计词频再挑出高频词交给 WordCloud 渲染图片。import jieba from collections import Counter def build_word_freq(df, stopwords_set, top_n30): text_pool df[df[score] 2][content_clean] words [] for text in text_pool: words [w for w in jieba.lcut(text) if len(w) 2 and w not in stopwords_set] return Counter(words).most_common(top_n)分词前必须准备好停用词表否则“但是”“还是”“这个”“商品”这类高频无意义词会占据词云前半屏。停用词是按品类动态补充的数码类评论里“手机”“耳机”不一定算噪声要看你想分析的是什么。词云本身只能展示高频词很难单独说明问题所以它通常在答辩里作为可视化大屏上的辅助图形真正论证评分和趋势用的还是前面的柱状图和折线图。5. 答辩前必做的三个验证让评论数据经得起追问5.1 采集完整性用 maxPage 和 COUNT(*) 交叉核对答辩老师很可能问“你抓的数据全吗”交叉核对的标准做法是拿同一个商品把接口返回的 maxPage 乘以 pageSize得到理论评论数再和库里按 sku_id 统计的条数对比。def check_fullness(product_id): first_data fetch_jd_comments(product_id, page0) expected first_data.get(maxPage, 0) * 10 actual load_sql( SELECT COUNT(*) AS c FROM comment WHERE sku_id %s, (product_id,) ).iloc[0][c] return {expected: expected, actual: actual, diff_rate: 1 - actual / max(expected, 1)}差值超过 5% 时最常被漏掉的是 page0 这一页其次是写入阶段主键冲突被 ON DUPLICATE 覆盖后实际入库行数少于采集行数。这个核对脚本比人工翻页数评论可靠得多是把采集和入库两个环节的信任建立起来的第一步。5.2 清洗损失率把“洗得多狠”变成一个可解释的数字清洗不是删得越多越好。每次运行清洗脚本时记录原始条数、去重后条数、最终保留条数并算出一个清洗损失率。我的经验值是把 15% 当作红线损失率超过 15%优先怀疑清洗规则太激进而不是原始数据质量太差比如内容长度下限设得过高或某条正则把正常文本也误删了。print(原始评论数:, len(raw_df)) print(去重后条数:, len(raw_df.drop_duplicates(subsetid))) print(清洗后条数:, len(clean_df)) print(清洗损失率: {:.2%}.format(1 - len(clean_df) / len(raw_df)))清洗损失率是一个很直观的指标写在课程设计报告里能让老师一眼看出你对自己数据质量有量化认知。如果损失率落在 15% 以内再抽样打印 20 条清洗后的文本人工扫一眼确认没有误删清洗环节就算合格。5.3 重复文本检测与一键校验脚本评论 ID 不能用来判断内容是否重复因为京东会给每条评价独立 ID同一用户复制粘贴相同内容会生成多个 ID。所以要按 content 分组来检测可疑的重复评论SELECT content, COUNT(*) AS cnt FROM comment GROUP BY content HAVING cnt 3 ORDER BY cnt DESC LIMIT 10;这条 SQL 的含义是把所有内容完全一致的评论聚在一起只保留出现 3 次以上的文本。如果某个内容反复出现几十次比如“物流很快质量不错”连续刷屏说明这批评论很可能是脚本或批量账号产生的分析时要单独标记必要时排除后重新计算评分分布。最后一件事是把所有校验合并成一个可重复运行的验证函数答辩时让老师直接看运行结果。def verify_dataset(df): checks {} checks[total_count] len(df) checks[dedup_count] len(df.drop_duplicates(subsetid)) checks[empty_content] int(df[content_clean].fillna().eq().sum()) checks[score_out_of_range] int((~df[score].between(1, 5)).sum()) checks[time_missing] int(df[create_time].isna().sum()) assert checks[score_out_of_range] 0, 存在超出1-5范围的评分 assert checks[time_missing] 0, 存在无法解析的时间字段 return checks把 verify_dataset 的输出打印在答辩演示页上用数据去回答“你怎么保证数据可信”比任何口头解释都更有说服力。这套验证动作本身就是数据库课程设计里“完整性约束”和“数据质量”这两个考核点的落地实现。本文还有配套的精品资源点击获取