用Python构建微博舆情分析系统:从爬虫采集到情感分析全流程
简介一套基于Python构建的微博数据挖掘与社交舆情分析系统源码面向计科、数据科学与大数据技术、人工智能等专业的课程设计和期末大作业场景。项目以Scrapy框架为底层整合代理池管理、爬虫调度、微博评论数据抓取、舆情分析与可视化展示等模块可直接作为毕设或初期项目立项的完整参考。压缩包内共669个文件以Python源码、JavaScript与Less前端样式、C底层代理/爬虫管理组件为核心另含Dockerfile、Makefile、配置文件与说明文档整体约4.63MB目录结构清晰便于快速部署与二次开发已有508人学习下载。该工程代码功能经过验证下载后可本地运行演示也可参照其中数据采集与舆情分析流程完成课程答辩、功能扩展或算法替换对入门数据挖掘与社交舆情研究具有较高参考价值。1. 微博舆情分析系统值得做吗从一个课程作业到能落地的舆情工具微博每分钟产生的数据量决定了靠人肉观察永远做不了舆情分析。很多课程大作业都会布置一个相似任务用 python 实现微博数据挖掘与社交舆情分析系统。我最初接到这个题目时以为把数据抓到再画个词云就完事。真正动手才发现从采集、清洗、分词到情感判断任何一步出错结果都会翻车这也是这类系统源码看起来简单、跑起来全是坑的原因。这篇笔记会按数据采集、预处理、情感分析、可视化这条主链路把这个系统拆成可复现的步骤。适合两类人第一次用 python 做文本挖掘、想快速跑通主流程的课程项目参与者以及已经有爬虫基础、但对舆情指标计算和工程化细节还不太有把握的同学。相关方案里的关键参数和避坑经验都会在对应章节展开。2. 数据采集层怎么搭微博API与爬虫的选型理由和落库细节2.1 为什么优先选API而不是爬虫稳定性和合规的权衡按实际开发的判断顺序第一步是定采集方案不是写代码。微博开放平台文档里能申请一个开发者应用拿到接口授权后按说明调用。公开接口对搜索微博内容、拉取用户资料都有支持返回字段干净、结构稳定这是理论上的最优路径。但 API 的实际门槛在于申请流程较长应用审核往往要 1 到 3 天而且不同数据接口要分开申请权限日调用次数还有配额。课程大作业的排期一般只剩一到两周等审核下来再动工整体节奏会被拖垮。爬虫方案于是成为课程项目里最常用的做法。抓取目标有两类一类是热搜榜数据量小、更新频繁适合用来观察舆情热点另一类是某条微博下的评论和转发数据量大适合做情感分析的语料。用 python 的 requests 库加合理请求头就能完成基础采集。但这里有一条必须守住的底线请求频率要克制否则 IP 会被服务端限制采集模块直接失效。这个问题的具体表现和解决方式我会在第 5 章单独讲。在开始写代码之前先确定数据流向请求页面拿 JSON解析提取字段清洗后写入 sqlite3 数据库。采集、解析、存储三个阶段用函数隔开后面要换数据源或者换存储方式只需要改对应模块其他部分不用动。2.2 抓热搜和评论的最小代码requests 加延时和重试先给一个抓热搜榜的完整函数。开发时先在浏览器开发者工具里观察热搜页面的网络请求发现热搜数据不在 HTML 源码里而是由一个异步接口返回的 JSON里面包含热搜词、热度值和排名。这种方式属于典型的动态页面数据获取也是现在大多数内容平台采取的渲染方式。import requests import time import random def fetch_hot_search(): 抓取微博热搜榜 返回: list[dict]每个元素含 word/heat/rank 三个字段 url https://weibo.com/ajax/side/hotSearch headers { 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: https://weibo.com/ } try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() data resp.json() except requests.RequestException as e: print(f[错误] 请求失败: {e}) return [] hot_list data.get(data, {}).get(realtime, []) results [] for item in hot_list: results.append({ word: item.get(word, ), heat: float(item.get(num, 0)), rank: item.get(rank, 0) }) return results if __name__ __main__: for _ in range(3): items fetch_hot_search() if items: for it in items[:5]: print(it) # 每次请求之间随机延时避免频率过高 time.sleep(3 random.random() * 2)逻辑说明热搜接口返回的是嵌套 JSONjson() 方法把它解析成字典然后依次取 data.realtime 列表。列表里每个元素对应一条热搜word 是热搜词num 是热度值rank 是排名。请求失败时不抛异常而是返回空列表这样采集流程能及时退出后面的落库逻辑不会拿到脏数据。这是爬虫模块的基本工程习惯采集部分要做到失败可控。参数说明timeout10 表示 10 秒内没有响应就失败避免程序因为网络问题长时间挂起。time.sleep(3 random.random() * 2) 生成 3 到 5 秒的随机等待目的是让请求节奏不像固定频率的机器脚本。对热搜这种低频数据源每 5 到 10 分钟采一次就够不用做成秒级循环那只会徒增被限流的概率。热搜适合观察事件热度但舆情分析还需要评论内容。抓评论的思路一样只是接口和解析逻辑不同。下面的代码演示抓取某条微博评论的写法重点是翻页控制和失败中断。def fetch_comments(weibo_id, max_pages5): 抓取指定微博的评论数据支持翻页 comments [] for page in range(1, max_pages 1): url https://weibo.com/ajax/statuses/buildComments params {id: weibo_id, page: page} headers { User-Agent: Mozilla/5.0, Referer: fhttps://weibo.com/detail/{weibo_id} } try: resp requests.get(url, headersheaders, paramsparams, timeout10) data resp.json() except Exception as e: print(f[错误] 第{page}页请求失败: {e}) break comment_list data.get(data, []) if not comment_list: break for c in comment_list: comments.append({ text: c.get(text, ), like_count: c.get(like_count, 0), created_at: c.get(created_at, ) }) time.sleep(2 random.random()) return comments逻辑说明评论接口通过三个参数控制翻页微博 id、页码和分页大小。这里只提取 text、like_count、created_at 三个字段分别对应后续分词情感分析的原始语料、传播强度参考和时间序列统计。当返回的 data 为空时说明没有更多评论直接跳出循环这是避免无效请求的关键判断。参数说明max_pages 限制最大翻页数防止某条热门微博有几百页评论时程序卡在采集阶段不往后退。time.sleep(2 random.random()) 的间隔比热搜接口更保守因为评论接口被限流后发现问题的成本远高于多等几秒。如果要采集大量评论建议把间隔提高到 3 到 5 秒并记录当前已经抓到的页码下次重跑时从断点继续。2.3 数据落库sqlite3 批量插入与字段设计数据抓到后如果只存在内存里程序一退出就全丢了。课程项目直接用 python 自带的 sqlite3 最方便不需要额外安装数据库服务。下面是建表和批量插入的完整流程。CREATE TABLE IF NOT EXISTS hot_search ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL, heat REAL, rank INTEGER, crawl_time TEXT ); CREATE INDEX IF NOT EXISTS idx_crawl_time ON hot_search(crawl_time);import sqlite3 import time DB_PATH weibo_data.db def save_hot_search(items): 批量写入热搜数据到 sqlite conn sqlite3.connect(DB_PATH) conn.execute(BEGIN) try: rows [ (it[word], it[heat], it[rank], time.strftime(%Y-%m-%d %H:%M:%S)) for it in items ] conn.executemany( INSERT INTO hot_search (word, heat, rank, crawl_time) VALUES (?, ?, ?, ?), rows ) conn.commit() except Exception as e: conn.rollback() print(f[错误] 写入失败: {e}) finally: conn.close()逻辑说明executemany 接收一个列表把每一行批量交给 sqlite 执行比逐条 execute 快一个量级数据量到几千条后差距非常明显。写入操作放在显式事务里先执行 BEGIN 再 commit中途出现异常可以 rollback避免表里留下半截数据。参数说明时间字段存成字符串而不是整数时间戳因为文本格式可读性好SQL 排序和筛选也能直接工作。crawl_time 建索引是因为后面做时间窗口分析时经常按时间范围过滤数据量一旦上万没有索引就是全表扫描。word 字段加 NOT NULL 约束防止解析异常时写入空值影响统计。这里有一个经常被误解的地方热搜表存的是时间切片数据同一热搜词在不同时间点会反复出现爬虫跑一小时就会产生大量重复词条。这不是错误舆情趋势分析靠的就是这条时间线。所以不能按 word 去重要靠 crawl_time 去还原事件热度变化过程。验证数据是否正常落地可以直接用 sqlite3 命令行查sqlite3 weibo_data.db SELECT word, heat, rank, crawl_time FROM hot_search LIMIT 10;后续分析阶段用 pandas 读取比 sqlite3 原生查询更方便import pandas as pd conn sqlite3.connect(DB_PATH) df pd.read_sql_query(SELECT * FROM hot_search, conn) conn.close() print(df.head())pandas 的 DataFrame 后续接清洗和可视化库都很顺手课程项目里建议从这一层开始做分析而不是在采集模块里循环处理数据。3. 文本清洗与分词把微博原始数据变成干净语料的三个步骤3.1 清洗规则为什么微博文本比普通文本难处理微博文本是中文 NLP 里典型的非规范语料不经过清洗直接喂给分词器结果会很惨。噪音来源主要有几类URL 链接、用户昵称、#话题标签#、表情符号以及“转发微博”这类系统后缀。URL 会被分词器切出奇怪字符昵称里的下划线会把词边界打乱表情符号则是不可见的特殊 Unicode 字符如果不处理后面无论是词频统计还是情感打分都会被这些噪音干扰。我习惯用一组正则表达式做统一清洗。顺序有讲究先清 URL 和 用户因为它们包含的冒号、斜杠会干扰后续的字符过滤再处理话题标签我的做法是去掉两边的 # 号、保留话题内部的词因为话题词往往是舆情分析的核心实体最后才过滤表情符号和特殊字符。import re def clean_weibo_text(raw_text: str) - str: 清理微博原始文本中的噪音字符 # 去除URL text re.sub(rhttps?://\S, , raw_text) # 去除用户昵称 text re.sub(r[\u4e00-\u9fa5a-zA-Z0-9_\-], , text) # 去掉#话题#外层的井号但保留话题里的关键词 text re.sub(r#([^#])#, r\1, text) # 过滤表情和特殊符号只保留中文、英文、数字、常用标点和空白 text re.sub( r[^\u4e00-\u9fa5a-zA-Z0-9。、\s], , text ) # 合并连续空白 text re.sub(r\s, , text).strip() return text逻辑说明清洗函数按“去链接 → 去用户 → 去话题标记 → 去特殊符号 → 整理空白”的顺序执行。话题标签那一步用分组引用 r\1 把 # 号去掉、内容保留因为话题词本身就是事件实体。最后一步的字符过滤写得很严格凡是中文、英文、数字和常用标点之外的字符全部移除。参数说明正则里的 [\u4e00-\u9fa5] 是中文的 Unicode 范围a-zA-Z0-9 保留英文和数字。保留的标点符号可以根据后续需求增减比如做情感分析就不需要保留问号感叹号但保留它们对句式判断有参考价值。还有一个容易忽略的点清洗函数要放在程序入口处统一执行一次不要在每次分词时重复调用那是浪费算力。清洗前后的效果大致是这样原始文本清洗后文本快来看 https://t.co/abc 这个视频娱乐小助手 #新歌首发# 太好听了 [泪]快来看这个视频 新歌首发 太好听了如果清洗后还有“转发微博”“分享图片”这类系统文本残留再加一条正则把它们去掉即可。这类固定后缀在微博语料里很常见不去掉的话“图片”这种词会高频出现在所有关键词统计里严重干扰分析结果。3.2 分词、去停用词和关键词抽取jieba 的调参细节清理干净后进入分词阶段。python 生态里最常用的中文分词库是 jieba它提供精确模式、全模式和搜索引擎模式。对舆情分析来说精确模式是默认选择它会在保证词边界准确的前提下输出最可能的切分结果。全模式会输出所有可能的词但存在大量重叠做统计时会造成词频虚高搜索引擎模式是在精确模式基础上对长词再切分适合检索场景做舆情任务会增加噪音。下面演示分词加停用词过滤的完整写法。注意 jieba 的词典加载要在程序启动时做一次不要在循环里反复加载那会拖慢整个分析流程。import jieba # 加载停用词表 stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: word line.strip() if word: stopwords.add(word) # 加载自定义词典 jieba.load_userdict(weibo_dict.txt) def tokenize(text: str): 分词并过滤停用词只保留长度大于等于2的词 words jieba.cut(text, cut_allFalse) # 精确模式 filtered [ w for w in words if w not in stopwords and len(w.strip()) 1 ] return filtered逻辑说明jieba.cut 在精确模式下返回一个生成器不会一次性把所有结果加载进内存。过滤条件有两层第一层按停用词表过滤去掉“的、了、吗、啊、在、是”这类高频但无语义贡献的词第二层做长度过滤去掉单字词。对微博这种短文本单字词在统计时噪音极大直接舍弃后关键词分布更稳定。参数说明cut_allFalse 是精确模式也是 jieba.cut 的默认值。如果代码里看到 cut_allTrue那是全模式不要用在统计场景。停用词表是纯文本文件每行一个词utf-8 编码。通用中文停用词表在网上能搜到但还要根据项目语料追加特定词比如“微博”“转发”“分享”这类几乎每条文本都会出现的词不加入停用词表它们会一直霸占高频词的位置。分词完成之后通常还要提取关键词看这个热搜事件在讲什么。常用算法是 TF-IDF 和 TextRank两者在 jieba.analyse 模块里都内置了。TF-IDF 适合提取文档集中的代表性词TextRank 适合单篇文本的关键短语抽取。做舆情事件分析我优先用 TF-IDF因为它对高频通用词的惩罚更明显。import jieba.analyse def extract_keywords(text: str, top_k: int 10): 用 TF-IDF 提取关键词按词性过滤 keywords jieba.analyse.extract_tags( text, topKtop_k, allowPOS(n, nr, ns, nt, v, a) ) return keywords逻辑说明allowPOS 参数让关键词抽取只保留指定词性的词目的是过滤掉副词、介词、语气词这些没有实体意义的成分。n 是名词nr 是人名ns 是地名nt 是机构名v 是动词a 是形容词。对舆情事件来说人名、地名、机构名往往就是核心实体。参数说明topK10 表示只保留权重最高的 10 个词。这个值取决于用途预览 10 个够用如果要送入情感分析特征可以提升到 20 到 30。特别注意如果语料量大不要对每条评论单独调用 extract_tags那会非常慢应该先把一个时间窗口内的所有文本拼接再统一抽取一次。3.3 自定义词典和停用词典的迭代方法用 jieba 默认词典处理微博语料会看到一个典型的翻车场景人名被拆散、网络新词被切成无意义的片段比如“迪丽热巴”被分成“迪丽”和“热巴”“奥特曼”被分成“奥”和“特曼”。这种错误会让舆情分析里最核心的实体信息彻底丢失。解决办法是建立项目自己的自定义词典。自定义词典是纯文本文件每行一个词后面可以跟词频和词性例如迪丽热巴 1000 nr 奥特曼 1000 nz 周杰伦 1000 nr词频的作用是告诉分词器这个词在语料中的使用强度词性则给后续关键词抽取保留实体标记。load_userdict 之后这些词在句子里会被当成整体切分出来。但自定义词典不是越大越好把低频词强行加入可能和默认词典产生冲突反而影响切分质量。我的做法是先跑一批数据把分词结果里肉眼可见的切词错误收集起来再批量把新词加入词典。每加一次词典跑一批新数据验证形成迭代闭环。这个循环比直接复制一个大型网络词典有效得多因为只有你自己项目的语料才是判断分词质量最可靠的标准。停用词典的维护也遵循同样的逻辑。通用停用词表覆盖了绝大多数功能词但项目里有些词需要在特定语境下过滤。比如分析娱乐话题时“直播”可能高频出现但对情感判断没有贡献可以加入停用词如果分析的是电商话题“直播”反而是需要保留的重点词。所以停用词表必须跟着语料走不是一份通用表走天下。4. 情感分析与舆情指标计算让文本数据说话的核心计算4.1 情感分类的两种路线基于词典 vs 机器学习模型情感分析是社交舆情分析系统的核心模块。对微博这种短文本密集、口语化程度高的语料所谓情感分析就是判断每条评论的情绪倾向正面、负面还是中性。实现路线大体分两种基于词典和基于机器学习。理解两者的差异不只是精度对比更是数据准备和工作方式的取舍。基于词典方法的核心是维护一个情感词典每个词有预先标注的极性分值比如“优秀”记 1“失望”记 -1。分析时对文本分词把命中的词的分值累加正数判正面、负数判负面、接近零判中性。这个方案的最大优点是部署快、可解释性好任何一步都能算清楚。最大缺点是词典覆盖度跟不上微博的造词速度——今天新出的网络热词如果没进词典分词器会把它拆成两个没有情感意义的部分情感判断直接失效。下面是一个完整的基于词典的实现包含词典加载和打分两个函数。def load_sentiment_dict(path): 读取情感词典格式词语\t情感值 score_map {} with open(path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: word, score parts[0], parts[1] score_map[word] float(score) return score_map def sentiment_score(text, score_map): 基于词典的情感打分返回(分数, 极性) words jieba.lcut(text) pos_words, neg_words set(), set() for w in words: if w in score_map: if score_map[w] 0: pos_words.add(w) elif score_map[w] 0: neg_words.add(w) total sum(score_map[w] for w in pos_words | neg_words) if total 0: return total, positive elif total 0: return total, negative return total, neutral逻辑说明打分函数里用集合做去重再累加这是我处理微博短文本的一个习惯。微博评论通常不到 30 个字同一个情感词出现两次说明用户情绪强但直接把次数乘进去会把强度放大到失真用词型去重再累加相当于统计不同正面词和负面词的数量差异。对长文本这个假设不成立长文里情感词重复次数本身就是强度信号应该用列表不复用。参数说明情感词典文件是纯文本每行“词语 空格 分值”。通用中文情感词典体量很大但直接用到微博场景效果不佳因为“下头”“绝了”这类网络词没收录。课程项目建议先用通用词典跑通流程再看分词结果补充 50 到 100 个热门词效果提升会非常明显。机器学习路线把情感分类当标准监督学习问题来处理。数据准备工作量更大但分类效果上限更高尤其是对网络新词的泛化性更好。典型流程是准备已标注好正、负、中性的文本数据集用 TfidfVectorizer 做向量化然后训练朴素贝叶斯分类器。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB import joblib def train_sentiment_model(texts, labels, test_ratio0.2): 用 TF-IDF 和朴素贝叶斯训练情感分类器 vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), min_df2 ) X vectorizer.fit_transform(texts) X_train, X_test, y_train, y_test train_test_split( X, labels, test_sizetest_ratio, random_state42 ) model MultinomialNB(alpha0.5) model.fit(X_train, y_train) acc model.score(X_test, y_test) joblib.dump(model, sentiment_model.pkl) joblib.dump(vectorizer, vectorizer.pkl) return model, acc逻辑说明TF-IDF 把每条文本转成稀疏向量max_features5000 限制特征数量避免把所有词都纳入导致维度爆炸。ngram_range(1,2) 加入相邻两个词的组合特征这一步很关键因为“不好”和“好”单独看情感方向完全相反只有加入二元组特征模型才能区分“不 好”这种组合。朴素贝叶斯在数据量不大的场景下表现稳定也适合课程项目。参数说明min_df2 去掉只出现一次的稀有词压缩特征空间也减少过拟合。alpha0.5 是平滑系数防止某些特征在训练集中没出现导致概率归零。如果测试集准确率低于 80%先不要急着换模型优先检查标注质量标注不一致的问题比模型选择影响更大。两条路线实际怎么选我的经验是看项目阶段。课程项目早期先跑通数据链路和页面演示用词典法就够等流程稳定了如果手头有标注好的数据再切换到机器学习模型对比一版效果。切换时保留词典法的接口保持输入输出格式一致下游可视化模块不用改。对比维度基于词典机器学习启动门槛需要情感词典需要已标注语料部署速度快中等可解释性高中低对新词的覆盖弱较强更新方式手工加词重新标注训练典型准确率60%75%80%90%4.2 舆情指标怎么算热度指数、情感指数和传播指数情感分类完成之后原始文本已经不是重点重点是汇总成可比较的指标。最常用的三个指标是热度指数、情感指数和传播指数。热度指数综合评论数、点赞数、转发数并加入时间衰减。原因是事件发布越久累计互动量自然越大如果不做时间衰减老事件的热度永远比新事件高这不符合舆情分析的需求。import math def heat_index(comments, likes, reposts, hours_since_publish): 热度指数互动量加权后随时间衰减 # 基础互动得分转发权重最高评论其次点赞最低 base comments * 1.0 reposts * 1.5 likes * 0.3 # 对数时间衰减符合爆发快降温慢的规律 decay 1 / (math.log2(hours_since_publish 2)) return round(base * decay, 2)逻辑说明转发代表二次传播权重最高评论代表参与度权重次之点赞的用户成本最低权重也最低。时间衰减采用对数形式曲线更平滑不会出现线性衰减那种先断崖式下降、后段长期无变化的失真感。hours_since_publish 是发布时刻到当前时刻的间隔必须保证计算前换算到同一时区否则小时数错误会直接导致热度计算翻车。参数说明如果分析目标偏向传播效率把 reposts 的权重提到 2.0如果偏向用户参与度提高 comments 的权重。权重设置没有绝对标准关键是全项目保持一致并且把口径写在文档里不然结果没有可比性。情感指数把正负面判断汇总成一个落在 -1 到 1 之间的数值正面多则接近 1负面多则接近 -1。def sentiment_index(positive_count, negative_count, neutral_count): 情感指数范围[-1,1]1为全正面-1为全负面 total positive_count negative_count neutral_count if total 0: return 0.0 return round((positive_count - negative_count) / total, 4)逻辑说明分母包含中性样本这很重要。实际舆情讨论中大量表达是中性的如果只拿正负相减再除以总数会把中性样本忽略指数会变得过于极端误导后续判断。包含中性样本会让指数向零收敛更符合真实讨论局面。参数说明如果中性样本占比过高比如超过 70%说明情感词典或模型判不准指数已经失真这时候要去查清洗和分词环节而不是调指标口径。传播指数在真实舆情系统里需要转发关系链数据但课程项目往往拿不到完整的转发网络。我一般用代理指标来近似比如“转发量 / 评论量”的比值或者“评论点赞比”。口径相对简单但足以看出一条内容的扩散效率。重点是指标的口径只要定义清楚、前后一致用它做相对比较是成立的真正怕的是概念没想清楚就套一个复杂公式最后谁都算不出来。配合时间序列做聚合是最常用的舆情呈现场景def aggregate_by_hour(df): 按小时聚合并计算情感分布 df[hour] df[crawl_time].str[:13] # 截取到小时 grouped df.groupby(hour).agg( total(label, count), pos(label, lambda x: (x positive).sum()), neg(label, lambda x: (x negative).sum()) ) grouped[sentiment_index] ( grouped[pos] - grouped[neg] ) / grouped[total] return grouped逻辑说明先按小时截取 crawl_time 字符串再按小时聚合计算总条数、正面数和负面数最后算出每个小时的情感指数。这样能看出某个舆情事件是“先爆发后降温”还是“持续争论”。聚合粒度可以在小时和天之间切换粒度越细则曲线越敏感。参数说明str[:13] 截取的是“2025-01-01 13”这种格式精确到小时。如果语料跨度只有几小时建议按分钟聚合参数相应改成 str[:16]。5. 微博舆情系统避坑指南5个最容易翻车的地方和修复方法5.1 反爬限制程序跑着跑着就断了现象程序刚开始能正常抓数据跑了几分钟后请求开始返回 403或者一直超时。重启程序又能跑一阵然后再断。原因请求频率过高触发服务端限流。尤其在没有携带完整 User-Agent 和请求来源信息时更容易被判定为异常流量。解决核心是控制节奏。每次请求之间加随机延时不要用固定的 sleep 时长固定节奏更容易被识别。另一个关键点是存断点让程序中断后能从上次位置继续跑而不是从头再来。我会把已抓取的评论页数写到一个 progress.json 文件里每次恢复时读取相当于给爬虫配了一个后悔药。import json import os PROGRESS_FILE progress.json def load_progress(): 读取历史采集进度 if os.path.exists(PROGRESS_FILE): with open(PROGRESS_FILE, r, encodingutf-8) as f: return json.load(f) return {last_comment_page: {}} def save_progress(progress): 保存采集进度到本地文件 with open(PROGRESS_FILE, w, encodingutf-8) as f: json.dump(progress, f, ensure_asciiFalse, indent2)逻辑说明进度文件让采集任务具备断点续跑能力。字段 last_comment_page 是字典key 是微博 idvalue 是已抓到的页码。每抓完一页就往内存里的 dict 写一次循环结束后调用 save_progress 持久化。程序中断后重新加载从记录位置继续不重复采集也不丢失进度。参数说明ensure_asciiFalse 让中文字段以明文形式写入 JSON 文件方便人工检查。indent2 只是格式化便于阅读。进度文件要在每次完整任务结束后清理避免旧记录干扰下一批数据的采集。5.2 数据入库乱码全角和转义字符没处理干净现象从数据库里查出的文本出现 \uXXXX 转义码或者中文变成“???”。直接打印 JSON 是正常中文一连到数据库就乱。原因微博接口返回的 JSON 里部分字段是 Unicode 转义格式print 时显示了真实字符但落到数据库或写文件时没有统一编码设置于是保存成了转义码原文。另一个常见来源是系统日志或外部文件用 GBK 打开字节错位后中文整体消失。解决所有文件和数据库连接统一显式声明 encodingutf-8。JSON 解析后直接用底层字符串字段不要用 str() 做二次转换。如果字段已经被转义可以用下面这个函数恢复import json def fix_unicode_text(text): 把被Unicode转义的文本还原为可读中文 try: if \\u in text: return json.loads(f{text}) except json.JSONDecodeError: # 转义恢复失败时保留原文本防止数据丢失 return text return text逻辑说明json.loads 可以把 \uXXXX 形式的转义序列还原成正常字符。函数先判断文本里是否真的存在转义标记避免对普通文本做无意义的解析。一旦 JSON 解析失败说明文本内部引号冲突此时保留原文是最安全的做法总比丢数据强。参数说明这里的修复方式只适合字段级别的小范围文本不适合对整个 JSON 文件做 json.loads。整个文件应该用 requests 的 json() 方法直接解析不需要额外修复。如果项目里大量出现这种问题说明采集环节已经出了偏差应该回查编码配置而不是逐条补救。5.3 分词效果差领域词典没有维护现象分词结果里人名、专有名词被拆得七零八落关键词提取出来的词都是无关紧要的通用词。原因jieba 的默认词典以新闻语料为主对微博这种网络短文本的覆盖度和切词倾向都不适配。人名、作品名、网络新词在默认词典里权重不高容易被切散。解决建立“语料观察 → 补词 → 重新切分”的闭环。随机抽 100 到 200 条清洗后的文本打印分词结果人工找出明显切错的词批量加进 weibo_dict.txt。迭代两三轮分词质量会有明显提升。过程中记录每一轮词典的变更内容不要无序修改否则出了问题很难回退。# 观察分词结果的快速命令 python -c import jieba; print(/.join(jieba.cut(新歌首发太好听了)))参数说明自定义词典每行一个词格式是“词 词频 词性”词频建议取默认词典的 2 到 3 倍词性按实体类型标注。加载词典放在程序启动时不要在循环里重复 load_userdict那会反复加载文件拖慢整体速度。5.4 时间戳换算错乱秒级和毫秒级混用现象表里的评论时间比实际早了八年或者晚了八年按时间排序时数据全乱。原因微博部分接口返回毫秒级时间戳比如 1710000000000 这种 13 位数字部分字段又返回可读日期字符串。如果不做单位换算直接用 datetime.fromtimestamp() 处理毫秒值时间就会偏移到 1970 年代附近完全不可用。解决统一封装一层时间解析函数先判断时间戳位数再分别处理。from datetime import datetime def parse_weibo_time(ts): 兼容秒级和毫秒级时间戳返回字符串时间 if isinstance(ts, (int, float)): if ts 1e12: ts ts / 1000 # 毫秒转秒 return datetime.fromtimestamp(ts).strftime(%Y-%m-%d %H:%M:%S)逻辑说明判断阈值的依据很简单当前秒级时间戳大约是 1e9 数量级毫秒级是 1e12 数量级用 1e12 作为分界绝大多数场景下不会误判。换算成秒后再交给 datetime.fromtimestamp 做格式化。参数说明函数返回字符串时间和前面表里 crawl_time 的格式保持一致方便 SQL 排序和分组。如果发现解析出的年份是 1970 年基本可以断定是毫秒值没用除法处理。5.5 情感判断翻车否定词和程度副词没处理现象“这部电影不好看”被判为负面这没问题但“这部电影不是不好看”在双重否定下实际是正面简单词典法直接判成负面。原因基于词典的情感判断只把命中的词的情感分相加没有考虑否定词会和后面词语组合成相反语义也没有考虑程度副词会放大或缩小情感强度。词袋模型在这里暴露了短板。解决在词典打分基础上增加否定词翻转和程度副词加权。基本实现是当当前位置的前一个词是否定词时把当前情感分值取反再往前找两个词如果存在程度副词就乘上程度系数。def sentiment_score_v2(text, score_map, degree_map, neg_words): 带否定翻转和程度加权的词典情感打分 words jieba.lcut(text) score 0.0 for i, w in enumerate(words): if w in degree_map: # 程度副词不直接参与求和用于加权后面的情感词 continue if w in score_map: val score_map[w] # 向前看一个词是否有否定词 if i 0 and words[i - 1] in neg_words: val -val # 再向前看两个词是否有程度副词 if i 2 and words[i - 2] in degree_map: val * degree_map[words[i - 2]] score val return score逻辑说明这个实现只往前看一个词和两个词的位置覆盖“不好看”“非常好看”这类常见结构。对“不是不好看”这种双重否定前一个否定词已经让分值翻转一次而“好”本身是正面情感取的相反数还是负值严格来说仍然不对。要正确处理双重否定需要继续往前递归判断奇偶次否定课程作业做到两步范围内的处理已经能挽回大部分错误。参数说明否定词集合包括“不、没、无、非、莫、勿、未、别”。程度词典用映射结构比如“非常”记 1.5“很”记 1.3“有点”记 0.5。程度系数的取值范围是 0.5 到 1.5 之间比较合理超过这个范围会大幅拉偏分数分布。6. 演示与进阶交互式可视化与增量采集的最后一公里课程项目验收时演示效果往往比算法细节更能说明问题。舆情分析结果如果只躺在 sqlite 里从外表看不出价值把数据变成可视化图表才算是一个能演示的系统。第一个值得做的是交互式词云。用 pyecharts 生成独立的 HTML 文件不需要启动 Web 服务双击就能在浏览器打开对课程演示非常友好。from pyecharts.charts import WordCloud from pyecharts import options as opts def make_wordcloud(word_freq: dict, outputwordcloud.html): 生成交互式词云页面 wc WordCloud() items [[word, str(freq)] for word, freq in word_freq.items()] wc.add(, items, word_size_range[20, 100], shapecircle) wc.set_global_opts(title_optsopts.TitleOpts(title微博热词词云)) wc.render(output)逻辑说明word_freq 是前面 TF-IDF 关键词或词频统计输出的字典。pyecharts 的 WordCloud 接收“词-频率”对组成的列表生成支持悬停和高亮的 HTML 页面演示效果比静态 matplotlib 图更直观。第二个值得做的是情感时序折线图把情感指数按小时画出来一眼看出舆情事件的发酵和降温过程。def make_sentiment_trend(df, outputtrend.html): 按小时生成情感指数折线图 from pyecharts.charts import Line hourly df.copy() hourly[hour] hourly[crawl_time].str[:13] grouped hourly.groupby(hour).apply( lambda x: (x[label] positive).mean() - (x[label] negative).mean() ) line Line() line.add_xaxis(grouped.index.tolist()) line.add_yaxis(情感指数, [round(v, 3) for v in grouped.values]) line.set_global_opts(title_optsopts.TitleOpts(title舆情情感时序变化)) line.render(output)增量采集方面更稳妥的做法是在建表时对“热搜词 抓取时间”建唯一索引然后写入用 INSERT OR IGNORE避免重复数据占用存储和干扰统计。定时任务用操作系统的 cron 或 Windows 任务计划即可不需要在程序内部维护复杂调度逻辑。CREATE UNIQUE INDEX IF NOT EXISTS idx_word_time ON hot_search(word, crawl_time);conn.executemany( INSERT OR IGNORE INTO hot_search (word, heat, rank, crawl_time) VALUES (?, ?, ?, ?), rows )当时做完这个项目我最深的体会是舆情系统最困难的部分不是写代码而是把整条数据链路打磨到能让人信任。比如今天跑出来的情感分布和昨天不一致不是因为代码改了而是采集时段不同导致语料有偏差如果不把采集、清洗、指标这三层口径固定下来后面每个环节都在为上一层的错误买单。后来我养成了一个习惯每次跑完分析先保留原始数据集再做特征计算最后出可视化三步分开存档。这个习惯坚持下来系统的可维护性和可解释性都会有明显提升。希望帮到你。本文还有配套的精品资源点击获取