Python爬虫小白教程(五)—— 多线程爬虫


在爬取许多网页或者爬取图片的时候,我们的爬虫会遇到一个严重的问题,爬取速度太慢。解决速度太慢的方法主要有三个,多线程,多进程和多协程,下面我们先学习多线程爬虫。

并发和并行

要学习多线程爬虫,首先我们应该理解为什么多线程爬虫可以增加爬取的速度。要理解为什么多线程能够增加爬取的速度,要先理解并发和并行的概念。

如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于 “存在” 这个词。在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
摘自:《并发的艺术》 — 〔美〕布雷谢斯

即并发就是指代码逻辑上可以并行,有并行的潜力,但是不一定当前是真的以物理并行的方式运行。
并发指的是代码的性质,并行指的是物理运行状态

同步和异步

同步和异步比起并发和并行要好理解得多。同步就是上一个任务执行完才可以进行下一个任务,异步是上一个任务正在被执行的时候执行下一个任务。

例如现在有两个任务:

  • 安装一个大型的软件,只需要点击安装,然后等待电脑将其完成安装即可
  • 出去拿快递,并且快递点很远,往返要耗费许多时间

如果我们先出去拿快递,拿完之后再点击安装,这就是串行;如果我们先点击安装,再出去拿快递,这时安装程序和拿快递是在同时被执行,这时候就是并行。在这个例子中显然先安装后拿快递比先拿快递再安装要节省许多时间,在计算机中也是如此。

单线程爬虫

首先,用单线程方式爬取中国访问量最大的50个网站,并剔除中国大陆无法正常访问的网站。

import time
import re
import requests
from bs4 import BeautifulSoup

# 定义获得页面函数
def get_page(url,params=None,headers=None,proxies=None,timeout=None):

    response = requests.get(url, headers=headers, params=params, proxies=proxies, timeout=timeout)
    print("解析网址:",response.url)
    page = BeautifulSoup(response.text, 'lxml')
    print("响应状态码:", response.status_code)
    
    return page

url = "https://www.alexa.com/topsites/countries/CN"
page = get_page(url)
div_list = page.find_all("div", class_="td DescriptionCell")

URLs = []
for div in div_list:
    URLs.append(div.p.a.text.strip())    
# 剔除无法正常访问的网站
URLs.pop(15)
URLs.pop(30)
URLs.pop(35)
URLs.pop(41)
URLs.pop(12)
URLs.pop(24)

print(URL)

运行代码,得到结果如下:
在这里插入图片描述
因为现在获得的网站网址只有后面部分,因此我们需要通过试错来获得正确的网址,代码如下:

for i in range(len(URLs)):
    try:
        new_url = URLs[i]
        page = get_page(new_url, timeout=1.5)
    except:
        try:
            try:
                new_url = "https://www." + URLs[i]
                page = get_page(new_url, timeout=1)
            except:
                new_url = "https://" + URLs[i]
                page = get_page(new_url, timeout=1)
        except:
            try:
                new_url = "http://www." + URLs[i]
                page = get_page(new_url, timeout=1)
            except:
                new_url = "http://" + URLs[i]
                page = get_page(new_url, timeout=1)
    finally:
        URLs[i] = new_url
        
print(len(URLs))

得到URLs的长度为44。
下面便访问这44个网站并记录下串行使用的时间:

start = time.time()
for url in URLs:
    page = get_page(url)

end = time.time()
print("串行耗时:{} s".format(end - start))

运行代码,得到结果如下:
在这里插入图片描述

学习Python多线程

我们使用threading模块来学习Python多线程。threading模块提供了Tread类来处理线程,包括一下方法。

  • run(): 用以表示线程启动的方法
  • start(): 启动线程活动
  • join([time]): 等待至线程中止。阻塞调用线程直至线程的 join() 方法被调用为止。
  • isAlive(): 返回线程是否是活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

下面介绍使用threading的一个简单的例子,看看多线程是如何运行的。

import threading
import time

# 为线程定义一个函数
class myThread(threading.Thread):
    def __init__(self, name, delay):
        threading.Thread.__init__(self)
        self.name = name
        self.delay = delay
    def run(self):
        print("Starting " + self.name)
        print_time(self.name, self.delay)
        print("Exiting " + self.name)
        
def print_time(threadNmae, delay):
    counter = 0
    while counter < 3:
        time.sleep(delay)
        print(threadNmae, time.ctime())
        counter += 1
        
threads = []

# 创建新线程
thread1 = myThread(name="Thread-1", delay=1)
thread2 = myThread(name="Thread-2", delay=2)

# 开启新线程
thread1.start()
thread2.start()

# 添加新线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
    t.join()
    
print("Exiting Main Thread")

