俗话说,兴趣所在,方能大展拳脚。so结合兴趣的学习才能事半功倍,更加努力专心,apparently本次任务是在视频网站爬取一些好看的小电影,地址不放(狗头保命)只记录过程。

实现功能:

从网站上爬取采用m3u8分段方式的视频文件,对加密的 "ts"文件解密,实现两种方式合并"ts"文件,为防止IP被封,使用代理,最后删除临时文件。

环境 &依赖

  • Win10 64bit

  • IDE:Pycharm

  • Python 3.8

  • Python-site-package:requests + BeautifulSoup + lxml + m3u8 + AES

在PyCharm中创建一个项目会创建一个临时目录存放环境和所需要的package包,所以要在PyCharm 中项目解释器(Project Interpreter)中添加所有需要的包,这张截图是本项目的包列表,红框中是所必须的包,其他有的包我也不知道做什么用的。

下面开始我们的正餐,爬取数据第一步我们需要解析目标网站,找到我们需要爬取视频的地址,F12打开开发者工具

很不幸,这个网站视频是经过包装采用m3u8视频分段方式加载

科普一下:m3u8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。但无论是哪种播放列表,其内部文字使用的都是 utf-8 编码。

当 m3u8 文件作为媒体播放列表(Meida Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。

OK,本着“没有解决不了的困难“的原则我们继续,依旧在开发者模式,从Elements模式切换到NetWork模式,去掉不需要的数据,我们发现了两个m3u8文件一个key文件和一个ts文件

分别点击之后我们可以 看到对应的地址

OK,现在地址已经拿到了,我们可以开始我们的数据下载之路了。

首先进行初始化,包括路径设置,请求头的伪装等,之后我们通过循环去下载所有ts文件,至于如何定义循环的次数我们可以通过将m3u8文件下载之后解析文件得到所有ts的列表,之后拼接地址然后循环就可以得到所有ts文件了。

第一层

新手学习,Python 教程/工具/方法/解疑+V:itz992
#EXTM3U#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=500000,RESOLUTION=720x406500kb/hls/index.m3u8

观察数据,不是真正路径,第二层路径在第三行可以看到,结合我们对网站源码分析再次拼接字符串请求:

第二层


#EXT-X-VERSION:3#EXT-X-TARGETDURATION:2#EXT-X-MEDIA-SEQUENCE:0#EXT-X-KEY:METHOD=AES-128,URI="key.key"#EXTINF:2.000000,IsZhMS5924000.ts#EXTINF:2.000000,IsZhMS5924001.ts#EXT-X-ENDLIST

之后我们循环得到的TS列表,通过拼接地址下载视频片段。但是问题远远没有这么简单,我们下载的ts文件居然无法播放,通过对第二层下载得到的m3u8文件进行分析我们可以发现这一行代码:

#EXT-X-KEY:METHOD=AES-128,URI="key.key"

此网站采用AES方法对所有ts文件进行了加密,其中

METHOD=ASE-128:说明此视频采用ASE-128方式进行加密,

URI=“key.key”:代表key的地址

综上所诉,感觉好难啊,好绕了,都拿到了视频还看不了,但是我们要坚持我们的初心不能放弃。Fortunately,我们应该庆幸Python强大的模块功能,这个问题我们可以通过下载AES模块解决。

完成之后我们需要将所有ts合并为一个MP4文件,最简单的在CMD命令下我们进入到视频所在路径然后执行:

copy /b *.ts fileName.mp4

需要注意所有TS文件需要按顺序排好。在本项目中我们使用os模块直接进行合并和删除临时ts文件操作。

完整代码:

方法一:

新手学习,Python 教程/工具/方法/解疑+V:itz992
import reimport requestsimport m3u8import timeimport osfrom bs4 import BeautifulSoupimport jsonfrom Crypto.Cipher import AESclass VideoCrawler():def __init__(self,url):super(VideoCrawler, self).__init__()self.url=urlself.down_path=r"F:\Media\Film\Temp"self.final_path=r"F:\Media\Film\Final"self.headers={'Connection':'Keep-Alive','Accept':'text/html,application/xhtml+xml,*/*','User-Agent':'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'}def get_url_from_m3u8(self,readAdr):print("正在解析真实下载地址...")with open('temp.m3u8','wb') as file:file.write(requests.get(readAdr).content)m3u8Obj=m3u8.load('temp.m3u8')print("解析完成")return m3u8Obj.segmentsdef run(self):print("Start!")start_time=time.time()os.chdir(self.down_path)html=requests.get(self.url).textbsObj=BeautifulSoup(html,'lxml')tempStr = bsObj.find(class_="iplays").contents[3].string#通过class查找存放m3u8地址的组件firstM3u8Adr=json.loads(tempStr.strip('var player_data='))["url"]#得到第一层m3u8地址tempArr=firstM3u8Adr.rpartition('/')realAdr="%s/500kb/hls/%s"%(tempArr[0],tempArr[2])#一定规律下对字符串拼接得到第二层地址, 得到真实m3u8下载地址,key_url="%s/500kb/hls/key.key"%tempArr[0]#分析规律对字符串拼接得到key的地址key=requests.get(key_url).contentfileName=bsObj.find(class_="video-title w100").contents[0].contents[0]#从源码中找到视频名称的规律fileName=re.sub(r'[\s,!]','',fileName) #通过正则表达式去掉中文名称中的感叹号逗号和空格等特殊字符串cryptor=AES.new(key,AES.MODE_CBC,key)#通过AES对ts进行解密urlList=self.get_url_from_m3u8(realAdr)urlRoot=tempArr[0]i=1for url in urlList:resp=requests.get("%s/500kb/hls/%s"%(urlRoot,url.uri),headers=crawler.headers)if len(key):with open('clip%s.ts' % i, 'wb') as f:f.write(cryptor.decrypt(resp.content))print("正在下载clip%d" % i)else:with open('clip%s.ts'%i,'wb') as f:f.write(resp.content)print("正在下载clip%d"%i)i+=1print("下载完成!总共耗时%d s"%(time.time()-start_time))print("接下来进行合并......")os.system('copy/b %s\\*.ts %s\\%s.ts'%(self.down_path,self.final_path,fileName))print("删除碎片源文件......")files=os.listdir(self.down_path)for filena in files:del_file=self.down_path+'\\'+filenaos.remove(del_file)print("碎片文件删除完成")if __name__=='__main__':crawler=VideoCrawler("地址大家自己找哦")crawler.start()crawler2=VideoCrawler("地址大家自己找哦")crawler2.start()

方法二在方法一中我们是下载所有ts片段到本地之后在进行合并,其中有可能顺序会乱,有时候解密的视频还是无法播放合并之后会导致整个视频时间轴不正确而且视频根本不能完整播放,在经过各种努力,多方查资料之后有的问题还是得不到完美解决,最后突发奇想,有了一个新的想法,我们不必把所有ts片段都下载到本地之后进行合并,而是采用另一种思维模式,一开始我们只创建一个ts文件,然后每次循环的时候不是去下载ts文件而是将通过地址得到的视频片段文件流直接添加到我们一开始创建的ts文件中,如果出现错误跳出当前循环并继续下次操作,最后我们直接得到的就是一个完整的ts文件,还不需要去合并所有片段。具体看代码如何实现。

本代码好多地方和上面都一样,我们只需要领悟其中的原理和方法就OK了


import reimport requestsimport m3u8import timeimport osfrom bs4 import BeautifulSoupimport jsonfrom Crypto.Cipher import AESimport sysimport randomclass VideoCrawler():def __init__(self,url):super(VideoCrawler, self).__init__()self.url=urlself.down_path=r"F:\Media\Film\Temp"self.agency_url='https://www.kuaidaili.com/free/'  #获取免费代理的网站,如果网站过期或者失效,自己找代理网站替换self.final_path=r"F:\Media\Film\Final"self.headers={'Connection':'Keep-Alive','Accept':'text/html,application/xhtml+xml,*/*','User-Agent':'Mozilla/5.0 (Linux; U; Android 6.0; zh-CN; MZ-m2 note Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 MZBrowser/6.5.506 UWS/2.10.1.22 Mobile Safari/537.36'}def get_url_from_m3u8(self,readAdr):print("正在解析真实下载地址...")with open('temp.m3u8','wb') as file:file.write(requests.get(readAdr).content)m3u8Obj=m3u8.load('temp.m3u8')print("解析完成")return m3u8Obj.segmentsdef get_ip_list(self,url, headers):web_data = requests.get(url, headers=headers).textsoup = BeautifulSoup(web_data, 'lxml')ips = soup.find_all('tr')ip_list = []for i in range(1, len(ips)):ip_info = ips[i]tds = ip_info.find_all('td')ip_list.append(tds[0].text + ':' + tds[1].text)return ip_listdef get_random_ip(self,ip_list):proxy_list = []for ip in ip_list:proxy_list.append('http://' + ip)proxy_ip = random.choice(proxy_list)proxies = {'http': proxy_ip}return proxiesdef run(self):print("Start!")start_time=time.time()self.down_path = r"%s\%s" % (self.down_path, uuid.uuid1())#拼接新的下载地址if not os.path.exists(self.down_path): #判断文件是否存在,不存在则创建os.mkdir(self.down_path)html=requests.get(self.url).textbsObj=BeautifulSoup(html,'lxml')tempStr = bsObj.find(class_="iplays").contents[3].string#通过class查找存放m3u8地址的组件firstM3u8Adr=json.loads(tempStr.strip('var player_data='))["url"]#得到第一层m3u8地址tempArr=firstM3u8Adr.rpartition('/')all_content = (requests.get(firstM3u8Adr).text).split('\n')[2]#从第一层m3u8文件中中找出第二层文件的的地址midStr = all_content.split('/')[0]#得到其中有用的字符串,这个针对不同的网站采用不同的方法自己寻找其中的规律realAdr = "%s/%s" % (tempArr[0], all_content)#一定规律下对字符串拼接得到第二层地址, 得到真实m3u8下载地址,key_url = "%s/%s/hls/key.key" % (tempArr[0], midStr)#分析规律对字符串拼接得到key的地址key_html = requests.head(key_url)#访问key的地址得到的文本status = key_html.status_code#是否成功访问到key的地址key = ""if status == 200:all_content=requests.get(realAdr).text#请求第二层m3u8文件地址得到内容if "#EXT-X-KEY" in all_content:key = requests.get(key_url).content#如果其中有"#EXT-X-KEY"这个字段说明视频被加密self.fileName = bsObj.find(class_="video-title w100").contents[0].contents[0]#分析网页得到视频的名称self.fileName=re.sub(r'[\s,!]','',self.fileName)#因为如果文件名中有逗号感叹号或者空格会导致合并时出现命令不正确错误,所以通过正则表达式直接去掉名称中这些字符iv = b'abcdabcdabcdabcd'#AES解密时候凑位数的ivif len(key):#如果key有值说明被加密cryptor = AES.new(key, AES.MODE_CBC, iv)#通过AES对ts进行解密urlList=self.get_url_from_m3u8(realAdr)urlRoot=tempArr[0]i=1outputfile=open(os.path.join(self.final_path,'%s.ts'%self.fileName),'wb')#初始创建一个ts文件,之后每次循环将ts片段的文件流写入此文件中从而不需要在去合并ts文件ip_list=self.get_ip_list(self.agency_url,self.headers)#通过网站爬取到免费的代理ip集合for url in urlList:try:proxies=self.get_random_ip(ip_list)#从ip集合中随机拿到一个作为此次访问的代理resp = requests.get("%s/%s/hls/%s" % (urlRoot, midStr, url.uri), headers=crawler.headers,proxies=proxies)#拼接地址去爬取数据,通过模拟header和使用代理解决封IPif len(key):tempText=cryptor.decrypt(resp.content)#解密爬取到的内容progess=i/len(urlList)#记录当前的爬取进度outputfile.write(tempText)#将爬取到ts片段的文件流写入刚开始创建的ts文件中sys.stdout.write('\r正在下载:%s,进度:%s %%'%(self.fileName,progess))#通过百分比显示下载进度sys.stdout.flush()#通过此方法将上一行代码刷新,控制台只保留一行else:outputfile.write(resp.content)except Exception as e:print("\n出现错误:%s",e.args)continue#出现错误跳出当前循环,继续下次循环i+=1outputfile.close()print("下载完成!总共耗时%d s"%(time.time()-start_time))self.del_tempfile()#删除临时文件def del_tempfile(self):file_list=os.listdir(self.down_path)for i in file_list:tempPath=os.path.join(self.down_path,i)os.remove(tempPath)os.rmdir(self.down_path)print('临时文件删除完成')if __name__=='__main__':url=input("输入地址:\n")crawler=VideoCrawler(url)crawler.run()quitClick=input("请按Enter键确认退出!")

问题与解决:

  1. 一开始以为电脑中Python环境中有模块就OK了,最后发现在Pycharm中自己虚拟的环境中还需要添加对应模块,

  2. No module named Crypto.Cipher ,网上看了很多最后通过添加pycryptodome模块解决,电脑环境Win10

  3. 文件名不能有感叹号,逗号或者空格等这些特殊字符,不然执行合并命令的时候会提示命令不正确

  4. 在下载中将ts文件流写入文件时会出现这种错误(‘Data must be padded to 16 byte boundary in CBC mode’,) Data must be padded,我们直接continue跳出当前循环继续下次下载。

  5. 有时出现 “Protocol Error, Connection abort, os.error”,应该是爬取操作太频繁ip被封,针对此问题我们使用免费代理。

Python爬虫实战之电影爬取过程相关推荐

  1. 转 Python爬虫实战一之爬取糗事百科段子

    静觅 » Python爬虫实战一之爬取糗事百科段子 首先,糗事百科大家都听说过吧?糗友们发的搞笑的段子一抓一大把,这次我们尝试一下用爬虫把他们抓取下来. 友情提示 糗事百科在前一段时间进行了改版,导致 ...

  2. 《python爬虫实战》:爬取贴吧上的帖子

    <python爬虫实战>:爬取贴吧上的帖子 经过前面两篇例子的练习,自己也对爬虫有了一定的经验. 由于目前还没有利用BeautifulSoup库,因此关于爬虫的难点还是正则表达式的书写. ...

  3. python爬虫实战之多线程爬取前程无忧简历

    python爬虫实战之多线程爬取前程无忧简历 import requests import re import threading import time from queue import Queu ...

  4. Python爬虫实战一之爬取糗事百科段子

    点我进入原文 另外, 中间遇到两个问题: 1. ascii codec can't decode byte 0xe8 in position 0:ordinal not in range(128) 解 ...

  5. python爬虫实战之异步爬取数据

    python爬虫实战之异步爬取数据 文章目录 前言 一.需求 二.使用步骤 1.思路 2.引入库 3.代码如下 总结 前言 python中异步编程的主要三种方法:回调函数.生成器函数.线程大法. 以进 ...

  6. Python爬虫实战Pro | (1) 爬取猫眼电影Top100榜单

    在本篇博客中,我们将使用requests+正则表达式来爬取猫眼电影官网的TOP100电影榜单,获取每部电影的序号,片名,主演,上映日期,评分和封面等内容. 之前在Python爬虫实战(1)中我们曾爬取 ...

  7. Python爬虫实战练习:爬取微信公众号文章

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:徐洲更 为了实现该爬虫我们需要用到如下工具 Chrome浏览器 Py ...

  8. Python爬虫实战项目案例——爬取微信朋友圈

    项目实战   Appium爬取微信朋友圈   自动化爬取App数据 基于移动端的自动化测试工具Appium的自动化爬取程序. 步骤 1.JDK - Download JDK,Appium要求用户必须配 ...

  9. python爬虫实战(一)~爬取百度百科人物的文本+图片信息+Restful api接口

    我的github地址:https://github.com/yuyongsheng1990/python_spider_from_bdbaike # -*- coding: UTF-8 -*- # @ ...

最新文章

  1. MVC4做网站后台:栏目管理1、添加栏目
  2. 业界 | 5个步骤开启你的数据科学职业生涯!(附链接)
  3. VC++下Window剪贴板的操作
  4. Python-----学了今天,忘了昨天.
  5. 架构杂谈《八》Docker 架构
  6. 领域驱动设计和实践(转:http://kb.cnblogs.com/page/112298/)
  7. 牛顿插值 | MATLAB源码
  8. unity3d 双人巡逻兵网络游戏
  9. C++ for_each函数
  10. 电脑桌面打开计算机打开多个,如何在电脑桌面并排显示多个Excel工作表
  11. 八爪鱼导出到mysql数据库_八爪鱼采集器怎么将数据导出数据库?
  12. android studio anr,Android ANR 分析
  13. 抖音搬运视频热门技巧 剪辑后会修改视频md5
  14. 国密gmssl命令行生成SM2证书
  15. 光学计算机PPT,光学课件
  16. 试一试 kolla部署OpenStack Ocata
  17. 我决定把一个收费视频课全免费公开了,今天起,慢慢放出“人人都需要的产品思维课”...
  18. DevOps团队如何为网络星期一做准备
  19. python知识补充
  20. “EdrawMax”已损坏,无法打开。 您应该将它移到废纸篓 。完美解决问题。

热门文章

  1. ces展会的一点看法
  2. MySQL—运算符详解(算术、比较、逻辑、范围运算符与集合运算符 模糊查询 NULL值运算与null值判断 位运算符)
  3. 社交网络中基于位置的影响力最大化 CIKM2015 译文
  4. 大学生如何修好进水的电脑
  5. Mysql实现统计查询
  6. 数据库的分组统计查询
  7. linux搭建keepalived+tomcat+nginx 双主机热备排坑
  8. HCIA 8-17 笔记
  9. vue 中echarts的使用
  10. Caused by: java.lang.NoSuchMethodError:No virtual method isSuccess()Z in class Lretrofit2/Response;