资讯详情 Python电商用户行为分析:从爬虫采集到Hive数仓再到Django看板
📅 2026/10/11 16:52:12
简介这份资源是一套基于Python和Django框架开发的电商用户行为分析系统适用于毕业设计、课程设计及大作业等场景也适合不同学习阶段的技术爱好者作为实战练手项目。系统包含管理员端与用户端管理员可管理商品信息、商品类型、购物日志、充值记录、订单等模块用户端则提供商品推荐、商品详情、公告资讯、购物车、个人中心等功能能有效支撑电商平台的日常运营与分析需求。资源包内共包含609个文件压缩包整体约48.81MB主体以Vue前端组件、SVG图标、JavaScript脚本、Python后端源码、SQL数据库文件、批处理启动脚本及项目文档构成结构清晰便于按需查阅。已有96人浏览学习可作为工程项目起步参考。拿到压缩包后可根据安装运行脚本快速完成Hive数据库初始化与系统启动直接查看前后端完整实现、数据表设计及配套说明文档节省搭建环境与编写核心功能的时间适合作为毕业设计、课程设计或实训项目的直接参考。1. 电商用户行为分析系统一条从爬虫到看板的数据流水线电商用户行为分析是 Python 毕设里出现频率很高的题目但大多数人把它做成「一个爬虫加一张静态报表」交付时才发现卡点根本不在采集端而在数据怎么组织、口径怎么统一。基于 Python 的电商用户行为分析系统标准技术栈是 spider 抓行为日志、Hive 做离线清洗与指标计算、Django 把结果变成可交互的 Web 看板这条链路本身比任何单点技术都值钱。适合有一学期 Python 基础、想完整走一遍从数据采集到可视化全流程的人。这篇笔记不堆概念按数据流动顺序把每层怎么做、参数怎么调、坑在哪里讲清楚。2. 采集层先行用 spider 把页面行为变成可分析的结构化日志2.1 先定 schema 再写爬虫行为日志最少需要五个字段很多毕设翻车的第一个原因是爬虫先把数据抓回来了再回头想「我该存什么」。行为分析的数据源不是商品快照而是用户与页面的每一次交互所以落地到日志里的每一行必须能回答四个问题谁、对什么、做了什么、什么时候。我一般会先定一张最小可行的行为表字段跟公开的 UserBehavior 数据集保持同一套口径user_id 用户标识item_id 商品标识category_id 商品所属类目behavior_type 行为类型pv/cart/fav/buy 四选一ts 行为发生的 Unix 时间戳秒级。再加一个 dt 日期分区字段给下游 Hive 按天扫描用。这套 schema 的用意在于pv 到 buy 的漏斗、用户购买间隔、商品类目偏好全部可以从这五个字段推出来。如果毕设想做得更完整可以在采集时额外抓 item_title 和 price 两个商品维度过来的字段但核心分析不依赖它。先用这个结构去设计爬虫代码写起来会顺很多。需要注意一个现实问题毕设阶段通常没有真实业务日志可抓。常见做法是找一个允许访问的公开商品页用 spider 把商品基础信息和页面路径抓下来再按随机采样生成行为事件写入日志文件模拟出足够规模的行为数据集。这样既走通了采集流程也不触碰真实用户数据演示和答辩都能站住脚。2.2 一个最小可用的 scrapy 商品页采集器采集层用 scrapy 而不是 requests 手写循环原因是 scrapy 自带并发调度、去重和错误重试这些在毕设答辩时都是能讲的技术点。下面是一个抓商品列表页并提取商品详情链接的 spider 骨架我通常把它作为行为日志采集的起点。# spiders/item_page.py import scrapy class ItemPageSpider(scrapy.Spider): name item_page # 列表页入口按?page1的方式翻页 start_urls [https://example.com/category?page1] max_page 5 # 演示用最大翻页数防止失控 cur_page 1 custom_settings { DOWNLOAD_DELAY: 1.5, # 每个请求间隔1.5秒优先级比settings.py高 CONCURRENT_REQUESTS: 4, # 并发数压到4避免触发对方限流 RETRY_TIMES: 2, # 403/503最多重试2次重试会放大请求量 DEFAULT_REQUEST_HEADERS: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, }, } def parse(self, response): # 提取商品卡片链接用xpath比正则更抗页面结构调整 item_links response.xpath( //a[contains(class, item-link)]/href ).extract() for link in item_links: if link.startswith(/item/): # 只抓站内商品链接 yield scrapy.Request( response.urljoin(link), callbackself.parse_item, meta{from_page: response.url}, ) next_page response.xpath( //a[contains(class, next)]/href ).get() # 有下一页且未超最大页数才继续避免死循环 if next_page and self.cur_page self.max_page: self.cur_page 1 yield scrapy.Request(response.urljoin(next_page), self.parse) def parse_item(self, response): # 解析商品详情页产出行为日志里商品维度的关键字段 item_id response.xpath(//span[iditemId]/text()).get() price response.xpath( //span[contains(class, priceNum)]/text() ).get() category response.xpath( //div[contains(class, crumb)]//text() ).getall() yield { item_id: item_id, price: price, category: .join(c.strip() for c in category if c.strip()), ts: int(response.url.split(_)[-1]), }这段代码的逻辑是先在列表页把所有商品链接捞出来逐个请求详情页详情页里再去提取 item_id、价格和类目面包屑组装成一行结构化记录。meta 参数把来源页 URL 传给 parse_item方便后续排查某个字段丢失时定位是哪个页面结构的问题。参数上值得说三句。DOWNLOAD_DELAY 调到 1.5 秒、CONCURRENT_REQUESTS 压到 4是在「采集速度」和「被封风险」之间取平衡点毕设数据量几千条完全不需要追求高并发。RETRY_TIMES 千万不要调大重试会成倍放大请求量本来一次 403 能结束的事情重试 5 次反而更容易被拉黑。User-Agent 保持桌面浏览器版本即可伪装成移动端反而容易因为接口字段对不上而拿到空页面。2.3 增量采集与限速把反爬当概率问题处理爬虫最常见的翻车是把对方服务器打崩或者把自己 IP 打到被拒绝连接。我一般把反爬理解成一个概率问题对方检测的是请求特征和频率分布而不是某个单一字段。所以要比对的是「请求间隔是否均匀」「访问路径是否像人」「UA 是否常见」而不是迷信某个代理或混淆技巧。断点续爬是毕设里容易被忽略的点。scrapy 本身不持久化进度跑一半断了重启又从头开始白白浪费请求量。常见做法是给 crawl 命令加一个 JOBDIR 参数scrapy crawl item_page -s JOBDIRcrawls/item_page加了 JOBDIR 后scrapy 会把队列和去重指纹持久化到指定目录中断后重新执行同一条命令能从上次断点继续而不是从头抓。这个技巧在答辩演示现场非常加分你可以现场演示「跑两分钟 CtrlC再跑同一命令继续」比任何口头解释都有说服力。写采集层时还要养成一个习惯在 parse_item 里捕获解析异常遇到某个字段缺失时打印带 URL 的警告日志而不是直接抛异常中断整个任务。数据采集中偶发页面结构异常是常态单条失败跳过任务整体继续这才符合工程直觉。3. 数据落 Hive从原始日志到行为宽表的三个层级3.1 为什么选 Hive离线批处理才是行为日志的家行为日志一旦进入分析阶段体量和查询模式都会变。几万条数据在 Pandas 里随便跑几十万条开始吃内存到百万条以上 group by 一下就卡死MySQL 虽然能存但多表关联和窗口函数写起来非常痛苦。Hive 的核心价值在于你把 SQL 写出来它翻译成分布式任务去跑单机内存不再是瓶颈复杂聚合也有了标准的 SQL 表达方式。毕设机器通常是一台 8G 内存的笔记本照样能跑 Hive原因在于 Hadoop 伪分布式模式下任务的计算压力主要在磁盘 IO 和 JVM 内存分配只要数据量控制在千万行以内单机完全扛得住。真正需要担心的不是性能而是「文件组织方式」——也就是接下来要讲的数仓分层。为什么非要分层直接一张大表也能查但行为日志是追加型数据清洗逻辑经常会变今天发现某个时间字段格式错了要重跑明天发现 behavior_type 里有脏值要过滤。如果所有逻辑压在一张原始表上每次修正都要全量扫描重算。分层之后每一层只做一件事原始层照单全收明细层做清洗指标层做聚合。哪一层错了只重刷哪一层。3.2 ODS/DWD/ADS 三层建设DDL 与普通建表的区别数仓分层的概念很多教材都有落地到 Hive 就是三组建表语句的差别每一层表的性质在 DDL 上就要写明白。ODS 层是原始日志区特性是「不改不删」。所以必须用外部表而且要按天做分区源文件放在 HDFS 指定路径下。外部表删掉表结构不会误删数据文件这是后悔药的来源。-- ods层原始行为日志外部表分区表字段类型对齐上游采集字段 CREATE EXTERNAL TABLE ods.user_behavior_log ( user_id BIGINT, item_id BIGINT, category_id INT, behavior_type STRING COMMENT pv/cart/fav/buy, ts BIGINT COMMENT unix秒级时间戳 ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS TEXTFILE LOCATION /warehouse/ods/user_behavior_log;DWD 层是清洗后的明细宽表特征是「统一口径、列存压缩」。这里我把时间戳转成可读字符串过滤空值和非法行为类型存储格式换成 Parquet 加 Snappy 压缩查询性能比 TextFile 好一个量级。-- dwd层清洗后的行为明细parquet列存按dt分区 CREATE TABLE dwd.user_behavior_wide ( user_id BIGINT, item_id BIGINT, category_id INT, behavior_type STRING, event_time STRING COMMENT 可读事件时间, hour STRING COMMENT 小时用于日间分布分析 ) PARTITIONED BY (dt STRING) STORED AS PARQUET TBLPROPERTIES (parquet.compressionsnappy);ADS 层放应用能直接查的结果表比如每日 PV/UV/加购/购买汇总。这一层数据量很小但它是 Django 展示层的数据来源字段命名要贴近业务语言。-- ads层天级汇总指标表供django同步查询 CREATE TABLE ads.daily_summary ( dt STRING, pv_cnt BIGINT, uv_cnt BIGINT, cart_cnt BIGINT, buy_cnt BIGINT ) STORED AS PARQUET;三张表的区别在用途而不是字段多少ODS 是原始存档DWD 是清洗后的分析底表ADS 是面向应用的窄表。答辩时被问到「为什么不用一张表」把这段分层逻辑讲清楚就是核心加分项。3.3 用一段 HiveQL 完成装载与清洗建表之后要做两件事把采集日志 load 进 ODS 分区再从 ODS 清洗进 DWD。输入日志是\t分割的文本文件放到 ODS 表对应分区目录后执行分区修复即可。-- 装载将日志文件放入ODS分区目录 -- 先hdfs dfs -put user_behavior.log /warehouse/ods/user_behavior_log/dt2025-01-01/ -- 再执行msck修复分区 MSCK REPAIR TABLE ods.user_behavior_log;装载时有个常见错误直接LOAD DATA LOCAL INPATH会把本地文件移动到表的目录同时删掉源文件本地没备份就找不回来了。所以我习惯用hdfs dfs -put先拷贝再用MSCK REPAIR TABLE注册分区源文件保留随时可以重新装载。ODS 就绪后用一条INSERT OVERWRITE完成清洗逻辑。注意这里用了${hiveconf:dt}传参这是 Hive 调度最常见的传参方式配合命令行-hiveconf dt2025-01-01使用避免在 SQL 里硬编码日期。-- 清洗过滤脏数据时间戳转可读时间按dt分区写入DWD INSERT OVERWRITE TABLE dwd.user_behavior_wide PARTITION (dt) SELECT user_id, item_id, category_id, behavior_type, FROM_UNIXTIME(ts, yyyy-MM-dd HH:mm:ss) AS event_time, FROM_UNIXTIME(ts, HH) AS hour, dt FROM ods.user_behavior_log WHERE dt ${hiveconf:dt} AND user_id IS NOT NULL AND item_id IS NOT NULL AND behavior_type IN (pv, cart, fav, buy) AND ts 0 DISTRIBUTE BY dt;这段 SQL 的清洗逻辑有四个维度空值过滤、行为类型白名单过滤、时间戳合理性判断、时间字段格式化。DISTRIBUTE BY dt是容易被忽略的一行它的作用是让同一个日期的数据在写入时落到同一个 reducer避免每个 reducer 都写所有分区的文件从源头上控制小文件数量。跑完这步可以顺手验证 DWD 层是否干净SELECT dt, behavior_type, COUNT(*) AS cnt FROM dwd.user_behavior_wide GROUP BY dt, behavior_type;如果某天的买buy记录占比异常高说明行为日志里模拟数据没按真实分布生成要回采集层调概率权重而不是在分析层硬凑结论。数据质量问题的根因往往在源头这也是分层的价值——每一层都能独立自检。4. 用 Django 把分析结果做成人人可点的看板4.1 项目布局管理后台、接口、展示三个 appDjango 端最怕的是把所有代码塞在一个 app 里models 和 views 越堆越长答辩时自己也讲不清。我一般会按职责拆成三个 appdashboard 做看板页面渲染api 提供 JSON 数据接口users 管登录和权限。命令只有三条django-admin startproject ecommerce_analysis cd ecommerce_analysis python manage.py startapp dashboard python manage.py startapp api python manage.py startapp users三种角色各司其职dashboard 里的视图只负责从模型取数、渲染模板api 里的视图只返回 JsonResponse不掺 HTMLusers 用来做最简单的登录和 session 管理让看板像「系统」而不是一个裸页面。app 的边界划清楚之后新增功能时不用在几百行文件里找函数。这里我踩过一个很实在的坑新建 app 之后必须去 settings.py 的 INSTALLED_APPS 里注册否则 migrate 时表建不出来模板也找不到。Django 启动报错不会直接说「你没注册 app」而是绕到各种奇怪的 No module named 或 table not found排查起来非常耗时。4.2 数据从 Hive 到 MySQL同步脚本与模型设计Django 不会直连 Hive。生产环境里 Hive 的响应延迟对 Web 请求不可接受最常见做法是把 ADS 层结果同步到 MySQLDjango 连 MySQL 展示。同步这一步可以用 Sqoop 做定时导出sqoop export \ --connect jdbc:mysql://localhost:3306/ecommerce \ --username root --password 123456 \ --table ads_daily_summary \ --export-dir /warehouse/ads/daily_summary/dt2025-01-01 \ --input-fields-terminated-by \t \ --update-key dt \ --update-mode allowinsert--update-key dt和--update-mode allowinsert是 sqoop 导出的关键参数组合效果是已有日期更新、新日期插入不会因为重复跑导出产生重复行。这相当于给数据同步加了一层幂等保护。Django 这边模型与 MySQL 表映射要严格控制字段类型。Hive 的 BIGINT 对应 BigIntegerField日期字段用 DateFieldcount 类指标用默认值 0 而不是允许空值这样页面渲染时不会出现空数据报错。# dashboard/models.py from django.db import models class DaySummary(models.Model): dt models.DateField(primary_keyTrue) pv_cnt models.BigIntegerField(default0) uv_cnt models.BigIntegerField(default0) cart_cnt models.BigIntegerField(default0) buy_cnt models.BigIntegerField(default0) class Meta: db_table ads_daily_summary ordering [-dt]Meta 里的db_table指到 sqoop 导出的 MySQL 表名Django 就不会擅自建一张新表。注意我没用 Django 的迁移去建这张表而是让它直接映射 Sqoop 建好的表避免两边结构管理冲突。4.3 接口与图表一个返回 JSON 的最小视图后端要喂给前端的数据结构通常按「最近 N 天趋势」「今日指标卡片」「品类分布」三种形态组织。最省事的做法是一个 JSON 接口对应一块图表前端拿到 data 直接渲染。# api/views.py from django.http import JsonResponse from django.utils.dateparse import parse_date from dashboard.models import DaySummary def summary_trend(request): # 处理前端传来的起始和结束日期带上默认值 start request.GET.get(start, 2025-01-01) end request.GET.get(end, 2025-01-31) rows DaySummary.objects.filter( dt__gteparse_date(start), dt__lteparse_date(end) ).values(dt, pv_cnt, uv_cnt, buy_cnt, cart_cnt) # values返回QuerySet需要转list才能被JsonResponse序列化 return JsonResponse({code: 0, data: list(rows)})这里有个新手必踩的坑values()返回的是 QuerySet 不是 list直接丢给 JsonResponse 会报 TypeError。转成 list 是一行代码的事但第一次遇到时会在调试页面耗掉不少时间。视图写好后在 urls.py 里注册路由同时打个最简单的登录校验用 Django 自带的login_required装饰登录接口避免看板裸奔到公网。前端图表我用 ECharts 的折线图和柱状图日期作为 x 轴pv/buy 数量作为 y 轴dt字段直接从接口数据里取不需要在后端做二次拼装。这个接口设计基本就是毕设演示的核心——开着 Django 服务浏览器输入地址图表秒出。5. 从采集到展示的避坑清单小文件、时区、CSRF 与编码毕设项目的血泪经验基本都集中在基础设施层。这些问题单个看都不难但每一条都能卡掉一整天我把它们按「现象 → 原因 → 解决」记录下来。5.1 Hive 小文件太多查询慢到怀疑人生现象刚跑完清洗还能接受多跑几天之后DWD 层一个COUNT(*)都要等好几分钟甚至 Hive 直接卡在 Tez 的 container 申请阶段。原因组装行为日志时每个 reducer 往每个分区各写一份输出文件。分区多、reducer 多文件数量就是两者乘积几千个小文件让 HDFS 的 NameNode 内存和任务调度瞬间吃满。解决源头控制加合并兜底。源头控制用DISTRIBUTE BY dt让同一天的数据进同一个 reducer每个分区只落少数文件兜底用 Hive 的合并参数在跑重分区或大查询前先设置SET hive.merge.mapfiles true; SET hive.merge.size.per.task 256000000; SET hive.merge.smallfiles.avgsize 16000000;这类参数的核心是告诉 Hive输出文件小于阈值时就自动合并。写代码的时候我不会一次性全开而是先跑一次小查询看执行计划里的文件数再决定要不要开盲开参数反而会影响正常查询性能。5.2 行为时间戳差 8 小时现象DWD 层 event_time 转换出来的时间比日志里记录的时间整整早了 8 个小时。原因爬虫端写入的 ts 是按北京时间生成的 Unix 时间戳但 Hive 的FROM_UNIXTIME默认按服务器操作系统时区转换服务器装的是 UTC转出来自然少 8 小时。解决在 Hive 会话里设置时区或者干脆在 SQL 里做时区补偿。我一般用后者因为不用依赖每台机器上的 hive-site.xml 配置是否一致SET timezone Asia/Shanghai; SELECT FROM_UNIXTIME(ts, yyyy-MM-dd HH:mm:ss) FROM ods.user_behavior_log LIMIT 1;更稳的做法是在采集端就多存一个可读时间字符串字段让 Hive 层只做透传不做时区转换。分析系统里时间口径必须统一宁可在源头多算一步也不要在分析层到处补偏移。5.3 Django 删除对象报 405ORM 的 delete 不是软删现象前端调一个删除记录的接口浏览器返回 405 Method Not Allowed后端明明写了objects.filter(...).delete()。原因Django 的视图函数默认只实现了 GET 和 POST如果用 GET 请求去调删除语义的接口会直接 405。另外如果是用fetch从页面发请求没有带 CSRF token还会被 Django 的 CSRF 中间件拦截表现为 403。两个错误混在一起新手容易误判。解决删除操作用 POST并在页面请求里带上 CSRF token如果是纯接口给前端框架调用用csrf_exempt标注后再做权限校验。from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse csrf_exempt def delete_record(request, record_id): if request.method ! POST: return JsonResponse({code: 1, msg: must be POST}, status405) deleted, _ DaySummary.objects.filter(pkrecord_id).delete() return JsonResponse({code: 0, deleted: deleted})顺带把 ORM delete 的语义说清楚QuerySet.delete()是真正的物理删除而且会级联删除外键关联的数据不是软删。毕设阶段如果要做「恢复」功能必须在模型里加 is_deleted 字段自己做软删否则数据没后悔药。5.4 爬虫偶发空数据和乱码现象日志里某几行 item_id 为空或者商品标题出现「锟斤拷」一类的乱码正则表达式匹配结果为空列表。原因两类问题。乱码大多是页面响应编码不是 UTF-8比如 GBK 或 GB2312而 requests/scrapy 默认按 UTF-8 解码正则匹配为空则是贪婪匹配把跨节点内容也吞进去了或者页面结构在不同分页略有差异。解决先确认页面对应编码打印响应前 200 个字符而不是直接解析正则里用非贪婪量词.*?控制匹配范围。下面这段是最常用的调试套路# 调试响应编码 raw response.body if bcharsetgbk in raw.lower(): text raw.decode(gbk, errorsignore) else: text response.text # 用非贪婪匹配提取标题字段 title re.search(rtitle(.*?)/title, text, re.S)errorsignore是解码兜底宁可丢弃个别字符也不能让脚本中断。正则里的.*?换成非贪婪后标题匹配会停在第一个闭合标签处而不是一路吞到页面末尾这是爬虫正则里最常见也最隐蔽的翻车点。5.5 Hive 3.1.3 启动报 ClassNotFoundException现象hive命令初始化阶段抛ClassNotFoundException: org.apache.hadoop.hive.ql.Driver或者连上 Metastore 之后执行 SQL 立刻报错退出。原因Hive 的 lib 目录下 guava 版本与 Hadoop 自带的 guava 不一致两者冲突导致 Hive 运行时加载不到核心类。这类问题在 Hive 3.1.3 搭配 Hadoop 3.x 的环境里尤其常见。解决用 Hadoop 集群里同版本的 guava 替换 Hive lib 下的旧版 guava替换前给原文件做备份出问题能快速还原。Hive 是重环境依赖的组件装好之后第一件事是跑一遍hive --version确认启动链路完整再进 beeline 建库不要一上来就跑建表脚本环境问题会被误判成业务问题。6. 进阶用窗口函数补上留存与购买间隔分析前面的 ADS 层只算了 PV、UV、购买量这些基础聚合但电商行为分析真正出彩的地方是看用户行为的时间规律。Hive 窗口函数在这里是关键工具它能在不改变行粒度的情况下让每行数据带上「前后文」做留存和购买周期分析时比 group by 灵活得多。最常见的场景是计算用户两次购买之间的平均间隔。先用 LAG 窗口函数把同一用户上一次购买时间挂在当前行上再求差值聚合WITH buy_seq AS ( SELECT user_id, event_time, LAG(event_time, 1) OVER ( PARTITION BY user_id ORDER BY event_time ) AS prev_time FROM dwd.user_behavior_wide WHERE behavior_type buy AND dt BETWEEN 2025-01-01 AND 2025-01-31 ) SELECT user_id, AVG(UNIX_TIMESTAMP(event_time) - UNIX_TIMESTAMP(prev_time)) AS avg_interval FROM buy_seq WHERE prev_time IS NOT NULL GROUP BY user_id;LAG 的三个参数分别控制取前几行、缺省返回值和是否分区内有序。第一次做这个分析时我把 PARTITION BY 写漏了结果所有用户的购买时间串在一起算间隔得出来的平均值完全失真。验证方法很简单挑一个用户在 Excel 里手动排一下购买日期和 SQL 结果对一遍就能定位问题。留存的简易近似也可以用窗口函数做。先给每个用户打上首购日期标签再统计他后续每天的活跃情况用首购日期分组、日期偏移作为留存天数一张透视表就出来了。这种用窗口函数替代复杂自关联的写法数据量在百万行以下时比 join 快很多代码也更短。做这套系统的最后一步我习惯把 Hive 的分析结果和 Django 里的图表做一次交叉验证随机选三天用 SQL 手算 PV 和 UV再对比看板接口返回的数字。能对上说明整条链路的口径是通的对不上就回头查清洗逻辑。有段时间我写窗口函数总把它当聚合函数用死磕 group by 一堆字段还查不出问题后来才想明白窗口函数不折叠行数它是给明细数据加列理解了这一点之后所有分析都顺了。希望这个思路帮你在自己的毕设里少走几步弯路。本文还有配套的精品资源点击获取