通过分析Ajax请求抓取【今日头条】“街拍”美图

20119.3.25更新:今日头条的“图集”模块已经改为“视频”了,可能是被人爬多了?
———————————分割线——————————
有一些网页直接请求得到的HTML代码并没有在网页中看到的内容,因为一些信息是通过Ajax加载,并通过js渲染生成的,这时就需要通过分析网页的请求来获取想要爬取的内容。本文通过抓取今日头条街拍美图讲解一下具体操作步骤。

网络库:Requests
解析库:BeautifulSoup+正则表达式
存储数据库:MongoDB
其他库:PyMongo
请确保以上库已经正确安装。

目标站点分析

在这里插入图片描述
打开今日头条的网页并搜索“街拍”。
在这里插入图片描述
我们想要抓取的是这些图集里面的内容。
在这里插入图片描述
右击空白处->审查->Network->勾选Preserve log->刷新网页
在这里插入图片描述
查看URL返回的都是一些js,并没有我们想要获取的内容。
在这里插入图片描述
点击XHR,再选中一个URL,查看请求的方法,发现是用get方法,所以使用requests库。
在这里插入图片描述
再看Preview,里面有许多data。我们展开来核对一下。
在这里插入图片描述
核对一下第一个data的title是不是和当前页面的第一个标题相同呢?如果是,那么说明当前网页的展示和对应的代码是没有问题的。并且URL的对应也没有问题。
现在下拉页面,可以看到左下方不断出现了新的URL请求:
在这里插入图片描述
重点在于都是通过offset这个参数的改变来实现的——变化范围为0,20,40···!那么我们通过循环就可以拿到这个“街拍”下组图形式的所有数据了!通过右边的数据可以看到这些都是一些json的数据,我们拿到后台数据后只需要调用json的包进行解析就可以了。
接下来就是分析查找图集详细页的代码,来找到图片的url,这个图片url隐藏的比较深,都在JS代码中:在这里插入图片描述
在这里插入图片描述

通过比对发现确实如此。
在这里插入图片描述
由于这个url是藏在gallery这个变量里的,然而这个变量并不是在html代码里的,所以不能使用BeautifulSoup和PyQuery来解析了,只能通过正则表达式来解析。

流程框架

1.获取索引页内容
利用requests请求目标站点,得到索引网页HTML代码,返回结果。
2.抓取详情页信息
解析返回结果,得到详情页的链接,并进一步抓取详情页的信息。
3.下载图片与保存数据库
将图片下载到本地,并把页面信息及图片URL保存至MongoDB。
4.开启循环及多线程
对多页内容遍历,开启多线程提高抓取速度。

爬虫实战

1.获取索引页

再看一下索引页的请求方式:
在这里插入图片描述
我们只需要按照这个格式构建一个Ajax请求。
注意!cur_tab为3时,搜索到的才是图集(一共有4个标签:综合、视频、图集、用户)。

from urllib.parse import urlencode
import requests
from requests.exceptions import RequestException
headers = {'User-Agent':'Mozilla/5.0(Macintosh;Intel Mac OS X 10_11_4)AppleWebKit/537.36(KHTML,like Gecko)Chrome/52.0.2743.116 Safari/537.36'}#这个信息是自定义的,根据自身需求来改变

#请求索引页(索引页中包含着许多图集的url)
def get_page_index():
    data = {#定义一个data字典,用于Ajax请求
        'offset': 0,
        'format': 'json',
        'keyword': '街拍',
        'autoload': 'true',
        'count': '20',
        'cur_tab': '3',
        'from': 'gallery'#这一行一定不能少
    }
    url = 'https://www.toutiao.com/search_content/?'+urlencode(data)
    try:
        response = requests.get(url,headers=headers)
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print("请求索引页错误")
        return None

def main():
    html=get_page_index()
    print(html)

if __name__ == '__main__':
    main()

