python爬虫实战之旅( 第七章:异步爬虫(线程池法))

上接: 第六章:代理
下接:第七章:异步爬虫(协程法)

写在开头:
这篇代码爬取的li/shi/ping网站的,本意是写在这里一个系列以后复习回看,但是一直因为版权问题被删,所以下列所有图片文字都做了一定的打码处理。(我保证不作商用!别删了别删了

1.高性能异步爬虫

1.1 目的

在爬虫中使用异步实现高性能的数据爬取操作。

1.2 实质

一个线程下有多个任务,当任务遇到I/O需要等待时就执行其他任务。

1.3 异步爬虫的方式:

1.3.1 多线程,多进程(不建议使用)

  • 好处:可以为相关阻塞(eg.requests.get()函数)的操纵单独开启线程或者进程,阻塞操作就可以异步执行。
  • 弊端:无法无限制的开启多线程或者多进程

1.3.2 线程池、进程池(适当地使用)

  • 好处:我们可以降低系统对进程或者线程创建和销毁的一个频率,从而很好的降低系统的开销。
  • 弊端:池中线程或者进程的数量是有上限的。
线程 进程
是程序执行的最小单位 一个进程可以有多个线程
线程是任务分配的最小单位 进程是资源分配的独立单位

1.3.3 单线程+异步协程(推荐):

协程相关概念 如下所示
even_loop 事件循环,相当于一个无限循环,我们把一些函数注册到这个事件循环上。
coroutine 协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用,我们可以使用async
task 任务,它是对协程对象的进一步封装,包含了任务的各个状态。
future 代表将来执行货还没有执行的任务,实际上和task没有本质区别。
async 定义一个协程。
await 用来挂起阻塞方法的执行。

2.前两种处理方式的比较

2.1 单线程串行方式:

#使用单线程串行方式执行
import time
def get_page(str):
    print("正在下载:",str)
    time.sleep(2)#模拟实际阻塞时间,假设每次下载耗时2s
    print("下载成功:",str)
    pass
name_list =['xiaozi','aa','bb','cc']
start_time =time.time()
for i in range(len(name_list)):
    get_page(name_list[i])
    pass
end_time = time.time()
print('%d seconds'%(end_time-start_time))

输出:

正在下载: xiaozi
下载成功:xiaozi
正在下载: aa
下载成功:aa
正在下载: bb
下载成功:bb
正在下载: cc
下载成功:cc
8 seconds

用时8s,可以看到单线程是一个接一个的

2.2 线程池的基本使用:

#使用线程池方式执行
import time
#导入线程池模块对应的类
from multiprocessing.dummy import Pool

def get_page(str):
    print("正在下载:",str)
    time.sleep(2)#模拟实际阻塞时间,假设每次下载耗时2s
    print("下载成功:",str)
    pass
name_list =['xiaozi','aa','bb','cc']
start_time =time.time()
#实例化一个线程池对象,该池对象中有四个阻塞操作
pool=Pool(4)
#将列表name_list中每一个列表元素传递给get_page处理
pool.map(get_page,name_list)
#map函数的返回值由“get_page”
end_time = time.time()
print('%d seconds'%(end_time-start_time))

输出:

正在下载:xiaozi  
正在下载:aa
正在下载:bb
正在下载: cc
下载成功:xiaozi
下载成功:aaxiaozi
下载成功:bb
下载成功:cc
2 seconds

可以看到时间变为2s,大大提高了程序运行的效率

3.线程池代码实战

3.1 用“线程池、进程池”的方法爬取视频网页

大致思路就是爬取视频的首页,然后从首页的文本中爬取到每个小视频的链接详情页,再从爬取到的小视频的详情页中抓到视频mp4的地址从而完成对视频的爬取。

3.1.1遇到的问题

但是在实际过程中遇到了以下问题:
1. 视频详情页本身没有视频的链接,mp4的地址是在点击播放图标后动态生成的。
解决方法:对ajex中获得的假地址进行处理获取真地址
在这里插入图片描述
在这里插入图片描述

2. 直接访问ajex请求中的url视频地址会失效
在这里插入图片描述
解决方法就是改变headers的键值:
''Referer': 'https://www.pearvideo.com/video_' + videoID_,'后就不会显示该视频已下架了

3. 在之后的实验中,会出现多个版本的地址
解决方法:看哪个能打开有效(真正的视频地址是在视频详情页动态生成的这个)。
在这里插入图片描述

3.1.2 梳理流程,数据分析

于是重新梳理数据爬取过程分析抓包工具中的请求与网页代码变化:
首页爬取没有问题
在这里插入图片描述

从首页文本中找到每个视频详情页的链接:
在这里插入图片描述

以及视频详情页的标题:
在这里插入图片描述
同时我们发现再点击访问任意一个视频详情页时,首页中出现了一个ajex请求:
在这里插入图片描述

仔细观察这个ajex请求的url,发现它与我们最终想得到的视频url(也就是与视频详情页动态生成的视频链接一致)很像,并且其中都有关键字符串“1721700”,大胆猜测这就是这个视频唯一对应的一个“标识符”,说明我们找到的ajex一定跟这个视频有联系。
在这里插入图片描述

下图是首页中对应的标签内容:
在这里插入图片描述

下图是ajex请求中的url:
在这里插入图片描述

下图是视频详情页动态生成的视频url:
在这里插入图片描述
知道了这个ajex请求很重要,但找到的url不一致,于是我们继续挖这个ajex请求携带的信息,最终发现这个请求的回应数据中就有我们找的视频url:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

对比一下视频详情页动态生成的实际视频url,发现很接近了
在这里插入图片描述
预期的得到真地址:

"https://video.pearvideo.com/mp4/adshort/20210301/cont-1721700-15618782_adpkg-ad_hd.mp4"
实际得到的伪地址:
"https://video.pearvideo.com/mp4/adshort/20210301/1614658283384-15618782_adpkg - ad_hd.mp4

仔细观察,发现真正的视频链接多了 cont-1721700 这一部分,同时1721700还是出现过的一个标识符,所以到时候在代码中就可以将假地址用spilt(’/’)分割,把’cont-movieID’ 加上去,多余的数字删去,就可以由得到的假地址变成真实的视频地址。

3.1.3 ajex请求

1)ajex的参数

  • contId:就是首页爬取到的视频对应的一串数字标识:
  • mid:多次尝试后发现mid竟然是随机生成的……所以代码中调用random随机生成函数赋值就可以了。

