Python爬虫实战:通过JSON接口采集电影短评并落地CSV

发布时间:2026/10/13 20:54:53

Python爬虫实战:通过JSON接口采集电影短评并落地CSV 简介以豆瓣短评抓取为实战场景的Python爬虫与数据分析资料包面向希望系统练习网络请求、页面解析、数据清洗与可视化的初中级学习者。项目以病毒和疾病题材电影短评为对象完整演示了构建爬虫获取HTML内容利用解析库提取短评正文和评分字段同时处理User-Agent、Cookie等常见反爬限制随后用Pandas完成数据去重、缺失值填充、文本预处理利用分词库进行词频统计最终通过词云图直观展示高频关键词。压缩包约7.84MB内含项目源码、抓取到的原始数据以及生成的可视化图表并采用RAR格式打包目录结构较为清晰方便分模块对照学习每一步实现逻辑。已有1205人学习下载可帮助读者快速掌握爬虫开发、数据清洗和可视化输出的综合技能也能作为模板迁移到其他电影评论或类似文本采集任务中。1. 爬取短评前先认清一件事数据通道比解析技巧更重要很多新手拿到python爬取短评这个需求第一反应是去抓 HTML 页面。实际做下来你会发现真正卡住你的往往不是解析代码而是请求被拦、翻页翻不动、数据对不上。这个标题要做的事情很具体把某电影社区的短评批量拿到本地存成结构化文件供口碑观察、文本挖掘或数据练习。用 Python 抓短评适合两类人拿真实中文文本做 NLP 练习的开发者以及需要持续观测某部影片口碑的数据从业者。我的做法是绕过渲染后的 HTML直接找浏览器加载短评时调用的 JSON 接口。它返回结构化字段、自带游标解析成本低得多。后面几章就按这条线展开先定位接口再写请求再翻页再落盘最后把坑挨个讲清。2. 定位数据通道短评页背后的 JSON 接口与请求头准备2.1 页面渲染只是表象真正有料的是 new_comments 接口在浏览器打开任意一部电影的短评页先别急着看页面结构。按 F12 进 Network 面板刷新页面在 XHR 过滤里能看到一个名为 new_comments 的接口请求。它的响应不是 HTML而是一个 JSON 对象。这个 JSON 里既带评论数组也带下一页游标 cursor。页面上的评论只是这个接口数据的渲染结果。抓 HTML 再解析需要面对页面结构改版、标签层次变动、评论折叠等问题。而直接请求这个接口字段是稳定的content 是正文rating 是评分对象useful_count 是有用数author 是用户名。我一般会先打开这个接口把请求里的 Query String Parameters 完整拷贝下来再换到 Python 里复现。这样第一页跑通后后面所有工作都围绕这个 JSON 展开比解析 HTML 稳健得多。为什么不建议用无头浏览器或自动化工具成本高、稳定性差而且更容易触发服务端的流量识别规则。一个干净的 HTTP 请求加上正确的参数是这个场景下性价比最高的方案。接口的典型请求地址形如https://movie.example.com/j/new_comments?subject_id{影片ID}cursor{游标}start0limit20statusPsortnew_score把这个请求里的关键参数拆开看每个都对应页面上的一个筛选条件参数含义常见取值subject_id影片在站内的唯一 ID短评页 URL 路径里能看到任意整数cursor翻页游标第一页为空之后用上一页返回的 cursorbase64 字符串start / limit分页偏移量一般 start0、limit200 / 20status只看何种状态的评论P 表示看过影片后写的短评Psort排序规则new_score 按时间hot 按热度new_score / hot这几个参数里最容易忽略的是 status。不带它接口偶尔会混入无评分或状态不一致的数据导致最终统计评分分布时对不上页面。sort 则会影响你能拿到的评论集合按时间抓到的和按热度抓到的并不是同一批想扩大数据规模两个排序都可以各抓一遍再按评论 id 去重。2.2 最小请求脚本让第一页短评落进 Python确认接口之后写一个能跑通的最小脚本。依赖只要 requests 和标准库 json解析暂时不做先把数据拿到打印出来。import requests # 目标影片的 ID可在短评页 URL 路径里找到 subject_id 999001 url https://movie.example.com/j/new_comments # 请求头User-Agent 必须和真实浏览器一致否则大概率被拦 headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 ), Referer: fhttps://movie.example.com/subject/{subject_id}/comments, } params { subject_id: subject_id, # 影片 ID cursor: , # 第一页游标为空 start: 0, limit: 20, # 每页条数建议保持 20 status: P, # 只看看过状态的短评 sort: new_score, # 按时间排序hot 是按热度 } resp requests.get(url, headersheaders, paramsparams, timeout10) data resp.json() print(状态码:, resp.status_code) print(返回评论数:, len(data.get(comments, []))) for c in data.get(comments, [])[:3]: print(c.get(author, {}).get(name), c.get(content, )[:30])这段代码有几点需要说明。headers 里的 User-Agent 必须写成完整浏览器标识requests 默认的 python-requests 标识在这个接口前几乎没有例外地会被拒绝。Referer 填短评页地址是为了让服务端认为请求来自站内页面。timeout 设置为 10 秒防止某个请求卡住拖死整个抓取流程。接口返回的 comments 是一个列表每条评论含 author、rating、content、useful_count、created_at 等字段。其中 author 是嵌套对象用户名在 author.name 里rating 也是对象评分值在 rating.value 里范围是 1 到 5。第一次跑通不要做任何加工先把返回结构原样打印出来对照字段名再写后续逻辑。如果这一步就报 418说明请求头或频率有问题先把 UA 换成浏览器完整的字符串再检查是否带上了 Referer。这两件事做对第一页基本没有不通过的道理。想抓更多页时登录态会更稳常见做法是把浏览器里复制的 Cookie 头原样塞进 headersheaders[Cookie] 你的浏览器请求里的完整 Cookie 字符串Cookie 不一定要有但带着它翻页触顶的概率会明显低一些。注意 Cookie 会过期过期后接口返回的数据会悄悄变成空列表或跳转登录页这也是后面避坑章节要处理的一个典型问题。3. 翻页抓全量cursor 接力与循环终止条件的正确写法3.1 为什么不要只靠 start 翻页页面 URL 里能直接改 start20、start40 来翻页早期很多脚本就是这么写的。但很快你会发现start 方式在遇到游客可见上限时会突然失效翻到一定页数接口返回空列表或者直接给你重定向到登录页。这不是你代码写错了而是服务端对未登录访问设置了可见边界。硬翻只会浪费请求数还可能把频率打高。new_comments 接口真正可靠的是 cursor 机制。每次响应里会带一个 cursor 字段它本质上是上一页最后一条评论的位置编码。下一轮请求把 cursor 原样传进去服务端就从那个位置继续给数据。拼接逻辑是上一轮返回什么下一轮就传什么比 start 数字更贴近服务端的翻页状态也不容易绕出边界。cursor 看起来是一长串 base64 字符别尝试手动解码或修改直接原样回传。我的习惯是保留 start 参数但固定为 0主翻页靠 cursor。每次拿到 data 后先取 cursor 存下来再判断 comments 数组是否为空为空就说明到底了。这里有个细节data.get(comments) 为空不仅代表没有数据也可能代表当前访问权限已经踩到上限。两种情况都应当终止循环避免无限请求浪费流量。脚本里可以打印一条提示说明是空列表还是游标丢失方便排查。3.2 完整翻页循环限速、重试与最大页数保护把翻页逻辑封装成一个函数循环里处理游标接力。同时加上随机限速和简单的指数退避重试避免被服务端判定为异常流量。import time import random import requests # USER_AGENTS 在脚本顶部维护 5~6 个真实浏览器的完整 UA 字符串 # 这里每次随机取一个避免单一 UA 被长期识别 USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15, ] def fetch_comments(subject_id, max_pages15, base_interval2.0): url https://movie.example.com/j/new_comments headers { User-Agent: random.choice(USER_AGENTS), Referer: fhttps://movie.example.com/subject/{subject_id}/comments, } cursor all_comments [] for page in range(max_pages): params { subject_id: subject_id, cursor: cursor, start: 0, limit: 20, status: P, sort: new_score, } # 每次请求前随机停顿2~4 秒均匀分布别固定死 time.sleep(random.uniform(base_interval, base_interval 2)) # 失败重试最多 3 次重试间隔翻倍 data None for attempt in range(3): try: resp requests.get(url, headersheaders, paramsparams, timeout10) if resp.status_code 418: # 请求频率异常或 UA 异常 time.sleep(5 * (attempt 1)) # 退避后下次换 UA 再试 headers[User-Agent] random.choice(USER_AGENTS) continue resp.raise_for_status() data resp.json() break except requests.RequestException as exc: print(f第 {page 1} 页第 {attempt 1} 次请求失败: {exc}) time.sleep(5 * (attempt 1)) if data is None: print(多次重试仍失败提前结束) break comments data.get(comments, []) if not comments: print(返回空评论列表结束翻页) break all_comments.extend(comments) print(f第 {page 1} 页抓到 {len(comments)} 条累计 {len(all_comments)} 条) cursor data.get(cursor, ) # 游标接力下一轮请求使用 if not cursor: break return all_comments这个循环里max_pages 是一个安全阀。正常情况下冷门片三五页就结束了热门片能抓十几页但设一个上限可以防止游标异常时脚本无限循环。base_interval 控制请求间隔2 秒是这套接口友好的下限脚本跑完也只是分钟级别的事没必要压缩到毫秒级去赌封禁概率。重试逻辑需要注意 418 的处理。418 出现在服务端认为请求方不是正常浏览器时退避后直接换一个 UA 再试比重试同一个请求更有效。requests 默认会在抛出异常时自动清理连接这里捕获了异常就不要继续深挖具体错误码保持重试三次、每次间隔 5 秒、10 秒、15 秒的节奏即可。另外一个常见误用是开多线程并发抓取。一个线程 5 秒发一个请求可能没事八个线程同时跑就等于每秒钟十几个请求基本跑不过两分钟就会全量 418。短评抓取的数据量不大单线程加随机限速才是能稳定跑完的方案。4. 落盘与字段清洗把短评存成干净 CSV 的实用设计4.1 字段设计先定表结构再谈存储抓到评论数组后直接整包存 JSON 也能用但后续做分析和 Excel 查看都麻烦。我习惯把每条评论压平成一行只保留高频字段。字段设计就一张表字段来源类型说明idcomment.id字符串短评唯一标识去重主键authorauthor.name字符串用户名ratingrating.value整数1~5 星contentcontent字符串短评正文useful_countuseful_count整数点有用的人数created_atcreated_at字符串评论时间sort_source请求参数字符串抓取时用的排序便于回溯rating 在接口里是个对象常见结构是 {value: 5}。有些评论没有评分这时 rating 可能是空 dict 或 None落盘前要统一转成空字符串否则导出 CSV 时会被写成 Python 的 None难看且影响后续统计。评论 id 是唯一的去重依据。翻页、双排序抓取、多日增量都会产生重复评论第一版脚本里就做集合去重比数据落地之后再清洗省事得多。推荐用 set 记 id写入前判断。评论正文的清洗也很关键。正文里常见的是首尾空白、连续空格、偶尔的换行符。换行符在 CSV 里会制造错行哪怕用了引号保护Excel 里查看依然难受。压成单行会损失一点原始格式但对后续文本统计没有任何影响反而让每一行数据更干净。4.2 写入 CSV编码与引号两个坑一次避开CSV 导入 Excel 有两个高频问题中文乱码、含换行的评论把一行撑成多行。第一个问题用编码 utf-8-sig 解决第二个问题靠 csv 模块的 quoting 参数解决。import csv CSV_HEADERS [id, author, rating, content, useful_count, created_at, sort_source] def comments_to_csv(comments, file_path, source): seen_ids set() with open(file_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesCSV_HEADERS, quotingcsv.QUOTE_ALL) writer.writeheader() for c in comments: cid str(c.get(id)) if cid in seen_ids: continue seen_ids.add(cid) rating_obj c.get(rating) or {} writer.writerow({ id: cid, author: c.get(author, {}).get(name, ), rating: rating_obj.get(value, ), content: c.get(content, ).replace(\r, ).replace(\n, ), useful_count: c.get(useful_count, 0), created_at: c.get(created_at, ), sort_source: source, })注意open 时 newline 在 Windows 下必须加否则 csv 写出的文件每两行之间会多一个空行数据量大了非常难处理。encoding 用 utf-8-sig 而不是 utf-8因为 Excel 默认按本地区码解析无 BOM 的 UTF-8中文就乱码了。quoting 设为 csv.QUOTE_ALL 后所有字段都带引号正文里即便有逗号也不会把列撑错。content 里的换行我直接替换成空格。这一步是经验之谈评论正文里偶尔有自然换行QUOTE_ALL 虽然能保证 CSV 结构不错但多行文本在 Excel 里查看仍然很别扭。压成单行会损失一点原始格式但对后续文本统计没有任何影响反而让每一行数据更干净。4.3 JSONL 备份让脚本断了也能接着抓CSV 是给人和 Excel 看的抓取过程中的原始数据我还会额外存一份 JSONL。JSONL 是每行一个 JSON 对象追加写入天然支持断点续抓。import json from pathlib import Path backup_path Path(fshort_comments_{subject_id}.jsonl) def append_backup(comments): with backup_path.open(a, encodingutf-8) as f: for c in comments: f.write(json.dumps(c, ensure_asciiFalse) \n)在 fetch_comments 的循环里每次拿到一页评论就调一次 append_backup。这样脚本中途断网、进程被杀、或者被反爬拦停已经落盘的行都不会丢。重新跑的时候可以从 backup 文件里加载已有的评论 id跳过已抓过的内容实现断点续抓。这个习惯帮我省过不少事。有次我开着脚本抓热门影片第三天凌晨服务端升级导致接口暂时不可用脚本在重试用尽后退出。因为每一页都写进了 JSONL我第二天只需把已抓 id 载入再跑一轮就补齐了缺失页而不是从头再来一遍。5. 避坑短评爬取中 4 个真实踩过的坑5.1 翻到几十条就停游客可见上限与排序策略现象脚本第一页、第二页都很正常到第三四页接口开始返回空列表或者同一批评论反复出现总数卡在某个数字附近不动。原因未登录状态下服务端对单个影片的短评访问有可见边界常见在 200 条左右。另外不同的 sort 排序对应不同的评论子集只抓一个排序天然只能得到该排序下的内容热门的和最新的并不全重合。解决先接受一次只能拿到部分评论这个前提不要试图硬翻。登录态配合有效 Cookie 能提高上限但仍有边界。更实际的办法是同时按 new_score 和 hot 各抓一遍用评论 id 去重后合并能得到更大且更多样的样本。5.2 请求一频繁就 418UA 单一与频率过快的连锁反应现象脚本刚跑 20 页没问题连续开跑半小时后所有请求都返回 418页面在浏览器里打开也需要重新过验证。原因服务端对同一 IP 的请求频次有动态阈值。固定 UA、固定间隔更容易被识别为脚本流量。单一 UA 时即便频率不高也可能触发规则。解决维护五六个真实浏览器的 UA 字符串每次请求随机取一个。睡眠时间用 random.uniform 生成随机值别固定 sleep(3)。遇到 418 不要原地死磕退避 5~10 秒换 UA 重试。短时间抓取任务控制在分钟级不要开 24 小时循环。5.3 评论数和页面对不上status 与 sort 参数不齐现象抓到的总条数和页面底部显示的共 N 条短评差很多或者评分分布明显偏了。原因status 参数没传接口可能返回多种状态的评论混合sort 不传服务端使用默认排序和页面当前选中的排序不一致。两个参数都会影响返回结果集合。解决把参数固定为 statusP、sort 显式指定 new_score 或 hot和页面左上角筛选条件保持一致。核对数据时用浏览器开发者面板复制一条真实请求对比 params 差在哪这是最直接的排错方式。5.4 落盘后文件打不开CSV 编码与换行现象CSV 用 Excel 打开乱码或者评论里有逗号、引号、换行导致每一列错位。原因写文件时用了 utf-8 而不是 utf-8-sigExcel 按本地区码解析出现乱码csv 写入时没用 QUOTE_ALL含特殊字符的字段破坏了列结构。解决代码里固定写入参数与 4.2 节一致encodingutf-8-sigquotingcsv.QUOTE_ALLnewline。内容里的换行在做字段清洗时压成空格一劳永逸。6. 增量与校验让短评数据从能跑变成能信6.1 增量更新用已抓 id 集合过滤新评论口碑观察任务通常要连续几天抓取。我的做法是把评论 id 集合存到一个本地文件里每轮抓取前载入抓完只保留新 id 的评论再更新集合文件。import json from pathlib import Path ID_FILE Path(seen_ids.json) def load_seen_ids(): return set(json.loads(ID_FILE.read_text())) if ID_FILE.exists() else set() def update_seen_ids(seen, comments): for c in comments: seen.add(str(c.get(id))) ID_FILE.write_text(json.dumps(list(seen)))增量逻辑看起来简单但有个细节判断新要用评论 id不能用内容条数。同一部影片每天能新增的短评量有限若只看数量变化判断有没有新数据很容易被热门影片的实时波动误导。6.2 三条数据校验习惯第一抽样核对。每次抓完随机抽 10 条和短评页面前几屏人工对照确认正文、评分、时间三者对得上。第二算重复率。重复率超过 5%说明翻页或游标接力出了问题先查 cursor 逻辑再查去重是否生效。第三看评分分布。一部正常影片的短评分值通常整体偏向 3~5 星如果抓出来的分布出现大量空评分或者集中在一两个数值多半是 status 参数没对齐回第 5.3 节排查。我之前有一次抓完一万多条评论信心满满地拿去做情感分析结果分布图出来一片诡异。后来一查是 Cookie 过期后请求静默降级返回了大量空评分评论。从那以后我养成了一个习惯脚本跑完先打印评分分布和最常用的前几个作者名肉眼看不合理就直接删数据重抓绝不拿脏数据进下游。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/13 20:54:53

