☰
爬虫之后:数据清洗与可视化实战,让数据真正开口说话
2026/10/9 6:21:58 网站建设 项目流程

第35天晚上,我的爬虫终于跑通了。按照计划里的安排,我从一个公开的图书评分站点抓了约1200本书的基础信息:书名、作者、评分、评价人数、出版年份、定价、简介,最后落成了一份4万多字符的CSV文件。今天进入第36天,主题是爬虫与数据分析可视化(下)——把昨天囤下来的数据清洗干净,做一轮探索性分析,再用图表让这些数字开口说话。这篇文章适合已经能写出简单爬虫、但还没把数据“用完”的人参考,我会把清洗、分析与可视化的完整路径、每一段代码背后的取舍、以及实际跑出来的结果一起讲清楚。先说结论:爬虫只是把数据搬进家门,真正有价值的动作,全在今天的后半程。

1. 从“爬到数据”到“读出结论”,这一步常常被教程跳过去

网上绝大多数爬虫教程的终点,是控制台里打印出来的几行字典或者一张CSV截图。说句实在话,我之前也停在这个阶段很久。直到真拿着1200条记录去做分析,才发现“能跑”和“能用”之间的距离,差不多有一整个白天的工作量。

1.1 昨天抓回来的数据长什么样

先看一眼原始数据的样子,我截取了CSV的一部分:

titleauthorratingvote_countpub_datepriceintro
一间只属于自己的房间弗吉尼亚·伍尔夫9.1分2384评价2019-08-01¥42.00女性写作…
克拉拉与太阳石黑一雄8.1分12345评价2021-03-01定价:79.00元太阳即将…
红楼梦曹雪芹9.6分102938评价2007/1/138.00无简介
盗墓笔记南派三叔缺缺2020-0259.0空

乍一看似乎挺整齐,但只要开始处理,问题全出来了。“9.1分”“2384评价”这类带单位的字符串没法做数学计算;出版日期有“2019-08-01”也有“2007/1/1”,格式不统一;价格字段里混着人民币符号和“定价:”前缀;还有评分缺失、简介为空、因为翻页拼接导致的重复行。

这些脏数据不会自己变干净,如果不处理,后面画出来的图就是错的,结论也可能是反的。

1.2 为什么“能跑”的代码不等于“能用”的数据

我见过不少朋友爬完数据,直接df.head()看一眼就开画图,画完发现Y轴出现了一堆莫名其妙的字符,或者某个分组里数值大得离谱,最后只能回头补数据。这个来回折腾的时间,往往比正式清洗还长。

我现在的习惯是:爬虫写完先不急着分析,先给数据做一次“体检”。把每个字段的类型、空值数量、唯一值数量、异常值范围全部过一遍。宁可在这个阶段多花半小时,也不要让错误结论带着你多折腾好几天。因为可视化的本质是把数据里的信号放大,脏数据被放大之后,只会变成更大的噪音。

1.3 今天的交付物:四张图和一句话

为了避免陷入“为了画图而画图”,我在动手前先定了今天的交付物:

  1. 一张评分分布直方图,回答“这批书的评分集中在哪里”;
  2. 一张出版年份与平均评分的折线图,回答“近年的书评分走势如何”;
  3. 一张评价人数与评分的散点图,回答“口碑和讨论度是否互相促进”;
  4. 一张简介关键词词云,回答“这批书都在讲什么主题”。

最终用一句人话总结出来。后面每一步都围绕这四个问题展开,不贪多。这个“先定问题再定图表”的习惯,是我觉得今天最值得分享的一点。

2. 清洗才是数据可视化的真正起跑线

如果说爬虫是做运输,清洗就是做质检和分拣。货没分好,后面所有的加工都是白费功夫。我把今天清洗的过程拆成几个固定动作,每一个动作我都会解释为什么必须做。

2.1 动手之前,先留下原始数据副本

清洗的第一条铁律:永远不要直接在原始数据上改。我先做一次备份:

import pandas as pd df = pd.read_csv('books_raw.csv', encoding='utf-8-sig') df.to_csv('books_raw_backup.csv', index=False, encoding='utf-8-sig') raw = df.copy()

理由很简单:清洗规则本身可能会写错,或者后面发现某个字段还需要原来的格式,没有备份就只能重新爬一遍。而重新爬的代价不只是时间,还可能因为站点数据已经更新,导致之后完全对不上账。

