
简介面向毕业设计场景的Python商品销售数据分析可视化系统源码包基于Django框架并整合爬虫模块适合需要快速搭建完整项目的高校学生。源码已经本地编译通过配置Python环境即可运行核心功能均经过导师验证满足课程设计与毕设验收要求。压缩包共1480个文件约17.68MB其中包含691个JavaScript文件、196个CSS样式文件以及大量图片、字体和地图资源用于前端交互与界面呈现同时提供13个Python源文件及编译缓存涵盖后端业务逻辑与数据采集处理代码。目前已有181人浏览学习。资源附带清晰的目录结构前端静态资源、后端代码与配置文件分离便于按模块阅读理解。除核心功能外还内置爬虫脚本可自动抓取商品数据配合可视化图表实现分析展示整体方案完整可直接用于答辩演示。1. 带爬虫的商品销售分析系统一次跑通数据采集到看板的完整链路做商品销售分析最烦的不是分析本身而是数据从哪来。很多从业者手头只有店铺后台导出的Excel字段乱、更新慢想按天看趋势还得手动刷新。基于Python的商品销售数据分析可视化系统带爬虫源码解决的就是这条链路用爬虫定时抓取平台页面上的商品价格、销量和评价数经过清洗入库再用图表把销售趋势、类目占比、单品排名直接呈现出来。它适合三类人想给自家店铺做销售监控的运营接单做数据分析小工具的开发者以及正在学爬虫和数据可视化、需要一个完整项目的初学者。这套方案的核心价值是数据自动更新图表跟着变化不用再手动维护报表。2. 系统架构与数据流先从源码里看懂四个模块怎么协作拿到这类源码包别急着跑先把数据流理清楚。整套系统一般拆成爬虫、数据清洗、分析与可视化四段每段独立成模块段与段之间靠数据文件或数据库衔接。理清这条链路后后面调参和排错才有方向。2.1 爬虫、清洗、分析、可视化四模块的职责边界与选型理由常见做法是把四个模块拆成四个目录或四个文件各干各的互不掺和。爬虫模块只负责采集页面并解析出结构化字段输出原始数据清洗模块负责去重、补缺、统一格式分析模块读取清洗后的数据计算聚合指标可视化模块把分析结果渲染成图表。这样拆的好处是任何一个环节出了问题只需要改对应的文件不用动其他部分。选型上爬虫用requests配合BeautifulSoup或lxml足够应对大多数静态列表页数据量大、页面是异步加载时才需要换成Selenium或直接请求接口。存储层用 SQLite 最省事单文件、免安装Python 内置sqlite3就能驱动如果只是几百条数据做演示用 CSV 也能接受。分析层用pandas它有现成的分组、聚合、透视功能。可视化层主流有两种选择matplotlib适合生成静态图片pyecharts适合生成可交互的 HTML 看板普通销售分析场景我会选pyecharts因为它生成的图表能缩放、能悬停查看数值给业务方看更直观。# 项目目录划分示例 project/ ├── crawler/ # 爬虫模块 │ ├── spider.py # 页面采集 │ └── parser.py # 字段解析 ├── pipeline/ # 清洗模块 │ └── cleaner.py # 去重与格式化 ├── analysis/ # 分析模块 │ └── aggregator.py # 聚合计算 ├── dashboard/ # 可视化模块 │ └── charts.py # 图表生成 └── main.py # 总入口各模块之间的衔接尽量用函数返回值或数据库表传递数据而不是在模块里互相 import 后直接改对方的数据。这样可以保持单向依赖。所谓单向依赖就是爬虫只写数据清洗只读爬虫的数据分析只读清洗后的数据可视化只读分析结果不绕回去。2.2 最小可用的数据表设计销售分析要存哪些字段很多初学者的数据表设计要么字段太少要么字段冗余。做商品销售分析最核心的字段就这么几个商品ID、商品名称、所属类目、价格、销量、评价数、采集时间。这七个字段基本能覆盖按商品看价格带、按类目看销量占比、按时间看趋势这几类需求再多加字段就要从业务出发别堆没用的信息。CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id TEXT NOT NULL, -- 商品唯一标识可去重 product_name TEXT, -- 商品标题 category TEXT, -- 所属类目 price REAL, -- 展示价格取数字 sales_count INTEGER, -- 销量取整数 review_count INTEGER, -- 评价数 crawled_at TEXT -- 采集时间ISO 格式 );采集时间字段容易被忽略但它是做时间序列分析的基础。如果只想看当前快照不加时间字段也能跑。可一旦想分析“近30天销量变化”就必须在每次采集时把这个字段写进去。另一处要注意的是价格和销量要区分类型价格存成REAL销量存成INTEGER否则后续聚合会出现字符串拼接的问题。解析网页时抽出来的数字往往带“万”字或逗号清洗时需统一转换成纯数字。2.3 源码目录怎么组织配置文件与入口函数先看哪几个拿到源码包先不急着运行按顺序看四个文件配置文件、爬虫入口、清洗函数、总入口。配置文件里通常是数据库路径、请求间隔、目标URL这些可变参数爬虫入口看采集流程怎么循环翻页清洗函数看它做了哪些字段处理总入口看整个流程怎么串联。把主线找出来之后其余代码都属于细节。# config.py 推荐的配置写法 DATABASE sales.db REQUEST_INTERVAL 2 # 每次请求间隔单位秒 REQUEST_TIMEOUT 10 # 请求超时单位秒 MAX_RETRY 3 # 单页最大重试次数 START_URL https://example.com/list?page1配置集中在一个文件里后续调采集频率、换目标地址时只改这里不用去各个模块里找散落的常量。如果源码里直接把URL写在爬虫函数里建议自己拆出配置项这是后续维护成本最低的做法。参数里我最看重REQUEST_INTERVAL它直接决定对目标站点的友好程度也影响被封的概率。提示拿到源码先看requirements.txt里的依赖列表缺失的库用pip install 包名补上别一次性装全部避免版本冲突。3. 爬虫模块的工程化写法请求、解析与反爬参数的调优爬虫是这套系统的数据入口也是出错率最高的地方。页面改版、接口加签名、IP被限制都会导致采集结果为空。把请求层、解析层和容错机制写扎实系统才能稳定跑。3.1 请求层请求头、超时与重试的推荐参数请求层最基础的是模拟浏览器访问。有些页面不带User-Agent会直接返回 403带全了请求头才能拿到 200。常用的做法是构造一个headers字典把User-Agent、Accept、Accept-Language都带上。如果目标页面登录后才能看部分价格还需要带上Cookie。import requests from time import sleep headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en;q0.6, # Cookie: 从浏览器复制实际登录后的Cookie } def fetch_page(url, retry3, timeout10): for attempt in range(retry): try: resp requests.get(url, headersheaders, timeouttimeout) resp.raise_for_status() return resp except requests.RequestException as e: print(f第{attempt 1}次请求失败: {e}) sleep(2 * (attempt 1)) # 退避等待避免连续重试 return None这段代码的核心是retry timeout组合缺一不可。timeout控制单次请求的最长等待不设置的话遇到慢接口会一直挂住retry控制失败后的重试次数重试间隔逐步拉大这是礼貌采集的关键。参数怎么调要看目标站点页面响应慢就把timeout调到 15–20 秒接口不稳定就把MAX_RETRY加到 5。千万不能把重试间隔设成固定 1 秒连续快速重试被封的概率会明显升高。3.2 解析层从列表页抽商品名、价格与销量的选择器写法拿到页面响应后解析层的工作是定位商品节点再从节点里抽取字段。定位节点用 CSS 选择器或 XPath 都行BeautifulSoup的select方法配合 CSS 选择器最直观。解析前先打开浏览器开发者工具查看目标字段所在的 HTML 结构确认选择器唯一性。from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, html.parser) items [] # 找到每个商品的根节点 for node in soup.select(div.product-item): name_node node.select_one(.title) price_node node.select_one(.price) sales_node node.select_one(.sales-amount) items.append({ product_name: name_node.get_text(stripTrue) if name_node else , price: parse_price(price_node.get_text() if price_node else ), sales_count: parse_sales(sales_node.get_text() if sales_node else ), }) return items写选择器时最容易翻车的点是只看了单个节点就写死结果页码一换就抽不到数据。稳健的做法是先确认这个选择器在列表页第 1 页和第 2 页都能命中再投入使用。抽取出的文本要用strip()去掉空格换行价格和销量里的“¥”“万”“”这类符号在清洗阶段统一处理别在解析层做太复杂的逻辑否则清洗模块就失去意义了。3.3 采集节奏与容错限速、异常捕获与断点续爬采集节奏直接关系账号安全和数据完整性。对商品列表这种多页抓取场景我一般会在每页之间加固定间隔同时记录已抓取的页码避免中途失败后从头再来。import json def crawl_all_pages(start_url, total_pages10): results [] # 读取断点记录跳过已完成的页码 done_pages load_progress() for page in range(1, total_pages 1): if page in done_pages: continue url f{start_url}page{page} resp fetch_page(url) if resp is None: continue # 失败页暂时跳过最后再补 items parse_list_page(resp.text) results.extend(items) save_progress(page) # 每成功一页就记录 sleep(REQUEST_INTERVAL) return results def load_progress(): # 读取本地进度文件返回已完成页码集合 try: with open(progress.json, r) as f: return set(json.load(f)) except FileNotFoundError: return set()断点续爬的价值在于采集 100 页商品列表时网络波动只中断了第 78 页重启后直接从第 79 页继续而不是重新抓 77 页。这个设计在数据量大时能省下大把时间和请求额度。要注意save_progress的成功条件必须等这一页的解析结果也正常返回后再记录否则会出现页码已记录但字段为空的情况。4. 分析与可视化把销售数据变成能直接做决策的图表数据爬下来只是第一步真正让这套系统有价值的是分析和可视化部分。这一章把聚合口径、图表选型和交互筛选讲清楚让图表能直接回答业务问题而不是为了好看摆一堆图。4.1 聚合口径怎么定按日、按SKU、按类目的三种对比同一份数据口径不同结论可能相反。比如按天看总销量可能周中平稳、周末冲高按SKU看单品的销量分布可能发现头部商品集中了大部分销售额。所以分析模块要支持多口径切换而不是写死一种。import pandas as pd def load_sales_data(db_pathsales.db): import sqlite3 conn sqlite3.connect(db_path) df pd.read_sql_query(SELECT * FROM products, conn) conn.close() return df def aggregate_by_category(df): # 按类目聚合销量和销售额销售额 价格 * 销量 df[total_revenue] df[price] * df[sales_count] result df.groupby(category).agg( total_sales(sales_count, sum), total_revenue(total_revenue, sum), avg_price(price, mean), ).reset_index() return result.sort_values(total_revenue, ascendingFalse)聚合并的groupby后面跟多个聚合函数是常用手法一次算出销量合计、销售额合计、平均价格三个指标。这里有个重要参数reset_index()必须写否则category会留在索引里后续绘制图表时传数据会多一道麻烦。聚合结果出来后可以继续做排序取 Top 10 类目绘图避免饼图上出现十几张小扇区既看不清也难交互。4.2 图表配置参数什么业务问题配什么图图表选型的原则是先问业务问题再选图表类型。要看出销量随时间的波动趋势用折线图和柱状图要看类目占比用饼图或环形图要看商品单品排名用横向柱状图。图不是越多越好一屏顶多放五六个关键图再多就是噪音。from pyecharts.charts import Bar, Line from pyecharts import options as opts def render_trend_chart(df, output_pathtrend.html): # df 需包含 date 和 daily_sales 两列 line ( Line() .add_xaxis(df[date].tolist()) .add_yaxis(日销量, df[daily_sales].tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title商品日销量趋势), yaxis_optsopts.AxisOpts(name销量), xaxis_optsopts.AxisOpts(name日期, axislabel_optsopts.LabelOpts(rotate45)), ) ) line.render(output_path)is_smoothTrue是折线平滑开关适合数据波动较大时看整体走势但也会掩盖尖峰细节做异常检测时建议关闭。axislabel_opts里的rotate参数用于日期标签旋转日期超过 10 个时标签会重叠旋转 45 度基本能解决。渲染方式是输出独立 HTML 文件方便通过任意浏览器打开和转发给运营同事。4.3 多图表联动的筛选器实现静态图表之间是孤立的想看“近 14 天某类目的销量趋势”要么重新计算重新出图要么引入联动筛选。pyecharts的Tab或Grid组件能实现基础联动更彻底的方案是给图表页加自定义筛选控件。from pyecharts.charts import Tab tab Tab() bar Bar().add_xaxis([类目A, 类目B, 类目C]).add_yaxis(月销量, [120, 200, 150]) line Line().add_xaxis([1月, 2月, 3月]).add_yaxis(月销售额, [8000, 12000, 9500]) tab.add(bar, 类目销量) tab.add(line, 销售额趋势) tab.render(dashboard.html)Tab的好处是把多个图表放进同一页面顶部可以切换视图不用滚动一长页。但如果业务方需要“选择一个类目所有图表同时刷新”的联动效果Tab就不够用了需要用Flask加ECharts实现图表数据通过接口动态读取。对大多数中小型销售分析场景Tab方案已经能覆盖日常需求不建议一上来就上全套前后端联动维护成本会翻倍。5. 源码跑通的避坑指南环境、编码与反爬的常见问题排查这一章是实战里最容易卡住人的地方我把高频问题按“现象 → 原因 → 解决”整理成五条照着排查能省不少时间。5.1 依赖版本冲突与缺库导致导入失败现象运行main.py直接报ModuleNotFoundError: No module named pyecharts或者某个库安装后导入时报ImportError提示cannot import name xxx from yyy。原因多数是环境混乱导致的版本不匹配。比如pandas的旧版本和新版本接口不同pyecharts1.x 和 0.5.x 的导入方式完全不同混装后代码直接炸。解决先检查当前环境的包版本用pip freeze对比源码附带的requirements.txt。建议直接为这个项目创建独立虚拟环境别在全局环境里装。创建命令如下python -m venv sales_env # Windows 激活 sales_env\Scripts\activate # Linux/macOS 激活 source sales_env/bin/activate pip install -r requirements.txt创建虚拟环境是这类项目最值得养成的习惯。虚拟环境可以把依赖隔离在项目内部即使其他项目装了不同版本的依赖互不影响。如果requirements.txt里的版本和当前 Python 版本不兼容优先调整 Python 版本而不是强行降低依赖版本后者容易引入其他兼容性问题。5.2 爬虫采不到数据时的三层排查路径现象运行爬虫模块没报错但存进数据库的product_name、price全是空字符串或者列表长度为零。原因通常是三个层面之一出了问题页面结构发生变化、请求被反爬拦截、解析选择器失效。解决按顺序排查输出中间结果定位问题。resp fetch_page(test_url) if resp is None: print(请求层失败检查headers、Cookie和IP状态) else: print(状态码:, resp.status_code) # 把页面保存到本地手动检查返回内容 with open(debug_page.html, w, encodingutf-8) as f: f.write(resp.text)页面保存到本地后用编辑器搜索商品名称看看返回的 HTML 里到底有没有数据。如果页面内容正常但解析结果为空说明选择器失效了如果页面内容本身是验证码或空白页说明请求层被拦需要更新User-Agent、加Cookie或调整采集频率。这三步排查法写进脚本里每次采集失败都能快速定位环节。5.3 中文乱码与图表演示字体缺失的处理现象采集到的商品名称在数据库里显示正常但写入 HTML 图表后变成一堆“方块”或问号或者爬虫抓回的页面里中文全部变成乱码。原因两类问题路径不同。爬虫乱码是页面编码识别错误常见于页面声明gb2312但实际返回gbk或者请求时resp.text自动猜测编码失败图表方块是运行环境缺少中文字体pyecharts在生成图片时找不到中文字体文件。解决爬虫乱码用resp.apparent_encoding修正编码图表字体问题在代码里显式指定字体路径。# 爬虫编码修正 resp.encoding resp.apparent_encoding # 使用自动检测的编码 # 图表指定中文字体 from pyecharts.globals import CurrentConfig, ThemeType CurrentConfig.ONLINE_HOST https://cdn.example.com/echarts字体缺失的处理在不同系统上有差异。在服务器上部署时最好先把系统中文字体安装好再生成图表。判断字体是否缺失有一个简单办法生成的 HTML 用浏览器打开如果浏览器端正常但导出图片时方块说明是服务器端字体问题如果浏览器里也是方块那可能是 CDN 资源加载失败或渲染配置问题。5.4 SQLite 数据库文件被占用导致写入失败现象爬虫采集中途报database is locked程序直接退出已采集的数据没保存。原因SQLite 单文件数据库同时只能有一个写连接可能是上一次运行残留的进程没关闭也可能是爬虫模块中多个页面并发写入导致锁冲突。解决检查是否有残留 Python 进程并在代码里显式控制连接开关。conn sqlite3.connect(sales.db) # 设置等待时间和超时避免锁冲突后立即报错 conn.execute(PRAGMA busy_timeout 5000) try: # 写入数据的事务操作 conn.execute(INSERT INTO products (...) VALUES (?,?,?,?), record) conn.commit() finally: conn.close()busy_timeout 5000表示遇到锁时最多等待 5 秒超时后才报错。这个参数在单文件数据库里很实用能吸收短时的锁冲突。另外批量插入时别每行数据都commit()一次攒够几十条再统一提交写入速度能提升好几倍。5.5 定时任务不触发或重复触发现象配置了每日自动采集但第二天发现没有新数据入库或者一次任务被重复执行了多次。原因常见的定时工具schedule库的阻塞特性被忽略代码里while True循环被中断或者主程序在执行长任务时下一次调度时间已经到了导致任务叠加。解决用schedule时确认循环没被break并把任务函数设置为幂等配合threading.Lock防止并发执行。import schedule import threading lock threading.Lock() def job_with_lock(): if lock.acquire(blockingFalse): try: run_crawler() finally: lock.release() schedule.every().day.at(08:00).do(job_with_lock) while True: schedule.run_pending() time.sleep(1)blockingFalse的目的是当上一次任务还没结束时下一次调度直接跳过而不是排队等待避免了日志里出现大量重复数据。time.sleep(1)控制每次调度的检查间隔间隔太短 CPU 空转太长会延迟任务触发1 秒钟是个平衡值。6. 进阶玩法把固定看板改造成定时更新的销售监控跑通基础流程后最值得做的改进是让系统自动运转定时采集、参数化复用、输出定期校验。这个阶段不增加新依赖只是把已有代码组织得更工程化。6.1 用 schedule 实现每日增量采集把爬虫和分析绑定在同一次任务里每天自动执行生成新的 HTML 看板。数据表里保留历史记录趋势图自动包含新一轮的数据点。def daily_job(): print(开始每日采集任务) crawl_all_pages(START_URL, total_pages10) df load_sales_data() trend_df aggregate_by_category(df) render_trend_chart(trend_df) print(任务完成, 看板已更新) # 每周一至周五上午 9 点执行 schedule.every().monday.at(09:00).do(daily_job) schedule.every().tuesday.at(09:00).do(daily_job) schedule.every().wednesday.at(09:00).do(daily_job) schedule.every().thursday.at(09:00).do(daily_job) schedule.every().friday.at(09:00).do(daily_job) while True: schedule.run_pending() time.sleep(1)节假日和平台大促期间每日定时任务往往不够可以临时调整执行频率到每 6 小时一次。把执行计划也提取成配置项改频率时不动任务函数本身更利于维护。6.2 参数化复用一份代码切换不同店铺与时间段把店铺 ID、时间范围、类目过滤条件抽成参数后同一套分析代码可以服务多份报表。比如运营提“对比 A 和 B 两个类目上周的表现”只需要重新传参调用不用复制粘贴代码。def build_dashboard(category_filterNone, days30): df load_sales_data() if category_filter: df df[df[category] category_filter] # 只保留近 N 天的数据 df[date] pd.to_datetime(df[crawled_at]).dt.date cutoff_date pd.Timestamp.today().date() - pd.Timedelta(daysdays) df df[df[date] cutoff_date] render_trend_chart(df, output_pathfdashboard_{category_filter or all}.html) return fdashboard_{category_filter or all}.htmldays30和category_filterNone是默认参数调用时不传就是全部类目最近 30 天。按日期筛选时用pd.Timedelta做减法比手动计算日期更可靠跨月时也不会出错。6.3 验证输出可靠性的三个习惯数据看板的输出直接用来做业务判断可靠性很重要。我养成了三个习惯首次采集后随机抽查 10 个商品手动打开页面核对价格和销量是否一致每次更新看板后对比总记录数和前一天是否在合理范围内增长每周导出一份 Excel 发给业务方交叉验证确认图表里的结论和业务感知一致。注意数据分析系统的价值不在代码写得多么花哨而在数据源稳定、口径一致、输出可验证。如果采集到的价格和实际成交价有差距再精美的图表也只会误导决策。这些习惯帮我避免过不少翻车场面。最惨的一次是某次页面改版导致价格字段解析错了单位从“元”变成了“角”看板上的销售额凭空放大了十倍要不是对照明细数据及时发现那周的业务汇报就要带错误数据上场。从此之后断言和抽查成了我每次跑数之后的保留动作。希望这套方案的拆解和避坑内容能帮到你少踩一点我已经踩过的坑。本文还有配套的精品资源点击获取