要学习多线程爬虫,首先我们应该理解为什么多线程爬虫可以增加爬取的速度。要理解为什么多线程能够增加爬取的速度,要先理解并发和并行的概念。
如果某个系统支持两个或者多个动作(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))
我们使用threading
模块来学习Python多线程。threading
模块提供了Tread
类来处理线程,包括一下方法。
下面介绍使用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
来解决这个问题。
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爬虫小白教程(五)—— 多线程爬虫