💻
Разработка в IT
Опубликовано:
15.04.2026
Обновлено:
15.04.2026

Парсер 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) 
Метод API Что возвращает Стоимость (единиц квоты)
search.list Поиск видео/каналов/плейлистов 100
videos.list Детали видео (статистика, длительность) 1 за каждый элемент
channels.list Статистика канала 1
commentThreads.list Комментарии к видео 1
playlistItems.list Видео из плейлиста 1

Способ 1: YouTube Data API v3 (основной)

Подготовка: API-ключ и проект Google Cloud

  1. Откройте Google Cloud Console: console.cloud.google.com.
  2. Создайте новый проект (или выберите существующий).
  3. В разделе «API и сервисы» → «Библиотека» найдите и активируйте YouTube Data API v3.
  4. В разделе «Учётные данные» создайте API-ключ.
  5. Ограничьте ключ: только 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 обновляет данные при повторном парсинге — просмотры и лайки меняются со временем, и свежие значения перезаписывают старые.

Сравнение подходов

Характеристика YouTube Data API v3 yt-dlp Скрытое API (youtubei)
Скорость 3–5 req/s 1–3 видео/мин 5–10 req/s
Нужен API-ключ Да Нет Нет
Квоты 10 000 ед./день/ключ Нет квот (но throttling) Нет квот
Стабильность Высокая Средняя (ломается) Низкая
Комментарии Да Да (медленно) Да (сложный парсинг)
Субтитры Нет текста Да (полный текст) Нет
Стоимость Бесплатно (в рамках квот) Бесплатно Бесплатно
Документация Полная Хорошая Отсутствует

Оптимизация квот

Квота 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 — это спасёт вас при падениях скрипта и сэкономит тысячи единиц квоты.

Читайте также