提示:备份这一步看着多余,但在数据流水线里,它相当于一个“后悔药”。我遇到过一次清洗正则写错,差点把200多行价格全改成空值,就是因为有备份才没重爬。

2.2 六个高频清洗动作,逐个拆解

针对这份数据,我依次做了以下操作。

去重。翻页拼接时同一本书可能被抓两遍,我用“书名+作者”作为组合主键去重:

raw.drop_duplicates(subset=['title', 'author'], inplace=True)

丢弃关键字段为空的行。评分和评价人数是分析的基础,缺失了就没办法参与计算:

raw.dropna(subset=['rating', 'vote_count'], inplace=True)

这里只丢弃关键的,简介为空的书反而不能丢——后面做词云时可以用NaN填充一个占位符。

清洗评分。原始值是“9.1分”这种字符串,需要去掉单位再转浮点:

raw['rating'] = raw['rating'].astype(str).str.replace('分', '', regex=False).astype(float)

清洗评价人数。字段里混着“2384评价”“102938人”等写法,我用正则把非数字部分去掉:

raw['vote_count'] = raw['vote_count'].astype(str).str.replace(r'[评价人个]', '', regex=True).astype(int)

统一出版日期。直接用pd.to_datetime统一格式,顺便提取年份字段,后面做趋势图时用:

raw['pub_date'] = pd.to_datetime(raw['pub_date'], errors='coerce') raw['pub_year'] = raw['pub_date'].dt.year

提取价格数字。原来有“¥42.00”、“定价:79.00元”、“38.00”等多种写法,只要数字:

raw['price'] = raw['price'].astype(str).str.extract(r'(\d+\.?\d*)')[0].astype(float)

最后用范围过滤把明显异常的数据剔除掉,比如评分大于10、出版年份早于1900或者晚于当前年份的记录:

raw = raw[raw['rating'].between(0, 10) & raw['pub_year'].between(1900, 2025)]

2.3 清洗前后对比:从“字符碎片”到“整洁的DataFrame”

清洗完成后,我再出一次体检报告,和清洗前对比:

指标清洗前清洗后
总行数12761188
重复行310
评分缺失230
评价人数缺失170
评分类型字符串float
出版日期格式混用3种统一为datetime
价格类型混合文本float

看到这个对比,我才有底气开始分析。清洗过程的每一步,本质上都是在和数据“核对口径”:评分的算法是什么、出版年份怎么定义、价格包不包括折扣。口径不一致的数据,画出来的图再漂亮也只是个空壳。

2.4 清洗要会“留痕”,别把过程都吞掉

我会在脚本里保存一份cleaning_log,记录每个动作删了多少行、改了多少字段。一方面是便于复查,另一方面是为了让流程可复现——昨天清洗的结果和明天的结果中间隔了网站数据更新,留痕才能追溯差异是从哪一步引入的。

cleaning_log = { 'raw_rows': len(df), 'after_dedupe': len(raw), 'after_dropna': len(raw), 'final_rows': len(raw), } print(cleaning_log)

这一步很多人会忽略,但它对工程化的帮助极大。尤其是当你把爬虫改成定时任务后,留痕就是排查数据异常的“监控日志”。

3. 画图之前,先用EDA摸底,让数据自己报一遍家底

我见过太多人拿到干净数据就直接上图表。我的建议是别急,先用探索性数据分析(EDA)问几个基础问题:数据总量多少?分布偏不偏?哪些字段之间有关系?这就像去一家新餐馆,先看菜单找感觉,再决定点什么菜。

3.1 describe() 和 info(),两行代码读出基本面

df_clean = raw.copy() print(df_clean.info()) print(df_clean[['rating', 'vote_count', 'price', 'pub_year']].describe())

输出的关键信息大概是这样的:

  • 评分:均值约7.5,中位数约7.7,25%分位数在6.8左右;
  • 评价人数:中位数只有200多人,但均值被少数几百本热门书拉到了好几千,右偏严重;
  • 价格:大部分集中在30到80元,个别签名本或套装书价格异常高;
  • 出版年份:从1980年代到2024年都有分布,最近十年占了多数。

读完这组数字我做的第一个判断是:分析时用中位数比均值更稳,因为评价人数这种长尾字段容易被极端值带偏。这个小细节后面会用到。

3.2 分组聚合,从单一记录看到时间截面

