vlambda博客
学习文章列表

多线程(三):互斥锁及多线程爬虫

多线程理解

THIS IS TITLE

      

       在介绍互斥锁之前先来想想我们之前所说的多线程,我们知道多线程是处在一个进程,就类似我们普通的一个程序就叫做一个进程。所以我们知道一个程序里面的全局变量是共享的。那么如果我们多个线程都对一个变量进行操作会出现什么问题呢?因为我们知道每个线程运行并不是一个接着一个运行的。接下来就给大家演示下这种操作。

import threadingimport timedef add(): global n n += 1if __name__ == "__main__": # 声明n n = 0 # 开启一百个线程进行操作 for i in range(100): d = threading.Thread(target=add) d.start() print(n)#################### 100

     

     

      这样我们发现结果是正确的,这是因为我们每个线程只有一次加的操作,所以等到下一个线程开始的时候我们就已经运行结束了上一个线程。这时候我们只要加一步延时一个线程的运行时间。



import threadingimport timedef add(): global n time.sleep(0.01) n += 1if __name__ == "__main__": # 声明n n = 0 # 开启一百个线程进行操作 for i in range(100): d = threading.Thread(target=add) d.start() print(n)################## 三次运行结果分别为:46, 49, 51


       

       这里应该不难理解,由于线程之间是进行随机调度,所以操作可能未运行结束就到了下一个操作。这时就可能导致结果出现错误。这里其实以前我想过很多,我的想法是产生这样的结果说明有些线程没有运行结束但是其实并不是这样的。




from dis import disdef add(): x = 1 + 1dis(add)###################  下面为打印结果:43 0 LOAD_CONST 1 (2) 2 STORE_FAST 0 (x) 4 LOAD_CONST 0 (None)              6 RETURN_VALUE

      

     






        这个方法会返回add函数的字节码,我的理解为这个add操作可能运行了部分到达其他线程,那么当其他线程运行时n可能到了80但是回到这个线程时,它因为运行了部分,所以这个线程运行结果还是50。这只是我的理解如果不正确请大家多多指正。 




      下面是关于守护线程和阻塞线程在这种大量线程中的设置方法,大家可以拷贝代码进行测试。(阻塞代码虽然结果是正确的但是中间可以看到很多异常值。)





import threadingimport timedef add():    global n tem = 1 time.sleep(1) print(f'{n}的值') n += tem    time.sleep(1)if __name__ == "__main__": # 声明n n = 0 # 开启一百个线程进行操作 ls = [] for i in range(100): d = threading.Thread(target=add) # d.setDaemon(True) d.start() ls.append(d) # 阻塞每个进程 for i in ls: i.join() print(n)

       

       下面介绍线程锁,即在此次运行时会保证该线程运行不会被其他线程影响,即该线程运行结束再跳转下一个线程。


import threadingimport timedef add(): global n # 开启互斥锁 t.acquire() tem = 1 time.sleep(1) print(f'{n}的值') n += tem time.sleep(1) # 释放互斥锁 t.release()if __name__ == "__main__": # 实例化锁 t = threading.Lock() # 声明n n = 0 # 开启一百个线程进行操作 ls = [] for i in range(100): d = threading.Thread(target=add) # d.setDaemon(True) d.start() ls.append(d)      

       这里运行可以看到每个线程是一个一个运行的,运行结束下一个线程才会开始,与阻塞线程对比可以看出差距。如果你想更直观可以不释放锁看一下结果。线程会卡住,这就是互斥锁的用途。我的例子可能不太好但是功能可以看得出来。互斥锁可能对于我们平常用的cpython来说并没有太多的用。因为GIL的存在,所以知道功能和怎么用就够了。我们一般所用的多线程基本不会处理这种计算操作,我们会用来处理I/O操作,比如爬虫。



     


多线程(三):互斥锁及多线程爬虫


下面就给大家写一个多线程爬虫和不用多线程的速度进行比较体现多线程的强大。




