使用threading模块创建多线程爬取妹子图网站图集

Python多线程

        多线程类似于同时执行多个不同程序,可以让程序的运行速度加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,可以释放一些珍贵的资源如内存占用等等。对于像妹子图这样的图片网站中大量图集的爬取,采用多线程能够大大提高图片的采集速度。

        Python中使用线程有两种方式:函数或者用类来包装线程对象。本文使用Threading模块创建多线程。

网站分析

  1.  妹子图网站主页是多个图集的首页集合展示,通过点击“下一页”进入下一批图集;
  2. 通过点击主业图集首页进入到图片展示网页,在图片展示网页又是通过“下一页”来翻页查看图集中的每张图片;
  3. 通过网站的简单分析,我们大概了解到需要网站主页获取到每个图集的链接,通过主页翻页来获取多个主页图集的链接;
  4. 获得单个图集的链接后,进入图片展示网页,在该网页,获取到图集中图片的总数量,然后获取每张图片在服务器中的存储位置;
  5. 最后将服务器中的图片按照图集创建文件夹,存储起来。

代码框架构建

        对于以上对网站的分析,一条条来构建代码,实现功能:

  1.  以上所有需求都离不开获取网页内容,于是单独构建了个网页获取模块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请求反爬的方法之一,相当于我们进去这个网页的服务器本身,假装自己本身在爬取数据。

  2. 使用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),从而开启一个新的图集下载线程。

  3.  最后在主函数中,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。

        最后来张成果展示吧:

使用threading模块创建多线程爬取妹子图网站图集_第1张图片

参考文章:

  1. Python多线程:https://www.runoob.com/python/python-multithreading.html
  2. 关于网页referer以及破解referer反爬虫的办法:https://blog.csdn.net/python_neophyte/article/details/82562330

  3. Python threading模块、BoundedSemaphore类讲解:https://blog.csdn.net/wo198711203217/article/details/83748575

  4. 详细解读Python中的__init__()方法:https://blog.csdn.net/qq_36534861/article/details/78794223

你可能感兴趣的:(爬虫)