Tool Calling 实战指南:从原理到落地的工具调用设计

1. 从一次接口对接的深夜加班说起前阵子帮一个做智能客服的朋友看他们新版本的设计方案,会上有人抛出一个问题:“用户问‘帮我查一下上周三的订单发货了没’,模型怎么知道要去调订单系统的接口,而不是自己编一个‘已经发货了’的答…

2026/10/13 20:54:53

时间复杂度不是考试题,而是程序员的性能直觉

1. 这不是“刷题考试”,而是你写每行代码时都在用的底层直觉 很多人第一次看到“时间复杂度”四个字,脑子里自动弹出的画面是:大学算法课上黑板上密密麻麻的O(n)、O(log n),老师念着“大O表示法”,台下有人偷偷刷手机&…

2026/10/13 20:49:52

构建Agent技能库:从工具调用到可复用执行技能的工程实践

先说个我最近深有体会的事:现在的对话式AI模型,单聊知识问答已经没什么门槛了,真正拉开差距的,是它能不能替你把一件事完整办完。你让它查资料,它能答;你让它订会议室、汇总邮件、跑一遍数据处理脚本&#…

2026/10/13 22:00:03

Pascal if语句嵌套详解:从语法陷阱到重构方法

简介:《pascal-if语句的嵌套》是一份面向Pascal初学者的PDF学习资料,重点讲解条件判断中if语句的嵌套写法与else子句的配对规则,帮助读者理清then与else的匹配关系、避免常见的逻辑歧义。资源共1个文件,即PDF电子文档,…

