Python爬虫实战:从片库网抓取m3u8视频并合并为MP4
1. 理解m3u8视频流的工作原理m3u8是一种基于HTTP Live StreamingHLS协议的视频流格式现在被广泛应用于各类视频网站。我第一次接触这种格式时也很困惑直到后来才发现它其实就像一本视频目录——m3u8文件本身并不包含视频内容而是记录了所有视频片段的地址信息。具体来说当你播放一个m3u8视频时播放器会先下载这个索引文件然后根据里面的地址逐个下载.ts格式的视频片段。这些片段通常只有几秒钟的长度播放器会在本地将它们拼接起来实现流畅播放。这种设计有三大优势适应不同网络环境可以根据带宽自动切换不同质量的视频流支持断点续传某个片段下载失败只需重试该片段便于CDN分发不同用户可以从不同服务器获取片段在实际抓取过程中我们经常会遇到二级m3u8结构。第一级m3u8通常包含不同清晰度的播放列表而第二级m3u8才是真正的ts片段索引。就像去图书馆找书先要找到正确的书架第一级m3u8然后才能拿到具体的图书第二级m3u8中的ts片段。2. 准备工作与环境搭建在开始编写爬虫之前我们需要准备好Python环境。我推荐使用Python 3.8版本太老的版本可能会遇到依赖问题。以下是需要安装的关键库及其作用pip install requests beautifulsoup4 pycryptodomerequests用于发送HTTP请求获取网页内容beautifulsoup4解析HTML页面结构pycryptodome有些网站会对ts片段加密需要这个库解密建议创建一个专门的虚拟环境来管理这些依赖python -m venv m3u8_env source m3u8_env/bin/activate # Linux/Mac m3u8_env\Scripts\activate # Windows对于开发工具我习惯使用VS Code配合Python插件它的调试功能非常方便。特别是处理m3u8这种多步骤流程时能够单步调试查看变量状态很重要。3. 分析目标网站结构以片库网为例我们先来看它的搜索机制。通过浏览器开发者工具F12观察可以发现搜索请求的URL构造很有规律base_url http://tv.cnco.me/ search_keyword 脱单告急 search_url f{base_url}vodsearch/-------------.html?wd{search_keyword}submit这里有几个需要注意的点中文关键词需要确保正确编码requests库会自动处理有些网站会检测Referer等header需要模拟浏览器行为搜索结果页面的HTML结构可能会变动需要定期检查获取到搜索结果后我们用BeautifulSoup提取播放页链接。关键是要找到正确的CSS选择器def find_play_url(search_url): r requests.get(search_url) r.encoding utf-8 soup BeautifulSoup(r.text, lxml) # 根据实际页面结构调整选择器 play_link soup.select_one(.margin-0 a)[href] return base_url play_link4. 提取m3u8播放地址进入播放页面后真正的挑战才开始。现代视频网站通常不会直接把m3u8地址放在HTML里而是通过JavaScript动态加载。这时候我们需要打开开发者工具的Network面板过滤XHR或Media类型的请求寻找包含.m3u8的请求如果找不到直接请求可能需要解析页面中的JavaScript代码。我遇到过最复杂的情况是地址被Base64编码后藏在某个JS变量里。这时候可以尝试以下方法def extract_m3u8_from_script(html): soup BeautifulSoup(html, lxml) scripts soup.find_all(script, typetext/javascript) for script in scripts: content str(script.string) if player_data in content: # 使用正则提取JSON数据 import re match re.search(rvar player_data(.*?});, content) if match: player_data eval(match.group(1)) return player_data[url].replace(\\, ) return None5. 处理多级m3u8文件获取到第一级m3u8后我们需要解析它的内容。第一级m3u8通常长这样#EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID1,BANDWIDTH800000 playlist_800k.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID1,BANDWIDTH1200000 playlist_1200k.m3u8我们需要选择合适码率的播放列表继续处理。有时候网站会使用相对路径需要手动拼接完整URLdef process_m3u8_level1(m3u8_url): r requests.get(m3u8_url) lines r.text.splitlines() base_url m3u8_url[:m3u8_url.rfind(/)1] for line in lines: if line.startswith(#EXT-X-STREAM-INF): # 这里可以解析BANDWIDTH选择清晰度 pass elif line.endswith(.m3u8): return base_url line第二级m3u8包含实际的ts片段列表#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.000000, segment000.ts #EXTINF:10.000000, segment001.ts6. 多线程下载ts片段下载大量小文件时单线程效率太低。我推荐使用concurrent.futures线程池from concurrent.futures import ThreadPoolExecutor def download_ts_segments(ts_urls, save_dirts_segments): if not os.path.exists(save_dir): os.makedirs(save_dir) def download_single(url): try: filename url.split(/)[-1] path os.path.join(save_dir, filename) r requests.get(url, timeout10) with open(path, wb) as f: f.write(r.content) return True except Exception as e: print(f下载失败 {url}: {str(e)}) return False with ThreadPoolExecutor(max_workers8) as executor: results list(executor.map(download_single, ts_urls)) success_rate sum(results)/len(results) print(f下载完成成功率: {success_rate:.2%})实际使用中还需要注意设置合理的超时时间建议10-30秒添加重试机制失败3次后放弃显示下载进度可以用tqdm库7. 合并ts文件为MP4下载完所有ts片段后我们需要将它们合并成一个完整的视频。最简单的方法是使用二进制拼接def combine_ts_to_mp4(ts_dir, output_file): ts_files sorted([f for f in os.listdir(ts_dir) if f.endswith(.ts)], keylambda x: int(x.replace(segment, ).replace(.ts, ))) with open(output_file, wb) as out_f: for ts_file in ts_files: with open(os.path.join(ts_dir, ts_file), rb) as in_f: out_f.write(in_f.read()) print(f合并完成: {output_file})如果视频有音频可能需要使用ffmpeg进行更专业的处理ffmpeg -i concat:segment000.ts|segment001.ts -c copy output.mp4在Python中可以通过subprocess调用ffmpegimport subprocess def ffmpeg_combine(ts_dir, output_file): ts_files sorted([f for f in os.listdir(ts_dir) if f.endswith(.ts)]) with open(file_list.txt, w) as f: for ts_file in ts_files: f.write(ffile {os.path.join(ts_dir, ts_file)}\n) subprocess.run([ ffmpeg, -f, concat, -i, file_list.txt, -c, copy, output_file ])8. 常见问题与解决方案在实际操作中我遇到过不少坑这里分享几个典型问题的解决方法ts片段下载失败检查URL是否正确拼接尝试添加Referer等header模拟浏览器有些网站会定期更换ts路径需要重新获取m3u8合并后的视频无法播放确保ts片段按正确顺序合并尝试用ffmpeg重新编码检查视频编码格式H.264通常兼容性最好遇到加密视频在m3u8文件中查找#EXT-X-KEY字段可能需要解密key和IV参数使用pycryptodome进行AES解密from Crypto.Cipher import AES def decrypt_ts_file(encrypted_path, decrypted_path, key, iv): with open(encrypted_path, rb) as enc_file: encrypted_data enc_file.read() cipher AES.new(key, AES.MODE_CBC, iviv) decrypted_data cipher.decrypt(encrypted_data) with open(decrypted_path, wb) as dec_file: dec_file.write(decrypted_data)反爬虫机制使用随机User-Agent控制请求频率建议每个请求间隔1-3秒考虑使用代理IP池最后提醒大家爬取视频时要遵守网站的使用条款不要用于商业用途。技术本身是中性的关键在于如何使用。我在实际项目中发现理解这些原理不仅对爬虫有帮助对日常的视频处理工作也大有裨益。