scrapy——m3u8视频线程池下载(不写scrapy了)

上一章讲到视频的爬取,用HLS技术,但是有几个问题要注意,对于普通的视频文件,只要在response中可以直接获取视频地址url的,通常都是直接通过视频地址下载,无需在进一步获取m3u8。所以这一章里面,将完善这一流程。

爬取含url的视频

需要获取video标签同时,该标签里面要有src的。

import requests
from bs4 import BeautifulSoup

url = 'https://haokan.baidu.com/v?pd=wisenatural&vid=18398456915049280724'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
paragraphs = soup.find_all("video", attrs={"src": True})
for paragraph in paragraphs:
    response_url = requests.get(paragraph['src'])
	if response.status_code == 200:
        with open(os.path.join(r'E:\Temp\video', 'video.mp4'), 'wb') as f:
            f.write(response.content)

然而,这个网址存在反扒措施,返回的response中不存在video标签。暂时不解决这个网站的反爬措施了,只要拿到src网址,就可以直接下载了。原理就是这么个原理。

爬取含m3u8的视频

之前已经解决了m3u8文件的获取,还需要做到有几点:

  • 如果有解密的话,要进行解密;
  • 如果实在没有m3u8的文件的话,那么就提供别的路径,比如输入的是m3u8url等方式(因为找到一个网站,他的m3u8是很恶心的在javascript里面写的,根本搞不到,可能要独立写这个网站的爬虫代码)

打开开发者工具功能

selenium允许我们开启开发者工具选项,以监听网络请求、响应等内容。

from selenium import webdriver
options = webdriver.ChromeOptions()
options.binary_location = chrome_path
# ...其他内容
options.set_capability(
        "goog:loggingPrefs", {"performance": "ALL"}
    ) # 开启日志

添加这个选项就能开启日志了,到时候日志就可以记录并提取。

从某以地址获取m3u8并返回真实的m3u8网址

def capture_m3u8_urls(driver, target_url):
    """捕获页面所有M3U8请求的URL和Headers"""
    m3u8_requests = []
    driver.execute_cdp_cmd("Network.setCacheDisabled", {"cacheDisabled": True})
    # 清除浏览器缓存和Cookies
    driver.execute_cdp_cmd("Network.clearBrowserCache", {})
    driver.execute_cdp_cmd("Network.clearBrowserCookies", {})

    driver.get(target_url)
    WebDriverWait(driver, 30).until(
        lambda d: d.execute_script("return document.readyState === 'complete'")
    )

    logs_buffer = driver.get_log("performance")
    for entry in logs_buffer:
        log = json.loads(entry["message"])["message"]
        if log["method"] == "Network.requestWillBeSent":
            request = log["params"]["request"]
            if ".m3u8" in request["url"]:
                m3u8_requests.append({
                    "url": request["url"],
                    "Referer": request["headers"].get("Referer", {})
                })

    return m3u8_requests
  • 可以自行打印entry内容看看,有反爬的网址的src通常会想目标服务器再次进行发送片段请求,但是这里面可能存在某些是真实的,或者是虚假的url,但是我们要统统打包,之后再一一尝试。
  • 这里的referer是因为部分网站会验证你的请求头是否是正确的,就会验证这个字段,如果不存在这个字段,或者字段有问题的话,那么会直接返回403代码。

验证m3u8

为了怕搞到的m3u8是错误的,或者是伪造的,所以每个都验证一下。

# 验证ts是否是伪造的,数量是否足够
def validata_ts(ts_list, m3u8_url, headers):
    """验证TS有效性并提取视频数据"""
    try:
        status = True
        try_times = 5
        if len(ts_list) < 5:
            try_times = len(ts_list)
        for _ in range(try_times):
            i = random.randint(0, len(ts_list) - 2)
            _url = ts_list[i]
            if not _url.startswith("http"):
                ts_url = urljoin(m3u8_url, _url)
            else:
                ts_url = _url
            response = requests.get(ts_url, headers=headers, timeout=10)
            header = response.raw.read(4)
            if response.status_code != 200 or header[:2] == b'\xff\xd8':
                status = False

        if status:
            return m3u8_url
    except Exception as e:
        print(f"TS验证失败: {e}")
        time.sleep(2)
    return None

# 验证m3u8
def get_ts_list(url, headers, check_ts=True):
    """验证M3U8有效性并提取TS列表"""
    response = requests.get(url, headers=headers, timeout=10)
    if response.status_code == 200 and response.text.startswith("#EXTM3U"):
        ts_urls = re.findall(r'^[^#].*$', response.text, re.MULTILINE)
        if check_ts:
            return validata_ts(ts_urls, url, headers)
        else:
            key_info = {}
            # 提取密钥URI和IV
            pattern = r'#EXT-X-KEY:' \
                      r'(?:.*?METHOD=([A-Z0-9-]+))?' \
                      r'(?:.*?URI="([^"]+))?' \
                      r'(?:.*?IV=(0x[0-9a-fA-F]+))?' \
                      r'(?:.*?auth_key=([^"]+))?'
            for line in response.text.splitlines():
                if '#EXT-X-KEY' not in line:
                    continue
                match = re.search(pattern, line, re.IGNORECASE)
                key_info['method'] = match.group(1) if match and match.group(1) else None
                key_info['uri'] = match.group(2) if match and match.group(2) else None
                key_info['iv'] = match.group(3) if match and match.group(3) else None
                key_info['auth_key'] = match.group(4) if match and match.group(4) else None

            return key_info, ts_urls
    return None
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',} # 伪造headers
m3u8_list = capture_m3u8_urls(driver, url) # 拿个参数接一下
real_m3u8_urls = []
for m3u8 in m3u8_list:
	headers["Referer"] = m3u8["Referer"]
	_url = get_ts_list(m3u8["url"], headers, check_ts=True)
	if _url:
		real_m3u8_urls.append(_url)
  • 怕给的是假的,所以随机请求了几个ts内容,并验证他们是不是乱七八糟的文件。
  • 因为尝试获取m3u8地址和真实获取m3u8地址的两个代码很像,所以我将他们合并,并且额外添加了个参数,到时候直接调用同一套代码就行了,还是有优化空间,但是我没怎么想去优化了。
  • 部分网站的视频存在加密,所以m3u8文件会有#EXT-X-KEY开头的内容,不然到时候合并了就无法打开了。

