Парсер YouTube на Python: три способа собрать видео, статистику и комментарии на Python

В прошлом квартале мне нужно было собрать данные по 5000 видео на тему «Python tutorial» для анализа контент-стратегий конкурентов: заголовки, просмотры, лайки, длительность, язык, возраст ролика. Один API-ключ с дневной квотой 10 000 единиц закончился через 40 минут. Рабочее решение собралось после ротации восьми ключей, кэширования ответов и батчевой записи результатов . YouTube Data API v3 это основной инструмент для парсинга YouTube — он предоставляет структурированный доступ к метаданным видео, каналов, плейлистов и комментариев.
В этой статье разбираю три подхода к парсингу YouTube: официальный Data API v3 (с ротацией ключей), yt-dlp (без API-ключа) и скрытое внутреннее API. Для каждого — рабочий код, ограничения и способы обхода квот.
Какие данные можно собрать
YouTube хранит обширные метаданные по каждому видео, каналу и комментарию.
- Название видео, описание, теги, категория
- Количество просмотров, лайков, дизлайков, комментариев
- Длительность, разрешение, дата публикации
- Субтитры (автоматические и загруженные)
- Статистика канала: подписчики, общее число видео, суммарные просмотры
- Плейлисты и их содержимое
- Комментарии с вложенными ответами
- Связанные видео и рекомендации
- Embed-код и статус лицензии (Creative Commons / Standard)
Способ 1: YouTube Data API v3 (основной)
Подготовка: API-ключ и проект Google Cloud
- Откройте Google Cloud Console: console.cloud.google.com.
- Создайте новый проект (или выберите существующий).
- В разделе «API и сервисы» → «Библиотека» найдите и активируйте YouTube Data API v3.
- В разделе «Учётные данные» создайте API-ключ.
- Ограничьте ключ: только YouTube Data API v3, только нужные IP-адреса.
Один ключ даёт 10 000 единиц квоты в день . Один вызов search.list стоит 100 единиц — это всего 100 поисков в сутки. Для масштабного парсинга нужно несколько ключей.
Установка библиотек
pip install google-api-python-client isodate langdetect pandas python-dotenvМинимальный пример: поиск видео
from googleapiclient.discovery import build
API_KEY = 'ваш-ключ'
youtube = build('youtube', 'v3', developerKey=API_KEY)
request = youtube.search().list(
q='python tutorial',
part='snippet',
type='video',
order='relevance',
maxResults=10,
regionCode='RU',
)
response = request.execute()
for item in response['items']:
video_id = item['id']['videoId']
title = item['snippet']['title']
channel = item['snippet']['channelTitle']
published = item['snippet']['publishedAt']
print(f'{title} | {channel} | {published}')
print(f' https://youtube.com/watch?v={video_id}')
search.list возвращает базовые сниппеты, но не статистику (просмотры, лайки). Для полных метаданных нужен дополнительный вызов videos.list.
Получение полной статистики видео
def get_video_details(youtube, video_ids):
'''Получает полные метаданные для списка videoId (до 50 за запрос).'''
all_details = []
chunks = [video_ids[i : i + 50] for i in range(0, len(video_ids), 50)]
for chunk in chunks:
request = youtube.videos().list(
id=','.join(chunk), part='snippet,contentDetails,statistics,status'
)
response = request.execute()
for video in response['items']:
stats = video.get('statistics', {})
snippet = video['snippet']
details = video['contentDetails']
all_details.append(
{
'video_id': video['id'],
'title': snippet['title'],
'channel': snippet['channelTitle'],
'channel_id': snippet['channelId'],
'published': snippet['publishedAt'],
'description': snippet.get('description', '')[:500],
'tags': ', '.join(snippet.get('tags', [])),
'duration': details['duration'],
'views': int(stats.get('viewCount', 0)),
'likes': int(stats.get('likeCount', 0)),
'comments': int(stats.get('commentCount', 0)),
'license': video.get('status', {}).get('license', ''),
'embeddable': video.get('status', {}).get('embeddable', False),
}
)
return all_details
videos.list принимает до 50 ID за один запрос и стоит 1 единицу квоты за элемент . Это значительно дешевле, чем search.list (100 единиц за вызов). Стратегия: собирайте ID через поиск, а детали запрашивайте пакетами.
Парсинг комментариев
import time
def get_comments(youtube, video_id, max_comments=500):
'''Собирает комментарии к видео с пагинацией.'''
all_comments = []
next_page = None
while len(all_comments) < max_comments:
try:
request = youtube.commentThreads().list(
videoId=video_id,
part='snippet,replies',
maxResults=100,
pageToken=next_page or '',
order='relevance',
textFormat='plainText',
)
response = request.execute()
except Exception as e:
if 'commentsDisabled' in str(e):
print(f'Комментарии отключены: {video_id}')
break
raise
for thread in response['items']:
top = thread['snippet']['topLevelComment']['snippet']
all_comments.append(
{
'video_id': video_id,
'author': top['authorDisplayName'],
'text': top['textDisplay'],
'likes': top['likeCount'],
'published': top['publishedAt'],
'is_reply': False,
}
)
# Вложенные ответы
if 'replies' in thread:
for reply in thread['replies']['comments']:
r = reply['snippet']
all_comments.append(
{
'video_id': video_id,
'author': r['authorDisplayName'],
'text': r['textDisplay'],
'likes': r['likeCount'],
'published': r['publishedAt'],
'is_reply': True,
}
)
next_page = response.get('nextPageToken')
if not next_page:
break
time.sleep(0.3)
return all_comments[:max_comments]
commentThreads.list возвращает до 100 верхнеуровневых комментариев с вложенными ответами. Некоторые видео имеют отключённые комментарии — это генерирует ошибку commentsDisabled, которую нужно обрабатывать.
Парсинг всех видео канала
Прямого метода «получить все видео канала» в API нет. Обходной путь: получить ID плейлиста «Uploads» через channels.list, затем перебрать его через playlistItems.list.
def get_channel_uploads(youtube, channel_id):
'''Получает все videoId из плейлиста загрузок канала.'''
# Шаг 1: Получаем ID плейлиста uploads
ch_response = (
youtube.channels().list(id=channel_id, part='contentDetails').execute()
)
uploads_id = ch_response['items'][0]['contentDetails']['relatedPlaylists'][
'uploads'
]
# Шаг 2: Перебираем плейлист
all_ids = []
next_page = None
while True:
pl_response = (
youtube.playlistItems()
.list(
playlistId=uploads_id,
part='contentDetails',
maxResults=50,
pageToken=next_page or '',
)
.execute()
)
for item in pl_response['items']:
all_ids.append(item['contentDetails']['videoId'])
next_page = pl_response.get('nextPageToken')
if not next_page:
break
time.sleep(0.3)
return all_ids
# Пример: канал Corey Schafer
video_ids = get_channel_uploads(youtube, 'UCCezIgC97PvUuR4_gbFUs5g')
print(f'Найдено {len(video_ids)} видео')
# Получаем детали пакетами по 50
details = get_video_details(youtube, video_ids)
Плейлист «uploads» содержит абсолютно все видео канала в хронологическом порядке. playlistItems.list стоит 1 единицу за запрос (до 50 элементов), что значительно дешевле search.list.
Ротация API-ключей
Один ключ = 10 000 единиц в день. Этого мало для серьёзного парсинга . Решение: создать несколько Google Cloud проектов, каждый со своим ключом, и ротировать их автоматически.
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import time
import logging
logger = logging.getLogger(__name__)
class APIKey:
def __init__(self, key: str):
self.key = key
self.service = build('youtube', 'v3', developerKey=key, cache_discovery=False)
self.used_units = 0
self.active = True
class KeyManager:
'''Ротация пула API-ключей с автоматическим переключением при исчерпании.'''
def __init__(self, keys: list[str]):
self.keys = [APIKey(k) for k in keys]
self.index = 0
def get_key(self) -> APIKey:
for _ in range(len(self.keys)):
api = self.keys[self.index]
self.index = (self.index + 1) % len(self.keys)
if api.active:
return api
raise Exception('Все ключи исчерпали квоту')
def deactivate(self, api: APIKey):
api.active = False
logger.warning(f'Ключ деактивирован: {api.key[:10]}...')
def execute(self, fn, max_retries=3):
'''Выполняет API-вызов с автоматической ротацией ключей.'''
attempt = 0
while True:
api = self.get_key()
try:
response = fn(api.service).execute()
return response
except HttpError as e:
error = str(e)
if 'quotaExceeded' in error or 'dailyLimitExceeded' in error:
self.deactivate(api)
continue
if 'rateLimitExceeded' in error and attempt < max_retries:
delay = 2**attempt
logger.warning(f'Rate limit, жду {delay}s...')
time.sleep(delay)
attempt += 1
continue
raise
# Использование
KEYS = ['KEY1', 'KEY2', 'KEY3', 'KEY4', 'KEY5']
km = KeyManager(KEYS)
# Любой вызов через KeyManager
response = km.execute(
lambda svc: svc.search().list(
q='machine learning',
part='snippet',
type='video',
maxResults=50,
)
)
KeyManager перехватывает ошибку quotaExceeded, деактивирует текущий ключ и автоматически переключается на следующий . При rateLimitExceeded (слишком частые запросы) применяется экспоненциальная задержка. С 5 ключами дневная квота — 50 000 единиц, с 10 — 100 000.
Кэширование ответов
Повторный запрос тех же данных тратит квоту впустую. shelve сохраняет ответы локально .
import shelve
CACHE = shelve.open('yt_cache.db')
def cached_search(km, query, region='RU', max_results=50, page_token=None):
cache_key = f'search:{query}:{region}:{page_token}'
if cache_key in CACHE:
return CACHE[cache_key]
response = km.execute(
lambda svc: svc.search().list(
q=query,
part='snippet',
type='video',
order='relevance',
regionCode=region,
maxResults=max_results,
pageToken=page_token or '',
)
)
CACHE[cache_key] = response
return response
def cached_videos(km, video_ids):
cache_key = f'videos:{','.join(sorted(video_ids))}'
if cache_key in CACHE:
return CACHE[cache_key]
response = km.execute(
lambda svc: svc.videos().list(
id=','.join(video_ids), part='snippet,contentDetails,statistics,status'
)
)
CACHE[cache_key] = response
return response
Кэш радикально экономит квоту при отладке и перезапусках. Один search.list стоит 100 единиц — при 10 запусках скрипта это 1000 единиц вместо 100 .
Способ 2: yt-dlp (без API-ключа)
yt-dlp это open-source утилита командной строки, наследник youtube-dl. Она извлекает метаданные и скачивает видео с YouTube и тысяч других сайтов без API-ключа.
Установка
pip install yt-dlp
Извлечение метаданных без скачивания
import yt_dlp
import json
def get_video_info(url):
'''Извлекает метаданные видео без скачивания.'''
opts = {
'quiet': True,
'no_warnings': True,
'skip_download': True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url, download=False)
return {
'id': info.get('id'),
'title': info.get('title'),
'description': info.get('description', '')[:500],
'channel': info.get('channel'),
'channel_id': info.get('channel_id'),
'upload_date': info.get('upload_date'),
'duration': info.get('duration'),
'views': info.get('view_count'),
'likes': info.get('like_count'),
'comments': info.get('comment_count'),
'categories': info.get('categories'),
'tags': info.get('tags'),
'thumbnail': info.get('thumbnail'),
'language': info.get('language'),
'subtitles': list(info.get('subtitles', {}).keys()),
'automatic_captions': list(info.get('automatic_captions', {}).keys()),
}
info = get_video_info('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
print(json.dumps(info, indent=2, ensure_ascii=False))
yt-dlp парсит страницу YouTube напрямую, без API-ключа и квот. Он извлекает больше данных, чем Data API: форматы видео, субтитры, информацию о главах (chapters), стриминговые URL.
Сбор метаданных всех видео канала
def get_channel_videos(channel_url, max_videos=100):
'''Собирает метаданные всех видео канала через yt-dlp.'''
opts = {
'quiet': True,
'no_warnings': True,
'skip_download': True,
'extract_flat': True, # Не скачивать, только ID
'playlistend': max_videos,
}
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(f'{channel_url}/videos', download=False)
videos = []
for entry in info.get('entries', []):
if entry:
videos.append(
{
'id': entry.get('id'),
'title': entry.get('title'),
'url': entry.get('url'),
'duration': entry.get('duration'),
'views': entry.get('view_count'),
}
)
return videos
videos = get_channel_videos('https://www.youtube.com/@CoreySchafer', max_videos=50)
for v in videos[:5]:
print(f'{v['title']} | {v['views']} просмотров')
Параметр extract_flat=True получает только базовые данные без захода на каждую страницу видео. Для полных метаданных уберите этот флаг, но скорость сбора упадёт в десятки раз.
Скачивание субтитров
def download_subtitles(video_url, lang='ru', output_dir='subtitles'):
'''Скачивает субтитры видео в формате SRT.'''
opts = {
'quiet': True,
'skip_download': True,
'writesubtitles': True,
'writeautomaticsub': True, # Автоматические субтитры
'subtitleslangs': [lang, 'en'],
'subtitlesformat': 'srt',
'outtmpl': f'{output_dir}/%(id)s.%(ext)s',
}
with yt_dlp.YoutubeDL(opts) as ydl:
ydl.download([video_url])
print(f'Субтитры сохранены в {output_dir}/')
yt-dlp скачивает как загруженные авторами субтитры, так и автоматически сгенерированные YouTube. Это уникальная возможность: Data API v3 не даёт доступ к тексту субтитров.
Ограничения yt-dlp
YouTube активно борется с yt-dlp — между обновлениями парсер может перестать работать на несколько дней. Также YouTube вводит задержки и ограничения скорости для автоматизированных запросов. Рекомендуется добавлять паузы между запросами:
opts = {
'sleep_interval': 3, # Минимальная пауза
'max_sleep_interval': 8, # Максимальная пауза (случайная)
'sleep_interval_requests': 1, # Пауза между HTTP-запросами
}
Способ 3: скрытое внутреннее API
YouTube использует внутренний API (youtubei/v1) для загрузки данных на фронтенде. Его можно перехватить через DevTools и отправлять запросы напрямую.
import requests
session = requests.Session()
session.headers.update(
{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'X-YouTube-Client-Name': '1',
'X-YouTube-Client-Version': '2.20260401.01.00',
}
)
# Поиск через внутренний API
def internal_search(query, max_results=20):
url = 'https://www.youtube.com/youtubei/v1/search'
payload = {
'context': {
'client': {
'clientName': 'WEB',
'clientVersion': '2.20260401.01.00',
'hl': 'ru',
'gl': 'RU',
}
},
'query': query,
}
response = session.post(url, json=payload)
if response.status_code != 200:
print(f'Ошибка: {response.status_code}')
return []
data = response.json()
results = []
# Парсим вложенную структуру ответа
contents = (
data.get('contents', {})
.get('twoColumnSearchResultsRenderer', {})
.get('primaryContents', {})
.get('sectionListRenderer', {})
.get('contents', [])
)
for section in contents:
items = section.get('itemSectionRenderer', {}).get('contents', [])
for item in items:
video = item.get('videoRenderer')
if not video:
continue
results.append(
{
'id': video.get('videoId'),
'title': video.get('title', {})
.get('runs', [{}])[0]
.get('text', ''),
'channel': video.get('ownerText', {})
.get('runs', [{}])[0]
.get('text', ''),
'views': video.get('viewCountText', {}).get('simpleText', ''),
'duration': video.get('lengthText', {}).get('simpleText', ''),
'published': video.get('publishedTimeText', {}).get(
'simpleText', ''
),
}
)
if len(results) >= max_results:
break
return results
Внутренний API не имеет квот и не требует ключа. Но структура ответа сложная и вложенная, а эндпоинты меняются без предупреждения. Этот подход нестабилен и подходит только для дополнения основного парсинга через Data API.
Сохранение в CSV и SQLite
CSV
import csv
def save_to_csv(videos, filename='youtube_data.csv'):
if not videos:
return
keys = videos[0].keys()
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(videos)
print(f'Сохранено {len(videos)} видео в {filename}')
SQLite (для больших объёмов)
import sqlite3
def init_db(path='youtube.db'):
conn = sqlite3.connect(path)
conn.execute(
'''
CREATE TABLE IF NOT EXISTS videos (
video_id TEXT PRIMARY KEY,
title TEXT,
channel TEXT,
channel_id TEXT,
published TEXT,
duration TEXT,
views INTEGER,
likes INTEGER,
comments INTEGER,
tags TEXT,
description TEXT,
license TEXT,
scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
'''
)
conn.execute(
'''
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
video_id TEXT REFERENCES videos(video_id),
author TEXT,
text TEXT,
likes INTEGER,
published TEXT,
is_reply BOOLEAN
)
'''
)
conn.commit()
return conn
def save_video(conn, video):
conn.execute(
'''INSERT OR REPLACE INTO videos
(video_id, title, channel, channel_id, published,
duration, views, likes, comments, tags, description, license)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(
video['video_id'],
video['title'],
video['channel'],
video.get('channel_id', ''),
video['published'],
video.get('duration', ''),
video['views'],
video['likes'],
video['comments'],
video.get('tags', ''),
video.get('description', ''),
video.get('license', ''),
),
)
conn.commit()
INSERT OR REPLACE обновляет данные при повторном парсинге — просмотры и лайки меняются со временем, и свежие значения перезаписывают старые.
Сравнение подходов
Оптимизация квот
Квота Data API v3 — главное ограничение при масштабном парсинге. Стратегии экономии:
1. Минимизируйте search.list. Это самый дорогой метод: 100 единиц за вызов. Используйте playlistItems.list (1 единица) для получения видео канала вместо поиска.
2. Пакетные запросы videos.list. Один вызов с 50 ID стоит столько же, сколько вызов с 1 ID. Всегда передавайте максимум 50 ID за запрос.
3. Запрашивайте только нужные part. Каждый part увеличивает стоимость. part="snippet" дешевле, чем part="snippet,contentDetails,statistics,status".
4. Кэшируйте всё. Используйте shelve или Redis для хранения ответов API.
5. Несколько ключей. 8 аккаунтов × 1 проект = 80 000 единиц/день. Этого хватает для сбора данных по 10 000–15 000 видео.
Юридические аспекты
- Условия использования YouTube API: Google требует, чтобы приложения соответствовали YouTube API Terms of Service. Сбор данных для перепродажи или создания конкурирующего сервиса запрещён.
- yt-dlp: Находится в правовой серой зоне. YouTube активно борется с этим инструментом. Скачивание видео без разрешения правообладателя нарушает авторское право.
- Персональные данные: Имена авторов комментариев — персональные данные. Массовый сбор без согласия нарушает GDPR и ФЗ-152.
- Безопасный путь: Используйте официальный API, собирайте только агрегированную статистику, не перепубликуйте контент, не храните персональные данные дольше необходимого.
Неочевидные детали
Первый факт: search.list возвращает результаты, отсортированные по «релевантности» по умолчанию, но при повторном вызове с теми же параметрами результаты могут отличаться. YouTube персонализирует выдачу даже для API-запросов. Для воспроизводимости используйте order="date".
Второй факт: поле dislikeCount больше не возвращается через API с ноября 2021 года. yt-dlp тоже не получает точное число дизлайков — YouTube скрыл его на уровне платформы.
Третий факт: playlistItems.list для плейлиста «uploads» возвращает видео от новых к старым, но максимум 20 000 элементов. Для каналов с более чем 20 000 видео нужно использовать search.list с фильтром channelId и publishedAfter/Before для разбиения по периодам.
Четвёртый факт: один commentThreads.list возвращает до 100 верхнеуровневых комментариев, а вложенные ответы (replies) ограничены 5 штуками. Для полных ответов нужен отдельный вызов comments.list с parentId.
Пятый факт: yt-dlp при частых запросах получает от YouTube HTTP 429 или JavaScript-капчу. Добавление --cookies-from-browser chrome (передача cookies авторизованного аккаунта) значительно снижает вероятность блокировки.
FAQ
Сколько видео можно собрать за день через API?
С одним ключом (10 000 единиц): около 100 поисков + 5000 детализаций видео. С 5 ключами — в 5 раз больше . Для каналов, где используется playlistItems.list, — до 500 000 videoId за день с одним ключом.
Можно ли собрать данные без API-ключа?
Да, через yt-dlp или скрытое API. Но эти методы менее стабильны и могут нарушать условия использования YouTube.
Как получить субтитры видео?
Только через yt-dlp. Data API v3 возвращает список доступных субтитров (через captions.list), но для скачивания текста нужен OAuth 2.0 и права владельца видео.
Можно ли собрать YouTube Shorts отдельно?
Через Data API нет отдельного фильтра для Shorts. Определяйте Shorts по длительности: менее 60 секунд . Через yt-dlp Shorts извлекаются как обычные видео.
Как экспортировать данные в Google Sheets?
Используйте Google Sheets API + Service Account. Скрипт из статьи на vc.ru показывает рабочий пример: считывание ключевых слов из таблицы и запись результатов обратно.
Мой совет: начните с YouTube Data API v3 и одного ключа. Соберите 50–100 видео, разберитесь в структуре данных. Затем добавьте ротацию 3–5 ключей и кэширование — это увеличит дневную мощность до 30 000–50 000 единиц квоты. Используйте playlistItems.list вместо search.list везде, где возможно — экономия квоты в 100 раз. yt-dlp держите как запасной инструмент для субтитров и случаев, когда API не возвращает нужных данных. И обязательно кэшируйте ответы API — это спасёт вас при падениях скрипта и сэкономит тысячи единиц квоты.