注意!由于不同浏览器请求时的headers信息是不同的,所以在定义headers时可以到你的常用浏览器中去获取,随意打开一个网页右键审查,点击一个元素network然后查看Headers:
在这里插入图片描述
可以把下图红框中的信息复制到代码中作为headers(注意格式是个字典)。在这里插入图片描述
运行一下:
在这里插入图片描述
如上,成功获得了索引页的html,里面包含着许多图集的url。

2.解析索引页

由于上文返回的html是json格式字符串对象——我们调用type方法就可以看出来:

print(type(html))

在这里插入图片描述
因此我们需要调用json.loads()方法对字符串进行解析。
json.loads()用于将str类型的数据转成字典。
再仔细分析下图,这是索引页的html(json格式)。可以看到,data对应还有许多值。
在这里插入图片描述

将这些值(0,1,2…)展开,可以看到每一个值又是一个字典(abstract,article_url…),我们要提取的就是这些子层字典中“article_url”对应的值。
在这里插入图片描述

#注意引入相关的包
import json
from json.decoder import JSONDecodeError


#传入索引页的html,解析出每个图集的url
def parse_page_index(html):
    try:#加入异常处理
        data = json.loads(html)#对html进行解析,转换为字典。
        if data and 'data' in data.keys():
        #data.keys()返回的是这个字典中的所有的键名,并判断:'data'这个键名是否其中,若在的话执行下面的for循环
            for item in data.get('data'):#data这个键对应着许多值,遍历这些值,并依次赋值给item
                yield item.get('article_url')#构造一个生成器,取出每一个item中的article_url对应的url
    except JSONDecodeError:#如果出现了JSON解析异常,则跳过
        pass

然后在main函数中调用以上的函数,解析出图集的url,这些url就是每个图集的入口。

def main():
    html=get_page_index()
    for url in parse_page_index(html):#通过生成器提取所有的url
        print(url)

在这里插入图片描述
很好,当前索引页中,所有的图集url都被我们提取到了。

3.获取详情页

若尝试进入上面提取到的url,那么则会进入详情页(也就是进入了某个图集)。现在我们要获取详情页的代码(因为我们最终要抓取的图片就隐藏在这些代码之中)。这部分很好理解,和1.获取索引页的代码是相同的。

#请求每个图集的详情页
def get_page_detail(url):
    try:
        response = requests.get(url,headers=headers)
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print('请求详情页出错',url)
        return None

再次在main函数中改动一下,把上面获取到的详情页的text打印一下,以此来检查到此为止一切是否顺利:

def main():
    html=get_page_index()
    for url in parse_page_index(html):#通过生成器提取所有的url
        print(get_page_detail(url))#依次请求(上面提取到的url)并打印返回的text

在这里插入图片描述
好,成功请求了url,并且得到了它们的text。通过type()可以知道,这些text都是str类型的。

4.解析详情页数据

关键的一步到了。我们先研究一下详情页的源代码。
在这里插入图片描述
要在“Doc”中才能看到比较原始的代码。我们再找找图片的url隐藏在哪里。
在这里插入图片描述
发现图片的url都在gallery键的值中(并且值里面还有许多的“\”符号)
现在定义一个函数来解析详情页的数据,目标是把这个图集下的所有图片url提取出来。
首先,获取每个图片集的标题title,用BeautifulSoup下的select方法选择title标签下的文本。(因为文本直接能用,所以这时候就可以使用特别方便的BeautifulSoup了,而下面的图片url就不是这样,还掺杂着别的信息)
在这里插入图片描述

from bs4 import BeautifulSoup#记得导入


#解析详情页,获取图集中每张图片的url
def parse_page_detail(html):
    soup = BeautifulSoup(html,'lxml')#传入解析器:lxml和解析对象:html
    title = soup.select('title')[0].get_text()
    #因为select返回的结果是一个list,所以要用[0]来指定元素(也就是第一个元素),这个元素的类型是bs4.element.Tag
    #get_text()方法是定义在bs4.element.Tag这个类上面的,而不是list上
    #get_text()方法获取“title”对应的内容
    print(title)

