本文是对《Python网络爬虫实战与机器学习应用》中百度音乐歌单爬虫与数据分析及可视化实战这个例子的学习总结。

需要的工具:

  • Python3
  • requests
  • BeautifulSoup
  • MySQL
  • pymysql
  • json

完整代码地址: https://github.com/huanyouchen/python-spider

爬取目标

爬取百度歌单页面:http://music.baidu.com/songlist/tag/%E5%85%A8%E9%83%A8?orderType=1&offset=240&third_type=

打开歌单页面,以及歌单对应的详情页面如下:

百度歌单页面以及歌单对应详细页面

我们需要爬的信息包括:歌单名字,链接,创建者,标签,歌单收藏量,评论量,播放量,以及该歌单下的每一首歌曲和歌曲演唱者。

将爬取的信息存储到MySQL数据库中.

数据库表的设计

首先需要在MySQL中创建一个baidu_songlist的数据库中,然后创建songlist_detail的表,字段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE DATABASE baidu_songlist;
USE baidu_songlist;
CREATE TABLE IF NOT EXISTS `songlist_detail` (
songlist_name VARCHAR(40) NOT NULL,
songlist_creater VARCHAR(30) NOT NULL,
songlist_tags VARCHAR(20),
songlist_collect_num INT,
songlist_comment_num INT,
songlist_share_num INT,
songlist_play_num INT,
songlist_musicname VARCHAR(50),
songlist_musicsinger VARCHAR(30),
songlist_url VARCHAR(70)
)ENGINE=INNODB CHARSET=UTF8;

使用baidu_songlist这个数据库来存储我们爬取到的信息。

然后使用pymysql来链接数据库:

1
2
3
4
5
6
7
8
9
10
# 链接数据库
conn = pymysql.connect(
host='localhost',
port=3306,
user='root',
password='password',
db='baidu_songlist',
charset='utf8'
)
cursor = conn.cursor()

爬取过程分析

获取所有歌单数量

打开百度歌单页面,每页都是5行4列总共20个歌单,下面的1….533表示这样的歌单页面总共有533个,也就是说总的歌单量大概是20*533左右。

第一页歌单的URL为:http://music.baidu.com/songlist/tag/%E5%85%A8%E9%83%A8?orderType=1&offset=0&third_type=

第二页歌单的URL为:http://music.baidu.com/songlist/tag/%E5%85%A8%E9%83%A8?orderType=1&offset=20&third_type=

往后翻几页就会发现每页的URL变化规律在offset=这个参数中,变化规律为 (n-1) * 20, n表示第n个页面。

先获取这533个页码,用Chrome打开开发者工具分析页面的部分,发现533在类名为page-inner的div内的最后一个a标签,实现代码:

1
2
3
4
5
6
def get_page_num():
url = "http://music.baidu.com/songlist/tag/%E5%85%A8%E9%83%A8?orderType=1&offset=0&third_type="
wbdata = requests.get(url).content
soup = BeautifulSoup(wbdata, 'lxml')
page_num = soup.select("div.page-inner > a.page-navigator-number")[-1].get_text()
return page_num

得到总的页码数量,总的页面数量*20得到总的歌单数量。

获取第一个页面所有的歌单名称和对应的歌单详情链接

用Chrome开发者工具分析第一个页面歌单部分,所有的歌单都在<div class="songlist-list">这个div里面的ul列表下:

1
2
3
4
5
6
<div class="songlist-list">
<ul>
<li>
<p class="text-title"><a href="/songlist/516287536" title="B面唱片,聆听大牌非主打">B面唱片,聆听大牌非主打</a></p>
<p class="text-user">by<a target="_blank" href="/user?nickname=%E6%AD%8C%E5%8D%95%E5%AE%9E%E9%AA%8C%E5%AE%A4" title="歌单实验室">歌单实验室</a></p>
</li>

获取歌单名称和对应的歌单详情链接代码:

1
2
3
4
5
6
7
8
9
10
def get_song_list(page):
songListUrl = \
"http://music.baidu.com/songlist/tag/%E5%85%A8%E9%83%A8?orderType=1&offset={0}&third_type=".format(page)
print("正在爬取链接: " + songListUrl)
wbdata = requests.get(songListUrl).content
soup = BeautifulSoup(wbdata, 'lxml')
songListLinks = soup.select("p.text-title > a")
for songListLink in songListLinks:
title = songListLink.get('title')
link = "http://music.baidu.com" + songListLink.get('href')

获取所有页面所有的歌单名称和对应的歌单详情链接

知道了第一个页面的歌单名称和详情链接,和所有的页码后,便可以利用循环把所有的页面下的歌单都获取到:

1
2
3
page_num = int(get_page_num())
for page in range(page_num):
get_song_list(page_num*20)

获取该歌单的详细信息存储到MySQL中。

