requests.get().content返回内容不稳定,提取YouTube Clip视频ID报错
问题描述
我写了一个Python函数用来抓取YouTube Clip页面源码,提取其中的startTimeMs、endTimeMs和videoId信息。下面是提取videoId的代码段:
class className(): def __init__(self, link) # 发送请求 self.r = requests.get(link) def originalVideoID(self): # 获取源码 source = str(self.r.content) # videoID的前后标识 start = "\"videoDetails\":{\"videoId\":\"" end = '\"' # 截取videoDetails之后的内容 videoID = source.split(start)[1] # 截取引号之前的内容 videoID = videoID.split(end)[0]
预期与实际结果
- 预期:传入YouTube Clip链接(比如
https://www.youtube.com/clip/UgkxU2HSeGL_NvmDJ-nQJrlLwllwMDBdGZFs)时,能稳定获取到videoId为NiXD4xVJM5Y。 - 实际:有时能得到正确结果,有时会触发
IndexError。
调试情况
添加start in source判断后发现,报错时该判断返回False,打印str(self.r.content)能看到返回的源码和预期完全不同。
错误栈
Traceback (most recent call last): File "PATHTOPROJECT\FILENAME.py", line 383, in <module> main() File "PATHTOPROJECT\FILENAME.py", line 165, in download_video downloadLink = className(link).originalVideoID() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "PATHTOPROJECT\FILENAME.py", line 67, in originalVideoID videoID = source.split(start)[1] ~~~~~~~~~~~~~~~~~~~^^^ IndexError: list index out of range
目标数据位置
目标数据位于页面源码的如下script块中:
<script nonce="601b9hyYx1NEaPf0pQewqA"> var ytInitialPlayerResponse = { ... "videoDetails": { "videoId":"NiXD4xVJM5Y", ... }, ... "clipConfig": { "postId": ... ,"startTimeMs":"0","endTimeMs":"15000" } } </script>
想问下我哪里错了?是不是应该用selenium这类工具,还是我的requests用法有问题,需要换实现方式?
问题分析与解决思路
核心问题原因
- 反爬拦截:YouTube会检测请求的合法性,直接用
requests.get()发送请求时,缺少浏览器标识(User-Agent)等必要头信息,容易被判定为爬虫,返回反爬页面或跳转页面,导致目标字符串不存在,触发索引错误。 - 字符串拆分的脆弱性:用
split()硬拆字符串的方式极度依赖固定格式,一旦YouTube微调页面源码的字符格式(比如引号转义、空格变化),代码就会直接失效。
解决方案
方案1:优化requests请求,添加请求头
先给请求加上模拟浏览器的头信息,大概率能解决反爬拦截问题:
import requests class YouTubeClipParser(): def __init__(self, link): # 添加模拟浏览器的请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36' } self.r = requests.get(link, headers=headers) # 用response.text替代str(r.content),避免编码问题 self.source = self.r.text def originalVideoID(self): start = '"videoDetails":{"videoId":"' end = '"' if start not in self.source: raise ValueError("无法找到videoId,可能被反爬拦截") videoID_part = self.source.split(start)[1] videoID = videoID_part.split(end)[0] return videoID # 扩展提取时间戳的方法 def get_clip_timestamps(self): import re start_match = re.search(r'"startTimeMs":"(\d+)"', self.source) end_match = re.search(r'"endTimeMs":"(\d+)"', self.source) if not start_match or not end_match: raise ValueError("无法找到时间戳信息") return start_match.group(1), end_match.group(1)
方案2:改用正则表达式匹配(更健壮)
比起硬拆字符串,用正则匹配目标字段的方式更能适应源码的小变动,提取videoId也可以改成正则:
def originalVideoID(self): import re match = re.search(r'"videoDetails":{"videoId":"([a-zA-Z0-9_-]+)"', self.source) if not match: raise ValueError("未找到videoId") return match.group(1)
方案3:解析ytInitialPlayerResponse的JSON数据
目标数据在ytInitialPlayerResponse这个JS变量里,直接提取该变量的JSON内容并解析,能一次性拿到所有需要的字段,这是最可靠的方式:
def get_all_data(self): import re import json # 匹配ytInitialPlayerResponse的JSON内容 match = re.search(r'var ytInitialPlayerResponse = (\{.*?\});', self.source, re.DOTALL) if not match: raise ValueError("未找到ytInitialPlayerResponse数据") json_str = match.group(1) # 处理JS语法兼容问题(比如末尾多余逗号) json_str = re.sub(r',\s*([}\]])', r'\1', json_str) data = json.loads(json_str) return { 'videoId': data['videoDetails']['videoId'], 'startTimeMs': data['clipConfig']['startTimeMs'], 'endTimeMs': data['clipConfig']['endTimeMs'] }
关于是否用Selenium
如果YouTube反爬机制升级,比如需要JS渲染才能加载数据,或者频繁请求被封IP,再考虑用Selenium模拟真实浏览器操作。一般情况下,优化requests请求+正则/JSON解析就足够解决问题,Selenium更重、速度也慢。
内容的提问来源于stack exchange,提问作者futium
相关产品推荐
相关产品推荐

