多线程or多进程爬虫案例

前置说明

关于python多线程和多进程的说明,请参考如下:

https://zhuanlan.zhihu.com/p/46368084 (一位知乎用户)

https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064 (廖雪峰)

这是我找到的两篇很棒的文章,里面详细说明的python多进程、多线程的原理以及用法,大家可以仔细看看

多进程爬虫例子

用一个实例说明下如何使用多进程进行爬虫

目标网站:https://imgbin.com/,本次爬取的也是一个图片网站,里面是一些透明背景图

 1.首先看一下不添加多进程/多线程时的爬取速度,代码如下

# -*- coding:utf-8 -*-
import requests
from requests.exceptions import RequestException
import os, time
from lxml import etree


def get_html(url):
    """获取页面内容"""
    response = requests.get(url, timeout=15)
    # print(response.status_code)
    try:
        if response.status_code == 200:

            # print(response.text)
            return response.text
        else:
             return None
    except RequestException:
        print("请求失败")
        # return None

def parse_html(html_text):
    """解析页面内容,提取图片url"""
    html = etree.HTML(html_text)

    if len(html) > 0:
        img_src = html.xpath("//img[@class='photothumb lazy']/@data-original")  # 元素提取方法
        # print(img_src)
        return img_src

    else:
        print("解析页面元素失败")

def get_image_content(url):
    """请求图片url,返回二进制内容"""
    try:
        r = requests.get(url, timeout=15)
        if r.status_code == 200:
            return r.content
        return None
    except RequestException:
        return None


def main(depth=None):
    """
    主函数,下载图片
    :param depth: 爬取页码
    :return:
    """
    j = 1
    base_url = 'https://imgbin.com/free-png/naruto/'  # 定义初始url

    for i in range(1, depth):
        url = base_url + str(i)  # 根据页码遍历请求url
        html = get_html(url)  # 解析每个页面的内容
        # print(html)
        if html:
            list_data = parse_html(html)  # 提取页面中的图片url
            root_dir = os.path.dirname(os.path.abspath('.'))
            save_path = root_dir + '/pics/'   # 定义保存路径
            for t in list_data:  # 遍历每个图片url
                try:
                    file_path = '{0}{1}.{2}'.format(save_path, str(j), 'jpg')
                    if not os.path.exists(file_path):  # 判断是否存在文件,不存在则爬取
                        with open(file_path, 'wb') as f:
                            f.write(get_image_content(t))
                            f.close()
                            print('第{}个文件保存成功'.format(j))
                    else:
                        print("第{}个文件已存在".format(j))
                    j = j + 1
                except FileNotFoundError as  e:
                    print("遇到错误:", e)
                    continue

                except TypeError as f:
                    print("遇到错误:", f)
                    continue
        else:
            print("无结果")


if __name__ == '__main__':
    start = time.time()
    main(3)
    end = time.time()
    print(end-start)

测试了一下,晚上10点多,在当时的网速下,爬取2页图片,大概用了403s,并且下载失败了几张;

 2.使用多进程爬取

如果要进行多进程爬取的话,必须要有一个准备并行执行的函数,既然要多进程爬取图片,所以应该把下载图片的功能定义为主函数

而上面代码中的main()函数不适合作为主函数,它是用爬取页码作为参数的,而我们并行执行时并不是一次爬取多页,而是并行爬取多个图片(有点绕)

需要改造一下:

(1)定义一个函数,来提取所有页面的图片url,并存到一个列表中(下面代码中的第39行:get_all_image_url()函数)

(2)定义一个主函数,接收图片url,然后下载图片(下面代码中的第82行:main()函数)