下载ts文件

当有且仅有一个m3u8文件的时候,我们就直接下载,当有多个m3u8文件的时候,我们就让用户手动选择。

if len(real_m3u8_urls) == 1:
	process_video(real_m3u8_urls[0], headers)
	print("视频处理成功!")
elif len(real_m3u8_urls) > 1:
	print("存在多个有效的M3U8文件,请手动选择")
	for i in range(0,len(real_m3u8_urls)):
		print(f"【{i}】:{real_m3u8_urls[i]}")
	user_input = input("请输入要处理的M3U8文件编号:")
	if user_input.isdigit() and int(user_input) < len(real_m3u8_urls):
		process_video(real_m3u8_urls[int(user_input)], headers)
		print("视频处理成功!")
	else:
		print("输入无效,请重新运行程序")
else:
	print("未找到有效的M3U8文件")

然后是处理video

from concurrent.futures import ThreadPoolExecutor, as_completed
from alive_progress import alive_bar
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from selenium.webdriver.support.ui import WebDriverWait

def download_ts(ts, m3u8_url, headers, idx, cipher=None):
    """下载TS片段"""
    for _ in range(3):
        try :
            if not ts.startswith("http"):
                ts_url = urljoin(m3u8_url, ts)
            else:
                ts_url = ts
            response = requests.get(ts_url, headers=headers, stream=True)
            response.raise_for_status()
            if response.status_code == 200:
                if cipher:
                    data = unpad(cipher.decrypt(response.content), AES.block_size)
                    # 如果加密了,就进行解密。
                else:
                    data = response.content
                return idx, BytesIO(data)
        except Exception as e:
            print(f"下载TS失败: {e}, 重试中...")
            time.sleep(2)
    return idx, None

def process_video(real_m3u8_url, headers, max_workers=5):
    """处理视频"""
    key_info, ts_list = get_ts_list(real_m3u8_url, headers, check_ts=False)
    if not ts_list:
        print("无效的M3U8文件")
        return None
    cipher = None
    if key_info['uri'] and key_info['iv']:
        key = download_key(key_info['uri'], key_info['auth_key'])
        if key_info['method'] == 'AES-128':
            iv_bytes = bytes.fromhex(key_info['iv'][2:]) if key_info['iv'].startswith('0x') else key_info['iv'].encode()
            # 初始化解密器
            cipher = AES.new(key, AES.MODE_CBC, iv=iv_bytes)
        else:
            cipher = None

    with alive_bar(len(ts_list), title="下载进度", bar="blocks",) as total_bar:
        results = [None] * len(ts_list)
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {executor.submit(download_ts, ts, real_m3u8_url, headers, idx, cipher): ts for idx, ts in enumerate(ts_list)}
            for future in as_completed(futures):
                idx, data = future.result()
                results[idx] = data
                total_bar.text(f"▶ {futures[future][:15]}...")
                total_bar()

    # merge ts
    valid_data = [d for d in results if d is not None]
    if valid_data:
        print("开始合并TS文件.")
        merge_ts(valid_data)
        print(f"成功合并 {len(valid_data)}/{len(ts_list)} 个分片")
    else:
        print("无有效分片可合并")
  • 由于多进程下载,他可能乱排序,所以提前安排了他们的顺序results = [None] * len(ts_list)
  • 解密目前只支持key_info['method'] == 'AES-128'这个,要是有其他的解密的话,还需要自行探索,反正方向很好找。
  • 剩下的就是合并了。

仅m3u8地址下载。

由于实在是没办法从某一页面获取真实的m3u8,没办法,只能配合cococut搞到m3u8的地址进行下载了

m3u8_url = ''
m3u8_pattern = re.compile(r'\.m3u8?(\?.*)?(#.*)?$', re.I)
real_m3u8_urls = []
if re.match(m3u8_pattern, m3u8_url):
	real_m3u8_urls.append(m3u8_url)
	headers["Referer"] = ""
  • headers要自己伪造一下

就酱~~

补充

selenium的动作还是很不错的,但是有时候配置不够,还是会被网站的反爬机制检测到,而注释掉options.add_argument('--headless')无头模式之后,反爬机制的检测就会失效,所以我们还要添加一些选项:

options = webdriver.ChromeOptions()
options.binary_location = chrome_path
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--headless')
options.add_argument('--disable-gpu')  # Windows 无头模式可能需要
options.add_argument(f'--user-data-dir={profile_path}')
options.add_argument(f'--disk-cache-dir={cache_path}')
options.add_argument("--disable-blink-features=AutomationControlled")  # 禁用自动化控制提示
options.add_experimental_option("excludeSwitches", ["enable-automation"])  # 隐藏自动化标志
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
options.add_argument(f"user-agent={user_agent}")
options.add_argument("--window-size=1920,1080")
    # 启用 WebGL 和 GPU 加速(部分反爬检测 GPU 支持)
options.add_argument("--use-gl=desktop")
options.add_argument("--enable-webgl")

上面这些选项,我测试过,是可以用的,可以绕过检测~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值