首页 > 其他分享 >记一次爬取组装音乐的过程(使用 requests, eyed3)

记一次爬取组装音乐的过程(使用 requests, eyed3)

时间:2023-05-09 14:55:41浏览次数:50  
标签:info album eyed3 url res 爬取 headers requests name

好久没写爬虫,试着将音乐通过直链爬下来,然后在本地组装成完整的音乐。

也可以作为主要用到的三个库 requestspandaseyed3 的一次实践。

本文的 headers 将会被省略

import time
import requests
import os
import eyed3

import pandas as pd

from collections import defaultdict
from bs4 import BeautifulSoup

def process_headers(raw: str) -> dict:
    """
    将从 F12 里复制的头信息转为字典形式
    """
    headers = raw.strip().split('\n')
    result = {}
    for row in headers:
        row = row.strip()
        if not row:
            continue
        for method in ['GET', 'POST']:
            if row.startswith(method):
                break
        else:
            sep = row.index(':')
            result[row[:sep]] = row[sep+2:]
    return result

爬取信息

import requests
import time
import pandas as pd
from collections import defaultdict

推荐使用这个直链搜索工具来减小工作量,音乐直链搜索|音乐在线试听 - by 刘志进实验室。直链下载下来只有 128kbps,不过已经够用了。

打开 F12,可以看到这个搜索是使用 POST 来传递的

  • 请求载荷为表单形式,参数分别为 input, filter, type, page
  • 响应为 JSON 格式,非常完好的返回了各种歌曲的相关参数,包括链接、标题、作者、歌词、图片。

如果没有这个工具,那么我们还需要先爬取歌曲详情,再在歌曲详情页爬取这些信息,在这里也感谢这个工具的制作人。

liuzhijin POST result

建议在网站里发送一遍请求后,直接打开 F12 复制表单,在之后的请求中只需要修改 page 来获得对应信息。

url = 'https://music.liuzhijin.cn/'
data = 'input=%E5%8F%91%E7%83%AD%E5%B7%AB%E5%A5%B3&' \
       'filter=name&'                                \
       'type=netease&'                               \
       'page='
headers = process_headers(
    '''
    Host: music.liuzhijin.cn
    ...
    '''
)

def fetch_info():
    result = []
    for i in range(1, 44 + 1):  # 提取查看一共需要多少页
        res = requests.post(url, data=data + str(i), headers=headers)
        if res.status_code == 200:
            info = json.loads(res.text)
            print(f'success {i}')
            result.append(info)
        else:
            print(f'fail {i}')
        time.sleep(3)  # 休息,减少服务器压力
    return result

然后将信息用 Pandas 的 DateFrame 表示,并做一些预处理和保存

  • 名字:要将标题和作者都显示在文件里面,删去 Windows 命名不能使用的符号;同时还要做防重名处理,将重名的文件名后面加上数字。
  • 封面:因为提取出的信息里,封面图片是缩小后的形式,我们将图片的 urlparam 参数去掉,来获取完整的图片(如果不需要完整封面的也可以不做);同时文件的 id 也要截取下来,为后续进一步爬取做铺垫。
info = fetch_info()
info = sum((info['data'] for info in result),
           start=[])
info = pd.DataFrame(info)
info = info.drop('type', axis=1)

# 命名处理
info['name'] = info.apply(
    lambda row: f'{row.title} - {row.author}',
    axis=1
)
repeat_counts = defaultdict(int)
for i, a in info.iterrows():
    name = a['name']
    # Windows 命名不能使用的符号
    for c in '\\/:*?"<>|':
        name = name.replace(c, ' ')
    # Windows 不区分大小写,用 lower() 来防止重名
    repeat_counts[name.lower()] += 1
    if (rc := repeat_counts[name.lower()]) != 1:
        info.loc[i, 'name'] = info.loc[i, 'name'] + f' - {rc}'

# 封面处理
info['fullpic'] = info['pic'].map(lambda p: p[:p.index('?')])
info['picid'] = info['fullpic'].map(lambda p: p[p.rindex('/')+1:])

# 保存 显示
info.to_csv('info.csv', index=False)
info.head()

info

进一步爬取

这里只是爬取的文件的基本信息,为了进一步完善音乐信息,现在我们需要爬取网易云音乐的专辑页面

进入专辑页,可以看到一个页面只能显示 12 个专辑,但点到第二页之后可以看到 url 出现了 limitoffset 两个参数,从名字上就可以判断这两个参数的作用。修改 limit 为 80 并删去 offset 使所有专辑都显示在一个页面上,F12 查看响应。

在响应的原始模式中搜索我们需要的封面信息、专辑名、专辑发布时间三个信息,可以看到它们在 li 表示的列表中,上一级为 <ul class="m-cvrlst m-cvrlst-alb4 f-cb" id="m-song-module" data-id="19783"> 表示的无序列表中,我们使用 BeatifulSoup 进行提取。

