python爬取蚂蜂窝游记图片:从XHR入手爬取异步加载(动态加载)网页

前言

此前尝试爬取蚂蜂窝帖子的图片: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,也就是说可以掌握完整的页面源码!


据此,我们提出获取网页完整源码的思路:

  1. get网页url,获取第一批源码;
  2. 如果网页存在未加载的页面,则找到第一个Json数据的url;
  3. 从第一个Json数据的内容判断是否存在第二个Json数据,并获得其url。
  4. 不断获取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。观察发现,这个字符串藏在首先获得的网页第一批源码末尾:
在这里插入图片描述


至此,我们的思路就完全确定了:

  1. get网页url,获取第一批源码。
  2. 用上述方法判断页面是否有未加载页面;如存在,从第一批源码中找到第一个Json数据url的“加粗字符串”,从而获得其url。
  3. 从第一个Json数据的内容判断是否存在第二个Json数据,并获得其url。
  4. 不断获取Json数据的url,直至页面完全加载。
  5. 从这些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幅图。可以看到,文件夹中最后一幅图对应游记最后一幅图,表明我们成功爬取了所有的图片。
在这里插入图片描述

后记

本代码可用于爬取蚂蜂窝的游记照片。我尝试了另外三篇游记,均成功爬取全部照片:
在这里插入图片描述
对于其他动态加载的网页的爬取,本代码也可以提供爬取思路上的参考。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值