【Python爬虫】提升爬虫的速度

提升爬虫的速度


提升爬虫的速度,主要有三种方法:多线程爬虫多进程爬虫多协程爬虫

1.并发和并行,同步和异步

并发(concurrency):是指在一个时间段内发生若干事件的情况;

并行(parallelism):是指在同一个时刻发生若干事件的情况。

同步:就是并发或并行的各个任务不是独立运行的,任务之间有一定的交替顺序;

异步:则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响。

2.多线程爬虫

多线程爬虫是以并发的方式执行的,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。

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类来处理线程。包括以下方法:

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

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方法能够增加抓取的效率。

3.多进程爬虫

多线程爬虫并不能充分发挥多核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即可。阻塞方法一定要等到某个进程执行完才会添加另一个进程。 

4.多协程爬虫

协程(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网络爬虫从入门到实践》

你可能感兴趣的:(Data,Mining&Analysis)