Python多线程
多线程类似于同时执行多个不同程序,可以让程序的运行速度加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,可以释放一些珍贵的资源如内存占用等等。对于像妹子图这样的图片网站中大量图集的爬取,采用多线程能够大大提高图片的采集速度。
Python中使用线程有两种方式:函数或者用类来包装线程对象。本文使用Threading模块创建多线程。
网站分析
- 妹子图网站主页是多个图集的首页集合展示,通过点击“下一页”进入下一批图集;
- 通过点击主业图集首页进入到图片展示网页,在图片展示网页又是通过“下一页”来翻页查看图集中的每张图片;
- 通过网站的简单分析,我们大概了解到需要网站主页获取到每个图集的链接,通过主页翻页来获取多个主页图集的链接;
- 获得单个图集的链接后,进入图片展示网页,在该网页,获取到图集中图片的总数量,然后获取每张图片在服务器中的存储位置;
- 最后将服务器中的图片按照图集创建文件夹,存储起来。
代码框架构建
对于以上对网站的分析,一条条来构建代码,实现功能:
-
以上所有需求都离不开获取网页内容,于是单独构建了个网页获取模块html_text.py。
# -*- coding: utf-8 -*- """获取网页内容""" import requests def get_html_text(url, head): try: r = requests.get(url, headers=head) r.raise_for_status() r.encoding = r.apparent_encoding return r.text except: return ""
注:在该模块中为了反爬,设置了请求头参数。headers是解决requests请求反爬的方法之一,相当于我们进去这个网页的服务器本身,假装自己本身在爬取数据。
-
使用threading模块创建线程,将其作为一个单独模块img_load.py来爬取单个图集内所有图片:
# -*- coding: UTF-8 -*- """使用Threading模块创建妹子图集下载线程""" import requests import os import threading from lxml import etree from html_text import get_html_text #写一个模块专门用来读取网页内容 class MeiZiTu(threading.Thread): #继承父类threading.Thread """重写__init__方法,并增加线程锁(thread_lock)等属性""" def __init__(self, thread_lock, main_referer, link_url, link_title): threading.Thread.__init__(self) self.thread_lock = thread_lock #传递线程锁 self.main_referer = main_referer #传递图集主页网址, self.link_url = link_url #传递图集首页网址 self.link_title = link_title #传递图集名称 """ 重写run函数,把要执行的代码写到run函数里, 线程在创建后会直接运行run函数 """ def run(self): link_load_url_num = self.get_link_load_url_num() for i in range(link_load_url_num): load_url = self.link_url + '/' + str(i+1) if i == 0: link_referer = self.main_referer self.save_photo(load_url, link_referer) else: link_referer = self.link_url + '/' + str(i) self.save_photo(load_url, link_referer) """别忘了在线程执行结束后,将锁释放,以便开启下一个线程""" self.thread_lock.release() """构造请求头""" def mk_heads(self, head_referer): link_head = { "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/\ 537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "referer": head_referer, #referer是针对网站图片反爬而设置 "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/\ webp,image/apng,*/*;q=0.8" } return link_head """获得图集中图片个数""" def get_link_load_url_num(self): link_head = self.mk_heads(self.main_referer) link_r = get_html_text(self.link_url, link_head) link_html = etree.HTML(link_r, etree.HTMLParser()) link_load_url_num = link_html.xpath('//div[@class="pagenavi"]/a[5]/\ span/text()')[0] return int(link_load_url_num) """存储图集中单张图片""" def save_photo(self, load_url, link_referer): """获得图集中单个网页内容""" load_html_head = self.mk_heads(link_referer) #构造请求头 load_text = get_html_text(load_url, load_html_head) load_html = etree.HTML(load_text, etree.HTMLParser()) load_html_src = load_html.xpath('//div[@class="main-image"]/p/a/img/\ @src')[0] #单张图片文件服务器中位置 load_html_img_path = '.vscode\\meizitu\\%s' % self.link_title #文件夹路径 """单张图片文件路径""" load_img_path = load_html_img_path + "/" + load_html_src[-6:] """获得每张图片的内容""" load_img_head = self.mk_heads(load_url) """ 由于写了个网页内容获取模块(html_text),通过模块中get_html_text函数 传递请求头,在这步读取单张图片在服务器中的内容时,习惯性忘了指定关键字实参。 请求头忘了写headers,搞了老半天!!!!!!!!!!!!!!!!!! """ load_img = requests.get(load_html_src, headers=load_img_head) if os.path.exists(load_html_img_path): with open(load_img_path, "wb") as f: f.write(load_img.content) else: os.mkdir(load_html_img_path) with open(load_img_path, "wb") as f: f.write(load_img.content)
注:1、该模块使用threading模块创建线程,需要重写__init__和run函数,__init__函数用来初始化该子类,除了继承父类之外,还可以添加自己特有的属性;run函数用来执行代码,所有操作最终要归到run函数中来运行。 2、referer是发起HTTP请求中headers的一部分,可以用来做网页的图片防盗链!比如一个网页的图片,想下载到电脑里,用requests第三方库访问图片时,出现403禁止访问,就有可能是提交request申请时,在浏览器中的空地址栏里键入了这个网页然后访问,而网站的设置是要求有referer,且referer的网站必须是跳转之前的网站,也就是这个图片的主页。所以为了反爬,在构造headers时需要加入referer。 3、该模块中get_link_load_url_num函数实现了获取图集中图片个数,save_photo函数实现了存储单张图片,在run函数中实现了对整个图集图片的存储。 4、由于会在主函数中调用该模块,创建该模块中MeiZiTu类的实例,并启动线程,且加上线程锁(acquire),故在run函数中当整个线程运行结束后,要进行解锁(release),从而开启一个新的图集下载线程。
-
最后在主函数中,for循环中通过循环嵌套来实现n个网页中n×m个图集的图片下载并存储。
# -*- coding: utf-8 -*- """通过输入下载网页页数和循环嵌套来实现图集下载并存储""" import requests from lxml import etree import threading import img_load from html_text import get_html_text if __name__ == "__main__": """输入下载多少页图集""" while True: try: load_html_num = int(input("请输入要下载的页数:")) load_html_num > 0 break except ValueError: print("输入错误,请重新输入!!!") """网站主页请求头""" main_head = { "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36\ (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" } """设置线程开启个数""" thread_lock = threading.BoundedSemaphore(5) for i in range(load_html_num): main_url = "https://www.mzitu.com/xinggan/page/%s/" % str(i+1) main_text = get_html_text(main_url, main_head) main_html = etree.HTML(main_text, etree.HTMLParser()) main_url_list = main_html.xpath('//ul[@id="pins"]/li/a/@href') print(main_url_list, end="\n******************************\n") main_title_list = main_html.xpath('//ul[@id="pins"]/li/a/img/@alt') for j in range(len(main_url_list)): thread_lock.acquire() #锁定线程 """创建线程实例""" t = img_load.MeiZiTu(thread_lock, main_url, main_url_list[j], \ main_title_list[j]) t.start() #开启线程
注:BoundedSemaphore是一个工厂函数,它返回一个新的BoundedSemaphore(有限制的信号量)对象。一个有限制的信号量负责检查它的当前值没有超过它的初始值,一旦超过了,就会报ValueError异常。大部分情况下,信号量主要用来保护有限的资源。如果一个信号量被release太多,这可能引起BUG。如果没有指定初始值,那么初始值为1。和普通的信号量一样,有限的信号量内部维护一个计数器,该计数器=initialValue+release-acquire。当 计数器的值为0时,acquire方法调用会被阻塞。计数器初始值为1。
最后来张成果展示吧:
参考文章:
- Python多线程:https://www.runoob.com/python/python-multithreading.html
-
关于网页referer以及破解referer反爬虫的办法:https://blog.csdn.net/python_neophyte/article/details/82562330
-
Python threading模块、BoundedSemaphore类讲解:https://blog.csdn.net/wo198711203217/article/details/83748575
-
详细解读Python中的__init__()方法:https://blog.csdn.net/qq_36534861/article/details/78794223