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

目录

  • 效果

  • 环境和外部库

  • 问题与解决办法

  • 代码相关知识讲解

  • 完整代码

  • 结语

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

效果

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

环境和外部库

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

问题与解决办法

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

代码相关知识讲解

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

完整代码

1# title:多线程爬取配乐网,下载免费配乐 2 3import os 4import re 5import time 6import threading 7from urllib import request 8from urllib.parse import quote 9from selenium import webdriver 10from queue import Queue 11from lxml import etree 12import requests 13 14BASE_URL = 'http://www.peiyue.com/' # 本文爬取的网站 15 16# 定义一个生产者类爬取页面,获得下载地址 17class Procuder(threading.Thread): 18 # 重写构造函数 19 def __init__(self, page_queue, url_queue, src_queue, *args, **kwargs): 20 super(Procuder, self).__init__(*args, **kwargs) # 调用父类的构造函数 21 self.page_queue = page_queue 22 self.url_queue = url_queue 23 self.src_queue = src_queue 24 25 # 定义一个解析page_queue中页面的函数,获得每个配乐主页的url 26 def get_data(self, url): 27 driver_path = "E://Mystudy/My_Project/Python_Project/TOLL/Driver/chromedriver.exe" 28 driver = webdriver.Chrome(executable_path=driver_path) 29 driver.get(url) 30 # 确定每一页中有多少个类 31 length = len(driver.find_elements_by_xpath( 32 "//div[@class='body_a']//div[@class='right']/table/tbody/tr[position()>1]")[:-1]) 33 for i in range(length): 34 # 因为在当前页面点击不同的类的盒子会在当前页面打开新页面,而当获取完数据在跳回到起始页面时,原来的元素失效,不再依赖当前页面 35 # 因此这里我们直接简单粗暴的循环打开起始页面,每一次处理一个配乐类 36 driver.get(url) 37 diff_kind = driver.find_elements_by_xpath( 38 "//div[@class='body_a']//div[@class='right']/table/tbody/tr[position()>1]//a[@class='q1']")[i] # 定位第i+1个配乐类 39 diff_kind.click() 40 text = driver.page_source 41 # 这里我们只下载每个分类中第一页的配乐,不下载该分类其它页 42 html = etree.HTML(text) 43 trs = html.xpath("//div[@class='body_a']/div[@class='right']/table/tbody//tr[position()>1]")[:-1] # 不要第一个和最后一个tr 44 for tr in trs: 45 # 因为有些类里面一个music也没有,故加入异常处理 46 try: 47 one_music_page_url = 'http://www.peiyue.com/' + tr.xpath("./td[2]/a/@href")[0] 48 music_name = tr.xpath("./td[2]/a/text()")[0] 49 self.url_queue.put((music_name, one_music_page_url)) # 将每一个music的名字和主页url添加到url_queue 50 except: 51 continue 52 53 driver.quit() # 关闭浏览器 54 55 # 定义一个解析每个配乐主页url的函数,获得每个配乐下载地址,一个配乐主页url和一个配乐下载地址一一对应 56 def get_src(self): 57 while True: 58 if self.url_queue.empty() and self.page_queue.empty(): 59 break 60 music_name, one_music_page_url = self.url_queue.get() 61 # 防止请求超时,做异常处理 62 try: 63 response = requests.get(one_music_page_url) 64 response.encoding = response.apparent_encoding 65 text = response.text 66 html = etree.HTML(text) 67 src = html.xpath("//div[@class='body1aaa']/table//tr[last()-1]//a/@href")[0] 68 self.src_queue.put((music_name, src)) # 将每一个music的名字和下载地址添加到src_queue 69 except: 70 continue 71 72 def run(self): 73 url = self.page_queue.get() 74 self.get_data(url) 75 self.get_src() 76 77# 定义一个消费者类,专门下载配乐 78class Consumer(threading.Thread): 79 # 重写构造函数 80 def __init__(self, src_queue, *args, **kwargs): 81 super(Consumer, self).__init__(*args, **kwargs) # 调用父类的构造函数 82 self.src_queue = src_queue 83 84 def run(self): 85 music_dir = os.path.join(os.path.dirname(__file__), 'music') 86 if not os.path.exists(music_dir): 87 os.mkdir(music_dir) # 在当前项目下创建保存下载下来的配乐的文件夹 88 while True: 89 if self.src_queue.empty(): # 当这个队列为空时,消费者结束 90 break 91 music_name, src = self.src_queue.get() 92 music_name = re.sub(r"[\??\.,。!!]*", '', music_name) # 处理名字中的特殊字符,因为这些字符不能是文件名 93 music_name = music_name + '.mp3' # 给文件名加上扩展名 94 src_end = re.split(r"/", src)[-1] # 拿到下载地址中含有中文的那一部分 95 src = 'http://mp3.peiyue.com/abcdefg/1234567/upload/' + quote(src_end) # 因为下载地址有中文字符,所以需要用urllib.parse.quote()来处理中文字符 96 print('%s开始下载' % music_name) 97 request.urlretrieve(src, os.path.join(music_dir, music_name)) 98 print('%s下载完成' % music_name) 99 100 101def main(): 102 page_amount = int(input("您要下载几页数据呢:")) 103 page_queue = Queue(page_amount) # 定义一个有page_amount个元素的存储要爬取页面url的队列 104 url_queue = Queue(10000) # 定义一个存储每个配乐主页url的队列 105 src_queue = Queue(10000) # 定义一个存储每个配乐下载地址的队列,与url_queue是一一对应关系 106 for i in range(1, page_amount+1): 107 page_queue.put('http://www.peiyue.com/zuopin.asp?page={}'.format(i)) #将要爬取的url添加到队列中 108 # 创建生产者类,一个生产者负责处理一个页面 109 # 当创建的生产者个数不少于要处理的个数时,生产者不必while True 110 for i in range(page_amount): 111 t = Procuder(page_queue, url_queue, src_queue) # 此处传入的参数会自动到构造函数 112 t.start() 113 time.sleep(30) # 因为在打开浏览器时比较慢,所以为了防止消费者直接break延时30秒 114 # 创建消费者类,每一个消费者每一次负责下载一个配乐 115 # 当创建的消费者个数小于要下载的个数时,因为消费者不够所以需要while True,让每个消费者不断地去工作 116 for i in range(10): 117 t = Consumer(src_queue) 118 t.start() 119 120 121if __name__ == '__main__': 122 main() 123 124 125

结语

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

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

代码交流 2021