我想知道“这几年出品的书评分到底在涨还是跌”,所以按出版年份分组聚合:

year_stats = df_clean.groupby('pub_year').agg( avg_rating=('rating', 'mean'), book_count=('title', 'count') ).reset_index() print(year_stats.head(10))

结果里能明显看到两个现象:一是样本量小的早期年份,评分波动特别大,比如1998年只有3本书,平均分却被其中一本9.4分拉得很高;二是2015年之后每年样本量都在50本以上,平均评分稳定在7.2到7.8之间,并且略有上升。

这个观察提示我,画折线图时必须把样本量一起展示,否则单看平均分很容易被极小样本误导。

3.3 相关系数:用数字确认直觉

我还想验证一个常见直觉:讨论度高的书,评分是不是也更高。直接看相关系数:

corr = df_clean[['rating', 'vote_count']].corr() print(corr)

计算出来的皮尔逊相关系数大约在0.34左右。这个数值说明两者确实存在正相关,但强度不算高。很多口碑一般的书也可能因为营销或争议获得大量评价,所以不能画完散点图就下“评价越多书越好”的结论。

另外我还做了一组简单的价格区间交叉统计,结论很有意思:把价格分成“50元以下”“50到100元”“100元以上”三档之后,三组的平均评分几乎没有差别。这个发现提醒我,后面千万不要把价格画进“评分影响因素”的叙事里。

3.4 从EDA里挖出来的三条线索

经过前面几步,我手里攒了三条可以展开的线索:

  1. 评分整体右偏,8到9分之间的书最多,说明该站点读者打分偏宽容;
  2. 近年评分稳中有升,但高分书里重版经典的比例明显高于新书;
  3. 评价人数和评分只有弱正相关,话题热度不等于口碑。

后面所有的图表,都围绕这三条线索展开。先有线索再作图,图才有明确的“任务”,而不是为了凑页面。

4. 选对图表,才叫让数据“说话”

图表不是装饰品,它是在替数据充当表情和语气。同一个指标,折线图强调变化趋势,柱状图强调高低对比,饼图强调构成比例,选错图等于用错误的语气说话。

4.1 一个简单的选图决策表

我根据要回答的问题,做了一张选图对照表:

要回答的问题推荐图表注意点
评分集中在哪个区间直方图 / 箱线图组距不要太大,否则看不出分布细节
评分随时间怎么变化折线图样本量少的年份要标记出来
评价人数和评分是否相关散点图点重叠严重时加透明度和抖动
哪些作者的高分书最多水平条形图只取前N名,避免标签拥挤
简介里出现最多的词词云先去掉停用词,否则全是“本书”“作者”

这张表帮我在画图前就把“语法”定好,代码只是填词。

4.2 Matplotlib把四张图快速画出来

用Matplotlib跑图,最关键的是先处理中文字体。中文环境默认字体经常是方框,必须先指定字体:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False

在Windows上这两个设置就够了,Linux服务器上如果还乱码,就需要安装文泉驿或Noto CJK字体。

第一张评分分布直方图:

fig, ax = plt.subplots(figsize=(10, 6)) ax.hist(df_clean['rating'], bins=20, color='#4C72B0', edgecolor='white') ax.set_title('全部书籍的评分分布') ax.set_xlabel('评分') ax.set_ylabel('书籍数量') plt.tight_layout() plt.savefig('rating_dist.png', dpi=150) plt.close()

这里我特意加了plt.close()。在循环里连续画多张图时,不关闭当前画布会导致内存持续上涨,脚本一长就会卡死或导出错图。

第二张历年平均评分折线图,我把样本量少于10本的年份单独标成灰色点,避免读者误读那些波动:

fig, ax = plt.subplots(figsize=(12, 6)) valid = year_stats[year_stats['book_count'] >= 10] ax.plot(valid['pub_year'], valid['avg_rating'], marker='o', color='#C44E52') ax.set_title('历年平均评分走势(仅展示样本量>=10的年份)') ax.set_xlabel('出版年份') ax.set_ylabel('平均评分') plt.tight_layout() plt.savefig('year_rating.png', dpi=150) plt.close()

第三张评价人数和评分的散点图,我加入透明度来解决点重叠问题:

fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(df_clean['vote_count'], df_clean['rating'], alpha=0.3, color='#55A868') ax.set_xscale('log') ax.set_title('评价人数与评分散点图(X轴取对数)') ax.set_xlabel('评价人数(对数坐标)') ax.set_ylabel('评分') plt.tight_layout() plt.savefig('vote_vs_rating.png', dpi=150) plt.close()