2026/10/13 22:00:03

C语言词法分析器完整实现:从预处理到符号表的编译原理参考

简介:编译原理课程设计中,词法分析器是编译器前端的关键模块,负责将源代码字符流转换为有意义的单词序列。该文档以docx格式封装,内含完整的C语言词法分析器代码与实现说明,面向计算机专业学生、编译原理学习者及需快速…

2026/10/13 22:00:03

Kepware OPC DA Client连接失败的三大DCOM配置要点

简介:本资源是一份面向工业物联网(IIoT)开发者与自动化工程师的Kepware OPC DA Client实战入门教程,聚焦于使用KEPServerEX实现设备数据采集与监控的核心配置流程。内容覆盖从新建Project、配置Channel(含Optimization…

2026/10/13 22:00:03

开源AI工具本地执行实战:从部署到自动化文件管理

1. 从聊天到干活:这个开源AI工具到底在解决什么问题大多数人第一次接触AI助手,都是从一个对话框开始的。你问它答,聊得挺热闹,但关掉窗口之后,该干的活一样没少——文件还得自己整理,表格还得自己填&#x…

2026/10/13 22:00:03

开源AI工具实战:让大模型从聊天到本地自动化干活

1. 从聊天到干活:这个开源AI工具到底在解决什么问题大多数人第一次接触AI,都是通过对话框。你问一句,它答一句,聊得挺热闹,但聊完之后呢?该手动复制的还得手动复制,该自己打开软件操作的还得自己…

2026/10/13 21:55:03

AI如何重塑芯片物理设计流程:从布局布线到签核优化

1. 这不是科幻,是正在发生的芯片设计革命“AI也能设计芯片了?”——这句话刚在技术圈刷屏时,我正蹲在某实验室的服务器机柜前,盯着一串跑了一整晚的RTL综合日志发呆。旁边桌上摊着新思科技(Synopsys)刚发布…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