为了账号安全,请及时绑定邮箱和手机立即绑定

[爬虫]使用多进程爬取并下载今日头条‘街角’图片并存入MongoDB

标签:
Python
  • 本文介绍的方法,用于爬取今日头条图片站‘街角’内容。


    webp

    在这里插入图片描述

1.2.使用工具 & 知识点

  • JSON;

  • urllib;

  • MD5;

  • bs4;

  • pymongo;

  • 正则表达式;

  • lxml;

  • AJAX;

  • 进程池;

2.编码过程

2.1. 获取索引页

首先,在搜索框输入搜索内容,查看返回的网页;

浏览过程中,发现返回的网页随着页面乡下滑动,不断加载新的内容,得知网页使用 AJAX 异步加载内容;

然后,在开发者模式network板块中,找到json加载的请求内容;

webp

在这里插入图片描述


获取到 JSON 内容之后,我们就可以编码构造请求了。

#-*- coding:utf-8 -*-from requests.exceptions import RequestException,Timeout,HTTPError,ConnectionError,TooManyRedirectsimport requestsimport jsonfrom urllib.parse import urlencodefrom hashlib import md5import osfrom bs4 import BeautifulSoup as bsimport timeimport pymongoimport refrom lxml import etree# 获取索引页的jsondef get_page_index(offset,keyword):
    
    # 创建json格式的data
    data = {        'offset': offset,        'format': 'json',        'keyword': keyword,        'autoload': 'true',        'count': 20,        'cur_tab': 3,        'from': 'gallery',
    }    
    # 构造请求URL
    url = 'https://www.toutiao.com/search_content/?' + urlencode(data) # 将data字典对象转换成请求参数

    # 获取URL的requests返回结果,设置超时时间为10s,并添加headers头
    response = requests.get(url,timeout=10,headers=headers)    # 如果返回的状态码为200,则返回页面text内容
    if response.status_code == 200:
        print('连接成功……')        return response.text    # 否则返回状态码   
    return response.status_codedef main(offset):

    # 获取索引页
    html = get_page_index(offset,KEYWORD)
    print(html)if __name__ == '__main__':

    GROUP_START = 1
    GROUP_END   = 1 

    KEYWORD = input('请输入要查找的关键字 >>> ')    # 设置headers
    headers = {            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0',           'Content-Type': 'application/x-www-form-urlencoded',            'Connection': 'Keep-Alive',            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'     
        }
    groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]
    main(groups)

运行,得到如下图片,代码OK。


webp

在这里插入图片描述

2.2.获取详情页url地址

为得到返回页中向企业的URL地址,我们需要先将返回的hmtl内容转化成json格式的键值对

from ... # ...内容同前文,此处不再重写def get_page_index(offset,keyword):
    ... # ...内容同前文,此处不再重写def parse_page_index(html):
    # 将html参数转化成json格式的键值对,并赋值与js
    js = json.loads(html)    # print(js.get('data'))  # 测试用,感兴趣的小伙伴可以打印试一下,看一下返回的内容
    # print(js.keys())  # 测试用,感兴趣的小伙伴可以打印试一下,看一下返回的内容
    # 由于js得到的是一个生成器对象,需要使用yield迭代获得详情页的URL
    for item in js.get('data'):        yield item.get('article_url')def main(offset):

    # 获取索引页
    html = get_page_index(offset,KEYWORD)    # print(html)
    # 得到索引页的URL
    for url in parse_page_index(html):
        print(url)if __name__ == '__main__':

    GROUP_START = 1
    GROUP_END   = 1 

    KEYWORD = input('请输入要查找的关键字 >>> ')    # 设置headers
    headers = {            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0',            'Content-Type': 'application/x-www-form-urlencoded',            'Connection': 'Keep-Alive',            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'     
        }
    groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]
    main(groups)

运行,得到下图内容,代码OK。


webp

在这里插入图片描述

2.3.获取详情页中图片URL

首先,打开图片索引页,查看图片。

发现图片并不是全部展示的,需要点击之后依次查看。

webp

图片并不是全部展示的,需要点击之后依次查看

这个时候我们会遇到一个问题,就是详情页中【每张图片各自的URL】从什么地方获取

打开网页源代码,在gallery中找到了一个URL列表,复制并在地址栏输入,得到了要找的的图片。
这个时候我们可以确认,这就是我们要找的照片URL。


webp

划红线的url即为图片url,其中的双反斜线 \ \ 需要去掉


