Scrapy多级请求实战:5sing伴奏网爬取踩坑与优化全记录(JSON提取+Xpath解析)

Scrapy多级请求实战:5sing伴奏网爬取踩坑与优化全记录(JSON提取+Xpath解析)

前言:本次实战围绕5sing伴奏网热榜歌曲爬取展开,核心需求是获取首页热榜歌曲基础信息,并深入详情页提取歌曲分类、格式、大小、下载量等完整数据。开发过程中,核心突破点在于发现网站数据存储的差异化的特点------首页热榜数据以JSON字段形式嵌入页面源码,详情页则为标准HTML结构,由此完成了从Xpath解析到正则提取的切换,同时实现Scrapy多级请求(即大家常说的"二次爬取",专业表述为Scrapy多级请求/二级页面爬取),全程踩坑不断,最终完成优化落地,特此整理成实战笔记,供各位爬虫爱好者参考避坑。

一、核心需求与技术架构

1.1 爬取目标

目标网站:5sing.kugou.com(5sing伴奏网)

爬取范围:首页热榜(hot)歌曲,需获取两类数据:

  • 一级页面(首页):歌曲ID、歌曲名称、歌手、上传用户ID、上传用户昵称

  • 二级页面(详情页):歌曲分类、文件格式、文件大小、下载次数

1.2 专业技术架构

Scrapy多级请求(二级页面爬取)

  1. 发起一级请求:请求首页(start_urls),获取页面源码;

  2. 解析一级页面:提取热榜歌曲的基础信息(歌曲ID等),构造详情页URL;

  3. 发起二级请求:通过Scrapy.Request()携带基础信息,请求每首歌的详情页;

  4. 解析二级页面:提取详情页扩展信息,与一级页面数据合并,统一输出。

核心核心依赖:Scrapy框架、正则表达式(re)、Xpath解析、JSON数据解析。

二、开发全过程:踩坑→排查→优化

本次开发最核心的难点的是:首页与详情页的数据存储方式完全不同,初期因惯性思维使用Xpath解析首页,导致爬取失败,后续通过排查源码发现JSON字段存储的规律,切换为正则提取,逐步完成优化。以下是完整踩坑与优化记录,全程还原真实开发场景。

2.1 初始开发:惯性思维踩坑------Xpath解析首页失败

2.1.1 初始思路

基于过往爬取静态网页的经验,默认首页热榜歌曲信息会以HTML标签(如div、li、table)形式渲染,因此初始开发时,直接使用Xpath定位热榜歌曲的各个字段,核心代码如下:

python 复制代码
def parse(self, response):
    # 错误思路:用Xpath定位热榜歌曲
    song_list = response.xpath('//div[@class="hot-song"]/li')
    for song in song_list:
        singer = song.xpath('./div[@class="singer"]/text()').get()
        song_name = song.xpath('./div[@class="song-name"]/text()').get()
        yield {
            'singer': singer,
            'song_name': song_name
        }
2.1.2 踩坑现象

运行命令 scrapy crawl 5sing 后,控制台无任何数据输出,也无报错信息,排查后发现两个关键问题:

  1. Xpath路径无误,但无法提取到任何文本(返回None);

  2. 查看页面源码(F12),在Elements面板能看到热榜歌曲列表,但在Page Source(页面原始源码)中,找不到对应的HTML标签。

2.1.3 排查过程
  1. 排除反爬问题:已配置请求头(User-Agent、Cookie),且能正常获取首页源码,排除反爬拦截;

  2. 分析页面渲染方式:通过对比Elements和Page Source,发现热榜歌曲数据并非静态HTML渲染,而是通过JavaScript动态加载,数据藏在页面源码的JSON字符串中;

  3. 定位数据位置:全局搜索页面源码(Ctrl+F),关键词"hot",最终发现热榜数据被包裹在 bz_songs = {"download": [...], "hot": [...]} 这个JSON结构中,所有热榜歌曲的基础信息都在"hot"对应的列表里。

2.2 优化第一步:切换解析方式------从Xpath到正则提取JSON

2.2.1 核心突破点

首页数据存储方式:JSON字段嵌入页面源码,无法用Xpath解析,需通过正则表达式(re)截取JSON片段,再解析为Python列表,提取所需字段。

2.2.2 优化代码(核心片段)
python 复制代码
import scrapy
import re
import json

def parse(self, response):
    # 1. 获取首页原始源码
    html_text = response.text
    # 2. 正则提取 "hot": [...] 对应的JSON片段(精准匹配,避免干扰)
    pattern = r'"hot":(\[.*?\])'  # 捕获hot对应的列表内容
    hot_json = re.findall(pattern, html_text, re.S)[0]  # re.S让.匹配换行符
    # 3. JSON转Python列表,提取基础信息
    hot_list = json.loads(hot_json)
    print(f"成功提取到{len(hot_list)}首热榜歌曲,开始构造详情页请求...")
    
    # 4. 循环构造详情页URL,发起二级请求
    for song in hot_list:
        # 用歌曲ID构造详情页URL(踩坑后发现songId是核心标识,无songUrl字段)
        detail_url = f"https://5sing.kugou.com/bz/{song['songId']}.html"
        # 用meta传递一级页面数据,供详情页解析时合并
        yield scrapy.Request(
            url=detail_url,
            callback=self.parse_detail,  # 详情页解析函数
            meta={
                'singer': song['singerName'],
                'song_name': song['songName'],
                'user_id': song['userId'],
                'user_nickname': song['userNickname']
            }
        )
