python爬虫之多线程、多进程爬虫 总结

python爬虫之多线程、多进程爬虫

多任务的介绍

什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边看电影,一边聊QQ,一边在用Word赶作业,这就是多任务,这时至少同时有3个任务正在运行。
单核CPU如何执行多任务?
多核CPU如何执行多任务?
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
注意:
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行: 指的是任务数小于等于cpu核数,即任务真的是一起执行的

原因

1.Python的多线程并不如java的多线程,其差异在于当python解释器开始执行任务时,受制于GIL(全局解释所),Python 的线程被限制到同一时刻只允许一个程执行这样一个执行模型。
2.Python 的线程更适用于处理 I/O 和其他需要并发行的阻塞操作(比如等待 I/O、等待从数据库获取数据等等),而不是需要多处理器行的计算密集型任务。幸运的是,爬虫大部分时间在网络交互上,所以可以使用多线程来编写爬虫。
3.这点其实和多线程关系不大,scrapy的并发并不是采用多线程来实现,它是一个twisted应用,通过异步非阻塞来达到并发,这个后面我会写文章来讲解。
4.Python中当你想要提高执行效率,大部分开发者是通过编写多进程来提高运行效率,使用multiprocessing进行并行编程,当然,你可以编写多进程爬虫来爬取信息,缺点是每个进程都会有自己的内存,数据多的话,内存会吃不消。
5.使用线程有什么缺点呢,缺点就是你在编写多线程代码时候,要注意死锁的问题、阻塞的问题、以及需要注意多线程之间通信的问题(避免多个线程执行同一个任务)。

线程

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
1. 使用threading模块

单线程执行

#coding=utf-8 
import time 
def saySorry():   
	for i in range(5): 
	print("亲爱的,我错了,我能吃饭了吗?") 
	time.sleep(1) 
def do(): 
	for i in range(5): 
	print("亲爱的,我错了,我给你按摩") 
	time.sleep(1) 
if __name__ == "__main__": 
	saySorry() 
	saydo()

多线程执行

#coding=utf-8 
import threading import time 
def saySorry():   
	for i in range(5): 
	print("亲爱的,我错了,我能吃饭了吗?") 
	time.sleep(1) 
def do(): 
	for i in range(5): 
	print("亲爱的,我错了,我给你按摩") 
	time.sleep(1) 
if __name__ == "__main__": 
	td1 = threading.Thread(target=saySorry) 
	td1.start() 
#启动线程,即让线程开始执行 
	td2 = threading.Thread(target=saySorry) 
	td2.start() 
	#启动线程,即让线程开始执行

threading.Thread参数介绍

  • target:线程执行的函数
  • name:线程名称
  • args:执行函数中需要传递的参数,元组类型
    另外:注意daemon参数
  • 如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出;
  • 如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。
  • 属性daemon的值默认为False,如果需要修改,必须在调用start()方法启动线程之前进行设置

说明
1.可以明显看出使用了多线程并发的操作,花费时间要短很多
2.当调用start()时,才会真正的创建线程,并且开始执行

主线程与子线程的执行顺序

#coding=utf-8 
import threading from time 
import sleep,ctime 
def sing(): 
	for i in range(3): 
	print("正在唱歌...%d"%i) 
	sleep(1) 
def dance(): 
	for i in range(3): 
	print("正在跳舞...%d"%i) 
	sleep(1) 
if __name__ == '__main__': 
	print('---开始---:%s'%ctime()) 
	t1 = threading.Thread(target=sing) 
	t2 = threading.Thread(target=dance) 
	t1.start() 
	t2.start() 
	t1.join() 
	t2.join() 
#sleep(5) 
# 屏蔽此行代码,试试看,程序是否会立马结束? 
print('---结束---:%s'%ctime())

线程-注意点

1. 线程执行代码的封装

通过上一小节,能够看出,通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法

示例如下:

import threading 
import time 
class MyThread(threading.Thread): 
	def run(self): 
		for i in range(3): 
		time.sleep(1) 
		msg = "I'm "+self.name+' @ '+str(i) 
		#name属性中保存的是当前线程的名字 
		print(msg) 
