我们以歌曲《七里香》作为案例,去爬取它的歌曲评论。
爬取更多的评论数据的难点似乎在翻页和点击加载更多。
显然这种数据的加载模式还是我们上一关熟悉的“动态加载”,即点击一个按钮(加载更多或者第n页),服务器就会根据新的XHR更新页面信息。
当你在豆瓣搜索“海边的卡夫卡”,它的网址会是这样:
https://www.douban.com/search?q=%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1
现在,我要揭晓规律:在上面,我们能看到每个url都由两部分组成。前半部分大多形如:https://xx.xx.xxx/xxx/xxx
后半部分,多形如:xx=xx&xx=xxx&xxxxx=xx&……
两部分使用?来连接。举例刚刚的豆瓣网址,前半部分就是:
https://www.douban.com/search
后半部分则是:q=%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1
它们的中间使用了?来隔开。
这前半部分是我们所请求的地址,它告诉服务器,我想访问这里。而后半部分,就是我们的请求所附带的参数,它会告诉服务器,我们想要什么样的数据。
这参数的结构,会和字典很像,有键有值,键值用=连接;每组键值之间,使用&来连接。
就像豆瓣。我们请求的地址是https://www.douban.com/search
而我们的请求所附带的参数是“海边的卡夫卡”:q=%E6%B5%B7%E8%BE%B9%E7%9A%84%E5%8D%A1%E5%A4%AB%E5%8D%A1(那段你看不懂的代码,它是“海边的卡夫卡”使用utf-8编码的结果)。
技能点学满了吧?那现在,我们要以《七里香》为例,爬取用户的精彩评论。
首先,进入网址:
https://y.qq.com/n/yqq/song/004Z8Ihr0JIu5s.html
打开Network,选中All,点击刷新。
上一关我们说到,第0个请求一般都会是html。我们点开第0个请求来看看(看Preview或Response都可以),看里面有没有我们想要的评论信息。
显然是没有的。我们现在去看XHR。
这次的XHR还挺多,有四五十个。常规来说我们有两种方法来寻找XHR:阅读它们的name看看哪个可能是评论;或者是一个一个翻。
现在再给你介绍一个简单的小技巧:先把Network面板清空,再点击一下精彩评论的点击加载更多,看看有没有多出来的新XHR,多出来的那一个,就应该是和评论相关的啦。
我们点开这个请求的Preview,能够在['comment']['commentlist']
里找到评论列表。列表的每一个元素都是字典,字典里键rootcommentcontent对应的值,就是我们要找的评论。
好嘞,于是我们就在找到拥有评论数据的页面链接(请求的Headers栏:General中的Request URL):
https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=GB2312¬ice=0&platform=yqq.json&needNewCode=0&cid=205360772&reqtype=2&biztype=1&topid=102065756&cmd=6&needmusiccrit=0&pagenum=1&pagesize=15&lasthotcommentid=song_102065756_3202544866_44059185&domain=qq.com&ct=24&cv=10101010
显然,这样一个长链接,阅读体验非常之差。Network面板提供了一个更友好的查看方式,我来带你看看它。
回到上面我们找到XHR的地方,选中Headers,保持General打开,保持Response Headers和Request Headers关闭。我们点开Query String Parametres。
Query String Parametres,它的中文翻译是:查询字符串参数。
现在,我们来观察比较,依然在“七里香”的歌曲详情页,点击精彩评论的点击加载更多按钮,此时Network会多加载出更多的XHR,但的Name为fcg_global_comment_h5…才是我们关心的XHR。
事实上答案已经很明显了,只要我们多点耐心就会发现,链接的众多参数中,只有一个参数在变化。这个参数是pagenum,第一次点击加载更多的值为1,第二第三次点击它的值就变成了2和3。
当然,pagenum这个复合英文本身也说明了问题,指的可不就是页码嘛!也就是说,pagenum=1等于告诉服务器:我要歌曲信息列表第一页的数据,pagenum=2:我要歌曲信息列表第二页的数据。
这样一来,按照之前学的知识,你大约会想:我们写一个循环,每次循环都去更改pagenum的值,这样不就能实现爬取好多好多精彩评论了吗?
import requests
for i in range(5):
res=requests.get('https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?\
g_tk=1676601595&loginUin=835926124&hostUin=0&format=json&inCharset=utf8\
&outCharset=GB2312¬ice=0&platform=yqq.json&needNewCode=0&\
cid=205360772&reqtype=2&biztype=1&topid=102065756&cmd=6&\
needmusiccrit=0&pagenum={}&pagesize=15&lasthotcommentid=\
song_102065756_34536033_1471101184&domain=qq.com&ct=24&cv=10101010'.format(i))
html=res.json()
comments=html['comment']['commentlist']
for comment in comments:
print(comment['rootcommentcontent'])
print('---------------------------')
这样写代码,的确能够完成我们的目标。但是,这样写代码修改链接的参数太麻烦了,显然不够优雅,因为它是在太长了。
我们来让这个代码变好看些。事实上,requests模块里的requests.get()提供了一个参数叫params,可以让我们用字典的形式,把参数传进去。
所以,其实我们可以把Query String Parametres里的内容,直接复制下来,封装为一个字典,传递给params。只是有一点要特别注意:要给他们打引号,让它们变字符串。
import requests
url='https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg'
for i in range(5):
params={
'g_tk': '1676601595',
'loginUin': '835926124',
'hostUin': '0',
'format': 'json',
'inCharset': 'utf8',
'outCharset': 'GB2312',
'notice': '0',
'platform': 'yqq.json',
'needNewCode': '0',
'cid': '205360772',
'reqtype': '2',
'biztype': '1',
'topid': '102065756',
'cmd': '6',
'needmusiccrit': '0',
'pagenum': str(i),
'pagesize': '15',
'lasthotcommentid': 'song_102065756_34536033_1471101184',
'domain': 'qq.com',
'ct': '24',
'cv': '10101010'
}
res=requests.get(url,params=params)
html=res.json()
comments=html['comment']['commentlist']
for comment in comments:
print(comment['rootcommentcontent'])
print('---------------------------')
被隐藏的歌曲清单
好了,现在回到一开始遇到的难题:我想要爬取周杰伦更多的歌曲信息,但是qq音乐告诉我:想要查看更多内容,请下载一个客户端。
来看看我们搜索的首页:
https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周杰伦
不出所料,这个链接的前半部分是https://y.qq.com/portal/search.html,后半部分是page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周杰伦,然而,分隔这两部分的符号不是?,而是#。
其实在这里,#和?的功能是一样的,作用都是分隔,若把链接的#替换成?,访问的效果是一样的(注意:用?分隔的url不一定可以用#代替)。
既然如此,我们是不是可以跟前面一样,对参数下手了呢?
观察一下后半部分的参数page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周杰伦,page(中文:页面),searchid(中文:搜索id),remoteplace(中文:远程位置),后面的t和w这俩参数虽然不知道是什么,但根据他们的值(song和周杰伦)可窥得一斑,应该是指类型和关键字。
前面我们在爬取评论的时候知道,改变pagenum就可以加载更多的数据。举一反三,试想,如果改变搜索页面(https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周杰伦)的page这个参数我们是否可以访问到其他页面的数据呢?
为了验证猜想,老师将网页链接中的page=1改成了page=2,果然就访问到了下一的数据,嘿嘿嘿,来吧,可以造作起来了!
还记得更快查找XHR的骚操作吗?1️⃣先把Network面板清空,2️⃣再修改page值按回车键,3️⃣查看Network多出来的新XHR,也就是这个client_search_cp…。
剩下的事情就简单了,重复上面的步骤,找到client_search_cp…,点开Query String Parametres,观察参数的变化规律。
找到了吗?这次也只有一个参数在变化哦~
这个参数是p,第1页XHR的参数p值为1,第2、3页XHR的参数p值则为2和3,说明在这个client_search_cp…的请求中,代表页码的参数是p(page的缩写)。
依然是爬取歌曲的相关信息(歌曲名、所属专辑、播放时长、播放链接),只不过这一次,可以爬取的可不止是第一页的数据。
如此,代码应该如下(同上,不推荐循环超过5次):
import requests
url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'
for i in range(5):
params = {
'ct':'24',
'qqmusic_ver': '1298',
'new_json':'1',
'remoteplace':'sizer.yqq.song_next',
'searchid':'64405487069162918',
't':'0',
'aggr':'1',
'cr':'1',
'catZhida':'1',
'lossless':'0',
'flag_qc':'0',
'p':str(i+1),
'n':'20',
'w':'周杰伦',
'g_tk':'5381',
'loginUin':'0',
'hostUin':'0',
'format':'json',
'inCharset':'utf8',
'outCharset':'utf-8',
'notice':'0',
'platform':'yqq.json',
'needNewCode':'0'
}
res = requests.get(url,params=params)
html = res.json()
songs = html['data']['song']['list']
for song in songs:
print(song['name'])
print('所属专辑:'+song['album']['name'])
print('播放时长:'+str(song['interval'])+'秒')
print('播放链接:'+'https://y.qq.com/n/yqq/song/'+song['mid']+'.html\n\n')
'''首先,我们假设歌曲链接在XHR里,然后去验证假设。 查看一首歌的真实QQ音乐链接。
比如告白气球:https://y.qq.com/n/yqq/song/003OUlho2HcRHC.html
再查看XHR,会发现没有完整链接。但是它有:003OUlho2HcRHC。 而链接的其它部分,都是固定的。我们可以把XHR里的信息,和固定链接拼接起来。 此时你需要做的只是,查看003OUlho2HcRHC在XHR里对应的键是什么,提取即可。'''
悄悄地告诉你,如果你将这个代码里’w’关键字参数值换成另一个歌手/歌曲名,那么它也能爬到这个歌手/同名歌曲的信息。如果你愿意,可以在本关卡结束后,练习做这件事。
当然,qq音乐的产品经理肯定是不希望我们能访问到第2页的内容,他们更希望我们能下载客户端,从客户端访问数据。
为此,服务器就可能会对我们这些“投机取巧”的爬虫做限制处理。一来可以降低服务器的访问压力,毕竟成千上万次的访问对代码来说就是一个for循环的事儿;二来可以拦截那些想要通过爬虫窃取数据的竞争者。
那这就有一个问题,服务器怎么判断访问者是一个普通的用户(通过浏览器),还是一个爬虫者(通过代码)呢?
这需要我们回到浏览器中,重新认识一个新的信息栏:请求头Request Headers。
什么是Request Headers
每一个请求,都会有一个Requests Headers,我们把它称作请求头。它里面会有一些关于该请求的基本信息,比如:这个请求是从什么设备什么浏览器上发出?这个请求是从哪个页面跳转而来?
如上图,user-agent(中文:用户代理)会记录你电脑的信息和浏览器版本(如我的,就是windows10的64位操作系统,使用谷歌浏览器)。
origin(中文:源头)和referer(中文:引用来源)则记录了这个请求,最初的起源是来自哪个页面。它们的区别是referer会比origin携带的信息更多些。
如果我们想告知服务器,我们不是爬虫是一个正常的浏览器,就要去修改user-agent。倘若不修改,那么这里的默认值就会是Python,会被浏览器认出来。
有趣的是,像百度的爬虫,它的user-agent就会是Baiduspider,谷歌的也会是Googlebot……如是种种。
而对于爬取某些特定信息,也要求你注明请求的来源,即origin或referer的内容。比如我有试过,在爬取歌曲详情页里的歌词时,就需要注明这个信息,否则会拿不到歌词。你可以在写练习的时候进行尝试。
如何添加Requests Headers
Requests模块允许我们去修改Headers的值。点击它的官方文档,搜索“user-agent”,你会看到:
如上,只需要封装一个字典就好了。和写params非常相像。
而修改origin或referer也和此类似,一并作为字典写入headers就好。就像这样:
import requests
url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'
headers = {
'origin':'https://y.qq.com',
# 请求来源,本案例中其实是不需要加这个参数的,只是为了演示
'referer':'https://y.qq.com/n/yqq/song/004Z8Ihr0JIu5s.html',
# 请求来源,携带的信息比“origin”更丰富,本案例中其实是不需要加这个参数的,只是为了演示
'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
# 标记了请求从什么设备,什么浏览器上发出
}
# 伪装请求头
params = {
'ct':'24',
'qqmusic_ver': '1298',
'new_json':'1',
'remoteplace':'sizer.yqq.song_next',
'searchid':'64405487069162918',
't':'0',
'aggr':'1',
'cr':'1',
'catZhida':'1',
'lossless':'0',
'flag_qc':'0',
'p':1,
'n':'20',
'w':'周杰伦',
'g_tk':'5381',
'loginUin':'0',
'hostUin':'0',
'format':'json',
'inCharset':'utf8',
'outCharset':'utf-8',
'notice':'0',
'platform':'yqq.json',
'needNewCode':'0'
}
# 将参数封装为字典
res = requests.get(url,headers=headers,params=params)
# 发起请求,填入请求头和参数