提示:此文章及爬虫代码仅供学习交流,请勿用于他处。
前言
在写爬虫的时候,我们用requests抓取页面,获得的HTML文本可能和浏览器上看到的不一样,原因是requests请求得到的是HTML源文档,而浏览器显示的内容可能是通过JavaScript渲染而成的,Ajax异步加载的数据也是通过JavaScript渲染后呈现在浏览器上。
本文以爬取百度小说为案例,分析如何爬取Ajax异步加载的内容。
爬虫实现过程
爬取的小说:《冰川天女传》
URL地址:http://dushu.baidu.com/pc/detail?gid=4345224252
我们直接从小说界面开始,从小说的目录下手,如下图所示:
用requests库请求这个网站
url = 'http://dushu.baidu.com/pc/detail?gid=4345224252'
res = requests.get(url)
print(res.text)
我们看一下返回的HTML网页中<body>标签里面的内容:
<body>
<div id="wrapper">
<div id="dushu"></div>
</div>
</body>
<body>标签里面什么也没有,说明我们想要的小说内容并不在HTML源文档里面,那么它可能就是JavaScript加载的。我们就通过浏览器开发者工具来分析一下吧。
按F12键进入调试界面,点击Network选项卡,再点击XHR。
如果是异步加载的内容都会出现在XHR中,现在还是空白的,我们要先刷新一下网页。
刷新之后出现了新的请求:
这里出现的两个请求,点击可以查看详细内容:
我们通常需要关注的就是Headers、Preview和Response。
- Headers 包括了请求和返回的头部信息
- Response 是服务端返回的内容
- Preview 其实就是对Response的内容进行预览,比如Response是图片信息的话,在Preview中就会把图片显示出来。
我们查看一下上面两个请求的的Response,里边的内容都是大括号 { } 括起来的内容,它是JSON格式的内容,点击Preview可以更为直观地展示里边的内容。(如果不知道JSON格式的话,可以先百度一下。)
仔细看一下JSON格式的内容,不难发现,真的可以找到了小说的目录,看图:
可以看到,这里显示了8个章节,回到网页上看看,刚好也是显示了8个章节。
现在我们已经知道,小说的内容包括在浏览器看到的这8个章节目录,都是通过Ajax异步加载的方式加载的,然后通过JavaScript显示出来。
现在点击页面上的 “查看全部” ,把全部章节显示出来。
这时,XHR又多了一个请求,可以断定,这个新的请求包括了所有章节目录的内容。
如下图所示:
而且点开每个章节之后,还能看到这个 cid,price_status,title这三个键值对:
cid 的值是一串纯数字构成的字符串,猜测以下,通过这个id也许可以找到这个章节的内容。
price_status 的值是 “0”,我们暂时还不知道是什么意思,不过通过英文单词我们可以猜测它是界定这个章节是免费的还是付费的。
title 肯定就是章节的标题了。
这时,我们点击一下Header看一下这个请求的头部信息,找到这个请求的 URL:
也就是说,我们通过请求这个URL,就可以访问我们刚刚看到的JSON格式的内容。
分析一下这个URL:%22是经过编码的内容,把他进行解码之后它是英文的双引号。
也就是说,整个URL其实长这样:
http://dushu.baidu.com/api/pc/getCatalog?data={“book_id”:“4345224252”}
有book_id字样,冒号后面还有一串数字。
在文章开始,我们说要爬取《冰川天女传》这部小说,这部小说的URL是http://dushu.baidu.com/pc/detail?gid=4345224252,现在我们知道了,这个 gid= 后面的数字就是这本小说的 book_id 值,通过这个 book_id 就可以找到这本小说了。
请看代码,根据原来的URL构造新的URL:
url = 'http://dushu.baidu.com/pc/detail?gid=4345224252'
index = url.find('gid=')
book_id = url[index+4:]
novel_url = 'http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"%s"}'%book_id
接下来请求novel_url 这个链接,就可以得到刚刚得到的JSON格式的数据了。
JSON格式的数据和访问python字典的方式是一样的,请看:
data = requests.get(novel_url).json()
for item in data['data']['novel']['items']:
cid = item['cid']
print(cid)
现在成功获取了每一个章节的 cid 值,看看能否根据cid值找到对应章节的文本内容。
可以猜测,章节的文本内容也是通过Ajax加载的,所以尝试用刚刚的方法看看能不能找到一些端倪。
果不其然,当我们在浏览器页面点开其中一个章节之后,Network中的XHR果然出现了小说的文本内容。
是吧,现在小说的内容也给我们找到了,来看看怎么构造每个章节的URL
看一下这个链接有什么特点:http://dushu.baidu.com/api/pc/getChapterContent?data={%22book_id%22:%224345224252%22,%22cid%22:%224345224252|1561658660%22,%22need_bookinfo%22:1}
解码之后比较好看:http://dushu.baidu.com/api/pc/getChapterContent?data={“book_id”:“4345224252”,“cid”:“4345224252|1561658660”,“need_bookinfo”:1}
通过这个URL我们也可以发现其中的规律,主要看data=后面的内容,通过 book_id 和 cid 就可以构建出来。
构造小说章节URL,访问这个URL,解析JSON文件得到小说文本内容啦
data = requests.get(novel_url).json()
for item in data['data']['novel']['items']:
cid = item['cid']
content_url = 'http://dushu.baidu.com/api/pc/getChapterContent?data={%22book_id%22:%22' + book_id \
+ '%22,%22cid%22:%224345224252|' + cid + '%22,%22need_bookinfo%22:0}'
content = requests.get(content_url).json()
print(content['data']['novel']['content'])
break
搞定!!!
不过要注意,百度小说有些是付费的,付费的内容可爬不下来哦,我们只能爬免费的内容啦。
总结
总结一下如何获取Ajax异步加载的内容。
既然是异步加载,说明它通常是跟人进行交互才会加载出来,那么我们就可以通过分析Network中的XHR里面的请求内容,看看浏览器请求了哪些内容,一步一步分析,通常在这些内容里面就可以找到我们想要爬取的内容了。
完整代码
import requests
import time
# url:小说界面的链接
url = 'http://dushu.baidu.com/pc/detail?gid=4345224252'
index = url.find('gid=')
book_id = url[index+4:]
novel_url = 'http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"%s"}'%book_id
data = requests.get(novel_url).json()
fp = open('novel.txt', 'w', encoding='utf-8')
for item in data['data']['novel']['items']:
if item.get('price_status') == '0':
n_title = item.get('title')
n_cid = item.get('cid')
last_url = 'http://dushu.baidu.com/api/pc/getChapterContent?data={%22book_id%22:%22' + book_id \
+ '%22,%22cid%22:%224345224252|' + n_cid + '%22,%22need_bookinfo%22:0}'
content = requests.get(last_url).json()['data']['novel']['content']
fp.write(n_title + '\n\n')
fp.write(content + '\n\n\n')
print('成功爬取:', n_title)
time.sleep(0.5)
elif item.get('price_status') == '1':
n_title = item['title']
print('付费章节,无法爬取:', n_title)
fp.close()
以下代码经过函数封装,输入小说名字就可以进行下载。
(写得比较急,代码不太优美,但功能已经实现)
import requests
import time
def novel_crawler(_novelName):
resultList = []
start_url = 'http://dushu.baidu.com/api/pc/getSearch?data={%22word%22:%22' + _novelName + '%22,%22pageNum%22:1}'
searchResult = requests.get(start_url).json()
totalNum = searchResult.get('data').get('total')
if totalNum == '0':
print('未找到该小说,请尝试修改搜索词')
book_id = book_name = None
return
elif totalNum == '1':
resultList += searchResult.get('data').get('list')
print('找到1本小说:\n{0}({1}著)'.format(resultList[0].get('book_name'),
resultList[0].get('author')))
input('回车之后即可下载')
book_name = resultList[0].get('book_name')
book_id = resultList[0].get('book_id')
elif eval(totalNum) > 9:
print('共搜索到%s本小说,如下:' % totalNum)
for page in range(1,int(eval(totalNum)/10)+2):
start_url = 'http://dushu.baidu.com/api/pc/getSearch?data={%22word%22:%22' + _novelName +\
'%22,%22pageNum%22:'+str(page)+'}'
searchResult = requests.get(start_url).json()
resultList += searchResult.get('data').get('list')
for li in range((page-1)*10,len(resultList)):
# liNum = (page-1)*10+li
liNum = li
print('{0}.{1}({2}著)'.format(liNum, resultList[liNum].get('book_name'),
resultList[liNum].get('author')))
if liNum%10 < 9:
listNo = input('已加载到最后下一页,请输入序号:')
while listNo == '':
listNo = input('已加载到最后下一页,请输入序号:')
else:
listNo = input('直接回车加载下一页或者输入序号:')
if listNo == '':
continue
else:
listNo = int(listNo)
while listNo >= len(resultList) or listNo < 0:
listNo = int(input('序号错误,请重新输入:'))
book_name = resultList[listNo].get('book_name')
book_id = resultList[listNo].get('book_id')
break
else:
print('共搜索到%s本小说,如下:' % totalNum)
resultList += searchResult.get('data').get('list')
for li in range(int(totalNum)):
print('{0}.{1}({2}著)'.format(li, resultList[li].get('book_name'),
resultList[li].get('author')))
listNo = int(input('请输入序号:'))
while listNo > int(totalNum) - 1 or listNo < 0:
listNo = int(input('序号错误,请重新输入:'))
book_name = resultList[listNo].get('book_name')
book_id = resultList[listNo].get('book_id')
print('正在进入下载程序,book_id=', book_id)
fp = open('{}.txt'.format(book_name), 'w', encoding='utf-8')
second_url = 'http://dushu.baidu.com/api/pc/getCatalog?data={%22book_id%22:%22' + str(book_id) + '%22}'
novel = requests.get(second_url).json()
novelItem = novel.get('data').get('novel').get('items')
for i in range(len(novelItem)):
if novelItem[i].get('price_status') == '0':
n_title = novelItem[i].get('title')
n_cid = novelItem[i].get('cid')
last_url = 'http://dushu.baidu.com/api/pc/getChapterContent?data={%22book_id%22:%22' + book_id \
+ '%22,%22cid%22:%224345224252|' + n_cid + '%22,%22need_bookinfo%22:0}'
content = requests.get(last_url).json().get('data').get('novel').get('content')
fp.write(n_title + '\n\n')
fp.write(content + '\n\n\n')
print('成功爬取:', n_title)
time.sleep(1)
elif novelItem[i].get('price_status') == '1':
print('付费章节,无法爬取')
break
fp.close()
if __name__ == '__main__':
# 输入要下载的小说书名
novel_crawler('冰川')