ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

图书数据分析可视化系统:从爬虫到推荐的Django全栈实践

图书数据分析可视化系统:从爬虫到推荐的Django全栈实践 这个项目的名字里虽然带着“机器学习”四个字但真正做完你会发现它骨子里是一个标准的数据分析全流程作品Python 爬虫负责采集当当网的图书数据清洗之后用 Django 框架搭后端接口再通过 ECharts 做可视化大屏展示最后补一个图书推荐模块把整条链路串起来。整体覆盖“采集—清洗—存储—分析—展示—推荐”六个环节放在毕业设计或者大数据课程实践里都属于性价比很高的选题。这套系统能解决什么问题说白了就是三件事第一用真实业务数据验证完整的采集和分析方法论第二把枯燥的表格数据变成答辩评委或业务方一眼就能看懂的大屏图表第三让访问者能从成千上万本图书中快速找到自己可能感兴趣的同类书。如果你正准备做类似的图书数据分析系统或者想用一份“可运行、可演示、可讲清楚”的全栈项目作为简历亮点这篇文章会从架构设计一路拆到细节实现。1. 项目全貌与需求拆解1.1 核心链路解析从采集到推荐的六个环节我第一版做这个项目时踩过最大的坑就是低估了“链路整合”的难度。很多人以为爬虫最难其实当你把六块功能放在一个系统里时真正费时间的反而是数据格式不统一、接口定义不一致、前端图表和后端字段对不上这类“黏合层”问题。所以先看整体设计。这套图书数据分析可视化系统我把中文完整链路拆成六个环节。环节核心任务产出物关键工具数据采集爬取当当网图书列表页与详情页原始 JSON 数据文件requests BeautifulSoup数据清洗去重、缺失值处理、字段类型规整干净的 DataFrame 或 CSVpandas数据存储设计表结构并落地MySQL 数据表 Redis 缓存Django ORM / SQLAlchemy统计分析围绕核心维度做聚合统计统计结果 JSONDjango ORM pandas可视化大屏将统计结果渲染为图表大屏可交互的大屏页面ECharts HTML/CSS图书推荐基于内容相似度推荐图书相似图书列表余弦相似度 numpy这个顺序不能乱因为下游环节严重依赖上游输出的结构。比如清洗环节如果没把价格字段从“45.00元”这种字符串转成 float后面统计价格分布时就会直接报错或者画出一堆异常值。推荐模块如果拿不到干净的分类和出版社数据特征向量就是一堆空值。我当时定的数据规模是抓取 8 到 10 万条图书记录。这个量级没有大数据平台也能轻松扛住MySQL 单表完全没问题又比几千条数据更有说服力。如果你只有一两万条图表会显得单薄但如果去爬几百万条单机 requests 又太慢还得上 Scrapy 分布式反而偏离了毕业设计的重心。1.2 技术选型背后的原因Django 不是唯一答案但最稳关于后端框架我先做了一版 Flask后来重构时换成了 Django。原因很实际。Flask 确实轻量一张 main.py 就能跑起服务适合做课程作业。但这类项目一旦涉及模型层、数据库迁移、后台管理、接口路由分离Flask 就暴露短板了——你需要自己集成 Flask-SQLAlchemy、Flask-Migrate、Flask-Admin中间还要处理各种配置冲突。Django 自带 ORM、Admin 后台、Migrate 迁移机制甚至自带分页和缓存框架你只需要规规矩矩把 app 划分好代码结构天然清晰。我记得有个细节特别能说明问题Django 的 ORM 在聚合查询上比 Flask 的 SQLAlchemy 更顺手像.values(category).annotate(countCount(id))一行就能求出每个分类的图书数量。大屏后端需要十几类统计接口写起来非常快。爬虫部分我坚持用 requests BeautifulSoup而不是一上来就上 Scrapy。这个选择很多教程会骂我但我的理由很简单几十万条数据、单机跑六到八个小时就能搞定requests 配合线程池完全够用。Scrapy 的异步下载器和中间件机制确实强大但项目复杂度也会同步上升对于以“全流程展示”为核心诉求的系统来说有点杀鸡用牛刀。存储上选用 MySQL Redis 双组合。MySQL 存全量明细数据保证可靠性和分析能力Redis 存大屏高频访问的统计结果和推荐结果的热缓存。为什么要 Redis因为大屏页面每隔十几秒就会拉一次统计数据如果每次都实时查 MySQL数据库压力会随着演示次数上升而且大屏打开时如果全部重新聚合计算首屏能卡三秒以上。把最热门的几个聚合接口做成五分钟过期缓存体感会好非常多。1.3 系统部署架构总览整套系统的部署架构我也顺手给出来方便你后面答辩画图。生产环境实际用的是 Nginx Gunicorn 跑 Django静态文件交给 Nginx 处理动态接口走 Gunicorn 转发。MySQL 负责持久化Redis 负责缓存。客户端浏览器访问大屏页面时Nginx 直接返回 HTML、CSS、JS 文件页面加载完成后通过 Ajax 请求 Django 的/api/statistics/系列接口。Django 先查 Redis 缓存若命中则直接把缓存 JSON 返回给页面若未命中则执行 ORM 聚合查询将结果写回缓存并设置过期时间再把 JSON 返回给前端。这个架构的好处是每层都能独立替换。学生版不用 Nginx直接python manage.py runserver也行不想用 Redis把缓存函数改成内存字典也能跑只是没那么优雅。但架构图完整画出来体现的是你对“分层”这个概念有认知这在答辩环节是加分项。2. 当当网爬虫与数据清洗实战2.1 爬虫方案设计先搞清楚页面结构和字段来源写爬虫之前第一件事是打开当当网的页面按 F12 看清页面结构。别急着写代码你可以先手动翻几页把列表页的 URL 规律找出来。当当图书列表页的地址格式比较有规律大致是图书分类路径加页码参数。例如某个分类第一页是https://category.dangdang.com/cp01.01.02.00.00.00.html往下翻页会看到?page_index2这样的参数。我的建议是选定一个主分类把列表页 URL 模板确定下来再遍历页数即可。列表页能拿到的核心字段包括图书标题、作者、出版社、价格、原价、评论数量、详情页 URL。但评分这个字段在列表页是不完整的很多书没有评论就没有评分这时需要进入详情页补抓评分数据。由于十万条数据全部走详情页会翻五倍请求量我做了策略区分列表页大批量抓取详情页只对评分缺失或者需要推荐特征的图书做补抓。这样整体请求量可控单个分类下几千本书requests 串行加线程池后大约十几分钟能跑完一轮。字段设计上我会多留一个raw_json字段把原始 JSON 原样落盘。这一步很多人会忽略但实际价值很大——清洗方案改来改去时你不需要重新爬一遍直接从原始数据重新清洗就行。2.2 反爬应对与采集节奏控制当当的反爬不像电商巨头那么严格但如果不加控制地高频请求照样会被限流。我踩过最明显的迹象就是请求返回 403 页面或者响应体里出现一个验证跳转页。我采用了一套组合策略按优先级排序随机 User-Agent把常见的 Chrome、Firefox、Safari UA 放到列表里随机挑选每次请求之间加随机延时1 到 3 秒之间随机请求失败时最多重试三次每次重试延长时间翻倍爬取一段时间后主动暂停 10 到 15 秒设置单 IP 下最大并发数我用线程池时控制为 4 个线程这里给一段请求核心代码作为参考import time import random import requests from fake_useragent import UserAgent ua UserAgent() def fetch_with_retry(url, max_retry3): for attempt in range(max_retry): try: headers { User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } resp requests.get(url, headersheaders, timeout10) if resp.status_code 200 and 验证 not in resp.text: return resp except Exception as e: print(f请求异常: {e}, 第 {attempt 1} 次重试) time.sleep(2 * (attempt 1)) return None需要注意一个合规性问题爬虫要在合理频率下采集公开信息不能对目标站点造成访问压力更不要绕过登录或验证码机制去获取非公开数据。我的建议是单次任务总量控制在几万条以内频率限制在每秒一两个请求。这个节奏既能保证演示数据量又不会把目标网站拖垮。2.3 数据清洗的三大关键操作清洗是我愿意花最多篇幅讲的部分因为数据库里每条脏数据都会在最终可视化大屏上原形毕露。清洗操作我归纳为三大类。第一类去重。图书数据重复主要来自两个原因同一本书在不同列表页出现多次比如新书榜和好评榜同时收录以及爬虫中断后重新爬取导致的重复记录。去重逻辑我用的是“书名 作者”联合判断比单纯用书名可靠得多因为不同出版社可能出同名书。pandas 里一行drop_duplicates(subset[title, author])就能处理。第二类缺失值处理。图书数据中图片、简介、出版社这些字段可能有缺失。对于展示型项目不要一删了之。评分缺失的图书用该分类下的平均评分填充即可出版社缺失的填充“未知出版社”图片缺失的用一张本地默认封面代替。数据量只有几万条时直接删行会影响统计分布的真实性。第三类字段类型规整。这是最容易出幺蛾子的地方。“价格”字段爬下来经常是¥45.00或者45.00元需要正则提取数字再转 float。“评论数”有时是1万这种格式要转成10000。出版时间也需要统一成YYYY-MM-DD格式。这类转换逻辑汇总成一个函数处理不要散落在各处。def clean_price(price_str): # 提取纯数字部分保留小数点 import re match re.search(r(\d\.?\d*), str(price_str)) return float(match.group(1)) if match else None def clean_comment_count(count_str): # 处理 1万 - 10000 的情况 import re text str(count_str) match re.search(r(\d\.?\d*), text) if not match: return 0 value float(match.group(1)) if 万 in text: value * 10000 return int(value)清洗完成后我会输出一版clean_books.csv做质量抽检随机抽 200 条人工看一遍字段是否正常。这个动作花不了十分钟但能把后面所有的分析环节都保护起来。2.4 存储设计MySQL 表和 Redis 缓存的配合清洗干净的数据最终要落库。我设计的 book 表结构大概长这样字段类型说明idBIGINT 自增主键主键titleVARCHAR(255)书名authorVARCHAR(255)作者publisherVARCHAR(255)出版社publish_dateDATE出版日期priceDECIMAL(10,2)现价original_priceDECIMAL(10,2)定价ratingDECIMAL(3,1)评分0 表示暂无rating_countINT评论数categoryVARCHAR(100)分类detail_urlVARCHAR(500)详情页 URLcreate_timeDATETIME数据入库时间索引方面我建了三个category、publisher、rating。这三个字段分别对应大屏的分类占比图、出版社榜单和评分分布图。别给所有字段都加索引索引过多会影响写入速度而且在这个数据量下毫无必要。Redis 缓存设计更简单。我在 Django 里封装了一个自定义缓存装饰器为每个大屏统计接口设置 300 秒过期时间。这样即使大屏每隔 15 秒拉一次数据后端也只有第一次请求会真正查询数据库。具体的缓存键我用接口名和参数拼接比如dashboard:category_count:all。3. 数据分析与可视化大屏的实现3.1 数据分析维度怎么定先问业务问题大屏好看的前提是分析维度有逻辑不是为了堆图表而堆图表。我设计大屏之前先列了四个业务问题图书定价集中分布在什么区间大众图书的合理价格带在哪里评分和评论数之间有没有关系高评分图书是否一定高关注度哪些出版社上榜数量最多出版市场上哪些出版社最活跃不同分类的图书占比如何哪几个分类是绝对主力这四个问题对应四类核心图表价格区间分布图直方图、评分与评论数散点图、出版社 TOP10 榜单横向条形图、分类占比图饼图或环形图。额外我还加了一个“出版年份与数量趋势”的折线图能看出近几年图书出版数量的变化趋势。这五个维度就足够撑起一个 1920x1080 的大屏布局了再多就显得杂乱。数据结果也很有意思。我爬的那批数据里价格在 30 到 60 元之间的图书数量最多评分在 8 到 9 分之间是集中区域评论数和评分并没有强正相关——有些评分 9 分以上的书评论数反而很少说明“叫好”和“叫座”是两回事。这类洞察都可以写进论文的结论部分比单纯贴几张图有说服力得多。3.2 Django 数据接口设计大屏前端不直接连数据库所有数据通过 Django 的 JSON 接口获取。我按功能拆分了五个接口每个接口返回结构固定/api/statistics/price_distribution/返回价格区间与数量列表/api/statistics/score_comment/返回评分、评论数散点数据/api/statistics/publisher_top/返回出版社 TOP10/api/statistics/category_ratio/返回分类占比/api/statistics/year_trend/返回出版年份趋势以价格分布为例后端用 ORM 查询很轻松完成def price_distribution(request): # 先查缓存 cache_key dashboard:price_distribution result cache.get(cache_key) if result: return JsonResponse({data: json.loads(result)}) # 定义价格区间 bins [0, 20, 40, 60, 80, 100, 200] labels [0-20, 20-40, 40-60, 60-80, 80-100, 100-200] data [] for i in range(len(bins) - 1): count Book.objects.filter(price__gtebins[i], price__ltbins[i1]).count() data.append({range: labels[i], count: count}) result {data: data} cache.set(cache_key, json.dumps(result), timeout300) return JsonResponse(result)所有接口都遵循“先查缓存、再查数据库、再回写缓存”的模式代码结构统一后期维护就轻松。3.3 可视化大屏布局与 ECharts 交互大屏页面我按经典的数据驾驶舱样式来布局顶部是系统标题和更新时间中间主体为价格分布主图和分类占比环形图左右两侧分别是出版社 TOP10 条形图、评分与评论数散点图、年度趋势折线图。底部再放置一个滚动更新的“图书榜单”区域从热门图书中取 10 本轮播展示。布局比例上我用的是 1920x1080 为基准的栅格。特别强调一个经验大屏不要用纯 px 写死尺寸否则在答辩教室不同的投影分辨率下会出现错位。更好的方案是用 rem 作为单位配合一段自适应脚本动态设置根字体大小。我实测下来这段脚本对 1366x768 和 2560x1440 的适配效果都令人满意。function setRem() { const baseWidth 1920; const scale document.documentElement.clientWidth / baseWidth; document.documentElement.style.fontSize (100 * scale) px; } window.addEventListener(resize, setRem); setRem();ECharts 部分我封装了一个公共的初始化函数传入图表容器 id、option 配置和接口地址。这样五张图表共用同一套“拉取数据 渲染 监听 window resize”逻辑代码量减少一半以上。还有一个容易被忽略的体验细节大屏要加一个每 15 秒刷新数据的定时器。刷新逻辑很简单重新请求接口然后调用chart.setOption(option)但在刷新时不能使用notMerge参数清空旧数据否则会有明显的闪烁感。另外在数据刷新过程中要避免重复创建图表实例正确做法是在图表初始化时先chart.dispose()销毁旧实例。4. 图书推荐模块轻量但完整的推荐链路4.1 为什么不做协同过滤这个项目的推荐模块我最终选择的是基于内容的推荐而不是很多人第一反应会选的协同过滤。原因是这个场景有几个现实约束。协同过滤需要有真实的用户行为数据——用户点击、购买、评分记录。我们这个系统里根本没有登录注册流程更别说行为日志了。就算硬凑出一张模拟用户评分表数据也非常稀疏每个用户都只评过几本书算出来的相似度矩阵毫无参考价值。这就是协同过滤最典型的“冷启动”问题新用户没有行为记录推荐系统完全失效。基于内容的推荐就没有这个困扰。它只依赖图书自身的特征只要每本书的分类、出版社、作者、价格、评分这些字段是完整的就能算出书与书之间的相似度然后给任意一个入口返回“和这本书相似的图书列表”。这非常适合我们的系统图书详情页里挂一个“猜你喜欢”的横条交互自然算法也能讲清楚。4.2 特征工程与相似度计算基于内容的推荐核心就是两步构造特征向量计算相似度。我提取了五个特征维度图书分类文本、出版社文本、作者文本、价格区间数值分箱、评分区间数值分箱。文本特征用 one-hot 编码数值特征先分箱再 one-hot。比如价格落入 40-60 元区间则该维度向量为 1否则为 0。这里我把分类做再细化处理。当当网的分类有多级结构如“文学 小说 中国当代小说”不能直接整个字符串拿去 one-hot否则维度爆炸且没有区分度。我取到二级分类为止并把父类和子类分开编码比如“文学”一个维度、“小说”一个维度这样同一个大类下的书即使子类不同也有父类的相似度基础。相似度计算用的是余弦相似度。sklearn 里有现成的cosine_similarity但我不建议在 Django 请求链路里直接跑全量计算十万本书两两计算会生成十亿量级的相似矩阵单机完全跑不动。我的策略很简单离线用 Python 脚本预先算好每本书的 top-N 相似书列表结果存到 Redis 里线上接口只做查询。from sklearn.feature_extraction.text import CountVectorizer from sklearn.metrics.pairwise import cosine_similarity # 特征文本拼接 books[feature_text] ( books[category] books[publisher] books[author] books[price_level] books[rating_level] ) vectorizer CountVectorizer() feature_matrix vectorizer.fit_transform(books[feature_text]) similarity_matrix cosine_similarity(feature_matrix) # 取每本书最相似的5本书 for idx, row in books.iterrows(): sim_scores list(enumerate(similarity_matrix[idx])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) top_books [books.iloc[t[0]][id] for t in sim_scores[1:6]] rds.hset(recommend:bookid: str(row[id]), mapping{suggest: top_books})注意这里feature_text里加的四个文本字段都确保是 string如果某个字段为空要用占位符填充否则 CountVectorizer 会把空值报错。这也是清洗环节做缺失值填充的原因之一。4.3 推荐接口与前端呈现推荐接口设计为/api/recommend/?book_id123返回该书的相似图书列表每个结果包含书名、作者、价格、评分和封面地址。前端在图书详情页右侧放一个“相似好书推荐”的卡片区域点击书名可以跳转到对应详情页。另外我还在大屏底部做了一个“热门图书推荐”模块它展示的是评论数最多的十本书从另一个角度给大屏增加了“推荐”的功能呈现。整体推荐模块在答辩讲解时我建议抓住三个要点其一推荐数据是离线预计算的不是实时全量匹配这样保证了接口速度其二相似度计算采用的是余弦相似度衡量的是特征向量方向的一致性它不受向量长度影响所以不会因为某本书特征文本长就天然相似其三推荐效果在人工抽查时是有可解释性的——一本讲 Python 爬虫的书推荐列表大概率是 Python 开发和数据分析类图书。5. 常见问题与避坑指南5.1 爬虫请求被限制怎么办这个现象我在开发时几乎每天都会遇到。一旦请求频率过高当当会返回 403 或者跳转到一个安全页面此时解析出来的数据全是乱码或空列表。我的排查顺序是先确认是否触发反爬再看频率再看数据是否能被重新解析。如果是请求频率过高就把随机延时拉长到 5 秒以上并暂停一段时间。不要为了追求速度把单次任务跑得太激进展示项目稳定比速度重要得多。另一个经验是爬虫不要用固定 IP 连续作战有条件的话可以隔一段时间换个网络环境再跑但这种操作要注意合规性尽量只在合法采集的范围内进行。5.2 中文乱码与编码问题requests 返回的页面编码判断错了中文直接显示为乱码这个坑十个人有九个踩过。当当的页面编码在不同页面可能不同不能写死resp.encoding utf-8。正确做法是用resp.apparent_encoding让 requests 自动判断编码或者直接从响应头里拿 char set。但apparent_encoding偶尔会误判所以我实际用的方式是先用resp.content.decode(utf-8, errorsignore)尝试抓取失败再用 gb18030 解码兜底。最终清洗出来的数据入库时全部统一转成 UTF-8MySQL 连接参数里也要带上charsetutf8mb4否则存入 emoji 或生僻字时会报错。5.3 大屏在不同分辨率下错乱原生 px 写死宽度是自坑第一来源。我前一个版本在 1920 笔记本上完美换到 1366 的笔记本演示就直接横向溢出图表的 tooltip 跑到屏幕外非常掉价。解决方案就是前面提到的 rem 自适应方案但还需要注意图表容器的高度也要用 rem 指定。ECharts 的chart.resize()方法需要在容器 size 变化后调用我在自适应脚本里加了一个 debounce 函数保证窗口连续变化时不会频繁触发 resize 导致卡顿。5.4 数据库查询过慢与 N1 问题大屏第一次加载时如果所有统计接口都实时跑 SQL你会看到页面空白好几秒。除了加 Redis 缓存外还要注意 ORM 的 N1 查询问题。最典型的场景是详情页展示图书列表时需要同时显示每本书的出版社名称和封面地址如果循环取出每条记录再逐条查询关联表几百本书就会产生几百条 SQL。解决办法是用select_related或prefetch_related提前把关联数据加载到内存或者干脆把需要展示的字段直接冗余在 book 表里。展示型项目我不反对适度冗余一张宽表解决 90% 的查询场景。5.5 容易被忽视的更新节奏设计数据不是只爬一次就完事的。我当时反复修改清洗规则导致库里的数据和原始 CSV 对不上非常头疼。我的建议是给数据加版本标记每次重爬或重清洗时记录一个 batch_id。大屏展示时可以只查最新 batch 的数据出问题时也可以快速回退到上一批数据。增量更新方面我写了一个定时脚本每周跑一次检查已有图书的价格和评论数是否有变化有变化则更新记录新增图书则插入。这样既保证了大屏演示时数据是新鲜的又不会因为全量重爬消耗太多请求配额。做完整套系统之后我最大的感受是不要被“机器学习”这四个字吓住这类项目真正的难点从来不是算法多高深而是你能不能把每个环节都稳稳地串起来。如果你想让这个项目更进一步可以考虑把推荐模块加上用户行为日志做成协同过滤或者把大屏的数据刷新改成 WebSocket 全双工推送甚至把离线计算脚本换成 Spark 处理百万级数据。但前提永远是先把当下这条链路跑通跑顺否则换什么技术栈都一样抓瞎。我的经验是留出至少两天时间专门做数据质量抽检和页面细节打磨这笔时间投入的回报远远高于多写两个用不上的功能。
返回列表