news 2026/10/22 11:33:54

Python爬虫实战:抓取豆瓣短评生成个性化词云

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:抓取豆瓣短评生成个性化词云

爬虫这个词,在不少人眼里可能还带点“黑科技”滤镜,但拆开了其实就是三步:发一个请求、拿回一段页面、把我们要的信息抠出来。今天这篇教程,我从头写了一套完整的 Python 爬虫小项目:抓取豆瓣最新电影短评,把短评文字清洗好,再交给 wordcloud 库,生成默认矩形、圆形、自定义形状等各种风格的词云图。整个过程要涉及 HTTP 请求、页面解析、中文分词、图像蒙版这几块知识,但我不准备讲太深的理论,重点放在每一步到底怎么做、为什么这么做、实际踩过哪些坑。这篇内容适合刚接触爬虫的朋友直接照着抄,也适合想用评论数据做可视化图表的同学参考。

为什么选“评论 + 词云”这个组合?因为电影短评本身就是一段段带情感色彩的短文本,分词之后很容易出效果,不像新闻稿那样全是规范书面语。把几千条评论揉成一张图,哪些词被提得最多、观众主要在讨论什么,一眼就能看出来。这个流程跑通之后,换任何一个带评论区的内容平台,改改选择器和 URL 规则就能复用。

1. 项目整体设计与技术选型

1.1 核心需求拆解

只看标题的话,很多人第一反应可能是“直接 pip install wordcloud 就行了”。但实际从零跑通“评论到词云”的全流程,至少要解决四件事:

  1. 抓数据:能稳定拿到评论区的正文内容;
  2. 提字段:把评论文字、评分、时间这些要素拆出来;
  3. 分词:把中文句子切成适合统计的词;
  4. 画图:让词云的布局、字体、颜色、形状都看得顺眼。

每一环拆开都不难,真正费时间的是串起来之后的调试。比如页面里一个评论容器,既包含用户信息又包含评论内容,选择器写得不准确,就可能把整块 HTML 全部捞出来,后面的分词效果自然一塌糊涂。

我自己的习惯是先把流程拆成独立函数,不要所有逻辑堆在同一个脚本里。爬虫和可视化本来就是两个阶段,分开写的好处是随时可以单独换数据源,也不用每次调字体颜色都把上一轮的请求重新跑一遍。

1.2 技术栈为什么这样选

这个项目的核心库是这几样:

  • requests:发 HTTP 请求,比标准库里的 urlib 顺手得多;
  • BeautifulSoup4 + lxml:解析 HTML 文档,用 CSS 选择器提取评论;
  • jieba:中文分词,这是中文词云的必需品;
  • wordcloud:词云生成主库;
  • matplotlib:wordcloud 底层渲染以及图片保存依赖它。

不做全套爬虫框架,是因为这类短评页面数据量并不大,requests 足够用。Scrapy 有自己的调度器、中间件和并发模型,适合大规模采集或者需要持续增量更新的场景。如果只是做个学习项目或者一次性分析,引 Scrapy 反而有点大材小用,写配置的成本比爬数据本身还高。

1.3 整体流程预览

整个项目的处理链路大致是:

  1. 构造请求,带浏览器 User-Agent 请求短评页;
  2. 用 BeautifulSoup 解析评论容器,逐条提取文本;
  3. 清洗文本,去掉空格、HTML 实体、表情符号;
  4. 保存成 txt 文件备用;
  5. 用 jieba 分词,再用空格拼接成词云可识别的字符串;
  6. 配置 WordCloud 参数,生成词云图并保存 PNG。

这个顺序基本是固定的。中间任何一步出问题,后面都会连锁报错,所以调试的时候按顺序来,一条一条验证。

2. 爬虫部分:把评论稳稳听下来

2.1 评论页 URL 结构和分页参数

我一般不记死 URL,直接在浏览器里打开一部电影的短评页,看地址栏里的真实链接。豆瓣电影短评页的 URL 大致是这个形态:

https://movie.douban.com/subject/{电影id}/comments?status=P&sort=new_score

