- PVSM.RU - https://www.pvsm.ru -
Современный мир пресыщен различной информацией, и в наше непростое время её важно уметь не только находить и сохранять. Многие наверняка заметили, что в на YouTube, кроме мусора, котиков и прочих бесполезных вещей (которые мы иногда не прочь посмотреть) есть масса полезного материала по самым различным темам. И иногда этот материал неплохо было бы сохранить себе на будущее, чтобы не зависеть от переменчивых настроений в мире.
В этой статье я хочу рассказать, как можно скачивать видео, аудио (1 часть статьи), плейлисты и целые каналы с YouTube (2 часть статьи) без использования VPN и на чистом Python-е. Сразу оговорка: VPN нам не понадобится, но мы сделаем собственное средство, которое будет решать "проблему с устаревшим и изношенным оборудованием Google Global Cache [1]" (вы поняли, о чём я). Я думаю это средство будет особенно актуально сегодня, когда у многих россиян YouTube почти или совсем не работает.
Я программирую на Python, и о чём мне рассказывать как не о нём?
У python есть широкие возможности для создания таких инструментов и готовые библиотеки с большим функционалом.
Существуют такие замечательные утилиты, как yt-dlp и youtube-dl, но они подходят, когда нужно по-быстрому что-то скачать. Тонкая настройка и скачивание, например целого канала, вещь довольно геморройная (если, нет - поправьте меня), так как постоянно нужно что-то гуглить и знать многочисленные ключи и параметры.
Существуют и другие десктопные программы, мобильные приложения и сайты, но как правило они или платные, или напичканы рекламой, или ограничены в функционале, или даже всё вместе.
Ну и по-моему, собственный скрипт всегда удобнее любой навороченной утилиты, да и глаз радуется на красивые строчки в редакторе)
Почему YouTube у нас работает не так, как надо? Конечно, все знают, что дело здесь не в оборудовании, а в том, что провайдеры используют такую зловредную (в данном случае) вещь, как DPI [2] (англ. Deep Packet Inspection «глубокая инспекция пакетов»)
Большинство DPI работает так. Когда вы пытаетесь получить доступ к заблокированному веб-сайту, DPI отправляет вам HTTP 302 Redirect, и делает это быстрее, чем веб-сайт к которому вы обращаетесь. В отличие от брандмауэров, Deep Packet Inspection анализирует не только заголовки пакетов, но и содержимое запросов, что позволяет интернет-провайдерам и государственным органам ограничивать доступ к запрещенным ресурсам, выявлять вторжения в сеть и останавливать распространение компьютерных вирусов.
Это довольно упрощенная модель пассивного DPI, но так как целью этой статьи не является подробное ознакомление с этой темой, да и я, честно признаюсь, не большой специалист в этом, то думаю этого достаточно для понимания того, что происходит. Если вас интересует эта тема, то можете почитать здесь [2] и здесь [3].
Но, на каждый болт найдется своя гайка, и есть способы обойти эту вещь (хотя они не всегда действуют). Многие наверняка слышали про GoodbyeDPI [4] или zapret [5]. Это замечательные инструменты, но раз уж мы говорим про Python, то давайте сделаем свой аналог на Python. Поискав немного, я нашел nodpi [6], который использует только python.
nodpi создает прокси-сервер (socket), через который мы гоним трафик. Все исходящие соединения он случайным образом фрагментирует, тем самым сбивая с толку DPI.
Это написано на Python!
Просто, дёшево, сердито и работает для большинства провайдеров
Не требуются привилегии администратора, как, например у GoodbyeDPI
Прокси-сервер можно настроить только в некоторых приложениях и не гнать через него весь трафик системы.
Простота - не всегда качество, поэтому гарантия не даётся
Работает только для TCP
Не поможет, если сайт забанен по IP
Программа написана довольно небрежно, словно у разработчика слипались глаза и он залил код прямо так, не разбираясь, и поэтому я приведу причёсанный код и заодно немного расскажу, что он делает.
Для начала создадим текстовый файл blacklist.txt и добавим туда домены, блокировку которых хотим обходить. Для YouTube это:
youtube.com
youtu.be
yt.be
googlevideo.com
ytimg.com
ggpht.com
gvt1.com
youtube-nocookie.com
youtube-ui.l.google.com
youtubeembeddedplayer.googleapis.com
youtube.googleapis.com
youtubei.googleapis.com
yt-video-upload.l.google.com
wide-youtube.l.google.com
Теперь создадим файл nodpi.py и добавим в него следующий код
import random
import asyncio
BLOCKED = [line.rstrip().encode() for line in open('blacklist.txt', 'r', encoding='utf-8')]
TASKS = []
Что мы здесь делаем:
Импортируем необходимые библиотеки
Создаём список заблокированных доменов
Создаём список, в котором будем хранить задачи передачи данных между клиентом и сервером
async def main(host, port):
server = await asyncio.start_server(new_conn, host, port)
await server.serve_forever()
Создаём входную точку - асинхронную функцию, которая запускает сокет-сервер на указанном хосте и порту и используем new_conn (ниже) для обработки новых соединений.
async def pipe(reader, writer):
while not reader.at_eof() and not writer.is_closing():
try:
writer.write(await reader.read(1500))
await writer.drain()
except:
break
writer.close()
Создаём асинхронную функцию, которая читает данные из reader и записывает их в writer до тех пор, пока не достигнут конец или соединение не закрыто.
async def new_conn(local_reader, local_writer):
http_data = await local_reader.read(1500)
try:
type, target = http_data.split(b"rn")[0].split(b" ")[0:2]
host, port = target.split(b":")
except:
local_writer.close()
return
if type != b"CONNECT":
local_writer.close()
return
local_writer.write(b'HTTP/1.1 200 OKnn')
await local_writer.drain()
try:
remote_reader, remote_writer = await asyncio.open_connection(host, port)
except:
local_writer.close()
return
if port == b'443':
await fragemtn_data(local_reader, remote_writer)
TASKS.append(asyncio.create_task(pipe(local_reader, remote_writer)))
TASKS.append(asyncio.create_task(pipe(remote_reader, local_writer)))
Создаём асинхронную функцию, которая обрабатывает новое соединение.
Читаем HTTP-заголовки и извлекаем тип запроса и целевой адрес.
Если тип запроса не CONNECT, закрываем соединение.
Отправляем ответ HTTP/1.1 200 OK клиенту.
Открываем соединение с целевым сервером.
Если порт равен 443 (порт HTTPS), вызываем функцию fragemtn_data (ниже)
Создаем задачи для передачи данных между клиентом и сервером.
async def fragemtn_data(local_reader, remote_writer):
head = await local_reader.read(5)
data = await local_reader.read(1500)
parts = []
if all([data.find(site) == -1 for site in BLOCKED]):
remote_writer.write(head + data)
await remote_writer.drain()
return
while data:
part_len = random.randint(1, len(data))
parts.append(bytes.fromhex("1603") + bytes([random.randint(0, 255)]) + int(
part_len).to_bytes(2, byteorder='big') + data[0:part_len])
data = data[part_len:]
remote_writer.write(b''.join(parts))
await remote_writer.drain()
Создаём асинхронную функцию, которая фрагментирует данные перед отправкой на сервер.
Читаем заголовок и данные.
Если данные не из заблокированных доменов, отправляем их без изменений.
В противном случае, разбиваем данные на случайные части и добавляем заголовки перед отправкой. Это помогает обойти блокировку DPI, так как фрагментированные данные выглядят как случайные пакеты.
asyncio.run(main(host='127.0.0.1', port=8881))
Запускаем сервер на локальном 127.0.0.1 и слушаем порт 8881
Всё, можно пользоваться!
Давайте проверим. Если у вас Firefox, то заходим в Настройки → Настройки сети и прописываем IP и порт:
Открываем YouTube и убеждаемся, что все работает или не работает :)
Для начала установим библиотеку pytubefix командой:
pip install pytubefix
pytubefix [7] - это мощная библиотека python для скачивания с YouTube видео, аудио, плейлистов и каналов со своим собственным cli. Сама по себе она фактически является форком pytube [8], но последние изменения в pytube были сделаны больше года назад, и в августе этого года, после изменений в api YouTube она перестала работать. Также в pytubefix исправлены многие недочеты предшественницы и добавлены новые функции. pytubefix, как и pytube, работает с api youtube, а кое-где и парсит его html/json.
Также нам понадобится ffmpeg, для объединения аудио и видео (если, конечно, вы хотите скачивать в высоком качестве). Почему? Дело в том, что YouTube дает видео с аудио в одном потоке только в разрешении 360p (раньше было 720p), и чтобы скачать видео, например в 1080p, нужно сначала скачать видео в 1080p, потом скачать аудио, и затем все это объединить конвертером. ffmpeg существует и для Windows. Проверенную сборку можно скачать с моего Google Диска [9], но вы можете найти ее и самостоятельно в интернете.
Давайте попробуем скачать какое-нибудь видео. Создайте файл yt_downloader.py и вставьте в него код:
from pytubefix import YouTube
from pytubefix.cli import on_progress
url = "https://www.youtube.com/watch?v=xxxxxxxxxxx"
video = YouTube(
proxies={"http": "http://127.0.0.1:8881",
"https": "http://127.0.0.1:8881"},
url=url,
on_progress_callback=on_progress,
)
print('Title:', yt.title)
stream = video.streams.get_highest_resolution()
stream.download()
Давайте разберём, что делает этот код. Сначала мы импортируем класс YouTube, с помощью которого мы скачиваем видео, и функцию on_progress. Она нужна для того, чтобы во время скачивания отображался прогрессбар загрузки. Согласитесь, что так гораздо удобнее? При желании можно написать свою функцию-callback. Она должна иметь аргументы (stream: Stream, chunk: bytes, bytes_remaining: int)
В переменную url вставьте свою ссылку для скачивания, так как я заменил id видео иксами. Далее мы создаем экземпляр класса YouTube и передаем ему словарь с адресом нашего прокси-сервера, url и callback. Все аргументы, кроме url являются необязательными. Также стоит отметить, что callback вызывается каждый раз, после скачивания нового чанка (pytubefix скачивает видео кусками - чанками).
Далее мы выводим на экран заголовок (название) видео. Также можно узнать дату публикации, длину видео (в секундах), описание, количество просмотров и автора. Размер файла можно узнать с помощью stream.filesize, так как он зависит от выбранного потока.
В строке stream = video.streams.get_highest_resolution() мы получаем список потоков и сортируем его по разрешению (качеству) видео. Тут я хочу остановиться и рассказать немного подробнее. У каждого видео на YouTube есть несколько десятков потоков, каждый из которых представляет собой одно видео, одно аудио или все сразу в разных форматах, кодировках и качестве. Так, для моего видео список потоков выглядит примерно так:
[
{
"itag": 18,
"mime_type": "video/mp4",
"resolution": "360p",
"vcodec": "avc1.42001E",
"acodec": "mp4a.40.2",
"progressive": True,
"type": "video",
},
{
"itag": 315,
"mime_type": "video/webm",
"resolution": "2160p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 337,
"mime_type": "video/webm",
"resolution": "2160p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 701,
"mime_type": "video/mp4",
"resolution": "2160p",
"vcodec": "av01.0.13M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 308,
"mime_type": "video/webm",
"resolution": "1440p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 336,
"mime_type": "video/webm",
"resolution": "1440p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 700,
"mime_type": "video/mp4",
"resolution": "1440p",
"vcodec": "av01.0.12M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 299,
"mime_type": "video/mp4",
"resolution": "1080p",
"vcodec": "avc1.64002a",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 303,
"mime_type": "video/webm",
"resolution": "1080p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 335,
"mime_type": "video/webm",
"resolution": "1080p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 699,
"mime_type": "video/mp4",
"resolution": "1080p",
"vcodec": "av01.0.09M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 298,
"mime_type": "video/mp4",
"resolution": "720p",
"vcodec": "avc1.4d4020",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 302,
"mime_type": "video/webm",
"resolution": "720p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 334,
"mime_type": "video/webm",
"resolution": "720p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 698,
"mime_type": "video/mp4",
"resolution": "720p",
"vcodec": "av01.0.08M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 135,
"mime_type": "video/mp4",
"resolution": "480p",
"vcodec": "avc1.4d401f",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 244,
"mime_type": "video/webm",
"resolution": "480p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 333,
"mime_type": "video/webm",
"resolution": "480p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 697,
"mime_type": "video/mp4",
"resolution": "480p",
"vcodec": "av01.0.05M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 134,
"mime_type": "video/mp4",
"resolution": "360p",
"vcodec": "avc1.4d401e",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 243,
"mime_type": "video/webm",
"resolution": "360p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 332,
"mime_type": "video/webm",
"resolution": "360p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 696,
"mime_type": "video/mp4",
"resolution": "360p",
"vcodec": "av01.0.04M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 133,
"mime_type": "video/mp4",
"resolution": "240p",
"vcodec": "avc1.4d4015",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 242,
"mime_type": "video/webm",
"resolution": "240p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 331,
"mime_type": "video/webm",
"resolution": "240p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 695,
"mime_type": "video/mp4",
"resolution": "240p",
"vcodec": "av01.0.01M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 160,
"mime_type": "video/mp4",
"resolution": "144p",
"vcodec": "avc1.4d400c",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 278,
"mime_type": "video/webm",
"resolution": "144p",
"vcodec": "vp9",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 330,
"mime_type": "video/webm",
"resolution": "144p",
"vcodec": "vp9.2",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 694,
"mime_type": "video/mp4",
"resolution": "144p",
"vcodec": "av01.0.00M.10.0.110.09.18.09.0",
"acodec": None,
"progressive": False,
"type": "video",
},
{
"itag": 139,
"mime_type": "audio/mp4",
"resolution": None,
"vcodec": None,
"acodec": "mp4a.40.5",
"progressive": False,
"type": "audio",
},
{
"itag": 140,
"mime_type": "audio/mp4",
"resolution": None,
"vcodec": None,
"acodec": "mp4a.40.2",
"progressive": False,
"type": "audio",
},
{
"itag": 249,
"mime_type": "audio/webm",
"resolution": None,
"vcodec": None,
"acodec": "opus",
"progressive": False,
"type": "audio",
},
{
"itag": 250,
"mime_type": "audio/webm",
"resolution": None,
"vcodec": None,
"acodec": "opus",
"progressive": False,
"type": "audio",
},
{
"itag": 251,
"mime_type": "audio/webm",
"resolution": None,
"vcodec": None,
"acodec": "opus",
"progressive": False,
"type": "audio",
},
]
Вы можете посмотреть полный список потоков для своего видео, просто выведя на экран print(video.streams), но как правило это не требуется, так как библиотека предоставляет средства для их сортировки и фильтрации. В нашем случае это get_highest_resolution(). Эта функция возвращает поток с самым лучшим разрешением, который содержит и аудио, и видео. Как я уже объяснял, таким разрешением окажется 360p, потому что YouTube дает видео с аудио в одном потоке только в разрешении 360p (раньше было 720p), и чтобы скачать видео, например в 1080p, нужно сначала скачать видео в 1080p, потом скачать аудио, и затем все это объединить конвертером. Скрипт, который скачивает видео в более высоком качестве, мы напишем чуть позднее.
Ну и наконец, в строке stream.download() мы скачиваем видео. При скачивании будет отображаться прогрессбар, и вы увидите процесс загрузки.
Запустите скрипт и посмотрите результат. Не забудьте перед этим запустить нашу программу для обхода DPI!
Видео в 360p, это, конечно, не очень. Давайте расширим нашу программу. Если вы еще не скачали ffmpeg, сделайте это. Без него скачивание в другом расширении невозможно. Предполагается, что ffmpeg лежит в той же папке, что и скрипт.
Создайте файл yt_downloader_2.py и добавьте в него следующий код:
import os
from pytubefix import YouTube
from pytubefix.cli import on_progress
url = "https://www.youtube.com/watch?v=xxxxxxxxxxx"
def combine(audio: str, video: str, output: str) -> None:
if os.path.exists(output):
os.remove(output)
code = os.system(
f'.\ffmpeg.exe -i "{video}" -i "{audio}" -c copy "{output}"')
if code != 0:
raise SystemError(code)
def download(url: str):
yt = YouTube(
proxies={"http": "http://127.0.0.1:8881",
"https": "http://127.0.0.1:8881"},
url=url,
on_progress_callback=on_progress,
)
video_stream = yt.streams.
filter(type='video').
order_by('resolution').
desc().first()
audio_stream = yt.streams.
filter(mime_type='audio/mp4').
order_by('filesize').
desc().first()
print('Information:')
print("tTitle:", yt.title)
print("tAuthor:", yt.author)
print("tDate:", yt.publish_date)
print("tResolution:", video_stream.resolution)
print("tViews:", yt.views)
print("tLength:", round(yt.length/60), "minutes")
print("tFilename of the video:", video_stream.default_filename)
print("tFilesize of the video:", round(
video_stream.filesize / 1000000), "MB")
print('Download video...')
video_stream.download()
print('nDownload audio...')
audio_stream.download()
combine(audio_stream.default_filename, video_stream.default_filename,
f'{yt.title}.mp4')
download(url)
Что изменилось? Мы добавили функцию combine, которая отвечает за объединение видео и аудио. Мы делаем это командой .ffmpeg.exe -i "filename_video" -i "filename_audio" -c copy "output_filename"
В основном коде мы по отдельности вытаскиваем потоки видео и аудио. Здесь видео не содержит аудио, но зато оно в высоком качестве. Чтобы получить его, мы сначала фильтруем потоки по типу (видео) filter(type='video'), затем сортируем по разрешению, затем сортируем по убыванию разрешения и берём первый поток. Аналогично с аудио. После этого мы выводим подробную информацию о скачиваемом видео и скачиваем аудио и видео по отдельности, после чего объединяем их с помощью ffmpeg. Ничего сложного!
Запускаем наш скрипт для обхода DPI и скрипт для скачивания (замените url на свой). Готово!
Чтобы узнать, в каких разрешениях можно скачать данное видео, можно написать такую функцию:
def resolutions(video: YouTube):
res = []
streams = self.video.streams.
filter(type='video').
order_by('resolution').
desc()
for stream in streams:
if stream.resolution not in res:
res.append(stream.resolution)
return res
Поток с выбранным разрешением можно получить следующим образом:
res = '1080p' # например
stream = self.video.streams.
filter(resolution=res, progressive=False).desc().first()
Если вы не используете ffmpeg, замените progressive=False на True
Из предыдущего примера вы видели, как мы скачивали аудио:
...
audio_stream = yt.streams.
filter(mime_type='audio/mp4').
order_by('filesize').
desc().first()
audio_stream.download()
Только учтите, что формат аудио будет m4a, поэтому, если вам нужен другой, придётся воспользоваться ffmpeg
На этом я хочу остановиться. В следующей части (если такая будет) я расскажу о том, как скачивать целые плейлисты, каналы и даже субтитры с YouTube.
Да, такое, конечно, возможно. Но у меня на провайдере ТТК всё работает отлично. Поэтому я могу лишь посоветовать использовать другие инструменты, например GoodbyeDPI.
А вот это действительно проблема, с которой я столкнулся. Она появилась, когда начали тормозить YouTube и я перешёл на nodpi. То ли ютуб банит подозрительную активность, то ли РКН химичит, а может в nodpi что-то не так. Я так и не понял. Если тут есть знатоки, может подскажут, что не так и как пофиксить)
Если статья вам интересна, не откладывайте её в долгий ящик, так как через некоторое время после публикации она может попасть под географические ограничения по запросу РКН
Автор: Lord_of_Rings
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/406262
Ссылки в тексте:
[1] Google Global Cache: https://ru.wikipedia.org/wiki/Google_Global_Cache
[2] DPI: https://ru.wikipedia.org/wiki/Deep_packet_inspection
[3] здесь: https://habr.com/ru/articles/111054/
[4] GoodbyeDPI: https://github.com/ValdikSS/GoodbyeDPI
[5] zapret: https://github.com/bol-van/zapret
[6] nodpi: https://github.com/theo0x0/nodpi
[7] pytubefix: https://github.com/JuanBindez/pytubefix
[8] pytube: https://github.com/pytube/pytube/
[9] Google Диска: https://drive.google.com/file/d/12flZfkV3dKQWPy8mPYfsABttgR-3w1yZ/view?usp=drive_link
[10] Источник: https://habr.com/ru/articles/870110/?utm_source=habrahabr&utm_medium=rss&utm_campaign=870110
Нажмите здесь для печати.