在这里插入图片描述

2)ajex回应数据是json对象,用字典中关键值对应的方法提取url:

在这里插入图片描述

3.1.4 代码实践:

#爬取视频网页的视频数据
import requests
from lxml import etree
import random
import os
from multiprocessing.dummy import Pool
#原则:线程池处理的是阻塞高且耗时的操作

if __name__ == '__main__':
  #先生成一个存视频的文件夹用于后续持续化存储
  if not os.path.exists('./video'):
         os.mkdir('./video')
  #首先拿到视频的首页url
  url = 'https://www.pearvideo.com/category_5'
  #UA伪装
  headers = {
    
    
         "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3823.400 QQBrowser/10.7.4307.400'
    }
  #对首页url发起请求获取首页数据
  response = requests.get(url=url, headers=headers)
  page_text = response.text
  #数据解析,从首页解析出视频详情页的url和视频的名称
  tree = etree.HTML(page_text)
  li_list = tree.xpath('//*[@id="listvideoListUl"]/li')
  urls = []  # 储存所有视频的连接和名字
  for li in li_list:
      #视频详情页
      new_url = 'https://www.pearvideo.com/' + li.xpath('./div/a/@href')[0]
      #视频名称
      new_name = li.xpath('./div/a/div[2]/text()')[0] + '.mp4'
      new_page_text = requests.get(url=new_url, headers=headers).text
      #从详情页中解析出视频的地址(url)
      new_tree = etree.HTML(new_page_text)
      name = new_tree.xpath('//*[@id="detailsbd"]/div[1]/div[2]/div/div[1]/h1/text()')[0]
      # print(name)
      # 然后我们发现mp4是动态加载出来的,因此详情页解析中没有视频链接
      # 需要抓包ajax请求中的url,用浏览器的抓包工具
      # 通过抓包ajax得到一个可以发送的url和请求伪装的视频的url
      id_ = str(li.xpath('./div/a/@href')[0]).split('_')[1]
      #每个视频对应的id在首页中对应的标签文本中,所以对标签内容进行处理提取
      #捕获到的ajex请求的url的公共部分
      ajax_url = 'https://www.pearvideo.com/videoStatus.jsp?'
      #相应的ajex请求需备的参数
      params = {
    
    
         'contId': id_,
         'mrd': str(random.random()),
      }
      ajax_headers = {
    
    
          "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3823.400 QQBrowser/10.7.4307.400',
          'Referer': 'https://www.pearvideo.com/video_' + id_,
      }
      # 加了'Referer': 'https://www.pearvideo.com/video_1721700'后就不会显示该视频已下架了
      dic_obj = requests.get(url=ajax_url, params=params, headers=ajax_headers).json()
      video_url = dic_obj["videoInfo"]['videos']["srcUrl"]
      # 此处视频地址做了加密即ajax中得到的地址需要加上cont-,并且修改一段数字为id才是真地址
      # 预期的得到真地址:"https://video.pearvideo.com/mp4/adshort/20210301/cont-1721700-15618782_adpkg-ad_hd.mp4"
      # 实际得到的伪地址:"https://video.pearvideo.com/mp4/adshort/20210301/1614658283384-15618782_adpkg - ad_hd.mp4
      # 为了得到真地址url,对伪地址做字符串处理
      video_true_url = ''
      s_list = str(video_url).split('/')#按照‘/’进行切割
      # print(s_list)
      for i in range(0, len(s_list)):
          if i < len(s_list) - 1:
             video_true_url += s_list[i] + '/'
          else:
             ss_list = s_list[i].split('-')
             # print(ss_list)
             for j in range(0, len(ss_list)):
                 if j == 0:
                    video_true_url += 'cont-' + id_ + '-'
                    pass
                 elif j == len(ss_list)-1:
                    video_true_url += ss_list[j]
                    pass
                 else:
                    video_true_url += ss_list[j] + '-'
      # print(video_true_url)
      dic = {
    
    
             'name': name,
             'url': video_true_url,
      }
      urls.append(dic)

     # 使用线程池对视频数据进行请求(较为耗时的阻塞操作)
  def get_video_data(dic_):
    url_ = dic_['url']
    print(dic_['name'], '正在下载.....')
    video_data = requests.get(url=url_, headers=headers).content
    video_path = './video/' + dic_['name']
    with open(video_path, 'wb') as fp:
        fp.write(video_data)
        print(dic_['name'], '下载成功!!!!!')

  pool = Pool(4)
  pool.map(get_video_data, urls)
  pool.close()
  pool.join()

3.1.5 运行结果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

小猫咪可真可爱!

猜你喜欢

转载自blog.csdn.net/KQwangxi/article/details/114286410