25 行 Python,下一首歌
我常被人问"Python 爬虫到底能写多短"。以前我会甩一段 50 行、80 行的代码出来,今天我想给你看一段真正极简的——25 行不到,把一首歌从搜索到落盘完整跑通。看完你会发现,所谓"爬虫工程师"做的事情没那么神秘,大部分时候就是搜页 → 详情页 → 播放 API → 写文件这四步。
先看完整脚本
importrequestsfromlxmlimportetreeimportjson url="https://www.gequbao.com/s/%E5%91%A8%E6%9D%B0%E4%BC%A6"headers={"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ""(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",}res=requests.get(url=url,headers=headers)data=etree.HTML(res.text)info=data.xpath('//div[@class="col-9 col-md-8"]')# 每首歌一个 divforiininfo[0:3]:title=i.xpath('.//span/text()')[0].strip()# 第一跳:列表页 → 详情页aurl=i.xpath('./a/@href')[0]zwurl="https://www.gequbao.com"+aurl zwdata=requests.get(zwurl,headers=headers).text.replace('\\u0022','"')# 从详情页源码里挖出 play_idpid=zwdata.split('"play_id":"')[1].split('"')[0]# 第二跳:详情页 → 真实下载地址api="https://www.gequbao.com/api/play-url"gequ=requests.post(api,headers=headers,data={"id":pid}).json()musicurl=gequ["data"]["url"]# 落盘mures=requests.get(musicurl,headers=headers)withopen(f"{title}.mp3","wb")asf:f.write(mures.content)25 行。真的就这么多。
为什么是 xpath,不是正则?
很多新人爬虫教程一上来就教re.findall,但真正工业级的爬虫,几乎不用正则匹配 HTML。原因有三:
- HTML 是结构化文档,xpath 走的是 DOM 树,能直接锁定"第 N 层 / 第 N 个 / 某个 class 的子节点",比正则写出来的魔法串好读太多;
- DOM 是容错的——标签多一个空格、少一个换行,xpath 一般照常命中,正则经常挂;
- 现代浏览器 DevTools 自带"Copy XPath",开发者不用手写表达式。
对应到代码里这一行:
info=data.xpath('//div[@class="col-9 col-md-8"]')意思就是"在整棵 DOM 树里找所有 class 同时包含col-9和col-md-8的 div"——这两列类名是 Bootstrap 栅格的写法,几乎不会和其它 div 撞车,是相当稳的锚点。
列表页拿到的是"半截链接"
注意这一行:
aurl=i.xpath('./a/@href')[0]zwurl="https://www.gequbao.com"+aurl列表页的<a href="/music/8475848">给你的是相对路径,必须手动拼上 origin。这种半截链接在爬虫里非常常见,看到@href没带协议头就要长个心眼。
详情页里挖play_id,是个字符串手术
pid=zwdata.split('"play_id":"')[1].split('"')[0]这一行长得丑,但理解了就通——把整段 HTML 源码当成字符串,先按"play_id":"切成两半,第二段里再按"切,第一个就是纯净的 id。
为什么不直接用正则?因为play_id在页面上是个内嵌 JSON的字段,被网站为了反爬故意把双引号转义成\u0022。replace('\\u0022', '"')是先做一步反转义,把字符串还原成正常 JSON,再做字符串切割。看起来像正则的活,本质是字符串手术。
POST 请求:把 id 喂回去换链接
gequ=requests.post(api,headers=headers,data={"id":pid}).json()musicurl=gequ["data"]["url"]这一步是整个脚本的"灵魂"。注意三件事:
- 走的是
POST,不是GET——参数在请求体里,不在 URL 里,浏览器历史和日志看不到,相对更隐蔽; - 提交的是
data={"id": pid},这是表单格式;如果对方要 JSON,要换成json={"id": pid}; - 返回的是 JSON,所以直接
.json()拿到字典,再走["data"]["url"]拿真正的 mp3 直链。
这一套"前端拼请求、后端发请求"的链条,几乎所有内容站都在用——B 站、抖音、知乎、闲鱼,只是字段名不同而已。
写到本地的最后一步
withopen(f"{title}.mp3","wb")asf:f.write(mures.content)两件事千万别搞错:
- 模式必须是
"wb",二进制写。mp3 是字节流,用"w"会在 Windows 上因为编码问题坏文件; - 写的是
mures.content,不是mures.text。.text是字符串(自动解码),.content是 bytes(原始字节流),下任何媒体文件都用.content。
25 行背后看不见的事
脚本跑起来当然没那么丝滑,真正写爬虫的功夫,都在脚本之外:
- 请求头要伪装:裸
requests.get经常被 403,UA、Referer、Cookie 至少要有一个。 - 频率要克制:循环里没加
time.sleep,一分钟内连发几十个请求,IP 几分钟就被封。生产代码建议加random.uniform(1, 3)。 - 要断点续跑:网络断一次就要从头来。可以先把列表页的歌曲 id 列表落盘到 JSON,循环读 JSON,没下完的标记
pending,下次接着跑。 - 要去重:同一首歌经常在多个搜索结果里出现,最终写入前用
if title not in done_titles跳过。 - 要日志:每首歌的状态打到一个
log.txt,失败重试。
这些"脚本之外"的工程化能力,恰恰是从作业到生产的分水岭。
把这套打法套到任何内容站
这套"列表页拿 id → 详情页拿播放参数 → POST/API 换直链 → 落盘"的四步法,几乎可以套用到所有 UGC 站点:
- B 站:
__playinfo__里找baseUrl; - 抖音:分享链接里解析
aweme_id,再去/api/aweme/v1/aweme/detail/拿视频流; - 知乎:问题的
/api/v4/questions/{id}/answers拿回答列表; - 公众号文章:
__NEXT_DATA__里有正文 HTML。
字段名千变万化,思路只有一套。
写在最后
爬虫从来不是"写得多"就是高手,写得多不代表写得好。一段 25 行的脚本里,藏着对 HTTP、对 HTML、对 JSON、对文件 IO 的完整闭环;把它读懂、改熟、举一反三,比刷十份 500 行的"大项目"有用得多。
下一步建议做三件事:把 UA 和 Cookie 挪进os.environ;用asyncio + aiohttp改成并发下载;给每首歌写一个 SHA1 比对,跳过已下载文件。做完这三件事,你手里就不再是"作业脚本",而是一个真正能用的工具。
最后留一句:抓得到不等于抓得对。所有抓回来的数据,第一时间想清楚怎么用、用在哪、给谁看——技术是手段,问题才是起点。
免责声明(请务必读完)
写到这里必须把丑话讲在前面。本文的代码与思路仅用于技术学习和个人研究,请务必遵守以下边界:
- 尊重版权与平台规则。绝大多数音乐平台的用户协议都明确禁止未经授权的批量下载、绕过付费墙、二次分发。脚本能跑不代表你"有权跑"——能下到不等于"你拥有"。下载到的 mp3 只供你个人试听、学习,请勿上传网盘、转载、公开传播,更不要用于任何商业用途。
- 不破坏平台正常运行。循环里没有 sleep、并发过高、伪造身份绕过付费/登录墙——这些都是《网络安全法》《数据安全法》和《刑法》第 285、286 条里可能踩到的雷。商业爬虫合规问题请咨询律师,不要拿"个人学习"当万能挡箭牌。
- 保护好你自己的账号。脚本里所有 Cookie / SESSDATA / 登录 token 都属于个人敏感凭据。任何公开仓库、博客、社交媒体、聊天群截图里都不要贴真实凭据。本文代码块里的
<YOUR_xxx_HERE>不是装饰,是底线。 - 网站结构随时变。今天能用的接口、XPath、字段名,明天可能就
403或改名。代码示例仅供学习"思路",实际使用请以目标站点当前接口为准,自负其责。 - 出了问题,与本文无关。脚本运行造成的账号封禁、设备损坏、数据丢失、法律纠纷,均由使用者自行承担。
技术是工具,不是免责符。如果你看完觉得"原来爬虫这么简单",请把这份"简单"用在合法、正当、有边界的场景里——这是我能给你的最后一点职业建议。