python爬虫基础Ⅱ——Ajax数据爬取、带参请求:QQ音乐歌单、QQ音乐评论





基础爬虫部分Ⅱ

Ajax技术

全称为Asynchronous JavaScript and XML,即异步 JavaScript 和 XML。它不是一门编程语言,而是利用JS在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
对于传统的网页,如果想更新其内容,那么必须要刷新整个页面(就是第一部分爬取的那种网页,也叫静态网页吧),但有了Ajax,便可以在页面不被全部刷新的情况下更新内容。
有时候我们在用requests抓取页面的时候,在浏览器中可以看到正常显示页面数据,但爬取结果没有。这是因为requests获取的都是原始的HTML文档,而浏览器中的页面都是经过JS处理数据后生成的结果。
比如有些网页下拉的时候经常有“正在加载更多”的字样,但是网页并无刷新,其实就是Ajax加载的过程。
待会下边要提到的QQ音乐网站,也是利用了这种技术。遇到这样的页面,直接用requests等库来抓取原始页面,是无法获得到有效数据的。


json

有点不知从何说起,那我就乱来了鸭 XD

现在呢,我想获取QQ音乐里周杰伦的歌曲清单。按照前边说的,可能会写出这样的代码:

import requests
from bs4 import  BeautifulSoup

res_music = requests.get('https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=%E5%91%A8%E6%9D%B0%E4%BC%A6')
bs_music = BeautifulSoup(res_music.text,'html.parser')# 解析html
list_music = bs_music.find_all('a',class_='js_song')
# 查找class属性值为“js_song”的a标签,得到一个由标签组成的列表
for music in list_music:
    print(music['title'])
    # 打印出音乐名

但是程序运行的结果,是什么都找不到……为啥子呢,就要看下面说滴这个啦


1. Network

打开对应的网页:url(暂且叫这个网页为 url 好吧) = https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=周杰伦

然后F12,然后点击Network(在 Element 那一列,我懒得截图了 TAT)。
Network的功能是:记录在当前页面上发生的所有请求。现在看上去好像空空如也的样子,这是因为Network记录的是实时网络请求。现在网页都已经加载完成,所以不会有东西。

刷新一下网页,浏览器会重新访问网络,这样就会有记录。哗~密密麻麻地出来了许多。
在这里插入图片描述

这个,正是我们的浏览器每时每刻工作的真相:它总是在向服务器,发起各式各样的请求。当这些请求完成,它们会一起组成我们在Elements中看到的网页源代码。

而我们刚刚写的代码,只是模拟了这52个请求中的一个(准确来说,就是第0个请求),而这个请求里并不包含歌曲清单的。

当然也有一些网页,直接把所有的关键信息都放在第0个请求里,像前边爬取书本信息的网页就是这样的,我们要的信息就在url里。而这个歌曲清单是不在url里的。

那我们拿到一个要爬的url前,要怎么判断我们要的信息在不在里边呢?

那就点开第0个请求,再点Preview:
在这里插入图片描述

然后看看这里边的信息,确实是没有歌曲清单的呀,这也就说明歌曲清单不在url里。这也就是我前面说的,QQ音乐也是采用了Ajax技术的。 那到底在哪里呢?
其实,Ajax有其特殊的请求类型,它叫XHR(XHR and Fetch)。

先来粗略了解一哈:
在这里插入图片描述

对于我圈起来的,最常用的是:

ALL(查看全部)

XHR(仅查看XHR,会重点讲它)

Doc(Document,第0个请求一般在这里)

有时候也会看看:

Img(仅查看图片)

Media(仅查看媒体文件)

Other(其他)

最后,JS和CSS,则是前端代码,负责发起请求和页面实现;Font是文字的字体;而理解WS和Manifest,好像需要网络编程的知识,倘若不是专门做这个的不需要了解。(我菜死了)

那就去XHR里的请求找找喽~


2. XHR怎么请求?

看看XHR里请求有十几二十个左右吧,要从里面找出带有歌单的那一个,显然最有可能藏在client_search请求里(客户端搜索嘛)……而且它最大,有10.9KB,点击它!