其中subject后面的数字是这部电影条目在豆瓣里的唯一 ID,status=P表示只看看过的人的短评,sort=new_score表示按最新评论排序。

翻页靠start和limit两个参数,limit每一页通常是 20 条,start从 0 开始翻。第二页的地址就带?start=20&limit=20,第三页是?start=40&limit=20。每部电影的短评总量不一样,想抓多少页,写一个循环把start递加上去就行。

我第一期调试的时候,只抓两页数据,也就是大约 40 条评论。先拿小样本跑通全流程,再决定要不要扩大范围,这样出了问题也好排查。

2.2 请求头设置和反爬判断

第一次写爬虫时很多人容易踩的坑是:不带 User-Agent 直接请求,结果要么拿到 HTTP 403,要么被打到验证页面。豆瓣对 UA 的校验不算苛刻,但裸请求一眼就会被识别成非浏览器访问。

一个比较稳妥的请求头至少包含 User-Agent 和 Referer:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36", "Referer": "https://movie.douban.com/", }

把浏览器真实版本信息原样抄过来就行,没有必要伪造得太夸张。另外一个必须重视的控制点是请求频率,单线程每请求一页睡 1 到 3 秒,不要一上来就连着打几十页。这个项目本质是学习技术,保持基本礼貌是对目标站点的尊重,也是保护自己 IP 不被临时限制的最简单做法。

2.3 HTML 解析逻辑

页面拿到手之后,处理流程是固定的:先解析 HTML,再定位评论容器。我最早也试图用正则硬抠评论内容,后来放弃了,因为页面结构稍微一变,正则表达式就要重写一遍。BeautifulSoup 的 CSS 选择器直观很多,也容易维护。

短评页里每条评论的容器一般带有comment-item这类的 class,评论正文通常在short这个 class 下面。解析代码大概长这样:

from bs4 import BeautifulSoup soup = BeautifulSoup(html, "html.parser") comments = [] for item in soup.select(".comment-item"): short = item.select_one(".short") if short: comments.append(short.get_text())

这里用html.parser而不是lxml也能跑,但lxml解析速度更快、容错更好。如果没安装 lxml,记得先pip install lxml。

有个细节:网站改版是常事,如果你抓的时候发现.comment-item一个都选不到,先不要怀疑代码逻辑,直接print(soup.prettify()[:2000]),看当前页面实际结构长什么样。类名变了就换选择器,这才是爬虫常态。

2.4 文本清洗与保存

从 HTML 里拿到的文本经常带着各种杂质,比如 这类 HTML 实体、多余换行、前后空格,还有大量表情符号。不洗一洗,分词阶段就会冒出一堆奇奇怪怪的“词”。

我常用的清洗顺序是:

import html import re def clean_text(raw): text = html.unescape(raw) # 去掉非中文、非字母、非数字的符号,替换成空格 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", text) # 连续空白合并成一个空格 text = re.sub(r"\s+", " ", text) return text.strip()

这里把表情符号一并过滤掉了。因为 wordcloud 对特殊 Unicode 字符的处理能力有限,留着反而容易在分词阶段产生无意义词项。

清洗后的数据统一追加保存到一个 txt 文件里,方便后面多次生成词云而不用重新爬。写文件时注意用 UTF-8 编码:

with open("comments.txt", "a", encoding="utf-8") as f: for c in comments: f.write(clean_text(c) + "\n")

3. wordcloud 词云生成核心细节

3.1 为什么中文必须先分词

wordcloud 的核心逻辑是按空格把文本拆成单词,然后根据单词出现频次决定显示大小。英文天然有空格,直接扔进去就能画。中文不一样,一句话连在一起,如果不做分词,生成出来就是一团乱码。

所以标准做法是先用 jieba 处理:

import jieba with open("comments.txt", "r", encoding="utf-8") as f: text = f.read() words = jieba.cut(text) filtered_words = " ".join(words)

jieba.cut返回的是一个生成器,用空格把切分结果拼成字符串以后,才能交给 WordCloud 的generate方法。

如果你发现某些专业词、电影名、角色名被切碎了,可以给 jieba 加自定义词典。比如有一部电影叫“长空之王”,jieba 可能会把它拆成“长空”和“之王”,那就在自定义词库里手动加入这个词,权重设高一点,分词结果会有立竿见影的改善。

