
做舆情分析那阵子接了个挺实在的需求品牌方想搞清楚自家新品发布后用户在新闻评论里到底在吵什么。我第一反应就是去抓百度新闻的评论——它聚合了大多数主流媒体评论区的活跃度和覆盖面都不错比单抓某家媒体更有参考价值。可真正上手才发现百度新闻的评论接口藏得比想象中深字段是动态拼接的分页走的是游标机制请求一频繁还会被风控盯上。这篇文章就把从定位接口、解析数据到落库分析的完整链路讲一遍也把那些踩过才知道的坑一次说清楚。适合刚接触网络爬虫的同学也适合想在舆情监测里补充评论维度的数据分析师参考。1. 新闻评论数据能做什么为什么我先盯上百度1.1 评论区是离真实舆论最近的地方新闻正文代表的是编辑立场评论区代表的才是用户真实的情绪和槽点。做舆情如果只看转发量和点赞数很容易被标题党的传播数据带偏。反复出现过的情况是一篇稿子在微博上没起水花但评论区里已经吵了几百楼这些声音才是品牌下一轮传播需要回应的东西。我当时的做法是抽了三个竞品的发布新闻每篇抓500条评论跑完一轮词频统计续航焦虑这个词条的占比高得吓人品牌部看到后把投放重点立刻转向了续航对比方向。这个结论如果单看阅读量数据是得不出来的。评论区还有一个价值在于它的即时性。新闻发布后的前几个小时评论往往最鲜活用户还在第一反应阶段措辞里带着最原始的情绪。等热度过了评论区会被各种转载、营销号内容填充分析价值反而下降。所以评论抓取一定要趁热如果把任务排到第二天再跑拿到手的样本已经变味了。1.2 选百度新闻的理由与取舍对比过一圈之后我最终把百度新闻当作第一个落地的平台看重的是它几个特点聚合源多同一个热点事件下页面上会聚合几十家媒体的报道评论区天然集中不用逐个媒体站去扒出稿效率高很多。用户画像有互补性百度新闻评论区的画风和微博、知乎明显不一样更多普通用户的直观感受噪音和真实感并存对舆情分析来说反而更有价值。技术门槛相对适中没有强制短信验证码登录这个墙也没有太变态的滑块验证但也不是完全敞开参数拼接和游标分页需要花时间研究。当然它也有短板单条新闻的评论量通常比微博话题少时效性好的头部内容才有大规模评论区冷门新闻可能一条评论都没有。我的经验是先在一个平台上跑通全流程再复制到其他平台而不是一上来就追求全网覆盖。百度新闻作为第一个练手对象数据量刚好压力不会太大。整套逻辑理清楚之后换到其他平台只是接口字段和参数名变了分析框架完全能复用。2. 评论数据不在页面里在Network面板里这里我要先纠正一个容易走偏的思路不要对着网页源码里那些HTML去正则抠评论因为百度新闻的评论列表是异步加载的直接抓HTML只能拿到加载更多这个按钮真正的内容在背后的XHR请求里。第一步永远是打开开发者工具看网络请求。2.1 用Chrome开发者工具定位评论请求操作流程很简单但顺序别乱否则容易盯着瀑布流里几十个请求发懵打开一篇有评论的新闻按F12进入开发者工具切到Network面板过滤条件选Fetch/XHR。清空面板然后滚动到评论区点加载更多评论。观察新增的网络请求优先找名字里带comment、getCommentList、reply这类关键词的请求。右键这个请求Copy → Copy as cURL先粘到本地或Postman里跑一遍确认能拿到数据。这个定位方法对绝大多数异步加载的网站都适用一次学会之后抓微博评论、抓资讯类App对应的Web版思路都一样。唯一要注意的是有些网站做了请求合并或接口网关名字可能跟评论完全不沾边这时候就挨个看响应内容哪个返回的JSON里含评论关键词哪个就是目标。2.2 评论请求URL的参数骨架定位到请求之后你会看到URL带了一串参数。以我当时抓到的结构举例具体字段会随版本变化但骨架逃不出这个套路GET /comment/api/list ?newsurlxxxx cursor0 size20 type1几个关键点newsurl是当前新闻页的URL编码用来告诉接口我要的是哪篇文章的评论cursor是游标第一页填0下一页取上一轮返回里的next_cursorsize是每页条数我在实测里一般用10到20太大会触发风控type在不同接口版本里含义不一样有时区分评论类型有时是排序方式需要抓包后试着改才知道。只看参数还不够请求头同样重要。Referer必须指向那篇新闻页否则接口会直接返回403User-Agent不要用默认的Python-requests伪装成Chrome的浏览器UA能少很多麻烦。携带Cookie与否取决于当前新闻的评论是否需要登录才能看全多数公开新闻是不用的。2.3 返回JSON里藏着的字段接口返回的JSON结构大体是{ data: { comments: [ { comment_id: 123456, nickname: 用户_abc, content: 这个功能确实解决了我一直以来的困惑, like_count: 32, create_time: 2024-05-12 10:23:11 } ], next_cursor: eyJvZmZzZXQiOjIwfQ, has_more: true }, errno: 0, errmsg: }拿到数据后我建议先别急着写解析先把原始JSON完整dump到文件里肉眼扫一遍字段名和层次。你会发现评论的楼层关系楼中楼、点赞数、回复数都有对应字段先摸清楚再动手比边写边试高效得多。另外要有一个心理准备同一批字段名平台调整过好几次如果你的请求参数和网上教程对不上不要慌按2.1的办法重新抓包一切以当前实际请求为准。网上很多教程的最大问题就是拿两年前的接口讲今天的事照着抄必翻车。3. 最折腾的三个问题动态参数、游标分页和风控社区里常见的求助帖大多是卡在这三个点上。我把排查思路和最终采用的方案展开讲。3.1 动态参数不是非破解不可评论接口URL里偶尔会出现一两个动态生成的参数不同版本位置不一样有时在query里有时在Header里。遇到这种参数我的建议是先用最笨的办法搜打开新闻页HTML源码搜参数名看它是不是藏在某个全局变量里。很多所谓动态参数其实是从页面预渲染数据里拿出来的正则直接抠就行根本不需要去逆向JS。如果确实遇到前端JS在运行时计算出参数也不建议去硬刚混淆代码。更省事的方案是用Playwright或Puppeteer这类无头浏览器打开新闻页让页面自己执行JS然后拦截评论XHR响应直接从响应里拿数据。代码看起来多一层但稳定性远远高于拆JS逻辑。我的原则是能用渲染结果解决的问题绝不去逆源代码不然版本一更新就要重写。3.2 游标分页的循环逻辑老式分页是page_num加page_size百度新闻评论用的是cursor游标。这个游标常见形态就是一串base64编码的字符串你不需要读懂它只需要在每次请求后把它原样取出来带给下一次请求就行。循环逻辑其实只有四行核心代码cursor 0 while True: data fetch_comments(news_url, cursor) items data[data][comments] if not items: break save_all(items) cursor data[data][next_cursor] if not data[data][has_more]: break结束条件有两个返回的评论列表为空或者has_more为false。任何一个满足都说明到头了继续请求只会白白增加风控风险。这里有一个细节拉完一页必须停下来随机睡2到5秒这个延迟不仅是为了礼貌更是为了让对方服务器觉得这是一个正常用户在慢慢翻评论而不是程序在高速扫描。用固定延迟容易被人一眼看穿随机延迟的效果好得多。3.3 风控识别与应对节奏请求频率一高评论接口不会直接提示你被限制了而是会安静地给你返回一个空列表。这是最容易误判的情况我当时排查了半个小时还以为是这个新闻真没人评论直到换了个干净浏览器环境访问才发现评论明明还在。典型的风控信号和应对次序整理成了一张表信号常见表现第一步应对返回空列表明明有评论接口返回comments: []停掉程序隔至少5分钟再试HTTP 429请求太频繁服务器明确拒绝按指数退避拉长等待时间不要死循环重试HTTP 403请求头不完整被判断为非法来源检查Referer和UA补全Cookie后再试验证码响应响应正文里出现verify、captcha字样彻底停手说明频率已经严重超标我个人的底线是单任务跑批时单IP下并发不超过2默认单线程加随机延时。很多风控不是因为你做了什么高深操作而是太贪。抓够要用的量就主动停这是做数据抓取的基本修养。尤其新手最容易犯的错是抓完评论还想把作者主页、历史评论全扒一遍这种越界行为大概率会把整个IP送进黑名单。4. 跑通一个完整抓取工程代码与落库过程讲完思路给一套可以直接抄来改的代码骨架。它不绑定某个具体接口版本核心结构是稳定的。4.1 目录结构和依赖项目只拆了三个文件不搞复杂框架baidu_comments/ ├── crawler.py # 请求和解析主逻辑 ├── config.py # 新闻URL清单和抓取参数 └── db.py # SQLite操作依赖就三样requests、pandas可选做数据分析时用、sqlite3Python内置不需要装。选SQLite而不是MySQL是因为评论数据量级通常不会大到需要独立数据库服务单文件存储加简单SQL查询够用还方便交付。真到了数据量超过几百万条的那天再迁移也不迟。4.2 带重试的请求函数请求函数里有一个容易被忽略的细节不要直接用requests.get()裸调要复用session。Session会自动保存服务端返回的Cookie后续请求带上同一个会话身份连续性会好很多。再加一个带指数退避的简单重试避免网络抖动导致整批数据白跑import requests import time session requests.Session() session.headers.update({ 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: https://news.baidu.com/ }) def fetch_comments(news_url, cursor, retry3): params { newsurl: news_url, cursor: cursor, size: 20, type: 1, } for attempt in range(retry): try: resp session.get(https://comment.example.com/comment/api, paramsparams, timeout10) if resp.status_code 200: return resp.json() if resp.status_code in (403, 429): wait 10 * (2 ** attempt) # 指数退避10秒、20秒、40秒 time.sleep(wait) except requests.RequestException: time.sleep(5) return None注意代码里的接口地址是演示用的你抓包后看到什么就填什么关键是重试和退避的骨架不变。timeout10这个参数很关键不写的话遇到服务端假死程序可能卡几分钟没反应。4.3 解析评论并落库拿到JSON数组后逐条清洗并写入SQLite。建表时我习惯把comment_id设为主键这样重复抓取时可以用INSERT OR IGNORE自动跳过天然去重。import sqlite3 from datetime import datetime def init_db(): conn sqlite3.connect(comments.db) conn.execute( CREATE TABLE IF NOT EXISTS comments ( comment_id TEXT PRIMARY KEY, news_url TEXT, nickname TEXT, content TEXT, like_count INTEGER, create_time TEXT, fetched_at TEXT ) ) conn.commit() return conn def save_comments(conn, news_url, comments): now datetime.now().strftime(%Y-%m-%d %H:%M:%S) for item in comments: conn.execute( INSERT OR IGNORE INTO comments (comment_id, news_url, nickname, content, like_count, create_time, fetched_at) VALUES (?, ?, ?, ?, ?, ?, ?), (item[comment_id], news_url, item[nickname], item[content], item[like_count], item[create_time], now) ) conn.commit()入库时content字段建议顺手做一次strip清理去掉首尾空白。表情符号可以保留至少不要在上游清洗过多因为后面做情感分析时表情本身也是情绪信号。4.4 批量抓多篇新闻的断点续跑设计单篇跑通之后把需求放大到几百篇新闻就会遇到断点续跑的问题。我的做法是维护一个news_urls.txt每篇新闻抓完就往finished.txt里追加一条启动时先读finished.txt跳过已经完成的URL。这样中途断电、断网、被风控暂停下次直接续跑不用重新来一遍。with open(news_urls.txt, encodingutf-8) as f: all_urls [line.strip() for line in f if line.strip()] with open(finished.txt, encodingutf-8) as f: done set(line.strip() for line in f) for url in all_urls: if url in done: continue crawl_one_news(url) done.add(url) with open(finished.txt, a, encodingutf-8) as f: f.write(url \n) time.sleep(random.uniform(5, 10))这个模板看着简单但非常实用我所有的抓取脚本都套它。另一个小技巧是在crawl_one_news内部把单篇新闻的抓取结果也拆成小文件单独保存万一单篇跑到一半挂了重跑时至少能确认哪些页已经成功不用盲目继续。5. 评论到手之后的清洗和粗粒度洞察数据落库只是开始。新闻评论是典型的短文本噪声占比不低不洗直接用会得到一堆误导性结论。5.1 清洗的三板斧去重、去HTML、去刷屏第一斧是去重。评论里大量一模一样的复制粘贴常见于营销号或刷屏党直接用GROUP BY content HAVING COUNT(*) 1先筛出来人工确认。第二斧是去HTML偶尔有评论会带上超链接标签用正则把.*?清掉。第三斧是过滤纯符号评论比如只有一串句号、一串问号的这类对分析没有贡献直接标记为无效。import re def clean_comment(text): text re.sub(r[^], , text) # 去HTML标签 text re.sub(r\s, , text).strip() # 压缩空白 if len(text) 2: return if all(ch in 。…~ for ch in text): # 纯符号过滤 return return text清洗逻辑里还有一个容易被忽略的点同一句话在不同新闻下的重复出现不一定是刷屏可能是用户习惯性地用固定句式表达。这类半重复用简单的完全去重处理不掉需要引入文本相似度。但在项目初期完全去重已经能把数据质量提升一个档次不要在一开始就追求完美。5.2 情感倾向先用词典打个底情感分析我推荐从最朴素的词典法起步。别看它简单在评论这种短文本上极性词数量不多但指向非常明确用正负词典统计往往比复杂的模型更直观。常见的做法是各准备一批词遍历评论里的关键词打分pos_words {好, 赞, 喜欢, 优秀, 满意, 信赖} neg_words {差, 烂, 坑, 垃圾, 失望, 投诉} def sentiment(text): p sum(1 for w in pos_words if w in text) n sum(1 for w in neg_words if w in text) if p n: return positive if n p: return negative return neutral需要明确一点这种词典法是粗粒度的适合自己发现趋势不适合直接当客户报告的依据。如果需求是正经的舆情项目建议在词典法跑完之后抽样50条人工复核看看准确率是否达标不够再上更重的模型。我自己踩过的坑是拿词典法结论直接写进报告结果被客户拿原始评论反问场面很难看。5.3 主题聚合词频与共现找槽点清洗完、打好情感标签后下一步通常是想知道大家在具体讨论什么。用jieba分词加Counter统计词频就够用不需要上LDA主题模型这种重武器。统计完高频词挑出跟产品、事件相关的名词比如电池价格售后再倒回去看原始评论每类随手翻10条基本就能判断出主要的正面和负面槽点。这种量化筛选加人工复核的组合在舆情分析里最实用也最不容易出错。词频统计有个容易误导人的地方高频词不等于高价值词。像这个真的觉得这类词永远排在最前面却没有分析价值。记得先加载一份停用词表把代词、助词、数字全部过滤掉再看结果。我用的是网上公开的通用中文停用词表再手动往里加了几个跟新闻评论场景强相关的词比如文章楼主小编效果会好很多。6. 抓取公开评论的边界可以做的事和不建议越的线最后聊一聊合规和分寸。爬虫技术本身是中性工具但用得好不好取决于边界感。6.1 什么是可以安全抓取的公开数据公开可见、无须登录即可阅读的评论属于公开数据抓取用于学习、研究和正当的舆情分析通常是被允许的。但如果一条评论需要登录后或者输入验证码才能看到那就说明平台在技术上表达了不想被机器访问的态度这类内容不应该去绕也不值得去试。另外很多网站的robots.txt里会写明对爬虫的约束抓之前花一分钟看一眼这是基本尊重。实际操作里还有一个判断标准如果你抓的数据会让一个普通人感到私人信息被暴露了那就越界了。比如把用户昵称、头像、主页地址全部打包转售这种明显是不该做的事。我做舆情分析时一般只保留评论正文、情感标签和发布时间用户身份信息能少存就少存。6.2 把抓取当成人肉浏览而不是机器扫荡我给自己定过几条硬规矩单IP下并发不超过2随机延时不低于2秒单篇新闻评论拉满后用不着反复刷每天抓取总量设上限。时刻提醒自己每一条请求都在消耗对方服务器的计算资源程序跑着不觉得背后是真实的带宽和CPU。遇到429或者空列表正确做法是停下来歇一阵不是换参数继续怼。你是在做数据分析不是在做攻击测试。如果需求方坚持要越快越好我会在项目排期阶段就明确告诉他服务器不是无限资源几万条评论要分几个小时来跑这是正常节奏。凡是催你半小时内抓完一百万条的需求本身就值得怀疑——要么数据量没他说得那么大要么他并不了解抓取的基本约束。6.3 数据解读要自带样本边界说明抓到的评论只是愿意在公开新闻下发言的那部分人的观点它代表不了所有用户更不代表这个社会的全部意见。做舆情分析报告时我习惯在结论里写明本结论基于XXX篇新闻的XXXX条公开评论样本集中于特定话题和时间段。这句话既是对数据的诚实也是对自己结论的保护。忽略样本偏差的结论再漂亮也有翻车风险。评论区的数据还有一个典型的幸存者偏差喜欢发声的人往往情绪更强要么非常满意要么非常不满中性态度的用户很少留言。所以看到负面占比60%的时候不要直接下结论说大部分用户不满意更准确的说法是在主动发表评论的用户中表达负面态度的比例偏高。这个表述差异客户可能感受不到但专业和非专业的差距就在这里。最后再说说个人体会。做百度新闻评论抓取这个需求真正让我觉得值回票价的不是跑通的代码而是从几千条评论里看出用户对一个产品最强烈的情绪集中在哪里。技术链路不复杂难的是控制住自己多抓一点快一点的冲动。抓得稳、守得住边界这套流程可以成为你以后所有评论类数据需求的基础模板。如果你也准备上手我的建议是先拿一篇评论数不多的新闻练手把接口和流程跑顺再慢慢放大不要一上来就追求全网数据。这个节奏能让你少走很多弯路。