点击Preview,你能在里面发现我们想要的信息:歌名就藏在里面!(只是有点难找,需要你一层一层展开:data-song-list-0-name,然后你就可以看到“晴天”。也显然list里有10个元素,其实就是当前页面的10首歌曲信息。

那如何把这些歌曲名拿到呢?这就需要我们去看看最左侧的Headers,点击它。如下所示,它被分为四个板块。
在这里插入图片描述

General里的Requests URL就是我们应该去访问的链接。如果在浏览器中打开这个链接,你会看到一个复杂的结构:最外层是一个字典,然后里面又是字典,往里面又有列表和字典……还是直接用Preview来看就好。列表和字典在此都会有非常清晰的结构,层层展开,按照这样的顺序data-song-list,又可以看到10个字典,歌曲名就在这10个字典里,而歌名是键为name对应的value

如何得到这一个复杂的结构,还能把它像正常的字典/列表一样,一层一层的取出来?重点来了。


3. 什么是json?

json是什么呢?粗暴地来解释,在Python语言当中,json是一种特殊的字符串,这种字符串特殊在它的写法——它是用列表/字典的语法写成的。

a = '1,2,3,4'
# 这是字符串
b = [1,2,3,4]
# 这是列表
c = '[1,2,3,4]'
# 这是字符串,但它是用 json格式 写的字符串

#引入json----------------------------------------
import json

a = [1,2,3,4]
# 创建一个列表a。
b = json.dumps(a)
# 使用dumps()函数,将列表a转换为json格式的字符串,赋值给b。
print(b)
# 打印b。
print(type(b))
# 打印b的数据类型。
c = json.loads(b)
# 使用loads()函数,将json格式的字符串b转为列表,赋值给c。
print(c)
# 打印c。
print(type(c))
# 打印c的数据类型.

这种特殊的写法决定了,json能够有组织地存储信息。

刚刚我们在XHR里查看到的列表/字典,严格来说其实它不是列表/字典,它是json


4. json数据如何解析?

方法很简单,请求到数据之后,使用json()方法即可成功读取。接下来的操作,就和列表/字典相一致。遇上聪明的你,相信我可以直接上代码了:

import requests

res_music = requests.get('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.song&searchid=60997426243444153&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=20&w=%E5%91%A8%E6%9D%B0%E4%BC%A6&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0') # XHR里的URL才是我们需要数据所在的URL

json_music = res_music.json() # 请求到数据后,使用json()方法,将response对象,转为列表/字典

list_music = json_music['data']['song']['list']# 一层一层地取字典,获取歌单列表

for music in list_music:
# list_music是一个列表,music是它里面的元素
    print(music['name'])
    # 以name为键,查找歌曲名

事实上,如果对这个程序稍加延展,它就能拿到:歌曲名、所属专辑、播放时长,以及播放链接。因为这些信息都在那个XHR里,聪明的你认真观察分析后,肯定很轻易就能做到。撒花撒花✿✿ヽ(°▽°)ノ✿


带参数请求

现在我还想爬取周杰伦的“晴天”这首歌前几页的评论。

1. 复习

打开有评论那个网页:https://y.qq.com/n/yqq/song/0039MnYb0qxYhV.html#comment_box

按照上一节说的方法,我们知道评论在这里,这一页有25个评论:
在这里插入图片描述

在0-24个字典里的 key 为rootcommentcontent的 value 就是评论。剩下的事情就简单了。我们去模拟这个请求,解析json,提取想要的内容就好。很容易写出以下的代码获取到本页评论:

import requests

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&notice=0&platform=yqq.json&needNewCode=0&cid=205360772&reqtype=2&biztype=1&topid=97773&cmd=8&needmusiccrit=0&pagenum=0&pagesize=25&lasthotcommentid=&domain=qq.com&ct=24&cv=10101010'
res = requests.get(url)
res_json = res.json() # 使用json()方法,将response对象,转为列表/字典
comments = res_json['comment']['commentlist']
for comment in comments: # comments是一个列表,comment是它里面的元素
    print(comment['rootcommentcontent'],'\n')

但是我想要的前几页的评论,怎么样更简单的获取呢?


2. params

有没有觉得上边代码的url太长了,不美观。

我们再去 Headers 里边看,看到Query String Parameters,其实这是一个字典。
在这里插入图片描述

我们能发现,

这个网址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&notice=0&platform=yqq.json&needNewCode=0&cid=205360772&reqtype=2&biztype=1&topid=97773&cmd=8&needmusiccrit=0&pagenum=0&pagesize=25&lasthotcommentid=&domain=qq.com&ct=24&cv=10101010

是由https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?

后边跟上Query String Parameters里的元素,以Key=Value,用&隔开连接的。


事实上,requests模块里的requests.get()提供了一个参数叫params,可以让我们用字典的形式,把参数传进去。

url = 'xxx'
params = {'xxx':'xxx','xxx':'xxx' .....} #把参数封装成字典
res = requests.get(url,params=params) #这样代码比较简洁明了

现在多翻几页评论列表,点击Headers,在General里看链接,在Query String Parametres里看参数,总结参数的规律。

经过比对,发现XHR有两个参数在不断变化:一个是pagenum,一个lasthotcommentid。其中pagenum好理解,就是页码,但是lasthotcommentid是什么?英文的意思是:上一条热评的评论id。

基于此,我们可以做一个猜想:每一页的请求,参数lasthotcommentid的值,是上一页的最后一条评论,所对应的id。

要验证这个猜想,需要确认:首先,每个评论都有commentid;其次,是验证这种对应关系。

比对之后,发现我们的猜想是正确的,每一页的最后一个评论的id是下一页 url 里参数lasthotcommentid的值。

且第一个的pagenum是0,第二页是1……,至此我们就找到每一页评论 url 的规律了,用一个循环就可以解决获取前几页的评论(循环最好不要超过5,不然会给对方服务器造成负荷):

import requests

url = 'https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg'
commentid = ''
# 设置一个初始commentid
for x in range(5):
    params = {
    'g_tk':'5381',
    'loginUin':'0',
    'hostUin':'0',
    'format':'json',
    'inCharset':'utf8',
    'outCharset':'GB2312',
    'notice':'0',
    'platform':'yqq.json',
    'needNewCode':'0',
    'cid':'205360772',
    'reqtype':'2',
    'biztype':'1',
    'topid':'102065756',
    'cmd':'8',
    'needcommentcrit':'0',
    'pagenum':str(x), #页码
    'pagesize':'25',
    'lasthotcommentid':commentid, #上一条评论的id 每页评论只有这俩参数在变
    'domain':'qq.com',
    'ct':'24',
    'cv':'101010  '
    } # 将参数封装为字典,其中pagenum和lastcommentid是特殊的变量
    res_comment = requests.get(url,params=params)
    json_comment = res_comment.json()
    list_comment = json_comment['comment']['commentlist']
    for comment in list_comment:
        print(comment['rootcommentcontent'])
   
	commentid = list_comment[24]['commentid']
    # 将最后一个评论的id赋值给comment,准备开始下一次循环

3. 添加Headers

除了带参数请求,我们最好还将自己的爬虫伪装成真实的浏览器——因为在这种情况下,服务器很可能拒绝爬虫访问。甚至有的网站,一开始就不允许爬虫访问。如,知乎、猫眼电影。

每一个请求,都会有一个Requests Headers,我们把它称作请求头。它里面会有一些关于该请求的基本信息,比如:这个请求是从什么设备什么浏览器上发出?这个请求是从哪个页面跳转而来?
在这里插入图片描述
最常需要的是user-agent,它会记录你电脑的信息和浏览器版本。如果我们想告知服务器,我们不是爬虫是一个正常的浏览器,就要去修改user-agent。倘若不修改,那么这里的默认值就会是Python,会被浏览器认出来:

url = 'xxx'
headers = {'user-agent':'xxx'}# 标记了请求从什么设备,什么浏览器上发出
params = {'xxx':'xxx'}
res_music = requests.get(url,headers=headers,params=params)

而对于爬取某些特定信息,也要求你注明请求的来源,即origin或referer的内容。


根据输入的歌手名获得相应歌单信息

观察相关URL的参数,有了前面的带参请求,我们就可以根据用户输入的歌手名,把它写入参数去请求,再爬取歌单(包括歌名+所属专辑+播放时长+播放链接)。先自己写写试试,需要注意的是参数中有指向歌手名和页码的,下面是我写的:

# -*- coding:utf-8 -*-
import requests
from bs4 import BeautifulSoup
singer = input('输入你喜欢的歌手名字,\n将在QQ音乐中搜索出他/她的歌单:')
url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp'
for i in range(5): #不要爬太多页哦
    parameters = {
    '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': singer, #歌手名
    'g_tk': '5381',
    'loginUin': '0',
    'hostUin': '0',
    'format': 'json',
    'inCharset': 'utf8',
    'outCharset': 'utf-8',
    'notice': '0',
    'platform': 'yqq.json',
    'needNewCode': '0'
    }
    res_music = requests.get(url, params=parameters) # 调用get方法,下载这个字典
    json_music = res_music.json() # 使用json()方法,将response对象,转为列表/字典
    #print(json_music['data']['song']['list'][0]['name']) 先用这个测试一下自己找对了没
    for music in json_music['data']['song']['list']:
        print ('歌名: ',music['name'])
        print('所属专辑:' + music['album']['name'])
        print('播放时长:' + str(music['interval'])+'秒')
        print('播放链接:https://y.qq.com/n/yqq/song/' + music['mid']+'.html\n\n')

又完了,✿✿ヽ(°▽°)ノ✿加油加油~!!




————————每个人都在抱怨生活不易,可是都在默默为生活打拼————————

发布了16 篇原创文章 · 获赞 113 · 访问量 4894

猜你喜欢

转载自blog.csdn.net/qq_43280818/article/details/96278101