在main函数中判断html是否正确,并打印结果:

main():
    html = get_page_index()
    for url in parse_page_index(html):
        html = get_page_detail(url)
        if html:
            parse_page_detail(html)

接下来获取每个图片集中的图片信息,所有图片信息都在gallery键的值中,通过re.comlile构建一个正则表达式pattern,再search得到结果,因为此时得到的结果中信息不正确,有很多多余的反斜杠’\’,于是利用replace去掉斜杠。
这一步的关键是正则表达式的写法。
注意,由于不同的浏览器返回的代码有可能不同,所以根据自己在浏览器(这个浏览器的headers应该与代码中的相对应,否则可能出错)中看到的代码来写正则表达式。
在这里插入图片描述
我们要匹配的是上图蓝色框中的内容(夹在括号内)。


import re
 
 #下面提取json串,串中包含了图片信息
    images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)#注意对括号进行转义
    result=re.search(images_pattern,html)
    if result:
        result = result.group(1).replace('\\', '')#替换反斜杠为空格

结果是json字符串的格式,需要用loads解析,提取其中的每张照片的url,最后返回的是图集的标题、链接和每张图片的url。
以上一步的思路进一步完善:

def parse_page_detail(html,url):#多传入一个当前详情页的url参数
    soup = BeautifulSoup(html,'lxml')#传入解析器:lxml和解析对象:html
    title = soup.select('title')[0].get_text()
    #get_text()方法获取“title”对应的内容
    print(title)
    # 下面提取json串,串中包含了图片信息
    images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)  # 注意对括号进行转义
    result = re.search(images_pattern, html)
    if result:
        result = result.group(1).replace('\\', '')
        data = json.loads(result)  # 转换成json对象
        if data and 'sub_images' in data.keys():
            sub_images = data.get('sub_images')
            # 每个sub_images都是一个字典,需要遍历它来提取url元素
            # 用一句话来构造一个list,把item赋值为sub_images的每一个子元素
            # 再取得sub_images的每一个item对象的url属性,完成列表的构建,这个列表名为images,里面是sub_images下所有的url
            images = [item.get('url') for item in sub_images]
            return {  # 以一个字典形式返回
                'title': title,
                'url': url,  # 这是当前详情页的url
                'images': images
            }

此时,所有的信息已经提取完毕,开始存储数据到MongoDB数据库。

存储到数据库

要把数据存储到mongodb数据库中,首先在同一目录下,建立配置文件config.py
在这里插入图片描述
这个配置文件需要写入以下内容:

MONGO_URL='localhost' #链接地址
MONGO_DB='toutiao'    #数据库
MONGO_TABLE='toutiao'    #数据集即“表”

通过from config import *调用该文件:

from config import *
import  pymongo
#声明mongodb数据库对象
client=pymongo.MongoClient(MONGO_URL)
db=client[MONGO_DB]

然后定义函数存储到数据库中,并判断如果存储成功输出相应信息

#把url存储到数据库
def save_to_mongo(result):
    if db[MONGO_TABLE].insert(result):
        print('存储到MongoDB成功',result)
        return  True
    return False

这个函数将在主函数中调用:

def main(offset):
    html=get_page_index(offset, KEYWORD)
    for url in parse_page_index(html):#获得每个图集的url
         html=get_page_detail(url)#用某个图集的url来请求详情页
         if html:
            result=parse_page_detail(html,url)#解析详情页的信息
            if result:save_to_mongo(result)#保存到数据库

6.下载图片

首先定义一个函数,利用pathlib库,根据传入的目录名创建一个文件目录,这是为了将图片分类:

from pathlib import Path
def create_dir(name):
    #根据传入的目录名创建一个目录,这里用到了 python3.4 引入的 pathlib 。
    directory = Path(name)
    if not directory.exists():
        directory.mkdir()
    return directory