album info

from bs4 import BeautifulSoup
url = 'https://music.163.com/artist/album?id=19783&limit=80'
headers = process_headers(
    '''
	Host: music.163.com
    ...
    '''
)

def get_artist_albums():
    res = requests.get(url=url, headers=headers)
    if res.status_code == 200:
        print('OK')
    return res

def parse_html(html):
    def image_name(url):
        b, e = url.rindex('/'), url.index('?')
        return url[b+1:e]
    soup = BeautifulSoup(html, "lxml")
    album_infos = soup.find_all('ul', id='m-song-module')[0].find_all('li')
    img_album_date_dict = {
        image_name(a.find('img')['src']):
        (
            a.find('p', class_='dec dec-1 f-thide2 f-pre')['title'],
            a.find('span', class_='s-fc3').text
        )
        for a in album_infos
    }
    return img_album_date_dict

将爬取的信息添加到 info 中,这时候之前处理的 picid 就有用到了,它可以将图片和专辑、日期联系起来

html = get_artist_albums()
img_album_date_dict = parse_html(html.text)

album_date_df = pd.DataFrame(info['picid'].map(
    lambda p: img_album_date_dict.get(p, {'album': None, 'date': None})
).to_list())

info = pd.concat((info, album_date_df), axis=1, sort=False)
info.to_csv('info_plus.csv', index=False)
info.head()

info plus

爬取图片、音频

现在图片在 fullpic 中,音频在 url 中,首先用浏览器打开一遍,复制 headers,并且查看其他情况。

发现图片可以直接获取,而音频则会被转发到不同的 CDN 服务器进行分流。而下载下来的音频几乎没有额外信息,这也是为什么我们需要爬取这些信息并且组装。

audio redirects

最关键的是,分流前后使用的 headers 是不同的,因此不能让 requests.get 进行重定向,而是手动提取 302 响应头的 Location 修改请求头后进行重定向。

headers = process_headers(
    '''
    Host: p3.music.126.net
    ...
    '''
)

def fetch_images():
    if not os.path.isdir('image/'):
        os.mkdir('image')

    pic_set = {}
    for i, a in result.iterrows():
        pic_url = a['fullpic']
        picid = a['picid']
        name = a['name']

        if picid in pic_set:
            print(f'OK {i} {name}')
            continue

        res = requests.get(url=pic_url, headers=headers)
        if res.status_code == 200:
            print(f'OK {i} {name}')
            with open(os.path.join('image', picid), 'wb') as p:
                p.write(res.content)
        else:
            print(f'fail {i} {name}')
        time.sleep(1)


headers = process_headers(
    '''
    Host: music.163.com
    ...
    '''
)

next_headers_list = [
    process_headers(
        '''
        Host: m10.music.126.net
        ...
        '''),
    process_headers(
        '''
        Host: m701.music.126.net
        ...
        '''
    )
]

next_headers = next_headers_list[0]


def fetch_mp3s(indexes=None, interval=None):
    if not os.path.isdir('music/'):
        os.mkdir('music')

    # 爬取失败的索引
    failed_indexes = []

    for i, a in result.iterrows():
        if indexes is not None and i not in indexes:
            continue
        failed = False
        url = a['url']
        name = a['name']
        path = os.path.join('music', name) + '.mp3'

        # 禁止自动重定向
        res = requests.get(url=url, headers=headers, allow_redirects=False)
        if res.status_code == 200:
            print(f'OK {i} {name}')
            with open(path, 'wb') as p:
                p.write(res.content)
        elif res.status_code == 302:
            url = res.headers['Location']
            res = requests.get(url=url, headers=next_headers)
            if res.status_code == 200:
                print(f'OK {i} {name}')
                with open(path, 'wb') as p:
                    p.write(res.content)
            else:
                failed = True
        else:
            failed = True
        if failed:
            print(f'FAIL {i} {res.status_code}')
            failed_indexes.append(i)
        if interval:
            time.sleep(interval)
    return failed_indexes

failed_indexes = fetch_mp3s()

之所以准备多个请求头,是因为分流的 CDN 需要的请求头是不一样的,在这里只需要用到两个请求头就可以获取全部的音频了。然后将失败的 url 更换请求头再次请求。

next_headers = next_headers_list[1]

while failed_indexes:
    failed_indexes = fetch_mp3s(failed_indexes)
    print('-----------')
    print(failed_indexes)
    print('-----------')

至此,所有需要网络连接的工作已经完成,接下来是使用 eyeD3 库进行将音频、封面以及其他信息进行组装。

组装

info plus

