资讯动态

Django+Scrapy用户行为分析系统:从爬虫到画像的全链路实践

发布时间:2026/10/18 10:05:23 来源:尧图企业网站定制
简介本资源是一套基于Python与Django开发的网易云音乐数据可视化分析系统面向计算机专业本科生、毕业设计学生及数据分析初学者聚焦用户画像构建与播放行为深度分析两大核心问题可直接用于课程设计、毕设选题或教学演示。压缩包共54个文件含24个核心Python源码涵盖Scrapy爬虫、Django后端、Matplotlib/Pyecharts可视化模块、4个CSV原始数据集、7个PNG/JPG图表结果图、3份Markdown说明文档及1份PDF设计报告辅以requirements.txt和字体等运行依赖整体10.57MB结构清晰、开箱即用。目前已有62人学习下载。读者可获得完整可运行代码、从数据采集热歌榜爬取MySQL存储到大屏展示Django动态渲染用户分群画像的全流程实现、配套详细设计文档与结课项目说明以及典型排错提示与环境配置要点具备良好延展性支持二次开发与功能拓展。1. 这不是「网易云后台」而是一套可落地的用户行为分析闭环从爬虫入库、Django实时渲染到画像标签生成与热力图联动——高校毕设答辩现场导师盯着大屏上「用户活跃时段聚类歌单偏好雷达图」连问三遍“数据怎么来的”A同学当场调出scrapy crawl hotrank -o hot.json和python manage.py runserver两个终端5秒切屏演示完整链路它不卖API、不接官方SDK、不依赖任何外部服务整套系统跑在本地MySQL Django ECharts三件套上。核心价值不是“看起来炫”而是把「播放行为→清洗规则→用户分群→可视化归因」这条数据链路用不到2000行Python代码全量串通。适合两类人一类是正在写毕设开题报告、卡在“如何让分析不浮于表面”的计算机本科生另一类是想快速验证用户分群逻辑、但又不想搭整套数仓的初级数据工程师。项目里没有黑匣子——爬虫字段映射表写在items.py里用户画像权重公式藏在analysis/user_profile.py的注释里连ECharts配置项都按dashboard/views.py里的get_chart_config()函数逐层return。你下载解压后看到的不是一堆静态图表而是一个能改、能调、能加字段、能换数据库的最小可行分析系统。2. 环境搭建与模块职责拆解Django项目结构即业务流程图2.1 目录结构即数据流地图从spiders/到templates/dashboard/的6层穿透解压后你会看到清晰的三层物理结构数据采集层spiders/目录下hotrank_spider.py热歌榜、playlist_spider.py歌单详情、user_spider.py用户公开行为三个Scrapy爬虫全部继承自cloudmusic/spiders/base_spider.py共用get_sign()签名算法和parse_song_detail()统一解析器数据存储层models.py中Song,Playlist,UserBehavior三张模型表字段名直译爬虫item如play_count,comment_count,score_avg外键关系明确——UserBehavior.song关联Song.idPlaylist.songs通过ManyToManyField反向关联分析与展示层analysis/包内user_profile.py基于播放频次/时长/跳过率生成LTV分层标签、behavior_cluster.py用KMeans对7日播放时段向量聚类、recommender.py协同过滤简易版仅用user_behavior__song_id和user_behavior__user_id两张表做矩阵分解。提示所有分析脚本都设计为可独立运行的管理命令比如python manage.py analyze_user_profile --days 30会自动拉取最近30天行为数据生成UserProfile新记录并更新User模型的profile_tag字段——这比写SQL再手动UPDATE更可控。2.2 核心依赖版本锚定requirements.txt里的4个关键约束requirements.txt看似普通实则暗藏玄机。重点看这四行Django4.2.7 scrapy2.11.2 pymysql1.1.0 pyecharts2.0.3Django4.2.7项目使用asgi.py启动Daphne服务器支持WebSocket实时推送用于大屏在线人数计数若升级到5.x需重写consumers.py中的AsyncWebsocketConsumer继承链scrapy2.11.2高版本Scrapy默认启用twisted异步DNS解析而网易云反爬策略会拦截非常规User-Agent头本项目在middlewares.py中强制关闭DNSCACHE_ENABLED False该配置在2.11.2中生效在2.12需改用DOWNLOADER_CLIENTCONTEXTFACTORYpymysql1.1.0MySQL驱动必须锁定此版本高版本默认开启autocommitTrue导致transaction.atomic()装饰器失效用户画像批量更新时会出现部分记录未落库pyecharts2.0.3这是最后一个兼容Jinja23.1的版本项目模板中{% include charts/song_heatmap.html %}依赖Jinja2的|tojson过滤器转义ECharts配置新版pyecharts已弃用该模式。2.3 数据库初始化三步走从空库到可运行的最小数据集项目不提供SQL dump文件而是用Django原生迁移机制保证环境一致性。执行顺序不能错# 1. 创建MySQL数据库字符集必须为utf8mb4 mysql -u root -p -e CREATE DATABASE cloudmusic DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 执行Django迁移注意migrations/0001_initial.py已预置基础表结构 python manage.py makemigrations python manage.py migrate # 3. 加载初始测试数据含3个模拟用户10首热歌2个歌单 python manage.py loaddata fixtures/initial_data.jsonfixtures/initial_data.json是关键——它包含User、Song、Playlist三类对象的JSON序列化数据其中Song对象的duration_ms字段值为2450004分5秒score_avg为4.72这些数值在dashboard/views.py的get_song_stats()函数中被硬编码为计算基准值。若你跳过此步直接运行爬虫Django Admin后台的「用户画像分析」页面会因缺少基准数据抛出UserProfile.DoesNotExist异常。3. 爬虫模块深度解析绕过网易云反爬的3个实战技巧3.1 签名算法逆向utils/crypto.py中get_sign()函数的参数组装逻辑网易云所有接口请求头必须携带X-Real-IP和X-Forwarded-For且URL参数需经AES加密。项目将核心逻辑封装在utils/crypto.py# utils/crypto.py from Crypto.Cipher import AES import base64 import json def get_sign(params: dict) - str: # 步骤1参数按key字典序排序并拼接成query string sorted_params .join([f{k}{v} for k, v in sorted(params.items())]) # 步骤2AES加密密钥固定为0CoJUm6Qyw8W8judIV为0102030405060708 cipher AES.new(b0CoJUm6Qyw8W8jud, AES.MODE_CBC, b0102030405060708) padded sorted_params (16 - len(sorted_params) % 16) * chr(16 - len(sorted_params) % 16) encrypted cipher.encrypt(padded.encode()) # 步骤3Base64编码并替换/为-_URL安全Base64 return base64.b64encode(encrypted).decode().replace(, -).replace(/, _)逻辑说明params字典必须包含csrf_token从首页HTML中正则提取、limit、offset等字段缺一不可。sorted_params拼接后长度必须为16的倍数否则AES加密报错——项目用chr(16 - len(...) % 16)填充而非PKCS#7标准填充这是网易云服务端的兼容性要求。参数说明params[csrf_token]需从https://music.163.com/响应HTML中用re.search(rcsrf_token.*?content(.*?), html)提取params[limit]在热歌榜爬虫中固定为100offset每次递增100。3.2 动态User-Agent轮询middlewares.py中RandomUserAgentMiddleware的实现细节反爬第一道关卡是User-Agent检测。项目不依赖第三方库而是维护一个本地UA池# middlewares.py import random class RandomUserAgentMiddleware: def __init__(self): self.user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 ] def process_request(self, request, spider): # 关键只对网易云域名启用UA轮询避免影响其他中间件 if music.163.com in request.url: request.headers[User-Agent] random.choice(self.user_agents) # 同时注入X-Real-IP模拟真实IP非代理IP request.headers[X-Real-IP] f11{random.randint(0,9)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}逻辑说明X-Real-IP必须是公网可路由IP段11.x.x.x属于IANA保留的TEST-NET-1且末位不能为0或255避免被识别为网关地址。若填入127.0.0.1或内网IP网易云返回HTTP 403。参数说明self.user_agents列表在spiders/base_spider.py的custom_settings中被覆盖为{DOWNLOADER_MIDDLEWARES: {...}}确保仅对网易云请求生效。3.3 请求频率控制settings.py中DOWNLOAD_DELAY与RANDOMIZE_DOWNLOAD_DELAY的协同机制网易云对单IP每分钟请求上限约60次超限返回{code: -1, msg: param error}。项目采用双保险策略# settings.py DOWNLOAD_DELAY 1.5 # 基础延迟1.5秒 RANDOMIZE_DOWNLOAD_DELAY True # 启用随机抖动0.5~1.5倍 CONCURRENT_REQUESTS_PER_DOMAIN 1 # 单域名并发1杜绝请求堆积逻辑说明RANDOMIZE_DOWNLOAD_DELAYTrue会使实际延迟在0.75~2.25秒间浮动有效规避固定间隔被识别为脚本特征。CONCURRENT_REQUESTS_PER_DOMAIN1强制串行请求虽然降低效率但保证start_requests()生成的每个Request都严格按序执行——这对需要csrf_token传递的登录态爬虫至关重要。参数说明若你修改DOWNLOAD_DELAY为0.5即使启用了随机抖动仍可能触发反爬因为0.5*0.50.25秒低于安全阈值。4. 用户画像与行为分析模块从原始点击到可解释标签的转换公式4.1 用户活跃度分层analysis/user_profile.py中calculate_activity_score()的加权逻辑用户画像不靠机器学习黑盒而是用可审计的规则引擎。核心函数calculate_activity_score(user_id: int)返回0~100分# analysis/user_profile.py def calculate_activity_score(user_id: int) - float: # 数据来源UserBehavior表近30天记录 behaviors UserBehavior.objects.filter( user_iduser_id, created_at__gtetimezone.now() - timedelta(days30) ) # 公式活跃分 0.4*日均访问次数 0.3*周均播放歌曲数 0.3*月均互动次数 daily_visits behaviors.values(date).distinct().count() / 30 weekly_songs behaviors.values(song_id).distinct().count() / 4 monthly_interactions behaviors.filter( Q(action_typelike) | Q(action_typecomment) | Q(action_typeshare) ).count() score ( min(daily_visits, 5) * 0.4 * 20 # 日均访问上限5次换算为20分 min(weekly_songs, 20) * 0.3 * 30 # 周均播放上限20首换算为30分 min(monthly_interactions, 100) * 0.3 * 50 # 月互动上限100次换算为50分 ) return round(score, 2)逻辑说明所有指标设硬上限如日均访问5次防止极端用户拉高均值。分数区间0~100对应UserProfile.ACTIVITY_LEVEL_CHOICES中的low/mid/high三级标签UserProfile.save()时自动调用此函数更新activity_score字段。参数说明behaviors.filter(...).count()在MySQL中走INDEX(created_at, user_id)联合索引若未建索引会导致全表扫描——项目migrations/0002_add_indexes.py已预置该索引。4.2 歌单偏好雷达图analysis/playlist_analyzer.py中generate_preference_vector()的7维构造大屏上的「用户歌单偏好雷达图」并非简单统计歌单类型而是基于用户播放行为反推音乐DNA# analysis/playlist_analyzer.py def generate_preference_vector(user_id: int) - List[float]: # 步骤1获取用户近90天播放过的所有歌单ID playlist_ids UserBehavior.objects.filter( user_iduser_id, action_typeplay, created_at__gtetimezone.now() - timedelta(days90) ).values_list(playlist_id, flatTrue).distinct() # 步骤2统计每个歌单的7个维度占比来自Playlist模型的genre字段 # genre字段格式华语,流行,男声,慢节奏,伤感,钢琴,深夜 vectors [] for pid in playlist_ids: try: playlist Playlist.objects.get(idpid) genres playlist.genre.split(,) # 维度顺序固定[华语, 流行, 男声, 慢节奏, 伤感, 钢琴, 深夜] vector [1.0 if g in genres else 0.0 for g in [华语,流行,男声,慢节奏,伤感,钢琴,深夜]] vectors.append(vector) except Playlist.DoesNotExist: continue # 步骤3取平均值作为用户偏好向量归一化到0~1 if not vectors: return [0.0] * 7 avg_vector [sum(col)/len(vectors) for col in zip(*vectors)] return [round(v, 2) for v in avg_vector]逻辑说明genre字段由爬虫在parse_playlist_detail()中用正则re.search(rtags:\[(.*?)\], response.text)提取原始数据为字符串数组项目存为逗号分隔字符串以简化查询。雷达图7个维度完全对应genre中高频标签无额外映射表。参数说明zip(*vectors)将[[1,0,1],[0,1,0]]转为[(1,0),(0,1),(1,0)]实现列聚合。若用户未播放任何歌单返回全零向量前端ECharts自动隐藏该用户雷达图。4.3 行为聚类可视化analysis/behavior_cluster.py中cluster_by_time_pattern()的KMeans实现播放时段分析不用热力图而用KMeans聚类揭示用户作息规律# analysis/behavior_cluster.py from sklearn.cluster import KMeans import numpy as np def cluster_by_time_pattern(user_ids: List[int]) - Dict[int, int]: # 构造特征矩阵每行用户7日播放时段向量24小时*7天168维 X [] user_map [] for uid in user_ids: # 统计该用户近7天每小时播放次数0点~23点 hourly_counts [0] * 168 behaviors UserBehavior.objects.filter( user_iduid, action_typeplay, created_at__gtetimezone.now() - timedelta(days7) ) for b in behaviors: # 将created_at转为第N天第H小时索引0~167 day_offset (timezone.now().date() - b.created_at.date()).days hour_idx day_offset * 24 b.created_at.hour if 0 hour_idx 168: hourly_counts[hour_idx] 1 X.append(hourly_counts) user_map.append(uid) # KMeans聚类k3对应早鸟/午间族/夜猫子 kmeans KMeans(n_clusters3, random_state42, n_init10) labels kmeans.fit_predict(np.array(X)) return {user_map[i]: int(labels[i]) for i in range(len(user_map))}逻辑说明n_init10确保聚类结果稳定random_state42固定随机种子便于复现。聚类中心坐标可导出为kmeans.cluster_centers_项目在management/commands/analyze_behavior.py中将其存入ClusterCenter模型供前端调用。参数说明hourly_counts向量稀疏度极高单日最多24次播放故采用np.array(X)而非稀疏矩阵内存占用可控1000用户≈13MB。5. 常见问题排查5条血泪经验总结每条都踩过坑5.1 现象Django Admin后台「用户画像」页面空白Network面板显示GET /api/user-profile/返回500原因UserProfile模型的user外键未设置on_deletemodels.CASCADE当User对象被删除时UserProfile记录残留但关联失效get_object_or_404(UserProfile, userrequest.user)抛出UserProfile.DoesNotExist。解决执行python manage.py migrate重新应用migrations/0003_fix_userprofile_foreignkey.py该迁移已修正外键约束。5.2 现象Scrapy爬虫运行后items.json为空日志显示DEBUG: Crawled (200) GET https://music.163.com/#/discover/toplist原因网易云将真实数据埋在iframe内https://music.163.com/#/discover/toplist是前端路由实际数据接口为https://music.163.com/weapi/v1/resource/comments/R_SO_4_爬虫未正确解析iframe srchttps://music.163.com/#/discover/toplist?id3778678中的id参数。解决打开spiders/hotrank_spider.py将start_urls [https://music.163.com/#/discover/toplist]改为start_urls [https://music.163.com/discover/toplist]去掉#Scrapy才能抓取真实HTML。5.3 现象ECharts雷达图显示「数据为空」浏览器Console报错Uncaught TypeError: Cannot read property length of undefined原因dashboard/views.py中get_radar_data()函数调用generate_preference_vector(user_id)时若用户无播放行为函数返回[0.0]*7但前端radar.js期望接收{value: [0,0,0,0,0,0,0], name: 用户A}格式对象未做空值校验。解决修改templates/dashboard/charts/radar_chart.html中JavaScript增加判断if (!data || data.length 0) { option.series[0].data [{ value: [0,0,0,0,0,0,0], name: 无数据 }]; }5.4 现象python manage.py analyze_user_profile命令执行后Admin中用户profile_tag字段仍为空原因analysis/user_profile.py中update_user_profile()函数调用UserProfile.objects.get_or_create(useruser)时未传入defaults参数导致新创建记录不填充activity_score等字段。解决检查management/commands/analyze_user_profile.py第45行确保get_or_create()调用形如profile, created UserProfile.objects.get_or_create( useruser, defaults{activity_score: score, profile_tag: tag} )5.5 现象大屏WebSocket连接失败浏览器Network显示ws://localhost:8000/ws/online/状态为(pending)原因Django Channels配置缺失asgi.py中未添加application ProtocolTypeRouter({ ... websocket: AuthMiddlewareStack(URLRouter([...])) })且settings.py未配置CHANNEL_LAYERS。解决确认requirements.txt已安装channels4.0.0并在settings.py末尾添加CHANNEL_LAYERS { default: { BACKEND: channels.layers.InMemoryChannelLayer, }, }6. 二次开发实战3个可立即落地的改造方案与验证方法6.1 方案一接入真实用户数据——替换爬虫为MySQL直连5分钟完成数据源切换项目默认爬取公开榜单但毕设常需分析真实用户行为日志。若你已有用户播放日志表user_play_log字段user_id,song_id,play_time,duration_ms只需两步替换数据源步骤1新建数据适配器在analysis/adapters/下创建mysql_adapter.py# analysis/adapters/mysql_adapter.py from django.db import connection def fetch_user_behaviors(user_id: int, days: int 30) - List[dict]: with connection.cursor() as cursor: cursor.execute( SELECT song_id, play as action_type, play_time as created_at FROM user_play_log WHERE user_id %s AND play_time DATE_SUB(NOW(), INTERVAL %s DAY) , [user_id, days]) return [ {song_id: row[0], action_type: row[1], created_at: row[2]} for row in cursor.fetchall() ]步骤2修改分析入口打开analysis/user_profile.py将calculate_activity_score()开头的ORM查询替换为# 替换前ORM behaviors UserBehavior.objects.filter(...) # 替换后直连MySQL from analysis.adapters.mysql_adapter import fetch_user_behaviors raw_data fetch_user_behaviors(user_id, days30) # 后续逻辑保持不变将raw_data转为QuerySet模拟对象验证方法在Django Shell中执行python manage.py shell输入from analysis.user_profile import calculate_activity_score print(calculate_activity_score(123)) # 123为你数据库中真实user_id若返回非零分数说明直连成功。此时manage.py analyze_user_profile命令将自动使用新数据源。6.2 方案二扩展画像维度——在UserProfile模型中新增「设备偏好」字段并自动填充用户常在手机/PC/平板多端使用设备信息是重要分群维度。改造只需三处① 修改模型在models.py中UserProfile类添加字段class UserProfile(models.Model): # ...原有字段 device_preference models.CharField( max_length20, choices[(mobile, 手机), (pc, 电脑), (tablet, 平板)], defaultmobile )② 编写填充逻辑在analysis/user_profile.py中新增函数def detect_device_preference(user_id: int) - str: # 统计该用户近7天各设备UA出现频次 from django.db import connection with connection.cursor() as cursor: cursor.execute( SELECT CASE WHEN user_agent LIKE %Mobile% THEN mobile WHEN user_agent LIKE %Windows% OR user_agent LIKE %Mac% THEN pc ELSE tablet END as device, COUNT(*) as cnt FROM user_play_log WHERE user_id %s AND play_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY device ORDER BY cnt DESC LIMIT 1 , [user_id]) result cursor.fetchone() return result[0] if result else mobile③ 更新保存逻辑在UserProfile.save()方法中插入def save(self, *args, **kwargs): if not self.pk: # 新建时填充 self.device_preference detect_device_preference(self.user_id) super().save(*args, **kwargs)验证方法在Admin后台创建新UserProfile查看「设备偏好」字段是否自动显示「手机」或「电脑」或执行python manage.py shellfrom myapp.models import UserProfile p UserProfile.objects.get(user_id123) print(p.device_preference) # 应输出mobile等值6.3 方案三大屏交互升级——为ECharts图表添加点击钻取点击歌单跳转详情页当前大屏图表仅展示无法下钻。要实现「点击雷达图某维度弹出该类型Top10歌单」需前后端联调前端修改static/js/dashboard.js找到radarChart.on(click, function (params) {事件添加radarChart.on(click, function (params) { const dimension [华语,流行,男声,慢节奏,伤感,钢琴,深夜][params.dataIndex]; // 发送请求获取该类型歌单 fetch(/api/top-playlists/?genre${dimension}) .then(r r.json()) .then(data { // 渲染弹窗使用Bootstrap Modal $(#playlistModal .modal-title).text(${dimension}风格Top10歌单); $(#playlistModal .modal-body).html( data.map((item, i) div classmb-2strong${i1}./strong ${item.name} (${item.play_count}万播放)/div ).join() ); $(#playlistModal).modal(show); }); });后端APIdashboard/views.py新增视图函数from django.http import JsonResponse from django.db.models import Count def top_playlists_by_genre(request): genre request.GET.get(genre, ) if not genre: return JsonResponse({error: genre required}, status400) playlists Playlist.objects.filter( genre__containsgenre ).annotate(play_countCount(userbehavior)).order_by(-play_count)[:10] return JsonResponse([ {name: p.name, play_count: p.play_count} for p in playlists ], safeFalse)URL路由dashboard/urls.py添加urlpatterns [ # ...其他路由 path(api/top-playlists/, views.top_playlists_by_genre, nametop_playlists), ]验证方法启动服务后打开http://localhost:8000/dashboard/点击雷达图任意区域观察是否弹出对应风格歌单列表。若Network面板中/api/top-playlists/?genre流行返回JSON数组则改造成功。从那以后我每次接到可视化类毕设需求都会先检查三点爬虫能否稳定产出结构化数据、Django模型字段是否与分析维度一一对应、ECharts配置是否预留了on(click)钩子——这三处不卡死后续所有交互扩展都是体力活。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