Python爬虫实战:多线程爬取配乐网,实现异步下载

效果

先来看看运行效果:
效果图
多线程异步下载会非常轻松的帮你完成下载任务,非一般的感觉哦!

环境和外部库

1.谷歌浏览器+selenium
2.python3+pycharm
3.requests
4.lxml
5.queue

问题与解决办法

先给出本篇要处理的网站:http://www.peiyue.com/,因为这个网站的反爬虫做到还是非常不错的,所以小编在爬取的时候遇到的问题还是蛮多的,这里我只列举几个有代表意义的。
一、我们定位到起始页里的不同配乐类的跳转盒子:
困难1
我们复制a标签里的href值,然后在浏览器打开这个链接,发现:
困难1.1
很明显,该网站做了反爬虫,所以用requests库来请求然后去获取这个href的方法是行不通了。对于这种情况,最简单的方法就是使用selenium库来处理了。
二、下载地址中含有中文,用urlretrieve无法下载
这时就需要用urllib.parse.quote()来处理中文字符获得最后的下载地址
三、直接从网站中得到的配乐的名字含有特殊字符,无法当作文件名
需要用正则表达式将这些字符去掉
四、在用selenium执行点击不同的配乐分类的盒子会在当前页面打开新页面,而当获取完具体数据在跳回到起始页面时,原来的元素失效,不再依赖当前页面,因此这里我们直接简单粗暴的循环打开起始页面,每一次处理一个配乐类

代码相关知识讲解

1.重写构造函数时,需要调用父类,super(类名, self).init(args, **kwargs),这里args, **kwargs连用代表任意参数,*args:任意个无名参数,如果我们不确定要往函数中传入多少个无名参数,或者我们想往函数中以列表和元组的形式传参数时,那就使要用;**kwargs:任意个关键词参数,如果我们不知道要往函数中传入多少个关键词参数,或者想传入字典的值作为关键词参数时,那就要使用。
2.本文使用队列安全的Queue,在进行多任务时,要么不做,要么做完。使用Queue的get()方法可以从队列中取出一个元素来处理,在使用多线程时因为被取走的元素就不在队列中了,所以不同线程不会取出重复元素。

完整代码

#  title:多线程爬取配乐网,下载免费配乐

import os
import re
import time
import threading
from urllib import request
from urllib.parse import quote
from selenium import webdriver
from queue import Queue
from lxml import etree
import requests

BASE_URL = 'http://www.peiyue.com/'  #  本文爬取的网站

#  定义一个生产者类爬取页面,获得下载地址
class Procuder(threading.Thread):
    #  重写构造函数
    def __init__(self, page_queue, url_queue, src_queue, *args, **kwargs):
        super(Procuder, self).__init__(*args, **kwargs)  #  调用父类的构造函数
        self.page_queue = page_queue
        self.url_queue = url_queue
        self.src_queue = src_queue

    #  定义一个解析page_queue中页面的函数,获得每个配乐主页的url
    def get_data(self, url):
        driver_path = "E://Mystudy/My_Project/Python_Project/TOLL/Driver/chromedriver.exe"
        driver = webdriver.Chrome(executable_path=driver_path)
        driver.get(url)
        #  确定每一页中有多少个类
        length = len(driver.find_elements_by_xpath(
            "//div[@class='body_a']//div[@class='right']/table/tbody/tr[position()>1]")[:-1])
        for i in range(length):
            #  因为在当前页面点击不同的类的盒子会在当前页面打开新页面,而当获取完数据在跳回到起始页面时,原来的元素失效,不再依赖当前页面
            #  因此这里我们直接简单粗暴的循环打开起始页面,每一次处理一个配乐类
            driver.get(url)
            diff_kind = driver.find_elements_by_xpath(
                "//div[@class='body_a']//div[@class='right']/table/tbody/tr[position()>1]//a[@class='q1']")[i]  #  定位第i+1个配乐类
            diff_kind.click()
            text = driver.page_source
            #  这里我们只下载每个分类中第一页的配乐,不下载该分类其它页
            html = etree.HTML(text)
            trs = html.xpath("//div[@class='body_a']/div[@class='right']/table/tbody//tr[position()>1]")[:-1]  # 不要第一个和最后一个tr
            for tr in trs:
                #  因为有些类里面一个music也没有,故加入异常处理
                try:
                    one_music_page_url = 'http://www.peiyue.com/' + tr.xpath("./td[2]/a/@href")[0]
                    music_name = tr.xpath("./td[2]/a/text()")[0]
                    self.url_queue.put((music_name, one_music_page_url))  #  将每一个music的名字和主页url添加到url_queue
                except:
                    continue

        driver.quit()  #  关闭浏览器

    #  定义一个解析每个配乐主页url的函数,获得每个配乐下载地址,一个配乐主页url和一个配乐下载地址一一对应
    def get_src(self):
        while True:
            if self.url_queue.empty() and self.page_queue.empty():
                break
            music_name, one_music_page_url = self.url_queue.get()
            #  防止请求超时,做异常处理
            try:
                response = requests.get(one_music_page_url)
                response.encoding = response.apparent_encoding
                text = response.text
                html = etree.HTML(text)
                src = html.xpath("//div[@class='body1aaa']/table//tr[last()-1]//a/@href")[0]
                self.src_queue.put((music_name, src))  #  将每一个music的名字和下载地址添加到src_queue
            except:
                continue

    def run(self):
        url = self.page_queue.get()
        self.get_data(url)
        self.get_src()

