Python商品销售数据分析可视化系统:爬虫+Pandas+ECharts实战
简介面向毕业设计场景这份基于Python的商品销售数据分析可视化系统源码包采用Django框架搭建内置商品数据采集爬虫与多维度可视化展示模块适合Python初学者、高校学生及需要快速搭建完整项目的毕设开发者参考使用。压缩包共收录1480个文件核心逻辑以Python脚本py/pyc为主前端涉及HTML、CSS/SCSS/LESS样式表及JavaScript脚本附带大量图表图片、字体资源、JSON/XML配置以及PSD设计源文件整体容量约17.68MB目录分层清晰便于按功能模块检索。源码已经本地编译验证配置相应环境后即可运行覆盖数据爬取、清洗、分析到可视化呈现的完整链路可直接作为毕业论文实现基础前端模板与样式文件预留了调整空间设计源文件也方便二次美化。目前已有181人学习下载适合用于毕业设计演示、课程项目参考或电商数据分析方向的功能拓展。1. 商品销售数据分析为什么要带爬虫数据不落地分析成空谈做商品销售数据分析的人多半有过这种经历运营手里有一堆商品ID但销售明细散在后台或网页上导出要权限、要等排期就算导出来也是脏数据得自己清洗。基于Python的商品销售数据分析可视化系统带爬虫这套源码要解决的就是从“网页上的商品数据”到“可视化的分析看板”这条链路爬虫负责把商品页面的销量、价格、评论数抓下来Pandas负责算销售额和增长率最后用ECharts把结果画成图表。它的定位不是炫技而是给电商运营和数据入门者一套能改、能跑的模板。适合两类人一是想拿真实网页数据做分析的从业者二是刚学完Python基础想做一个完整项目来练手的人。下面我会按爬虫、分析、可视化、排错的顺序把每个环节的关键代码和参数讲透。2. 爬虫采集层用 requests XPath 把商品页面变成结构化数据2.1 爬虫选型为什么是 requests XPath 而不是 Scrapy很多源码包里爬虫用的是 Scrapy但 Scrapy 是一个完整的框架有引擎、调度器、下载器、Item Pipeline学习成本高调试起来也更绕。这个项目的目标是做商品销售分析不是做采集平台所以用 requests lxml 的 XPath 更直接。requests 负责发 HTTP 请求lxml 负责解析 HTML两者加一起代码量不到 Scrapy 的一半而且每一步都能在终端里打印出来看结果。选型标准要按数据量和反爬强度来定。如果只是单机每天抓几百到几千条商品数据requests 完全够用如果要做分布式采集、断点续爬、多线程调度那才需要换 Scrapy。另一个常见做法是加一个 session 保持登录态requests.Session 会自动管理 Cookie遇到需要登录的商品后台比每次重新构造 Cookie 头省事。还有一点要注意requests 不是万能的遇到滑块验证码或加密参数它仍然解决不了。那种情况要换 Playwright 或 Selenium但对商品销售数据这种不涉及用户行为的页面用 requests 是性价比最高的。网络爬虫原理大家都懂但真正落地时选轻量工具能少走很多弯路。2.2 最小可用的爬虫代码商品列表页解析与翻页循环先给一个能跑的完整脚本它做的事情是循环抓取商品列表页从每个商品节点里提取标题、价格、销量最后写入 CSV。假设页面结构是每个商品在一个div.goods-item节点里价格在span.price销量在span.sales。import requests from lxml import html import time import re import csv HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 , Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9 } def fetch_page(page_no): # 这里用示例域名实际使用时替换成你自己的目标页面 url fhttps://example.com/list?page{page_no} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 如果页面是GBK编码手动指定否则后续解析会乱码 if resp.encoding.lower() in (gbk, gb2312): resp.encoding gbk return resp.text def parse_goods(html_text): tree html.fromstring(html_text) items tree.xpath(//div[classgoods-item]) result [] for item in items: title item.xpath(.//h3/a/text()) price item.xpath(.//span[classprice]/text()) sales item.xpath(.//span[classsales]/text()) result.append({ title: title[0].strip() if title else None, price: parse_number(price[0]) if price else 0.0, sales: parse_sales(sales[0]) if sales else 0, }) return result def parse_number(text): match re.search(r\d\.?\d*, text) return float(match.group()) if match else 0.0 def parse_sales(text): # 销量文本常见两种“已售500”和“1.2万” match re.search(r[\d.], text) if not match: return 0 value float(match.group()) if 万 in text: value * 10000 return int(value) def main(): all_data [] # 抓前10页实际使用根据总页数调整 for page in range(1, 11): print(ffetching page {page}) try: html_text fetch_page(page) all_data.extend(parse_goods(html_text)) except requests.RequestException as e: # 单独一页失败不要中断整个采集任务 print(fpage {page} failed: {e}) # 基础限速避免给对方服务器太大压力 time.sleep(1) with open(goods.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, price, sales]) writer.writeheader() writer.writerows(all_data) print(fsaved {len(all_data)} rows) if __name__ __main__: main()这段代码虽然简短但把几个关键参数都覆盖了。timeout10防止某个页面卡死导致整个任务挂起resp.raise_for_status()会把 404、500 这类错误变成异常方便你定位是哪一页出了问题time.sleep(1)是最简单的限速如果你的目标站对频率不敏感可以降到 0.5但建议不要低于 0.3。这里有个新手很容易忽略的点item.xpath(.//h3/a/text())前面的.表示从当前节点开始查找而不是从整个文档根节点重新查。如果去掉这个点你会把页面里所有h3里的标题都混进来结果串行。2.3 数据清洗与落库csv/sqlite 怎么选爬下来的数据不能直接拿去分析常见的脏数据有标题重复、价格为 0、销量带“万”字没换算、编码乱码。所以清洗这一步必须做。我一般会把数据先落成 CSV再用 Pandas 读进来处理处理完再写入 SQLite。这样做的好处是 CSV 作为原始备份万一清洗逻辑改坏了还能从头再来。import pandas as pd import sqlite3 # 读原始CSV注意编码要和写入时一致 df pd.read_csv(goods.csv, encodingutf-8-sig) # 去重同标题同价格视为同一商品 df df.drop_duplicates(subset[title, price]) # 过滤掉价格为0或销量为负的异常数据 df df[df[price] 0] df df[df[sales] 0] # 类型转换避免后续计算时出现字符串拼接 df[price] df[price].astype(float) df[sales] df[sales].astype(int) # 写入SQLite单文件无需额外服务 conn sqlite3.connect(goods.db) df.to_sql(goods, conn, if_existsreplace, indexFalse) conn.close() print(df.shape)为什么用 SQLite 而不是 MySQL因为这套源码的目标是本地能跑SQLite 是一个文件拷走就能用不需要安装数据库服务。当数据量到了几百万行或者你打算做 web 多用户访问再迁移到 MySQL 不迟。if_existsreplace表示每次重新采集后覆盖旧表适合做全量快照如果你要保留历史改成if_existsappend并在表里加一个采集时间字段。清洗这一步还要注意drop_duplicates 不传参数时是全行去重但爬虫每次抓到的评论数可能变所以要用subset指定业务主键。如果目标平台没有唯一商品ID用“标题价格”组合已经能覆盖 90% 的情况。3. 数据分析层Pandas 算销售指标别在循环里算总数3.1 销售数据分析的核心指标销售额、销量、增长率和环比商品销售数据分析最基础的四件事总销售额是多少、哪些商品卖得好、销售趋势是涨还是跌、价格和销量之间有没有关系。如果只有当前快照算不出同比和环比因为你需要多天的数据。所以源码包里通常会配套一份“采集历史”逻辑——每天跑一次爬虫把数据追加进 SQLite这样积累一周后就能画趋势线。在只有一份快照的前提下能算的指标包括各商品销售额贡献占比、价格带分布、销量 Top N、平均单价。这些指标足够支撑一个商品运营周报。如果数据里已经有crawl_date字段每次采集时自动填当天日期那就能按天聚合出销售额曲线。我见过很多人一上来就写两层 for 循环算总数数据量几千行还好几万行就把内存吃满了。Pandas 的 groupby 是向量化操作一次聚合解决而且代码更短。下面这部分重点讲怎么用 groupby 和 to_datetime 把日期和分组处理好。3.2 用 Pandas 做日期处理与分组聚合先看代码。假设 SQLite 里的表有title、price、sales、crawl_date其中crawl_date是字符串2025-03-20这样的格式。import pandas as pd import sqlite3 conn sqlite3.connect(goods.db) df pd.read_sql_query(SELECT * FROM goods, conn) conn.close() # 日期字段统一转成 datetime才能按天分组 df[crawl_date] pd.to_datetime(df[crawl_date], format%Y-%m-%d, errorscoerce) # 计算销售额 df[revenue] df[price] * df[sales] # 总览指标 total_revenue round(float(df[revenue].sum()), 2) total_sales int(df[sales].sum()) avg_price round(float(df[price].mean()), 2) # 按日汇总销售额 daily df.groupby(df[crawl_date].dt.date)[revenue].sum().reset_index() daily.columns [date, revenue] # 按商品汇总销量和销售额取前10 top10 ( df.groupby(title)[[sales, revenue]] .sum() .reset_index() .sort_values(sales, ascendingFalse) .head(10) ) print(总销售额:, total_revenue) print(总销量:, total_sales) print(top10)关键点有三个。第一pd.to_datetime一定要指定format不指定虽然也能解析%Y-%m-%d但遇到“2025/03/20”或“2025年3月20日”就容易报错或解析错误。errorscoerce表示无法解析的变成NaT后续 groupby 会自动忽略不会让整个程序崩溃。第二groupby(df[crawl_date].dt.date)是按“日期部分”分组而不是按“日期时间”分组。如果不加.dt.date时间戳里带了00:00:00还好说如果采集时间是2025-03-20 14:32:11分组就会精确到秒导致同一天的数据被拆成几十组。第三groupby 之后必须reset_index()把分组键变成普通列。很多人拿到 Series 后直接传给前端发现顺序不对或者没法索引根源就在这里。reset_index()之后你得到的是一个标准的 DataFrame列名还能自己改。3.3 数据分析结果怎么和可视化衔接Pandas 算出来的结果不能直接塞进 HTML前端图表库需要 JSON 数组。最常见的做法是用to_dict(orientrecords)把每一行变成一个字典组一个列表。# 转成前端能用的JSON结构 trend_data daily.to_dict(orientrecords) top10_data top10.to_dict(orientrecords) payload { total_revenue: total_revenue, total_sales: total_sales, avg_price: avg_price, trend: trend_data, top10: top10_data, } # 如果不需要经过Flask可以直接存成JSON文件 import json with open(analysis_result.json, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse, indent2)orientrecords会生成[{sales:100,title:xxx}, ...]这种结构和 ECharts 的 data 字段天然匹配。ensure_asciiFalse让 JSON 文件里的中文可读否则会变成\u5427这种转义字符虽然前端也能解析但调试时很痛苦。到这个阶段你已经完成了从数据库到指标计算的闭环。下一步要解决的是怎么把这些指标展示给业务人员看也就是可视化层。这里不建议用 Matplotlib 画静态图因为它的交互能力和网页集成都很弱。4. 可视化层用 Flask ECharts 把分析结果变成看板4.1 选型为什么推荐 ECharts 而不是 MatplotlibMatplotlib 适合生成报告里的静态图Excel 截图粘一下完事但它不适合做看板。你看“可视化大屏”诉求往往是要交互鼠标悬停看到具体数字、时间轴拖拽、点击图例筛选品类。ECharts 开发时只需引入一个 JS 文件配置项是 JSON和 Python 后端天然解耦。pyecharts 也能用但它相当于在 Python 里拼 JS 字符串一旦要自定义样式你还得去看生成的 JS多了中间一层。常见做法是 Flask 提供/api/overview、/api/trend这类 JSON 接口前端页面放在 Flask 的templates或static目录下由 Flask 一起托管。这样浏览器访问的是同一个域名规避了跨域问题。如果你前端用 webpack 单独开发那才需要配 CORS。4.2 后端 Flask 提供 JSON 接口的代码骨架一个最小可用的 Flask 应用长这样from flask import Flask, jsonify, render_template import pandas as pd import sqlite3 app Flask(__name__) def load_data(): conn sqlite3.connect(goods.db) df pd.read_sql_query(SELECT * FROM goods, conn) conn.close() df[revenue] df[price] * df[sales] return df app.route(/api/overview) def overview(): df load_data() return jsonify({ total_revenue: round(float(df[revenue].sum()), 2), total_sales: int(df[sales].sum()), avg_price: round(float(df[price].mean()), 2) }) app.route(/api/trend) def trend(): df load_data() df[crawl_date] pd.to_datetime(df[crawl_date], errorscoerce) daily df.groupby(df[crawl_date].dt.date)[revenue].sum().reset_index() return jsonify(daily.to_dict(orientrecords)) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)注意几个参数。host0.0.0.0允许局域网内其他设备访问如果你只是本机调试改成127.0.0.1更安全。debugFalse必须关掉否则监听的接口会带一个 debugger PIN别人可能通过调试器执行任意代码。每次请求都重新读数据库确实慢但数据量小时无所谓等数据量变大可以加一个简单的内存缓存后面第 6 章再讲。Flask 的render_template会去templates目录找index.html所以你的前端文件要和 Python 文件放在对应目录结构下。很多源码包会把这个做成 Flask 静态文件放在一起直接双击启动这也是它比较好上手的原因。4.3 前端 ECharts 渲染看板的必要配置前端页面核心是取/api/overview和/api/trend然后把数据塞进 ECharts 的setOption。下面是最简模板!DOCTYPE html html langzh-CN head meta charsetUTF-8 title商品销售看板/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body h2销售趋势/h2 div idtrendChart stylewidth:100%;height:400px;/div script const chart echarts.init(document.getElementById(trendChart)); fetch(/api/trend) .then(res res.json()) .then(data { const dates data.map(d d.date); const revenues data.map(d d.revenue); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ name: 销售额, type: line, smooth: true, data: revenues }] }); }); /script /body /html这里最容易踩的坑是容器没有高度。echarts.init要求目标 DOM 有明确的width和height如果style里没写高度图表会渲染成 0x0。另一个坑是fetch的地址如果前端直接用文件协议打开file://fetch 会跨域失败所以一定要通过 Flask 的地址http://127.0.0.1:5000访问。ECharts 的配置项还有很多比如grid控制图边距legend控制图例tooltip决定悬停提示。对于销售数据看板我习惯把tooltip的trigger设为axis这样悬停到任意一天能看到那天的销售额。线图加smooth: true让曲线平滑但如果你要表现真实波动不建议加它会掩盖突变点。5. 避坑指南从爬虫到看板最容易翻车的 5 个问题5.1 爬虫请求被拒绝Headers 和 Cookie 的坑现象requests 请求首页正常但翻到第二页或点击详情时返回 403或者页面内容变成验证码。原因服务器不信任你的请求。很多网站只检查 User-Agent但更严格的反爬会检查 Accept、Referer、Cookie 的完整性以及请求频率。两次请求间隔太短IP 被临时封禁也是常见原因。解决第一把 Headers 补全至少带上 User-Agent、Accept、Accept-Language。第二用requests.Session()保持会话这样第一次登录或访问时种下的 Cookie 会在后续请求中自动带上。第三加上重试机制遇到失败等几秒再试试。import requests import time from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry session requests.Session() retry Retry(total2, backoff_factor1, status_forcelist[500, 502, 503]) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) HEADERS { User-Agent: Mozilla/5.0 ..., Accept: text/html, Referer: https://example.com/ } resp session.get(https://example.com/list?page2, headersHEADERS, timeout10)Retry(total2, backoff_factor1)表示重试 2 次每次等待 1 秒后递增。status_forcelist指定哪些状态码触发重试。注意backoff_factor不是固定时间是 1 秒、2 秒、4 秒这样指数递增。5.2 XPath 提取文本为空text() 和 .//text() 的区别现象用//span[classprice]/text()提取价格返回空列表但浏览器里明明能看到价格。原因目标节点的文本不在直接子层级而在更深的子标签里。比如真实结构是span classpriceem98/em元/span/text()只能取到“元”而价格“98”在em标签里。解决改用.//text()取所有后代文本然后拼起来price_parts item.xpath(.//span[classprice]//text()) price_text .join(price_parts).strip()xpath(.//text())返回一个列表比如[98, 元]用join拼成98元再用正则提取数字。这个坑在爬虫解析里非常高频尤其是定位到商品列表时几乎每个字段都可能有嵌套标签。5.3 日期字符串解析报错to_datetime 格式现象pd.to_datetime(df[crawl_date])直接报错或者解析出来年份是 25 年而不是 2025 年。原因字符串格式不统一。比如有的行是2025-03-20有的是2025/03/20还有的是20-3月-2025。Pandas 默认只认 ISO 格式遇到混合格式会尝试猜测猜不出来就报错。年份两位数的字符串会被解析为 2025 年还是 2055 年取决于 Pandas 的yearfirst参数默认很坑。解决先df[crawl_date].astype(str).str[:10]看前 10 个样本是什么格式然后统一formatdf[crawl_date] pd.to_datetime(df[crawl_date], format%Y-%m-%d, errorscoerce)如果源头混杂了多种格式errorscoerce会把解析失败的行变成NaT你再用df[crawl_date].dt.date.isna()找出这些行来修复。不要直接dropna先看看坏数据的分布有时候是爬虫字段错位清洗掉会造成缺失。5.4 中文乱码requests 的 encoding 处理现象爬回来网页用print(resp.text)正常写进 CSV 用 Excel 打开却是乱码。原因requests 用 HTTP 响应头里的 charset 判断编码但很多网站响应头写的是text/html;charsetUTF-8实际内容却是 GBK。resp.text在这种场景下可能已经乱码写进 CSV 后再乱一层。Excel 打开 CSV 时默认按 GBK 解析如果是 UTF-8 编码的文件中文也会乱。解决用resp.apparent_encoding检测或直接根据站点历史指定resp.encoding resp.apparent_encoding # 基于页面内容探测慢一点但更准更稳妥的做法是拿到响应后先看前几百字节判断有没有 GBK 特征然后写死。写 CSV 时用encodingutf-8-sig它会在文件头加 BOMExcel 打开就不会乱码df.to_csv(goods_cleaned.csv, indexFalse, encodingutf-8-sig)5.5 看板接口跨域或端口冲突现象前端页面报错Access-Control-Allow-Origin或者 Flask 启动时报Address already in use。原因前端通过file://打开 HTML 时fetch(/api/trend)请求的 host 是file://不是 Flask 的127.0.0.1:5000跨域被浏览器拦截。端口冲突是因为上一次启动的 Flask 进程没关干净。解决前端文件放 Flaks 的templates目录通过http://127.0.0.1:5000访问。如果必须前后端分离给 Flask 加一段 CORS 中间件但本地调试不建议因为 security 风险大。端口占用时手动杀掉旧进程lsof -i :5000 kill -9 PID如果你换端口启动参数改成app.run(port5001)同时前端 fetch 地址也要跟着改否则依然是跨域错误。6. 进阶把系统做成可持续运行的数据管道这套源码如果只是手动跑一次价值会缩水一半。真正能让销售分析发挥作用的是每天定时采集、增量入库、看板自动刷新。我一般会在爬虫脚本里加一个crawl_date字段每次运行填入当天日期然后用 APScheduler 或系统 crontab 触发。from apscheduler.schedulers.blocking import BlockingScheduler import subprocess def job(): # 每天凌晨2点执行采集和清洗 subprocess.run([python, spider.py], checkTrue) subprocess.run([python, clean.py], checkTrue) print(job done) if __name__ __main__: scheduler BlockingScheduler() scheduler.add_job(job, cron, hour2, minute0) scheduler.start()hour2, minute0选凌晨是因为商品页面访问量低反爬触发概率小。如果你在 Windows 上也可以用计划任务macOS 和 Linux 上 crontab 更轻。定时任务跑一段时间后你会发现看板接口越来越慢因为每次请求都全表读取。我常用一个土办法把分析结果 json 存成文件每次爬虫跑完更新一次Flask 接口直接读文件。数据量百万以下这个方案比 Redis 简单得多。验证数据是否正确是最容易被忽略的环节。我习惯每次爬完随机抽 5 条商品手动打开页面比对标题、价格、销量。比对程序很简单import random checked df.sample(5) for _, row in checked.iterrows(): print(row[title], row[price], row[sales])如果连续三次抽样都一致说明 XPath 没写错如果隔一周再跑时对不上多半是页面改版了XPath 失效。这个经验我吃过亏——今天能跑的爬虫下周可能就静默失败所以日志和异常告警一定要留。我给爬虫加日志的方式是先写文件再打印import logging logging.basicConfig(filenamespider.log, levellogging.WARNING)这样不用每天盯着终端出了事看spider.log最后几行就知道是哪个页面挂了。如果你刚拿到这套源码我建议先让它跑三天每天留下 csv 快照然后对比相邻两天的goods.db行数和销售额如果行数差太多说明爬虫漏页或反爬拦截得去修 XPath 和请求频率。数据正确性永远比图表漂亮重要这个顺序别搞反了。希望帮到你动手跑一遍比看一万个字有用。本文还有配套的精品资源点击获取