现在,可以编写代码获取照片URL。

from ...  # ...内容同前文,此处不再重写def get_page_index(offset,keyword):
    ... # ...内容同前文,此处不再重写def parse_page_index(html):
    ...  # ...内容同前文,此处不再重写def get_page_detail(url):
    response = requests.get(url,timeout=10,headers=headers)    if response.status_code == 200:        # 因为要获取的时文本信息,所以返回的时text
        return response.text    else:        return response.status_code
        get_page_detail(url)    

def parse_page_detail(html,url):

    # 使用BeautifulSoup获取目标元素
    soup = bs(html,'lxml')    # 获取title
    # 注意:因为soup.select('tittle')返回的是list列表,需要使用 [] 取出之后,使用 get_text() 获取字符
    title = soup.select('title')[0].get_text()    # 注意:此处在使用正则表达式时,需要对正则表达式 [] 的使用有一定了解
    image_pattern = re.compile(r'gallery: JSON.parse[(]"(.*?)"[)],\n',re.S)
    result = re.search(image_pattern,html)    if result:        # 使用 replace 将URL中的 \\ 清除掉
        image_data = result.group(1).replace('\\','')        # return image_data  # 测试用,感兴趣可以打印出来看一下返回结果
        data = json.loads(image_data)        # return data.keys()  # 测试用,感兴趣可以打印出来看一下返回结果
        if data and 'sub_images' in data.keys():
            sub_images = data.get('sub_images')
            images_url = [item.get('url') for item in sub_images]            # print(images_url)  # 测试用,感兴趣可以打印出来看一下返回结果
            return {                'title':title,                'url':url,                'images':images_url
                }def main(offset):
    # 获取索引页
    html = get_page_index(offset,KEYWORD)    # 从索引页获取详情页
    for url in parse_page_index(html):        # print(url)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 打开索引页
        html = get_page_detail(url)        # print(html)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 解析索引页,获取详情页图片URL
        if html:
            result = parse_page_detail(html,url)
            print(result)if __name__ == '__main__':
    ...  # ...内容同前文,此处不再重写

运行,得到下图内容,代码OK。


webp

注意:url中的双反斜线 \ \ 已经被处理掉

2.4. 保存图片

我们计划将爬取的图片保存到本地文件夹,每一个索引链接得到的一组图片存到单独的文件夹中,同时将索引页URL链接保存到MongoDB数据库中。

我们在MongoDB中创建名为 toutiao 的DataBase,并创建名为toutiao1的collection,具体步骤自行参考网上教程,这里不在赘述。

from ...  # ...内容同前文,此处不再重写# MongoDB数据库连接MONGO_URL   = 'localhost'MONGO_DB    = 'toutiao'MONGO_TABLE = 'toutiao1'client      = pymongo.MongoClient(MONGO_URL)
db          = client[MONGO_DB]def get_page_index(offset,keyword):  
    ...  # ...内容同前文,此处不再重写def parse_page_index(html):  
    ...  # ...内容同前文,此处不再重写
    def get_page_detail(url):
    ...  # ...内容同前文,此处不再重写def parse_page_detail(html,url):

    # 使用BeautifulSoup获取目标元素
    soup = bs(html,'lxml')    # 获取title
    # 注意:因为soup.select('tittle')返回的是list列表,需要使用 [] 取出之后,使用 get_text() 获取字符
    title = soup.select('title')[0].get_text()    # 为图片设定保存的文件路径
    root_dir = os.getcwd()    # 其中title为每组图片的标题
    download_dir = '{0}/{1}'.format(root_dir,title)    # 每个详情页,创建一个文件夹
    os.makedirs(download_dir)    # 注意:此处在使用正则表达式时,需要对正则表达式 [] 的使用有一定了解
    image_pattern = re.compile(r'gallery: JSON.parse[(]"(.*?)"[)],\n',re.S)
    result = re.search(image_pattern,html)    if result:        # 使用 replace 将URL中的 \\ 清除掉
        image_data = result.group(1).replace('\\','')        # return image_data  # 测试用,感兴趣可以打印出来看一下返回结果
        data = json.loads(image_data)        # return data.keys()  # 测试用,感兴趣可以打印出来看一下返回结果
        if data and 'sub_images' in data.keys():
            sub_images = data.get('sub_images')
            images_url = [item.get('url') for item in sub_images]            # print(images_url)  # 测试用,感兴趣可以打印出来看一下返回结果
            # 下载图片
            for image in images_url:                # return images_url  # 测试用,感兴趣可以打印出来看一下返回结果
                download_image(download_dir,image)          
            return {                'title':title,                'url':url,                'images':images_url
                }# 定义下载程序def download_image(save_dir,url):
    print('正在下载:',url)
    response = requests.get(url,headers=headers)    if response.status_code == 200:        # 因为要下载的是图片,所以需要传入的内容是 response.content
        save_images(save_dir,response.content)    return Nonedef save_images(save_dir,content):
    # 使用 md5(content).hexdigest() 为图片创建名称
    file_path = '{0}/{1}.{2}'.format(save_dir,md5(content).hexdigest(),'jpg')    if not os.path.exists(file_path):
        print('file_path:',file_path)        with open(file_path,'wb') as f:
            f.write(content)
            print('下载完成!')
            f.close()# 保存数据至MongoDB模块def save_to_mongo(result):
    if db[MONGO_TABLE].insert_one(result):
        print('存储到MongoDB成功')def main(offset):
    # 获取索引页
    html = get_page_index(offset,KEYWORD)    # parse_page_index(html)
    # 从索引页获取URL的list
    for url in parse_page_index(html):        # print(url)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 获取详情页
        html = get_page_detail(url)        # print(html)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 解析索引页,获取详情页图片URL
        if html:            # parse_page_detail(html,url)
            result = parse_page_detail(html,url)            # print(result)
            if result:
                save_to_mongo(result)if __name__ == '__main__':
    ...  # ...内容同前文,此处不再重写

