Python零基础之多线程爬取王者荣耀官方网站高清壁纸(threading、queue)

Python零基础之多线程爬取王者荣耀官方网站高清壁纸

1. 目标

  • 通过多线程和队列的方式快速抓取王者荣耀高清壁纸
  • 程序架构以生产者-消费者模式进行设计,数据缓存在两个队列中
  • 将壁纸文件按照英雄名称为目录的方式保存
  • 实现对下载失败的文件重新下载

2. 代码示例

# !/usr/bin/python
# Filename: 多线程方式实现王者荣耀壁纸图片抓取.py
# Data    : 2020/08/21
# Author  : --king--
# ctrl+alt+L自动加空格格式化


import requests
from urllib import parse
import os
from urllib import request
import threading
import queue

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'}


# 1.定义生产者
class Producer(threading.Thread):

    def __init__(self, page_queue, image_queue, *args, **kwargs):

        # 由于需要动态的获取父类的属性,所以用到super()
        # Producer类的父类是threading.Thread,调用父类的__init__函数初始化了传入值
        # 由于不知道要传入什么,用*args和**kwargs代替
        super(Producer, self).__init__(*args, **kwargs)
        self.page_queue = page_queue
        self.image_queue = image_queue

    def run(self) -> None:

        # 如果队列不为空
        while not self.page_queue.empty():

            # 从队列中获取页面url信息
            page_url = self.page_queue.get_nowait()
            resp = requests.get(page_url, headers=headers)

            # 获取的是一个json类型的字符串
            # print(resp.text)
            # 可以通过.json()直接处理,替代json.loads()
            # print(type(resp.json()),resp.json())
            # 返回的是一个字典,可以通过K-V取值
            result = resp.json()

            datas = result['List']

            for data in datas:
                # 通过图片地址处理函数处理列表数据
                image_urls = extract_images(data)

                # 获取图片的名称,由于要按照文件名创建目录,所以要对空格进行处理,测试中发现以下报错
                # FileNotFoundError: [WinError 3] 系统找不到指定的路径。: '1:1等身雕塑·铠'
                name = parse.unquote(data['sProdName']).replace('1:1', '').strip()
                # print('-' * 70)
                # print(name)
                # print('-' * 70)
                # print(image_urls)
                # print('-' * 70)

                # 创建一个文件夹./image/name/
                # 创建路径
                dir_path = os.path.join('image', name)
        
                # 当文件夹不存在时创建,当存在时跳过
                if not os.path.exists(dir_path):
                    os.mkdir(dir_path)

                # 遍历列表把图片url放入队列当中
                for index, image_url in enumerate(image_urls):
                    # 由于要保存入图片队列的不止图片地址,还有图片的完整保存路径(含文件名和后缀)
                    self.image_queue.put(
                        {'image_url': image_url, 'image_path': os.path.join(dir_path, '%d.jpg' % (index + 1))})


# 2.定义消费者
class Consumer(threading.Thread):

    # 不需要导入page_queue了
    def __init__(self, image_queue, *args, **kwargs):
        # 由于需要动态的获取父类的属性,所以用到super()
        # Producer类的父类是threading.Thread,调用父类的__init__函数初始化了传入值
        # 由于不知道要传入什么,用*args和**kwargs代替
        super(Consumer, self).__init__(*args, **kwargs)
        self.image_queue = image_queue

    def run(self) -> None:

        # 如果图片队列不为空,则继续
        # while not self.image_queue.empty(): # 测试中发现存在问题,如果生产者还没有生产出来,消费者就要去处理,会阻塞程序.
        while True:
            # 获取图片的url和下载路径
            try:
                # 获取图片队列对象,等待5秒,如果报错则退出循环
                image_obj = self.image_queue.get(timeout=5)
                image_url = image_obj.get('image_url')
                image_path = image_obj.get('image_path')

                # 如果文件不存在,则下载
                if not os.path.exists(image_path):
                    # 异常处理,如果下载中报错,则打印失败信息
                    try:
                        # 下载图片
                        request.urlretrieve(image_url, image_path)

                        print(image_path, '下载完成!')
                    except:

                        print(image_path + '下载失败!')
            except:
                break


# 3.定义一个函数获取每张图片的url
def extract_images(data):

    image_urls = []
    for x in range(1,9):
        image_url = parse.unquote(data['sProdImgNo_%d' % x]).replace('200', '0')
        image_urls.append(image_url)

    return image_urls


# 4.定义主函数
def main():
    # 创建保存文件的目录,如果image目录不存在,则创建
    if not os.path.exists('./image'):
        os.mkdir('./image')
    # 创建页数的队列,需要在生产者中初始化
    page_queue = queue.Queue(23)
    # 创建图片的队列,指定了队列的长度限制
    image_queue = queue.Queue(1000)

    # 遍历0-22页,共23页
    for x in range(23):

        page_url = 'https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page={page}&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1597325146548'.format(page=x)
        # print(page_url)

        # 把页面url添加到队列中
        page_queue.put(page_url)

    # 定义3个生产者线程,让生产者处理队列中的图片url
    for x in range(3):

        p = Producer(page_queue, image_queue)
        p.start()

    # 定义5个消费者线程,把处理好的图片url进行下载保存
    for x in range(5):

        c = Consumer(image_queue)
        c.start()


if __name__ == '__main__':
    main()

3. 注意

  1. super()的用法,继承父类的方法,减少代码量,实现动态继承,并且父类的方法可以不用传入self
  2. 当传参不明确时,用*args和**kwargs传参
  3. 对文件名中含有特殊参数的处理,用replace()替换和strip()去空格
  4. 异常处理:
    • 用os.path.exists()进行重复文件夹的处理,重复文件的处理
    • 用try→except进行报错的处理和跳出循环
  5. 可以用queue.Queue(数字)定义队列的容量
  6. .put()向队列放置对象时,可以设置timeout时间,避免阻塞
  7. 用for x in range(数字):可以实现启动多个生产者或消费者
    • 生产者或消费者的数量应当根据实际工作的工作量进行分配,并不是越多越好
    • 超过一定数量后,数量越多,争夺线程发生冲突的可能性越大,运行效率越低

4. 引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kingx3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值