上一章讲到视频的爬取,用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
的文件的话,那么就提供别的路径,比如输入的是m3u8
的url
等方式(因为找到一个网站,他的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")
上面这些选项,我测试过,是可以用的,可以绕过检测~