万事俱备,我们遍历表格,通过 picid 寻找图片,通过 name 寻找音频,再组装其他信息。

import eyed3

def attach_tag(audio, artist, album, title, album_artist, date, lyrics, image):
    if not audio.tag:
        audio.initTag()
    audio.tag.artist = artist or ''
    audio.tag.album = album or ''
    audio.tag.title = title or ''
    audio.tag.album_artist = album_artist or ''
    if date and isinstance(date, str):
        audio.tag.release_date = eyed3.core.Date(*map(int, date.split('.')))
    if lyrics and isinstance(lyrics, str):
        audio.tag.lyrics.set(lyrics, 'lyrics', b'jp')
    if image:
        audio.tag.images.set(eyed3.id3.frames.ImageFrame.FRONT_COVER,
                             image, 'image/jpeg')
    # 编码
    audio.tag.save(encoding='utf-8')

info = pd.read_csv('info_plus.csv')

for i, a in info.iterrows():
    name = a['name']
    audio = eyed3.load(os.path.join('music', name) + '.mp3')
    with open(os.path.join('image', a['picid']), 'rb') as p:
        attach_tag(
            audio=audio,
            artist=a['author'],
            album=a['album'],
            title=a['title'],
            album_artist='発热巫女~ず',
            date=a['date'],
            lyrics=a['lrc'],
            image=p.read()
        )
    print(i, a['name'])

Tag 的每一项数据都是以 Frame 类表示的,一般关注它的几个属性 id, description, lang, text,再通过 FrameAccessor 接口类的方法 get, set 方法来修改这几个属性。

对于一般的文字性的 Tag,比如 artist, album, title,使用的是 str 直接存储,然后写入 TextFrame 中,这个 TextFrame 包含了大部分地文字性描述。调用 save() 的时候才会写入 TextFrame

对于日期 DateFrame,需要使用类 eyed3.core.Date,其初始化为 __init__(year=None, month=None, day=None, ...) 一直到秒,已经通过 @property 修饰过了,直接赋值其实就已经调用了 set 方法。

对于歌词 LyricsFrame,显式地通过 set 方法进行配置,set(text, description, lang) 对应歌词、描述、语言,虽然我选的歌不少是英文,不过还是使用了 b'jp' 作为语言,描述则可以随意配置。

对于图片 ImageFrame,显式地通过 set 方法进行配置,set(type, img_data, mime_type, description, img_url) 对应图片类型、图片数据、图片格式、描述、URL,后两项省略不填。类型指的是下列的一长串,因为是封面所以选 FRONT_COVER;格式就用 image/jpeg

image type

还需要注意参数的类型,通过 @requireBytes, @requireUnicode 修饰的方法对参数类型有要求,比如下面这个 __init__ 函数就需要 1,3 位置的参数 id, langBytes2,4 位置的参数 description, textUnicode,其实就是 str,所以有 Unicode 描述的参数还需要注意编码。

DescriptionLangTextFrame require

结果

result

property result

封面、歌词、专辑、作者、日期显示正常。

错误

eyeD3 编码错误

eyeD3 UnicodeEncodeError

在完善 Tag 的过程中,发生 latin-1 编码错误,这是因为有些歌曲的 Tag 已经默认使用了 latin-1 编码。查看源码可知 eyeD3 使用的编码,我们需要的是 UTF-8 编码。

encoding

解决方法有三种,eyeD3 使用不同的 Frame 表示标签 Tag 的组成,比如图片就是 ImageFrame、歌词就是 LyricsFrame,这里出现错误的是 TextFrame。不论方法是什么,都需要改变这个 Frame 的编码。按正常的途径 Frame 上面设有一个 FrameAccessor 接口类来控制 Frame 数据的修改,但是通过这个接口无法修改编码。所以有四种解决方法:

  • Python 没有严格的访问限制,可以直接修改对应 TextFrameencoding 属性,但是这个对应的 TextFrame 比较难找;

  • eyeD3 相比其他库,比较方便的地方就是它是由 Python 编写的,因此我们可以轻松的通过报错找到错误发生的位置,并且加入输出和修复。

    上面的错误已经提供了错误发生的位置 eyed3\id3\frames.py:314, in TextFrame.render(self),直接找到这个文件修改为

    def render(self):
        self._initEncoding()
        try:
            self.data = (self.encoding +
                         self.text.encode(id3EncodingToString(self.encoding)))
        except UnicodeEncodeError as e:
            print(e)
            print(self.text)
            self.data = (self.encoding +
                         self.text.encode(id3EncodingToString(UTF_8_ENCODING)))
        assert type(self.data) is bytes
        return super().render()
    
    
  • 第三个是最简便的方法

    错误发生的原因其实是音频文件已经有了自己的 TextFrame,而且编码已经设置好了。干脆直接把 Tag 清除重新写入就可以了,因为本来就没有多少信息,而且我们自己要写入的信息其实是很充足的。

    将写入 Tag 的代码前清除 Tag,改为

    def attach_tag(song, artist, album, title, date, lyrics, image):
        audio.initTag()
        song.tag.artist = artist or ''
        ...
        audio.tag.save()
    
  • 第四个也是最简便的方法,就是文章里面写的,保存的时候提供编码即可

    audio.tag.save(encoding='utf-8')
    