3.2 WordCloud 核心参数怎么调

wordcloud 的参数不少,但真正影响最终效果的主要是这些:

参数作用我的建议
width/height画布大小,单位像素一般用 1200x800,太小的图文字挤在一起
background_color背景颜色常用白色,方便展示;黑底也很有质感
font_path字体文件路径中文必填,否则全是方框
max_words最多显示的词数默认 200,评论多的时候调到 300 也没问题
max_font_size最大字号不设反而更自然,让词频自己决定
min_font_size最小字号设到 10 左右,避免出现密密麻麻的小字
stopwords停用词集合中文必须自己准备,默认停用词只覆盖英文
mask形状蒙版传 NumPy 数组,决定词云整体轮廓
colormap配色方案用 matplotlib 的 colormap,比如 “viridis”

这些参数不需要一次全调完。我自己的经验是先把字体大小和停用词解决,再调形状和颜色,因为这两者最影响观感。

3.3 中文方框问题的本质

很多人在 Linux 服务器上生成的词云全是小方框,根本不是词云的问题,而是系统里没有可用的中文字体。wordcloud 需要知道字体文件在哪个路径,并且那个字体必须支持中文字形。

常见的中文字体位置:

  • Windows:C:\Windows\Fonts\simhei.ttf或C:\Windows\Fonts\msyh.ttc
  • macOS:/System/Library/Fonts/PingFang.ttc
  • Linux:/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc

代码里按平台写一个候选路径列表,逐个用os.path.exists检查,能规避大部分环境差异问题:

import os from wordcloud import WordCloud font_candidates = [ "C:/Windows/Fonts/simhei.ttf", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", ] font_path = next((p for p in font_candidates if os.path.exists(p)), None)

Linux 环境缺字体时,安装fonts-wqy-zenhei一类中文字体包就可以解决。

3.4 形状词云的实现条件

想生成圆形、爱心或者自定义形状的词云,核心是mask参数。wordcloud 会把 mask 当作一张“画布遮罩”,只允许在非白色区域绘制文字,白色区域留空。

实际操作分三步:

  1. 用 Pillow 打开图片;
  2. 转换成 RGB 或 RGBA 数组;
  3. 传给 WordCloud 的 mask 参数。
from PIL import Image import numpy as np mask_img = Image.open("custom_mask.png").convert("RGBA") mask_array = np.array(mask_img) wc = WordCloud( font_path=font_path, background_color="white", mask=mask_array, width=800, height=800, ) wc.generate(filtered_words) wc.to_file("custom_wordcloud.png")

这里有个常见的坑:mask 图片最好本身是 1:1 左右的比例,而且图形区域要清晰。如果图片有复杂的渐变或半透明区域,wordcloud 会不知道哪些地方该留白,结果文字乱成一团。推荐准备一张纯黑背景、白色轮廓的图,效果最稳定。

3.5 给词云上色的两种方式

第一种:直接用colormap参数,生成的是同一色系渐变,非常省事。比如colormap="cool"就是蓝到青色的过渡。

第二种:让词云颜色从一张彩色图片里取色。这个场景更适合做品牌相关内容,例如先把一张充满电影海报色调的图片塞进去,生成的词云颜色就和海报自然统一。

from wordcloud import ImageColorGenerator img_colors = ImageColorGenerator(mask_array) wc = wc.recolor(color_func=img_colors)

注意recolor是生成完词之后重新着色,不影响原有词的布局和大小。

4. 完整实战:爬取评论并生成多种词云

4.1 完整代码流程

把前面所有环节串起来,就是一个可以直接运行的脚本:

import os import time import html import re import requests import jieba import numpy as np from PIL import Image from bs4 import BeautifulSoup from wordcloud import WordCloud from wordcloud import ImageColorGenerator HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36", "Referer": "https://movie.douban.com/", } def clean_text(raw): text = html.unescape(raw) text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", text) text = re.sub(r"\s+", " ", text) return text.strip() def fetch_comments(subject_id, pages=2): comments = [] for page in range(pages): start = page * 20 url = f"https://movie.douban.com/subject/{subject_id}/comments?start={start}&limit=20&status=P&sort=new_score" resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code != 200: print(f"请求失败,状态码:{resp.status_code}") break soup = BeautifulSoup(resp.text, "html.parser") items = soup.select(".comment-item") if not items: print("未找到评论节点,页面结构可能已变化") break for item in items: short = item.select_one(".short") if short: comments.append(clean_text(short.get_text())) time.sleep(2) return comments def save_comments(comments, path="comments.txt"): with open(path, "a", encoding="utf-8") as f: for c in comments: f.write(c + "\n") def build_wordcloud(text_path, mask_path=None, out_path="wordcloud.png"): with open(text_path, "r", encoding="utf-8") as f: text = f.read() words = " ".join(jieba.cut(text)) font_candidates = [ "C:/Windows/Fonts/simhei.ttf", "/System/Library/Fonts/PingFang.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", ] font_path = next((p for p in font_candidates if os.path.exists(p)), None) stop_words = set(["真的", "觉得", "感觉", "还是", "一个", "有点", "这部", "电影", "但是", "所以", "不过"]) wc = WordCloud( font_path=font_path, background_color="white", width=1200, height=800, max_words=300, min_font_size=10, stopwords=stop_words, ) if mask_path: mask_img = Image.open(mask_path).convert("RGBA") mask_array = np.array(mask_img) wc.mask = mask_array wc.generate(words) wc.to_file(out_path) print(f"词云已保存:{out_path}") if __name__ == "__main__": subject_id = "某个你想分析的电影ID" comments = fetch_comments(subject_id, pages=2) save_comments(comments) build_wordcloud("comments.txt", mask_path=None, out_path="rectangle_wordcloud.png") build_wordcloud("comments.txt", mask_path="circle_mask.png", out_path="circle_wordcloud.png")

这个脚本里我把爬虫和词云拆成了两个函数。第一次跑的时候,建议注释掉最后一行,先只抓两页数据,确认文本文件内容没问题,再重新执行词云部分。

4.2 多风格词云效果对比

同样一份评论数据,换了 mask 和配色,效果差异很大。

  • 默认矩形适合做日报,信息密度高,适合放进文档。
  • 圆形适合做分享图,主体突出,视觉上更干净。
  • 自定义形状适合做专题封面,比如针对一部关于爱情的电影,用爱心蒙版,观众一眼就能从图形联想到主题。

mtp:爱心蒙版图直接在网上找一个黑白的爱心轮廓,转成拉片,效果就很好。注意权重维度不能太小,我一般用 800x800 的图。

4.3 参数调优经验

我最常调的两个地方:停用词和最大词数。

停用词没做好,词云里就会有一堆“真的”、“觉得”、“还是”这类跟情感无关的连接词。我的做法是先跑一版,看看图上高频词里哪些明显是废话,再把它们加进stop_words,重复两三轮就差不多了。

max_words设多少更合适?我不建议一上来设很大,先设 150 到 200,观察密度。如果一个词云看着像整个页面被墨水糊满了,那就把max_words调小,或者把min_font_size调高一点,让画面有呼吸感。

5. 高频问题与排错实录

这里把我实际遇到过的几类问题整理成表格备查。

现象可能原因解决方案
词云里全是方框没有指定中文字体或字体路径错误检查字体文件是否存在;Linux 安装中文字体包
词云里全是“的/了/就是”没有停用词列表准备中文停用词集合,传入stopwords
请求返回 403 或验证页缺少 User-Agent 或请求频率太高补全请求头;增加 sleep 间隔
评论列表为空页面结构改版,class 名变了打印前 2000 字符 HTML,重新确认选择器
mask 后文字只有外围一圈蒙版图里有大片浅色区域干扰使用纯黑背景白色轮廓图,避免透明渐变
保存 PNG 后文字模糊画布尺寸太小把 width/height 调大,比如 1600x1200

除了表格里的问题,还有个容易忽略的点:评论文件是追加写入的,第二次运行前如果忘了清空旧文件,词云里会出现上一次遗留的数据。我一般会在每次爬取前先删除旧 txt。

请求被临时限制是新手很常见的事。遇到这种情况别硬冲,先停个几分钟,检查自己是不是把间隔设太短了。爬虫这种活儿,慢就是快,把频率控制好,整个流程反而更顺。

6. 延伸玩法与我的几点体会

这个项目往后还有不少扩展空间。

比如不只看单一电影的评论,把几部同类电影的评论分别抓下来,生成多张词云图做对比;也可以把文本清洗改成同时保留评分和评论区时间,做时间维度的分析;甚至对分词结果做一个简单的情感打分后,再按情感分数给词云着色,让“好评高频词”和“差评高频词”从颜色上就能区分。

如果以后数据量变大,文本文件动辄几万条,jieba 分词的速度就会明显变慢。到时候可以引入多进程把分批处理,或者换 Pandas 做聚合,wordcloud 本身只要能拿到分词后的字符串就行,不关心前面是什么框架。

我个人在实际操作中的体会是,这个项目最难的不是任何一个单独环节,而是“出了问题不知道是哪一环”的挫败感。所以我的调试顺序永远是:先确认请求状态码,再打印解析结果,最后才怀疑词云参数。每一步都验证过,再往下走,反而很少遇到需要整段重写的情况。

最后分享一个小技巧:准备一个固定的停用词文件,把所有常见中文虚词一次性维护好,换任何文本做词云都能直接用。别小看这一步,维护好这份词表以后,每次生成词云都会快很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:33:40

Python爬虫实战:抓取豆瓣最新短评并生成中文词云

我最早写爬虫练手,就是拿豆瓣影评做词云,当时没想太多,纯粹想看看大家的评论里到底在聊什么。结果一上手才发现,从“能拿到数据”到“生成一张像样的词云图”,中间隔着一大堆细节:反爬怎么处理、网页结构怎…

作者头像 李华
网站建设 2026/10/22 11:32:06

fastbin_reverse_into_tcache:绕过 tcache 双重释放检查的堆利用深入解析

how2heap 的练习我前后刷过三遍,每次都会发现一些之前忽略的细节。今天想聊的是第二篇里一个非常经典的 2.31 考点: fastbin_reverse_into_tcache 。这个技巧本质上是把 fastbin 里的伪造节点反向塞进 tcache,从而绕过 tcache 的 double fr…

作者头像 李华
网站建设 2026/10/22 11:32:06

嵌入式C语言指针核心应用:内存模型、寄存器操作与野指针排查

指针这东西,C语言里绕不过去的坎。尤其是嵌入式开发,每天跟寄存器、缓冲区、硬件地址打交道,不懂指针可以说是寸步难行。我见过太多人刚开始学嵌入式,被指针搞得心态爆炸,其实指针没那么玄乎,你把它想明白了…

作者头像 李华
网站建设 2026/10/22 11:32:06

基于中心化Q-Learning的双无人机NOMA通信路径与功率联合优化

1. 项目概述与核心思路拆解 1.1 双无人机加NOMA:这个组合到底在解决什么问题 先说个大白话版本的背景。如果你现在打开手机基站地图,会发现大量基站是固定的,覆盖区域也是规划死的。但在某些场景下,这种固定基础设施根本来不及部…

作者头像 李华
网站建设 2026/10/22 11:29:48

主成分分析(PCA)详解:原理、手写实现与常见坑

提到主成分分析(PCA),很多朋友的第一反应是“又来了一个降维算法”。确实,几乎每一个涉及数据处理的项目里,PCA都像一个默认选项:数据太乱?跑一下PCA。特征太多?跑一下PCA。可视化做…

作者头像 李华
网站建设 2026/10/22 11:28:29

基于预设时间观测器的无人机编队容错控制仿真复现

简介:一份论文复现资料包,面向自动化控制科研人员与研究生,聚焦多无人机系统的预设时间编队容错控制。其核心是新型观测器设计,将状态观测、扰动估计与执行器故障在线辨识统一起来,并结合分布式预设时间编队跟踪控制协…

作者头像 李华