代码如下

  1 # -*- coding:utf-8 -*-
  2 import requests
  3 from requests.exceptions import RequestException
  4 from bs4 import BeautifulSoup
  5 import bs4
  6 import os, time
  7 from hashlib import md5
  8 from lxml import etree
  9 from multiprocessing import Pool, Lock, cpu_count
 10 
 13 def get_html(url):
 14     response = requests.get(url, timeout=15)
 15     # print(response.status_code)
 16     try:
 17         if response.status_code == 200:
 18 
 19             # print(response.text)
 20             return response.text
 21         else:
 22              return None
 23     except RequestException:
 24         print("请求失败")
 25         # return None
 26 
 27 
 28 def parse_html(html_text):
 29     html = etree.HTML(html_text)
 30 
 31     if len(html) > 0:
 32         img_src = html.xpath("//img[@class='photothumb lazy']/@data-original")  # 元素提取方法
 33         # print(img_src)
 34         return img_src
 35 
 36     else:
 37         print("解析页面元素失败")
 38 
 39 def get_all_image_url(page_number):
 40     """
 41     获取所有图片的下载url
 42     :param page_number: 爬取页码
 43     :return: 所有图片url的集合
 44     """
 45 
 46     base_url = 'https://imgbin.com/free-png/naruto/'
 47     image_urls = []
 48 
 49     x = 1  # 定义一个标识,用于给每个图片url编号,从1递增
 50     for i in range(1, page_number):
 51         url = base_url + str(i)  # 根据页码遍历请求url
 52         try:
 53             html = get_html(url)  # 解析每个页面的内容
 54             if html:
 55                 data = parse_html(html)  # 提取页面中的图片url
 56                 # print(data)
 57                 # time.sleep(3)
 58                 if data:
 59                     for j in data:
 60                         image_urls.append({
 61                             'name': x,
 62                             'value': j
 63                         })
 64                         x += 1  # 每提取一个图片url,标识x增加1
 65         except RequestException as f:
 66             print("遇到错误:", f)
 67             continue
 68     # print(image_urls)
 69     return image_urls
 70 
 71 def get_image_content(url):
 72     """请求图片url,返回二进制内容"""
 73     # print("正在下载", url)
 74     try:
 75         r = requests.get(url, timeout=15)
 76         if r.status_code == 200:
 77             return r.content
 78         return None
 79     except RequestException:
 80         return None
 81 
 82 def main(url, name):
 83     """
 84     主函数:实现下载图片功能
 85     :param url: 图片url
 86     :param name: 图片名称
 87     :return:
 88     """
 89     save_path = os.path.dirname(os.path.abspath('.')) + '/pics/'
 90     try:
 91         file_path = '{0}/{1}.jpg'.format(save_path, name)
 92         if not os.path.exists(file_path):  # 判断是否存在文件,不存在则爬取
 93             with open(file_path, 'wb') as f:
 94                 f.write(get_image_content(url))
 95                 f.close()
 96 
 97                 print('第{}个文件保存成功'.format(name))
 98         else:
 99             print("第{}个文件已存在".format(name))
100 
101     except FileNotFoundError as f:
102         print("第{}个文件下载时遇到错误,url为:{}:".format(name, url))
103         print("报错:", f)
104         raise
105 
106     except TypeError as e:
107         print("第{}个文件下载时遇到错误,url为:{}:".format(name, url))
108         print("报错:", e)
109 
110 
111 if __name__ == '__main__':
112     start = time.time()
113     urls = get_all_image_url(3)  # 获取所有图片url列表,爬取2页内容
114     # print(urls)
115     # print(cpu_count())  # 查看电脑是几核的
116 
117     pool = Pool(6)  # 我的电脑是6核的,所以开启6个线程试试
118 
119     for t in urls:  # 遍历列表中的每个图片下载url
120         # print(i)
121         pool.apply_async(main, args=(t["value"], t["name"]))  # 使用apply_async函数实现多进程(并行请求url,下载图片)
122 
123     pool.close()
124     pool.join()
125 
126     end = time.time()
127     print(end-start)

开启了6个进程,晚上10点多,同样爬取2页内容,大概用了30s,速度提升还是挺明显的

 多线程爬虫例子

看了开头分享的两篇文章后,应该了解到如下2点:

1、python解释器有GIL全局锁,导致多线程不能利用多核,多线程并发并不能在python中实现;

2、任务类型分为计算密集型IO密集型,对于IO密集型任务,大部分时间都在等待IO操作完成,在等待时间中CPU是不需要工作的,即使提供多核CPU也利用不上

网络爬虫属于IO密集型任务,发送网络请求等待响应、把爬取图片保存到本地,很多时间都消耗在等待中,如果启动多线程会明显提高效率