requests 重定向错误

这就是前面说的因为分流前后请求头不同而发生的,默认情况下 requests.get()allow_redirects=True,使用重定向之后请求头却没有修改,容易引起服务器拒绝请求,即 403 错误。

通关 Wireshark 抓包,也可以发现(11~22 为 requests.get81~3764 为浏览器,图中没有显示具体的请求头并且隐去了 Source 和 Destination),不同的请求方式都接受了 302 Found,但重定向后分别引起了 502 Bad Gateway 和 200 OK。

wireshark requests

标签:info,album,eyed3,url,res,爬取,headers,requests,name
From: https://www.cnblogs.com/violeshnv/p/17385022.html

相关文章

  • 小D-新版接口自动化教程- http 请求 Requests 实战
     #-*-coding:UTF-8-*-importrequestsresponse=requests.get("https://www.baidu.com")print(response.text)......
  • 爬取过程中出现stale element reference: element is not attached to the page docum
    Bug实际情况描述如下:当我想爬取信息类型中的6个元素时,使用这样的1smallClassList=seleniumService.waitFindElements(bidCrawler.getSmallClassTag());2for(WebElementsmallLi:smallClassList){3try{4Thr......
  • requests基本介绍
    1.requests的基本使用:  2.实例:请求丁香园疫情首页: ......
  • python requests模块
    requests是Python中用于发送HTTP请求的常用第三方库,可以方便地实现多种请求方式(如GET、POST、PUT、DELETE等),以及文件上传、Cookies管理等功能。以下是requests模块的使用方法和注意事项:安装requests:使用pip命令安装requests库,命令为"pipinstallrequests"。......
  • 成功绕过阿里无痕验证码,一键爬取飞瓜数据
    成功绕过阿里无痕验证码,一键爬取飞瓜数据飞瓜数据的登录接口,接入了阿里云的无痕验证码;通过接口方式模拟登录,难度比较高。所以,我们使用自动化的方式来实现模拟登录,并且获取到cookie数据。[阿里无痕验证码]https://help.aliyun.com/document_detail/122247.htm使用Playwrigh......
  • 轻松爬取灰豚数据的抖音商品数据
    轻松爬取灰豚数据的抖音商品数据调用两次登录接口实现模拟登录我们分析登录接口,发现调用了两次不同的接口;而且,需要先调用https://login.huitun.com/weChat/userLogin,然后再调用https://dyapi.huitun.com/userLogin接口。登录成功之后,SESSION在第二个接口的Cookie中返......
  • 轻松绕过 Graphql 接口爬取有米有数的商品数据
    轻松绕过Graphql接口爬取有米有数的商品数据有米有数数据的API接口,使用的是一种API查询语言graphql。所有的API只有一个入口,具体的操作隐藏在请求数据体里面传输。模拟登录,获取sessionId调用登录接口,进行模拟登录。cookies={}headers={}json_data={'......
  • 使用爬虫利器 Playwright,轻松爬取抖查查数据
    使用爬虫利器Playwright,轻松爬取抖查查数据我们先分析登录的接口,其中url有一些非业务参数:ts、he、sign、secret。然后根据这些参数作为关键词,定位到相关的js代码。最后,逐步进行代码的跟踪,发现大部分的代码被混淆加密了。花费了大半天,来还原这些混淆加密的代码,但是也没......
  • 解决报错requests.exceptions.ConnectionError: HTTPSConnectionPool(host=‘xxx’, p
    解决报错requests.exceptions.ConnectionError:HTTPSConnectionPool(host=‘xxx’,port=443):Maxretriesexceededwithurl使用requests时出错解决报错requests.exceptions.ConnectionError:HTTPSConnectionPool(host=‘xxx’,port=443):Maxretriesexceededwithurl......
  • Python很多时候要从键盘连续输入一个数组,并用空格隔开;Python爬取一些数据;python pip安
    Python要从键盘连续输入一个数组,并用空格隔开,Python中的实现方法如下:str=input(‘以空格为间隔连续输入一个数组:’)然后在键盘中输入,会·得到的str为一个字符串,要将其转为一个列表有两种方法方法一:a=[int(n)forninstr_in.split()]方法二:a=list(map(int,str.strip().sp......