#  定义一个消费者类,专门下载配乐
class Consumer(threading.Thread):
    #  重写构造函数
    def __init__(self, src_queue, *args, **kwargs):
        super(Consumer, self).__init__(*args, **kwargs)  # 调用父类的构造函数
        self.src_queue = src_queue

    def run(self):
        music_dir = os.path.join(os.path.dirname(__file__), 'music')
        if not os.path.exists(music_dir):
            os.mkdir(music_dir)  # 在当前项目下创建保存下载下来的配乐的文件夹
        while True:
            if self.src_queue.empty():  #  当这个队列为空时,消费者结束
                break
            music_name, src = self.src_queue.get()
            music_name = re.sub(r"[\??\.,。!!]*", '', music_name)  #  处理名字中的特殊字符,因为这些字符不能是文件名
            music_name = music_name + '.mp3'  #  给文件名加上扩展名
            src_end = re.split(r"/", src)[-1]  #  拿到下载地址中含有中文的那一部分
            src = 'http://mp3.peiyue.com/abcdefg/1234567/upload/' + quote(src_end)  #  因为下载地址有中文字符,所以需要用urllib.parse.quote()来处理中文字符
            print('%s开始下载' % music_name)
            request.urlretrieve(src, os.path.join(music_dir, music_name))
            print('%s下载完成' % music_name)


def main():
    page_amount = int(input("您要下载几页数据呢:"))
    page_queue = Queue(page_amount)  #  定义一个有page_amount个元素的存储要爬取页面url的队列
    url_queue = Queue(10000)  #  定义一个存储每个配乐主页url的队列
    src_queue = Queue(10000)  #  定义一个存储每个配乐下载地址的队列,与url_queue是一一对应关系
    for i in range(1, page_amount+1):
        page_queue.put('http://www.peiyue.com/zuopin.asp?page={}'.format(i))  #将要爬取的url添加到队列中
    #  创建生产者类,一个生产者负责处理一个页面
    #  当创建的生产者个数不少于要处理的个数时,生产者不必while True
    for i in range(page_amount):
        t = Procuder(page_queue, url_queue, src_queue)  #  此处传入的参数会自动到构造函数
        t.start()
    time.sleep(30)  #  因为在打开浏览器时比较慢,所以为了防止消费者直接break延时30秒
    #  创建消费者类,每一个消费者每一次负责下载一个配乐
    #  当创建的消费者个数小于要下载的个数时,因为消费者不够所以需要while True,让每个消费者不断地去工作
    for i in range(10):
        t = Consumer(src_queue)
        t.start()


if __name__ == '__main__':
    main()

结语

总的来说,这个网站的反爬虫做的还是比较不错的,但是具体问题具体分析、见招拆招的能力是我们必须具备的。代码中的有些处理还是可以再进一步优化的,如果有想法可以在评论区直接留言,我们可以交流一下。

获取更多实战项目,请关注公众号’青云学斋’:

微信公众号二维码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值