运行代码,得到的结果为:
在这里插入图片描述
在上述代码中,我们在myThread这个类中对线程进行设置,使用 run() 表示线程运行的方法。通过创建thread对象手动将任务分配进两个线程中,然后我们使用 thread.start() 方法来启动线程,使用threads.append() 方法来将线程添加到线程列表中,用 t.join() 等待所有子线程结束后再结束主线程。

简单多线程爬虫

通过简单修改之前的多线程代码,将Python多线程的代码应用到获取44个网页上,并开启4个线程,代码如下:

import requests
import threading
import time

start = time.time()
# 为线程定义一个函数
class myThread(threading.Thread):
    def __init__(self, name, link_range):
        threading.Thread.__init__(self)
        self.name = name
        self.link_range = link_range
    def run(self):
        print("Starting " + self.name)
        crawl(self.name, self.link_range)
        print("Exiting " + self.name)
        
def crawl(threadNmae, link_range):
    for i in range(link_range[0], link_range[1]+1):
        try:
            r = requests.get(URLs[i], timeout=1.5)
            print(threadNmae, r.status_code, URLs[i])
        except Exception as e:
            print(threadNmae, "Error: ", e)
        
threads = []
link_range_list = [(0,11),(12,22),(23,33),(34,44)]


for i in range(1,5):
    # 创建4个新线程
    thread = myThread("Thread-" + str(i), link_range=link_range_list[i-1])
    # 开启新线程
    thread.start()
    # 添加新线程到线程列表
    threads.append(thread)

# 等待所有线程完成
for thread in threads:
    thread.join()
    
end = time.time()
print("简单多线程爬虫耗时:{} s".format(end - start))
print("Exiting Main Thread")

运行上述代码得到结果如下。
在这里插入图片描述
\cdots
在这里插入图片描述
上述代码中有一个缺点,从图片中可以看到最后只有 Thread-2 在运行了,即每个线程结束后,总线程数就会少一个,最后 Thread-4 结束后只剩下 Thread-2 就变成了单线程。下面使用Queue来解决这个问题。

使用Queue的多线程爬虫

Python中的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列 Queue、LIFO(后入先出)队列 LifoQueue和优先级队列 PriorityQueue。
将这44个网页放入 Queue 的队列中,各线程都是从这个队列中获得链接,直至所有网站都完成爬取为止,代码如下:

import threading
import requests
import time
import queue

start = time.time()
# 为线程定义一个函数
class myThread(threading.Thread):
    def __init__(self, name, q):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q
    def run(self):
        print("Starting " + self.name)
        while True:
            try:
                crawl(self.name, self.q)
            except:
                break
        print("Exiting " + self.name)
        
def crawl(threadNmae, q):
    url = q.get(timeout=2)
    try:
        r = requests.get(url, timeout=1.5)
        print(threadNmae, r.status_code, url)
    except Exception as e:
        print(threadNmae, "Error: ", e)

# 填充队列            
workQueue = queue.Queue(len(URLs))
for url in URLs:
    workQueue.put(url)
    
threads = []
for i in range(1,5):
    # 创建4个新线程
    thread = myThread("Thread-" + str(i), q=workQueue)
    # 开启新线程
    thread.start()
    # 添加新线程到线程列表
    threads.append(thread)
    
# 等待所有线程完成
for thread in threads:
    thread.join()
    
end = time.time()
print("Queue多线程爬虫耗时:{} s".format(end - start))
print("Exiting Main Thread")

与之前的简单多线程爬虫不同,这里我们先使用workQueue = queue.Queue(len(URLs))创建了一个队列对象,再使用workQueue.put(url)将网址放入队列中。将队列对象传入myTread中,即:thread = myThread(name, queue)。在线程中使用url = q.get(timeout=2)来获取队列中的链接。

运行代码后得到结果如下:
在这里插入图片描述
\cdots
在这里插入图片描述
发现使用 Queue 方法的多线程爬虫竟然还没有简单多线程爬虫节省时间,这是为什么呢?

经过多次实验,我发现Queue多线程爬虫爬取完所有网址所用的时间大约为3秒,剩下的时间都是最后退出线程花费的时间。因此我认为当需要爬取网站的数目比较大时,使用Queue多线程爬虫会比不使用Queue的多线程爬虫所花费的时间更少。

爬虫系列

Python爬虫小白教程(一)—— 静态网页抓取
Python爬虫小白教程(二)—— 爬取豆瓣评分TOP250电影
Python爬虫小白教程(三)——使用正则表达式分析网页
Python爬虫小白教程(四)—— 反反爬之IP代理池
Python爬虫小白教程(五)—— 多线程爬虫

猜你喜欢

转载自blog.csdn.net/weixin_44547562/article/details/103955734
今日推荐