注意我给评价人数加了对数坐标。因为这个字段从几十到十几万,跨度太大,直接线性坐标会让绝大多数点挤在左侧,组间关系根本看不出来。

4.3 用Pyecharts升级成交互式图表

静态图适合自己检查数据,但要给别人看、让读者自己“问数据”,交互图好用得多。Pyecharts基于ECharts,渲染成HTML后鼠标悬停就能看具体数值,还能缩放和切换图例,特别适合做汇报。

安装方式很简单:

pip install pyecharts

把刚才的折线图改成交互式版本:

from pyecharts.charts import Line from pyecharts import options as opts line = Line() line.add_xaxis([str(y) for y in valid['pub_year']]) line.add_yaxis( "平均评分", [round(v, 2) for v in valid['avg_rating']], is_symbol_show=True, ) line.set_global_opts( title_opts=opts.TitleOpts(title="历年平均评分走势"), tooltip_opts=opts.TooltipOpts(trigger="axis"), ) line.render("year_rating.html")

用Pyecharts有两个容易踩的坑。第一个是版本问题,0.5.x和1.x的API差异很大,网上很多老教程直接会报错,装完记得先看版本;第二个是图表默认在本地生成HTML文件,如果部署到服务器上,还需要把JS资源一并处理好,否则别人打开页面图表空白。我现在更偏向用官方CDN加载资源,省去一堆静态文件同步的麻烦。

4.4 从单图到“可视化大屏”的组装思路

单张图是“一句话”,大屏是“一篇报告”。网上热门的可视化大屏项目,本质就是在同一页面里组合多张ECharts图表,并配上筛选器和数据卡片。

我用一个很轻的方案就能拼出一个简易大屏:用Flask提供一个页面,页面上用多个<div>分别渲染不同的ECharts实例,再写几个JS回调做联动。Pyecharts本身支持把多个图表放到一个Page对象里快速生成:

from pyecharts.charts import Bar, Line from pyecharts import options as opts page = Page(layout=Page.SimplePageLayout) page.add( bar, line, scatter, ) page.render("dashboard.html")

不过我更建议大家先不要追求“大屏效果”。把单张图画到能回答一个业务问题,比堆十个图表更有价值。我第一次做大屏时堆了十几张图,结果没有一张能讲清楚数据到底在表达什么。

5. 一条命令产出报告:把爬虫、分析、可视化流水线化

单次把所有步骤写在Jupyter里没问题,但如果你准备长期跟踪这份数据,就必须把流程固化下来。我给自己的要求是:以后每天更新数据,只跑一条命令,得到一份完整报告。

5.1 拆成四个函数,各司其职

我按职责把昨天和今天的代码整理成四个模块:

# pipeline.py import pandas as pd def crawl_to_raw(cache=True): if cache and os.path.exists('books_raw.csv'): return pd.read_csv('books_raw.csv', encoding='utf-8-sig') # 爬虫逻辑... 在这里调用昨天的抓取函数 df = fetch_books() df.to_csv('books_raw.csv', index=False, encoding='utf-8-sig') return df def clean_data(df): # 所有清洗动作 ... return df_clean def analyze_data(df_clean): # 分组、相关性等统计 ... return stats def visualize_data(df_clean, stats): # 生成所有图表 ... return output_paths if __name__ == '__main__': raw = crawl_to_raw() clean = clean_data(raw) stats = analyze_data(clean) visualize_data(clean, stats)

模块化最大的好处是:图表参数想调整时,只动visualize_data;站点规则变了,只动crawl_to_raw;清洗规则出了问题,只动clean_data。互相不干扰,出问题也容易定位。

5.2 为什么我坚持要加缓存层

昨天完整爬一次大概需要40多分钟,因为我每抓几页就休息几秒,避免给站点造成压力。如果每次调整图表都要重新爬一遍,一天就得浪费大半天。

所以我加了缓存:第一次爬完把结果存成CSV,后面再跑就优先读本地文件,只有显式执行“全量刷新”时才重新抓取。时间就这样从40分钟降到了几秒。

提示:缓存虽然省时间,但也要记得给文件标上抓取日期。我习惯在文件名里带时间后缀,比如books_raw_20250101.csv,避免隔了一个月还分不清哪份是新的。

