FNN网站m3u8视频抓取--python爬虫--抓包、浏览器模拟、openssl解密、FFmpeg视频合成、Tkinter图形界面、多线程

注意:本程序由于要使用windows的命令行,只在windows上测试通过。如果是Linux平台,将代码中windows的命令行部分换成相应的Linux命令就行。

提示:大家可以先看博客https://blog.csdn.net/liujiayu2/article/details/86083400。 获取m3u8的基础知识。如果对FNN网站感兴趣,可以使用上面博文的方法对FNN网站的m3u8文件进行分析。FNN上的m3u8一般有3个。我们只需要获取主m3u8(main_m3u8),然后解析main_m3u8文件,可以获得视频video_m3u8和音频audio_m3u8另外两个m3u8文件。

一、任务目标

输入:FNN网站的URL,例如https://www.fnn.jp/articles/-/210727

输出:对应网页的mp4视频

二、m3u8视频抓取基本思路

1.使用webdriver模拟chrome浏览器访问指定的URL。

2.在访问的过程中,利用browsermobproxy进行抓包,并获得视频的主m3u8(main_m3u8).

3.分析main_m3u8,获得视频video_m3u8和音频audio_m3u8。

4.分析视频video_m3u8和音频audio_m3u8,下载多个视频和多个音频。

5.使用openssl和视频video_m3u8和音频audio_m3u8中的key和IV对每个视频和音频进行解密。

6.使用windows命令行对多个解密的视频进行合成。

7.使用windows命令行对多个解密的音频进行合成。

8.使用ffmpeg对音频文件和视频文件进行合成。

三、一些坑

1.安装browsermobproxy时,需要安装JAVA,我安装的是jdk-10.0.2。注意要将环境变量添加到系统。

2.安装browsermobproxy时,不仅需要pip install,还需要在电脑上存放browsermob-proxy-2.1.4文件夹。具体安装可以自行百度。

3.安装browsermobproxy时,需要将证书导入chrome浏览器中,不然chrome会罢工。我采用的是公用证书,在网上自己找的。你也可以自行百度,如果百度不了。可以给我发邮件(heguannan@163.com),我将证书发送给你。你导入到chrome中就行了。

4.使用webdriver,获取网页中的元素时,要注意find_element_by_??和find_elements_by_??。注意一个是element,是单数。一个是elements,是复数。当寻找的元素有多个符合要求的时候,一定要使用复数形式,这样会返回一个list。

5.我将ffmpeg加入到环境变量中,在命令行中直接调用ffmpeg有问题。因此,本代码是将ffmpeg的完整路径写了下来,并调用。

6.注意windows的路径中的’\’和python中的转义符冲突,要使用’\\’来表示’\’。具体查看代码。

7.使用Tkinter进行界面编程时,如果一个按钮的作用是执行一大段代码,比如本程序中的爬虫,会造成主界面未响应,这时候可以使用多线程解决。

四、代码

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from browsermobproxy import Server
import urllib.request
import urllib.parse
import os
import time
from tkinter import *
import tkinter.messagebox
from tkinter.filedialog import askdirectory
from tkinter.filedialog import askopenfilename
import threading

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'}
dowload_index = 0
def get_main_m3u8(base_url,browsermob_path):
    browsermob_path = browsermob_path +'/bin/browsermob-proxy.bat'  
    server = Server(browsermob_path)
    try:
        server.start()
    except:
        text_box.insert(END,'代理服务器打开失败,请检查browsermob安装\n')
        return '',''
    proxy = server.create_proxy()
    chrome_options = Options()
    chrome_options.add_argument('--proxy-server={0}'.format(proxy.proxy))
    chrome_options.add_argument('--headless')
    driver = webdriver.Chrome(chrome_options=chrome_options)  
    proxy.new_har("fnn", options={'captureHeaders': True, 'captureContent': True})
    text_box.insert(END,'正在尝试连接url: '+ base_url+'\n')
    start  = time.time()
    driver.set_page_load_timeout(30)
    driver.set_script_timeout(30)
    try:
        driver.get(base_url)
    except:
        text_box.insert(END,'未完整获得网页信息, 耗时'+str(round(time.time()-start))+'秒\n')   
    mp4_name = base_url.split('/')[-1]
    result = proxy.har
    for entry in result['log']['entries']:
        _url = entry['request']['url']
        if "master.m3u8?" in _url:
            server.stop()
            driver.quit()
            text_box.insert(END,'成功获取m3u8信息!耗时' +str(round(time.time()-start)) +'秒\n')
            return _url,mp4_name
    server.stop()
    driver.quit()
    text_box.insert(END,'没有成功获取m3u8信息,请检查网络,清理浏览器缓存,确认提供的url网页中含有m3u8信息.\n')
    time.sleep(5)
    return '',''

