python如何在网络爬虫程序中使用多线程(threading.Thread)

一、多线程的基础知识

关于多线程相关的基础知识,已经在另一篇文章中有过详细描述,此处不再赘述。有需要的可以参考:
Python并发编程之threading模块

要点主要是:

  • 使自己的类继承自threading.Thread
  • 在自己的类里重写run方法

二、在网络爬虫中使用多线程

2.1 从单线程版本入手

这里,我们仍然以下载单张图片的简单程序为例,首先,我们可以实现一个单线程版的图片下载器,代码范例可以从这篇文章中获取:
python使用requests库下载单张图片的简单示例

import requests
import random
import os

class PicDownloader():
    def __init__(self):
        self.count = 0
        self.file_name_prefix = 'img'
        self.working_dir = '.'
    
    def download_img(self, img_url, dir='.'):
        '''
        下载url指定的图片到本地dir目录
        '''
        self.count += 1
        file_name = self.file_name_prefix + '_' + str(self.count) + '.jpg'

        if os.path.isdir(dir):
            self.working_dir = dir
        else:
            try:
                os.makedirs(dir)
                self.working_dir = dir
            except OSError as e:
                print(f'create dir: {dir} failed')
        
        file_path = os.path.join(self.working_dir, file_name)
        print(f'开始下载文件:{file_path}')
        headers = self.get_headers()
        try:
            res = requests.get(url=img_url, headers=headers)
            if 200 == res.status_code:
                with open(file_path, 'wb') as file:
                    file.write(res.content)
        except Exception as e:
            print(f'下载文件失败: {file_path}')
            print(repr(e))
        print('文件下载成功')
        
    # 本例中实际不需要这么多
    def get_headers(self):
        user_agent_list = [
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
            "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"]

        userAgent = random.choice(user_agent_list)
        headers = {'User-Agent': userAgent}
        return headers


if __name__== '__main__':
    downloader = PicDownloader()
    downloader.download_img('https://images.weserv.nl/?url=https%3A%2F%2F23img.com%2Fi%2F2022%2F08%2F17%2Fqykvvx.jpg', 'd:/test')
    

2.2 将单线程版本改写为多线程版本

如何将其改造为多线程版本呢?

首先,导入threading模块,并使PicDownloader继承自threading.Thread, 构造函数中带入一些必要的参数,即可

class PicDownloader(threading.Thread):
    def __init__(self, name, img_url, file_name, dir='.'):
        threading.Thread.__init__(self)   # 注意,这一步是必须的,先对Thread进行相关初始化
        self.setName(name)                # 线程名是可选的,这里设置只是为了后面观察线程方便
        self.daemon = True                # 按需设置
        self.url = img_url
        self.file_name = file_name
        self.working_dir = dir

现在PicDownloader类就可以以线程的方式来运行了,我们还需要重写一下run()方法, 其实就是把原先的主要工作方法download_img()里面的内容挪到了run()方法内,你甚至可以理解为就是把download_img()重命名为run()方法。

def run(self):
    '''
    下载url指定的图片到本地dir目录
    '''
    if os.path.isdir(self.working_dir):
        pass
    else:
        try:
            os.makedirs(self.working_dir)
        except OSError as e:
            print(f'create dir: {dir} failed')
    
    if not self.file_name.endswith('.jpg'):
        self.file_name = self.file_name + '.jpg'
    
    file_path = os.path.join(self.working_dir, self.file_name)
    print(f'thread:{self.name} -> 开始下载文件:{file_path}')
    headers = self.get_headers()
    try:
        res = requests.get(url=self.url, headers=headers)
        if 200 == res.status_code:
            with open(file_path, 'wb') as file:
                file.write(res.content)
    except Exception as e:
        print(f'下载文件失败: {file_path}')
        print(repr(e))
    print(f'thread:{self.name} -> 文件下载成功:{file_path}')

至此,多线程的改造基本结束。

完整的代码:

# coding=utf-8

import requests
import threading
import random
import os

class PicDownloader(threading.Thread):
    def __init__(self, name, img_url, file_name, dir='.'):
        threading.Thread.__init__(self)
        self.setName(name)
        self.daemon = True
        self.url = img_url
        self.file_name = file_name
        self.working_dir = dir
    
    def run(self):
        '''
        下载url指定的图片到本地dir目录
        '''
        if os.path.isdir(self.working_dir):
            pass
        else:
            try:
                os.makedirs(self.working_dir)
            except OSError as e:
                print(f'create dir: {dir} failed')
        
        if not self.file_name.endswith('.jpg'):
            self.file_name = self.file_name + '.jpg'
        
        file_path = os.path.join(self.working_dir, self.file_name)
        print(f'thread:{self.name} -> 开始下载文件:{file_path}')
        headers = self.get_headers()
        try:
            res = requests.get(url=self.url, headers=headers)
            if 200 == res.status_code:
                with open(file_path, 'wb') as file:
                    file.write(res.content)
        except Exception as e:
            print(f'下载文件失败: {file_path}')
            print(repr(e))
        print(f'thread:{self.name} -> 文件下载成功:{file_path}')
        
    def get_headers(self):
        user_agent_list = [
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
            "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"]

        userAgent = random.choice(user_agent_list)
        headers = {'User-Agent': userAgent}
        return headers