if __name__ == '__main__': 
	t = MyThread() 
	t.start()

说明

  • python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。

线程的执行顺序

#coding=utf-8 
import threading 
import time 
class MyThread(threading.Thread): 
    def run(self):
		 for i in range(3): 
 		time.sleep(1) 
		 msg = "I'm "+self.name+' @ '+str(i) print(msg)
 	def test(): 
  		for i in range(5): 
  		t = MyThread()
   	t.start()
if __name__ == '__main__':
     test()

说明

  • 从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定
    总结
  • 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字
  • 当线程的run()方法结束时该线程完成。
  • 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式

多线程-共享全局变量

from threading import Thread 
import time g_num = 100 
def work1(): 
	global g_num 
	for i in range(3): 
		g_num += 1 
		print("----in work1, g_num is %d---"%g_num) 
def work2(): 
	global g_num 
	print("----in work2, g_num is %d---"%g_num) 
	print("---线程创建之前g_num is %d---"%g_num) 
t1 = Thread(target=work1) 
t1.start() #延时一会,保证t1线程中的事情做完 
time.sleep(1) 
t2 = Thread(target=work2) 
t2.start()

运行结果

---线程创建之前g_num is 100--- 
----in work1, g_num is 103---
----in work2, g_num is 103---

元组当做实参传递到线程中

from threading import Thread 
import time 
def work1(nums): 
	nums.append(44) 
	print("----in work1---",nums) 
def work2(nums): #延时一会,保证t1线程中的事情做完 			
	time.sleep(1)
	 print("----in work2---",nums) 
	 g_nums = [11,22,33] 
	 t1 = Thread(target=work1, args=(g_nums,)) 
	 t1.start() 
	 t2 = Thread(target=work2, args=(g_nums,)) 
	 t2.start()

总结

  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

互斥锁(重点)当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

使用互斥锁完成2个线程对同一个全局变量各加100万次的操作

import threading
import time

g_num = 0

def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---test1---g_num=%d"%g_num)

def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---test2---g_num=%d"%g_num)

# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()

# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()

p1.join()
p2.join()

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:
2个线程对同一个全局变量操作之后的最终结果是:2000000

可以看到最后的结果,加入互斥锁后,其结果与预期相符。
上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

死锁问题在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子

#coding=utf-8
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

多线程爬虫案例(jobbole)

Queue(队列对象)

Queue是python中的标准库,可以直接import Queue引用;
队列是线程间最常用的交换数据的形式

python下多线程的思考

对于资源,加锁是个重要的环节。因为python原生的list,dict等,都是not thread safe的。而Queue,是线程安全的,因此在满足使用条件下,建议使用队列

  • 初始化: class (FIFO 先进先出)

    queue = queue.Queue(maxsize)

  • 包中的常用方法:

    queue.qsize() 返回队列的大小

    queue.empty() 如果队列为空,返回True,反之False

    queue.full() 如果队列满了,返回True,反之False

    queue.full 与 maxsize 大小对应

    queue.get(block,timeout)获取队列,timeout等待时间

创建一个“队列”对象

import queue
myqueue = queue.Queue( maxsize = 10 )

将一个值放入队列中

myqueue.put(10)

.将一个值从队列中取出

myqueue.get()

案例

class crawlThread(threading.Thread):
    """自定义爬取线程"""
    def __init__(self,threadName,page_queue,data_queue):
        super(crawlThread,self).__init__()
        self.threadName = threadName
        self.page_queue = page_queue
        self.data_queue = data_queue
        self.headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0',}

    def run(self):
        # 这里从page_queue获取对应的页码
        while not self.page_queue.empty():
            #get()从队列中取值,先进先出
            page = self.page_queue.get()
            print(page)
            full_url = 'http://blog.jobbole.com/all-posts/page/'+str(page)+'/'
            response = requests.get(full_url,headers=self.headers)
            response.encoding = 'utf-8'
            if response.status_code == 200:
                #将获取到的结果,存放在data_queue队列中
                self.data_queue.put(response.text)
        
