前言
此前尝试爬取蚂蜂窝帖子的图片:https://blog.csdn.net/snsb_csdn/article/details/105048237
该代码存在一个硬伤:所获取的网页html是不完整的,因此我也只抓到了这篇游记的前24张图片,这是由于蚂蜂窝游记页面异步加载的原因导致的。经过研究,这个问题得到了解决,重新写一篇博客记录一下。
声明:游记为随机选择,爬取图片仅为个人练习,侵删。
1.页面异步加载机制分析
目标网页是http://www.mafengwo.cn/i/18845218.html。
用Firefox浏览器打开网页,并在任意处右键点击“查看元素>网络”,筛选“XHR”,观察网页刷新和内容加载过程。▲按F5刷新网页,看到XHR中get到一个html。这个html便是我们get网页url时可以获得的网页源码,上一篇博客的结果已经表明,这个html是不完整的,因为蚂蜂窝游记页面是异步加载:只有当滚动条快拉到底之后,新的网页内容才会加载出来。
那么怎么才能找到剩余的网页源码呢?
我们往下拉滚动条:
▲可以看到,滚动条快到底时,滚动块出现了一个闪现,说明页面加载了更多内容,滚动条变长。
此时XHR中get到了一个新的数据,这个数据是Json格式数据,格式与dict相同,它包含两个键:
第一个键为‘hasmore’,键值为‘True’表示下方还有更多未加载内容;
第二个键为‘html’,键值即为刚刚加载出来的页面源码,这正是我们所需要的。
这个数据的url为:
http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18845218&iid=971030304&seq=974972849&back=0
原来剩余的源码藏在Json数据中,访问这个url就可以获得新加载页面的源码。那么还有更多的Json数据吗?
我们继续往下拉,直至页面见底,期间观察到XHR中一共get到三个Json数据,他们的url分别是:
http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18845218&iid=971030304&seq=974972849&back=0
http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18845218&iid=971030304&seq=975157885&back=0
http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18845218&iid=971030304&seq=975545616&back=0
观察发现:
- 三个Json数据的url,除了加粗字符串不同,其余内容均相同(下文中出现的“加粗字符串”都是指此字符串)。
- 前两个Json数据的‘hasmore’键值为‘True’,表明还有未加载页面;最后一个数据的‘hasmore’键值为‘False’,表明页面已完全加载。
- 前两个Json数据的html末尾,均有字符串‘data-seq=“xxxx”’,恰好是下一个Json数据的url中的加粗字符串,因此可以指向下一个Json数据的url。
因此,我们只要获取到第一个Json数据的url,便可以掌握新加载的页面源码,以及下一个Json的url,也就是说可以掌握完整的页面源码!
据此,我们提出获取网页完整源码的思路:
- get网页url,获取第一批源码;
- 如果网页存在未加载的页面,则找到第一个Json数据的url;
- 从第一个Json数据的内容判断是否存在第二个Json数据,并获得其url。
- 不断获取Json数据的url,直至页面完全加载。
在这个思路中,第一个Json数据是最关键的。我们的爬虫程序如何判断是否有未加载的页面(是否存在第一个Json数据)?如果有,如何获得第一个Json数据的url?
判断第一个Json数据是否存在:
据我观察,当我们执行以下代码时,所返回的数据也是类似的Json数据,
request.get('http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18845218')
其‘hasmore’键值可以告诉我们是否存在尚未加载的页面,从这个结果看,是存在的。由此可以确定第一个Json数据存在。
获取第一个Json数据的url:
只要找到第一个Json数据url的“加粗字符串”,就可以得到其url。观察发现,这个字符串藏在首先获得的网页第一批源码末尾:
至此,我们的思路就完全确定了:
- get网页url,获取第一批源码。
- 用上述方法判断页面是否有未加载页面;如存在,从第一批源码中找到第一个Json数据url的“加粗字符串”,从而获得其url。
- 从第一个Json数据的内容判断是否存在第二个Json数据,并获得其url。
- 不断获取Json数据的url,直至页面完全加载。
- 从这些url中获取完整源码,并爬取图片。
2.完整代码
# -*- coding: utf-8 -*-
"""
Created on Tue Mar 24 15:36:27 2020
"""
import urllib.request
import requests
import json
import re
import os
def getID(url,cookie):
#用于根据网页url获取所需要的url_id和iid
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0','Cookie': cookie}
html = requests.get(url,headers=headers)
html.encoding = 'utf-8'#r.apparent_encoding
html = html.text
if re.match(r'{"data"',html):
html = json.loads(html)
html = html['data']['html']
html_splited = re.split(r'\s+',html)
for i in html_splited:
if re.match(r'.*iid',i):
result=i
break
iid = re.search(r'.*new_iid":"(.*)","show',result).group(1)
url_id=re.search(r'.*/(.*).html',url).group(1)
return url_id,iid
def hasmoreURL(url_id,url,cookie):
#用于判断Json数据是否存在
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0','Cookie': cookie}
url_togo = 'http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id='
if (url == ''): #url为空,则判断第一个Json数据是否存在
html = requests.get(url_togo+str(url_id),headers=headers)
html.raise_for_status()
html.encoding = 'utf-8'
html = json.loads(html.text)
print(html['data']['has_more'])
return html['data']['has_more']
else: #url不为空,为某Json数据的url,则判断下一个Json数据是否存在
html = requests.get(url,headers=headers)
html.raise_for_status()
html.encoding = 'utf-8'
html = json.loads(html.text)
print(html['data']['has_more'])
return html['data']['has_more']
def getDataSeq(url,cookie):
#寻找下一个Json数据的“加粗字符串”
DataSeq=[]
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0','Cookie': cookie}
html = requests.get(url,headers=headers) #get网页url或某Json数据的url,用于寻找下一个Json数据的“加粗字符串”
html.raise_for_status()
html.encoding = 'utf-8'
html =html.text
if re.match(r'{"data"',html): #根据格式判断是否为Json数据,若是,则需要执行额外提取html的操作
html = json.loads(html)
html = html['data']['html']
html = re.split(r'\s+',html) #将html数据按空格分割
for i in html:
if re.match(r'data-seq.*',i): #匹配“加粗字符串”关键词data-seq
DataSeq = re.search(r'data-seq="(.*)"',i).group(1) #每次匹配到关键词,就用加粗字符串更新DataSeq,直至获得最后的“data-seq”,即为下一个Json的“加粗字符串”
return DataSeq
def getURLlist(url,url_id,iid,cookie):
#将获取页面完整源码所需的所有url提取并以list返回
url_togo = 'http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id='
URLlist=[url] #首先将页面url存入list
hasmore = hasmoreURL(url_id,'',cookie) #判断第一个Json数据是否存在
if hasmore:
DataSeq = getDataSeq(url,cookie) #获取第一个Json数据的“加粗字符串”
URLlist.append(url_togo+str(url_id)+'&iid='+str(iid)+'&seq='+DataSeq+'&back=0') #将“加粗字符串”组装形成第一个Json数据的url,存入list中。
hasmore = hasmoreURL(url_id,URLlist[-1],cookie) #判断下一个Json数据是否存在
while (hasmore): #持续获取“加粗字符串”-组装并保存url,直至不再存在下一个Json数据
DataSeq = getDataSeq(URLlist[-1],cookie)
URLlist.append(url_togo+str(url_id)+'&iid='+str(iid)+'&seq='+DataSeq+'&back=0')
hasmore = hasmoreURL(url_id,URLlist[-1],cookie)
return URLlist
def getHTML(url,cookie):
#从url获取源码
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0','Cookie':cookie}
html = requests.get(url,headers=headers)
html.encoding = 'utf-8'
html = html.text
if re.match(r'{"data"',html): #如果是Json数据,则需要执行额外提取html的操作
html =json.loads(html)
html = html['data']['html']
return html
def getImageUrl(html):
#将html按空格分割并存入一个列表中,然后获取其中的图片url
html_splited = re.split(r'\s+',html) #对html文本按空格分割
targetURL = [] #用于保存目标url的list
for i in html_splited:
if (re.match(r'.*data-rt-src',i)): #筛选目标url的条件
if (re.match(r'.*?png',i)) or (re.match(r'.*?jpg',i)) or (re.match(r'.*jpeg',i)) or (re.match(r'.*?JPG',i)): #筛选目标url的条件
if(re.match(r'.*http.*',i)): #筛选目标url的条件
if (re.match(r'.*\?',i)):
url = re.search(r'.*src="(.*)\?',i).group(1) #获取准确图片url
else:
url = re.search(r'.*src="(.*)"',i).group(1)
#url = i
targetURL.append(url)
#print(url)
return targetURL
def crawlImage(urlList,num=1,savepath='folder'):
#根据图片的url,批量保存图片到本地
count = 1
for i in urlList:
img_webpage = urllib.request.urlopen(i)
img_data = img_webpage.read()
pathExist = os.path.exists(savepath)#检查保存路径(文件夹)是否存在,若否则创建文件夹
if (pathExist == False):
os.mkdir(savepath)
if re.match(r'.*jpg',i):#检查图片格式,未知图片格式则以png格式保存
imageType = '.jpg'
elif re.match(r'.*jpeg',i):
imageType = '.jpeg'
elif re.match(r'.*gif',i):
imageType = '.gif'
else:
imageType = '.png'
#open函数打开(创建)一个文件,其中模式'wb'表示以二进制格式打开一个文件只用于写入:
#如果该文件已存在则打开文件,并从开头开始编辑,原有内容会被删除;如果该文件不存在,则创建新文件。
save_image = open(savepath+'/'+str(num)+'-'+str(count)+imageType,'wb')
save_image.write(img_data)
save_image.close()
count += 1
if __name__ == '__main__':
#使用说明:
#爬某一蚂蜂窝游记时,更新以下内容:
#1.将网页url赋给“url”
#2.刷新网页,从开发者工具中获取最新cookie赋给“cookie”
#3.选好保存路径savepath
url = 'http://www.mafengwo.cn/i/18870889.html'
cookie='mfw_uuid=5e15a493-892d-30fd-8f7b-f9b427d723e0; _r=baidu; _rp=a%3A2%3A%7Bs%3A1%3A%22p%22%3Bs%3A18%3A%22www.baidu.com%2Flink%22%3Bs%3A1%3A%22t%22%3Bi%3A1578476691%3B%7D; __jsluid_h=59e2d5954da4576186658ea27c253918; __mfwa=1578476692581.63366.23.1585098433635.1585147899359; __mfwlv=1585147899; __mfwvn=15; __mfwlt=1585148907; uva=s%3A307%3A%22a%3A4%3A%7Bs%3A13%3A%22host_pre_time%22%3Bs%3A10%3A%222020-01-08%22%3Bs%3A2%3A%22lt%22%3Bi%3A1578476693%3Bs%3A10%3A%22last_refer%22%3Bs%3A180%3A%22https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DzIIeEAiySnkbqfJPHb9C5AVbpHakB4DYW5hweW9_1G6COmUNxEebXW-syv4VC0rFGyXqsd14AbrOhpkq0m-rhQ77RyXSl3CxJFSI0WQeew7%26wd%3D%26eqid%3D8aa9855e0014e809000000065e15a48a%22%3Bs%3A5%3A%22rhost%22%3Bs%3A13%3A%22www.baidu.com%22%3B%7D%22%3B; __mfwurd=a%3A3%3A%7Bs%3A6%3A%22f_time%22%3Bi%3A1578476693%3Bs%3A9%3A%22f_rdomain%22%3Bs%3A13%3A%22www.baidu.com%22%3Bs%3A6%3A%22f_host%22%3Bs%3A3%3A%22www%22%3B%7D; __mfwuuid=5e15a493-892d-30fd-8f7b-f9b427d723e0; Hm_lvt_8288b2ed37e5bc9b4c9f7008798d2de0=1584962529,1585017265,1585098434,1585147899; UM_distinctid=16f848aebd79cd-088c79933954f78-4c302a7b-1fa400-16f848aebd84a5; CNZZDATA30065558=cnzz_eid%3D1650508070-1578473002-null%26ntime%3D1585146029; oad_n=a%3A5%3A%7Bs%3A5%3A%22refer%22%3Bs%3A21%3A%22https%3A%2F%2Fwww.baidu.com%22%3Bs%3A2%3A%22hp%22%3Bs%3A13%3A%22www.baidu.com%22%3Bs%3A3%3A%22oid%22%3Bi%3A1026%3Bs%3A2%3A%22dm%22%3Bs%3A15%3A%22www.mafengwo.cn%22%3Bs%3A2%3A%22ft%22%3Bs%3A19%3A%222020-03-23+19%3A22%3A07%22%3B%7D; __mfwc=referrer%7Cwww.baidu.com; __mfwothchid=referrer%7Cwww.baidu.com; __omc_chl=; __omc_r=; bottom_ad_status=0; __jsl_clearance=1585147897.033|0|ThfBzJ7m5NSmW0Drzn7bb5B3Z2E%3D; PHPSESSID=j30psl4iu4mding0q2no5aga55; Hm_lpvt_8288b2ed37e5bc9b4c9f7008798d2de0=1585148907; __mfwb=f1437db48e2b.4.direct'
savepath= 'www.mafengwo.cn5'
url_id,iid=getID(url,cookie)
URLlist = getURLlist(url,url_id,iid,cookie) #获取页面所有url
num = 1
for i in URLlist: #对每一个url,依次提取源码、提取图片url、并保存到本地。
html = getHTML(i,cookie)
targetURL = getImageUrl(html)
crawlImage(targetURL,num=num,savepath=savepath)
num += 1
#http://www.mafengwo.cn/note/ajax/detail/getNoteDetailContentChunk?id=18897487&iid=405984955&seq=968679808&back=0
代码基本是按上述思路来写的。
关于爬取图片url和保存图片到本地的代码的考虑和解释,可以参看上一篇博客。
3.运行结果
运行上述代码后,我们获得了包含了完整源码的4个url,包括页面url和三个Json数据的url。
所爬取的图片保存在本地文件夹中,一共有160幅图。可以看到,文件夹中最后一幅图对应游记最后一幅图,表明我们成功爬取了所有的图片。
后记
本代码可用于爬取蚂蜂窝的游记照片。我尝试了另外三篇游记,均成功爬取全部照片:
对于其他动态加载的网页的爬取,本代码也可以提供爬取思路上的参考。