数据可视化项目复盘:从爬虫采集到Pandas清洗与ECharts展示
第三次作业交上去一周了成绩也出来了趁着思路还热乎把整个从选题到踩坑的过程完整捋一遍。这里说的“第三次作业”是某高校“数据采集与可视化”实训课的大作业要求自选主题完成数据采集、清洗、分析和可视化展示最后提交一份报告加演示Demo。同类课程在很多学校都有形式大差不差这篇复盘既是给自己留个档也希望对正在做类似作业的朋友有点参考价值。这门课的第一次作业是基础爬虫第二次是数据清洗第三次等于把前面所有东西串起来做个完整的小项目。作业要求不算苛刻数据源必须是真实可访问的不能造假采集量不少于2000条至少做三种图表其中必须包含一个交互式图表最后写一份说明文档。听起来不复杂但真正上手后坑是一个接一个。这篇就按我的完整过程来写从设计思路到具体实现再到翻车现场和补救方案全部摊开讲。1. 内容整体设计与思路拆解1.1 选题的纠结与取舍第三次作业最让人头疼的不是技术而是选题。班里四十多个人一眼望去十个里有七个在做电影评分爬取剩下三个做房价数据。这些题目确实经典但正因为太经典数据源反爬严重、字段雷同、展示方案模板化很难做出差异。我当时给自己定了三条选题标准第一数据源要稳定且结构足够规整。某招聘网站、某点评平台这种反爬等级高的直接排除作业周期只有两周耗不起封IP的折腾。第二要有真实的分析价值而不只是“把数据抓下来画个图”。纯粹为了交差而做的项目写报告时自己都觉得空洞。第三整个链路要能覆盖课程知识点请求、解析、清洗、存储、可视化一个都不能少。最终我选了“某开放图书社区的在售书目数据”。这个站点的数据结构非常规整分类清晰页面有分页有搜索接口而且对爬虫不算敏感实测下来基本没有验证码干扰。加上图书数据天然自带多个维度价格、出版社、出版年份、评分、评论数、分类每个维度都能做分析展示。这个选题既避开了热门赛道又保证了技术链路的完整性。1.2 技术方案选型为什么是“Python Requests Pandas ECharts”技术选型上我没有太多犹豫直接用了一套最成熟、资料最多的组合Requests做数据采集Pandas做清洗分析ECharts做可视化展示。理由很简单这种组合在课程作业场景里是性价比最高的任何一个环节遇到问题网上都有海量现成案例可以参考。具体分工是这样的数据采集层Requests BeautifulSoup。这个站点虽然提供了接口但直接解析HTML反而更稳定因为接口参数经过编码处理后不好读而HTML结构里有清晰的CSS类名可以定位。数据存储层先用Pandas处理再导出到CSV和SQLite两种格式。CSV方便检查数据质量SQLite方便做后续的查询统计。分析层Pandas做分组统计、数值区间分布、排序等常规操作重点产出“出版社出书量Top N”“不同分类的价格分布”“出版年份趋势”“评分与评论数的关系”这几个分析维度。可视化层前端用ECharts的柱状图、箱线图、散点图和词云通过一个本地HTML文件聚合展示。为什么不选可视化工具如Power BI或Tableau因为作业的核心考察点包含Python的pandas处理能力纯拖拽式工具绕过了这个考察点分数上限会受限。而为什么不选Plotly或Bokeh这类Python可视化库说实话ECharts的文档和案例生态更全做交互式图表时踩坑成本最低。这个选择在后来的实现中被证明是明智的至少省了我一半的调试时间。1.3 模块拆分与任务排期两周时间看起来不少但实际上要上课、要写其他作业真正投入的时间每天大概两三个小时。如果不能把任务拆清楚很容易陷入“前期摸鱼后期熬夜”的恶性循环。我的拆分方式是按数据流来切模块第一周前三天确认数据源分析页面结构写采集脚本原型先把单页解析跑通。 第一周后三天扩展全量采集加入重试和限速机制完成全部数据落库同时写清洗脚本。 第二周前两天做数据分析和统计产出各维度数据表。 第二周后三天做可视化页面写说明文档做最终验收和查漏补缺。这个排期的核心逻辑是把“最不确定”的部分放在前面。页面结构分析和解析逻辑是最容易出意外的比如你以为的稳定结构其实在不同分类下字段不一致这必须尽早验证。可视化虽然看起来工作量大但技术确定性高只要数据准备好做出来只是时间问题。2. 核心细节解析与实操要点2.1 页面结构分析与采集策略我选的这个图书站点分类页URL的规律非常明显形如/sort/{分类ID}/{页码}共十多个分类每个分类下分页少则几页多则几十页。每本书的信息包含在一个结构统一的卡片区块里书名、作者、出版社、出版时间、价格、评分、评论数都在固定的CSS类名下。第一次跑通单页解析大概用了半小时。这里有个经验可以分享先用浏览器开发者工具检查页面元素时不要只盯着类名还要注意是否有动态加载。我研究后发现这个站点是服务端直出HTML不存在接口异步加载这样用Requests拿字符串再解析是最稳的。采集策略上需要注意的是页面URL的编码问题。这个站点的分类参数虽然看起来是中文直接用Requests访问时其实会自动完成编码不需要手动转码。如果你遇到URL编码问题优先检查请求头里的Referer和User-Agent对于大多数非硬核反爬站点这两个字段设置正确就能正常访问。关于限速我设置了每次请求间隔1.5到2秒。这个站点没有明确的反爬提示但为了不给服务器添加压力也为了避免触发隐性的访问频率限制这个速度是合理的。实测下来一千多个页面大概花了五十多分钟在可接受范围内。2.2 字段设计别一股脑全抓先想好要分析什么很多同学做爬虫时习惯“有多少字段就抓多少”这在作业场景下是大忌。字段过多不仅增加清洗工作量还会让分析阶段陷入“不知道该用什么”的迷茫。我在写解析规则之前先花了半小时把“要做什么分析”想清楚了再反向推导需要抓哪些字段。最终确定的字段是书名、作者、出版社、出版时间、分类、价格、评分、评论数、详情页链接。其中详情页链接不是必须的但保留它的好处是方便回溯检查数据是否正确也可以拓展到详情页去补充抓取简介。这里有一个细节出版时间字段在页面上的格式是“2023-05”这样的年月格式解析时直接切字符串就能拆出年份。分类字段则是从URL参数里取的而不是从页面按钮文字里提取因为后者在不同页面可能有一字之差。抓取过程中我遇到了一个比较隐蔽的问题部分图书的评论数显示的是“暂无评价”而不是数字。如果不加处理这个字段在后续统计时会出现类型转换报错。这个问题的处理方式我放在清洗环节统一解决采集阶段只需要把原始字符串完整保留。2.3 数据清洗脏数据比预想的更磨人数据清洗是整个过程中最耗时、最不显眼但最重要的环节。第二次作业专门讲过数据清洗这次正好实践。清洗流程我分成了四步。第一步是去重。由于分类页之间偶尔会有交叉推荐同一本书可能在两个分类下都出现这导致采集结果里有重复记录。去重逻辑选择了“书名 出版社 价格”三字段联合判断因为单纯用书名去重可能会误删同一本书的不同版本。第二步是处理缺失值。这里的缺失主要分两类一类是评分字段确实没有这个只能填充为“无评分”并单独分组另一类是评论数字段异常填充为0。这里特别要注意的是填充0之后要和真实的0条评论做区分我加了一个辅助字段标注这个值是否经过了填充方便后续分析时选择。第三步是类型转换。价格字段从字符串变成浮点数评论数从带逗号的字符串变成整数年份从字符串切出来变成整数。这一步需要处理一个常见的坑字符串转数字时带不可见字符。很多页面上的数字除了数字本身还可能有空格、换行、或者是全角字符直接int()就会报错。第四步是价格异常值处理。我画了一个价格分布直方图初稿之后发现有几个价格高达四位数甚至五位数的商品点进去发现是“套装礼盒”或者“绝版签名版”这些严格来说不算普通图书。处理办法是设定一个阈值把价格高于500元的记录单独存到一个“特殊商品”表里不参与常规统计分析。这个处理让后续的分布图从“几乎看不出规律”变成“有明显的长尾特征”。清洗这块的经验是不要指望一次清洗就能得到“完美数据”。清洗是一个循环往复的过程先做初步清洗然后画图、做统计发现异常再回去看原始数据再改清洗规则。我大概在这个循环里走了三轮才算把所有明显的脏数据问题清理干净。2.4 存储方案选择CSV和SQLite的双轨制存储方案我用了双轨制一个CSV文件打天下一个SQLite数据库做查询。这种设计的初衷很简单CSV方便人工检查和导入到分析工具里SQLite方便做SQL查询和统计。两种存储各有适用场景在作业里同时用两个知识点都覆盖到了。CSV的坑主要在于编码。直接用Pandas的to_csv默认是UTF-8编码但如果你在Excel里打开中文会乱码。解决方式是不用默认参数加一个utf-8-sig编码。这个细节在平时的练习里不会遇到但真正要把数据交付给别人查看时几乎必踩。SQLite的坑在于字段类型。Pandas的DataFrame写入SQLite时如果某列混合了字符串和数字SQLite会自动把整列推断为文本类型导致后续SQL查询时CAST操作满天飞。解决办法是在清洗完成后显式指定每一列的dtype再写入数据库。这个双轨制存储让我在最后写分析报告时非常省力。CSV版本用来快速做pandas聚合SQLite版本用来验证SQL查询结果是否一致两个结果互相校验能发现隐藏的计算误差。3. 实操过程与核心环节实现3.1 采集脚本的骨架与关键代码整个采集流程其实就是一个标准的生产者-消费者模型。我这里没有写得过分工程化就是基于requests最简单的方式代码不足百行拆出来核心逻辑如下import requests from bs4 import BeautifulSoup import time import random import pandas as pd headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://example.com/ } def fetch_page(category_id, page): url fhttps://example.com/sort/{category_id}/{page} resp requests.get(url, headersheaders, timeout15) resp.encoding resp.apparent_encoding return resp.text def parse_books(html): soup BeautifulSoup(html, html.parser) items soup.select(.book-item) rows [] for item in items: title item.select_one(.title).text.strip() author item.select_one(.author).text.strip() publisher item.select_one(.publisher).text.strip() pub_date item.select_one(.pub-date).text.strip() price item.select_one(.price).text.strip() rating item.select_one(.rating).text.strip() comments item.select_one(.comments).text.strip() rows.append([title, author, publisher, pub_date, price, rating, comments]) return rows all_data [] for cid in range(1, 16): for page in range(1, 50): try: html fetch_page(cid, page) books parse_books(html) if not books: break all_data.extend(books) print(f分类{cid} 第{page}页 采集到{len(books)}条) except Exception as e: print(f分类{cid} 第{page}页 出错: {e}) time.sleep(3) time.sleep(random.uniform(1.5, 2.0)) df pd.DataFrame(all_data, columns[书名, 作者, 出版社, 出版时间, 价格, 评分, 评论数]) df.to_csv(books_raw.csv, indexFalse, encodingutf-8-sig)这里的细节是resp.encoding resp.apparent_encoding这一行如果去掉部分页面中文会出现乱码。apparent_encoding 是根据页面内容推断的编码格式实测这个站点是GB2312而requests默认从响应头拿到的编码可能是ISO-8859-1这就会导致解析出一堆乱码。另外翻页循环里我用了一个break条件当某一页解析出来为空时默认这个分类已经没有更多页了直接跳出循环。这个逻辑在理想状态下没有问题但如果页面结构临时变化导致解析失败也会出现提前空页的情况所以在清洗阶段需要再做一次总量校验。3.2 清洗流程的完整实现清洗流程我按之前说的四步展开。这里把关键代码也贴出来其中每步都有一个核心逻辑import pandas as pd df pd.read_csv(books_raw.csv, encodingutf-8-sig) # 第一步去重 df df.drop_duplicates(subset[书名, 出版社, 价格]) # 第二步处理缺失 df[评分] df[评分].replace(暂无评分, ).fillna(无评分) df[评论数] df[评论数].replace(暂无评价, 0).fillna(0) df[评论数] pd.to_numeric(df[评论数], errorscoerce).fillna(0).astype(int) # 第三步类型转换 df[价格] df[价格].astype(str).str.replace(元, ).str.strip() df[价格] pd.to_numeric(df[价格], errorscoerce) df[年份] df[出版时间].str.extract(r(\d{4})).astype(float).astype(Int64) # 第四步特殊值单独存放 special df[df[价格] 500].copy() normal df[df[价格] 500].copy() # 保存 normal.to_csv(books_clean.csv, indexFalse, encodingutf-8-sig)清洗代码不复杂但每一行都有讲究。比如第三行处理评分时我先replace后fillna是因为原始数据里既存在“暂无评分”这个字符串也可能存在空单元格。第四步拆特殊商品时我选择了500元的阈值。这个阈值不是拍脑袋定的而是查看了价格分布直方图后观察到400元到500元之间有一个明显的断档500元以上是一个稀疏的长尾。选定阈值后常规分析的数据量从两千八百多条变成两千六百多条损失很小但分布图的可读性大大提升了。3.3 可视化页面的搭建与交互实现可视化部分我用的是ECharts的CDN加速版通过一个HTML文件把多个图表聚合在同一个页面上。整体布局是左右分栏上面是全局统计卡片下面是各图表模块。每个图表的初始渲染数据都写在JavaScript数组里这些数组是我在Python端分析完毕后手动粘贴过来的。这里有一个很重要的操作思路不要直接用JavaScript去读CSV文件。浏览器本地读取文件需要文件选择器或服务器的支持在作业演示场景下很麻烦。更稳妥的方式是直接在Python里把分析结果转成JSON格式的数组再嵌入HTML模板中。我之前还尝试过写一个Python脚本自动将图表数据填充到HTML模板的占位符里实现半自动生成但最后因为时间紧手动粘贴也够用。做出来的一共有五个图表第一个是柱状图展示出版社出书量Top 15横坐标是出版社名纵坐标是图书数。这里用鼠标悬停能看到每家的具体数值交互性体现在点击柱子可以高亮联动右侧的排行榜明细表。第二个是箱线图展示图书分类的价格分布。箱线图能很好的展示数据的分散情况比普通柱状图信息密度高很多一眼就能看出来哪个分类的图书溢价严重哪个分类的图书价格集中。第三个是散点图横坐标是评论数纵坐标是评分每个点是一本书。这个图是本项目里最有“发现感”的图表因为它直接揭示了一个反直觉的现象高评分的图书评论数并不一定高存在大量“小众高分书”。这种结论不是预先构思的是通过数据可视化才看到的。第四个是饼图展示分类数量占比。虽然饼图在信息传达上常被诟病但在这个场景下它的作用是用最直观的方式让读者快速建立对整体结构的认知所以还是有保留价值的。第五个是词云图由书名分词后生成主要作用是趣味性展示高频关键词。交互式图表的核心技巧在于ECharts的tooltip和dataZoom配置。如果只是把图画出来不加tooltip其实也算不上“交互式图表”。我在每个图里都加了自定义的tooltip格式化函数显示更完整的信息并在柱状图里加了dataZoom滑动条方便在图书数量较多时滑动查看。3.4 分析维度的确定从数据中挖掘可讲的故事做可视化最容易犯的错误是“为了画图而画图”拿到数据后不加思考就生成一堆图表但每个图都说不清楚想表达什么。我在做分析前给自己定了一个原则每个图表都必须回答一个问题。沿着这个原则推导最终的分析问题清单是哪些出版社的图书供给量最大这反映市场供给结构。 不同分类的图书价格差异有多大这反映分类定价策略。 图书出版年份的分布是什么样的这反映平台的库存结构是否偏重经典老书还是新书。 评分和评论数是否有相关性这反映口碑与热度的关系。 评分Top 50的书里面哪个分类占比最高这反映高质量内容集中在哪里。每个问题对应一个图表每个图表对应一段文字说明。这样的结构让整篇报告逻辑清晰老师一眼就能看出你是真的理解了数据而不是排了一堆花哨的图形。其中最有分析价值的发现是关于评分和评论数关系的。散点图画出来之后可以看到明显的分群现象评分7.5分以上、评论数100以下的书构成了一小撮“宝藏书”评论区热度不高但口碑极佳。这个发现为选书提供了一个有意思的参考维度让我在报告里多写了一整段分析。这种通过数据自然发现结论的过程是做这类作业最令人愉悦的部分。3.5 说明文档的写法与细节说明文档占了作业总分的一定比例不能写成“操作流水账”。我的写法是分成六块项目背景、数据来源说明、数据采集与清洗过程、分析维度与图表解读、结论与反思、附录代码结构说明。数据来源说明部分花了一些心思。因为涉及第三方网站我写清楚了数据获取时间、总量、采集方式并声明数据仅用于课程学习。这个说明既体现实操的专业性也能让数据来源可追溯。虽然课程要求不强制写但一份完整的作业在细节处更能加分。结论与反思部分是导师比较看重的。作业做得再好如果结论是空话套话也会显得没有独立思考。我写了三条真实发现第一该平台图书供给呈现出头部出版社集中的特征第二图书价格存在明显的分类差异和长尾现象第三评分与评论数并不强相关说明口碑与热度是两个维度。每一条都配了对应图表和数据支撑这样的说明文档才算真正闭环。4. 常见问题与排查技巧实录4.1 采集阶段页面内容拿到但解析为空第一次做全量采集时跑了十分钟回来一看CSV文件里只有几百条数据而分类循环显示还在继续。检查发现有些分类的第20页左右开始解析结果为空但其实页面上是有内容的。这个问题的根源在于我用的选择器太严格。某个分类底部的图书卡片样式和首页的样式有一些差异少了一个CSS类名导致选择器匹配不上。排查方法是用浏览器无痕模式打开对应分类的页面手动检查实际HTML然后给选择器加了一个备选方案。后来我改成了先定位卡片容器再在容器内做字段提取这样即使个别类名不同也不会全盘失败。另外一个问题是采集断点续抓。如果中途脚本崩溃跑过的页面又得重来一遍。我后来加了一个简单的断点记录功能每抓完一页就把当前页码和分类ID记录到本地文件重启时跳过已完成的页面。这个方法虽然简陋但面对作业量级的采集完全够用。4.2 清洗阶段类型转换报错让人崩溃清洗时最崩溃的瞬间是执行pd.to_numeric时报ValueError。排查到原始数据后发现价格字段里有类似¥45.00这样的值货币符号混在里面。这个问题的解决方式是先做正则提取只保留数字部分df[价格] df[价格].str.extract(r(\d\.?\d*))这个方法比先replace再strip更稳健因为无论前面带的是什么符号正则都直接从字符串里提取数字部分。类似的还有评论数里的1.2万这样的格式不能直接转数字。我的处理是用判断加乘法展开包含“万”字的先乘以10000再转整数。清洗还有一个容易被忽视的坑空值的“假象”。有些字段在页面源码里是空格字符打印出来看着是空的但是用isnull()判断却是False。这个问题的排查方式是检查字符串长度对于长度不为零但strip后为空的统一替换为NaN再做填充。4.3 可视化阶段ECharts地图加载失败做可视化的时候我曾经想加一个分地域的统计图用ECharts地图展示不同省份的图书供给量但地图JSON文件总是加载失败。排查后发现是CDN的map数据源不支持跨域引用换了本地JSON文件之后要配置地图注册也麻烦。最终这个想法被砍掉了改用柱状图来展示地域分布效果反而更加直观。这个经验让我意识到不要为了秀技术而增加不可控的依赖。地图虽然是加分项但如果你对ECharts地图机制不熟悉很可能在演示前才发现加载不出来。稳妥的方案永远优先于出彩的方案。另外一个可视化里的常见小问题是数据量太大导致图表渲染卡顿。我在做散点图时一开始放了将近两千八百个点拖拽缩放时明显掉帧。解决方式是降低点的透明度、关闭动画效果。经过调整后渲染流畅度提升不少。4.4 演示时被问到的问题答辩演示时导师可能会问为什么选择这个数据集、怎么保证数据质量、有哪些结论是之前不知道的。这些问题只要认真做了其实都能答上来。我当时被问到的一个问题印象比较深“你的数据清洗有没有考虑过版本差异同一本书的精装版和平装版价格差很多你有没有处理”这个问题确实是一个盲区。好在我之前去重用的是“书名出版社价格”组合不会误删版本不同的书而且在分析中以“条”为单位而不是以“种”为单位来表述规避了版本混淆的问题。从这里我学到的经验是作业分析中每下一个结论都要先想清楚数据的粒度表述严谨一些很多问题都能提前规避掉。5. 验收自查与个性化加分项5.1 提交前的验收清单作业交之前两天我列了一个Checklist逐项检查强烈建议任何人都做这一步数据量方面原始数据采集量是否达标清洗后数据是否保存了原始备份。我保留了两个版本的数据raw和clean两份老师如果需要核查数据来源可以对照查看。代码可运行性方面从头跑一遍采集脚本确认不会因为网络变化导致的偶发问题直接崩溃。爬虫类作业最怕的就是演示时重新跑代码结果因为某个页面超时就挂了。我的解决办法是给请求加了异常处理超时自动重试三次之后才跳过。可视化的可访问性方面HTML文件在本地双击能否正常打开CDN的ECharts库在无网络时是否还能加载。为了保险起见我把ECharts的JS库文件下载到本地目录demo演示时即使现场网络状况不好也不受影响。说明文档完整性方面所有图表是否都在文档里配了文字解读代码有没有加注释。数据合规性方面文档里写了数据只用于课程作业不进行二次传播。5.2 加分项设计多做一个词云和自动化报告在基础要求之上我做了一个词云图和一个自动化的分析汇总脚本。词云图虽然不一定有非常深入的分析价值但视觉效果好放在演示开头能起到不错的吸睛效果。弄自动化脚本则是为了把整个清洗和分析流程串联起来只需一条命令就能跑完所有数据处理并输出统计摘要。虽然这个自动化脚本很简单但在报告里展示了完整的工程化思维这比只交一堆零散脚本要体面得多。自动化脚本的核心是一个run.pyimport subprocess steps [ python crawler.py, python clean.py, python analyze.py ] for step in steps: print(frunning {step}) subprocess.run(step, shellTrue)三个脚本串联形成一个完整的数据流水线。当我把这个流水线跑通的那一刻整个作业给到的成就感是很明显的。从爬虫采集到分析出结论一条命令全部完成你已经不是在“交作业”了而是在构建一个小型的数据工程项目。5.3 复盘如果再让我做一次我会怎么改如果时间充裕我会做三处改进。第一处是给采集端加一个简单的增量更新机制让程序检测到已采集的数据后只抓取新增数据。第二处是分析端加上更精细的自然语言处理比如对书名做主题聚类分析不同主题的评分差异。第三处是前端做成一个本地启动的Web服务而不是静态的HTML文件这样在切换数据集时不用手动改JavaScript数组。但这些都是锦上添花的部分对于课程作业来说完成度已经足够。更重要的是这个项目让我真正体验了“数据-信息-结论”的完整链路从拿到一堆HTML源码到最后能自信地说出“这个平台的图书市场存在头部效应”“评分与热度相关性弱”这个过程是靠动手做出来、靠数据验证出来的和只看别人的分析报告完全不同。最后再分享一个小技巧整个项目实施过程中每完成一个阶段就做一个带日期的本地存档包括脚本版本、数据版本和阶段性的截图。这样做的好处是当你想回看某个环节是怎么一步步变化的时候你手上有完整的时间线索写文档时也能快速回忆每个阶段遇到的问题。作业做完了这些记录以后找实习、做作品集的时候也能拿出来用。