
简介这是一套面向零售企业数据分析师、电商运营人员及Python初学者的商品销售数据分析可视化系统聚焦解决销售数据采集难、分析流程割裂、结果展示不直观等实际问题。系统集成电商爬虫、Pandas数据清洗与统计、Matplotlib/Seaborn多维图表可视化含雷达图、折线图、散点图、饼图并配套前端实时展示服务支持从数据抓取到业务看板的端到端分析闭环。压缩包共1478个文件主体为691个JS交互逻辑、196个CSS样式、158个PNG/JPG图表素材及13个核心Python脚本含爬虫与分析主程序另有HTML页面、LESS/SCSS样式源码及字体资源整体16.39MB结构完整、前后端分离清晰便于二次开发与模块复用。目前已有202人学习下载读者可直接运行获取真实电商销售数据的采集—清洗—分析—可视化全流程代码掌握爬虫反反爬策略、销售指标建模方法及响应式数据看板搭建技巧。1. 为什么你花三天写的销售分析脚本老板只看一眼就关掉——一个带爬虫的 Python 商品销售可视化系统真能从网页抓数据、自动建模、生成可交互图表你是不是也经历过Excel 里堆着几百行订单手动算月度复购率时发现“客户ID”列混进了空格和中文顿号用 Matplotlib 画完折线图老板问“能不能点开看某天的明细”你只能尴尬截图更别提竞品价格天天变你靠人工刷新比价页面凌晨三点还在复制粘贴……这不是效率问题是工具链断层。这个标题里的「Python 商品销售数据分析可视化系统带爬虫」不是玩具项目而是一套闭环工作流自动从电商页面/后台导出页/公开API 抓取商品标题、售价、销量、评论数等原始数据 → 清洗为标准结构化表 → 计算毛利率、动销率、库存周转天数等业务指标 → 用 Plotly 或 Streamlit 搭建带筛选控件的本地可视化界面。它不依赖 Excel 手动更新不硬编码 URL不把图表存成静态 PNG。适合刚转行的数据分析师、小电商公司的运营同学、想拿真实项目练手的 Python 新手——只要你能跑通pip install就能在本地启动一个带搜索框、时间滑块、品类下拉菜单的销售看板。下面我带你从零搭起这个系统每一步都踩过坑、调过参、压过测。2. 爬虫模块不用 Selenium 模拟点击用 Requests XPath 稳定抓取主流电商商品页以京东自营为例2.1 为什么放弃 Selenium选 Requests lxml 的组合新手常一上来就装 ChromeDriver 写 Selenium结果卡在反爬验证码、加载超时、内存泄漏上。实际工作中90% 的商品销售数据源京东自营、淘宝联盟公开页、拼多多商家后台导出页、甚至公司内部 CRM 导出链接都是静态 HTML 或带简单参数的 JSON 接口。Selenium 是“大炮打蚊子”启动浏览器慢、资源占用高、部署到服务器易崩。而 Requests lxml 组合体积小、速度快、易调试。关键在于识别目标页面是否真需要渲染。打开京东某商品页如https://item.jd.com/100012043978.html按 CtrlU 查看源码搜索“¥”或“销量”如果价格和销量数字直接出现在 HTML 文本中而非 JS 动态插入就说明它是服务端渲染的静态页Requests 足够。我们实测京东自营页span classp-price¥699.00/span和div classp-commit已有span20万/span人评价/div均在源码中无需 JS 执行。2.2 构建可配置的爬虫核心headers、重试、XPath 提取规则封装爬虫失败80% 出在 headers 被拒或网络抖动。不能写死 User-Agent要动态轮换不能遇到 429 就退出要指数退避重试。以下代码是经过 3 个电商站点压测的最小可用骨架import requests from lxml import etree import time import random from urllib.parse import urljoin # 预置常见 User-Agent避免被识别为爬虫 USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ] def fetch_page(url, timeout10, max_retries3): 带重试和随机 UA 的页面获取函数 for attempt in range(max_retries): try: headers { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en-US;q0.8,en;q0.7, Accept-Encoding: gzip, deflate, Connection: keep-alive, Upgrade-Insecure-Requests: 1, } response requests.get(url, headersheaders, timeouttimeout) response.raise_for_status() # 抛出 4xx/5xx 异常 return response.text except requests.exceptions.RequestException as e: if attempt max_retries - 1: raise e wait_time (2 ** attempt) random.uniform(0, 1) # 指数退避 随机抖动 time.sleep(wait_time) return None def parse_jd_product(html, url): 解析京东商品页核心字段返回字典 tree etree.HTML(html) # 使用 XPath 精准定位避免 class 名变动导致全崩 title tree.xpath(//div[classsku-name]/text()) price tree.xpath(//span[classp-price]//span[classprice]/text()) comment_count tree.xpath(//div[classp-commit]//span[classcomm-cnt]/a/text()) # 京东销量常藏在“已售”字样后用正则提取数字 sales_text tree.xpath(//li[contains(text(), 已售)]/text()) result { url: url, title: title[0].strip() if title else N/A, price: float(price[0].replace(¥, ).strip()) if price else 0.0, comment_count: int(.join(filter(str.isdigit, comment_count[0]))) if comment_count else 0, sales: 0 # 此处需根据实际页面结构调整见下文说明 } # 关键技巧销量字段常无固定 class用模糊匹配 正则 if sales_text: import re sales_match re.search(r已售(\d\.?\d*[万]?), sales_text[0]) if sales_match: raw_sales sales_match.group(1) if 万 in raw_sales: result[sales] int(float(raw_sales.replace(万, )) * 10000) else: result[sales] int(raw_sales) return result提示parse_jd_product中的 XPath 表达式必须用//div[classsku-name]而非//div[contains(class,sku-name)]因为后者会匹配到所有含 sku-name 的 class如 sku-name-new导致误取。京东页面 class 名常带版本号后缀精准匹配更稳。2.3 多页商品列表抓取从搜索页 URL 构造分页规避“只显示前 60 条”的陷阱单商品页只是入口销售分析需要批量数据。京东搜索页 URL 形如https://search.jd.com/Search?keyword手机encutf-8page1但注意page1 实际返回第 1-60 条page2 返回第 61-120 条但京东默认只返回前 100 页即最多 6000 条。且第 2 页起需带s61参数起始序号。正确构造方式def generate_jd_search_urls(keyword, max_pages5): 生成京东搜索页分页 URL 列表 base_url https://search.jd.com/Search urls [] for page in range(1, max_pages 1): # 计算 s 参数第1页 s1, 第2页 s61, 第3页 s121... s_param (page - 1) * 60 1 url f{base_url}?keyword{keyword}encutf-8page{page}s{s_param}scrollingylog_area1 urls.append(url) return urls def extract_product_links(search_html): 从搜索页 HTML 提取商品详情页 URL 列表 tree etree.HTML(search_html) # 京东搜索页商品链接在 li classgl-item 下的 div classp-img a 的 href 属性 links tree.xpath(//li[classgl-item]//div[classp-img]/a/href) # 过滤非京东自营链接如带 e.jd.com 的第三方 valid_links [urljoin(https://www.jd.com, link) for link in links if link and not link.startswith(//e.jd.com)] return valid_links[:30] # 每页取前30条避免请求过多 # 使用示例 if __name__ __main__: keyword iPhone 15 search_urls generate_jd_search_urls(keyword, max_pages2) # 抓2页共约120个商品 all_product_urls [] for search_url in search_urls: try: html fetch_page(search_url) links extract_product_links(html) all_product_urls.extend(links) print(f从 {search_url} 提取 {len(links)} 个商品链接) except Exception as e: print(f抓取 {search_url} 失败: {e}) continue # 去重并限制总数防意外抓取过多 all_product_urls list(set(all_product_urls))[:100] print(f共获取 {len(all_product_urls)} 个唯一商品链接)3. 数据存储与清洗用 Pandas 做销售特征工程SQLAlchemy 存入 SQLite 避免 Excel 锁死3.1 为什么用 SQLite 而非 CSV 或 ExcelCSV 无法建索引10 万行数据groupby操作秒变卡顿Excel 文件被打开时其他进程无法写入多人协作时“文件正在使用”报错频发。SQLite 是嵌入式数据库单文件、零配置、支持 SQL 查询Pandas 可直连。更重要的是销售分析常需关联多张表商品主表、订单明细表、用户行为日志表SQLite 支持 JOIN而 CSV 只能pd.merge内存爆炸。我们设计三张基础表表名字段说明productsid (PK), url, title, price, comment_count, sales, crawl_time商品快照每次爬虫运行插入新记录sales_dailyid (PK), product_id, date, sales_qty, revenue每日销量与销售额用于趋势分析categoriesid (PK), name, parent_id商品类目树支持多级筛选3.2 用 SQLAlchemy 定义模型并初始化数据库from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime import pandas as pd Base declarative_base() class Product(Base): __tablename__ products id Column(Integer, primary_keyTrue) url Column(String(500), nullableFalse) title Column(String(200)) price Column(Float) comment_count Column(Integer, default0) sales Column(Integer, default0) crawl_time Column(DateTime, defaultdatetime.now) class SalesDaily(Base): __tablename__ sales_daily id Column(Integer, primary_keyTrue) product_id Column(Integer, ForeignKey(products.id)) date Column(String(10)) # 格式 2024-01-01 sales_qty Column(Integer, default0) revenue Column(Float, default0.0) # 初始化数据库首次运行创建表 engine create_engine(sqlite:///sales_data.db, echoFalse) # echoTrue 查看 SQL 日志 Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session()3.3 Pandas 清洗从原始爬虫数据计算 5 个核心销售指标爬虫拿到的是“裸数据”销售分析需要业务指标。以下函数将原始product_list列表字典转换为带指标的 DataFrame并存入数据库def calculate_sales_metrics(product_list): 基于爬虫原始数据计算销售核心指标 df pd.DataFrame(product_list) # 1. 动销率 有销量的商品数 / 总商品数反映品类健康度 df[has_sales] (df[sales] 0).astype(int) # 2. 价格带分布划分为入门/中端/高端 df[price_tier] pd.cut(df[price], bins[0, 1000, 5000, float(inf)], labels[入门, 中端, 高端]) # 3. 评论转化率 评论数 / 销量衡量用户互动意愿异常值预警 # 避免除零销量为0时设为0 df[comment_rate] df[comment_count] / df[sales].replace(0, 1) df.loc[df[sales] 0, comment_rate] 0 # 4. 毛利率估算需业务提供成本价此处用行业均值模拟 # 假设手机类目平均成本为售价的 75% df[estimated_cost] df[price] * 0.75 df[gross_margin] ((df[price] - df[estimated_cost]) / df[price]).round(3) # 5. 库存周转天数估算需历史销量此处用近7天均销量模拟 # 假设当前库存为 1000 件实际应从 ERP 获取 df[inventory_turnover_days] (1000 / (df[sales] / 7)).round(1) df.loc[df[sales] 0, inventory_turnover_days] 999 # 无销量设为极大值 return df def save_to_db(df_products, df_metrics): 将清洗后数据存入 SQLite # 先存 products 表 df_products.to_sql(products, engine, if_existsappend, indexFalse) # 再存 metrics 表需关联 product_id此处简化用最新插入的 id # 实际项目中应先查出刚插入的 product id再 insert sales_daily # 为演示简洁此处只存 products 表sales_daily 留作扩展 print(f成功存入 {len(df_products)} 条商品数据到 products 表) # 使用示例 if __name__ __main__: # 假设 product_list 是从爬虫获得的列表 product_list [ {url: https://item.jd.com/100012043978.html, title: iPhone 15, price: 5999.0, comment_count: 12500, sales: 8500}, {url: https://item.jd.com/100023456789.html, title: 小米14, price: 3999.0, comment_count: 8200, sales: 6300} ] df_raw pd.DataFrame(product_list) df_metrics calculate_sales_metrics(product_list) # 合并原始字段与指标 df_final pd.concat([df_raw, df_metrics.drop(columns[url, title, price, comment_count, sales])], axis1) save_to_db(df_raw, df_metrics) print(指标计算与存储完成)4. 可视化系统用 Streamlit 快速搭建带筛选器的交互式看板非静态图4.1 为什么选 Streamlit 而非 Flask/DjangoFlask 需写路由、模板、前后端分离Django 更重。而销售分析看板的核心诉求是快速验证想法、让业务同事能自己拖拽筛选、不关心高并发。Streamlit 一行命令streamlit run app.py启动所有 UI 控件下拉框、滑块、日期选择用 Python 函数调用逻辑与视图完全融合。老板说“看下高端机近30天销量”你只需改两行代码不用重启服务。4.2 构建可筛选的销售看板从数据库读取、动态绘图、响应式布局import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from sqlalchemy import create_engine # 页面配置 st.set_page_config( page_title商品销售分析看板, layoutwide, initial_sidebar_stateexpanded ) # 侧边栏筛选器 st.sidebar.header( 数据筛选) engine create_engine(sqlite:///sales_data.db) # 从数据库读取商品数据 st.cache_data(ttl600) # 缓存10分钟避免重复查询 def load_data(): query SELECT p.*, s.sales_qty, s.revenue, s.date FROM products p LEFT JOIN sales_daily s ON p.id s.product_id WHERE p.crawl_time (SELECT MAX(crawl_time) FROM products) return pd.read_sql(query, engine) df load_data() # 类目筛选从 title 提取关键词模拟类目 if not df.empty: # 简单类目提取手机、电脑、耳机... df[category] df[title].str.extract(r(手机|电脑|耳机|平板|手表|充电宝), expandFalse).fillna(其他) selected_categories st.sidebar.multiselect( 选择类目, optionsdf[category].unique(), defaultdf[category].unique() ) # 价格区间滑块 min_price, max_price int(df[price].min()), int(df[price].max()) price_range st.sidebar.slider( 价格区间 (¥), min_valuemin_price, max_valuemax_price, value(min_price, max_price) ) # 应用筛选 mask (df[category].isin(selected_categories)) \ (df[price] price_range[0]) \ (df[price] price_range[1]) filtered_df df[mask].copy() # 主内容区 st.title( 商品销售分析可视化看板) st.markdown(f当前筛选{len(filtered_df)} 个商品覆盖 {len(selected_categories)} 个类目) # 卡片指标 col1, col2, col3, col4 st.columns(4) with col1: st.metric(总商品数, len(filtered_df)) with col2: st.metric(平均售价, f¥{filtered_df[price].mean():.0f}) with col3: st.metric(最高销量, f{filtered_df[sales].max():,} 件) with col4: st.metric(平均毛利率, f{filtered_df[gross_margin].mean()*100:.1f}%) # 图表1价格-销量散点图带类目颜色 st.subheader(价格 vs 销量关系按类目着色) fig_scatter px.scatter( filtered_df, xprice, ysales, colorcategory, sizecomment_count, hover_data[title], labels{price: 售价 (¥), sales: 预估销量 (件), category: 类目}, title商品价格与销量分布 ) st.plotly_chart(fig_scatter, use_container_widthTrue) # 图表2类目销量占比环形图 st.subheader(各类目销量占比) category_sales filtered_df.groupby(category)[sales].sum().reset_index() fig_pie px.pie( category_sales, namescategory, valuessales, hole0.4, title类目销量分布 ) st.plotly_chart(fig_pie, use_container_widthTrue) # 表格TOP 10 商品可排序 st.subheader(销量 TOP 10 商品) top10 filtered_df.nlargest(10, sales)[[title, price, sales, comment_rate, gross_margin]] st.dataframe(top10.style.format({ price: ¥{:.0f}, sales: {:,}, comment_rate: {:.2%}, gross_margin: {:.1%} }), use_container_widthTrue) else: st.warning(暂无数据请先运行爬虫脚本获取数据。)注意st.cache_data是 Streamlit 的关键优化避免每次交互都重新查数据库。ttl600表示缓存10分钟平衡实时性与性能。若需实时数据可设为ttlNone并加刷新按钮。5. 避坑指南爬虫与可视化落地过程中这 4 个坑让我重写了 3 次代码5.1 现象爬虫跑着跑着突然全部返回 403但浏览器能正常访问原因京东等平台会检测请求频率和请求头一致性。即使你用了随机 UA如果Accept-Encoding固定为gzip而浏览器实际发送的是gzip, deflate, br服务器会标记为异常流量。更隐蔽的是部分 CDN 会检查Sec-Fetch-*系列 header如Sec-Fetch-Dest: documentRequests 默认不发送这些字段。解决在fetch_page函数的 headers 中显式添加Sec-Fetch-*字段并确保Accept-Encoding与主流浏览器一致headers { # ... 其他字段 Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: none, Sec-Fetch-User: ?1, Accept-Encoding: gzip, deflate, br # 加上 brBrotli压缩 }同时将请求间隔从固定 1 秒改为random.uniform(1, 3)彻底打乱节奏。5.2 现象Streamlit 看板启动后图表区域一片空白控制台报PlotlyError: No data found原因st.cache_data缓存了空 DataFrame如首次运行数据库为空后续即使数据库有数据缓存仍返回空。Streamlit 不会自动失效空缓存。解决强制清除缓存。在 Streamlit 启动时加判断st.cache_data(ttl600) def load_data(): try: df pd.read_sql(SELECT COUNT(*) as cnt FROM products, engine) if df.iloc[0][cnt] 0: st.warning(数据库为空请先运行爬虫) return pd.DataFrame() # 明确返回空DF避免缓存脏数据 # ... 正常查询 except Exception as e: st.error(f数据加载失败: {e}) return pd.DataFrame()并在 UI 上加一个“强制刷新数据”按钮点击时调用st.experimental_rerun()。5.3 现象Pandas 计算inventory_turnover_days时大量商品显示inf或负数原因sales字段为 0 时1000 / 0得inf而某些爬虫误取了“预售”商品sales字段是字符串“预售中”转int报错后被fillna(0)导致周转天数计算失真。解决清洗阶段严格类型校验与异常处理# 在 calculate_sales_metrics 函数开头加入 df[sales] pd.to_numeric(df[sales], errorscoerce) # 错误值转 NaN df[sales] df[sales].fillna(0).astype(int) # NaN 转 0再转 int # 再计算周转天数 df[inventory_turnover_days] (1000 / (df[sales] / 7)).round(1) df.loc[df[sales] 0, inventory_turnover_days] 999 df.loc[df[inventory_turnover_days] 365, inventory_turnover_days] 365 # 截断上限5.4 现象导出的.rar包在 Windows 解压后app.py中文注释变乱码Streamlit 启动报 SyntaxError原因.rar压缩工具如 WinRAR默认用 GBK 编码解压而 Python 文件是 UTF-8 编码。Windows 记事本打开.py文件时若未指定编码会用系统默认 ANSIGBK解析 UTF-8 字节显示乱码。解决在 Python 文件首行强制声明编码并指导用户用 VS Code 等现代编辑器打开# -*- coding: utf-8 -*- 商品销售分析可视化系统 作者一线工程师 同时在README.md中明确写出“请用 VS Code、PyCharm 或 Sublime Text 打开代码勿用 Windows 记事本”。6. 进阶技巧用 Plotly 的update_layout实现“点击商品标题下钻查看该商品7天销量曲线”6.1 为什么需要下钻分析——从“哪个类目卖得好”到“哪款手机在促销期爆发”老板不会满足于“手机类目销量第一”他会问“iPhone 15 Pro 在双11当天销量是多少比上个月同期涨了多少” 这就是下钻Drill-down从汇总层点击具体项跳转到明细层。Plotly 原生不支持点击事件回调那是 Dash 的领域但我们能用Streamlit 的st.session_statest.experimental_rerun()模拟实现轻量级下钻。6.2 实现步骤状态管理 动态查询 双视图切换# 在 app.py 开头定义会话状态 if selected_product not in st.session_state: st.session_state.selected_product None # 在 TOP 10 表格下方添加“查看详情”按钮 if not filtered_df.empty and st.session_state.selected_product is None: st.subheader( 点击查看详情) # 为每行商品添加按钮 for idx, row in filtered_df.nlargest(10, sales).iterrows(): if st.button(f 查看 {row[title][:15]}... 销量趋势, keyfbtn_{idx}): st.session_state.selected_product row[id] st.experimental_rerun() # 当有商品被选中时显示其7天销量曲线 if st.session_state.selected_product is not None: # 查询该商品最近7天的 sales_daily 数据 product_id st.session_state.selected_product query f SELECT date, sales_qty, revenue FROM sales_daily WHERE product_id {product_id} ORDER BY date DESC LIMIT 7 daily_data pd.read_sql(query, engine) if not daily_data.empty: # 按日期升序排列便于画趋势图 daily_data[date] pd.to_datetime(daily_data[date]) daily_data daily_data.sort_values(date) st.subheader(f {filtered_df[filtered_df[id]product_id][title].iloc[0]} 近7天销量趋势) col1, col2 st.columns(2) with col1: fig_line px.line( daily_data, xdate, ysales_qty, markersTrue, labels{date: 日期, sales_qty: 销量 (件)}, title日销量趋势 ) st.plotly_chart(fig_line, use_container_widthTrue) with col2: # 计算环比增长 if len(daily_data) 2: last_day daily_data.iloc[-1][sales_qty] prev_day daily_data.iloc[-2][sales_qty] change_pct ((last_day - prev_day) / prev_day * 100) if prev_day ! 0 else 0 st.metric(昨日销量, f{last_day:,} 件, f{change_pct:.1f}%) # 返回按钮 if st.button(← 返回总览): st.session_state.selected_product None st.experimental_rerun() else: st.warning(暂无该商品的每日销量数据请确认 sales_daily 表是否有记录。) if st.button(← 返回总览): st.session_state.selected_product None st.experimental_rerun()6.3 关键参数与调试技巧如何让下钻体验丝滑不卡顿st.session_state是核心它让 Streamlit 在多次 rerun 间保持变量状态避免每次点击都丢失上下文。key参数防重复st.button(..., keyfbtn_{idx})中的key必须唯一否则多个按钮会绑定同一状态。用idxDataFrame 索引最安全。数据查询优化LIMIT 7和ORDER BY date DESC确保只查最新7天避免全表扫描。若sales_daily表很大需在product_id和date字段建复合索引CREATE INDEX idx_sales_product_date ON sales_daily (product_id, date);用户体验细节返回按钮放在右下角符合阅读习惯st.metric的 delta 参数自动加绿色↑/红色↓箭头老板一眼看懂涨跌。我最初做这个系统时以为“爬下来、画出来”就完了结果第一次给运营同事演示她点开 iPhone 15 的销量图脱口而出“咦怎么没有标注双11当天的销量峰值” —— 那一刻我才明白可视化不是把数据变成图而是把业务问题变成可交互的答案。现在我的习惯是每加一个图表必问自己“老板会用它回答什么问题”然后补上对应的下钻或筛选。这套流程跑通后我把它打包成.rar发给团队新人照着 README 10 分钟就能跑起来再也不用求 IT 部署服务器。希望帮到你。本文还有配套的精品资源点击获取