#GET main m3u8
def get_videoaudio_m3u8(url):
    req = urllib.request.Request(url=url, headers=headers, method='GET')
    response = urllib.request.urlopen(req)
    m3u8_master = response.read().decode('utf-8')
    m3u8_video_url = m3u8_master.split('\n')[4]
    rendition2_tmp = m3u8_master.split('\n')[2]
    m3u8_audio_url = rendition2_tmp.split('URI=')[1][1:-1]
    return m3u8_video_url,m3u8_audio_url

def download_m3u8(url):
    req = urllib.request.Request(url, headers=headers, method='GET')
    res = urllib.request.urlopen(req)
    m3u8_text = res.read().decode('utf-8')
    return m3u8_text 

def download_keyIV(m3u8_text):
    m3u8_line = m3u8_text.split('\n')
    for index, line in enumerate(m3u8_line):        
        if "EXT-X-KEY" in line:
            key_url_tmp = line.split('URI=')[1]
            key_url=key_url_tmp.split(',IV=')[0][1:-1]
            IV = key_url_tmp.split(',IV=')[1][2:]
            key_req = urllib.request.Request(url=key_url, headers=headers, method='GET')
            key_res = urllib.request.urlopen(key_req).read()
            key = key_res.hex() 
            return key,IV
        
def download_ts(m3u8_text,m3u8_type,mp4_path):
    m3u8_line = m3u8_text.split('\n')
    URI_Ready = False
    if m3u8_type =='audio':
        filetype = 'a'
        text_box.insert(END,'正在下载音频ts文件\n')
    elif m3u8_type =='video':
        filetype = 'v'
        text_box.insert(END,'正在下载视频ts文件\n')
    else:
        text_box.insert(END,'错误的ts类型,请检查!\n')
    ts_index = 0
    for index, line in enumerate(m3u8_line):        
        if "EXTINF" in line:
            URI_Ready = True
            continue
        if  URI_Ready==True:
            ts_req = urllib.request.Request(url=line, headers=headers, method='GET')
            ts_res = urllib.request.urlopen(ts_req).read()
            #text_box.insert(END,'下载第'+str(ts_index)+'个ts文件成功\n')
            URI_Ready=False
            filepath = mp4_path +'/' +filetype +str(ts_index) 
            with open(filepath+".ts", 'wb') as f:
                    f.write(ts_res)
                    f.flush()               
            ts_index = ts_index+1
            key,IV = download_keyIV(m3u8_text)
            decryption(key,IV,filepath)
            #text_box.insert(END,'解密第'+str(ts_index)+'个ts文件成功\n')
    if m3u8_type =='audio':
        text_box.insert(END,'解密音频ts文件成功\n')
    elif m3u8_type =='video':
        text_box.insert(END,'解密视频ts文件成功\n')
    else:
        text_box.insert(END,'错误的ts类型,请检查!\n')

def decryption(key,IV,file):    
    cmd = 'openssl aes-128-cbc -d -in '
    cmd = cmd + file+".ts " + '-out '+ file +'_de.ts -nosalt -iv '+IV+' -K '+key   
    os.system(cmd)

def merge_ts(mp4_path,ffmpeg_path,mp4_name):
    cmd_a = 'copy /b '+(mp4_path+'/a*_de.ts '+ mp4_path +'/merge_audio.m4a').replace('/', '\\')   
    os.system(cmd_a)
    cmd_v = 'copy /b '+(mp4_path+'/v*_de.ts '+ mp4_path +'/merge_video.mp4').replace('/', '\\')
    os.system(cmd_v)
    cmd = (ffmpeg_path +'/bin/ffmpeg -i '+mp4_path+ '/merge_video.mp4 -i '+mp4_path+'/merge_audio.m4a '+mp4_path +'/'+ mp4_name +'.mp4').replace('/', '\\')
    os.system(cmd)
    text_box.insert(END,'合成mp4文件成功!\n')
    cmd_del1 = ('del '+mp4_path+'/'+'*.ts').replace('/', '\\')             
    cmd_del2 = ('del '+mp4_path+'/'+'merge_audio.m4a').replace('/', '\\')
    cmd_del3 =  ('del '+mp4_path+'/'+'merge_video.mp4').replace('/', '\\')
    os.system(cmd_del1)
    os.system(cmd_del2)
    os.system(cmd_del3)
    
def read_path(urls_path):
    with open(urls_path,'r') as f:
        path_list =  f.read().splitlines()
        return path_list

