爬虫入门之多线程与线程池的使用

什么是线程

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
1.线程是cpu执行的基本单元
2.线程之间的执行是无序的
3.同一进程下的线程的资源是共享的 (线程锁,互斥锁)
4.线程可以实现多任务,多用来处理I/O密集型任务

使用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__":
    saySorry() 
    saydo()

多线程执行

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:执行函数中需要传递的参数,元组类型

  • kwargs:传参数(字典)
    另外:注意daemon参数

  • 如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出;

  • 如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。

  • 属性daemon的值默认为False,如果需要修改,必须在调用start()方法启动线程之前进行设置

说明

  1. 可以明显看出使用了多线程并发的操作,花费时间要短很多
  2. 当调用start()时,才会真正的创建线程,并且开始执行
方法名 作用
start()方法 开启线程
join()方法 线程阻塞
daemon = False 后台线程,主线程结束不影响子线程运行
daemon = True 前台线程,主线程结束子线程随之结束

互斥锁

  • GIL:由于python的CPython解释器的原因,存在一个GIL全局解释器锁用来保证同一时刻只有一个线程在执行,类似于单核处理,所有说多线程并不能充分的利用cpu资源
  • 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
  • 互斥锁为资源引入一个状态:锁定/非锁定
  • 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
  • hreading模块中定义了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)状态。
总结

锁的好处

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

线程池

导入模块包

from concurrent.futures import ThreadPoolExecutor

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

#创建一个线程池 
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)

案例

from concurrent.futures import ThreadPoolExecutor
import requests
from lxml.html import etree
import requests

class CollegateRank(object):

    def get_page_data(self,url):
        response = self.send_request(url=url)
        if response:
            # print(response)
            with open('page.html','w',encoding='gbk') as file:
                file.write(response)
            self.parse_page_data(response)


    def parse_page_data(self,response):
        #使用xpath解析数据
        etree_xpath = etree.HTML(response)
        ranks = etree_xpath.xpath('//div[@class="scores_List"]/dl')
        # print(ranks)
        pool = ThreadPoolExecutor(10)
        for dl in ranks:
            school_info = {}
            school_info['url'] = self.extract_first(dl.xpath('./dt/a[1]/@href'))
            school_info['icon'] = self.extract_first(dl.xpath('./dt/a[1]/img/@src'))
            school_info['name'] = self.extract_first(dl.xpath('./dt/strong/a/text()'))
            school_info['adress'] = self.extract_first(dl.xpath('./dd/ul/li[1]/text()'))
            school_info['tese'] = '、'.join(dl.xpath('./dd/ul/li[2]/span/text()'))
            school_info['type'] = self.extract_first(dl.xpath('./dd/ul/li[3]/text()'))
            school_info['belong'] = self.extract_first(dl.xpath('./dd/ul/li[4]/text()'))
            school_info['level'] = self.extract_first(dl.xpath('./dd/ul/li[5]/text()'))
            school_info['weburl'] = self.extract_first(dl.xpath('./dd/ul/li[6]/text()'))

            print(school_info['url'],school_info)
            result = pool.submit(self.send_request,school_info['url'])
            result.add_done_callback(self.parse_school_detail)
        # pool.shutdown()

    # 线程执行完毕的回调方法
    def parse_school_detail(self,future):
        text = future.result()
        print('解析数据',len(text))

    def extract_first(self,data=None,defalut=None):
        if len(data)  > 0:
            return data[0]
        return defalut


    def send_request(self, url, headers=None):
        headers = headers if headers else {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'}
        response = requests.get(url=url,headers=headers)
        if response.status_code == 200:
            return response.text

if __name__ == '__main__':
    url = 'http://college.gaokao.com/schlist/'
    obj = CollegateRank()
    obj.get_page_data(url)


你可能感兴趣的:(爬虫入门之多线程与线程池的使用)