百度新闻评论爬虫实战:从接口分析到数据清洗与舆情分析
1. 项目概述与核心痛点做舆情分析时经常需要把新闻评论当成样本文本百度新闻聚合了全网大量新闻源评论数据相对集中抓取下来做文本挖掘很方便。这个需求看起来只是“爬个评论”但实际落地时会遇到接口定位、动态加载、数据清洗、反爬应对等一系列问题。这篇文章会把整个项目从零到一讲透适合刚接触网络爬虫、想做舆情分析或文本挖掘的读者参考。先说结论百度新闻的评论数据并不是直接写在静态 HTML 页面里的而是通过页面内部的 XHR 接口动态加载。只要找到这个接口用 Python 的 requests 库模拟请求就能稳定拿到结构化的 JSON 评论数据。整个过程不依赖浏览器自动化工具速度快、资源占用低也更容易做分页抓取和增量更新。1.1 这个需求到底要解决什么问题抓取新闻评论的直接目标是收集用户在某条新闻下的公开评论内容。但从项目角度看这背后往往隐藏着三个层次的诉求。第一是文本数据积累。训练情感分析模型、做舆情监测、判断用户对某个事件的态度都需要大量真实文本。新闻评论是天然的高质量语料带有明确的主题标签适合做分类、聚类、情感倾向分析。第二是观点趋势追踪。一条新闻从发布到热度消退往往只有几天评论数量、点赞数量、评论时间分布可以反映舆论走势。如果能把评论按时间维度持续抓取下来就能画出一条热度曲线这对运营同学和产品经理非常有价值。第三是用户画像与互动行为研究。评论中的用户昵称、点赞数、回复数、地域信息等字段虽然不能直接代表用户身份但聚合后可以分析评论者的活跃时段、情绪倾向和互动偏好。只是这块需要特别注意隐私边界不能过度解读更不能用来定位到具体个人。1.2 适合谁参考这套方案如果你是刚入门爬虫的 Python 新手这个项目非常适合做练手。新闻评论接口属于典型的半公开 JSON API不像电商平台那样有高强度反爬也没有复杂的加密签名。通过它你可以完整走一遍“分析网络请求→构造参数→解析数据→清洗入库”的爬虫标准流程。如果你已经在做数据分析或舆情相关的工作这套方案可以直接改造成定时任务配合后续的情感分析和可视化模块形成一套轻量级的舆情监控工具。整个过程只用到 requests、pandas 这些常用库不需要上 Scrapy 或分布式采集系统维护成本很低。1.3 整体技术路线盘点整个抓取项目的核心链路可以拆成五个环节页面分析、接口定位、数据请求、数据清洗、结果存储。页面分析的目的是弄清楚评论数据从哪来接口定位是在开发者工具里找到真正返回评论内容的 XHR 请求数据请求是用 Python 模拟这个请求并处理分页数据清洗是对拿到的 JSON 做去重、去空、格式整理结果存储则是把最终数据落成 CSV 或 SQLite方便后续分析使用。我推荐用 requests 加 BeautifulSoup 的组合而不是直接用 Selenium 或 Playwright。原因很简单requests 直接走 HTTP 接口速度更快资源占用更低代码也更简洁。只有在接口做了明显的前端加密或请求参数依赖 JS 计算时才需要考虑浏览器自动化方案。百度新闻评论接口对普通抓取没有做变态的限制用 requests 完全够用。2. 准备工作技术选型与环境搭建动手写代码之前先把开发环境和技术栈确认好。别小看这一步环境不一致导致的报错往往比业务逻辑本身更让人头疼。2.1 为什么选 Python 而不是其他语言这个项目本质上是个轻量级数据采集脚本用 Python 是最优选择。原因有三个。第一是生态成熟。requests 处理 HTTP 请求、BeautifulSoup 和 lxml 解析 HTML、pandas 做数据清洗与统计这套组合在爬虫领域打磨了十几年踩坑经验充分遇到问题搜一下基本都有答案。第二是上手门槛低。整个脚本核心代码不超过一百行不需要像 Java 或 Go 那样关心类型定义、编译打包和并发模型。你只需要按顺序把逻辑写清楚一个 .py 文件就能跑完整个流程。第三是后续分析方便。抓完数据大概率要做词频统计、情感分析、时间序列分析这些在 Python 里都有现成的库。如果你用 Go 或 Node 抓数据最后还得转一手数据给 Python 处理徒增工作量和转换出错的风险。2.2 依赖安装与环境验证先用 conda 或 venv 建立一个干净的 Python 3.9 环境避免和系统自带的 Python 环境互相干扰。建好环境后安装下面几个依赖pip install requests beautifulsoup4 lxml pandas如果后面要做分词和情感分析可以再补两个库pip install jieba snownlp装完后跑一个快速验证脚本确保所有库都能正常导入import requests import bs4 import lxml import pandas as pd print(requests version:, requests.__version__) print(bs4 version:, bs4.__version__) print(pandas version:, pd.__version__)这一步看着多余但实际能帮你提前暴露很多环境问题。最常见的是 lxml 库在某些 Python 版本下没有预编译包需要手动编译这时候直接换用 bs4 的 html.parser 解析器也能跑只是速度会慢一些。2.3 开发者工具快速定位评论接口评论数据是动态加载的打开网页直接查看源码看不到评论内容。所以第一步必须在浏览器开发者工具里找到真正的请求地址。操作流程是这样的在 Chrome 或 Edge 中打开一条百度新闻页面按 F12 打开开发者工具。切到 Network 面板刷新页面。在筛选框输入comment或news关键字缩小范围。在请求列表中找到返回 JSON 数据的 XHR 请求。点击该请求在 Preview 或 Response 标签页里确认返回结果中是否包含评论内容。我第一次抓的时候走了弯路花了大半个小时在 HTML 里翻找评论内容结果一无所获。后来才反应过来新闻页面的评论模块是页面加载完成后才通过异步请求渲染上去的直接在源代码里找等于刻舟求剑。所以请记住遇到动态页面第一反应应该是打开 Network 面板找 XHR而不是去解析 HTML。找到评论接口后重点记录三个信息请求的 URL、请求方式GET 还是 POST、请求参数列表。这三个信息是整个抓取脚本的核心依据。3. 评论接口分析与数据抓取实现接口定位完成之后接下来就是核心的代码实现阶段。我会从接口分析和代码编写两个维度展开把每一步的原理和坑都讲清楚。3.1 接口地址和参数的确定方法以百度新闻某个新闻详情页为例打开开发者工具后你能看到一个返回评论数据的接口。这个接口通常是一个 GET 请求URL 的大致格式是https://comment.api.news.baidu.com/v1/comments这样。请求参数一般包括这几个字段参数名含义示例值news_id新闻唯一标识MIWk4Yo...page_num页码从 1 开始1page_size每页数量通常取 20 或 5020sort排序方式按时间或按点赞数timetoken某些接口需要的鉴权令牌动态值这里面最关键的是news_id。每个新闻页面对应一个唯一的 news_id它通常出现在新闻页 URL 的参数里或者埋在页面的 JavaScript 变量中。你可以通过两种方式拿到它。第一种方式是从新闻详情页 URL 里直接提取。如果 URL 中带有idxxx或newsIdxxx这类参数直接截取即可。第二种方式是从页面 HTML 中找到初始化数据。搜索news_id或comment_id关键字通常在某个window.__INITIAL_STATE__或>import requests import pandas as pd import time def fetch_comments(news_id, max_pages10, page_size20): 抓取指定新闻的评论数据 :param news_id: 新闻唯一标识 :param max_pages: 最大抓取页数 :param page_size: 每页评论条数 :return: DataFrame base_url https://comment.api.news.baidu.com/v1/comments headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: fhttps://news.baidu.com/, Accept: application/json, text/plain, */*, } all_comments [] for page in range(1, max_pages 1): params { news_id: news_id, page_num: page, page_size: page_size, sort: time, } try: resp requests.get(base_url, paramsparams, headersheaders, timeout10) resp.raise_for_status() data resp.json() # 注意这里的路径需要根据实际 JSON 结构调整 comments data.get(data, {}).get(comments, []) if not comments: print(f第 {page} 页没有更多评论提前结束) break for c in comments: all_comments.append({ comment_id: c.get(comment_id), content: c.get(content), user_name: c.get(user_name), user_location: c.get(user_location, ), like_count: c.get(like_count, 0), reply_count: c.get(reply_count, 0), create_time: c.get(create_time), }) print(f第 {page} 页抓取完成累计 {len(all_comments)} 条评论) except requests.exceptions.RequestException as e: print(f第 {page} 页请求失败: {e}) break # 控制请求频率避免对服务端造成压力 time.sleep(1) return pd.DataFrame(all_comments) if __name__ __main__: # 这里替换成你要抓取的新闻 news_id df fetch_comments(你的_news_id_占位符, max_pages5, page_size20) if not df.empty: df.to_csv(baidu_news_comments.csv, indexFalse, encodingutf-8-sig) print(f已保存 {len(df)} 条评论到 baidu_news_comments.csv)这段代码虽然简单但工程细节值得说几句。resp.raise_for_status()会在返回状态码不是 200 时主动抛出异常避免你拿到一个错误页面还在傻傻解析。break逻辑保证了当某一页没有评论时及时终止循环不会白等。time.sleep(1)是给请求之间的间隔留出缓冲既能降低被封风险也能减小对方服务器的压力。3.3 分页与防重复的细节处理分页抓取最怕两件事重复和遗漏。重复的原因通常是接口参数没传对导致第 1 页和第 2 页返回了相同的数据。遗漏的原因则是评论总数超过预设页数或者接口在翻页时对页数有上限限制。我在实际项目中解决重复问题的方法是给评论的comment_id做唯一性校验。每次拿到新数据后先用一个 set 记录已有的 comment_id新数据进来时只有 id 不在 set 里才加入最终结果。这样即使某个页面因为网络抖动重复请求了两次最后的结果也不会出现重复。seen_ids set() for c in comments: cid c.get(comment_id) if cid and cid not in seen_ids: seen_ids.add(cid) # 添加到结果列表至于遗漏问题最稳妥的方式是响应字段中如果返回了总评论数 total就用 total 除以 page_size 计算出实际总页数然后动态决定循环次数。比如 total 156page_size 20总页数就是 ceil(156/20) 8循环 8 次即可。import math total data.get(data, {}).get(total, 0) total_pages math.ceil(total / page_size) real_pages min(max_pages, total_pages) if max_pages else total_pages这样可以避免因为预设页数不足导致数据抓不全。我在早期版本里就吃过这个亏设定了 max_pages3结果某条热门新闻评论有 120 条3 页只能抓到 60 条数据量少了一半后面的分析结果自然失真。3.4 抓取频率与限速策略爬虫的核心伦理之一是不要过度打扰目标站点。百度新闻虽然是公开资讯平台但也不意味着可以无节制地并发请求。我的习惯是默认每次请求间隔 1 到 2 秒如果抓取的量比较大间隔拉长到 3 秒。在高频请求被拒绝后不是加大频率硬撞而是停下来观察响应头和错误码判断是否有验证码或封禁策略。一个更妥善的做法是在脚本里加入随机延时。固定间隔看起来像机器行为随机延时更接近人的操作节奏。import random # 在每次请求之间使用随机延时 time.sleep(random.uniform(1, 2.5))延时不是为了骗过反爬而是为了体现对服务资源的尊重。很多反爬机制本质上是在惩罚无节制的访问行为如果你自己把节奏控制好大部分情况下不会被限制。4. 数据清洗与简单分析把评论抓下来只是第一步真正要拿到能用于分析的数据还得做一遍清洗。新闻评论这种 UGC 内容天然带着各种“脏数据”不处理干净后面的词频统计和情感分析都会被带偏。4.1 评论数据的常见脏数据我整理了一下实际抓取中遇到的问题类型大概有下面几类问题类型具体表现对分析的影响空内容评论内容为空字符串或只有空格占用统计结果需删除HTML 标签残留内容中包含br、amp;等字符影响文本清洗结果Emoji 表情内容中包含各种 Emoji 字符导致 JSON 解析异常或分词错误重复评论用户刷屏或抓取去重不彻底放大情绪指数和词频权重无效符号纯符号、纯数字的噪声评论干扰模型训练用户名敏感信息评论中可能包含手机号、微信号有隐私风险需要脱敏这些数据不清理直接用 pandas 读进去做统计轻则结果不准重则报错。例如 pandas 在处理某些特殊 Unicode 字符时可能出现编码问题如果不做清洗导出 CSV 后打开就乱码。4.2 清洗脚本的实现思路下面是一个针对性很强的清洗函数覆盖了上面提到的几类问题import re import html def clean_comment(text): if not text: return # 去掉 HTML 实体字符比如 amp; - text html.unescape(text) # 去掉 HTML 标签 text re.sub(r[^], , text) # 去掉 URL text re.sub(rhttp[s]?://\S, , text) # 去掉多余空白 text re.sub(r\s, , text).strip() # 去掉纯符号或纯表情的内容 if re.fullmatch(r[\W_], text): return # 只是简单地过滤手机号和微信号防止敏感信息被采集后滥用 text re.sub(r1[3-9]\d{9}, [手机号], text) text re.sub(r微信[号]?[:]?\s*\w{6,20}, [微信号], text) return text这个函数的核心思路是按顺序做四件事反转义、去标签、去空白、过滤敏感信息。每个步骤都不复杂但顺序不能乱。如果先去空白再去 HTML 标签可能出现标签删除后留下大量空行还得再清洗一次。Emoji 的处理方式取决于你的分析目标。如果做情感分析Emoji 本身也带有情绪价值建议保留并用特殊标记替换。比如把笑脸表情统一替换成[微笑]把哭脸替换成[流泪]这样既保留情绪信号又不会在分词阶段变成无意义的乱码。4.3 用 pandas 做基础统计与导出数据清洗完成后就可以进入分析环节。对于一个只有几百条评论的样本用 pandas 做简单的统计足够发现很多规律。df pd.read_csv(baidu_news_comments.csv) df[clean_content] df[content].apply(clean_comment) df df[df[clean_content] ! ] df df.drop_duplicates(subsetcomment_id) # 基础统计 print(总评论数:, len(df)) print(平均点赞数:, df[like_count].mean()) print(评论时间范围:, df[create_time].min(), -, df[create_time].max()) # 按点赞数排序找到热度最高的评论 top_comments df.nlargest(5, like_count) print(top_comments[[user_name, like_count, clean_content]]) # 导出清洗后的数据 df.to_csv(baidu_news_comments_cleaned.csv, indexFalse, encodingutf-8-sig)做词频统计时推荐配合 jieba 分词代码非常简洁import jieba from collections import Counter all_words [] for content in df[clean_content].tolist(): # 去掉停用词可以有效提升词频统计的有效性 words [w for w in jieba.cut(content) if len(w) 1 and w not in stopwords] all_words.extend(words) word_freq Counter(all_words).most_common(20) print(word_freq)这套流程跑下来你至少能回答几个基本问题用户在聊什么话题、什么内容被点赞最多、评论的高峰时段在什么时候。这些信息对舆情判断非常有帮助。5. 常见问题与排查技巧爬虫项目最耗时间的地方往往不是写代码而是排错。我把实际操作中遇到过的高频问题整理成下面的排查表希望能帮你节省几个小时的摸索时间。5.1 请求返回 403 或被重定向问题表现代码发出去的请求返回状态码 403或直接被重定向到验证码页面。排查思路先确认请求头是否完整。很多接口会校验 User-Agent 和 Referer如果这两个头缺失或异常服务端会直接拒绝请求。我在代码里加上的两个 Headers 字段就是针对这种情况。如果加了 Headers 还不行打开浏览器开发者工具对比一下自己代码里发出去的请求和浏览器发出的请求看看是不是少了 Cookie 或额外的鉴权字段。有些接口的 Cookie 是从页面初始化时种下的第一次请求需要先访问新闻页拿到 Cookie再带着 Cookie 去请求评论接口。这种情况下用 requests.Session 来维持会话是最直观的解法。session requests.Session() # 先访问新闻页拿到必要的 Cookie session.get(news_url, headersheaders, timeout10) # 再请求评论接口 resp session.get(base_url, paramsparams, headersheaders, timeout10)注意如果对方上了验证码或行为校验建议停下来。这不是绕不过去的问题而是从合规角度讲强行突破验证码的风险远高于采集数据的收益。5.2 页面评论是动态加载直接解析 HTML 拿不到问题表现用 BeautifulSoup 解析新闻页 HTML结果页面里完全没有评论内容。排查思路确认你是不是在直接访问新闻页的静态 HTML。百度新闻的评论是通过异步请求加载的HTML 源码里没有评论数据是正常的。正确的做法是打开开发者工具的 Network 面板找到返回评论的 XHR 请求直接请求这个接口而不是解析 HTML。绝大多数动态加载页面的数据都可以在 XHR 请求里找到新闻评论尤其如此。如果你在 Network 面板里找到了评论请求说明数据源是接口直接走 requests 就好。如果确实找不到单独的评论接口而是页面通过 JavaScript 渲染完成那就需要考虑 Playwright 或 Selenium 了。5.3 评论内容包含特殊编码或 Emoji问题表现抓到的评论里出现\uXXXX之类的 Unicode 转义或者\ud83d开头的异常字符pandas 写入 CSV 时直接报编码错误。排查思路\uXXXX其实是 JSON 标准中的 Unicode 编码表示Python 的 json 库会自动解码成正常的 Unicode 字符一般情况下不需要额外处理。真正麻烦的是 Emoji这类字符在 UTF-8 编码中是 4 字节字符而某些老旧的 Windows 终端或 Excel 环境默认用 GBK 处理 CSV打开时就会乱码。解决办法有两个。第一个是在保存 CSV 时指定编码为utf-8-sig这样 Excel 打开时就能正确识别 UTF-8 编码不会出现乱码。第二个是对于 Emoji如果不是分析的重点可以在清洗阶段直接过滤掉。# 过滤掉 4 字节以上的扩展 Unicode 字符 def remove_emoji(text): return re.sub(r[\U00010000-\U0010FFFF], , text)5.4 断点续抓与日志记录问题表现抓取少量数据没什么问题一旦要抓几千条评论或连续跑几小时中途程序崩溃或网络波动会导致已经抓到的数据丢失。排查思路从一开始就设计好“断点续抓”和“日志记录”机制而不是等到出了问题再补。最简单的做法是把每次请求到的数据立即追加写入 CSV 文件而不是攒到最后一次性写入。import csv with open(comments.csv, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[comment_id, content, like_count]) for c in comments: writer.writerow(c)同时用 logging 模块记录每次请求的 URL、状态码和异常信息import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(crawler.log), logging.StreamHandler()] )这样即使半夜程序挂掉第二天看一眼日志就能知道断在哪一页从那一页继续跑就行不用从头再来。6. 合规红线与数据使用边界爬虫不是“能爬就行”怎么用数据才是更重要的问题。新闻评论抓取涉及的合规问题值得每个做数据分析的人提前想清楚。6.1 抓取前必须确认的几件事第一确认目标网站的 robots.txt。通过https://news.baidu.com/robots.txt可以查看站点声明的抓取规则。虽然是纯技术层面的约定但遵守它是一个负责任的爬虫工程师的基本修养。第二确认网站服务条款中关于数据采集的说明。有些平台在用户协议里明确禁止未经授权的数据采集行为这时候即使技术上可以抓也不应该抓。如果项目确实是商业或学术用途建议联系平台方申请 API 或授权。第三只抓取公开可访问的数据。评论既然能对所有人展示抓取它本身通常不构成对隐私的侵犯。但这不代表你可以随意对所有用户生成画像并公开传播分析结果。尤其是用户名和评论内容组合起来可能间接指向真实身份需要特别谨慎。6.2 评论数据的隐私与使用限制抓取新闻评论时最常见的个人数据字段是用户昵称。昵称本身不算敏感个人信息但与 QQ 号、微信号、手机号等账号信息关联后情况就完全不同了。所以我在清洗步骤里加了手机号和微信号过滤逻辑这个操作不是技术问题而是意识问题。还有一点需要特别提醒评论内容可能包含用户的真实姓名、工作单位、家庭住址等敏感信息。在公开的舆情报告中引用这些内容时一定要做脱敏处理把能识别到具体个人的信息全部去掉。哪怕是所谓的“网友评论”也要把它当成真实用户的表达来尊重。我不建议你抓取评论后用于任何形式的精准推送、用户画像变现或可能损害用户权益的场景。新闻评论数据的正确打开方式是做群体情绪分析和舆论倾向研判而不是针对某个用户做个体标签化。把边界划清楚这个项目走得远你自己也睡得着觉。7. 扩展玩法从文本到价值基础抓取流程跑通后这个项目的真正价值才刚刚开始。我给你梳理了几个可以直接扩展的方向按投入产出比排个序。7.1 舆情情感分析用训练好的情感分析模型对评论文本打分可以直观看到用户对新闻事件的正负面情绪比例。国产的 snownlp 库虽然很小但做中文本体情感分析是够用的。from snownlp import SnowNLP sentiments [] for content in df[clean_content]: try: score SnowNLP(content).sentiments sentiments.append(score) except Exception: sentiments.append(0.5) df[sentiment_score] sentiments # 情感倾向大于0.6为正向小于0.4为负向其余为中性 df[sentiment_label] df[sentiment_score].apply( lambda x: positive if x 0.6 else (negative if x 0.4 else neutral) )抓取评论时同时保留 create_time 字段情感分数和时间结合后可以画出一条“情绪随时间变化”的曲线观察事件发酵过程中舆论情绪的转折点。7.2 定时增量抓取舆情分析的核心是连续观察不是一次性快照。把抓取脚本改造成定时任务每隔一小时或一天跑一次只抓新增评论就能形成长期趋势数据。最简单的方案是用系统自带的 cron 或 Windows 任务计划程序。脚本内部通过记录最后抓取到的评论时间或最大 comment_id来实现增量更新。复杂一点的可以用 Airflow 或 DolphinScheduler 管理定时任务但对于个人学习项目来说完全没必要。7.3 技术栈迁移与工程化如果你后面发现自己对爬虫这件事真感兴趣可以把这套 Python 脚本升级成 Scrapy 框架获得更好的并发调度和增量处理能力。也可以尝试用 Go 重写抓取逻辑Go 的并发模型在处理大规模抓取时优势明显部署成单一二进制文件也更方便。我在另一个小项目里就尝试过用 Go 写一个类似的新闻评论采集器。虽然代码量比 Python 多了一些但编译后只有一个可执行文件放在服务器上跑非常干净。尤其是并发抓取多个新闻源时Go 的 goroutine 比 Python 的多线程要稳得多资源占用也小不少。但这个扩展方向的前提是你已经把 Python 版本的抓取逻辑、数据清洗和分析流程完全吃透了。换语言只是换工具分析思路和项目经验是通用的。最后分享一个小技巧抓取脚本跑完之后不要急着关掉终端。先随机挑几条评论回到新闻页面人工核对一下评论内容和点赞数确认数据准确。这步“人工采样验收”看着笨但能帮你发现接口字段映射错误、数据截断等隐蔽问题。我在多个项目里靠这个动作发现了接口里部分高赞评论被服务端截断返回的 bug否则数据清洗阶段怎么处理都补不回来。抓数据从来不只是“拿到就行”拿得准、拿得全后面的分析才有意义。