多线程爬虫

来自CloudWiki
跳转至: 导航搜索

多线程与网络爬虫

Python的多线程对于IO密集型代码比较友好,网络爬虫能够在获取网页的过程中使用多线程,从而加快速度。

下面将以获取访问量最大的1000个中文网站的速度为例,通过和单线程的爬虫比较,证实多线程方法在网络爬虫速度上的提升。这1000个访问量最大的中文网站是在Alexca.cn上获取的,地址如下:https://github.com/Santostang/PythonScraping/blob/master/Cha%207%20-%E6%8F%90%E5%8D%87%E7%88%AC%E8%99%AB%E7%9A%84%E9%80%9F%E5%BA%A6/alexa.txt

简单单线程爬虫

首先,以单线程(单进程)的方式抓取这1000个网页,代码如下:

import requests
import time

link_list = []
with open('alexa.txt','r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()
for eachone in link_list:
    try:
        r=requests.get(eachone)
        print(r.status_code,eachone)
    except Exception as e:
        print('Error: ',e)

end = time.time()
print('串行的总时间为:',end-start)

运行结果为:

串行的总时间为: 2341.845444202423

Python 3多线程

threading模块则提供了Thread类来处理线程,包括以下方法。

·run():用以表示线程活动的方法。
·start():启动线程活动。
·join([time]):等待至线程中止。阻塞调用线程
·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(threadName,delay):
    counter = 0
    while counter <3:
        time.sleep(delay)
        print(threadName,time.ctime())
        counter +=1
        

threads = []

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

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

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

#等待所有线程完成
for t in threads:
    t.join()

print("Exiting Main Thread")

运行上述代码,得到的结果是:

Starting Thread-1
Starting Thread-2
Thread-1 Wed Jul 18 10:48:42 2018
Thread-2 Wed Jul 18 10:48:43 2018
Thread-1 Wed Jul 18 10:48:43 2018
Thread-1 Wed Jul 18 10:48:44 2018
Exiting Thread-1
Thread-2 Wed Jul 18 10:48:45 2018
Thread-2 Wed Jul 18 10:48:47 2018
Exiting Thread-2
Exiting Main Thread

在上述代码中,我们将任务手动地分到两个线程中,即thread1=myThread("Thread-1",1)。然后在myThread这个类中对线程进行设置,使用run()表示线程运行的方法,当counter小于3时,打印该线程的名称和时间。

然后使用thread.start()开启线程,使用threads.append()将线程加入线程列表,用t.join()等待所有子线程完成才会继续执行

简单多线程爬虫

刚刚我们使用threading完成了多线程的简单代码,现在就将Python多线程的代码应用在获取1000个网页上,并开启5个线程,代码如下:

import threading
import requests
import time

link_list = []
with open('alexa.txt', 'r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)
        
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)#线程开始
        crawler(self.name, self.link_range)#爬取指定范围的网页
        print ("Exiting " + self.name)#线程结束

#爬取函数,爬取指定范围的网页,
#threadName指定线程名,link_range指定爬取网页的范围
def crawler(threadName, link_range):
    for i in range(link_range[0],link_range[1]+1):
        try:
            r = requests.get(link_list[i], timeout=20)#用requests库获取网页
            print (threadName, r.status_code, link_list[i])#打印网页状态码
        except Exception as e: 
            print(threadName, 'Error: ', e)#处理异常
        
thread_list = []
link_range_list = [(0,200),(201,400),(401,600),(601,800),(801,1000)]

# 创建新线程
for i in range(0,5):#从0到4,开启5个线程。
    thread = myThread("Thread-" + str(i), link_range_list[i])
    thread.start()
    thread_list.append(thread)#将线程加入线程列表之中
    
# 等待所有线程完成
for thread in thread_list:
    thread.join()

end = time.time()
print ('简单多线程爬虫的总时间为:', end-start)
print ("Exiting Main Thread")

该程序运行后,得到如下输出:

简单多线程爬虫的总时间为: 331.42331290245056
Exiting Main Thread

上述代码存在一些可以改进之处:因为我们把整个链接列表分成了5等份,所以当某个线程先完成200条网页的爬虫后会退出线程,这样就只剩下有4个线程在运行。相对于5个线程,速度会有所下降,到最后剩下一个线程在运行时,就会变成单线程。

使用Queue的多线程爬虫

有没有一种方式能够在完成1000个网页的抓取之前都使用5个线程的全速爬虫呢?这时可以使用Queue。Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue

将这1000个网页放入Queue的队列中,各个线程都是从这个队列中获取链接,直到完成所有的网页抓取为止,代码如下:

import threading
import requests
import time
import queue as Queue

#从文件中获取待爬取的网址
link_list = []
with open('alexa.txt', 'r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split('\t')[1]
        link = link.replace('\n','')
        link_list.append(link)

#网页爬取线程:myThread        
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:
                crawler(self.name, self.q)
            except:
                break
        print ("Exiting " + self.name)

#主爬取函数      
def crawler(threadName, q):
    readme='''
这就好比银行排队,
单线程表示该支行只有一个窗口,要处理1000个人需要花费很长时间;
简单的多线程是开5个窗口,然后将1000个人平均分到5个窗口中,
因为有些窗口可能处理得比较快,所以先处理完了,
但是其他窗口的人不能去那个已经处理完的窗口排队,这样就造成了资源的闲置。
Queue方法相对于前两种方法而言,是将1000人排一个长队,
5个窗口中哪个窗口有了空位,便叫队列中的第一个过去
(FIFO先入先出方法)。
'''
    url = q.get(timeout=2)#获取队列中的链接
    try:
        r = requests.get(url, timeout=20)
        print (q.qsize(), threadName, r.status_code, url)
    except Exception as e: 
        print (q.qsize(), threadName, url, 'Error: ', e)


        
threadList = ["Thread-1", "Thread-2", "Thread-3","Thread-4", "Thread-5"]
workQueue = Queue.Queue(1000)#建立了一个队列的对象workQueue
threads = []

# 创建新线程
for tName in threadList:
    thread = myThread(tName, workQueue)#将workQueue对象传入了myThread中,
    thread.start()
    threads.append(thread)
    
# 填充队列
for url in link_list:#将获得的1000个网址填充到队列之中
    workQueue.put(url)

# 等待所有线程完成
for t in threads:
    t.join()

end = time.time()
print ('Queue多线程爬虫的总时间为:', end-start)
print ("Exiting Main Thread")

Queue多线程爬虫的总时间为: 283.01684951782227

Exiting Main Thread

返回 大数据分析

参考文档:《Python爬虫:从入门到实践》