2.2.3 踩坑补充:构造详情页URL时的小失误

初期构造详情页URL时,误写为 f"https://5sing.kugou.com/bz/{song['songUrl']}.html",运行后报错"KeyError: 'songUrl'",排查后发现:JSON字段中只有"songId"(歌曲唯一标识),无"songUrl"字段,修正为用songId构造URL后,成功发起二级请求。

2.3 优化第二步:详情页解析------Xpath回归使用,适配HTML结构

2.3.1 关键发现:详情页与首页数据存储方式差异
页面类型 数据存储方式 解析方式
首页(一级页面) JSON字符串嵌入源码,动态加载 正则表达式(re)提取JSON,再解析
详情页(二级页面) 标准HTML标签静态渲染 Xpath解析,直接定位字段
2.3.2 详情页解析代码
python 复制代码
def parse_detail(self, response):
    # 1. 接收一级页面传递的基础数据
    base_data = response.meta
    # 2. Xpath定位详情页扩展信息(分类、格式、大小、下载次数)
    category = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[2]/text()').get()
    file_format = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[3]/text()').get()
    file_size = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[4]/text()').get()
    download_count = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[5]/text()').get()
    
    # 3. 合并一级、二级页面数据,统一输出
    yield {
        'song_id': response.url.split('/')[-1].replace('.html', ''),  # 从URL中提取歌曲ID
        'singer': base_data['singer'],
        'song_name': base_data['song_name'],
        'user_id': base_data['user_id'],
        'user_nickname': base_data['user_nickname'],
        'category': category,
        'format': file_format,
        'size': file_size,
        'download_count': download_count
    }
    print(f"✅ 完成歌曲《{base_data['song_name']}》详情页爬取,数据已输出")

2.4 优化第三步:添加日志打印,便于调试与监控

优化前,运行爬虫后无法直观看到爬取进度,若出现失败也难以定位问题,因此添加简单清晰的日志打印,监控每一步爬取状态,核心优化如下:

python 复制代码
def parse(self, response):
    print("=" * 50)
    print("✅ 首页请求成功,开始提取热榜JSON数据...")
    html_text = response.text
    pattern = r'"hot":(\[.*?\])'
    hot_json = re.findall(pattern, html_text, re.S)[0]
    hot_list = json.loads(hot_json)
    print(f"✅ 成功提取{len(hot_list)}首热榜歌曲,开始发起详情页请求")
    print("=" * 50)
    # 后续循环构造请求...

def parse_detail(self, response):
    base_data = response.meta
    song_name = base_data['song_name']
    print(f"🎵 正在爬取详情页:{song_name} | URL:{response.url}")
    # 后续Xpath解析、数据合并...
    print(f"✅ {song_name} 详情页爬取完成,数据已输出")

2.5 最终优化:规范代码结构,提升可维护性

结合前面的踩坑与优化,整理出完整可运行的爬虫代码,同时规范请求头配置、字段命名(全部改为英文,符合开发规范),避免冗余代码,确保代码可直接复制运行。

三、完整可运行代码

3.1 爬虫文件(spiders/5sing_spider.py)

python 复制代码
import scrapy
import re
import json

class A5singSpider(scrapy.Spider):
    name = "5sing"
    allowed_domains = ["5sing.kugou.com"]
    start_urls = ["https://5sing.kugou.com/index.html"]

    # 配置请求头,避免反爬
    custom_settings = {
        "DEFAULT_REQUEST_HEADERS": {
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'accept-language': 'zh-CN,zh;q=0.9',
            'upgrade-insecure-requests': '1',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
            'cookie': 'kg_mid=94ae5cb0ff9fda8bd1e89ea8c46853a4; kg_dfid=3MqGF81g2Tq64cy0SV3kT5pr; sl-session=Z1wSDz6f3GmaGwefEC9jzA==; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; ACK_SERVER_10015=%7B%22list%22%3A%5B%5B%22bjlogin-user.kugou.com%22%5D%5D%7D; cct=01259e80; wsp_volume=0.8',
        }
    }

    def parse(self, response):
        print("=" * 50)
        print("✅ 首页请求成功,开始提取热榜JSON数据...")
        html_text = response.text
        # 正则精准提取hot对应的JSON列表
        pattern = r'"hot":(\[.*?\])'
        hot_json = re.findall(pattern, html_text, re.S)[0]
        hot_list = json.loads(hot_json)
        print(f"✅ 成功提取{len(hot_list)}首热榜歌曲,开始发起详情页请求")
        print("=" * 50)

        for song in hot_list:
            # 用songId构造详情页URL,避免KeyError
            detail_url = f"https://5sing.kugou.com/bz/{song['songId']}.html"
            # 传递基础数据到详情页
            yield scrapy.Request(
                url=detail_url,
                callback=self.parse_detail,
                meta={
                    'singer': song['singerName'],
                    'song_name': song['songName'],
                    'user_id': song['userId'],
                    'user_nickname': song['userNickname']
                }
            )

    def parse_detail(self, response):
        base_data = response.meta
        song_name = base_data['song_name']
        print(f"🎵 正在爬取详情页:{song_name} | URL:{response.url}")

        # Xpath解析详情页扩展信息
        category = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[2]/text()').get()
        file_format = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[3]/text()').get()
        file_size = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[4]/text()').get()
        download_count = response.xpath('/html/body/div[5]/div[1]/div[1]/div[3]/div[1]/ul/li[5]/text()').get()

        # 合并数据并输出
        yield {
            'song_id': response.url.split('/')[-1].replace('.html', ''),
            'singer': base_data['singer'],
            'song_name': base_data['song_name'],
            'user_id': base_data['user_id'],
            'user_nickname': base_data['user_nickname'],
            'category': category,
            'format': file_format,
            'size': file_size,
            'download_count': download_count
        }
        print(f"✅ {song_name} 详情页爬取完成,数据已输出")