有了歌单详情链接后,利用request来获取页面,然后用开发者工具分析需要信息对应的相关页面代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def get_songlist_info(title, link):
try:
print("正在获取歌单:" + title + " 的详细信息...")
songlistdata = requests.get(link, headers=headers).content
soup = BeautifulSoup(songlistdata, 'lxml')
songlist_name = soup.h1.get_text() # 歌单名字
songlist_url = link # 歌单链接
songlist_creater = soup.find(name="a", class_="songlist-info-username").get_text() # 歌单创建者
songlist_info_tags = soup.select("div.songlist-info-tag > a")
songlist_tags = ""
# 歌单标签
for tag in songlist_info_tags:
songlist_tags = songlist_tags + tag.get_text() + " "
songlist_play_num = int(soup.find(name="span", class_="songlist-listen").get_text()[:-3]) # 播放次数
songlist_share_num_str = soup.find(name="i", class_="share-num-text").get_text() # 分享次数
# 如果分享次数为0, 将"分享"换为0
if songlist_share_num_str == "分享":
songlist_share_num_str = 0
songlist_share_num = int(songlist_share_num_str)
songlist_collect_num = int(soup.find(name="em", class_="collectNum").get_text()) # 收藏次数
songlist_comment_num = int(soup.find(name="em", class_="discuss-num").get_text()) # 评论数目
songlist_song_num = int(soup.find(name="span", class_="songlist-num").get_text()[:-1]) # 歌曲数目
# 如果该歌单中的歌曲数量超过20个,需要往后翻页,每页歌曲数量20个。
for song_num in range(0, songlist_song_num, 20):
songlist_url = link + "/offset/{0}?third_type=".format(song_num)
songlistdata = requests.get(songlist_url, headers=headers).content
songlistsoup = BeautifulSoup(songlistdata, 'lxml')
songlist_musics = songlistsoup.select("div.normal-song-list > ul > li")
for music_info in songlist_musics:
music_data = json.loads(music_info["data-songitem"])
songlist_musicname = music_data["songItem"]["sname"]
songlist_musicsinger = music_data["songItem"]["author"]
print(songlist_play_num, songlist_comment_num, songlist_share_num, \
songlist_musicname, songlist_musicsinger)
try:
# 将信息存储到数据库中
cursor.execute(
"insert into songlist_detail("
"songlist_name, songlist_url, songlist_creater, songlist_tags, songlist_collect_num, \
songlist_comment_num, songlist_share_num, songlist_play_num, songlist_musicname, \
songlist_musicsinger) \
values ('{songlist_name}', '{songlist_url}', '{songlist_creater}', \
'{songlist_tags}', '{songlist_collect_num}', '{songlist_comment_num}', \
'{songlist_share_num}', '{songlist_play_num}', '{songlist_musicname}', \
'{songlist_musicsinger}');".format(songlist_name=songlist_name, songlist_url=songlist_url, \
songlist_creater=songlist_creater, songlist_tags=songlist_tags,\
songlist_collect_num=songlist_collect_num, songlist_comment_num=songlist_comment_num, \
songlist_share_num=songlist_share_num, songlist_play_num=songlist_play_num,
songlist_musicname=songlist_musicname, songlist_musicsinger=songlist_musicsinger))
conn.commit()
except Exception as e:
print(e)
print("获取歌单:" + title + " 数据完毕...")
except BaseException as e:
print(e)

爬取中需要注意的几个地方:

  • 歌单的标签可能不止一个,需要遍历一下各个标签把名字拼在一起
  • 歌单分享部分,如果这个歌单已经有分享过,会显示分享数量;如果这个歌单还没有被分享过,会显示”分享”两个字,我们需要的是数字,因此需要判断一下,如果是”分享”两个字,需要把歌单分享数量设为0
  • 在歌单详情页面下的歌曲列表数量<=20时候,不会出现分页,如果该歌单歌曲数量超过20,就会按每页20首歌曲出现1,2,3..这样的分页码,需要循环把每个页面下的歌曲信息都获取到。
  • 对于每首歌需要保存歌曲名字和演唱者,信息保存在这个地方:
    1
    <li data-songitem="{&quot;yyr_song_id&quot;:null,&quot;songItem&quot;:{&quot;sid&quot;:&quot;288075&quot;,&quot;author&quot;:&quot;\u4efb\u8d24\u9f50&quot;,&quot;sname&quot;:&quot;\u8001\u5f20\u7684\u6b4c&quot;,&quot;pay_type&quot;:&quot;0&quot;}}" class=" bb-dotimg clearfix song-item-hook csong-item-hook ">

使用json格式的

1
2
3
4
5
6
data-songitem{
songItem{
author:\u4efb\u8d24\u9f50,
sname:\u8001\u5f20\u7684\u6b4c;,
}
}

需要用json.loads把Unicode编码转换成中文

在存储到MySQL中遇到的问题

mysql存储的时候报错:pymysql.err.DataError: (1406, "Data too long for column 'songlist_url' at row 1")

查看网上资料有说因为数据库中设置的字符长度不够,我加长了以后依然会报错。另一种说法是由于输入了中文,编码出现了问题,我一直以为是json转换歌曲名字和演唱者那里格式转换错了,,尝试了好久,还是报错。

找到的解决方法:
http://huanyouchen.github.io/2018/05/22/mysql-error-1406-Data-too-long-for-column/

爬取结果

因为运行时间关系,只爬到第14页就停止了,总过获取了14页,共有4496首歌。如果要全部歌曲,需要等待时间较长,这也是不足点,暂时没有使用多线程。

将信息存储到数据库后,信息如下:

百度歌单部分歌曲信息