最近不少同学在准备毕业设计或找工作简历项目时,都会问同一个问题:Python 数据分析到底做什么项目才有亮点?今天我想分享一个很适合拿来练手、也足够写进简历的完整案例——泡泡玛特热搜评论数据分析与可视化。这个案例覆盖了爬虫采集、数据清洗、情感分析、关键词提取、可视化大屏展示的完整链路,代码量适中,但技术栈非常全,而且主题贴近年轻人的消费热点,做出来的效果也很直观。
本文将基于 Python3 手把手拆解整个项目,从数据采集到最终可视化图表生成,每一行代码都会解释作用。不管你是 Python 入门不久的新手,还是正在准备数据分析岗位面试的求职者,都可以按文章顺序复现这个项目。项目用到的核心库包括 requests、pandas、pyecharts、jieba 等,我会在对应章节说明安装方式。
先说明一点:本文的爬虫部分以公开可访问的评论内容为演示对象,重点在于讲解技术流程。实际抓取时,你需要结合目标站点的页面结构和平台规则进行调整,并遵守网站的爬虫协议与相关法律法规,数据仅限用于学习研究。
1. 项目背景与核心概念
1.1 为什么选择泡泡玛特评论数据
泡泡玛特是近年热度很高的潮玩品牌,旗下 Molly、SKULLPANDA、DIMOO 等 IP 在社交平台上的讨论量非常大。用户评论里既包含对具体产品的评价,也包含对 IP 形象、抽盒体验、价格、质量等维度的真实反馈。
这些评论数据天然具备几个适合做数据分析的特点:
- 数据量可观:热门产品评论区往往有成百上千条真实评论,足够支撑统计分析和情感判断。
- 文本信息丰富:评论里包含大量关键词,比如“好看”“隐藏款”“雷款”“品控”“发货速度”等,适合做关键词提取和情感分析。
- 用户画分明晰:不少平台会展示用户的所在城市、等级、性别等信息,方便做多维度的交叉分析。
- 商业价值明显:分析结果能帮助品牌了解用户口碑、发现问题产品、评估 IP 热度,是一个很有说服力的业务分析场景。
从学习角度来说,这个项目不需要复杂的分布式爬虫架构,单机即可完成全部流程,同时又能把 Python 数据分析的核心技能都串联起来,非常适合作为第一个完整的数据分析实战项目。
1.2 数据爬虫、数据分析、数据可视化的关系
很多初学者会把“爬虫”“数据分析”“可视化”当成三个独立的技能,实际上在一个真实项目中它们是环环相扣的。
爬虫负责解决“数据从哪来”的问题。它通过模拟浏览器请求,从目标网页或接口中提取结构化数据,比如评论内容、评分、时间、用户名等。
数据分析负责解决“数据说明了什么”的问题。拿到原始数据后,通常不能直接使用,因为里面会有缺失值、重复值、无意义字符等。这一步要做数据清洗、字段提取、统计聚合、情感判断,最终得到有价值的结论。
数据可视化负责解决“结论如何呈现”的问题。通过柱状图、饼图、词云、地图等图表,把分析结果直观地表达出来。相比一大堆 Excel 表格,图表形式更适合做汇报和展示,也是简历项目中容易出彩的部分。
这个案例的顺序非常典型:评论采集 → 数据清洗 → 关键词统计 → 情感分析 → 可视化展示。每一步都依赖上一步的结果,所以建议严格按照文章顺序逐步完成。
1.3 本项目适合哪些读者
我在设计这个教程时,主要考虑了以下三类读者:
- Python 基础语法已经学过,但不知道如何完成一个完整项目的初学者。
- 需要做课程设计、毕业设计,想找一个难度适中且有展示效果题目的同学。
- 正在准备数据分析或 Python 开发岗位面试,需要简历项目经历和面试案例的求职者。
如果你目前只会编写简单的 Python 脚本,对 pandas 和 pyecharts 不太熟悉,当然也可以跟着做。文章中出现的每一行代码都会有注释,遇到不懂的库只需要记住“它解决了什么问题”就行。
2. 环境准备与项目结构
2.1 开发环境说明
本文示例代码基于以下环境编写,具体版本可以根据你的电脑环境适当调整。
操作系统:Windows 10 / 11,macOS,Linux 均可 Python 版本:Python 3.8 及以上 IDE:PyCharm 或 VS Code 浏览器:Chrome / Edge(用于查看生成的 HTML 可视化图表)建议使用虚拟环境隔离项目依赖,避免污染全局 Python 环境。下面是在项目目录下创建虚拟环境的命令。
python -m venv venv在 Windows 系统下激活虚拟环境:
venv\Scripts\activate在 macOS 或 Linux 系统下激活虚拟环境:
source venv/bin/activate激活后,命令行提示符前会出现(venv)字样,说明当前已经进入虚拟环境。
2.2 依赖库安装
本项目主要用到以下 Python 库:
| 库名称 | 用途 |
|---|---|
| requests | 发送 HTTP 请求,获取网页或接口数据 |
| BeautifulSoup4 | 解析 HTML 页面,提取评论信息 |
| pandas | 数据清洗、聚合统计、DataFrame 操作 |
| jieba | 中文分词,提取评论关键词 |
| pyecharts | 生成交互式可视化图表,输出 HTML |
| wordcloud | 生成词云图 |
| matplotlib | 生成基础静态图表,配合 wordcloud 使用 |
执行下面的命令一次性安装所需依赖。
pip install requests beautifulsoup4 pandas jieba pyecharts wordcloud matplotlib如果安装速度较慢,可以临时使用国内镜像源安装。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 pandas jieba pyecharts wordcloud matplotlib需要注意,pyecharts 的版本差异会影响 API 调用方式,本文以常用稳定写法为准。如果你之前安装过旧版 pyecharts,建议升级到最新版本再继续操作。
pip install --upgrade pyecharts2.3 项目目录结构
为了保持代码整洁,建议按下面的目录结构组织项目文件:
popmart-analysis/ ├── venv/ # 虚拟环境目录 ├── data/ # 存放原始数据和清洗后的数据 │ ├── comments_raw.csv │ └── comments_clean.csv ├── output/ # 存放输出的可视化图表 │ ├── comment_trend.html │ ├── ip_rank.html │ ├── city_distribution.html │ ├── sentiment_pie.html │ └── wordcloud.png ├── crawler.py # 评论数据采集脚本 ├── data_clean.py # 数据清洗与预处理脚本 ├── analysis.py # 数据分析与可视化脚本 └── requirements.txt # 依赖清单实际开发时,可以根据项目规模灵活调整,但建议把爬虫、清洗、分析三个环节拆分成不同脚本,这样后期维护和调试会方便很多。如果写到 Jupyter Notebook 里,则可以按单元格顺序执行,效果也类似。
3. 评论数据采集:从网页到结构化数据
3.1 爬虫的合规边界
在编写爬虫之前,必须先明确法律和平台规则边界。《网络安全法》《数据安全法》《个人信息保护法》都对网络数据采集有明确要求。做学习项目时,请严格遵守以下几点:
- 只采集公开可访问的数据,不突破登录权限、不绕过反爬机制。
- 遵守目标网站的 robots.txt 协议。
- 控制请求频率,不要对目标服务器造成压力。
- 采集的个人信息用于学习研究,不做商业用途。
- 不对采集到的数据做用户身份识别和敏感信息分析。
本文的核心是讲解技术方法,示例中的页面结构仅用于演示。实际运行时一定要根据目标站点的规则调整代码。
3.2 设计评论数据字段
在写爬虫之前,先想清楚要采集哪些字段。设计良好的字段结构能减少后续清洗的工作量。
针对泡泡玛特评论场景,建议保留以下字段:
| 字段名 | 说明 | 示例 |
|---|---|---|
| user_name | 用户昵称 | 一颗泡泡糖 |
| user_city | 用户所在城市 | 上海 |
| comment_time | 评论时间 | 2024-11-20 15:32:10 |
| comment_content | 评论正文 | 手感很好,隐藏款做工超赞! |
| product_name | 评论对应的产品名 | SKULLPANDA 温度系列 |
| rating | 评分或星级 | 5.0 |
| like_count | 点赞数 | 128 |
有了这个字段设计,后续无论是做时间趋势、城市分布还是词云分析,都有对应的数据支撑。
3.3 使用 requests 获取页面数据
下面是一个基础的请求代码示例。使用 requests 库向目标评论页面发送 GET 请求,并设置合理的请求头。
# 文件路径:crawler.py import requests import time import csv import random def fetch_html(url): """ 发送 HTTP GET 请求,返回网页 HTML 文本。 示例代码,实际使用时请根据目标站点的请求方式调整。 """ 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = "utf-8" return resp.text except requests.RequestException as e: print(f"[请求失败] {url}, 错误信息: {e}") return None代码中做了几件比较重要的事情:
- 自定义 User-Agent,模拟浏览器环境。
- 设置 timeout,避免某个请求长时间阻塞程序。
- 捕获 requests.RequestException,便于分析失败原因。
- 设置响应编码为 utf-8,避免中文乱码。
3.4 解析评论数据
拿到 HTML 文本后,需要使用 BeautifulSoup 解析页面结构,定位到评论列表所在的标签区域。
from bs4 import BeautifulSoup def parse_comments(html): """ 解析页面中的评论数据。 注意:这里的选择器仅为演示,实际运行时要根据目标页面结构调整。 """ if not html: return [] soup = BeautifulSoup(html, "html.parser") comments = [] # 假设每条评论都包含在 class="comment-item" 的 div 中 comment_items = soup.select(".comment-item") for item in comment_items: try: user_name = item.select_one(".user-name").get_text(strip=True) user_city = item.select_one(".user-city").get_text(strip=True) comment_time = item.select_one(".comment-time").get_text(strip=True) comment_content = item.select_one(".comment-content").get_text(strip=True) product_name = item.select_one(".product-name").get_text(strip=True) rating_text = item.select_one(".rating").get_text(strip=True) comments.append({ "user_name": user_name, "user_city": user_city, "comment_time": comment_time, "comment_content": comment_content, "product_name": product_name, "rating": rating_text, }) except AttributeError: # 某条评论缺少字段时跳过,避免程序中断 continue return comments这里的.select_one()方法返回第一个匹配的标签,.get_text(strip=True)表示提取标签文本并去掉首尾空白。由于不同网站的 HTML 结构差异很大,实际使用时你需要打开浏览器开发者工具,找到评论对应的真实 class 或 id,再进行选择器调整。
3.5 完整采集流程与数据保存
实际评论数据通常不止一页,需要翻页采集。翻页时要注意控制请求间隔,随机休眠 1 到 3 秒,降低对目标服务器的压力。
def crawl_comments(base_url, total_pages=10): """ 翻页采集评论数据,保存到 CSV 文件。 """ all_comments = [] for page in range(1, total_pages + 1): page_url = f"{base_url}?page={page}" print(f"正在采集第 {page} 页,URL: {page_url}") html = fetch_html(page_url) page_comments = parse_comments(html) if not page_comments: print(f"第 {page} 页没有解析到数据,停止翻页") break all_comments.extend(page_comments) # 随机休眠 1-3 秒,避免请求频率过高 time.sleep(random.uniform(1, 3)) # 保存为 CSV with open("data/comments_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=[ "user_name", "user_city", "comment_time", "comment_content", "product_name", "rating" ]) writer.writeheader() writer.writerows(all_comments) print(f"采集完成,共获取 {len(all_comments)} 条评论") return all_comments if __name__ == "__main__": # 实际使用时请替换为目标评论页地址 demo_url = "https://example.com/popmart/comments" crawl_comments(demo_url, total_pages=5)CSV 文件使用utf-8-sig编码保存,这样做的好处是:用 Excel 直接打开时中文不会乱码。如果你只是用 pandas 读取,使用utf-8编码也完全可以。
到这里,爬虫部分的完整流程就结束了。真实项目中你可能会遇到动态加载的评论数据,这类数据一般通过 Ajax 接口返回 JSON 格式,此时不需要解析 HTML,直接请求接口地址并用resp.json()解析即可。本质思路是一样的。
4. 数据清洗与预处理
4.1 读取原始数据
采集完成后,接下来进入数据分析的关键环节:数据清洗。先用 pandas 读取 CSV 文件,查看数据基本状况。
# 文件路径:data_clean.py import pandas as pd # 读取原始数据 df = pd.read_csv("data/comments_raw.csv", encoding="utf-8-sig") print("原始数据形状:", df.shape) print("字段列表:", df.columns.tolist()) print("\n前 5 行数据:") print(df.head()).shape返回数据表的行数和列数,.head()默认显示前 5 条数据。这一步主要是确认数据是否成功读取,并观察字段是否完整。
4.2 处理缺失值与重复值
评论数据中经常出现部分字段为空的情况,比如用户没有填写城市、评分字段缺失等。处理方式要根据业务场景选择:
- 如果缺失比例很低,可以直接删除缺失行。
- 如果某字段缺失比例较高,可以考虑填充默认值。
- 评论内容为空的记录没有分析价值,建议删除。
# 查看每列缺失值数量 print("缺失值统计:") print(df.isnull().sum()) # 删除评论内容为空的行 df = df.dropna(subset=["comment_content"]) # 删除完全重复的行 df = df.drop_duplicates() # 用户名和城市缺失的,填充为“未知” df["user_name"] = df["user_name"].fillna("未知用户") df["user_city"] = df["user_city"].fillna("未知") # 重置索引 df = df.reset_index(drop=True) print("清洗后数据形状:", df.shape)dropna(subset=["comment_content"])表示只在 comment_content 这一列检测缺失值。drop_duplicates()默认删除所有列完全相同的重复行。如果你想根据某一列去重,可以写成drop_duplicates(subset=["comment_content"])。
4.3 时间字段标准化
评论时间可能是多种格式,比如“2024-11-20 15:32:10”“11月20日”“刚刚”等。为了后续分析趋势,需要统一转换为 pandas 的 datetime 类型。
# 将评论时间转换为 datetime 类型,无法转换的设为 NaT df["comment_time"] = pd.to_datetime(df["comment_time"], errors="coerce") # 删除时间解析失败的行 df = df.dropna(subset=["comment_time"]) # 提取日期和小时,便于按天统计 df["comment_date"] = df["comment_time"].dt.date df["comment_hour"] = df["comment_time"].dt.hour print(df[["comment_time", "comment_date", "comment_hour"]].head())errors="coerce"是 pandas 中很实用的参数,遇到无法解析的字符串时会转换为 NaT(缺失时间),而不会直接报错中断程序。
4.4 评论文本长度与简单清洗
评论正文中可能包含大量空格、换行、特殊符号、表情符号。在做词频统计之前,需要做一轮文本清洗。
import re def clean_text(text): """ 清洗评论文本: 1. 去除多余空白 2. 去除非中文、非英文、非数字的符号 """ if not isinstance(text, str): return "" # 将换行、制表符替换为空格 text = re.sub(r"\s+", " ", text) # 只保留中文、英文、数字和常用标点 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、. ]", "", text) return text.strip() df["clean_content"] = df["comment_content"].apply(clean_text) # 过滤清洗后内容为空的评论 df = df[df["clean_content"] != ""] # 添加文本长度列 df["content_length"] = df["clean_content"].apply(len) print("文本清洗完成,最短长度:", df["content_length"].min()) print("文本清洗完成,最长长度:", df["content_length"].max())这段代码中的正则表达式有两个作用:\s+匹配连续空白字符并替换为单个空格;[^\u4e00-\u9fa5a-zA-Z0-9,。!?、. ]匹配所有不在中文字符、英文字母、数字和常见标点范围内的字符,替换为空字符串。
4.5 保存清洗后的数据
完成以上步骤后,把清洗结果保存为新的 CSV 文件,方便后续分析脚本直接读取。
# 保存清洗后的数据 clean_columns = [ "user_name", "user_city", "comment_time", "comment_date", "comment_hour", "product_name", "rating", "clean_content", "content_length" ] df[clean_columns].to_csv("data/comments_clean.csv", index=False, encoding="utf-8-sig") print("清洗后数据已保存到 data/comments_clean.csv")清洗环节在整个项目中非常重要。很多初学者拿到数据后直接做可视化,最终图表里出现大量“None”“NaN”“未知”等脏数据,分析结论自然不可信。只有保证数据质量,后续的可视化才有意义。
5. 数据处理与关键指标分析
5.1 评论数量趋势分析
拿到清洗后的数据,先做基础统计。评论量的时间变化能够直观反映产品热度。
# 文件路径:analysis.py import pandas as pd df = pd.read_csv("data/comments_clean.csv", encoding="utf-8-sig") df["comment_date"] = pd.to_datetime(df["comment_date"]) # 按日期统计评论量 daily_count = df.groupby(df["comment_date"].dt.date).size().reset_index(name="count") print(daily_count.head())groupby是 pandas 中最常用的聚合方法。size()统计每个日期出现的次数,reset_index(name="count")把分组结果转换成 DataFrame,并将计数字段命名为 count。
5.2 产品 IP 热度排行
评论中通常包含产品名称或 IP 系列名,可以按产品分组统计评论数量,找出讨论度最高的产品。
# 按产品统计评论量并排序 product_rank = df.groupby("product_name").size().reset_index(name="count") product_rank = product_rank.sort_values("count", ascending=False) print("产品讨论度 Top10:") print(product_rank.head(10))sort_values("count", ascending=False)表示按 count 列降序排列。如果产品名称中存在同一产品不同写法,比如“SKULLPANDA 温度系列”和“温度系列”,可以在清洗阶段做映射替换。
5.3 城市分布分析
如果评论数据中有用户城市字段,可以按城市统计评论数量,观察不同城市的消费活跃度。
# 按城市统计评论量 city_rank = df.groupby("user_city").size().reset_index(name="count") city_rank = city_rank.sort_values("count", ascending=False) print("城市评论量 Top10:") print(city_rank.head(10))这里有一个需要注意的问题:城市字段可能包含“上海”“上海市”两种写法,建议在清洗阶段做统一的省份/城市归一化,或者用str.contains做模糊归类。
5.4 关键词提取与词频统计
中文文本无法直接按空格分词,需要使用 jieba 库。jieba 支持三种分词模式,这里使用精确模式,它最适合文本分析场景。
import jieba def extract_keywords(text): """ 使用 jieba 精确模式分词,返回词语列表。 过滤长度为 1 的单个字和停用词。 """ words = jieba.lcut(text) stopwords = {"的", "了", "是", "我", "你", "他", "这", "那", "也", "都", "就", "而", "及", "与", "或", "一个", "没有", "真的", "感觉", "还是", "可以", "这个", "那个", "什么", "怎么", "自己"} result = [] for word in words: word = word.strip() if len(word) <= 1: continue if word in stopwords: continue result.append(word) return result # 对所有评论内容进行分词 df["keywords"] = df["clean_content"].apply(extract_keywords) # 展开所有关键词并统计词频 from collections import Counter word_counter = Counter() for kw_list in df["keywords"]: word_counter.update(kw_list) print("Top20 高频词:") for word, count in word_counter.most_common(20): print(f"{word}: {count}")Counter.update()可以批量更新计数。most_common(20)返回出现频次最高的前 20 个单词及其次数。
5.5 简单情感倾向判断
情感分析是评论分析中最有业务价值的部分。考虑到教学项目的复杂度,这里使用基于情感词典的简单方法:准备一份积极词表和消极词表,统计评论中出现两类词语的数量,最终判断评论情感。
# 简单情感词典,实际项目可以使用 SnowNLP 等现成工具 positive_words = ["好看", "可爱", "喜欢", "漂亮", "惊喜", "完美", "赞", "好评", "值得", "满意", "舒服", "高级", "精致", "强烈推荐"] negative_words = ["雷", "差", "丑", "失望", "退货", "瑕疵", "垃圾", "后悔", "粗糙", "掉色", "破损", "客服差", "发货慢", "不值"] def judge_sentiment(text): """ 根据情感词典粗判评论情感。 返回:positive / neutral / negative """ pos_count = sum(1 for w in positive_words if w in text) neg_count = sum(1 for w in negative_words if w in text) if pos_count > neg_count: return "positive" elif neg_count > pos_count: return "negative" else: return "neutral" df["sentiment"] = df["clean_content"].apply(judge_sentiment) # 统计情感分布 sentiment_count = df["sentiment"].value_counts() print("情感分布情况:") print(sentiment_count)这种基于词典的方法准确率有限,因为评论里存在否定表达、讽刺表达等情况。但作为入门项目演示已经足够。如果你希望提升准确率,可以改用 SnowNLP、BERT 等模型,这可以作为项目的进阶优化方向。
6. 数据可视化:让分析结果一目了然
6.1 pyecharts 基础使用
pyecharts 是 Python 中非常流行的可视化库,它基于百度开源的 ECharts 实现,能生成交互式 HTML 图表。语法上采用链式调用,代码结构清晰。
使用 pyecharts 时,需要先导入对应图表类型。下面是一个最简单的柱状图示例。
from pyecharts.charts import Bar from pyecharts import options as opts bar = ( Bar() .add_xaxis(["A", "B", "C"]) .add_yaxis("评论量", [10, 20, 15]) .set_global_opts(title_opts=opts.TitleOpts(title="示例柱状图")) ) bar.render("output/demo_bar.html")运行代码后,会在 output 目录下生成一个 demo_bar.html 文件,用浏览器打开即可看到交互式图表。链式调用的意思是,每个方法都返回对象本身,所以可以连续调用.add_xaxis()、.add_yaxis()等方法。
6.2 评论量时间趋势折线图
下面结合前面统计好的 daily_count 数据,生成评论量随时间变化的折线图。
from pyecharts.charts import Line import pyecharts.options as opts # 按日期排序 daily_count = daily_count.sort_values("comment_date") line = ( Line() .add_xaxis([str(d) for d in daily_count["comment_date"]]) .add_yaxis( "评论量", daily_count["count"].tolist(), is_smooth=True, label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="泡泡玛特评论量时间趋势"), tooltip_opts=opts.TooltipOpts(trigger="axis"), xaxis_opts=opts.AxisOpts(name="日期", axislabel_opts=opts.LabelOpts(rotate=45)), yaxis_opts=opts.AxisOpts(name="评论量"), datazoom_opts=[opts.DataZoomOpts(range_start=0, range_end=100)], ) ) line.render("output/comment_trend.html")这里几个参数的作用:
is_smooth=True:让折线更平滑。label_opts=opts.LabelOpts(is_show=False):隐藏折线上的数值标签,让图表更干净。axislabel_opts=opts.LabelOpts(rotate=45):X 轴标签旋转 45 度,防止日期重叠。datazoom_opts:添加缩放组件,方便查看长时间范围的数据。
6.3 产品 IP 热度柱状图
接下来生成产品讨论度 Top10 的横向柱状图。横向柱状图适合展示较长的产品名称。
from pyecharts.charts import Bar # 取 Top10 数据 top10 = product_rank.head(10).sort_values("count", ascending=True) bar = ( Bar() .add_xaxis(top10["product_name"].tolist()) .add_yaxis( "评论量", top10["count"].tolist(), label_opts=opts.LabelOpts(position="right"), ) .reversal_axis() .set_global_opts( title_opts=opts.TitleOpts(title="泡泡玛特产品讨论度 Top10"), xaxis_opts=opts.AxisOpts(name="评论量"), yaxis_opts=opts.AxisOpts(name="产品名称"), ) ) bar.render("output/ip_rank.html").reversal_axis()是 pyecharts 中实现横向柱状图的关键方法,它会交换 X 轴和 Y 轴。排序时先将数据升序排列,这样柱状图会从高到低自然展示,视觉效果更好。
6.4 城市分布地图
pyecharts 绘制中国地图需要先注册地图数据。新版本的 pyecharts 可能需要额外安装地图扩展包。如果不需要地图,也可以使用柱状图展示城市排名,效果同样直观。
下面先给出柱状图方式:
# 城市评论量 Top10 city_top10 = city_rank.head(10).sort_values("count", ascending=True) city_bar = ( Bar() .add_xaxis(city_top10["user_city"].tolist()) .add_yaxis("评论量", city_top10["count"].tolist()) .reversal_axis() .set_global_opts( title_opts=opts.TitleOpts(title="泡泡玛特评论用户城市 Top10"), xaxis_opts=opts.AxisOpts(name="评论量"), ) ) city_bar.render("output/city_distribution.html")如果你希望生成地图,需要先安装地图扩展包:
pip install echarts-countries-pypkg echarts-china-provinces-pypkg echarts-china-cities-pypkg如果安装后仍然显示空白,可以在代码中通过current_config.ONLINE_HOST切换 CDN 地址,这个问题会在下一节详细说明。
6.5 情感分布饼图
情感分布使用饼图展示,可以直观看到积极、中性、消极评论的占比。
from pyecharts.charts import Pie # 准备数据 sentiment_data = [ ("积极", int(sentiment_count.get("positive", 0))), ("中性", int(sentiment_count.get("neutral", 0))), ("消极", int(sentiment_count.get("negative", 0))), ] pie = ( Pie() .add( series_name="情感占比", data_pair=sentiment_data, radius=["40%", "70%"], label_opts=opts.LabelOpts(formatter="{b}: {d}%"), ) .set_global_opts( title_opts=opts.TitleOpts(title="泡泡玛特评论情感分布"), legend_opts=opts.LegendOpts(orient="vertical", pos_top="15%", pos_left="2%"), ) ) pie.render("output/sentiment_pie.html")radius=["40%", "70%"]表示内半径和外半径,这样生成的是环形饼图。formatter="{b}: {d}%"表示标签显示名称和百分比。
6.6 词云图生成
词云图能直观展示高频关键词。这里使用 wordcloud 库生成 PNG 图片,需要配合 matplotlib 展示。
如果你的系统没有中文字体,词云图会出现大量方块乱码。解决办法是指定一个支持中文的字体文件,比如 Windows 下的C:\Windows\Fonts\simhei.ttf或 macOS 下的PingFang.ttc。
from wordcloud import WordCloud import matplotlib.pyplot as plt # 将高频词转为词典形式 word_freq_dict = dict(word_counter) # 指定中文字体路径,请根据你的系统调整 font_path = "C:/Windows/Fonts/simhei.ttf" wordcloud = WordCloud( font_path=font_path, width=800, height=600, background_color="white", max_words=200, max_font_size=120, colormap="viridis", ).generate_from_frequencies(word_freq_dict) # 使用 matplotlib 展示并保存 plt.figure(figsize=(10, 8)) plt.imshow(wordcloud, interpolation="bilinear") plt.axis("off") plt.title("泡泡玛特评论高频词云") plt.savefig("output/wordcloud.png", dpi=300, bbox_inches="tight") plt.show()generate_from_frequencies()可以直接从词频字典生成词云,不需要传入原始文本,这样能精准控制哪些词显示。
7. 整合项目:一键运行完整流程
7.1 项目文件整合
为了方便演示和后续扩展,建议把以上代码整合为一个主流程脚本,通过命令行参数控制运行哪一步。
下面是一个简单的整合示例:
# 文件路径:main.py import crawler import data_clean import analysis def run_all(): print("========== 步骤1:采集评论数据 ==========") # crawler.crawl_comments("https://example.com/popmart/comments", total_pages=5) print("========== 步骤2:清洗数据 ==========") # data_clean.clean_data() print("========== 步骤3:分析与可视化 ==========") # analysis.run_analysis() print("========== 全部完成 ==========") if __name__ == "__main__": run_all()实际开发中,你可以把crawler.crawl_comments()、data_clean.clean_data()、analysis.run_analysis()分别实现为可调用的函数,然后在main.py中按顺序调用。
7.2 运行方式说明
完成所有脚本后,在项目根目录下执行:
python main.py程序会自动执行爬虫采集、数据清洗、分析统计和可视化生成。最终在 output 目录下得到多个 HTML 图表文件和一张词云 PNG 图片。
如果你使用的是 Jupyter Notebook,也可以把各个步骤拆成单元格逐个执行。这样在调试阶段更方便查看中间结果。
8. 常见问题与排查思路
在实际运行项目时,你可能会遇到各种报错。下面是几个高频问题及排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 爬虫请求返回 403 | 请求头不完整或请求频率过高 | 补全 User-Agent、Referer,降低请求频率 |
| 中文乱码 | 响应编码设置错误或 CSV 编码问题 | 设置 resp.encoding = "utf-8",CSV 保存使用 utf-8-sig |
| 评论内容解析为空 | HTML 选择器不匹配 | 打开浏览器开发者工具,重新定位评论标签 |
| pandas 读取 CSV 报编码错误 | 文件保存编码与读取编码不一致 | 统一使用 encoding="utf-8" 或 encoding="utf-8-sig" |
| jieba 分词结果包含无意义单字 | 未设置停用词表 | 增加停用词过滤,保留长度大于等于 2 的词 |
| pyecharts 地图显示空白 | 地图数据未加载或 CDN 访问失败 | 安装地图扩展包,或切换 ON_LINE_HOST |
| 词云图显示方块乱码 | 缺少中文字体 | 指定系统已有中文字体路径 |
| matplotlib 窗口不显示图表 | 没有调用 plt.show() 或后端配置问题 | 检查是否调用 plt.show(),或保存图片后直接查看 |
这里重点说两个容易卡住的点。
第一个是 pyecharts 地图空白。老版本的 pyecharts 会从公共 CDN 加载地图 JS 文件,如果网络无法访问该地址,地图区域就是空白。解决方法有两种:一是安装地图数据包,二是配置本地静态文件服务。对于学习项目,最简单的方式是使用柱状图代替地图,展示效果同样清晰。
第二个是爬虫请求 403 问题。很多网站会检查请求是否来自真实浏览器,最简单的方法是补全 User-Agent,再加上 Referer、Origin 等请求头。如果仍然 403,可能需要使用 cookies 或增加更真实的请求头组合。切记不要暴力请求,避免影响网站正常服务。
9. 最佳实践与工程建议
9.1 代码组织与命名规范
项目变大后,代码组织方式会直接影响维护效率。建议遵循以下原则:
- 爬虫、清洗、分析代码放不同模块,职责单一。
- 函数命名使用动词开头,比如
fetch_html、parse_comments、clean_text。 - 魔法数字定义为常量,比如总页数、休眠时间范围。
- 文件路径使用
os.path.join或pathlib.Path拼接,避免不同系统分隔符问题。
9.2 数据存储与备份
学习项目保存为 CSV 足够,但如果数据量很大,建议改用 SQLite 或 MySQL。使用数据库有以下好处:
- 支持增量爬取,不用每次都全量采集。
- 支持 SQL 查询,方便复杂统计。
- 数据更安全,不容易因为程序中断而丢失。
在分析前,建议先对原始数据做备份。洗数据是不可逆操作,一旦清洗逻辑写错,原始数据就会丢失。你可以在采集时同时保存原始响应和使用 pandas 处理后的结果两份文件。
9.3 异常处理与日志记录
写爬虫时至少要有 try-except 包裹网络请求代码,否则一个请求超时就会导致整个程序崩溃。更规范的做法是使用logging模块记录日志,而不是到处使用print。
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("app.log", encoding="utf-8"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) logger.info("开始采集第 1 页数据") logger.warning("第 2 页请求失败,重试中")日志不仅能记录正常流程,也方便排查线上问题。这在实际工程中非常重要。
9.4 分析结论的可解释性
数据分析不能只停留在“画图”层面。每张图背后都要能解释业务含义。比如,如果发现某一天评论量突然暴涨,可以尝试关联当天是否有新品发布或促销活动;如果某个产品消极评论占比很高,可以进一步分析消极评论集中在哪些关键词上,从而定位产品问题。
面试中,面试官通常不会只问“你怎么画这个图”,而更关心“这个图说明了什么,你怎么验证你的结论”。多思考分析背后的业务逻辑,会让你的项目更有深度。
10. 总结与下一步学习方向
到这里,一个完整的泡泡玛特热搜评论数据分析可视化项目就结束了。我们做的事情可以总结为五步:用 requests 和 BeautifulSoup 采集评论数据;用 pandas 完成缺失值处理、时间格式化、文本清洗;用 jieba 做中文分词和关键词提取;用情感词典粗判评论情感;用 pyecharts 和 wordcloud 生成可视化图表。整个过程不依赖付费工具,完全使用 Python 开源库完成。
如果你希望在这个基础上继续深入,推荐按下面的方向进阶。
第一,把爬虫部分升级为 Scrapy 框架。Scrapy 自带并发请求、去重、中间件、Pipeline 等机制,适合大规模采集。这也是爬虫岗位面试中常考的内容。
第二,把静态可视化升级为 Web 应用。可以使用 Flask 或 Django 搭建后台,通过 ECharts 实现数据大屏的效果。这样项目会更像一个完整的商业产品。
第三,用更专业的模型替代情感词典。比如 SnowNLP、BERT 中文情感分类模型,分析准确率会明显提升。这类改进点非常适合写在简历项目里,体现你对模型效果的思考。
第四,引入自动化定时采集。把爬虫部署到服务器上,用 cron 或 APScheduler 定时执行,配合数据库做增量更新,就能形成一个持续运行的数据分析系统。
这个项目上手难度适中,但覆盖的知识面很广。只要你把每一行代码都吃透,理解每一步的输入输出,完全有资格把它作为简历上的核心项目。随便打开一个可视化图表,你能讲清楚它的数据来源和处理流程,面试官就会认为你是真的做过,而不是背代码。
如果你决定动手复现这个项目,建议先不要着急跑完整个流程,而是从爬虫开始,每完成一个环节就输出一个中间结果,这样遇到问题时也能快速定位。遇到报错就在评论区告诉我,也可以直接搜索错误信息,通常都能找到解决方案。
希望这篇教程能帮你完成一个拿得出手的数据分析项目,也欢迎收藏备用。