ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Scrapy爬取豆瓣短评:中文情感分析全流程实战

Scrapy爬取豆瓣短评:中文情感分析全流程实战 简介一套围绕豆瓣电影Top250短评的完整数据挖掘项目覆盖Scrapy爬虫采集、数据清理、特征工程与中文文本情感建模适合爬虫、数据分析和NLP学习者作为综合实战参考。压缩包共214个文件、约73.43MB含34个Python脚本、4个Notebook、43个JSON中间数据、51张PNG图及shp地图可支撑短评人地域分布分析目录按三个数据子任务拆分便于对照学习。项目以肖申克的救赎短评为中心制作词云分别用朴素贝叶斯、SVC、fasttext、CNN、RNN、GRU构建中文分类模型还提供总评分排行、类别/国家/导演/演员/语言分析、星级占比和发布时间规律支持三表协同用短评预测电影类型。包内附带词云图、模型文件、可视化结果及地图数据从爬虫到建模形成完整链路按目录可分步复现也可迁移到其他影视评论项目。已有3774人学习下载。1. 从TOP250短评到情感模型一条能完整走通的中文文本处理链路把豆瓣TOP250的全部电影短评抓成结构化数据在本地完成清洗——去掉“看过”、“没用”这类噪声再按用户评分把文本打上正负标签最后训练一个能自动判断短评情绪的中文情感分析模型——这条链路是很多爬虫和NLP从业者入门时必过的一关。它不追求最深的模型也不追求抓取量而是让你在几天内把采集、清理、分析和建模四个环节各走一遍每一环都能看到实打实的产物。适合正在学Scrapy的爬虫新手也适合手里有一批文本数据、却不知道如何下手做情感分析的工程师。读完你会得到一套可以直接复用的采集配置、清洗规则和模型选型思路以及那些文档里不会写出来的坑。2. 用Scrapy抓TOP250短评限速、去重与动态渲染的落地配置2.1 项目骨架与四个核心文件的职责划分常见做法是先用scrapy startproject生成项目骨架再按“一个爬虫、一套Item、一条Pipeline、一份配置”的方式组织代码。我一般习惯把 Item、Spider、Pipeline 分开写因为后续清洗和建模都要依赖这几个文件各自输出干净的结构化数据。scrapy startproject douban_top250 cd douban_top250 scrapy genspider top250 movie.douban.com/top250生成后目录里最核心的是四个文件items.py定义字段spiders/top250.py写解析逻辑pipelines.py做去重和入库settings.py控制并发、延迟和请求头。千万别把解析逻辑全塞进parse方法里后面加字段的时候会很痛苦。# items.py import scrapy class MovieItem(scrapy.Item): movie_id scrapy.Field() # 豆瓣subject编号 title scrapy.Field() # 电影名 rating_value scrapy.Field() # 豆瓣评分10分制 rating_count scrapy.Field() # 评分人数 class CommentItem(scrapy.Item): movie_id scrapy.Field() comment_id scrapy.Field() # 评论唯一ID去重用 user_name scrapy.Field() rating_star scrapy.Field() # 星级1~5 comment_time scrapy.Field() content scrapy.Field()movie_id是后面关联电影和短评的外键comment_id是评论去重的唯一键。搞清楚这两个字段Pipeline 的去重和 SQLAlchemy 建表就能直接对上。2.2 别急着加并发DOWNLOAD_DELAY 与 CONCURRENT_REQUESTS 的配合刚接触爬虫的人容易犯一个错误上了 Scrapy 就开 32 并发结果抓不到 100 条就收到 418 或者验证码页。豆瓣的反爬不算极端但它的频率限制很敏感尤其是短评接口会在请求过快时直接返回空列表。我的习惯是先把并发压到 4延迟放到 2 秒抓到 500 条没问题再逐步加。# settings.py 关键项 ROBOTSTXT_OBEY False # 学习用途实际生产请先查看robots.txt并遵守 CONCURRENT_REQUESTS 4 # 同时发出的请求数 DOWNLOAD_DELAY 2.0 # 每个请求之间的间隔秒数 RANDOMIZE_DOWNLOAD_DELAY True # 在0.5~1.5倍延迟间随机防止规律性请求 DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, } RETRY_TIMES 3 # 超时或5xx时重试 DOWNLOAD_TIMEOUT 15这里DOWNLOAD_DELAY是每个请求发出前等待的固定秒数RANDOMIZE_DOWNLOAD_DELAY会让实际等待时间在 1~3 秒之间浮动避免出现精确的“每两秒一次”这种机器特征。RETRY_TIMES不要设太大否则被封之后重试会加重封禁。# spiders/top250.py 排行榜页与详情页 import scrapy from douban_top250.items import MovieItem, CommentItem class Top250Spider(scrapy.Spider): name top250 start_urls [https://movie.douban.com/top250] def parse(self, response): for movie in response.css(div.item): detail_url movie.css(a::attr(href)).get() yield response.follow(detail_url, callbackself.parse_movie) next_page response.css(span.next a::attr(href)).get() if next_page: yield response.follow(next_page, callbackself.parse) def parse_movie(self, response): movie_item MovieItem() movie_item[movie_id] response.url.split(/)[-2] movie_item[title] response.css(h1 span::text).get().strip() movie_item[rating_value] response.css(strong.rating_num::text).get() movie_item[rating_count] response.css( a.rating_people span::text ).get() yield movie_item comments_url ( fhttps://movie.douban.com/subject/{movie_item[movie_id]} f/comments?statusPsortnew_score ) yield scrapy.Request( comments_url, callbackself.parse_comments, cb_kwargs{movie_id: movie_item[movie_id]}, )排行榜页本身是服务端渲染直接抓 HTML 即可。短评页虽然也是 HTTP 请求能拿到数据但豆瓣的评论列表默认按热度排序如果你想要按时间排序的短评URL 里的sortnew_score参数是必加的。statusP表示只看看过的人写的短评去掉这个参数会混入“想看”的短评内容质量差异很大。2.3 短评是异步加载的用 scrapy-playwright 兜底动态 iframe 场景短评页在页面底部有一个“加载更多”的按钮点击后才会请求下一页数据。如果只是抓 Top 20 条短评Scrapy 直接处理 HTTP 响应就够了但要抓一部电影的几百条短评就需要处理这种动态加载。常见的两个方案是直接抓 XHR 接口或者用scrapy-playwright渲染页面。抓 XHR 接口更轻量但需要对浏览器 Network 面板抓包而且接口参数里有签名逻辑维护成本偏高。我一般先试 XHR失败再上 Playwright。scrapy-playwright的接入方式是在 settings 里切换下载处理器# settings.py 追加 DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor请求时在 meta 里声明启用渲染yield scrapy.Request( urlcomments_url, callbackself.parse_comments, meta{ playwright: True, playwright_include_page: True, }, cb_kwargs{movie_id: movie_id}, )在回调里把page拿出来模拟浏览器滚动触发接口加载async def parse_comments(self, response, movie_id): page response.meta.get(playwright_page) for _ in range(5): await page.mouse.wheel(0, 3000) await page.wait_for_timeout(1500) html await page.content() # 把渲染后的HTML交给原来的解析逻辑 from scrapy.http import HtmlResponse resp HtmlResponse(urlresponse.url, bodyhtml, encodingutf-8, requestresponse.request) yield from self.parse_comment_html(resp, movie_id) await page.close()这里每次滚动 3000 像素等待 1.5 秒让 XHR 返回并渲染。page.close()必须做否则浏览器的内存会一直涨长跑任务跑到后面直接 OOM。要注意parse_comments变成了异步方法Scrapy 的 callback 支持async def但异常处理要更仔细一旦渲染超时整个 spider 会被卡住。如果页面里确实嵌了 iframe 类型的评论区page.frames里找到对应 frame 再操作即可。TOP250 短评目前不是 iframe 结构但电商、社交类评论很多是这个套路通用。2.4 落库用 SQLAlchemy在 Pipeline 里完成去重和入库抓下来的数据不建议直接写 CSV因为爬虫中断重跑会导致大量重复行。我在 Pipeline 里用 SQLAlchemy 维护一张表给comment_id movie_id建唯一索引重复写入自然失败比在代码里做if seen判断更可靠。# pipelines.py import hashlib from scrapy.exceptions import DropItem from sqlalchemy import create_engine, Column, String, Integer, UniqueConstraint from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class CommentORM(Base): __tablename__ comments id Column(Integer, primary_keyTrue, autoincrementTrue) movie_id Column(String(16)) comment_id Column(String(64)) content Column(String(2000)) rating_star Column(Integer) comment_time Column(String(32)) __table_args__ ( UniqueConstraint(movie_id, comment_id, nameuq_movie_comment), ) engine create_engine(sqlite:///douban.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine) class SQLAlchemyPipeline: def process_item(self, item, spider): if isinstance(item, CommentItem): session Session() existed session.query(CommentORM).filter_by( movie_iditem[movie_id], comment_iditem[comment_id] ).first() if existed: session.close() raise DropItem(重复评论丢弃) session.add(CommentORM(**dict(item))) session.commit() session.close() return item这里用 SQLite 起步最省事单文件、无需配置后续分析用 pandas 直接读。UniqueConstraint是最后一道防线即使你的去重逻辑写错了数据库也会拦下重复记录。等数据量到几十万条再换 MySQL 或 PostgreSQL表结构不用改只换create_engine的连接串。启用 Pipeline 只需在 settings 里把类路径配到ITEM_PIPELINESITEM_PIPELINES { douban_top250.pipelines.SQLAlchemyPipeline: 300, }数字 300 表示执行顺序越小越先执行。如果后面还有清洗 Pipeline注意顺序先入库还是先清洗取决于你是否想保留原始数据。3. 数据清理与规范化把脏文本变成能直接训练的语料3.1 先清噪声去空白、HTML 实体与评分前缀直接抓下来的短评文本里混着各种东西HTML 实体amp;、全角空格、\u3000、短评开头的“看过”、以及“没用”这种按钮文本。如果不清理这些噪声会成为模型的特征学习到“看过”和消极评论的虚假相关性。import re import html def clean_text(raw: str) - str: if not raw: return raw html.unescape(raw) # amp; - #39; - raw re.sub(r[\s\u3000\xa0], , raw) # 统一空白字符 raw raw.replace(有用, ).replace(没用, ) raw re.sub(r^(看过|想看)\s*, , raw) # 去掉状态前缀 raw re.sub(r^[^\u4e00-\u9fa5a-zA-Z0-9], , raw) return raw.strip()html.unescape必须放在正则之前因为实体字符展开后可能引入空格或引号。用“没用”这种按钮文字在实际 HTML 中是独立的 span偶尔会被选择器误抓进文本里直接替换掉最稳妥。开头的“看过”是短评者标记状态的文字和评论语义无关同样要清掉。有一个坑要注意短评正文里如果提到“没用”这个词比如“这片子说教得没用”直接.replace(没用, )会把语义也删掉。实操中我不会无脑全删只在文本开头和结尾截掉按钮噪声正则改成只匹配“有用”或“没用”加上前后空白的情况。如果不确定先跑一遍结果肉眼扫几百条再定规则。3.2 分词与停用词jieba 配电影领域词典中文情感分析绕不开分词。英文按空格切就行中文必须用分词器。最常用的是 jieba默认词典覆盖日常用语但电影领域的人名和片名经常被切成碎块。import jieba # dict/movie_dict.txt # 无间道 1 nz # 霸王别姬 1 nz # 这个杀手不太冷 1 nz # 肖申克的救赎 1 nz jieba.load_userdict(dict/movie_dict.txt) with open(dict/stopwords.txt, encodingutf-8) as f: stopwords set(line.strip() for line in f) def segment(text: str): words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w.strip()) 0]load_userdict的格式是“词 频次 词性”频次填 1 表示最低优先级但已经足够避免默认词典优先切词。停用词表里至少要有的、了、嗯、啊、就、都、而、及、与、着这些词对情感判断没有贡献留着只会增加特征维度。参数上有两个注意点第一停用词表不能用网上随便找的通用版通用版会把“喜欢”这个词拆成“喜”和“欢”再过滤掉第二jieba 的lcut返回 List后续训练时直接 .join(words)拼成空格分隔文本喂给 TF-IDF 即可。3.3 评分映射与类别平衡4 星以上正例2 星以下负例豆瓣短评的星级是 1~5 的整数或半星但大多数短评没有填星级只有“看过”标记。清洗时要先从 HTML 的 class 属性里把星级解析出来。allstar50是 5 星allstar40是 4 星以此类推。def star_from_class(class_name: str): mapping { allstar50: 5, allstar40: 4, allstar30: 3, allstar20: 2, allstar10: 1, } return mapping.get(class_name, 0)有了星级就可以把短评转成监督学习的标签。我的策略是4 星和 5 星标为正向11 星和 2 星标为负向03 星直接丢弃。为什么不要 3 星因为 3 星短评往往是“还行”、“凑合”、“不至于很差”这类中性表达两头都不靠硬塞进二分类模型里会把决策边界搅浑。def label_from_star(star: int): if star 4: return 1 if star 2: return 0 return None这套映射做完通常正负样本比例接近 2:1TOP250 的观众整体偏正面负样本天然少。后面建模时如果 F1 偏低先回来检查这个比例而不是急着换模型。3.4 落地为 CSV 与 Excel给下游分析一个干净入口清洗完的数据建议导出成两个文件一个完整版 CSV 给 pandas 分析用一个 Excel 给你自己打开看。导出时指定编码utf-8-sig这是 Windows 下 Excel 打开不乱码的关键utf-8会被 Excel 识别成 ANSI 乱码。import pandas as pd df pd.read_sql(SELECT * FROM comments, sqlite:///douban.db) df[content_clean] df[content].apply(clean_text) df[label] df[rating_star].apply(label_from_star) df df.dropna(subset[label]) df.to_csv(clean_comments.csv, indexFalse, encodingutf-8-sig) df.to_excel(clean_comments.xlsx, indexFalse)dropna(subset[label])会把 3 星和中性的评论直接剔掉。Excel 导出需要openpyxl库pip install openpyxl即可。这时候可以肉眼抽几十条看看清洗效果再进入下一步。4. 短评数据分布与可视化建模前必做的三张图4.1 评分分布TOP250 的偏态结构拿到干净数据后别急着训练模型先画三张图。第一张是评分分布直方图看正负样本差距到底有多大。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False df[rating_star].value_counts().sort_index().plot(kindbar) plt.title(TOP250 短评星级分布) plt.xlabel(星级) plt.ylabel(短评数量) plt.savefig(rating_dist.png, dpi150, bbox_inchestight)这张图最大的作用是提醒你TOP250 的短评是典型的偏态数据5 星和 4 星占了大多数。如果直接用准确率评估模型模型全部预测为正向也能拿到 70% 以上的正确率但实际毫无用处。所以后面模型评估要看 F1或者对负样本做上采样。4.2 词频与词云先看高频词再定特征第二张图统计分词后的词频能帮你快速确认清洗效果和特征方向。from collections import Counter all_words df[content_clean].apply(lambda x: .join(segment(x))).str.split() word_freq Counter([w for words in all_words for w in words]) top_words pd.DataFrame(word_freq.most_common(20), columns[word, count]) top_words.plot(kindbarh, xword, ycount) plt.gca().invert_yaxis() plt.title(TOP250 短评高频词 TOP20) plt.tight_layout() plt.savefig(word_freq.png, dpi150)如果词云图里出现“电影”、“一部”、“感觉”这类词占满画面说明停用词表还不够厚返回 3.2 再补。高频词里出现“经典”、“好看”、“感动”是正常的这些词本身就和正向评分强相关。如果你发现“烂”、“差”、“浪费”也在 TOP20说明负样本量还够模型有机会学到东西。4.3 时间维度把评论日期和评分放在一起看第三张图看时间。爬下来的短评comment_time是字符串要转成日期再聚合。这张图能告诉你数据是否存在时间断层比如某部电影的短评集中在某一天那多半是上映或获奖带来的热度高峰。按月份聚合看平均评分变化也能直观发现口碑发酵的过程。df[dt] pd.to_datetime(df[comment_time], errorscoerce) df[month] df[dt].dt.to_period(M) trend df.groupby(month).agg( avg_star(rating_star, mean), count(rating_star, count) ).reset_index() fig, ax1 plt.subplots() ax1.bar(trend[month].astype(str), trend[count], alpha0.5) ax2 ax1.twinx() ax2.plot(trend[month].astype(str), trend[avg_star], colorred) plt.xticks(rotation45) plt.tight_layout() plt.savefig(time_trend.png, dpi150)errorscoerce会把解析失败的日期变成 NaT后续聚合时自动跳过。如果comment_time里混了“2024-10-01 10:30:00”和“10-01 10:30”两种格式pandas 的to_datetime会报错或解析错最好在清洗阶段统一格式。这三张图画完你对数据的认知就不再是“我爬了两万条短评”这种模糊印象而是清楚地知道样本比例、高频特征和时间分布。做 python 爬虫可视化界面时直接把这几个图接到 Plotly 或 Streamlit 上就是最简单的数据看板。5. 中文情感分析模型从词典基线到深度学习的三条路线5.1 基线SnowNLP 情感词典为什么不能直接用于电影短评先跑一个最原始的基线用情感词典类的工具直接打分。SnowNLP 自带一个训练好的情感分类模型基于电商评论语料接口简单到一行代码from snownlp import SnowNLP test_cases [ 这片子太震撼了看完一晚上没睡着, 剧情稀烂浪费了两个小时, 没有想象中的好但也说不上差, ] for text in test_cases: print(text, SnowNLP(text).sentiments)跑完大概率发现那种客观中性的短评也被打出了 0.7 以上的正面分。原因很简单SnowNLP 的语料来自电商商品评论里“不错”、“性价比高”是高频正面信号电影短评里的“后劲大”、“拍得细腻”、“结局反转”是另一个分布。SnowNLP 的隐藏好处是支持用自己的语料重新训练。把清洗好的正向短评和负向短评各准备几千条每行一条然后from snownlp import sentiment sentiment.train( train_data/neg.txt, train_data/pos.txt, ) sentiment.save(models/movie_sentiment.marshal)sentiment.train内部是一个朴素贝叶斯分类器训练速度快到秒级。但要注意它把文本按字切分还是按词切分取决于内部实现且不区分否定词的作用。所以这个方案定位是“基线”让你有一个最低线后面机器学习模型超过它才算有效果。5.2 机器学习路线TF-IDF 逻辑回归多数场景够用真正建模时我第一个认真跑的是 TF-IDF 加逻辑回归。TF-IDF 把分词后的文本转成稀疏向量逻辑回归在这类高维稀疏特征上表现稳定训练速度快还能输出概率值用于阈值调整。import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import cross_val_score df pd.read_csv(clean_comments.csv) df[token_str] df[content_clean].apply(lambda x: .join(segment(x))) data df.dropna(subset[label]) pipe Pipeline([ (tfidf, TfidfVectorizer( token_patternr\S, ngram_range(1, 2), max_features50000, sublinear_tfTrue, )), (lr, LogisticRegression(max_iter1000, C1.0)), ]) scores cross_val_score( pipe, data[token_str], data[label], cv5, scoringf1, ) print(F1:, scores.mean())三个参数是关键。第一token_patternr\S的原因是文本已经用 jieba 分词并用空格拼接向量器不需要再做字符级切分否则中文会被切成单字。第二ngram_range(1, 2)保留相邻两个词的组合特征“不”和“好看”分开是两个词但“不 好看”的二元组直接表达了否定语义对中性表达多的电影短评很有效。第三max_features50000限制特征维度避免 3 万条评论产生几十万个特征导致内存爆掉。C1.0是正则强度的倒数C 越小正则越强。可以先跑一遍默认值然后试 0.5 和 2.0看验证 F1 的变化。如果 F1 稳定在 0.85 以上这个方案直接可以用于生产不需要上深度学习。5.3 深度学习路线TextCNN 的关键参数与训练当数据量超过 2 万条且 TF-IDF 的 F1 卡在瓶颈时可以上 TextCNN。它的核心思想是用多个不同宽度的卷积核在词向量序列上滑动抓取 2-gram、3-gram、4-gram 的局部特征再用最大池化保留最强信号。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters128, kernel_sizes(2, 3, 4), num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in kernel_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(len(kernel_sizes) * num_filters, num_classes) def forward(self, x): x self.embedding(x) # (B, L, E) x x.transpose(1, 2) # (B, E, L) Conv1d 要求通道在第二维 pooled [] for conv in self.convs: out torch.relu(conv(x)) # (B, F, L-k1) pooled.append(torch.max(out, dim2).values) out torch.cat(pooled, dim1) # (B, F*3) return self.fc(self.dropout(out))kernel_sizes(2, 3, 4)意味着三组卷积核分别看连续 2 个词、3 个词、4 个词。短评平均长度不到 50 个字这个窗口基本够用。embed_dim128是词向量的维度用预训练词向量如腾讯词向量可以压到 100 维随机初始化建议 128。num_filters128是每组卷积核输出通道数数据量小时可以降到 64。训练部分的超参criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) batch_size 64 max_seq_len 64 epochs 6文本长度超过max_seq_len的策略是截断短评基本都在 64 字内。这里有个血泪教训embedding的padding_idx0必须和在build_vocab时预留的 0 号位置对应否则填充位置参与梯度更新模型会学到“空字符有情绪”这种鬼东西。训练结束后用测试集算一次混淆矩阵from sklearn.metrics import classification_report model.eval() with torch.no_grad(): preds, y_true [], [] for batch_x, batch_y in test_loader: logits model(batch_x) preds torch.argmax(logits, dim1).tolist() y_true batch_y.tolist() print(classification_report(y_true, preds, target_names[负极, 正极]))5.4 数据量与超参的选择先做减法再做加法三条路线放在一张表里看边界很清楚方案训练耗时最低数据量F1 预期适用阶段SnowNLP 重训秒级2000 条0.75~0.82快速基线TF-IDF LR分钟级5000 条0.84~0.90多数生产场景TextCNN小时级2 万条起0.86~0.92有 GPU、追求上限BERT 微调半天级3 万条起0.90不差钱且要刷榜数据量少于 5000 条时TextCNN 很容易过拟合F1 反而不如逻辑回归。我见过有人拿 3000 条短评硬跑 BERT验证集损失一路飙升最后还不如 TF-IDF 加逻辑回归。做工程要学会先做减法别为了简历上多一行“深度学习”就无脑上 Transformer。多模态情感分析、视频人物情感分析这些热门方向是后续延伸前提是先把单模态文本这一关打扎实。6. 避坑TOP250 短评采集与情感分析中的 5 个典型事故6.1 采集侧空正文、HTTP 418 与重复入库第一坑抓到的短评全是空正文只有“看过”两个字。现象是数据库里 content 字段大量为空但短评确实存在。原因是 CSS 选择器匹配到了整个评论容器但正文在.short里而.short这个类在“展开全文”的场景下会被替换成.short-content。解决方法是选择器同时匹配两个类content item.css(.short::text).get() if not content: content item.css(.short-content::text).get()第二坑翻页到第 5 页以后出现大量 HTTP 418。418 表示服务器识别到你是爬虫但并没有直接封 IP而是拒绝服务。原因是请求频率起来了DOWNLOAD_DELAY2.0对短评页不够因为每个短评页响应里有 20 条评论翻 10 页就 200 个请求还是太密。解决方法是单独给评论页设置更高的延迟或者加一个随机延时中间件在请求之间随机 sleep 3~6 秒。你也可以监听 418 状态码连续出现 3 次就暂停 2 分钟再继续。# middlewares.py 片段 import time, random class ThrottleMiddleware: def process_response(self, request, response, spider): if response.status 418: time.sleep(random.uniform(60, 120)) return request.replace(dont_filterTrue) return responsedont_filterTrue的意思是重新请求时不经过去重避免新请求被调度器当成重复请求丢掉。第三坑爬虫中断后重启数据库里出现大量重复。现象是评论数对不上重复率超过 30%。原因是 SQLAlchemy 的唯一约束只在写入时触发异常但 Scrapy 的日志里全是IntegrityErrorPipeline 没捕获异常写入失败后 item 被当成成功丢弃。解决方法是捕获IntegrityError并主动 DropItemfrom sqlalchemy.exc import IntegrityError try: session.add(CommentORM(**dict(item))) session.commit() except IntegrityError: session.rollback() raise DropItem(重复评论)6.2 分析侧编码乱码、语义误判与样本失衡第四坑CSV 在 Excel 里打开全乱码。现象是文件用记事本打开正常Excel 打开变成“鍝”开头的乱码。原因是没有写 BOM 头Excel 默认按 ANSI 解码。解决就是用encodingutf-8-sig这已经是老生常谈但还是会有人踩。另外matplotlib 绘图里中文变方块需要显式设置字体Windows 上是SimHei或Microsoft YaHeiLinux 服务器上没有这两个字体需要单独安装中文字体包。第五坑模型把“太难看了”预测成正面。现象是测试集 F1 挺高但抽检时翻车。原因是 TF-IDF 的特征是词袋模型学到的是“难看”和负面强相关但“太”这个程度副词改变了语义强度而二元组特征里只出现了“太 难”却没覆盖“太 难看”。解决思路是加一列否定词密度特征把“不、没、难、别”等词的统计量拼进特征向量def neg_density(text: str): return len(re.findall(r不|没|难|别|白, text)) / max(len(text), 1) df[neg_density] df[content_clean].apply(neg_density)然后把这一列追加到 TF-IDF 矩阵后面再喂给逻辑回归。特征工程的作用就是告诉模型你只管管词义否定密度这个信号由我来显式提供。这个 trick 对反讽短评效果有限但能让“难度”这类常用词不那么容易被误判。还有一个隐蔽的坑是样本失衡。TOP250 短评里 4 星以上占了大半负样本稀疏。如果你只抽了 2000 条做训练负样本可能只有 500 条模型自然偏向预测正向。解决方法是训练前做分层抽样保证训练集和验证集的正负比例一致或者对负样本过采样重复复制几遍。评估指标别盯准确率F1 才是正负样本不均衡时的可靠指标。7. 用交叉验证与人工抽检给模型做“体检”一个值得长期坚持的习惯模型训练完不是终点。我习惯做三件事第一用交叉验证算 F1保存所有折的预测结果第二把预测错的样本捞出来按电影分组看模式第三抓一批全新的短评用模型打标后人工抽检 20 条算抽检准确率。from sklearn.model_selection import cross_val_predict pred cross_val_predict( pipe, data[token_str], data[label], cv5, methodpredict_proba, ) df[pred_prob] pred[:, 1] df[pred_label] (df[pred_prob] 0.5).astype(int) wrong df[df[pred_label] ! df[label]] print(wrong[content_clean].head(20))cross_val_predict返回的是每个样本在验证轮次里的预测概率不是测试集上的精调结果所以它反映的是模型的真实泛化水平。逐条看wrong这个 DataFrame 里的文本比看一百个训练曲线都有用。你会发现两类高频错误一类是“还行”、“一般”这种中性表达被模型强行分到正或负另一类是“结局毁了一切”这种前半段正面后半段反讽的复合型短评。看完错误样本把阈值从默认的 0.5 调一下比如调到 0.6只有概率超过 0.6 才算正向小于 0.4 才算负向中间段标为“不确定”。三分类输出比硬二分类更贴合真实需求。我现在的习惯是每次抓完新数据先跑一遍模型随机抽 20 条有正有负的样本人工标注后和模型预测比对翻车率超过 10% 就回去调清洗规则或换特征。这个体检流程比换更贵的模型实在得多。模型的价值不只在于准确率多高而在于你清楚知道它在哪些句式上会错。把这些抽检结果存成一份model_audit.csv下次改完特征再跑一遍对比翻车率变化这才是能让情感分析模型持续迭代的抓手。做笔记、留样本、复盘错误这条习惯帮我避开了很多重复性的翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表