尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Django在线音乐数据采集实战:从爬虫抓取到Web播放下载全流程解析

Django在线音乐数据采集实战:从爬虫抓取到Web播放下载全流程解析 在刚结束的一个练手项目里我把整个“django在线音乐数据采集”的流程完整走了一遍从爬虫抓取公开的歌曲元数据到Django建模落库再到前端搜索、播放、下载全部打通。这套东西听起来像是个课程设计但实际做完之后我发现它特别适合拿来当Python Web开发的综合练习几乎把Django最常用的技能点全串起来了。如果你也想做一个能展示爬虫数据、能搜索、能下载的音乐站那这篇文章应该能帮你少走不少弯路。我先说下这项目具体是干嘛的利用Python爬虫从公开的网页接口抓取音乐信息歌名、歌手、专辑、时长、封面地址、播放地址等然后通过Django框架做一个管理后台和用户前端支持浏览、搜索、点击播放以及文件下载。整个项目是一个完整的前后端一体Web应用源码编号22647也是我当时整理的版本号。文章后面我会把设计思路、关键代码、常见的坑都写清楚适合已经懂一点Python基础、想进阶Django的同学参考。1. 项目概述与设计思路1.1 这个项目解决什么问题很多人一听到“在线音乐数据采集”第一反应就是做个爬虫去别人的音乐网站抓数据。但实际做下来你会发现单纯写爬虫太简单了难的是把数据完整地整理、存储、展示出来让这些数据真正在一个Web应用里跑起来。这个项目解决的就是“从数据获取到数据消费”的完整链路。具体来说包括三件事第一从公开的音乐榜单或搜索接口里采集歌曲的元数据比如歌名、歌手、专辑、封面、时长第二用Django的ORM设计数据模型把这些数据结构化地存进MySQL或SQLite并提供一个干净的后台管理界面第三写一个用户可以访问的页面支持按歌手、歌名搜索点进去可以看到详情甚至能试听或下载。整个流程走完你就能明白爬虫、Web框架、数据库、前端模板是怎么协同工作的。我选择Django作为主框架原因也很直接——它自带admin后台和ORM开发效率极高。对于一个以数据管理为主的项目来说Django比Flask、FastAPI更适合因为后台管理、数据库迁移、表单校验这些功能都是现成的不需要自己从零搭。后面我会详细讲每个模块是怎么实现的。1.2 技术选型背后的理由技术栈清单是这样的组件选择理由Web框架Django 4.x自带admin、ORM、模板系统开发效率高数据库SQLite开发/ MySQL生产开发时用SQLite零配置部署时换MySQL爬虫requests BeautifulSoup4 / lxml请求和解析足够轻量不需要Scrapy那么重前端Django Templates Bootstrap不搞前后端分离减少复杂度适合单机部署后台Django Admin定制用Xadmin或simpleui美化也可以自己写CSS这里重点说为什么爬虫不用Scrapy。Scrapy是强大的爬虫框架但在这个项目里我们采集的页面量级并不大可能就是几个榜单页面数据在几百到几千条用requests加BeautifulSoup就能搞定代码逻辑更直观也更容易和Django项目集成。如果你用了Scrapy还得单独跑一个爬虫进程处理信号和管道对练手项目来说反而繁琐。Django版本我建议直接用4.x或者最新稳定版Python用3.10以上。老教程里的Django 2.x写法在URL配置上差异很大别踩这个坑。1.3 整体项目结构规划我创建Django项目之后目录是这样划分的music_project/ ├── manage.py ├── music_project/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── music/ # 核心应用 │ ├── models.py # 音乐数据模型 │ ├── views.py # 视图函数 │ ├── urls.py # 应用路由 │ ├── admin.py # 后台管理注册 │ ├── management/ # 自定义爬虫命令 │ │ └── commands/ │ │ └── scrape_music.py │ ├── templates/music/ # 前端模板 │ │ ├── music_list.html │ │ ├── music_detail.html │ │ └── search.html │ ├── static/ # 静态文件 │ └── services/ # 爬虫和数据处理业务逻辑 │ ├── scraper.py │ └── downloader.py └── requirements.txt这个结构的关键在于把爬虫逻辑放到management/commands里这样我可以通过python manage.py scrape_music命令直接采集数据和Django的数据库环境无缝衔接。services/scraper.py放具体的解析代码命令行里只要调用它就行。这样管理和爬虫分开后续改采集规则很重要。2. 在线音乐数据采集模块拆解2.1 数据源的选取与解析策略采集数据首先要找到合法的目标。我平时做技术演示会选那些公开的音乐开放接口或排名页面比如某些音乐平台提供的榜单网页页面里包含歌曲的JSON数据接口是公开的不需要登录。项目里我模拟了一个公开的数据源页面结构类似于div classsong-item span classsong-name晴天/span span classsong-author周杰伦/span span classsong-album叶惠美/span span classsong-duration04:29/span /div实际爬虫中解析过程就是先用requests获取HTML再用BeautifulSoup定位这些DOM节点。import requests from bs4 import BeautifulSoup def fetch_music_list(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) songs [] for item in soup.select(.song-item): name_node item.select_one(.song-name) author_node item.select_one(.song-author) album_node item.select_one(.song-album) duration_node item.select_one(.song-duration) if name_node and author_node: songs.append({ name: name_node.get_text(stripTrue), author: author_node.get_text(stripTrue), album: album_node.get_text(stripTrue) if album_node else , duration: duration_node.get_text(stripTrue) if duration_node else }) return songs这段代码的核心思路是先设置一个常见的浏览器User-Agent避免被最简单的反爬拦截然后用resp.apparent_encoding去解决中文乱码最后通过CSS选择器精准取值。注意select_one在元素不存在时会返回None要做空值判断不然直接取get_text会报错。2.2 请求头伪装与反爬应对采集模块里最容易出问题的就是反爬。小时候我天真的以为设置一个User-Agent就够了结果第一次写就被封了IP。后来总结出最少要处理的三个方面请求头补齐除了User-Agent还应该带上Referer、Accept、Accept-Language特别是Referer很多站点会校验你从哪个页面跳转过来的。请求频率控制直接写一个time.sleep(1)或者用随机间隔避免短时间内高频请求同一域名。异常捕获与重试用requests.adapters.HTTPAdapter来配置重试连接遇到临时网络错误可以自动重试一两次。我写的请求模块是这样的import time import random import requests from requests.adapters import HTTPAdapter HEADERS { User-Agent: 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, Referer: https://example.com/, Accept: text/html,application/json,text/plain,*/*, Accept-Language: zh-CN,zh;q0.9 } def get_session(): session requests.Session() adapter HTTPAdapter(max_retries3, pool_connections10, pool_maxsize20) session.mount(https://, adapter) session.mount(http://, adapter) session.headers.update(HEADERS) return session def safe_request(url, sessionNone): session session or get_session() for attempt in range(3): try: resp session.get(url, timeout8) if resp.status_code 200: return resp elif resp.status_code in (403, 429): wait_seconds 2 ** attempt random.uniform(0, 1) time.sleep(wait_seconds) except requests.exceptions.Timeout: time.sleep(1) except requests.exceptions.ConnectionError: time.sleep(1.5) return None这段逻辑里有两个细节一是HTTPAdapter(max_retries3)允许底层自动重试不会再因为临时网络问题抛ConnectionError二是遇到403/429这种反爬状态码会用指数退避的方式等待可以大幅度减少被封的风险。2.3 数据清洗与入库前的规范化拿到原始页面数据后绝不能直接入库。我在实际处理的时候发现原始数据里经常有这些问题歌名前面有空格、时长格式不统一、专辑名是空值、有的歌曲名里有特殊符号。所以清洗这一步很重要我一般会写一个clean_data函数统一处理。def clean_music_item(raw_item): name raw_item.get(name, ).strip() author raw_item.get(author, ).strip().replace(群星, Various Artists) album raw_item.get(album, ).strip() duration raw_item.get(duration, ).strip() # 统一时长格式为秒数例如04:29 - 269 seconds 0 parts duration.split(:) if len(parts) 2: seconds int(parts[0]) * 60 int(parts[1]) elif len(parts) 3: seconds int(parts[0]) * 3600 int(parts[1]) * 60 int(parts[2]) return { name: name, author: author, album: album, duration_seconds: seconds }把时长转成秒数是为了后续排序和筛选方便。页面里显示的时候再自己格式化成“mm:ss”就行。如果歌名叫空就直接丢弃该条数据可以在列表推导式里过滤掉。3. Django模型与数据库设计3.1 Music模型字段设计数据采集下来后下一步就是用Django模型落库。我在设计模型时参考了常见的音乐信息字段加上了索引和唯一约束。因为采集的是元数据所以并没有存音频二进制文件只存了音频文件的URL地址播放和下载再由视图去拉取。from django.db import models class Music(models.Model): song_id models.CharField(max_length64, uniqueTrue, verbose_name歌曲ID) name models.CharField(max_length128, db_indexTrue, verbose_name歌名) author models.CharField(max_length128, db_indexTrue, verbose_name歌手) album models.CharField(max_length128, blankTrue, verbose_name专辑) duration models.IntegerField(default0, verbose_name时长(秒)) cover_url models.URLField(blankTrue, verbose_name封面地址) audio_url models.URLField(blankTrue, verbose_name音频地址) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table music ordering [-created_at] verbose_name 音乐信息 verbose_name_plural verbose_name def __str__(self): return f{self.name} - {self.author}这里几个设计点值得说下song_id设置uniqueTrue一般源数据里有稳定的歌曲ID防止重复入库。name和author加db_indexTrue后期前端搜索按歌名或歌手查询时性能会好很多。如果数据量到百万级别可以加MySQL全文索引这里作为练手倒不用。audio_url存的是音频直链直接从源站引用。但是要注意盗链问题有些站点会校验Referer所以我在前端播放时一般通过Django视图代理一下音频流这个后面说。created_at自动记录创建时间方便按“最近更新”排序。3.2 ORM操作与批量入库采集到一批数据后最忌讳逐条循环save()性能太差。Django自带的bulk_create可以一次批量插入。但是bulk_create遇到重复记录不会自动更新所以我一般配合update_or_create或先查重再插入。实际代码里我在management/commands/scrape_music.py中这样写from django.core.management.base import BaseCommand from music.models import Music from music.services.scraper import fetch_music_list, clean_music_item class Command(BaseCommand): help 抓取在线音乐数据并入库 def add_arguments(self, parser): parser.add_argument(--url, typestr, defaulthttps://example.com/top) parser.add_argument(--limit, typeint, default100) def handle(self, *args, **options): url options[url] limit options[limit] raw_items fetch_music_list(url) objs [] for item in raw_items[:limit]: cleaned clean_music_item(item) if not cleaned[name]: continue # 检查是否已存在存在则跳过 if Music.objects.filter(namecleaned[name], authorcleaned[author]).exists(): continue objs.append(Music(**cleaned)) if objs: Music.objects.bulk_create(objs, batch_size100) self.stdout.write(self.style.SUCCESS(f成功新增 {len(objs)} 条数据)) else: self.stdout.write(没有新数据需要入库)这里用nameauthor做联合查重而不是依赖song_id原因是有些源接口并没有稳定的歌曲ID但歌名和歌手的组合基本能标识一首歌。如果源数据有愁人偶发的同一首歌发行过不同版本会重复保存也可以在模型里加一个UniqueConstraint但处理起来比较麻烦练手项目就用简单查重够了。3.3 admin后台的定制与美化Django自带的admin后台说实话有点素但是胜在实用。为了让后台能直接搜索、筛选、看封面我在admin.py里做了不少定制。from django.contrib import admin from .models import Music admin.register(Music) class MusicAdmin(admin.ModelAdmin): list_display (name, author, album, duration_text, created_at) list_filter (author,) search_fields (name, author, album) list_per_page 20 def duration_text(self, obj): minutes, seconds divmod(obj.duration, 60) return f{minutes:02d}:{seconds:02d} duration_text.short_description 时长如果你嫌原生样式太丑可以换个主题。我用的还是简洁方案在settings.py里注册一个simpleui或者直接给admin的css重写样式。更多时候我觉得换个后台主题并不改变功能重点是search_fields在这里是走数据库索引的效率比较高。我试过用list_filter按歌手筛选当歌手很多时后台会列一大串反而不好用。如果歌手是固定的一批标签可以考虑用外键关联歌手表但作为单表模型就先用字符字段将就。admin界面美化方面如果想从零开始自定义可以把后台换成Django Admin后台自定义CSS但维护成本高。我用的办法是引入django-admin-interface这个第三方库它可以在后台上传LOGO、调整配色并且不改动原有逻辑。4. 前端浏览与下载功能实现4.1 列表页与搜索过滤用户端我做了简单的音乐列表页和详情页模板放在templates/music/下面。列表页直接展示最近入库的歌曲同时支持关键词搜索。视图函数用Django的ListView配合搜索条件from django.views.generic import ListView from .models import Music class MusicListView(ListView): model Music template_name music/music_list.html context_object_name musics paginate_by 24 def get_queryset(self): queryset super().get_queryset() keyword self.request.GET.get(keyword, ).strip() if keyword: queryset queryset.filter(name__icontainskeyword) | \ queryset.filter(author__icontainskeyword) return queryset这里用icontains是大小写不敏感的模糊匹配。不过要注意如果keyword里有特殊符号比如%或者_Django的ORM会自动转义不用额外处理。但是icontains在MySQL里会全表扫描数据量大以后建议放在Elasticsearch或者用子查询但练手项目完全够用。对应模板里的搜索框很简单form methodget action input typetext namekeyword value{{ request.GET.keyword }} placeholder搜索歌曲或歌手 button typesubmit搜索/button /form列表页用Bootstrap卡片展示封面和歌名点击进入详情页。分页用的是Django自带Paginator在模板里渲染is_paginated即可。4.2 详情页与音频文件处理详情页展示歌曲完整信息还要有播放器。我直接用了HTML5的audio标签把audio_url填进去就能试听。但这里有坑如果目标音频地址有防盗链浏览器直接访问会403。这时必须在后端做一个代理视图把请求转发到目标服务器再把流返回给前端。from django.http import StreamingHttpResponse import requests def proxy_audio(request, music_id): music Music.objects.get(idmusic_id) audio_url music.audio_url if not audio_url: return HttpResponse(status404) resp requests.get(audio_url, streamTrue, headers{Referer: }) response StreamingHttpResponse( resp.iter_content(chunk_size4096), content_typeresp.headers.get(Content-Type, audio/mpeg) ) response[Content-Disposition] finline; filename{music.name}.mp3 return responseStreamingHttpResponse是Django处理大文件流的推荐方式。它不会一次性把整个音频文件读进内存而是按块传。这里注意不要把源站的Referer带过去有些站点会校验带错反而不行。我在实际测试中很多公开的试听地址都不需要Referer但如果源站要校验就得请求时带上从源头页面拿到的正确Referer有些隐蔽地址甚至还要带Cookie。下载功能本质上和代理差不多区别只是把响应头改成attachmentresponse[Content-Disposition] fattachment; filename{music.name}.mp3这样用户点击下载就会弹出保存文件而不是直接播放。4.3 下载功能与StreamingHttpResponse很多教程里下载文件是直接把整个文件读进内存再传给浏览器但那样会占用大量内存万一文件是几百MB服务器直接卡死。用StreamingHttpResponse的好处是边下载边写内存占用非常小。完整下载视图可以这样写import requests from django.http import StreamingHttpResponse def download_music(request, music_id): music Music.objects.get(idmusic_id) audio_url music.audio_url if not audio_url: return HttpResponse(音频地址为空, status404) upstream requests.get(audio_url, streamTrue) if upstream.status_code ! 200: return HttpResponse(源站文件不可用, statusupstream.status_code) filename f{music.name} - {music.author}.mp3 response StreamingHttpResponse( upstream.iter_content(chunk_size8192), content_typeaudio/mpeg ) # Windows浏览器识别UTF-8文件名需要urlencode from urllib.parse import quote response[Content-Disposition] fattachment; filename*UTF-8{quote(filename)} return response这里文件名用了filename*UTF-8格式因为中文文件名直接用filename会被浏览器忽略用这个标准可以让中文正常下载。踩过不少坑老铁们注意。5. 常见问题与排查技巧实录5.1 中文乱码与编码识别爬虫最常遇到的就是乱码。很多网页虽然声明了utf-8实际返回可能是gbk。直接resp.encoding不管用的时候用resp.apparent_encoding自动检测大多数情况能对。但apparent_encoding也不是100%靠谱如果页面里有奇奇怪怪的字节检测结果也有误差。稳妥的办法是看响应头里的Content-Type字段比如resp.headers.get(Content-Type)里面通常会写charsetgbk直接按那个编码解析。或者可以这样先拿原始bytes用chardet.detect检测。不过ch丁是个重库为了一个编码功能引入它有点不划算所以一般我用apparent_encoding加手动指定兜底。Django项目里保存中文也要确保数据库是utf8mb4MySQL建库时用CREATE DATABASE ... CHARACTER SET utf8mb4否则生僻字和emoji会报错。5.2 重复数据与去重策略数据采集跑多了以后库里都是重复记录。我之前遇到的情况是爬虫脚本每天跑一次如果不做任何判断一周后库里重复了一半数据。解决的办法有三个层级第一层源接口是否有唯一ID有就靠song_iduniqueTrue约束。第二层模型加UniqueConstraint比如models.UniqueConstraint(fields[name, author, album], nameunique_music)。第三层在代码里先查再插就像前面写的filter().exists()。第一层最可靠但源站不一定提供ID。第二层会直接拦截数据库层面的重复但会抛IntegrityError需要在代码里捕获。第三层好处是不会报错但多一条查询如果数据量极大批量查重效率不高。我一般做组合方案先查批量去重再bulk_create因为练手项目数据量小简单直接。5.3 超时、重试与断点续采爬虫跑一半挂了是最无语的事。为了尽可能稳住我在请求层做了超时和重试但还不够。如果目标有几千页数据跑着跑着网络抖动前面采的数据全丢了。这时需要记录增量状态或者叫“断点续采”。我的做法是维护一张采集日志表记录当前采集页码或者最新歌曲的唯一ID。每次启动爬虫时先查上次采到哪再往后继续。因为源码里没加这个表我在services里单独写了一个基于本地文件记录偏移量的类简单好用class ResumeManager: def __init__(self, task_name): self.filepath f{task_name}_resume.txt self.offset self._load() def _load(self): try: with open(self.filepath, r, encodingutf-8) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return 0 def save(self, offset): with open(self.filepath, w, encodingutf-8) as f: f.write(str(offset))每次成功采集一批后调用save记录结束的偏移量。下次运行时读回来继续。如果是分布式任务这个方式就不太行了但在单机Django命令里够用。6. 实操心得与项目扩展建议6.1 我在开发过程中踩过的坑第一坑是Django项目的静态文件路径。开发时如果DEBUGTrue静态文件靠django.contrib.staticfiles自动找但部署后必须手动执行python manage.py collectstatic否则CSS、JS全丢。我在测试环境因为这个问题排查了半天页面纯HTML没样式以为是模板写错了。第二坑是StreamingHttpResponse和中间件的关系。如果你在中间件里做了全局缓存比如CacheMiddleware流式响应可能被缓存导致用户拿到一堆乱码。我的做法是这类视图上加cache_control(no_cacheTrue)保证不会被缓存。第三坑是爬虫请求别人服务器的时候要注意对方版权和数据使用条款。我做的音乐数据采集主要是研究用途采集之后也只是展示歌曲元数据没有大范围分发音频文件。这点一定要有版权意识尤其是做在线演示的时候建议用自己生成的数据或者平台公开的试用接口不要把有版权的音频直接部署到公网。我还记得第一次跑爬虫的时候把请求间隔设成0.1秒跑了一个小时源站把一个机房IP都封了后来换代理池才解决。所以写爬虫要克制尽量用缓存不用重复跑的任务绝不重复跑。6.2 后续可扩展的方向这个项目做完之后我发现它有很多可以往上加的东西用户系统注册登录、收藏歌曲、创建歌单。推荐系统基于歌手、标签做简单协同过滤给用户推荐音乐。数据可视化在后台展示排行榜、歌手歌曲数量分布图可以用ECharts。异步采集把爬虫放到Celery任务队列里定时抓取最新榜单再通过WebSocket推送给前端。前后端分离Django只做REST API前端用Vue或者React写SPA这也是目前主流方案。如果往深了做还可以把音频文件下载到本地转存到对象存储用分布式文件系统缓存这样就不会依赖源站稳定性了。我还试过加一个简单的“相似歌曲”推荐直接按歌手分组再按时长相近度排序虽然逻辑简单但用户反馈还不错。这种小功能不需要上机器学习一条ORM查询就能搞定很适合你拿来练手。源码里我用了requirements.txt管理依赖核心只有Django4.0,5.0、requests、beautifulsoup4、lxml。如果你在部署环境装不上lxml也可以把解析器换成html.parser虽然速度会慢一点但不用编译库。如果你不想用MySQL修改DATABASES配置里的ENGINE为django.db.backends.sqlite3就行基本不用改别的代码。最后分享一个我实际帮你避坑的小技巧开发初期尽量用SQLite代码全部带好后再切MySQL。用SQLite连迁移都不用装项目跑起来非常方便切MySQL时只需要pip install mysqlclient或者pymysql注意pymysql需要额外执行一句pymysql.install_as_MySQLdb()否则Django会报“No module named MySQLdb”。这个项目的源码我整理得不算复杂但每个功能点都对应了一个常见的Web开发场景。你拿到手之后可以按README里步骤先把爬虫命令跑一次然后顺着admin后台把数据看一遍再打开前台页面搜索基本就能理解整个数据流了。要是还想加深把文章里的每个代码块自己敲一遍然后给项目加上一个你自己的小功能比看十遍教程都管用。
返回列表