# #线程的采集任务
# def crawl_data(page_queue,data_queue):
#     header = {
#         'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0',
#     }
#     # 这里从page_queue获取对应的页码
#     while not page_queue.empty():
#         #get()从队列中取值,先进先出
#         page = page_queue.get()
#         print(page)
#         full_url = 'http://blog.jobbole.com/all-posts/page/'+str(page)+'/'
#         response = requests.get(full_url,headers=header)
#         response.encoding = 'utf-8'
#         if response.status_code == 200:
#             #将获取到的结果,存放在data_queue队列中
#             data_queue.put(response.text)



class parseThread(threading.Thread):
    """自定义解析线程"""
    def __init__(self,threadName,data_queue,lock):
        super(parseThread,self).__init__()
        self.threadName = threadName
        self.data_queue = data_queue
        self.lock = lock

    def run(self):
        #不为空的时候去取值,为空说明没有解析任务了
        while not self.data_queue.empty():
            html = etree.HTML(self.data_queue.get())
            articles = html.xpath('//div[@class="post floated-thumb"]')
            for item in articles:
                title = item.xpath('.//a[@class="archive-title"]/text()')[0]
                print(title)
                #加锁
                self.lock.acquire() 
                with open('jobbole.txt','a') as f:
                    f.write(title+'\n')
                #解锁
                self.lock.release() 

# def parse_data(data_queue):
#     #不为空的时候去取值,为空说明没有解析任务了
#     while not data_queue.empty():
#         html = etree.HTML(data_queue.get())
#         articles = html.xpath('//div[@class="post floated-thumb"]')
#         for item in articles:
#             title = item.xpath('.//a[@class="archive-title"]/text()')[0]
#             print(title)

def spider():
    #创建一个任务队列:里面的参数maxsize表示最大的存储量
    page_queue = queue.Queue(40)
    #http://blog.jobbole.com/all-posts/page/2/  (2表示页码)
    for i in range(1,30):
        page_queue.put(i)

    #将解析后的数据放在这个队列中,供后后面的解析线程去做解析
    data_queue = queue.Queue()

    #创建线程取下载任务
    lock = threading.Lock()
    crawlThreadName = ['crawl1号','crawl2号','crawl3号','crawl4号']
    thread_list = []
    for threadName in crawlThreadName:
        # thread = threading.Thread(target=crawl_data,name=threadName,args=(page_queue,data_queue))
        thread = crawlThread(threadName,page_queue,data_queue)
        thread.start()
        thread_list.append(thread)
        # thread.join() 不能直接写在这里

    for thread in thread_list:
        thread.join()

    #创建解析线程:
    parseThreadName = ['parse1号','parse2号','parse3号','parse4号']
    parseThread_list = []
    for threadName in parseThreadName:
        # thread = threading.Thread(target=parse_data,name=threadName,args=(data_queue,))
        thread = parseThread(threadName,data_queue,lock)
        thread.start()
        parseThread_list.append(thread)
    
    for thread in parseThread_list:
        thread.join()
    
    #打印当前线程的名称
    print(threading.current_thread().name)
    

if __name__ == '__main__':
    spider()

线程池

使用python3.2之后为我们封装的线程池

(案例一)线程池简单使用样例
导入模块包

from concurrent.futures import ThreadPoolExecutor

线程池中的线程需要执行的任务

def get_page_data(page):
    """
    线程需要执行的任务
    :param url:
    :return:
    """
    print(page)

    return 'done下载完成'

任务执行完毕之后的回调函数,可以获取到现场执行完毕的返回值

def done(future):
    print(future.result())

创建线程池,并往线程池中添加任务

#创建一个线程池
pool = ThreadPoolExecutor(10)
#如何提交任务给线程池呢?

#网线程池中添加任务
for pagenum in range(50):
    #submit: 表示将我们需要执行的任务给这个线程池,
    handler = pool.submit(get_page_data,pagenum)
    #给线程池设置任务之后,可以设置一个回调函数,
    #作用是:当我们某个任务执行完毕之后,就会回调你设置的回调函数
    handler.add_done_callback(done)
pool.shutdown(wait=True)
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值