简介:这份资源是面向高校学生与技术学习者的抖音电商数据可视化完整项目源码,采用Hadoop、Flask、MySQL 5.7与Vue构建前后端分离架构,可作为毕业设计、课程设计、大作业或工程实训的实践素材。压缩包共406个文件,约17.24MB,涵盖Python后端脚本、Vue组件与JavaScript前端代码、SVG图标与PNG/JPG图片资源、CSS样式、SQL建表脚本及bat一键安装运行脚本,并附doc/docx说明文档与爬虫脚本,便于快速搭建环境。项目以Hadoop处理海量电商数据,Flask提供Web服务,MySQL负责数据存储,Vue实现可视化界面,spider模块完成数据采集,形成从数据获取到展示的完整链路。目前已有96人学习下载,适合希望理解分布式数据处理与前后端协作流程的读者参考实践。
1. 抖音电商数据可视化:从爬虫到 Hadoop 再到 Flask 的完整链路
抖音电商的公开数据里藏着大量值得分析的东西——商品标题里的品类词、价格分布、销量区间、达人带货的转化节奏。但真要把这些数据跑通一条「采集 → 存储 → 计算 → 可视化」的链路,很多人卡在第一步就放弃了:爬下来的数据格式乱、字段缺失、中文编码翻车,好不容易洗干净了又发现单机 pandas 跑几十万行就开始喘。这套方案的核心思路是用 spider 负责采集、Hadoop 负责分布式存储与聚合、Flask 负责把结果渲染成可交互的 ECharts 页面。它适合做课程设计的学生、想搭一套电商数据看板的初中级工程师,以及需要快速验证「数据能不能出结论」的产品同学。下面按我实际搭过一遍的顺序,把每个环节的参数和坑讲清楚。
2. 数据采集层:spider 抓抖音电商公开数据的正则与反爬处理
2.1 为什么选 requests + 正则而不是重型框架
抖音电商的公开页面结构相对规整,商品卡片里的标题、价格、销量通常以 JSON 片段或固定 HTML 结构出现。用 Scrapy 当然可以,但引入调度器、中间件、管道这一整套,对课程设计级别的数据量来说是过度设计。我一般用 requests 拿页面,配合正则表达式直接抽字段,代码量少、调试直观。正则的关键是别写得太贪婪,.*?和.*的区别在这里就是「能不能正确匹配到下一个商品」的分水岭。
import requests import re import csv import time HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(url): # 每次请求间隔 1.5 秒,避免触发频率限制 time.sleep(1.5) resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" # 抖音页面常见 utf-8,若乱码改 gbk 试 return resp.text def parse_products(html): items = [] # 非贪婪匹配商品块,避免跨商品吞字段 pattern = re.compile( r'"title":"(.*?)".*?"price":"(.*?)".*?"sales":"(.*?)"', re.S ) for m in pattern.finditer(html): title, price, sales = m.groups() items.append({ "title": title.strip(), "price": float(price) if price.replace('.', '').isdigit() else 0.0, "sales": int(sales) if sales.isdigit() else 0, }) return items def save_csv(items, path="douyin_raw.csv"): with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "price", "sales"]) writer.writeheader() writer.writerows(items) if __name__ == "__main__": html = fetch_page("https://example.com/douyin/list") data = parse_products(html) save_csv(data) print(f"采集 {len(data)} 条")这段代码的逻辑是:先请求页面拿到 HTML 文本,再用一条正则同时抽出标题、价格、销量三个字段,最后落成 CSV。参数上要注意三个点。time.sleep(1.5)是保命间隔,低于 1 秒连续请求很容易被限流;resp.encoding必须显式设置,requests 自动推断中文页面经常猜错,导致后面正则匹配全空;encoding="utf-8-sig"写 CSV 是为了 Excel 打开不乱码,这个细节很多人踩过。
2.2 字段清洗与去重的三个必做步骤
爬下来的原始数据不能直接进 Hadoop,否则 MapReduce 阶段全是脏数据。我一般做三件事:去重、价格归一、销量补零。去重按标题的哈希值做,因为同一商品可能被多个页面重复抓取;价格归一处理「¥19.9」「19.9元」「19.90」这类格式差异;销量字段缺失时补 0 而不是丢弃,否则聚合时总数会偏小。
import hashlib def clean_items(items): seen = set() cleaned = [] for it in items: # 标题去重,用 md5 做指纹 key = hashlib.md5(it["title"].encode("utf-8")).hexdigest() if key in seen: continue seen.add(key) # 价格归一:去掉货币符号和单位 price_str = re.sub(r"[^\d.]", "", str(it["price"])) it["price"] = float(price_str) if price_str else 0.0 # 销量缺失补 0 it["sales"] = int(it["sales"]) if str(it["sales"]).isdigit() else 0 cleaned.append(it) return cleaned去重逻辑用 md5 而不是直接比字符串,是因为标题可能很长,哈希后比较更快。价格清洗用re.sub把所有非数字非小数点的字符干掉,这样「¥19.9」和「19.9元」都会变成「19.9」。销量补 0 这个决定要看业务:如果做的是「平均销量」分析,补 0 会拉低均值,这时候应该单独标记缺失而不是补零。我一般会在 CSV 里加一列sales_missing做标记,后面在 Hive 里按需过滤。
3. Hadoop 伪分布式搭建:让采集数据真正跑在 HDFS 上
3.1 伪分布式的最小配置与 hadoop_home 环境变量
单机跑 Hadoop 有两种模式:本地模式和伪分布式。本地模式不走 HDFS,调试 MapReduce 逻辑够用,但你要验证「数据真的存到分布式文件系统」就必须上伪分布式。核心配置就三个文件:core-site.xml指定 HDFS 地址,hdfs-site.xml设副本数为 1(单机没那么多节点),mapred-site.xml指定用 YARN 跑任务。
# 解压并配置环境变量 tar -zxvf hadoop-3.3.6.tar.gz -C /opt/ echo 'export HADOOP_HOME=/opt/hadoop-3.3.6' >> ~/.bashrc echo 'export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH' >> ~/.bashrc source ~/.bashrc # 验证 hadoop versionHADOOP_HOME这个环境变量是后面所有命令的基础,配错了hdfs、yarn命令全部找不到。注意路径别带空格,Windows 下用 WSL 或虚拟机跑更稳。配置完执行hdfs namenode -format格式化,这一步只能做一次,重复格式化会导致集群 ID 不一致,NameNode 起不来——这是新手最常见的翻车点。
3.2 把 CSV 灌进 HDFS 并跑第一个聚合任务
数据清洗完的 CSV 要进 HDFS 才能被 MapReduce 或 Hive 处理。用hdfs dfs -put上传,然后用一个简单的 MapReduce 统计各价格区间的商品数量。
# 建目录并上传 hdfs dfs -mkdir -p /douyin/input hdfs dfs -put douyin_clean.csv /douyin/input/ # 确认上传成功 hdfs dfs -ls /douyin/input/# mapper.py:按价格区间打标签 import sys for line in sys.stdin: line = line.strip() if line.startswith("title") or not line: continue parts = line.split(",") if len(parts) < 3: continue try: price = float(parts[1]) except ValueError: continue if price < 50: bucket = "0-50" elif price < 100: bucket = "50-100" elif price < 500: bucket = "100-500" else: bucket = "500+" print(f"{bucket}\t1")# reducer.py:累加每个区间的计数 import sys from collections import defaultdict counts = defaultdict(int) for line in sys.stdin: bucket, cnt = line.strip().split("\t") counts[bucket] += int(cnt) for bucket, cnt in sorted(counts.items()): print(f"{bucket}\t{cnt}")用 Hadoop Streaming 跑这两个脚本,不需要编译 Java:
hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-3.3.6.jar \ -files mapper.py,reducer.py \ -mapper "python3 mapper.py" \ -reducer "python3 reducer.py" \ -input /douyin/input/douyin_clean.csv \ -output /douyin/output/price_bucket参数说明:-files把脚本分发到各节点,-mapper和-reducer指定执行命令,-input和-output是 HDFS 路径。输出目录必须不存在,否则任务直接报错退出,这是 Hadoop 的保护机制。跑完后用hdfs dfs -cat /douyin/output/price_bucket/part-*看结果。如果 mapper 报编码错误,在脚本开头加# -*- coding: utf-8 -*-并确保PYTHONIOENCODING=utf-8。
4. Flask 后端:把 HDFS 聚合结果变成可视化接口
4.1 Flask 与 FastAPI 的选型对比及本项目取舍
热词里常有人问 flask 与 fastapi 比较,这里说下我的判断。FastAPI 异步性能好、自带 Swagger 文档,适合高并发 API;但这个项目的接口就三五个,数据量也不大,Flask 的轻量和生态成熟度更合适。Flask 配合 Jinja2 模板可以直接渲染 ECharts 页面,不用前后端分离,部署成本低。如果你后面要做实时推送或大量并发查询,再换 FastAPI 也不迟。
from flask import Flask, jsonify, render_template import subprocess app = Flask(__name__) def read_hdfs_result(path): # 用 subprocess 调 hdfs 命令读结果,简单直接 result = subprocess.run( ["hdfs", "dfs", "-cat", path], capture_output=True, text=True ) data = [] for line in result.stdout.strip().split("\n"): if not line: continue bucket, cnt = line.split("\t") data.append({"bucket": bucket, "count": int(cnt)}) return data @app.route("/") def index(): return render_template("index.html") @app.route("/api/price_bucket") def price_bucket(): data = read_hdfs_result("/douyin/output/price_bucket/part-*") return jsonify(data) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)这段代码的关键是read_hdfs_result函数,它通过 subprocess 调用hdfs dfs -cat读取 MapReduce 输出,解析成 JSON 返回给前端。host="0.0.0.0"让外部能访问,debug=True开发时方便热重载,上线要关掉。注意part-*通配符会匹配多个 reducer 输出文件,如果结果有多份要合并处理。
4.2 ECharts 前端渲染与接口联调
前端用 ECharts 画柱状图,数据从/api/price_bucket拿。核心是fetch请求后把数据映射成 ECharts 的xAxis和series。
fetch('/api/price_bucket') .then(res => res.json()) .then(data => { const buckets = data.map(d => d.bucket); const counts = data.map(d => d.count); const chart = echarts.init(document.getElementById('main')); chart.setOption({ title: { text: '抖音电商商品价格区间分布' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: buckets }, yAxis: { type: 'value' }, series: [{ name: '商品数', type: 'bar', data: counts, itemStyle: { color: '#5470c6' } }] }); });联调时最常见的坑是跨域。Flask 和前端如果不同端口,浏览器会拦请求。开发阶段用 Flask 的CORS扩展加一行CORS(app)就行,或者干脆把前端模板放 Flask 的templates目录下,同源就没这个问题。ECharts 的init要在 DOM 加载完后调用,否则容器宽度为 0,图表渲染不出来——这个坑我踩过不止一次。
5. 避坑与排查:这条链路上最容易翻车的五个地方
5.1 现象:正则匹配结果全为空
原因通常是页面编码没设对,requests 默认用 ISO-8859-1 解码,中文全变乱码,正则自然匹配不到。解决方法是显式设resp.encoding = "utf-8",如果还不行就resp.content.decode("utf-8", errors="ignore")手动解码。
5.2 现象:HDFS 上传报「Could not resolve hostname」
原因是core-site.xml里fs.defaultFS配的是主机名而不是 IP,但/etc/hosts没做映射。解决方法是把fs.defaultFS改成hdfs://localhost:9000,或者在/etc/hosts里加一行127.0.0.1 你的主机名。
5.3 现象:MapReduce 任务卡在 map 0% reduce 0%
多半是 YARN 的yarn-site.xml里yarn.nodemanager.aux-services没配成mapreduce_shuffle,或者内存给太小。解决方法是检查yarn.nodemanager.resource.memory-mb至少给 2048,yarn.scheduler.maximum-allocation-mb也要相应调大。
5.4 现象:Flask 接口返回 500 但日志没细节
原因是 subprocess 调用 hdfs 命令失败但异常被吞了。解决方法是在read_hdfs_result里加if result.returncode != 0: raise RuntimeError(result.stderr),把 stderr 暴露出来。另外确认 Flask 进程的运行用户有权限执行 hdfs 命令。
5.5 现象:ECharts 图表空白但接口有数据
原因是echarts.init执行时容器还没渲染完,或者容器没有显式高度。解决方法是把初始化放在window.onload里,并给图表容器设height: 500px这样的固定高度。CSS 里height: 100%在父元素没高度时等于 0,这是前端经典坑。
6. 进阶技巧:用 Hive 替代手写 MapReduce 并做多维分析
手写 mapper/reducer 适合理解原理,但真实分析场景里维度一多就力不从心。我后来把数据表挂到 Hive 上,用 SQL 做多维聚合,效率高很多。建外部表指向 HDFS 上的 CSV 目录,然后直接写 SQL 查价格区间、销量 Top N、标题关键词分布。
CREATE EXTERNAL TABLE douyin_products ( title STRING, price DOUBLE, sales INT ) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/douyin/input/'; -- 价格区间分布 SELECT CASE WHEN price < 50 THEN '0-50' WHEN price < 100 THEN '50-100' WHEN price < 500 THEN '100-500' ELSE '500+' END AS bucket, COUNT(*) AS cnt FROM douyin_products WHERE title != 'title' GROUP BY CASE WHEN price < 50 THEN '0-50' WHEN price < 100 THEN '50-100' WHEN price < 500 THEN '100-500' ELSE '500+' END;建表时EXTERNAL关键字很重要,删表不会删数据。LOCATION指向 HDFS 目录,Hive 会自动读该目录下所有文件。WHERE title != 'title'是过滤 CSV 表头,因为 Hive 建表时默认不跳过首行。如果数据量大,可以改成分区表按日期分区,查询时加WHERE dt='2024-01-01'做分区裁剪,速度提升明显。
验证方法上,我一般会拿 Hive 的结果和手写 MapReduce 的结果对一遍,数字对得上说明链路没问题。另外可以在 Flask 里加一个/api/top_sales接口,用 Hive 查销量 Top 10 商品,前端用 ECharts 横向柱状图展示,这样整个看板就有两个维度了。
最后说个习惯:每次改完 HDFS 配置或 Hive 表结构,先跑一条SELECT COUNT(*)确认数据能读出来,再跑复杂查询。这个「后悔药」动作能帮你快速定位是数据问题还是 SQL 问题,省下大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取