资讯动态

Python电商用户行为分析实战:从爬虫采集、Hive数仓到Django可视化

发布时间:2026/10/31 19:33:13 来源:尧图企业网站定制
简介一份基于Python的Django框架与MySQL数据库的电商用户行为分析系统整合Hive与爬虫技术适合作为毕业设计、课程设计或大作业。系统覆盖管理员端与用户端完整功能后台提供系统首页、个人中心、用户管理、商品信息管理、商品类型管理、购物日志、充值记录、系统管理和订单管理等模块前台包含商品个性化推荐、详情展示、公告资讯、购物车及个人中心适用人群涵盖小白与进阶学习者可用于工程实训或初期项目立项。资源包共609个文件以Vue前端、Python后端、SQL脚本和批处理脚本为主辅以JS、CSS、图片等素材压缩包整体大小48.81MB内含可运行源码、SQL文件及文档并提供安装、运行、初始化Hive数据库等一键批处理脚本便于快速部署与二次开发。目前已有96人学习项目模块划分清晰便于逐项对照源码理解整套电商管理流程。1. 电商用户行为分析系统到底在分析什么一张订单日志背后的三个问题拿到“基于 Python 的电商用户行为分析系统”这类毕设包先别急着解压跑 demo。它的完整链路是 spider 采集行为日志 → hive 建数仓算指标 → django 做后台展示看起来三个组件各管一段实际调试时你会发现真正卡住你的不是算法而是埋点数据太脏、hive 查得慢、django 页面数据对不上。这套系统适合两类人——需要交毕设/课设的学生以及想亲手把一条数据管道从采集到展示完整跑通的初学者。先想明白三个问题再动手行为日志和订单表有什么区别、分析指标从哪一层算、结果怎么安全地送到 web 端。2. 数据采集层怎么搭用 spider 把用户行为日志爬回来并清洗入仓2.1 爬虫选型与字段设计别一上来就上 scrapy 全家桶电商用户行为分析的原始数据核心是四件套字段user_id、item_id、behaviorpv / cart / fav / buy、timestamp。采集层常见做法是用requests BeautifulSoup或requests lxml不必一开始就引入 scrapy 的 Item Pipeline 机制——毕设链路里 scrapy 的组件会显著增加调试成本尤其是中间件报错时新人容易卡住。我一般用 requests 写一个轻量采集脚本把页面接口返回的 JSON 直接落盘。import requests def fetch_product_behavior(session, user_id, page): 抓取某个用户在某一页商品上的行为事件。 返回的是结构化字段列表后续清洗直接喂给正则层。 url https://example-shop/api/events resp session.get( url, params{user_id: user_id, page: page}, timeout10 ) # 接口返回 JSON 数组每个元素是一次行为事件 raw_events resp.json() rows [] for ev in raw_events: rows.append({ user_id: ev[uid], # 用户ID item_id: ev[sku_id], # 商品ID behavior: ev[action], # 行为类型 pv/cart/fav/buy ts: ev[timestamp], # 毫秒级时间戳 price: ev.get(price, 0), # 成交价可能带货币符号 }) return rows if __name__ __main__: s requests.Session() s.headers.update({User-Agent: Mozilla/5.0 (compatible; edu-project/1.0)}) events fetch_product_behavior(s, user_idu_1001, page1) print(len(events))为什么要强调session复用因为电商接口通常有频控频繁建立新连接比复用连接更容易触发封禁timeout10是必填参数避免某个接口卡死时整个采集脚本挂住。ev.get(price, 0)用了get而不是直接下标访问因为埋点数据经常缺字段直接ev[price]遇到脏数据就抛 KeyError而毕设里你最不缺的就是脏数据。2.2 正则清洗把脏字符串变成能入 hive 的结构化数据采集回来的 JSON 不能直接进 hive常见脏数据有三类价格字段混着人民币符号和中文逗号、时间戳秒级毫秒级混用、行为类型字段超出四类枚举值。清洗层用re模块逐字段处理我的原则是“先保数据不丢再谈字段标准”。import re def clean_event(row: dict) - dict: 把原始日志清洗成 hive 能直接 load 的文本行 # 价格去掉 ¥ 逗号 空格再转 float price str(row.get(price, 0)) price re.sub(r[¥,\s], , price) row[price] float(price) if price else 0.0 # 时间戳秒级补 000 转毫秒hive 里 from_unixtime 必须处理 13 位 ts str(row.get(ts, )) if len(ts) 10: ts ts 000 row[ts] int(ts) # 行为类型白名单其余归为 pv避免下游 group by 出现未知枚举 behavior row.get(behavior, pv) if behavior not in (pv, cart, fav, buy): behavior pv row[behavior] behavior return row正则这里有个容易翻车的点re.sub的第二个参数是替换字符串如果用来替换的内容里有反斜杠或正则组引用符号会被二次解析。所以价格清洗我宁愿分两步先re.sub(r[¥,\s], , price)把干扰符号删掉再float()强转而不是写一条“匹配带符号的完整价格”的复杂规则。清洗后的数据按日期写成 TSV 文件文件名带上采集批次号方便 hive 加载后排查脏数据来源。2.3 落盘与采集频率按日期分区写文件给 hive 留好后路清洗后的数据写盘时目录结构直接按dt2024-05-01这种 hive 分区格式来组织能省掉后面一次ALTER TABLE ADD PARTITION的手工操作。采集频率上我一般按小时跑一次增量每次追加当天目录下新文件。断点续爬的简单做法是写文件前检查目标路径是否已存在同批次文件存在就跳过避免重复采集同一个接口翻倍数据量。import os from datetime import datetime def save_clean_rows(rows, base_dirraw_logs): dt datetime.now().strftime(%Y-%m-%d) out_dir os.path.join(base_dir, fdt{dt}) os.makedirs(out_dir, exist_okTrue) batch datetime.now().strftime(%H%M) out_path os.path.join(out_dir, fevents_{batch}.tsv) if os.path.exists(out_path): print(fskip existed: {out_path}) return out_path with open(out_path, w, encodingutf-8) as f: for row in rows: # 字段顺序固定hive load 时不用再指定列映射 line \t.join([ row[user_id], row[item_id], row[behavior], str(row[price]), str(row[ts]), ]) f.write(line \n) return out_path注意 TSV 字段顺序必须和建表语句严格一致。如果 hive 表里字段顺序和这里不一致LOAD DATA不会报错但查出来的数据会整体错位——这是数据管道最常见的“黑匣子”问题每个环节都正常最后指标就是不对。用固定字段顺序写文件能从源头消灭这一类错位。3. 用 hive 搭用户行为数仓从原始日志到转化漏斗的窗口函数实践3.1 hive 表 DDL 设计分区、字段类型与 Hive 3.1.3 的取舍hive 侧第一步是建分层表。毕设项目里常见做法是建两层ODS 层存原始清洗数据DWD 层做明细宽表。Hive 3.1.3 是课设和毕设环境里出现频率最高的版本它和 Hadoop 3 主线兼容窗口函数和INSERT OVERWRITE语法都比 2.x 稳定。建表时字段先全用 STRING 落地查询时再 CAST而不是建表时就定好 BIGINT 或 DOUBLE——埋点数据只要有一条非数字内容整条查询就会因为类型转换失败中断用字符串先入仓是更稳的选择。-- DWD 层用户行为明细表按天分区 CREATE TABLE dwd_user_behavior ( user_id STRING COMMENT 用户ID, item_id STRING COMMENT 商品ID, behavior STRING COMMENT 行为类型: pv/cart/fav/buy, price DOUBLE COMMENT 成交价清洗后转浮点, ts BIGINT COMMENT 毫秒级时间戳, ref STRING COMMENT 来源页面, hour INT COMMENT 小时用于分时统计 ) PARTITIONED BY (dt STRING COMMENT 日期分区如 2024-05-01) STORED AS TEXTFILE;分区的选择需要想清楚按dt做一级分区足够覆盖毕设查询场景不需要再加二级分区。STORED AS TEXTFILE的取舍是查询速度不如 ORC但调试时能直接SELECT *看到原始内容排查脏数据方便项目答辩演示时也更直观。LOAD 数据时用LOAD DATA LOCAL INPATH把上一章清洗好的 TSV 文件装进来LOAD DATA LOCAL INPATH raw_logs/dt2024-05-01/events_1030.tsv INTO TABLE dwd_user_behavior PARTITION (dt2024-05-01);3.2 hive 给每一行标号窗口函数算相邻行为间隔与会话切分行为分析里两个最通用的操作一是按用户分组后给行为事件编号二是计算相邻行为之间的时间差。前者用ROW_NUMBER()后者用LAG()。这也是热词里“hive 给每一行标号”的落地场景。SELECT user_id, behavior, ts, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts) AS seq, LAG(ts) OVER (PARTITION BY user_id ORDER BY ts) AS prev_ts, (ts - LAG(ts) OVER (PARTITION BY user_id ORDER BY ts)) AS gap_ms FROM dwd_user_behavior WHERE dt 2024-05-01 LIMIT 20;PARTITION BY user_id表示按用户分组ORDER BY ts表示组内按时间排序ROW_NUMBER()给每个用户的行为按时间标上 1、2、3……这个编号在会话切分里很常用如果gap_ms超过 30 分钟即 1800000 毫秒就认为用户开启了一次新会话。这里有个易错细节LAG(ts)不能单独使用必须写LAG(ts) OVER (...)指定窗口否则 hive 直接报语法错误。LIMIT 20是为了先在命令行里快速验证结果避免全量扫描。3.3 hive 小文件优化为什么查询越来越慢怎么合并采集脚本每小时写一个 TSV 文件跑一周就是上百个小文件。hive 查询时每个文件都要开一个 Map 任务文件越多任务调度开销越大——这就是“hive 优化小文件”的核心场景。常见做法是跑一次INSERT OVERWRITE把 DWD 层按分区重写一遍让小文件在写入阶段被合并。INSERT OVERWRITE TABLE dwd_user_behavior PARTITION (dt2024-05-01) SELECT user_id, item_id, behavior, price, ts, ref, hour FROM dwd_user_behavior WHERE dt 2024-05-01 DISTRIBUTE BY hour;这里的DISTRIBUTE BY hour是关键参数它让数据按小时字段分发到 reducer每个 reducer 写一个文件合并后每分区最多 24 个文件比原来几十个碎片文件稳定得多。如果文件还是偏多再配合设置SET hive.merge.mapredfilestrue;让 Map 阶段输出前先做一轮合并。注意合并完必须立即SELECT COUNT(*)验证行数一致我有一次就是因为INSERT OVERWRITE里的 SELECT 漏了过滤条件把整张表覆盖成了空表。3.4 用三句 SQL 串起转化漏斗pv 到 buy 的人数变化漏斗分析是电商行为分析系统里最能出效果的指标也是答辩时最容易被追问的部分。核心逻辑是统计当天有过浏览、加购、购买行为的去重用户数每一层人数逐级下降。WITH base AS ( SELECT user_id, behavior FROM dwd_user_behavior WHERE dt 2024-05-01 ) SELECT COUNT(DISTINCT IF(behavior pv, user_id, NULL)) AS pv_users, COUNT(DISTINCT IF(behavior cart, user_id, NULL)) AS cart_users, COUNT(DISTINCT IF(behavior fav, user_id, NULL)) AS fav_users, COUNT(DISTINCT IF(behavior buy, user_id, NULL)) AS buy_users FROM base;用COUNT(DISTINCT IF(...))而不是COUNT(DISTINCT user_id) FILTER (WHERE ...)是因为 hive 对 FILTER 语法支持不如 MySQL 8.0 完整IF嵌套在COUNT(DISTINCT ...)里能兼容 Hive 3.1.3。这一层算出来的结果表就是后面 django 要展示的核心数据。到这里hive 侧的数据管道已经闭环明细入仓、窗口函数加工、漏斗指标沉淀接下来是把结果安全地送到 web 端。4. django 应用层落地models 映射、对象删除与查询接口的调试记录4.1 django 创建 app 与 models 映射hive 结果怎么进 MySQLdjango 不直接连 hive这句话值得写在前头。常见做法是hive 算好的指标用INSERT OVERWRITE或SELECT ... FROM导出成 CSV再灌入 MySQLdjango 的 ORM 只面向 MySQL。这个“中转”设计是为了避免在 Django 里折腾 hive JDBC 驱动省掉大量环境配置时间。python manage.py startapp analyze创建好analyzeapp 之后在models.py里建一张和 hive 输出对齐的统计表。表结构不用完全复刻 hive 宽表只保留 django 页面要展示的字段。from django.db import models class BehaviorStat(models.Model): hive 聚合结果落 MySQL 后的映射表字段对齐前端展示需求 stat_date models.DateField(verbose_name统计日期, db_indexTrue) metric models.CharField(max_length32, verbose_name指标名) value models.FloatField(verbose_name指标值) class Meta: db_table behavior_stat indexes [ models.Index(fields[stat_date, metric]), ]stat_date metric的联合索引是查询性能的关键。前端按日期筛选 按指标名取值这条索引能让filter(stat_date..., metric...)走覆盖索引数据量到十万级时页面响应仍能保持在百毫秒级。db_indexTrue和Meta.indexes二选一即可我习惯用Meta.indexes把复合索引写清楚迁移时一目了然。4.2 django 执行查询与删除对象filter 惰性、delete 返回与级联策略django ORM 的filter()返回的是惰性查询集真正执行 SQL 是在迭代或取值时。这里有新手常踩的坑在循环里反复filter()查库产生 N 条 SQL正确做法是先取查询集再循环。from django.http import JsonResponse from .models import BehaviorStat def stat_api(request): 按日期返回 pv/cart/fav/buy 指标给前端图表用 date request.GET.get(date, 2024-05-01) qs BehaviorStat.objects.filter(stat_datedate) data {item.metric: item.value for item in qs.iterator()} return JsonResponse({date: date, data: data}).iterator()是我强烈建议加上的它让 ORM 用服务端游标逐条取数而不是一次性把所有结果加载进内存。毕设数据量级下可能感觉不出差别但如果 hive 导出的统计表有几十万行不带iterator()的查询会把内存直接打满。删除操作则是另一个高频场景。def cleanup_old_data(request): 删除指定日期之前的历史统计注意 delete 返回值结构 deleted, detail BehaviorStat.objects.filter(stat_date__lt2024-01-01).delete() return JsonResponse({deleted_rows: deleted, detail: detail})delete()的返回值需要说清楚它是一个二元组(总删除行数, {模型名: 删除行数})。上面代码里detail就是这个字典调试时打印出来能看到具体删了哪个表的数据。如果你删的是有关联外键的模型delete()会级联删除关联对象这是 django 的行为很多时候是好事但如果你只是想清空一张表而不想动子表就要把外键的on_deletemodels.SET_NULL配好否则删一行母表数据可能连带删掉几十行子表数据。4.3 查询接口参数化与展示优化日期、指标与分页接口层除了stat_api还要做分页和参数校验。分页用 django 内置的Paginator避免一次把整张表输出到页面。参数校验上日期格式必须严格校验我见过不少项目因为前端传了2024/05/01这种斜杠格式导致 ORM 查询直接报错。from django.core.paginator import Paginator from datetime import datetime def list_stats(request): page int(request.GET.get(page, 1)) page_size int(request.GET.get(page_size, 20)) date_str request.GET.get(date, ) try: dt datetime.strptime(date_str, %Y-%m-%d).date() except ValueError: return JsonResponse({error: date format must be YYYY-MM-DD}, status400) qs BehaviorStat.objects.filter(stat_datedt).order_by(-value) p Paginator(qs, page_size) items list(p.get_page(page)) return JsonResponse({ page: page, total: p.count, items: [ {metric: it.metric, value: it.value} for it in items ], })order_by(-value)让指标按数值降序展示前端图表显示更直观。这里有个参数选择page_size从请求里取但要做上限限制比如min(page_size, 100)防止有人传page_size999999把接口拖垮。到这一步django 已经能把 hive 算好的指标以 JSON 接口形式提供给前端了。5. hive 与 django 联调避坑五个让毕设现场翻车的常见问题与排查5.1 现象hive 里查出的订单数django 页面显示少了 30%第一次联调时发现hive 控制台里COUNT(*)是 12 万django 页面却只有 8 万。原因很快就定位了——hive 导出到 MySQL 时用了INSERT OVERWRITE但导出脚本里 WHERE 条件少了dt过滤后跑的增量数据把前一天的分区数据覆盖了而 MySQL 表里又混着不同日期的数据。解决方式导出前先按stat_date清空目标表再执行导入或者给 hive 的导出 SQL 加上明确的WHERE dt...让每次导出只写一个日期分区的数据。排查这类问题最快的方法是在 django 里选三个日期分别数行数如果某些日期行数明显异常基本就是覆盖逻辑出了问题。5.2 现象hive 跑一个简单 count 花了十几分钟Map 任务数上千采集脚本按小时写文件一周下来 ODS 层积累了 200 多个小文件每个小文件都会启动一个 Map 任务调度开销远大于计算本身。解决分两步先跑一次INSERT OVERWRITE ... DISTRIBUTE BY hour合并 DWD 层文件再设置SET hive.merge.mapredfilestrue;和SET hive.merge.size.per.task128000000;让后续写入自动触发合并。合并完验证行数是必要步骤我有过一次合并后行数对不上的经历最后发现是有个小文件重复被 LOAD 了两次排查靠的是按批次号查文件列表。5.3 现象清洗后 price 字段大量为 0但原始日志里价格明明有值这是正则清洗顺序的问题。原始价格格式是¥1,299.00我的清洗正则写成了re.sub(r[¥,\s], , price)之后又float(price)结果字符串变成了1299.00单看没问题。但遇到¥1,299.00和1,299混用时第二次处理把逗号当千分位删掉小数点后只剩两位个别金额就变了。后来改成先按split(.)拆成整数部分和小数部分整数部分再去掉逗号最后再拼回去转 float。验证方法是随机抽 100 条清洗前后的数据对比价格没有差异再放量跑。5.4 现象窗口函数按时间排序行为编号 seq 总是乱序ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts)看起来没问题但结果里同一个用户的 seq 顺序和页面展示的行为顺序对不上。原因ts字段在清洗层没统一有一部分是秒级 10 位数字有一部分是毫秒级 13 位数字数值上毫秒级永远大于秒级排序就把时间顺序搅乱了。解决清洗层里统一按len(ts)判断补000入库前再在 hive 侧加一层CAST(ts AS BIGINT)校验过滤掉小概率的异常值。这类问题最隐蔽因为数据量一大肉眼很难从排序结果里看出来。5.5 现象django 页面打开就 502但 hiveserver2 是正常的反向代理超时原因是页面某个接口在同步调用 hive 相关的统计任务而查询队列排得长超过了 nginx 的默认 60 秒超时。解决django 接口请求 hive 的统计操作全部改成离线的即提前把结果算好写进 MySQL页面只读 MySQL另一个兜底是给 nginx 的proxy_read_timeout调大但这属于治标不治本。更彻底的方案是把统计任务丢进后台线程队列页面接口立刻返回任务 ID前端轮询任务状态。这个方案能让你在答辩现场不卡页面值得提前做。5.6 现象djangodelete()把关联表数据一起删了删除一张父表数据子表数据也随之消失。django 默认的级联删除行为在毕设项目里很容易被忽略如果子表关联外键没有显式指定on_deletemodels.SET_NULL默认的CASCADE就会把关联行全删掉。解决在设计 models 时对需要保留历史的关联字段统一用on_deletemodels.SET_NULL, nullTrue已经建好的表需要在迁移文件里手动改on_delete并重新makemigrations。删除前建议先打印返回值里的 detail 字典看清到底会删哪些表再执行真正的删除。6. 进阶验证技巧两套口径交叉核对留存率答辩时多一张底牌做到这里系统已经能跑通spider 采数据、hive 算漏斗、django 出页面。但如果想让它比普通毕设更扎实我建议加一道“交叉验证”的工序用完全独立的工具链把留存率重算一遍和 hive 的结果对比。这套验证能帮你发现数仓链路里的隐性 bug。-- 留存率hive 口径取 2024-05-01 当天购买用户次日仍有行为的比例 SELECT a.dt AS first_dt, COUNT(DISTINCT a.user_id) AS day0_users, COUNT(DISTINCT IF(b.dt DATE_ADD(a.dt, 1), b.user_id, NULL)) AS day1_users FROM dwd_user_behavior a JOIN dwd_user_behavior b ON a.user_id b.user_id WHERE a.behavior buy AND a.dt 2024-05-01 GROUP BY a.dt;这段 SQL 的 JOIN 条件是用户 ID 相等用IF DATE_ADD判断第二天是否还有行为。运行前要确认b.dt字段确实存在分区否则 JOIN 会把全表扫一遍查询会慢到让你怀疑 hive 是不是坏了。算完之后用 Python 侧从原始日志文件直接读一遍算同一指标from collections import defaultdict users_day0 defaultdict(set) users_day1 defaultdict(set) with open(raw_logs/dt2024-05-01/events_1030.tsv) as f: for line in f: parts line.strip().split(\t) uid, behavior, _, ts parts[0], parts[2], parts[3], parts[4] if behavior buy: users_day0[uid] True # 判断时间是否落在次日这里简化实际用 datetime 解析 users_day1[uid] behavior in (pv, cart, fav, buy) retention len([u for u in users_day0 if u in users_day1]) / max(len(users_day0), 1) print(fretention: {retention:.2%})我一般只抽一天的数据做交叉验证不会全量跑确认两套结果偏差在可接受范围就够。留一条这样的验证记录答辩时你可以直接说“hive 结果经过独立脚本验证误差来自时间窗口边界不是逻辑错误”这句话的分量远大于多写几个图表。别把 hive 当成不可挑战的黑匣子——你亲手验证过的数据才经得住追问。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