运行,下面是爬取的图片。


webp

在这里插入图片描述

2.5.爬虫结果

==============================================================
关键字           :街角
共获取图片   :20组    
共用时 :1分36秒
下载峰速      :5M/s
==============================================================

3.错误处理

在编码过程中,遇到了诸多问题,其中有两个挑出来说一下。

一个是加载超时的问题,如图。

webp

在这里,加载超时一般是网络问题

另一个是文件夹重名问题,如图。

这种情况在输入一些特殊的地名、物品名称时,遇到的比较多。

webp

文件夹重名问题可以通过加时间戳等方式解决,这里不赘述

注意:异常处理是个单独的课题,这里不多赘述,会在其他文章会另行介绍。

4.完整代码

#-*- coding:utf-8 -*-from multiprocessing import Poolfrom requests.exceptions import RequestException,Timeout,HTTPError,ConnectionError,TooManyRedirectsimport requestsimport jsonfrom urllib.parse import urlencodefrom hashlib import md5import osfrom bs4 import BeautifulSoup as bsimport timeimport pymongoimport datetimeimport refrom lxml import etree# MongoDB数据库连接MONGO_URL   = 'localhost'MONGO_DB    = 'toutiao'MONGO_TABLE = 'toutiao2'client      = pymongo.MongoClient(MONGO_URL)
db          = client[MONGO_DB]# 获取索引页的jsondef get_page_index(offset,keyword):
    
    try:        # 创建json格式的data
        data = {            'offset': offset,            'format': 'json',            'keyword': keyword,            'autoload': 'true',            'count': 20,            'cur_tab': 3,            'from': 'gallery',
        }        
        # 构造请求URL
        url = 'https://www.toutiao.com/search_content/?' + urlencode(data) # 将data字典对象转换成请求参数

        # 获取URL的requests返回结果,设置超时时间为10s,并添加headers头
        response = requests.get(url,timeout=10,headers=headers)        # 如果返回的状态码为200,则返回页面text内容
        if response.status_code == 200:
            print('连接成功……')            return response.text        # 否则返回状态码   
        return response.status_code    except Exception as e:
        print('获取索引页时,遇到妖孽 >>> ',e)        returndef parse_page_index(html):
    # 将html参数转化成json格式的键值对,并赋值与js
    js = json.loads(html)    # print(js.get('data'))  # 测试用,感兴趣的小伙伴可以打印试一下,看一下返回的内容
    # print(js.keys())  # 测试用,感兴趣的小伙伴可以打印试一下,看一下返回的内容
    # 由于js得到的是一个生成器对象,需要使用yield迭代获得详情页的URL
    for item in js.get('data'):        yield item.get('article_url')        # 设置 sleep 等待时间为1秒
        time.sleep(1)def get_page_detail(url):
    response = requests.get(url,timeout=10,headers=headers)    if response.status_code == 200:        return response.text        # return response.content
    else:        return response.status_code
        get_page_detail(url)    