#!/usr/bin/env python# -*- coding: utf-8 -*-''' 正则提取 1、电影名称 2、电影评分 3、评分人数 4、电影图片 5、剧情简介'''
import requestsfrom parsel import Selectorimport csvimport reimport threading
class Douban():
def __init__(self): self.headers = { # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', # 'Accept-Encoding': 'gzip, deflate, br', # 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', # 'Cache-Control': 'no-cache', # 'Connection': 'keep-alive', # 'Cookie': 'll="108296"; bid=u7uQV8qAq48; __yadk_uid=R3ZNfQ9BBDWNCnmDIrF4sSq2z1hVb91a; __gads=ID=736cd1a27746d07f:T=1568767437:S=ALNI_MbO0DsElacGED0en44UsxVC59S2ZA; douban-fav-remind=1; viewed="24250054"; gr_user_id=3dc30cfc-7a3e-4c36-a228-045db66b36de; _vwo_uuid_v2=D5AD102F3212719FA6A5C5C43416A7321|d9fed3644bde8c520e9267a1bfb0b37d; push_noty_num=0; push_doumail_num=0; __utmv=30149280.19369; douban-profile-remind=1; __utmz=30149280.1573099331.10.9.utmcsr=bing|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); __utmz=223695111.1573099331.4.4.utmcsr=bing|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1573751493%2C%22https%3A%2F%2Fwww.bing.com%2F%22%5D; _pk_ses.100001.4cf6=*; __utma=30149280.244436708.1568767400.1573099331.1573751493.11; __utmb=30149280.0.10.1573751493; __utmc=30149280; __utma=223695111.2128838216.1568767400.1573099331.1573751493.5; __utmb=223695111.0.10.1573751493; __utmc=223695111; ap_v=0,6.0; _pk_id.100001.4cf6=a10a63a3970adb76.1568767400.5.1573751594.1573099361.', # 'Host': 'movie.douban.com', # 'Pragma': 'no-cache', # 'Sec-Fetch-Mode': 'navigate', # 'Sec-Fetch-Site': 'none', # 'Sec-Fetch-User': '?1', # 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3941.4 Safari/537.36', } with open('豆瓣爬取.csv', 'w', encoding='gbk') as f: f.write('电影名称, 电影评分, 评分人数/人, 电影图片, 剧情简介' + '\n')

# 获取详细页链接 def get_url(self, page): '''
:param page: :return : '''
# 上锁 # lock.acquire() # print('上锁完成') url = f'https://movie.douban.com/top250?start={25 * (page - 1)}&filter=' res = requests.get(url, headers=self.headers).text # res.encoding = # print(res) html = Selector(res) detail_urls = html.xpath('//div[@class="hd"]/a/@href').extract() for detail_url in detail_urls: self.get_data(detail_url)

# 解析数据 def get_data(self, url): '''
:param url: :return data: '''
res = requests.get(url, headers=self.headers, timeout=1).text html = Selector(res) # 1、电影名称 movie_name = html.xpath('//div[@id="content"]/h1/span/text()').extract()[0] # 2、电影评分 movie_score = html.xpath('//strong[@class="ll rating_num"]/text()').extract()[0] # 3、评分人数 score_person = html.xpath('//div[@class="rating_sum"]/a/span/text()').extract()[0] # 4、电影图片 movie_pic_link = html.xpath('//a[@class="nbgnbg"]/img/@src').extract()[0] # 5、剧情简介p movie_introductions = html.xpath('//div[@class="indent"]/span/text()').extract()
# 格式化剧情 s = '' for movie_introduction in movie_introductions: movie_introduction = re.sub(r'\\n ', '', movie_introduction) s += movie_introduction
movie_introduction = s.strip().replace('\n', '').replace(' ', '')
data = [movie_name, movie_score, score_person, movie_pic_link, movie_introduction]
# 保存数据 self.reserve(data) print(data[0] + '保存完成')
# 解锁 # lock.release()
# 保存文件 def reserve(self, data): '''
:param data: :return none: ''' with open('豆瓣爬取.csv', 'a', newline='', encoding='gb18030') as fi: csv.writer(fi).writerow(data)

if __name__ == "__main__": # 实例化锁 # lock = threading.Lock() douban = Douban()
# 每一页为一个线程 ts = [] # semaphore = threading.BoundedSemaphore(1) # 最多允许1个线程同时运行 for page in range(1, 11): t = threading.Thread(target=douban.get_url, args=(page,)) print(f'线程{page}开启') t.start() ts.append(t)
# 为每个线程设置阻塞线程 for t in ts: t.join()

       

           

      


       这里因为我设置的线程有点贪所以直接封了我的ip,和大家解释下被封原因,因为我的每个线程是一页,一页包含20个请求,所以多个线程一起启动同时会有很多请求所以直接封ip。

     

       给大家一个建议吧,可以把线程放在一页的每个请求上,这样就会大大减少每个线程的请求数量。我之前是启动了ip池进行爬取的,确实以前不懂,这样会给服务器带来负担。我希望大家多多了解爬虫有关的法律知识,还有尽量规范爬虫不要为难前端人员。 



      关于代码部分里面注释的很多部分有些拓展内容,大家看我也不容易这么晚还在给大家更新毕竟昨天说好了今天发布的。所以希望大家多多支持可以推荐给你身边正在学习或者对python感兴趣的朋友。谢谢大家啦,有什么不足之处希望大家多多批评。



    “ 关于多线程还有些事件等的相关知识,不过用的不多,线程池我也用得很少因为基本之后会用scrapy框架,很少自己去写这些。不过学习这些知识是有必要的,不然遇到很多错误你便不能正确的解决了。希望大家一起加油