5.3 定时更新的思路:尊重规则,控制频率

如果想做成周更或月更的自动化报告,可以用系统计划任务或APScheduler。核心原则是低频、礼貌:别人网站的数据是公开资源,但高频率抓取会给对方服务器带来压力。

我之前踩过一个坑:为了测试定时任务,把间隔设成了10分钟一次,结果第二天被目标站点限流,整批数据没抓全,还连累了后续分析。后来把频率降到了每天一次,数据一直很稳定。

5.4 实测一次全流程的耗时分布

我在本机跑了一次完整流水线,记录下来的耗时大致如下:

阶段耗时
爬虫(全量抓取)约42分钟
爬虫(读缓存)0.8秒
清洗3.2秒
分析聚合0.6秒
生成四张图+一个HTML5.4秒

看到这个时间分布,你应该明白为什么我反复强调缓存。真正的工作重心在清洗和图表设计,而不在重复抓取上。

6. 复盘与踩坑:那些代码之外,真正决定成败的细节

今天一天下来,程序本身没出什么大问题,但我在中途还是踩了几个有意思的坑。它们都不算难,可每一个都能让人浪费一两个小时。

6.1 爬虫阶段埋下的雷,全在可视化阶段炸了

第一个雷是编码。抓取时页面声明的是UTF-8,但个别老页面用了GBK,混在同一个列表里,清洗时没发现,画词云时才看到一堆乱码。最后我只能重新写解码逻辑,把异常字符按errors='ignore'处理,再补抓一次。

第二个雷是重复。翻页时页面底部有推荐位,同一本书会出现在相邻两页,去重时如果只看书名不看作者,两个同名不同作者的书就会被误删。所以我的去重键必须用['title', 'author']组合。

第三个雷是空简介。做词云之前我偷懒没填充空值,结果词云里全是“nan”这个词,第一版图丑得没法看。处理方式是先用空字符串填充,再统一过滤停用词。

6.2 关于可视化的细节:字体、Y轴起点、配色

字体问题前面讲过,这里补充一个更隐蔽的坑:默认折线图如果Y轴从6.5开始画,视觉上会把0.3分的波动放大成“剧烈变化”。这种做法在汇报里其实很常见,但如果不加说明,就有点误导的嫌疑。

我的处理办法是:让坐标轴包含合理的下界,同时在标题里写明Y轴的截取范围。用更专业的说法,就是给读者足够的信息去理解图表的比例尺,而不是让读者被你“精心挑选”的视角蒙住。

配色方面,我选择的事色盲友好型配色:用蓝、红、绿三种颜色区分不同系列,避免红绿同时作为唯一区分维度。这个细节在公开分享时尤其重要,因为读者里可能有色觉障碍者。

6.3 让数据说话,不等于让数据替你撒谎

这是今天最想强调的一点。我这份数据只抓了该站点一个分类下的书,样本本身就有偏。比如点进去评价人数的分布,极端右偏意味着绝大多数书只有几十条评价,真正有统计意义的是那几百本“热门书”。

所以我在写分析结论时,特别注意措辞:

  • 不说“全网高分书都是这个规律”,而是说“在这批样本中,评价人数与评分的相关系数约为0.34”;
  • 不把价格和评分的关系说成因果,因为这只是简单的分组对比;
  • 把样本范围、抓取时间和清洗规则一并写进报告备注里。

数据可视化最大的价值不是“证明你对了”,而是帮助你看清本来看不清的结构。一旦开始为了说服别人而挑选视角,图就不再是数据的嘴,而是你自己的嘴。

6.4 我今天最痛的一个坑和下一步计划

今天最痛的坑发生在最后一步:我在Pyecharts里把一个字段名写错了,导致渲染出的折线图只有第一个点。排查了十分钟才发现,是分组后列名被reset_index重置了,而我还用原来的列名去引用。这个小问题教会我一件事:可视化程序出问题时,先检查数据进图之前的样子,别急着怀疑图表库。

如果这篇文章让你走到这里,我的建议是:先克制住“再多画一张图”的冲动,挑一张今天最想讲清楚的问题图,把它打磨到能回应一个具体疑问。等这条链路跑顺了,再往大屏、自动化报告、数据库存储的方向扩。这套“爬虫+数据分析+可视化”的组合拳打完之后,再回头处理真实业务里的数据,你会更容易听见数据想说的那句话。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询