然后定义下载图片函数,要求返回的是content,是二进制文件:

def download_image(save_dir,url):
    print("正在下载:",url)
    try:
        response = requests.get(url,headers=headers)#还是熟悉的请求方式
        if response.status_code == 200:
            #调用存储图片函数,返回二进制
            save_image(save_dir,response.content)#调用存储图片的函数
        return None
    except RequestException:
        print("请求图片出错",url)
        return None

定义存储图片函数

import os
from hashlib import md5

def save_image(save_dir,content):
    '''把文件保存到本地,文件有三部分内容(路径)/(文件名).(后缀)
    用format构造字符串(项目路径,文件名,格式),md5文件名可以避免重复'''
    #os.getcwd()程序同目录,但是现在我们要自定义目录
    #file_path='{0}/{1}.{2}'.format(os.getcwd(),md5(content).hexdigest(),'jpg')
    file_path = '{0}/{1}.{2}'.format(save_dir, md5(content).hexdigest(), 'jpg')
    #如果文件不存在,开始存入
    if not os.path.exists(file_path):
        with open(file_path,'wb') as f:
            f.write(content)
            f.close()


在parse_page_detail函数中,调用download_image:

root_dir=create_dir('E:\spider\\'+KEYWORD)  # 保存图片的根目录,这个是自定义的,E:\spider这个文件夹需要提前在本地建好。此后程序会根据KEYWORD建一个子文件夹。create_dir函数是上面我们定义过的。
download_dir = create_dir(root_dir / title)  # 根据每组图片的title标题名创建目录
for image in images:
    download_image(download_dir, image)    #下载所有的图片  

为了方便代码的复用,还可以把offset、搜索关键词等参数放到配置文件中:

MONGO_URL='localhost' #链接地址
MONGO_DB='toutiao'    #数据库
MONGO_TABLE='toutiao'    #数据集即表

GROUP_START = 1
GROUP_END = 20

KEYWORD = '街拍'#若想爬取其他内容,在此替换关键词即可

7.开启循环及多线程

开启多线程可以提高抓取效率:同时下载多个页面的图片
循环可以抓取更多页面的信息

from multiprocessing import Pool

if __name__ == '__main__':

    groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]
    #把offset做成一个列表20,40,60...
    #GROUP_START,GROUP_END用来限制起始和结束时的offset,也就是想要爬取的页面范围,这已在配置文件中定义过了
    pool=Pool()
    pool.map(main,groups)#将列表传入主函数,并且开启多线程


还需要修改:
将请求索引页时的offset和keyword改为由调用方(主函数)传入
在这里插入图片描述
修改主函数:
offset是由上面的groups列表传入, KEYWORD是在配置文件中定义的。

def main(offset):
    html=get_page_index(offset, KEYWORD)

完整代码:

import  requests
from urllib.parse import urlencode
from requests.exceptions import RequestException
import json
from bs4 import BeautifulSoup
import re
from config import *
import  pymongo
import os
from hashlib import md5
from multiprocessing import Pool
from json.decoder import JSONDecodeError
from pathlib import Path

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'}
#声明mongodb数据库对象
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]

#请求索引页(索引页中包含着许多图集的url)
def get_page_index(offset,keyword):
    data = {#定义一个data字典,用于Ajax请求
        'offset': offset,
        'format': 'json',
        'keyword': keyword,
        'autoload': 'true',
        'count': '20',
        'cur_tab': '3',
        'from': 'gallery'
    }
    url='http://www.toutiao.com/search_content/?'+urlencode(data)
    try:
        response = requests.get(url,headers=headers)
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print('请求索引页出错')
        return None


#传入索引页的html,解析出每个图集的url
def parse_page_index(html):
    try:#加入异常处理
        data = json.loads(html)#对html进行解析,转换为字典。
        if data and 'data' in data.keys():#data.keys()返回的是这个json的所有的键名,这里判断'data'在这些键名中
            for item in data.get('data'):#data对应还有许多值,遍历这些值
                yield item.get('article_url')#构造一个生成器,取出data中的每一个article_url对应的url
    except JSONDecodeError:
        pass