if __name__== '__main__':
    downloader = PicDownloader(img_url='https://images.weserv.nl/?url=https%3A%2F%2F23img.com%2Fi%2F2022%2F08%2F17%2Fqykvvx.jpg', file_name='1', dir='d:/test')
    downloader.start()
    downloader.join()

2.3 运行多线程版本程序

多线程版本的类class PicDownloader(threading.Thread)改造完成后,我们只需要调用start()方法就可以启动线程。例如:

if __name__== '__main__':
    downloader = PicDownloader(img_url='https://images.weserv.nl/?url=https%3A%2F%2F23img.com%2Fi%2F2022%2F08%2F17%2Fqykvvx.jpg', file_name='1', dir='d:/test')
    downloader.start()
    downloader.join()
  • start()
    此方法会在一个单独的控制线程中调用run()方法,启动线程。此方法只能调用一次。

  • join([timeout])
    等待直到线程终止或者出现超时为止。timeout是一个浮点数,用于指定以秒为单位的超时时间。必须在start()方法之后调用,如果在线程启动之前就连接它将出现错误。

2.4 将多线程应用到爬虫程序中

以上程序单独执行,即下载单张图片,还不能体现多线程的优势。结合爬虫程序,我们看看如何运用多线程版本的图片下载器。

假设一个最简单的场景,就是我们想从一个页面上,爬取该页面上所有的图片。

当然了,这前期有一些工作要做,比如将所有的图片url都解析出来(本例中存到了列表img_url_list中),那么之后就可以发挥出多线程版本图片下载器的威力了。示例代码如下:

count = 0
path = 'd:/test'
pic_download_threads = []
for img_url in img_url_list:
    count += 1
    file_name = 'img_' + str(count)
    pic_download_threads.append(PicDownloader(str(count), img_url, file_name, path))

for working_thread in pic_download_threads:
    working_thread.start()

for working_thread in pic_download_threads:
    working_thread.join(5)

即使img_url_list里只有十几张图片的url,多线程版本也比单线程版本要快的多。如果图片url数目更多的话,速度优势就更明显了。

三、考虑使用线程池

上面是常规的多线程处理方式,如果img_url_list里只有十几张图片的url,多线程版本将比单线程版本要快的多。

但我们回过头来考虑以上程序,假如img_url_list里面的成员数非常多的话,比如该页面可能有数百张图片需要下载时,此时会发生什么?如果像上面这么做,我们将启动数百个工作线程!!!而系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。并且,当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃。

在这种情况下,使用线程池就可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

关于线程池的使用,请移步我的另一篇博文:《在Python网络爬虫程序中使用线程池》

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
爬虫多线程和队列是非常常见的技术应用。下面是它们在爬虫的应用示例: 1. 多线程使用多线可以提高爬取数据的效率,同时可以并发处理多个请求。例如,可以将爬虫程序分为多个线程,每个线程负责处理一个请求,这样可以同时发送多个请求并同时处理响应。 2. 队列:使用队列可以实现任务的调度和管理。例如,可以将待爬取的URL放入队列,然后创建多个爬虫线程从队列获取URL,并进行相应的爬取操作。这样可以有效地控制任务的执行顺序,避免资源竞争和重复爬取。 综合应用示例: ```python import threading import queue import requests # 创建队列 url_queue = queue.Queue() # 定义爬虫函数 def crawler(): while not url_queue.empty(): url = url_queue.get() # 发送请求并处理响应 response = requests.get(url) # 其他处理操作... # 添加待爬取的URL到队列 url_queue.put('http://example.com/page1') url_queue.put('http://example.com/page2') url_queue.put('http://example.com/page3') # 创建多个爬虫线程 num_threads = 3 threads = [] for _ in range(num_threads): t = threading.Thread(target=crawler) threads.append(t) # 启动线程 for t in threads: t.start() # 等待所有线程执行完毕 for t in threads: t.join() # 所有任务完成 print("All tasks finished.") ``` 这个示例展示了如何使用多线程和队列来进行爬虫任务的并发处理和调度。通过将待爬取的URL放入队列,然后创建多个爬虫线程从队列获取URL并进行相应的爬取操作,可以实现高效的爬取任务处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

smart_cat

你的鼓励将是我写作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值