提升爬虫的速度,主要有三种方法:多线程爬虫、多进程爬虫和多协程爬虫。
并发(concurrency):是指在一个时间段内发生若干事件的情况;
并行(parallelism):是指在同一个时刻发生若干事件的情况。
同步:就是并发或并行的各个任务不是独立运行的,任务之间有一定的交替顺序;
异步:则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响。
多线程爬虫是以并发的方式执行的,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。
Python本身的设计对多线程的执行有所限制。在设计之初,为了数据安全所做的决定设置有GIL(Global Interpreter Lock,全局解释器锁)。在Python中,一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。
每次释放GIL锁,线程之间都会进行锁竞争,而切换线程会消耗资源。由于GIL锁的存在,Python里一个进程永远只能同时执行一个线程,这就是在多核CPU上Python的多线程效率不高的原因。
Python的多线程对于IO密集型代码比较友好,网络爬虫能够在获取网页的过程中使用多线程,从而加快速度。
单线程爬虫:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : Promoting.py
@Author: Xinzhe.Pang
@Date : 2019/7/10 23:12
@Desc :
"""
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)
Python中使用多线程的两种方法:
(1)函数式:调用_thread模块中的start_new_thread()函数产生新线程。(注意:是一个下划线)
(2)类包装法:调用Threading库创建线程,从threading.Thread继承。
thread提供了低级别、原始的线程,相比于threading模块,功能还是比较有限的。threading模块提供了Thread类来处理线程。包括以下方法:
threading的一个简单例子:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiThread02.py
@Author: Xinzhe.Pang
@Date : 2019/7/10 23:34
@Desc :
"""
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")
简单的多线程爬虫:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiThread03.py
@Author: Xinzhe.Pang
@Date : 2019/7/10 23:42
@Desc :
"""
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, 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):
for i in range(delay[0], delay[1] + 1):
try:
r = requests.get(link_list[i], timeout=20)
print(threadName, r.status_code, link_list[i])
except Exception as e:
print(threadName, 'Error:', e)
thread_list = []
delay_list = [(0, 200), (201, 400), (401, 600), (601, 800), (801, 1000)]
# 创建新线程
for i in range(1, 6):
thread = myThread("Thread-" + str(i), delay_list[i - 1])
thread.start()
thread_list.append(thread)
# 等待所有线程完成
for thread in thread_list:
thread.join()
end = time.time()
print('简单多线程爬虫的总时间为:', end - start)
print("Exiting Main Thread")
使用Queue的多线程爬虫。Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO队列Queue、LIFO队列LifoQueue和优先级队列PriorityQueue。代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiThread04Queue.py
@Author: Xinzhe.Pang
@Date : 2019/7/10 23:57
@Desc :
"""
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)
start = time.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)
while True:
try:
print_time(self.name, self.delay)
except:
break
print("Exiting " + self.name)
def print_time(threadName, delay):
url = delay.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(delay.qsize(), threadName, r.status_code, url)
except Exception as e:
print(delay.qsize(), threadName, url, 'Error:', e)
threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5"]
workQueue = Queue.Queue(1000)
threads = []
# 创建新线程
for tName in threadList:
thread = myThread(tName, workQueue)
thread.start()
threads.append(thread)
# 填充队列
for url in link_list:
workQueue.put(url)
# 等待所有线程完成
for t in threads:
t.join()
end = time.time()
print('Queue多线程爬虫的总时间为:', end - start)
print("Exiting Main Thread")
结果比对发现,使用Queue方法比之前的简单多线程爬虫方法时间短,可见Queue方法能够增加抓取的效率。
多线程爬虫并不能充分发挥多核CPU的资源。多进程爬虫则可以利用CPU的多核,进程数取决于计算机CPU的处理器个数。在Python中,如果要用多进程,就需要用到multiprocessing这个库。
使用multiprocessing库有两种方法,一种是使用Process+Queue的方法,另一种是使用Pool+Queue的方法。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiProcess01.py
@Author: Xinzhe.Pang
@Date : 2019/7/11 22:01
@Desc :
"""
from multiprocessing import Process, Queue
import time
import requests
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 MyProcess(Process):
def __init__(self, q):
Process.__init__(self)
self.q = q
def run(self):
print("Starting ", self.pid)
while not self.q.empty():
crawler(self.q)
print("Exiting ", self.pid)
def crawler(q):
url = q.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(q.qsize(), r.status_code, url)
except Exception as e:
print(q.qsize(), url, 'Error: ', e)
if __name__ == '__main__':
ProcessName = ["Process-1", "Process-2", "Process-3"]
workQueue = Queue(1000)
# 填充队列
for url in link_list:
workQueue.put(url)
for i in range(0, 3):
p = MyProcess(workQueue)
p.daemon = True
p.start()
p.join()
end = time.time()
print('Process + Queue多进程爬虫的总时间为:', end - start)
print('Main process Ended!')
Pool可以提供指定数量的进程供用户调用。当有新的请求提交到Pool中时,如果池还没有满,就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定的最大值,该请求就会继续等待,直到池中有进程结束才能够创建新的进程。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiProcess02.py
@Author: Xinzhe.Pang
@Date : 2019/7/11 22:11
@Desc :
"""
from multiprocessing import Pool, Manager
import time
import requests
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()
def crawler(q, index):
Process_id = 'Process-' + str(index)
while not q.empty():
url = q.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(Process_id, q.qsize(), r.status_code, url)
except Exception as e:
print(Process_id, q.qsize(), url, 'Error: ', e)
if __name__ == '__main__':
manger = Manager()
workQueue = manger.Queue(1000)
# 填充队列
for url in link_list:
workQueue.put(url)
pool = Pool(processes=3)
for i in range(4):
pool.apply_async(crawler, args=(workQueue, i))
print("Started processes")
pool.close()
pool.join()
end = time.time()
print('Pool + Queue多进程爬虫的总时间为:', end - start)
print('Main process Ended!')
注意:通过pool.apply_async创建非阻塞进程,也就是说不需要等到进程运行完成就可以添加其他继承了,如果要使用阻塞方法,可以将其改成pool.apply即可。阻塞方法一定要等到某个进程执行完才会添加另一个进程。
协程(Coroutine)是一种用户态的轻量级线程,在程序级别模拟系统级别用的进程。在一个进程中,一个线程通过程序的模拟方法实现高并发。
使用协程主要有三个好处:1.单线程,少了上下文切换,所以系统消耗很小;2.方便切换控制流,能保存上一次调用时的状态(所有局部状态的一个特定组合);3.高扩展性和高并发性。
缺点:1.协程本质是一个单线程,不能同时使用单个CPU的多核,需要和进程配合才能运行在多CPU上;2,有长时间阻塞的IO操作时不要用协程,因为可能会阻塞整个程序。
在Python的协程中可以使用gevent库。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@File : MultiCoroutine.py
@Author: Xinzhe.Pang
@Date : 2019/7/11 22:25
@Desc :
"""
import gevent
from gevent.queue import Queue, Empty
import time
import requests
from gevent import monkey # 把下面有可能有IO操作的单独做上标记
monkey.patch_all() # 将IO转为异步执行的函数
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()
def crawler(index):
Process_id = 'Process-' + str(index)
while not workQueue.empty():
url = workQueue.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(Process_id, workQueue.qsize(), r.status_code, url)
except Exception as e:
print(Process_id, workQueue.qsize(), url, 'Error: ', e)
def boss():
for url in link_list:
workQueue.put_nowait(url)
if __name__ == '__main__':
workQueue = Queue(1000)
gevent.spawn(boss).join()
jobs = []
for i in range(10):
jobs.append(gevent.spawn(crawler, i))
gevent.joinall(jobs)
end = time.time()
print('gevent + Queue多协程爬虫的总时间为:', end - start)
print('Main Ended!')
gevent库中的monkey能把可能有IO操作的单独做上标记,将IO变成可以异步执行的函数。
多协程爬虫能够很好地支持高并发的工作。
参考资料:《Python网络爬虫从入门到实践》