def fnnDownloader(url_path,mp4_path,ffmpeg_path,browsermob_path):
    url_list = read_path(url_path)
    url_list = list(filter(None, url_list))
    len_url_list =len(url_list) 
    text_box.insert(END,'读取到'+str(len_url_list)+'个网页的URL\n')
    if (len_url_list>0):
        try_list = url_list
        retry_list = []
        num_try = 0
        max_try = 10
        while (True):
            num_try = num_try+1
            for dowload_index in range(len_url_list):
                text_box.insert(END,'正在解析第'+str(dowload_index+1)+'个网页, '+str(dowload_index+1)+'/'+str(len(try_list))+'\n')
                m3u8_url,mp4_name = get_main_m3u8(try_list[dowload_index],browsermob_path)
                if m3u8_url =='':
                    retry_list.append(try_list[dowload_index])
                    continue
                m3u8_video_url,m3u8_audio_url = get_videoaudio_m3u8(m3u8_url)
                video_text = download_m3u8(m3u8_video_url)
                audio_text = download_m3u8(m3u8_audio_url)
                download_ts(video_text,'video',mp4_path)
                download_ts(audio_text,'audio',mp4_path)
                merge_ts(mp4_path,ffmpeg_path,mp4_name)  
            if (len(retry_list)<=0 or num_try > max_try):
                break
            if (len(retry_list)>0 and num_try <= max_try):
                try_list = retry_list
                len_url_list = len(try_list)
                retry_list = []
                text_box.insert(END,'等待'+str(2*num_try) +'秒后,进行第'+str(num_try)+'次重试'+'\n')
                time.sleep(2*num_try)
        
def selectUrl():
    path = askopenfilename()
    url_path.set(path) 
    
def selectMp4():
    path = askdirectory()
    mp4_path.set(path) 

def selectFfmpeg():
    path = askdirectory()
    ffmpeg_path.set(path) 
   
def selectBrowsermob():
    path = askdirectory()
    browsermob_path.set(path) 
    
def getPath():
    url_path = url_entry.get()
    mp4_path = mp4_entry.get()
    ffmpeg_path = ffmpeg_entry.get()
    browsermob_path = browsermob_entry.get()   
    run_but.config(state="disabled")
    fnnDownloader(url_path,mp4_path,ffmpeg_path,browsermob_path)
    run_but.config(state="normal")
    tkinter.messagebox.showinfo('提示','程序运行结束,需要人工查看视频下载完成情况')
 
    
def thread_it(func, *args):
    t = threading.Thread(target=func, args=args)
    t.setDaemon(True)
    t.start()

if __name__ == '__main__': 
    root = Tk()
    root.title('fnn下载器')
    root.geometry('940x500')
    root.minsize(940, 500)            
    root.minsize(940, 500) 
    #path
    url_path = StringVar()
    mp4_path = StringVar()
    ffmpeg_path = StringVar()
    browsermob_path = StringVar()
    #set label entry box button
    #URL path
    Label(root,text = "URL 文件:",font = '宋体 -20 bold').grid(row = 0, column = 0)
    url_entry=Entry(root, textvariable = url_path,width = 100)
    url_entry.grid(row = 0, column = 1)
    url_entry.insert(0,'D:/fnn/url.txt')
    Button(root, text = "选择", command = selectUrl,font = '宋体 -20 bold').grid(row = 0, column = 2)    
    #mp4 path
    Label(root,text = "视频存储路径:",font = '宋体 -20 bold').grid(row = 1, column = 0)
    mp4_entry=Entry(root, textvariable = mp4_path,width = 100)
    mp4_entry.grid(row = 1, column = 1)
    mp4_entry.insert(0,'D:/fnn/mp4')
    Button(root, text = "选择", command = selectMp4,font = '宋体 -20 bold').grid(row = 1, column = 2)   
    #ffmpeg_path
    Label(root,text = "ffmpeg路径:",font = '宋体 -20 bold').grid(row = 2, column = 0)
    ffmpeg_entry=Entry(root, textvariable = ffmpeg_path,width = 100)
    ffmpeg_entry.grid(row = 2, column = 1)
    ffmpeg_entry.insert(0,'D:/ffmpeg-4.4-full_build')
    Button(root, text = "选择", command = selectFfmpeg,font = '宋体 -20 bold').grid(row = 2, column = 2)
    ###browsermob_path
    Label(root,text = "browsermob路径:",font = '宋体 -20 bold').grid(row = 3, column = 0)
    browsermob_entry=Entry(root, textvariable = browsermob_path,width = 100)
    browsermob_entry.grid(row = 3, column = 1)
    browsermob_entry.insert(0,'D:/browsermob-proxy-2.1.4')
    Button(root, text = "选择", command = selectBrowsermob,font = '宋体 -20 bold').grid(row = 3, column = 2)
    #run button
    run_but = Button(root, text = "设置完成,开始下载", command = lambda:thread_it(getPath),font = '宋体 -20 bold')
    #run_but = Button(root, text = "设置完成,开始下载", command = getPath,font = '宋体 -20 bold')
    run_but.grid(row = 4, column = 0,columnspan = 3, sticky="n")
    #textbox
    text_box = Text(root, height=22, width=130)
    text_box.grid(row = 5, column=0,columnspan = 3,sticky="nsw")
    #scroll
    scroll = Scrollbar()
    scroll.grid(row = 5,column= 2,sticky="nse")
    scroll.config(command=text_box.yview) # 将文本框关联到滚动条上,滚动条滑动,文本框跟随滑动
    text_box.config(yscrollcommand=scroll.set) # 将滚动条关联到文本框   
    Label(root,text = "中山大学",font = '宋体 -20 bold').grid(row = 6,column= 0,sticky="w")
    root.mainloop()
    
  


    
   
    
    

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值