本系列为自己学习爬虫的相关笔记,如有误,欢迎大家指正
要学习提升爬虫速度用到的知识,必须先熟悉并发和并行、同步和异步的概
并发(concurrency)和并行(parallelism)是两个相似的概念。并发是指在一个时间段内发生若干事件的情况,并行是指在同一时刻发生若干事件的情况。
使用单核CPU和多核CPU来说就是:在使用单核CPU时,多个工作任务是以并发的方式运行的,因为只有一个CPU,所以各个任务会分别占用CPU的一段时间依次执行。如果在自己分得的时间段没有完成任务,就会切换到另一个任务,然后在下一次得到CPU使用权的时候再继续执行,直到完成。在这种情况下,因为各个任务的时间段很短、经常切换,所以给我们的感觉是“同时”进行。在使用多核CPU时,在各个核的任务能够同时运行,这是真正的同时运行,也就是并行。
同步就是并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在运行完一个任务得到结果后,另一个任务才会开始运行。就像接力赛跑一样,要拿到交接棒之后下一个选手才可以开始跑。
异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响,任务之间就像比赛的各个选手在不同的赛道比赛一样,跑步的速度不受其他赛道选手的影响。
多线程爬虫是以并发的方式执行的。也就是说,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。
在Python设计之初,为了数据安全所做的决定设置有GIL(Global Interpreter Lock,全局解释器锁)。在Python中,一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。在一个Python进程中,只有一个GIL,拿不到GIL的线程就不允许进入CPU执行。
正因为如此,在多核CPU上Python的多线程效率也不高。因为每次释放GIL锁,线程之间都会进行锁竞争,而切换线程会消耗资源。
虽然如此,但是因为网络爬虫是IO密集型,线程能够有效地提升效率,因为单线程下有IO操作会进行IO等待,所以会造成不必要的时间浪费,而开启多线程能在线程A等待时自动切换到线程B,可以不浪费CPU的资源,从而提升程序执行的效率。
Python的多线程对于IO密集型代码比较友好
import requests
import time
link_list = []
with open('most.txt') as file:
file_list = file.readlines()
for each in file_list:
link = each.split()[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中使用多线程有两种方法
调用_thread模块中的start_new_thread()函数产生新线程
简单示例
import _thread
import time
# 为线程定义一个函数
def print_time(threadName,delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(threadName,time.ctime())
_thread.start_new_thread(print_time,('Thread-1',1))
_thread.start_new_thread(print_time,('Thread-2',2))
#time.sleep(5)
print('Main Finished')
这个代码没出现自己想要的结果,暂时没看出来问题在哪。
_thread中使用start_new_thread ()函数来产生新线程
def start_new_thread(function, args, kwargs=None):
function表示线程函数
args为传递给线程函数的参数,它必须是tuple类型
调用Threading库创建线程,从threading.Thread继承
threading模块提供了Thread类来处理线程,包括以下方法。
示例:
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')
import threading
import requests
import time
link_list = []
with open('most.txt','r') as file:
file_list = file.readlines();
for eachone in file_list:
link = eachone.split()[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)
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)
print(threadName,r.status_code,link_list[i])
except Exception as e:
print(threadName,'Error: ' ,e)
link_range_list = [(0,200),(201,400),(401,600),(601,800),(801,1000)]
thread_list = []
# 创建新线程
for i in range(1,6):
thread = myThread('Thread-'+str(i),link_range_list[i-1])
thread.start()
thread_list.append(thread)
# 等待所有线程完成
for thread in thread_list:
thread.join() #thread.join()方法等待各个线程执行完毕
end = time.time()
print('简单多线程爬虫的总时间为:',end-start)
print('Exiting Main Thread')
这个代码还是有改进的余地,比如说某一个线程中的200个已经结束了,那么就还剩4个线程。到最后就可能变成单线程了。
import threading
import time
import requests
import queue as Queue
link_list = []
with open('most.txt','r') as file:
file_list = file.readlines()
for eachone in file_list:
link = eachone.split()[1]
link = link.replace('\n','')
link_list.append(link)
start = time.time()
class myThread(threading.Thread):
def __init__(self,threadName,q):
threading.Thread.__init__(self)
self.name = threadName
self.q=q
def run(self):
print('starting '+self.name)
while True:
try:
crawler(self.name,self.q)
except:
break
print('exit '+self.name)
def crawler(threadName,q):
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)
thread_list = ['Thread-1','Thread-2','Thread-3','Thread-4','Thread-5']
workQueue = Queue.Queue(1000)
threads = []
# 创建新线程
for tName in thread_list:
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')
Python的多线程爬虫只能运行在单核上,各个线程以并发的方法异步运行。由于GIL(Global Interpreter Lock,全局解释器锁)的存在,多线程爬虫并不能充分地发挥多核CPU的资源。
多进程爬虫则可以利用CPU的多核,进程数取决于计算机CPU的处理器个数。由于运行在不同的核上,各个进程的运行是并行的。在Python中,如果我们要用多进程,就需要用到multiprocessing这个库。
使用multiprocess库的两种方法
当进程数量大于CPU的内核数量时,等待运行的进程会等到其他进程运行完毕让出内核为止。因此,如果CPU是单核,就无法进行多进程并行。在使用多进程爬虫之前,我们需要先了解计算机CPU的核心数量
from multiprocessing import cpu_count
print(cpu_count())
from multiprocessing import cpu_count
from multiprocessing import Process,Queue
import time
import requests
link_list = []
print(cpu_count())
with open('most.txt','r') as file:
file_list = file.readlines()
for eachfile in file_list:
link = eachfile.split()[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('exit:',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__':
ProcessNames = ['Process-1','Process-2','Process-3','Process-4','Process-5']
workQueue = Queue(1000)
# 填充队列
for url in link_list:
workQueue.put(url)
for i in range(0,3):
p = MyProcess(workQueue)
p.daemon = True #如果将daemon设置为True,当父进程结束后,子进程就会自动被终止。
p.start()
p.join()
end = time.time()
print('Process + Queue多进程爬虫的总时间为:',end-start)
print('Main process Ended!')
from multiprocessing import Pool,Manager
import time
import requests
link_list = []
with open('most.txt','r') as file:
file_list = file.readlines()
for each in file_list:
link = each.split()[1].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__':
manager = Manager()
workQueue = manager.Queue(1000)
# 填充队列
for url in link_list:
workQueue.put(url)
pool = Pool(processes=3) #使用Pool(processes=3)创建线程池的最大值为3
for i in range(4):
pool.apply_async(crawler,args=(workQueue,i)) # 创建子进程 这里采用的是非阻塞方法
print('Start process')
pool.close()
pool.join()
end = time.time()
print('Pool +Queue多进程爬虫的总时间为:',end-start)
print('Main process End')
使用协程的好处:
协程的缺点:
在python的协程中可以使用gevent库
pip install gevent
import gevent
from gevent.queue import Queue,Empty
import time
#把下面有可能有IO操作的单独坐上标记
"""
以下两行,可以实现爬虫的并发能力,如果没有这两句的话,整个抓取过程就会变成
依次抓取gevent库中的monkey能把可能有IO操作的单独做上标记,将IO变成可以异步执行的函数
"""
from gevent import monkey
monkey.patch_all()#将IO转为异步执行的函数
import requests
link_list = []
with open('most.txt','r') as file:
file_list = file.readlines()
for each in file_list:
link_list.append(each.split()[1].replace('\n',''))
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() # 将队列中加入的内容整合到gevent中
'''
下面4行是创建多协程爬虫的程序
'''
jobs = []
for i in range(10): #创建10个协程
jobs.append(gevent.spawn(crawler,i))
gevent.joinall(jobs)
end = time.time()
print('多协程爬虫的总时间为:',end-start)
print('Main Ended')
并发(concurrency)和并行(parallelism):并发是指在一个时间段发生若干事件的情况。并行是指在同一时刻发生若干事件的情况
同步是指并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在执行完一个任务并得到结果后,另一个任务才会开始运行。
异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个影响。
程序的执行是在不同线程之间切换的。当一个线程等待网页下载时,进程可以切换到其他线程执行。
程序的执行是并行、异步的,多个线程可以在同一时刻发生若干事件。
协程是一种用户态的轻量级线程,在程序级别来模拟系统级别用的进程。在一个进程中,一个线程通过程序的模拟方法实现高并发。