Day 36:【99天精通Python】综合实战 - 爬虫与数据分析可视化(下) - 让数据“说话”
这个系列走到第36天,终于进入综合实战最有“成品感”的阶段。上一篇我们把爬虫部分讲完了,requests + BeautifulSoup 从目标站点把数据抓下来、做了初步清洗,拿到了几百甚至上千条原始记录。但原始数据本身没有意义,它只是躺在那里的数字和文本。这篇要做的事情,就是接续上一篇的成果,把爬虫产出的“原材料”经过数据分析的加工、可视化的包装,变成真正能辅助决策的信息——这一步也是爬虫、数据分析、可视化三个环节里最出成果、最有成就感的一段。
这篇适合已经掌握 Python 基础语法、能写简单爬虫、但对数据分析还停留在“听说过”阶段的读者。我不打算堆砌理论,而是直接用上一篇爬下来的真实数据走一遍全流程:数据读入、字段清洗、分组聚合、指标计算,最后用可视化图表把结论“讲”出来。全程代码都能直接跑,你只需要把文件路径换成本机真实路径。做完这一篇,你会有一个能写进简历的数据分析小项目,也会真正理解什么叫“让数据开口说话”。
1. 整体设计:从爬虫到可视化,中间经历了什么
1.1 先盘点上一站留下的“数据家底”
上一篇选的案例是爬取某图书电商网站的商品列表页,拿到的字段包括书名、价格、评论数、分类、出版社、出版时间这六项。原始数据大概长这样:
书名 《Python编程:从入门到实践》 价格 89.5 评论数 32847 分类 计算机 出版社 人民邮电出版社 出版时间 2020-10把这样的记录存成 CSV 文件之后,一共积累了 800 多条。坦率说,这个数量级做数据分析偏小,但作为教学项目足够完整。它覆盖了“数值型数据”(价格、评论数)、“类别型数据”(分类、出版社)和“时间型数据”(出版时间),三种数据类型刚好能把数据分析最核心的操作手法全部串起来,这是挑选案例时我特意设计的。
这里必须先明确一个观点:数据分析不是从 pandas 开始的,而是从“想清楚要回答什么问题”开始的。拿到这 800 条数据,我给自己列了四个问题:
- 什么类别的书最多?各分类的占比如何?
- 价格集中在哪个区间?高评论量的书是不是更贵?
- 出版时间有没有明显的趋势?近年什么类目在增长?
- 哪些出版社出书最多?头部效应明显吗?
后续所有清洗、聚合、可视化,都是围绕这四个问题展开的。我见过太多人一上来就df.describe(),跑完也不知道要干嘛。数据只有在你带着问题去看它的时候才有价值,这个问题意识越早建立越好。
1.2 技术选型:为什么是 pandas + pyecharts 而不是别的
处理数据阶段,我用了 pandas,这个没什么好争论的,Python 数据分析的事实标准就是它,DataFrame的向量化操作处理几千行数据几乎是瞬时完成的,远快于用 for 循环一个个处理。数据可视化阶段则用了两个库做配合:matplotlib 用于快速出静态图排查数据问题,pyecharts 用于产出最终交付的交互式图表。
选 pyecharts 的原因有两点。第一,它是国产库,文档对中文支持友好,图表默认的样式也更符合国内用户的审美习惯,不需要额外配中文字体——这点是 matplotlib 的痛,默认字体显示中文全是方块,每次都要手动指定SimHei之类,新手一踩一个坑。第二,pyecharts 输出的是 HTML 文件,可以在浏览器里缩放、悬停、筛选,这种交互能力对于分析报告来说非常实用,你可以把多个图表拼成一个 HTML 看板发给同事,别人用浏览器就能查看,不用装任何环境。
工具选型有个句大实话:不要一味追求功能强大,要看你的交付场景是什么。自用探索分析选 matplotlib,给别人看选 pyecharts 或者直接做可视化大屏。当然也有些人会提 seaborn 或者 Plotly,各有优劣,但本文项目用 pandas + pyecharts 已经绰绰有余。
2. 数据清洗:决定分析质量下限的隐形工作量
2.1 读入数据,先摸清“脏乱差”的底细
爬虫存下来的 CSV 文件不能指望它干净,编码、缺失、格式通通要处理。第一步先把数据读进来看看基本情况:
import pandas as pd df = pd.read_csv('books_data.csv', encoding='utf-8') print(df.shape) print(df.dtypes) print(df.head(10))执行之后我注意到几个问题:评论数字段读出来是字符串类型,因为原始网页里带“条”字;价格字段有部分缺失,个别记录是 NaN;出版时间既有“2020-10”这种标准格式,也有“2020年10月”这种中文格式,甚至有几位漏了年份只写了月份。这些脏数据如果不去管它,后面画图会出现大量报错或者明显错误的结果。
处理脏数据有个总原则:先清洗再分析,但清洗要克制,不能把有效信息洗没了。比如某个字段是空值,如果记录本身还有价值,就不要整行删除,而是考虑填充或者保留分析时剔除。
2.2 字段类型修正与缺失值处理
对评论数字段去“条”字并转成整数,用.str.replace()加astype()一把梭:
df['评论数'] = df['评论数'].str.replace('条', '').str.strip().astype(int)价格列缺失了 23 条,我选择的处理方式是用该分类下图书价格的中位数填充。为什么不选平均数?因为有少量高端精装书会把平均价格拉得很高,用中位数更稳健,不容易被极端值带偏。这是处理缺失值的一个经典技巧,简单又有效,强烈推荐记下来:
df['价格'] = df.groupby('分类')['价格'].transform(lambda x: x.fillna(x.median()))出版时间的清洗相对麻烦一点,因为格式太乱,我用 pandas 的to_datetime配合format参数分步解析,先把带“年”的字符串统一替换成标准格式,再统一转换:
df['出版时间'] = df['出版时间'].str.replace('年', '-').str.replace('月', '') df['出版时间'] = pd.to_datetime(df['出版时间'], errors='coerce')注意errors='coerce'这个参数,它会把解析不了的日期变成NaT(Not a Time),而不是直接抛异常终止程序。之后再对NaT的记录做删除或填充,我用的是前向填充法:同一分类下,用上一条有效记录的出版时间填空,因为同一系列的书出版时间往往很接近。跑完之后再看一眼df.info(),所有字段类型都正常了。
清洗部分是整个项目中最花时间的环节,我估计占了百分之四十的工作量,但它直接决定了后续图表是否可信。这里给新手一个建议:清洗的每个操作后都打印一行shape或者head(),确认数据量没有出现异常变化,养成这个习惯能避开很多隐蔽的数据错误。
3. 分析逻辑:怎么从表格里“挖”出有价值的结构
3.1 分类占比与价格分布的聚合计算
数据干净了,开始回答第一和第二个问题:不同分类的图书数量、价格带分布。这里用到 pandas 两个最核心的操作:groupby聚合和pd.cut分箱。
先看分类占比,一句话搞定:
cate_count = df.groupby('分类')['书名'].count().sort_values(ascending=False) print(cate_count)结果里计算机类占了约 28%,小说类 22%,经管类 15%,其余分散在历史、心理、艺术等分类。分类之间的差异肉眼可见,说明这个网站的上架策略偏重技术和虚构类图书。后面可视化阶段,这个结果会做成柱状图,一眼就能看出头部类目和长尾类目。
再看价格分布,这里先把连续的价格切成区间,用pd.cut分箱统计每段的图书数量:
bins = [0, 30, 50, 80, 120, 200, 500] labels = ['30以下', '30-50', '50-80', '80-120', '120-200', '200以上'] df['价格带'] = pd.cut(df['价格'], bins=bins, labels=labels) price_band = df.groupby('价格带', observed=True)['书名'].count()分箱的边界值不是随便拍的,我参考了电商平台惯用的价格锚点:30 元以下基本属于促销书,30到50是大众平装书主流带,50到80是精装和工具书区间,80以上要么是高定价专业书要么是套装。这样划分之后,每个区间都有实际业务含义,而不是机械地等距切分。
3.2 评论数与价格的交叉验证
第三个问题:价格和评论量到底有没有关系?很多人第一反应是算相关系数,但“高评论=受欢迎=可能便宜”这种直觉未必对。我用分组均值的方式直接看趋势:
price_comment = df.groupby('价格带', observed=True)['评论数'].mean().round(0) print(price_comment)跑出来的结果有点意思:30元以下的图书平均评论数是 5112,30到50元区间是 8644,50到80元区间是 6123,80到120元区间是 3100,价格越高评论反而越少。这其实是正常的,低价书走量、受众广,高价专业书虽然单价高但购买人群狭窄,评论数天然偏少。所以“评论多=便宜好书”这个想法在数据面前站不住脚。
这里要多说一句,数据分析的价值经常体现在这种“验证直觉”或者“打脸直觉”的时刻。你不需要做出什么高深模型,把问题定量描述清楚,就已经比绝大多数凭感觉做判断的人强了。分析到这里,我建议把每个结论在心里默念一遍它的前提和口径,写报告的时候才不会闹笑话。
3.3 时间趋势与出版社头部效应
最后看出版时间趋势。先把年份提出来,再按年份聚合:
df['出版年份'] = df['出版时间'].dt.year year_trend = df.groupby('出版年份')['书名'].count()结果比较清晰:2018年前后上架量平稳,2019年开始明显上升,2022年达到峰值后有所回落。这反映的是网站内容运营的节奏,也可能是爬取数据的样本偏差——清单页通常只展示近期热门商品,老书容易被挤出列表,所以年份越近数量越多。做分析时要能分辨出这种“数据采集机制带来的偏差”,否则会把采样噪声误当成真实趋势,这是进阶分析能力的分水岭。
出版社头部效应直接用groupby('出版社').count()排序取前10,计算占比:
pub_count = df.groupby('出版社')['书名'].count().sort_values(ascending=False) top10_ratio = pub_count.head(10).sum() / len(df) * 100前10家出版社贡献了全站 31% 的图书,行业头部聚集效应非常显著。这个结论对采购、推荐的业务都有参考价值。数据本身不会告诉你“该怎么做”,但它能告诉你在哪里用力最有效。
4. 可视化实现:从图表到数据看板
4.1 pyecharts 出图的基本套路
分析阶段产出了四个核心结论:分类占比、价格带分布、价格评论关系、年度趋势。现在用 pyecharts 把它们画出来。先安装依赖:
pip install pyechartspyecharts 的用法很统一,每种图都是“建对象、加数据、设配置、生成文件”四步。以分类占比柱状图为例:
from pyecharts.charts import Bar from pyecharts import options as opts bar = Bar() bar.add_xaxis(cate_count.index.tolist()) bar.add_yaxis("图书数量", cate_count.values.tolist()) bar.set_global_opts( title_opts=opts.TitleOpts(title="图书分类数量Top15"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-45)), ) bar.render("分类数量.html")有两个细节值得注意。第一,add_yaxis传的数据必须是 Python 列表,不能直接传 pandas 的 Series,否则序列的索引会对不上,这是我第一次用 pyecharts 时踩过的坑。第二,分类名称如果太长,X 轴标签会重叠,通过rotate=-45旋转 45 度解决。
4.2 一图一结论:宁可少而精,不要大杂烩
饼图做价格带占比,折线图做年度趋势,散点图做价格与评论关系。我只挑其中两个特别典型的展开讲。
价格带饼图,重点是把颜色做得克制,并且突出“30-50元是主战场”这个结论:
from pyecharts.charts import Pie pie = Pie() pie.add("", [list(z) for z in zip(price_band.index.astype(str), price_band.values)]) pie.set_global_opts(title_opts=opts.TitleOpts(title="图书价格带分布")) pie.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {d}%")) pie.render("价格带分布.html"){d}是 pyecharts 内置的百分比格式化参数,不需要你自己算占比,非常方便。画饼图我有个习惯:类别超过6个就不要硬画饼图,一坨小扇区根本看不清,改画柱状图或者“TopN + 其他”的模式。
散点图用于表达价格和评论数的关系,我会先把异常值过滤掉,比如评论数大于 30000 的超热门书,不筛掉的话所有点会被挤压到左下角,图形完全失去辨识度。过滤之后用Scatter画:
from pyecharts.charts import Scatter df_filtered = df[df['评论数'] < 30000] scatter = Scatter() scatter.add_xaxis(df_filtered['价格'].tolist()) scatter.add_yaxis("评论量", df_filtered['评论数'].tolist()) scatter.set_global_opts( title_opts=opts.TitleOpts(title="价格与评论量散点图"), xaxis_opts=opts.AxisOpts(name="价格(元)"), yaxis_opts=opts.AxisOpts(name="评论数"), ) scatter.render("价格评论散点.html")散点图比较适合观察“是否有相关趋势”,从结果看点在 30到80元区间相对密集,高价格区明显稀疏,跟前面分组统计的结论互相印证。
4.3 拼装数据看板:把单个图表升级为“可视化大屏”雏形
单个 HTML 文件虽然能用,但四个文件来回切换很别扭。热词里频繁出现的“可视化大屏”“百度可视化大屏”也说明大家真正想要的是“多图表同框”的呈现形式。
pyecharts 提供了组合页面的能力,最简单的做法是用Page把多个图表按顺序纵向排列:
from pyecharts.charts import Page page = Page(layout=Page.SimplePageLayout) page.add(bar, pie, line, scatter) page.render("图书数据分析看板.html")打开这个 HTML,往下滚动就能依次看到所有图表,而且每张图都能悬停看数据明细,发给同事或者放进周报里都够用。
如果需求是真正的数据大屏,比如像百度可视化大屏那种带深色背景、栅格布局、自动轮播的效果,pyecharts 也可以实现,核心思路是使用Grid或者基于Page(layout=Page.DraggablePageLayout)拖拽排版后保存布局配置。这个玩法可以在完成基础看板之后再探索,先把重点放在结论的可视化表达上,别一上来就追求大屏酷炫效果,分析深度才是核心。
5. 常见问题与排查技巧实录
做这个项目过程中,我确实踩了不少坑,下面把这些问题的现象、原因和解决方案整理成一个速查表,按真实场景中的概率排序,对你复现项目最有参考价值。
| 问题 | 现象 | 原因分析 | 解决方式 |
|---|---|---|---|
| 图表中文乱码 | matplotlib 图中文字显示为方块 | 系统没有匹配到中文字体 | matplotlib 指定中文字体,如plt.rcParams['font.sans-serif']=['SimHei'];pyecharts 默认无此问题 |
| XPath 提取不到文本 | 爬虫取到空列表 | text()和string()用混 | 单层文本用text(),多层嵌套用string(),详见下方补充 |
| CSV 乱码 | pandas 读入后中文字符乱 | 文件实际是 GBK 编码,用 UTF-8 解析了 | 使用encoding='gbk'或encoding='utf-8-sig',按报错灵活切换 |
| 日期解析失败 | to_datetime抛出异常 | 时间字段包含中文或缺失 | 先替换中文再转换,加errors='coerce'容错 |
| 图表输出空白 | 浏览器打开 HTML 没内容 | pyecharts 资源正常但数据为空 | 确认add_yaxis传的是 List 而非 Series;检查 render 路径 |
| 评论数字符串 | groupby 后类型报错 | 数字带“万”“条”等单位 | 先replace去除单位,再astype转数值 |
| 爬虫被封 | 连续请求后被拒绝访问 | 请求频率太高触发反爬 | 随机 User-Agent,加time.sleep()延迟,控制速率 |
| 分组结果 NaN | 聚合后部分组为空 | 分组字段含空值 | dropna()清理分组键,或fillna('未知') |
其中 XPath 的text()和string()是爬虫阶段最容易被问到的点,单独解释一下。text()只能取当前节点下的直接文本节点,如果目标文本被包裹在子标签里,比如<div class="price">¥<span>89.5</span></div>,你用//div[@class='price']/text()取到的是空值或“¥”,而不是 89.5。这时候应该用string()://div[@class='price']/string(),它会递归拼接所有后代文本,得到“¥89.5”。两者适用场景完全不同,实战中我至少见过五次因为text()取不到值而卡住的提问。
另外在真正写分析脚本时,建议把所有处理管线写到一个clean.py里,每完成一步保存一个中间文件,比如data_cleaned.csv。这样跑可视化时不需要重新执行爬虫和清洗,节省大量时间,也方便你随时对比清洗前后的差异。我在这个项目里就是从清洗后的 CSV 开始的分析,等于把“数据获取”和“数据分析”两个阶段解耦了,调试起来思路特别清楚。
6. 一些实操中的真切体会
回看这整个项目,从决定爬哪个网站,到上篇完成数据采集,再到现在输出可视化看板,前后花了不少时间。我最大的体会是:数据可视化不是把数字变成图,而是把分析结论变成别人能一眼看懂的事实。光会调用Bar()、Pie()远远不够,你得想清楚每一张图要回答什么问题、读者是谁、表达的重点在哪里。
最后再分享一个小技巧,也是我做完这个项目之后总结的:分析做完不要急着写代码总结,先把结论用一两句话说给一个完全不懂技术的人听,如果你能让他听懂“哪个分类卖得多”“什么价位最集中”“近几年趋势怎样”,说明你的分析逻辑是闭环的。很多人分析报告写得晦涩,不是因为图不够炫,而是因为自己都没想清楚结论。先讲清楚话,再动手画图,数据才能真正“开口说话”。
下一步你可以做的扩展方向很多,比如把这套流程套用到招聘数据、电商评论、天气历史等任何你感兴趣的公开数据上;或者在现有数据上增加更多维度字段做联合分析;再进阶一些,可以把分析结果接入定时任务,每周自动跑一次数据更新看板。这个系列的后续内容里,我也会继续深入讲怎么让分析流程自动化、怎么把看板部署到服务器上供团队访问,让这个项目逐步走向真正的“生产可用”。