多线程爬虫

Python通过两个标准库threadthreading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。threading是对thread进行封装的高级模块,一般情况下只需要学习这个就可以了。

threading 模块提供的其他方法:

threading.currentThread()     返回当前的线程变量。
threading.enumerate()    返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount()   返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

run()      用以表示线程活动的方法。
start()    启动线程活动。
join([time])  等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive()     返回线程是否活动的。
getName()     返回线程名。
setName()     设置线程名。

线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下:

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。

考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。

那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。

经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。

线程优先级队列( Queue)

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。

Queue模块中的常用方法:

Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作

多线程爬虫使用方法如下:
首先导入多线程以及队列模块

import threading
import queue as Queue

示例代码如下:

import pymongo
import requests
import threading
import queue as Queue
import time


client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
library = mydb['library']#连接数据库及集合

headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
    }

class myThread(threading.Thread):   #继承父类threading.Thread
    #初始化参数
    def __init__(self, name, q):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q

    def run(self):  #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数 
        print('开始' + self.name)
        while True:
            try:
                crawler(self.name, self.q)  #调用crawler()爬虫函数
            except:
                break
        print('退出' + self.name)

def crawler(threadName, q):
    #自定义爬虫函数,传入线程名和队列
    url = q.get(timeout=2)  #获取队列中的url,timeout是等待时间
    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)
        

if __name__ == '__main__':        
    urls = [items['real_url'] for items in library.find()]  #待爬取url列表
    start = time.time() #计时开始
    threadList = ['Thread-1','Thread-2','Thread-3','Thread-4','Thread-5']   #线程名列表
    workQueue = Queue.Queue(len(urls))  #指明队列存放的上限,一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制。
    threads = []    #定义空进程列表

    #遍历线程名列表,创建新线程
    for tName in threadList:
        thread = myThread(tName, workQueue) #实例化新线程,传入线程名和队列上限
        thread.start()      #开启线程
        threads.append(thread)  #添加线程到线程列表

    #将url填充到workQueue队列
    for url in list(urls):
        workQueue.put(url)

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

    end = time.time()
    print('Queue多线程爬虫的总时间为:', end-start)
    print('退出主线程')


©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 线程 操作系统线程理论 线程概念的引入背景 进程 之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有...
    go以恒阅读 1,670评论 0 6
  • 一文读懂Python多线程 1、线程和进程 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运...
    星丶雲阅读 1,480评论 0 4
  • 多线程爬虫 有些时候,比如下载图片,因为下载图片是一个耗时的操作。如果采用之前那种同步的方式下载。那效率肯会特别慢...
    久壑阅读 346评论 0 2
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,803评论 0 10
  • 写在前面的话 代码中的# > 表示的是输出结果 输入 使用input()函数 用法 注意input函数输出的均是字...
    FlyingLittlePG阅读 2,834评论 0 8