ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium爬YouTube与TikTok:动态渲染页面的合规采集方案

Selenium爬YouTube与TikTok:动态渲染页面的合规采集方案 简介本资源是一个基于Python与Selenium实现的YouTube和TikTok双平台数据爬虫项目面向爬虫初学者、自动化测试入门者及社交媒体数据分析需求者解决动态网页内容抓取难、反爬验证绕过复杂等实际问题。项目共9个文件含8个核心Python脚本如tiktok_fetch.py、youtube_KeyFetch.py、YoutubeMain.py等分别承担主控调度、关键词搜索、长/短视频抓取、TikTok视频提取等模块功能另含1个.gitignore配置文件压缩包仅18KB轻量易读便于快速理解整体架构与分工逻辑。目前已有553人学习下载反映出较强的学习热度与实践参考价值。读者可直接复用各模块脚本掌握Selenium模拟浏览器行为、处理滑块/登录验证、解析动态加载内容等关键能力并结合Pandas等库拓展数据清洗与存储为市场调研、舆情分析或教学演示提供可运行、可调试的完整工程范例。1. 为什么用 Selenium 爬 YouTube 和 TikTok 不是“偷懒”而是不得不做的技术妥协你写好一段 requests BeautifulSoup 的爬虫本地跑通了一上线就 403加了 headers、cookie、user-agent还是秒封换上代理池IP 频繁失效请求延迟飙到 8s 以上——这不是你代码写得差而是 YouTube 和 TikTok 早已把「静态 HTML 抓取」这条路焊死了。它们的首页、推荐流、评论区、视频详情页95% 以上内容由 React/Vue 动态渲染数据藏在 XHR 接口里而这些接口又层层校验x-youtube-client-version、x-tt-params、device_id、sig、captcha_token……全靠前端 JS 运行时生成。这时候Selenium 不是“重武器”而是唯一能模拟真实用户行为、绕过前端反爬逻辑的最小可行执行环境。它不是为自动化测试而生但恰恰因为要测“人怎么点、怎么滑、怎么等加载”反而成了目前最稳定、可调试、可复现的 YouTube/TikTok 数据采集入口。适合两类人一是需要获取公开视频标题/描述/点赞数/评论趋势做竞品分析的产品运营二是搭建短视频选题库、带货素材库、舆情监测底座的数据工程师。注意本文不涉及登录态、私域数据、下载视频文件只聚焦公开页面结构化数据的可持续抓取——这是合规边界内一线团队真正在跑的方案。2. 从零搭起可复用的 Selenium 环境ChromeDriver 无头模式 反检测配置2.1 为什么必须用 ChromeDriver 而非直接调用 Chrome 浏览器Selenium 本身不提供浏览器它只是协议客户端。YouTube 和 TikTok 对 Chromium 内核版本极其敏感YouTube 2024 年 Q2 起强制校验navigator.webdriver false旧版 ChromeDriverv113 以下默认暴露webdriver: true直接触发403 ForbiddenTikTok 的tt-webapp框架会读取window.chrome、navigator.permissions.query、navigator.plugins.length等指纹字段未抹除则返回空 JSON 或跳转验证码页。所以不能简单pip install selenium chrome就完事。必须精确匹配 Chrome 浏览器主版本并启用--disable-blink-featuresAutomationControlled等 7 项启动参数。常见错误是直接webdriver.Chrome()结果连首页都打不开——那不是网络问题是被前端 JS 直接拦截了。2.2 安装与版本对齐三步锁定稳定组合提示不要用chromedriver-autoinstaller类自动工具。它常安装错版本且无法控制启动参数。手动配才是生产级做法。# 1. 查看本机 Chrome 版本Mac/Linux 终端Windows 在 cmd 中 google-chrome --version # 输出示例Google Chrome 126.0.6478.126 # 2. 下载严格匹配的 ChromeDriver官网https://chromedriver.chromium.org/ # 注意126.0.x 对应 ChromeDriver v126.0.6478.126必须小数点后三位完全一致 # 解压后得到 chromedriver 二进制文件放入项目根目录 ./drivers/chromedriver # 3. Python 初始化配置关键7 个反检测参数缺一不可 from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service options Options() options.add_argument(--headless) # 无头模式服务器必备 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-gpu) options.add_argument(--disable-extensions) options.add_argument(--disable-blink-featuresAutomationControlled) # 核心隐藏 webdriver 标识 options.add_experimental_option(excludeSwitches, [enable-automation]) # 移除 Chrome 正受到自动测试软件控制 提示 options.add_experimental_option(useAutomationExtension, False) # 关键一步注入 JS 覆盖 navigator.webdriver 属性 options.add_argument(--disable-blink-featuresAutomationControlled) service Service(./drivers/chromedriver) driver webdriver.Chrome(serviceservice, optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }) })这段代码不是“锦上添花”而是能否拿到数据的生死线。我见过太多团队卡在driver.get(https://www.youtube.com/)后白屏 30 秒超时查日志发现是前端 JS 检测到navigator.webdriver true直接throw new Error(Bot detected)。上面的 CDP 注入必须在get()之前执行且仅对新文档生效——这就是为什么不能写在get()之后。2.3 验证环境是否真正“隐身”三行代码自检# 执行后以下三项必须全部返回 True否则后续请求必失败 print(driver.execute_script(return window.navigator.webdriver)) # 应输出 None 或 undefined print(driver.execute_script(return chrome in window.navigator)) # 应输出 TrueTikTok 依赖此 print(driver.execute_script(return window.chrome ! undefined)) # 应输出 TrueYouTube 依赖此 # 补充检查访问 youtube.com 后查看页面 title driver.get(https://www.youtube.com/) print(driver.title) # 正常应为 YouTube若为 Checking your browser 则说明反检测失败如果navigator.webdriver返回True或 title 是验证页说明 CDP 注入时机错误或参数缺失。此时不要往下写爬虫逻辑——先修环境否则所有 XPath 都是空中楼阁。3. YouTube 页面解析从首页推荐流到视频详情页的 DOM 定位策略3.1 为什么不能用find_element(By.TAG_NAME, ytd-video-renderer)直接抓列表YouTube 的 DOM 结构是典型的「懒加载 动态插入」首页初始只渲染前 20 个视频卡片滚动到底部时JS 动态创建新的ytd-video-renderer元素并 append 到 DOM这些元素没有idclass 名含随机哈希如style-scope ytd-video-rendererXPath 极不稳定更致命的是ytd-video-renderer是 Web Component其 shadow-root 内部结构需通过shadow_rootAPI 访问普通find_element无法穿透。所以必须用「滚动 等待 Shadow DOM 解析」三步法。这是 YouTube 爬虫区别于其他网站的核心难点。3.2 可复用的首页视频卡片提取函数含 Shadow DOM 支持from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time def extract_youtube_home_videos(driver, max_videos50): 从 YouTube 首页提取视频卡片信息 :param driver: 已初始化的 WebDriver 实例 :param max_videos: 最大提取数量防无限滚动 :return: list of dict, keys: title, channel, views, publish_time, video_url videos [] last_height driver.execute_script(return document.documentElement.scrollHeight) while len(videos) max_videos: # 1. 滚动到底部触发加载 driver.execute_script(window.scrollTo(0, document.documentElement.scrollHeight);) time.sleep(2) # 给 JS 渲染留出时间 # 2. 等待新卡片出现用通用 selector避开 shadow-root try: # YouTube 卡片外层容器是 ytd-rich-grid-row内部视频链接在 a#thumbnail cards WebDriverWait(driver, 5).until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, ytd-rich-grid-row a#thumbnail)) ) except: break # 没有新卡片了 # 3. 遍历每个卡片提取信息避开 shadow-root用 CSS 选择器直达 a 标签 for card in cards[len(videos):]: # 防重复提取 try: # 视频 URLa 标签 href url card.get_attribute(href) if not url or watch?v not in url: continue # 标题在 card 内部的 h3 标签 title_elem card.find_element(By.XPATH, ./ancestor::ytd-rich-grid-row//h3/a) title title_elem.text.strip() if title_elem else # 作者和播放量在 card 下方的 ytd-channel-name 和 ytd-video-meta-block meta_block card.find_element(By.XPATH, ./ancestor::ytd-rich-grid-row//ytd-video-meta-block) channel meta_block.find_element(By.ID, channel-name).text.strip() views_and_time meta_block.find_element(By.ID, metadata-line).text.strip() # views_and_time 示例1.2M views · 3 days ago views views_and_time.split(·)[0].strip() if · in views_and_time else videos.append({ title: title, channel: channel, views: views, video_url: https://www.youtube.com url, publish_time: views_and_time.split(·)[-1].strip() if · in views_and_time else }) if len(videos) max_videos: break except Exception as e: continue # 单条失败不影响整体 # 检查是否到底部 new_height driver.execute_script(return document.documentElement.scrollHeight) if new_height last_height: break last_height new_height return videos # 使用示例 driver.get(https://www.youtube.com/) videos extract_youtube_home_videos(driver, max_videos30) print(f成功提取 {len(videos)} 条视频数据)这个函数的关键在于不依赖ytd-video-renderer而是用a#thumbnail作为锚点因为该元素始终在 light DOM 中且稳定存在用./ancestor::ytd-rich-grid-row向上定位避免因 shadow-root 导致的查找失败time.sleep(2)不可省略YouTube 的懒加载有防抖逻辑太快滚动会导致 JS 未触发cards[len(videos):]切片防止重复提取已处理过的卡片。3.3 视频详情页数据提取避开动态加载陷阱进入单个视频页如https://www.youtube.com/watch?vxxx后关键数据点赞数、评论数、描述并非初始 HTML 加载而是通过 XHR 异步拉取。直接driver.page_source只能得到骨架真实数据藏在ytd-watch-flexy的 shadow-root 里。正确做法是等待#info区域出现并用execute_script获取其 innerTextdef extract_youtube_video_detail(driver, video_url): driver.get(video_url) # 等待 info 区域加载包含标题、描述、点赞数 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, #info)) ) # 用 JS 获取 shadow-root 内容YouTube 2024 年已开放 access try: info_root driver.execute_script( return document.querySelector(#info).shadowRoot ) # 从 shadow-root 中提取文本 title info_root.find_element(By.ID, title).text.strip() description info_root.find_element(By.ID, description).text.strip() # 点赞数在 #segmented-like-button 中 likes_elem info_root.find_element(By.ID, segmented-like-button) likes likes_elem.text.strip() if likes_elem else N/A return { title: title, description: description, likes: likes, url: video_url } except Exception as e: # 若 shadow-root 访问失败回退到 light DOM 的备用方案 title driver.find_element(By.XPATH, //h1[classtitle]).text.strip() description driver.find_element(By.XPATH, //div[iddescription]).text.strip() return {title: title, description: description, likes: N/A, url: video_url}注意YouTube 的 shadow-root 访问权限在 2024 年已开放但需确保 ChromeDriver ≥ v124。低于此版本会报Cannot read property shadowRoot of null。4. TikTok 页面解析应对无限滚动与动态路由的双层解构法4.1 为什么 TikTok 比 YouTube 更难URL 不反映真实内容TikTok 的首页/是 SPA单页应用所有视频流都由同一个 URLhttps://www.tiktok.com/渲染真实内容由window.__INIT_PROPS__注入的 JSON 驱动。这意味着driver.current_url始终是https://www.tiktok.com/无法通过 URL 区分不同视频视频卡片 DOM 是div classcss-1k5p7e0-DivVideoCardContainer但 class 名含随机字符串XPath 失效滚动时新卡片不是 append而是替换已有节点find_elements会返回 stale element reference。解决方案放弃 DOM 定位转向 JavaScript 内存数据提取。4.2 从__INIT_PROPS__提取原始数据TikTok 爬虫的黄金路径TikTok 在页面加载时将首屏视频数据序列化为全局变量window.__INIT_PROPS__格式为{props: {pageProps: {...}}}。该对象包含完整视频列表、作者信息、互动数且无需解析 DOM。这是最稳定、最快、最抗反爬的方式。def extract_tiktok_home_videos(driver, max_videos50): 从 TikTok 首页提取视频数据基于 __INIT_PROPS__ :param driver: WebDriver 实例 :param max_videos: 最大提取数 :return: list of dict, keys: id, desc, author, digg_count, comment_count, share_count driver.get(https://www.tiktok.com/) # 等待页面加载完成TikTok 会设置 window._WEB_INIT_PROPS_ WebDriverWait(driver, 15).until( lambda d: d.execute_script(return typeof window._WEB_INIT_PROPS_ ! undefined) ) videos [] # 第一次提取初始数据 init_props driver.execute_script(return window._WEB_INIT_PROPS_;) if init_props and props in init_props and pageProps in init_props[props]: items init_props[props][pageProps].get(itemList, []) for item in items[:max_videos]: videos.append({ id: item.get(id, ), desc: item.get(desc, ), author: item.get(author, {}).get(uniqueId, ), digg_count: item.get(stats, {}).get(diggCount, 0), comment_count: item.get(stats, {}).get(commentCount, 0), share_count: item.get(stats, {}).get(shareCount, 0), video_url: fhttps://www.tiktok.com/{item.get(author, {}).get(uniqueId, )}/video/{item.get(id, )} }) # 滚动加载更多TikTok 滚动后会更新 _WEB_INIT_PROPS_ last_len len(videos) for _ in range(5): # 最多滚动 5 次 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(3) # 检查 _WEB_INIT_PROPS_ 是否更新 try: new_props driver.execute_script(return window._WEB_INIT_PROPS_;) if new_props and props in new_props and pageProps in new_props[props]: new_items new_props[props][pageProps].get(itemList, []) # 只添加新出现的视频按 id 去重 new_ids set([v[id] for v in videos]) for item in new_items: if item.get(id) not in new_ids: videos.append({ id: item.get(id, ), desc: item.get(desc, ), author: item.get(author, {}).get(uniqueId, ), digg_count: item.get(stats, {}).get(diggCount, 0), comment_count: item.get(stats, {}).get(commentCount, 0), share_count: item.get(stats, {}).get(shareCount, 0), video_url: fhttps://www.tiktok.com/{item.get(author, {}).get(uniqueId, )}/video/{item.get(id, )} }) new_ids.add(item.get(id)) if len(videos) max_videos: break if len(videos) max_videos: break except: pass if len(videos) last_len: # 无新增提前退出 break last_len len(videos) return videos[:max_videos] # 使用示例 tiktok_videos extract_tiktok_home_videos(driver, max_videos30) print(fTikTok 提取 {len(tiktok_videos)} 条视频)这个方法的优势零 DOM 依赖不受 class 名变化、shadow-root、动态替换影响数据更全itemList包含点赞、评论、分享、作者 ID、音乐 ID、标签等原始字段速度快JS 执行比 XPath 查找快 3~5 倍稳定性高_WEB_INIT_PROPS_是 TikTok 官方用于 SSR 的变量长期不会删除。4.3 搜索页与话题页适配URL 参数驱动的 props 切换TikTok 的搜索页https://www.tiktok.com/search?qxxx和话题页https://www.tiktok.com/tag/xxx同样使用_WEB_INIT_PROPS_但数据结构略有不同搜索页pageProps.searchResult下的itemList话题页pageProps.hashtagPage下的itemList。只需修改提取路径即可复用同一套逻辑# 搜索页提取 def extract_tiktok_search_videos(driver, keyword, max_videos30): driver.get(fhttps://www.tiktok.com/search?q{keyword}) WebDriverWait(driver, 15).until( lambda d: d.execute_script(return typeof window._WEB_INIT_PROPS_ ! undefined) ) props driver.execute_script(return window._WEB_INIT_PROPS_;) items props[props][pageProps].get(searchResult, {}).get(itemList, []) # ... 同上处理 items5. 避坑指南YouTube 和 TikTok 爬虫的 5 个血泪现场5.1 现象YouTube 页面打开后空白Network Tab 显示大量 403原因ChromeDriver 版本与 Chrome 浏览器不匹配导致navigator.webdriver无法被覆盖前端 JS 直接拦截。解决严格按chrome --version输出下载对应小版本号的 ChromeDriver确认execute_cdp_cmd在get()之前执行用driver.execute_script(return navigator.webdriver)验证返回值为None。5.2 现象TikTok 滚动后itemList不更新始终只有首屏 20 条原因滚动速度过快TikTok 的防抖逻辑未触发新请求或未等待_WEB_INIT_PROPS_重新赋值。解决每次滚动后time.sleep(3)用WebDriverWait等待window._WEB_INIT_PROPS_变化可用driver.execute_script(return JSON.stringify(window._WEB_INIT_PROPS_))做轮询比对。5.3 现象XPath 定位//ytd-video-renderer报NoSuchElementException原因ytd-video-renderer在 shadow-root 内普通 XPath 无法穿透且该标签名在新版 YouTube 中已弃用改用ytd-rich-item-renderer。解决放弃直接定位 Web Component改用a#thumbnail或#contents ytd-rich-grid-row等 light DOM 锚点或用execute_script读取document.querySelector(ytd-rich-grid-row).shadowRoot。5.4 现象TikTok 提取的digg_count为 0但页面显示有 10W 点赞原因itemList中的stats字段是服务端预估值非实时数据真实点赞数藏在#main-content-others-home-page的 shadow-root 里。解决对单个视频页用driver.execute_script(return document.querySelector(#main-content-others-home-page).shadowRoot.querySelector(#like-count).innerText)提取实时数但注意这会大幅降低速度建议仅对重点视频做补充。5.5 现象程序运行 2 小时后突然全部 403重启即恢复原因TikTok/Youtube 的 IP 行为指纹累积同一 IP 短时间内高频请求10 次/分钟触发风控。解决加入time.sleep(random.uniform(3, 8))随机延时每 50 次请求后driver.quit()并重建 WebDriver释放 socket 和 cookies严禁使用公共代理池TikTok 已将主流代理 IP 段加入黑名单效果不如不用。注意所有延时、重启、IP 轮换策略目的不是“绕过封禁”而是模拟真实用户行为节奏。这是合规采集的底线。6. 数据落库与工程化用 SQLAlchemy SQLite 做轻量级持久化附去重与增量逻辑6.1 为什么选 SQLite 而非 MySQL/PostgreSQLYouTube/TikTok 爬虫的典型场景是单机运行、每日采集 1k~5k 条、数据只读分析、无并发写入。此时 SQLite 的优势凸显零配置pip install sqlalchemy后create_engine(sqlite:///data.db)即可用文件级备份整个数据库就是一个.db文件cp data.db backup.db即完成备份ACID 保障即使程序崩溃已提交事务不丢失兼容 ORMSQLAlchemy 的declarative_base写法与 PostgreSQL 完全一致未来迁移成本趋近于零。而 MySQL 需额外维护服务进程、用户权限、连接池对爬虫这种短生命周期任务纯属冗余。6.2 建表与去重用INSERT OR IGNORE实现幂等写入from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime import hashlib Base declarative_base() class YouTubeVideo(Base): __tablename__ youtube_videos id Column(String(50), primary_keyTrue) # video_id如 dQw4w9WgXcQ title Column(Text) channel Column(String(100)) views Column(String(20)) publish_time Column(String(50)) video_url Column(String(200), uniqueTrue) created_at Column(DateTime, defaultdatetime.now) class TikTokVideo(Base): __tablename__ tiktok_videos id Column(String(50), primary_keyTrue) # video_id desc Column(Text) author Column(String(100)) digg_count Column(Integer, default0) comment_count Column(Integer, default0) share_count Column(Integer, default0) video_url Column(String(200), uniqueTrue) created_at Column(DateTime, defaultdatetime.now) # 初始化数据库 engine create_engine(sqlite:///crawler_data.db, echoFalse) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session() # 插入 YouTube 数据自动去重 def save_youtube_videos(videos): for v in videos: # 从 URL 提取 video_idhttps://www.youtube.com/watch?vdQw4w9WgXcQ → dQw4w9WgXcQ video_id v[video_url].split(v)[-1].split()[0] record YouTubeVideo( idvideo_id, titlev[title], channelv[channel], viewsv[views], publish_timev[publish_time], video_urlv[video_url] ) try: session.add(record) session.commit() except Exception as e: session.rollback() # 主键冲突时回滚不中断流程 print(f跳过重复视频 {video_id}) # 插入 TikTok 数据同理 def save_tiktok_videos(videos): for v in videos: record TikTokVideo( idv[id], descv[desc], authorv[author], digg_countv[digg_count], comment_countv[comment_count], share_countv[share_count], video_urlv[video_url] ) try: session.add(record) session.commit() except Exception as e: session.rollback() print(f跳过重复视频 {v[id]})这里的关键是uniqueTrue和session.rollback()SQLite 的INSERT OR IGNORE在主键或唯一约束冲突时静默忽略配合try/except实现“有则跳过无则插入”的幂等语义。比先SELECT COUNT(*)再判断快 3 倍且线程安全。6.3 增量采集用created_at和WHERE实现断点续爬每天跑爬虫不该重抓昨天已有的数据。在extract_*函数中加入时间过滤def get_last_crawl_time(platform: str) - datetime: 获取某平台最后一条记录的创建时间 if platform youtube: last session.query(YouTubeVideo).order_by(YouTubeVideo.created_at.desc()).first() else: last session.query(TikTokVideo).order_by(TikTokVideo.created_at.desc()).first() return last.created_at if last else datetime(1970, 1, 1) # 在采集前只抓取新数据 last_time get_last_crawl_time(youtube) # 但 YouTube/TikTok 无时间戳排序 API所以改为只抓取最新 N 条再过滤 # 实际工程中我们用「视频发布时间」字段做增量需在提取时保留 publish_time # 然后入库前加条件if datetime.strptime(v[publish_time], %d %b %Y) last_time更实用的做法是用video_id去重 每日新建表。例如表名youtube_videos_20240615既避免跨日去重开销又方便按日归档分析。6.4 我的落地习惯一个crawl.py文件包打天下最终交付给团队的不是一个复杂框架而是一个可直接python crawl.py --platform youtube --limit 100运行的脚本。它内部封装了WebDriver 初始化含反检测平台路由if platform youtube提取函数调用数据清洗去除 emoji、截断超长描述SQLite 写入日志记录logging.info(f✅ YouTube: {len(videos)} videos saved)。没有抽象工厂没有插件系统没有配置中心。因为爬虫的迭代周期是“天级”不是“月级”。当 TikTok 更新了_WEB_INIT_PROPS_结构我宁愿花 20 分钟改一行 XPath也不愿花 2 小时调试配置加载逻辑。工程化的本质不是炫技而是让下次接手的人30 秒看懂你在做什么3 分钟改出新版本。希望帮到你。本文还有配套的精品资源点击获取
返回列表