改造一下上面的代码,由多进程爬虫改为多线程爬虫,如下

  1 # -*- coding:utf-8 -*-
  2 import requests
  3 from requests.exceptions import RequestException
  4 import os, time
  5 from lxml import etree
  6 import threading
  7 
  8 
  9 def get_html(url):
 10     response = requests.get(url, timeout=15)
 11     # print(response.status_code)
 12     try:
 13         if response.status_code == 200:
 14 
 15             # print(response.text)
 16             return response.text
 17         else:
 18              return None
 19     except RequestException:
 20         print("请求失败")
 21         # return None
 22 
 23 
 24 def parse_html(html_text):
 25     html = etree.HTML(html_text)
 26 
 27     if len(html) > 0:
 28         img_src = html.xpath("//img[@class='photothumb lazy']/@data-original")  # 元素提取方法
 29         # print(img_src)
 30         return img_src
 31 
 32     else:
 33         print("解析页面元素失败")
 34 
 35 def get_all_image_url(page_number):
 36     """
 37     获取所有图片的下载url
 38     :param page_number: 爬取页码
 39     :return: 所有图片url的集合
 40     """
 41 
 42     base_url = 'https://imgbin.com/free-png/naruto/'
 43     image_urls = []
 44 
 45     x = 1  # 定义一个标识,用于给每个图片url编号,从1递增
 46     for i in range(1, page_number):
 47         url = base_url + str(i)  # 根据页码遍历请求url
 48         try:
 49             html = get_html(url)  # 解析每个页面的内容
 50             if html:
 51                 data = parse_html(html)  # 提取页面中的图片url
 52                 # print(data)
 53                 # time.sleep(3)
 54                 if data:
 55                     for j in data:
 56                         image_urls.append({
 57                             'name': x,
 58                             'value': j
 59                         })
 60                         x += 1  # 每提取一个图片url,标识x增加1
 61         except RequestException as f:
 62             print("遇到错误:", f)
 63             continue
 64     # print(image_urls)
 65     return image_urls
 66 
 67 def get_image_content(url):
 68     """请求图片url,返回二进制内容"""
 69     # print("正在下载", url)
 70     try:
 71         r = requests.get(url, timeout=15)
 72         if r.status_code == 200:
 73             return r.content
 74         return None
 75     except RequestException:
 76         return None
 77 
 78 def main(url, image_name):
 79     """
 80     主函数:实现下载图片功能
 81     :param url: 图片url
 82     :param image_name: 图片名称
 83     :return:
 84     """
 85     print('当前子线程: {}'.format(threading.current_thread().name))
 86     save_path = os.path.dirname(os.path.abspath('.')) + '/pics/'
 87     try:
 88         file_path = '{0}/{1}.jpg'.format(save_path, image_name)
 89         if not os.path.exists(file_path):  # 判断是否存在文件,不存在则爬取
 90             with open(file_path, 'wb') as f:
 91                 f.write(get_image_content(url))
 92                 f.close()
 93 
 94                 print('第{}个文件保存成功'.format(image_name))
 95         else:
 96             print("第{}个文件已存在".format(image_name))
 97 
 98     except FileNotFoundError as f:
 99         print("第{}个文件下载时遇到错误,url为:{}:".format(image_name, url))
100         print("报错:", f)
101         raise
102 
103     except TypeError as e:
104         print("第{}个文件下载时遇到错误,url为:{}:".format(image_name, url))
105         print("报错:", e)
106 
107 
108 if __name__ == '__main__':
109     start = time.time()
110     print('这是主线程:{}'.format(threading.current_thread().name))
111 
112     urls = get_all_image_url(3)  # 获取所有图片url列表
113     thread_list = []  # 定义一个列表,向里面追加线程
114 
115     for t in urls:
116         # print(i)
117         m = threading.Thread(target=main, args=(t["value"], t["name"]))  # 调用threading.Thread方法,传入主函数及其参数,启动多线程
118 
119         thread_list.append(m)
120 
121     for m in thread_list:
122         m.start()  # 调用start()方法,开始执行
123 
124     for m in thread_list:
125         m.join()  # 子线程调用join()方法,使主线程等待子线程运行完毕之后才退出
126 
127 
128     end = time.time()
129     print(end-start)

同样爬取2页,因为有100张图片,所以一共启动了100个子线程,耗时大约6.5s

 如果打开文件夹来看的话,图片是一下子都出现的 

 

 通过对比,可以看到对于网络爬虫这种IO密集型任务,多线程的效率其实是比多进程高的(6.5s VS 29.9s)

小结:本篇通过一个图片爬虫实例来说了一下如何使用python的多线程与多进程,对比单线程爬虫效率有明显提高,更多细节请自行查看,网上有很多优质资料,这里就不细说了

原文地址:https://www.cnblogs.com/hanmk/p/12747093.html