def parse_page_detail(html,url):

    try:        # 使用BeautifulSoup获取目标元素
        soup = bs(html,'lxml')        # 获取title
        # 注意:因为soup.select('tittle')返回的是list列表,需要使用 [] 取出之后,使用 get_text() 获取字符
        title = soup.select('title')[0].get_text()        # 为图片设定保存的文件路径
        root_dir = os.getcwd()        # 其中title为每组图片的标题
        download_dir = '{0}/{1}'.format(root_dir,title)        # 每个详情页,创建一个文件夹
        os.makedirs(download_dir)        # 注意:此处在使用正则表达式时,需要对正则表达式 [] 的使用有一定了解
        image_pattern = re.compile(r'gallery: JSON.parse[(]"(.*?)"[)],\n',re.S)
        result = re.search(image_pattern,html)        if result:            # 使用 replace 将URL中的 \\ 清除掉
            image_data = result.group(1).replace('\\','')            # return image_data  # 测试用,感兴趣可以打印出来看一下返回结果
            data = json.loads(image_data)            # return data.keys()  # 测试用,感兴趣可以打印出来看一下返回结果
            if data and 'sub_images' in data.keys():
                sub_images = data.get('sub_images')
                images_url = [item.get('url') for item in sub_images]                # print(images_url)  # 测试用,感兴趣可以打印出来看一下返回结果
                # 下载图片
                for image in images_url:                    # return images_url  # 测试用,感兴趣可以打印出来看一下返回结果
                    download_image(download_dir,image)     
                    time.sleep(1)     
                return {                    'title':title,                    'url':url,                    'images':images_url
                    }    except Exception as e:
        print('解析详情页时,遇到妖孽 >>> ',e)# 定义下载程序def download_image(save_dir,url):
    print('正在下载:',url)
    response = requests.get(url,headers=headers)    if response.status_code == 200:        # 因为要下载的是图片,所以需要传入的内容是 response.content
        save_images(save_dir,response.content)    return Nonedef save_images(save_dir,content):
    # 使用 md5(content).hexdigest() 为图片创建名称
    file_path = '{0}/{1}.{2}'.format(save_dir,md5(content).hexdigest(),'jpg')    if not os.path.exists(file_path):
        print('file_path:',file_path)        with open(file_path,'wb') as f:
            f.write(content)
            print('下载完成!')
            f.close()# 保存数据至MongoDB模块def save_to_mongo(result):
    if db[MONGO_TABLE].insert(result):
        print('存储到MongoDB成功')def main(offset):
    # 获取索引页
    html = get_page_index(offset,KEYWORD)    # parse_page_index(html)
    # 从索引页获取URL的list
    for url in parse_page_index(html):        # print(url)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 获取详情页
        html = get_page_detail(url)        # print(html)  # 测试用,感兴趣可以打印出来看一下返回结果
        # 解析索引页,获取详情页图片URL
        if html:            # parse_page_detail(html,url)
            result = parse_page_detail(html,url)            # print(result)  # 测试用,感兴趣可以打印出来看一下返回结果
            if result:
                save_to_mongo(result)if __name__ == '__main__':    # 获取开始时间
    start_time = datetime.datetime.now()

    KEYWORD = input('请输入要查找的关键字 >>> ')    # 设置headers
    headers = {            'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3602.2 Mobile Safari/537.36',            'Content-Type': 'application/x-www-form-urlencoded',            'Connection': 'Keep-Alive',            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'     
        }        
    # 设定json格式的data中offset的开始和结束的值
    GROUP_START = 1
    GROUP_END   = 1 
    groups = [x*20 for x in range(GROUP_START,GROUP_END+1)]    
    # 运行主程序,并将offset值作为参数传入
    # 创建进程池,设置最大进程数为20个
    pool = Pool(20)    # 在进程池中运行程序
    pool.map(main,groups)    
    # main(groups)
    
    # 获取结束时间
    end_time = datetime.datetime.now()

    print('*'*100)
    print('开始时间:',start_time)
    print('结束时间:',end_time)
    print('共计用时:',end_time - start_time)    # 得到数据总数
    total_nums = db[MONGO_TABLE].count()
    print('共计获取数据:',total_nums,' 条')
    print('*'*100)



作者:猎户座alpha
链接:https://www.jianshu.com/p/268d04af20f0


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消