3.2 运行命令

powershell 复制代码
# 进入Scrapy项目根目录,执行以下命令
scrapy crawl 5sing

四、核心踩坑总结与实战经验

4.1 核心踩坑点

  1. 惯性思维误区:默认所有页面都能用Xpath解析,忽略了动态加载的JSON数据,导致首页数据提取失败;

  2. 字段错误:构造详情页URL时,误使用不存在的"songUrl"字段,引发KeyError;

  3. 调试困难:初期未添加日志打印,爬取失败后无法定位问题所在,效率低下;

  4. 数据存储差异:未提前分析首页与详情页的数据存储方式,导致解析方式不匹配,浪费开发时间。

4.2 实战优化经验

  1. 爬取前先分析页面源码:通过Page Source查看数据存储方式(是HTML静态渲染,还是JSON动态加载),再选择对应解析方式;

  2. 正则提取JSON需精准匹配:使用r'"hot":(\[.*?\])' 精准捕获目标JSON片段,避免匹配到无关内容,同时添加 re.S 适配换行符;

  3. 多级请求数据传递:使用Scrapy的meta参数,将一级页面的基础数据传递到二级页面,实现数据合并;

  4. 添加日志打印:关键节点(请求成功、数据提取、详情页爬取)添加日志,便于调试和监控爬取进度;

  5. 规范字段命名:全部使用英文字段,符合Python开发规范,避免中文字段引发的编码问题。

4.3 延伸思考

本次爬取的5sing伴奏网,首页用JSON存储热榜数据、详情页用HTML存储详情数据,这种混合存储方式在很多网站中都很常见(首页追求加载速度,用JSON动态渲染;详情页追求稳定性,用静态HTML渲染)。

后续可进一步优化方向:

  • 添加数据持久化:将爬取的数据保存到CSV、JSON文件或数据库(如SQLite);

  • 完善反爬策略:添加请求延迟、随机User-Agent,避免频繁请求被封IP;

  • 异常处理:添加try-except捕获解析失败、请求失败等异常,提升爬虫稳定性。

五、总结

本次Scrapy多级请求实战,核心突破了"首页JSON动态加载、详情页HTML静态渲染"的混合数据存储解析难题,从最初的Xpath解析失败,到通过排查源码发现JSON字段,再到切换为正则提取、完善二级请求和日志监控,全程踩坑但收获颇丰。

对于爬虫新手而言,最容易陷入"惯性解析"的误区,忽略页面数据存储的差异,本次实战也再次验证:爬取前的页面分析,远比盲目编写代码更重要。掌握Scrapy多级请求、正则提取JSON、Xpath解析HTML的核心技巧,能应对大多数网站的爬取需求,后续可结合实际场景,进一步优化爬虫的稳定性和效率。

如果本文对你有帮助,欢迎点赞、收藏、评论,关注我,持续分享Scrapy实战干货与爬取技巧!

相关推荐
周周记笔记5 小时前
初识HTML和CSS(一)
前端·css·html
aq55356005 小时前
网页开发四剑客:HTML/CSS/JS/PHP全解析
javascript·css·html
willhuo5 小时前
基于Playwright的抖音网页自动化浏览器项目使用指南
爬虫·c#·.netcore·webview
带刺的坐椅5 小时前
Snack JSONPath 项目架构分析
java·json·java8·jsonpath
试试勇气5 小时前
C++实现json-rpc框架
网络协议·rpc·json
小贾要学习6 小时前
【Linux】应用层自定义协议与序列化
linux·服务器·c++·json
haierccc6 小时前
Win7、2008R2、Win10、Win11使用FLASH的方法
前端·javascript·html
ZC跨境爬虫6 小时前
海南大学交友平台开发实战day7(实现核心匹配算法+解决JSON请求报错问题)
前端·python·算法·html·json
-To be number.wan8 小时前
Python爬取百度指数保姆级教程
爬虫·python