ARTICLE DETAIL

资讯详情

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

Python爬虫+wordcloud:从豆瓣评论到中文词云完整实战

Python爬虫+wordcloud:从豆瓣评论到中文词云完整实战 最近好几个朋友问我同一个问题Python基础语法学完之后练手项目做什么最合适。我的回答里永远排第一位的是网络爬虫配wordcloud词云。原因很简单这个组合足够真实——你得从豆瓣电影短评页把用户评论一条条抓下来处理请求头、编码、反爬这些实实在在的问题还得解决中文分词、停用词过滤这些容易翻车的细节最后生成一张立刻能拿出去发朋友圈的漂亮词云图。这篇文章就把整套流程拆开揉碎了讲一遍从爬取豆瓣最新评论开始到jieba中文分词再到用wordcloud做出心形、圆形、白底极简等各式词云适合刚学完Python基础、想动手做完整项目的朋友也适合想给数据分析结果加点可视化表达的开发者。1. 需求拆解与整体技术路线1.1 这个任务本质上拆成了两半先说人话这个项目表面上叫爬取豆瓣评论生成词云实际做起来是两个相对独立的问题。爬虫那一半核心是拿到豆瓣电影短评页里的评论正文。而词云那一半核心是让wordcloud在指定形状里把高频词按大小排列出来。这两半之间隔着一道非常关键的工序——中文分词。如果不做分词wordcloud拿到一整段没有空格的中文句子会把一句话甚至一段话当成一个词来处理生成出来的图基本是废的。所以整套链路是requests请求短评页面拿到HTMLlxml解析HTML提取评论正文用jieba对评论做分词配合停用词表过滤无意义词把分词结果按空格拼接成字符串交给wordcloud通过参数控制字体、形状、颜色输出词云图1.2 为什么这些库能成为主流选择爬虫部分我选requests而不是Python自带的urllib。常写爬虫的人都懂requests在Cookie处理、Session保持、异常处理上都更顺手代码短一截踩坑少一截。解析HTML我用lxml它支持xpath定位在结构相对固定的页面上非常稳定。豆瓣短评页虽然改版过几次但评论内容的class始终很稳定用xpath一次性就能把所有短评摘出来。词云部分就是wordcloud jieba matplotlib三件套。wordcloud是目前最通用的词云生成库也是中文教程最多的库jieba是中文分词的标准选择默认词典覆盖日常词汇足够matplotlib用来在Jupyter里预览效果也方便导出带网格的调试图。1.3 环境准备与安装建议Python版本3.8以上下面的示例在Python 3.10环境验证过。安装命令很简单pip install requests lxml jieba wordcloud matplotlib pillow numpypillow和numpy不是直接调用的但wordcloud在读掩膜图时需要依赖它们顺手一起装上能避免不少环境报错。2. 豆瓣短评爬取从URL结构到落地数据2.1 页面URL结构与翻页逻辑豆瓣电影短评的URL规律非常固定一个通用模板就能覆盖https://movie.douban.com/subject/{subject_id}/comments?start0limit20sorttime其中subject_id是豆瓣电影内部编号。以宫崎骏的《千与千寻》为例它的主页是https://movie.douban.com/subject/1291561/那短评页就是https://movie.douban.com/subject/1291561/comments。这里有个容易踩的小坑很多人不知道短评页可以排序默认进入页面是热门排序顶部是点赞最多的评论。标题想表达最新评论所以URL里要加sorttime这样拿到的是按时间倒序排列的最新短评。start参数是翻页偏移量一页20条第二页start20第三页start40以此类推。2.2 请求头为什么UA伪装是第一道门槛豆瓣对爬虫不算特别激进但对明显不是浏览器的请求判定很干脆——直接403。如果裸请求访问短评页大概率拿到一个异常页面。因此请求头至少要带两个字段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://movie.douban.com/ }User-Agent代表了用户是在用Chrome、Firefox还是命令行工具在访问服务器侧重度最高的就是它。Referer则代表了页面跳转来源加上这个字段更像用户从豆瓣首页点进短评页的正常访问路径。另外一个很现实的点未登录状态下豆瓣对很多电影的最新评论只展示前几页想看到更完整的最新短评列表需要把本机浏览器里已登录的Cookie复制进来。实际做法是从浏览器的开发者工具里找到任意一个请求把Cookie字段的值复制到headers里headers[Cookie] 你的豆瓣登录Cookie2.3 用lxml提取评论正文拿到HTML之后先用lxml把页面结构解析成可查询的树from lxml import etree resp requests.get(url, headersheaders) resp.encoding utf-8 tree etree.HTML(resp.text)豆瓣短评页面的结构中每条短评都在一个class为comment-item的div里评论文本在class为short的span里。用xpath提取items tree.xpath(//div[contains(class,comment-item)]//span[contains(class,short)]/text()) comments [item.strip() for item in items if item.strip()]有个细节值得说豆瓣页面里的评论有展开全部机制当一条短评文字超过一定长度时HTML里会同时存在完整评论和折叠后的短文本。我们提取class为short的span拿到的恰好是默认展示的那段文本数据干净不会重复。完整爬虫函数写出来是这样的import requests import time import random from lxml import etree def fetch_douban_comments(subject_id, pages5): 抓取豆瓣电影最新短评。 subject_id: 电影编号 pages: 需要翻多少页 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://movie.douban.com/ } comments [] for start in range(0, pages * 20, 20): url fhttps://movie.douban.com/subject/{subject_id}/comments?start{start}limit20sorttime resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 tree etree.HTML(resp.text) items tree.xpath(//div[contains(class,comment-item)]//span[contains(class,short)]/text()) comments.extend([item.strip() for item in items if item.strip()]) # 每请求完一页随机停一小会儿 time.sleep(random.uniform(0.8, 2.0)) return comments2.4 礼貌爬取与限流处理翻页循环里每抓一页就随机sleep 0.8到2秒这个习惯一定要养成。豆瓣短评页本身加载很快但服务器对连续高频请求的容忍度很低。我以前不sleep直接循环抓抓到第三页就被封了一段时间整个IP访问豆瓣都变成校验页面。后来老老实实加了随机延时也把pages控制在5页以内再没出过问题。如果发现返回的页面里出现登录校验码字样大概率是触发限流了。先停下手里的脚本过10分钟再试还不行就加上登录Cookie重新跑。到这里文本数据已经到手了。接下来的问题从怎么拿到数据变成了怎么让数据变成好看的图。3. 中文文本清洗与分词词云质量的胜负手3.1 为什么原始评论不能直接扔进wordcloud新手最容易犯的错误是把所有评论拼成一大段字符串直接传给wordcloud。结果往往是这样的词云里没有合理的词全是一句句完整的话在画布上横七竖八。背后原因在于wordcloud默认按空白字符来切分文本。英文和拼音天然有空格切分出来就是单词中文一句话里字与字之间没有空格wordcloud只会把整句话当成一个单词。所以我们必须自己先用jieba把中文切成一个个有独立含义的词再用空格拼起来喂给wordcloud。3.2 jieba精确分词与文本拼接先把抓到的评论列表合并成一个字符串同时清理掉换行和多余空白import re text .join(comments) text re.sub(r\s, , text)然后做分词。jieba有三种模式这里用默认的精确模式就够了它会按最合理的方式把句子切开import jieba words jieba.lcut(text)注意jieba.cut返回的是生成器直接用列表推导式转成list更直观。如果机器内存紧张也可以用jieba.cut_for_search做搜索模式会分出更多词但词云展示时反而显得细碎没必要。分词结果里会混着大量标点符号、数字、英文单词和单字语气词需要提前过滤。我习惯的做法是先去掉长度只有1个字符的词再统一清洗words [word for word in words if len(word.strip()) 1]3.3 停用词表的建立逻辑词云里最常见的翻车现场一张图里密密麻麻全是我们真的感觉一个没有。这些词在口语评论里出现频率极高但没有一点信息量不剔除掉真正有内容的词反而排不上号。所以必须引入停用词表。网上有现成的中文停用词表下载后按行读取即可stopwords set() with open(stopwords_cn.txt, r, encodingutf-8) as f: stopwords set(f.read().splitlines())同时根据当前场景补充自定义停用词。这一步非常重要因为我爬的是电影短评可以预期电影这部片子这类词必然高频。但它们本身没有信息增量所以我都会加进自定义停用词stopwords.update({ 电影, 这部, 一部, 这个, 一个, 真的, 还是, 就是, 自己, 我们, 你们, 他们, 没有, 什么 })停用词表不是一次到位的。我每次生成词云后都会看一眼词频最高的前20个词凡是那种去掉之后完全不影响理解的词就顺手加入停用词。迭代几次之后词云质量会有肉眼可见的提升。3.4 词频统计的灵活玩法processed_text之后重点是把分词结果转成词频字典。使用collections.Counterfrom collections import Counter word_freq Counter(words)把高频词打印出来看看确认数据质量再进入下一步for word, count in word_freq.most_common(20): print(word, count)词频字典还有一个好处某些特定场景下可以人工干预词频比如你希望某个词在词云里更突出直接给它加权重再传给wordcloud。这种操作在文本型的generate里做不了但用generate_from_frequencies就很自然。后面我会专门讲到这两个方法的差别。4. wordcloud参数逐个拆解与心形词云实战4.1 font_path中文乱码的根源wordcloud画中文词云的第一大坑就是字体。默认字体不支持中文不指定font_path时生成出来的词云上要么是一堆方框要么直接乱码。这个不怪wordcloud它只是按字体文件去渲染文字没有中文字体文件自然画不出中文字形。正确做法是给wordcloud指定一个系统中文字体路径。不同系统的路径不一样我整理了一份常用对照系统字体文件路径Windows黑体C:/Windows/Fonts/simhei.ttfWindows微软雅黑C:/Windows/Fonts/msyh.ttcmacOS苹方/System/Library/Fonts/PingFang.ttcLinux思源黑体/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc如果推荐的路径找不到可以用fc-list :langzh命令在Linux/macOS上列出所有中文字体路径。Windows上直接去Fonts目录翻一下就清楚了。4.2 核心布局参数逐个讲下面这套参数是我调出来的基线先把图跑起来再微调from wordcloud import WordCloud wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite, max_words200, max_font_size80, min_font_size10, random_state42, colormapviridis, stopwordsstopwords, collocationsFalse ) wc.generate_from_frequencies(word_freq) wc.to_file(movie_comment_wordcloud.png)注意这里用的是generate_from_frequencies它接受的是词频字典不受文本顺序影响也不会把相邻的两个词自动合并成词组。它和generate(text)最大的区别就在collocations这个参数上——generate(text)默认会做双词组检测把相邻高频词如依然温暖合并成依然温暖但对中文来说我们分词后的词边界已经很有意义再合并反而干扰词频统计所以用generate_from_frequencies更可控。核心参数的作用可以看这张表参数作用我的建议width / height画布尺寸800x600起步太小会糊background_color背景色白底用white透明用Nonemax_words最多显示多少词200个左右视觉最均衡max_font_size / min_font_size字号上下限80/10比较稳妥colormap整套配色不指定就是默认蓝紫色mask蒙版形状传入numpy数组改变词云轮廓random_state随机种子固定后每次生成布局一致scale整体缩放分辨率不够时调到2-34.3 配色colormap决定第一印象很多人做完词云发现颜色乱糟糟其实只需要指定一个colormap就能让整体配色统一。wordcloud的colormap直接复用matplotlib的色系我常用这几个viridis紫绿渐变科技感强RdBu红蓝渐变适合影评里的好评差评Set2多个离散色丰富但不会太乱summer黄绿渐变小清新如果想让颜色更有设计感可以用ImageColorGenerator从一张参考图里提取配色然后wc.recolor(color_funccolor_generator)重新上色。这个玩法适合追求视觉效果的场景本质是把图片的颜色规律映射到每个词上。4.4 mask蒙版原理为什么图片必须是白底黑图很多人对mask的理解有偏差以为随便找一张心形图片传进去就能生成心形词云。实际不是这样。wordcloud读取mask后只会把它当成一个二维数组判断每个像素位置是否允许放置文字。这里的核心规则是背景区域白色不落字图案区域黑色落字。所以蒙版图片必须经过预处理变成一张完美的白底黑图图案区域越饱满词云效果越好。我把预处理逻辑写成通用函数import numpy as np from PIL import Image def build_mask_from_image(image_path, invertFalse): img Image.open(image_path).convert(RGB) arr np.array(img) gray arr.mean(axis2) # 转灰度 mask np.where(gray 200, 255, 0).astype(np.uint8) if invert: mask 255 - mask return mask逻辑说明先读图并统一转成RGB再按灰度平均值判断——亮度大于200的像素视为背景白色255其余视为图案黑色0。如果手里的图是白心黑底反过来那在调用时把invert设为True把黑白反转即可。还有个容易翻车的细节当指定mask后wordcloud的width和height会被忽略画布尺寸完全由mask的数组形状决定。所以高质量蒙版图尽量找大图比如1000x1000以上的心形图不然生成的字会挤在一起。4.5 白底心形词云的完整实现这是很多人提到的心形词云 白底需求。完整代码一次性贴出来import random import numpy as np from PIL import Image from wordcloud import WordCloud # 1. 读取心形蒙版并二值化白底黑心 img Image.open(heart.png).convert(RGB) arr np.array(img) gray arr.mean(axis2) mask np.where(gray 200, 255, 0).astype(np.uint8) # 2. 生成词云 wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, background_colorwhite, # 白底 maskmask, colormapReds, # 红配色更贴合爱心主题 max_words300, max_font_size120, min_font_size8, random_state42, collocationsFalse ) wc.generate_from_frequencies(word_freq) # 3. 保存并预览 wc.to_file(heart_wordcloud.png) import matplotlib.pyplot as plt plt.imshow(wc) plt.axis(off) plt.show()这里wordcloud会读取mask数组把所有词限制在心形黑色区域内部。白底背景本身又和控制参数里的background_colorwhite重合所以整个图就是白底红字的心形视觉干净。实际测试下来效果最好的心形蒙版有两种来源一种是直接用心形气球的剪影图片边缘光滑另一种是用黑白线条勾勒的心形简笔画。千万避开那种带有渐变阴影的心形图片二值化之后边缘会坑坑洼洼。4.6 透明背景词云的做法如果想在深色海报、PPT背景上叠词云白底反而碍事。wordcloud本身支持输出透明背景wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, background_colorNone, # 背景设为None modeRGBA, # 让PNG支持透明度 maskmask, colormapviridis ) wc.generate_from_frequencies(word_freq) wc.to_file(transparent_heart.png)关键就是background_colorNone和modeRGBA两行。保存出来的PNG自带alpha通道拖进PPT、PS或者微信头像框都非常好用。5. 踩坑实录与调试思路5.1 mask不生效或者生成一整块色块这是词云定制里最常遇到的问题我刚开始做心形词云时也卡了很久。现象一传了mask但词云还是矩形。原因基本是mask图没有正确二值化。比如传的是一张PNG透明爱心图透明通道在二值化时全部变成0传给wordcloud后整个mask全是有效区域等于没有蒙版。解决方法是转成RGB后对非白色区域做二值化就像build_mask_from_image那样。现象二生成出来是一大团颜色文字都挤在边缘或角落里。原因是蒙版中的有效区域太小或者词的数量太多、字号太大词云放不下那么多字。解决方法是调大max_words为300以上调小max_font_size和min_font_size或者换一张有效区域更大的蒙版图。5.2 词云里出现奇怪的词组或重复单字有时候词云里会出现依然温暖依然一个个个这种莫名其妙的重复。多数原因是停用词表没过滤干净导致高频单字、无意义词反复出现打开了collocations相邻词被自动配对成双词组处理手段有两个方向词频统计阶段过滤掉len(word) 2的词并在Counter之后手动剔除人工确认过的干扰词生成阶段关闭collocations直接用generate_from_frequencies。5.3 豆瓣请求返回校验页面或者登录页前面提到过触发限流时页面内容不再是正常的短评列表。如果fetch_douban_comments返回的comments是空列表先别急着怀疑xpath写错把响应内容打印出来看一眼resp requests.get(url, headersheaders, timeout10) print(resp.status_code) print(resp.url) # 看看有没有被重定向到登录/校验页如果status_code是302或者url变成了login相关地址就代表被拦了。对策优先级先等10分钟降低频率再补上Cookie。请求间隔拉长到2-3秒pages控制在3页以内多数场景就没问题了。5.4 调试自查清单每次出现问题我习惯按这张清单排查基本都能快速定位现象优先检查点词云全是方框font_path路径是否正确词云全是完整句子分词是否完成文本是否按空格拼接背景白但图没形状mask二值化是否正确图案是否为黑色词云异常拥挤max_font_size是否过大mask是否太小词全是“的”“了”停用词表是否加载自定义停用词是否足够爬虫返回为空UA/Referer是否齐全是否被重定向颜色不好看colormap是否设置字号范围是否合理5.5 让词云更耐看的两个小技巧文字密度控制。wordcloud默认会把max_words范围内的词全塞进去词一多就会显得脏。想让图更耐看可以把max_words调到120到150之间观察词频top150那批词滤掉最后十几个低频词图会清爽很多。导出时提高清晰度。如果你的词云要打印、要当壁纸建议把scale设为2或3这样输出的分辨率会成倍提升wc WordCloud( ..., scale3 )scale的原理是对画布做整体放大再交给渲染器绘制最终PNG的物理尺寸会变大字与字之间的间距也会更清晰。代价是生成时间变长但对普通图片完全值得。一个更容易被人忽略的参数是margin它控制词与词之间的像素间隔。默认是2做极简风格时可以调到4到6视觉上会有明显的呼吸感白底红字的心形词云就会从拥挤的贴纸堆变成有留白的排版作品。我个人做心形词云时最喜欢这个参数先调好形状和配色再慢慢拉大margin最后总能找到一个让整张图松弛下来的临界值。这套流程跑通之后你手里就有一套完全属于自己的词云生成管线了——换一部电影改一行subject_id抓下来一肚子新评论重新分一次词一分钟不到就能得到一张新的词云图。后续还可以往这个方向扩展把短评按好评差评分桶分别生成红蓝两色词云做对比或者把电影台词、书籍全文拿来做成更有故事感的词云。我一直觉得这类小项目的价值不在于用了多复杂的算法而在于它让你把请求、解析、清洗、可视化这一整条数据处理链路亲手走了一遍以后再碰到任何文本类需求脑子里都会自动浮现出这套流程的边界在哪里、哪个环节容易出问题。
返回列表