#请求每个图集的详情页
def get_page_detail(url):
    try:
        response = requests.get(url,headers=headers)
        if response.status_code == 200:
            return response.text
        return None
    except RequestException:
        print('请求详情页出错',url)
        return None

#解析详情页,获取图集中每张图片的url
def parse_page_detail(html,url):
    soup = BeautifulSoup(html, 'lxml')
    # 用BeautifulSoup来提取title信息
    title = soup.select('title')[0].get_text()
    print(title)
    #下面提取json串,串中包含了图片信息
    images_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)#注意对括号进行转义
    result=re.search(images_pattern,html)
    if result:
        result = result.group(1).replace('\\', '')
        data = json.loads(result)#转换成json对象
        if data and 'sub_images' in data.keys():
            sub_images = data.get('sub_images')
            #每个sub_images都是一个字典,需要遍历它来提取url元素
            # 用一句话来构造一个list,把item赋值为sub_images的每一个子元素
            # 再取得sub_images的每一个item对象的url属性,完成列表的构建,这个列表名为images,里面是sub_images下所有的url
            images = [item.get('url') for item in sub_images]
            root_dir=create_dir('E:\spider\jiepai')
            download_dir = create_dir(root_dir/title)
            for image in images: download_image(download_dir,image)#通过循环把图片下载下来
            return {#以一个字典形式返回
                'title':title,
                'url':url,#这是当前详情页的url
                'images':images
            }

#把url存储到数据库
def save_to_mongo(result):
    if db[MONGO_TABLE].insert(result):
        print('存储到MongoDB成功',result)
        return  True
    return False

#通过url来请求图片
def download_image(save_dir,url):
    print('正在下载',url)
    try:
        response = requests.get(url,headers=headers)
        if response.status_code == 200:
            save_image(save_dir,response.content)#content返回的是二进制内容,一般处理图片都用二进制流
            return response.text
        return None
    except RequestException:
        print('请求图片出错',url)
        return None

def create_dir(name):
    #根据传入的目录名创建一个目录,这里用到了 python3.4 引入的 pathlib 。
    directory = Path(name)
    if not directory.exists():
         directory.mkdir()
    return directory

def save_image(save_dir,content):
    file_path = '{0}/{1}.{2}'.format(save_dir,md5(content).hexdigest(),'jpg')
    if not os.path.exists(file_path):#如果文件不存在
        with open(file_path,'wb') as f :
            f.write(content)
            f.close()

def main(offset):
    html=get_page_index(offset, KEYWORD)
    for url in parse_page_index(html):#获得每个图集的url
         html=get_page_detail(url)#用某个图集的url来请求详情页
         if html:
            result=parse_page_detail(html,url)#解析详情页的信息
            if result:save_to_mongo(result)





if __name__ == '__main__':

    groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]#20,40,60...
    pool=Pool()
    pool.map(main,groups)

实战结果

最后结果了解一下:
在这里插入图片描述
上图是保存在本地目录下的按标题分类好的图片。
在这里插入图片描述
上图是使用Studio 3T所查看到的、保存在MongoDB数据库中的信息。

实战总结

这一次的实战需要掌握以下知识:

  • 有关网页的一些基础知识
  • Ajax格式的请求的分析与构造
  • requests、urllib、BeautifulSoup中一些常用方法的使用
  • 用json库来对json格式的字符串进行解析
  • 正则表达式来提取信息
  • 如何把数据存储到MongoDB
  • 保存图片到本地、动态创建目录的方法

总的来说,对一个网页的结构进行正确地分析,确定好提取信息的方案(例如由索引到详情页的请求方法、根据相应网页代码选择正确的库、正则表达式的写法等等),是成功完成类似抓取任务的关键。
ps:直接换个关键词,就可以抓取到别的图片啦!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值