Отказ от ответственности: это руководство предназначено исключительно для образовательных целей и не должно использоваться для пиратства любого контента, доступного в Интернете. Я не несу ответственности за использование кода, опубликованного в этой статье и на GitHub.

Вы когда-нибудь отправлялись в путешествие (например, на автомобиле или на самолете) и очень хотели скачать лучшие моменты из последних спортивных игр или фильмов с (не одобренных) пиратских сайтов? Но подождите, этих основных моментов нет на YouTube или любом другом сайте, на котором есть общедоступный конвертер MP4.

Как вы собираетесь загружать видеоконтент? Первая мысль, которая приходит на ум, — запись видео с экрана, но это отнимает много времени и теряет часть исходного качества видео. Следующей мыслью будет проверить элемент и найти видеофайл, из которого воспроизводится веб-страница. Это грубый метод, и он не работает для сайтов, использующих буферизацию (а таких подавляющее большинство), поэтому мы не можем его использовать.

Что такое буферизация и почему она не позволяет нам напрямую загружать видеофайл?

В современных онлайн-системах воспроизведения видео мы используем технологию под названием «буферизация», чтобы по существу загружать видео, когда пользователь его воспроизводит. Это звучит немного сложно, но позвольте мне показать вам реальность этого.

Веб-страница загружает первый сегмент видео (обычно около 10–25 секунд) из какого-то удаленного места в формате «блоб». Как только зритель просматривает около 6 или 7 секунд, при условии, что сегмент длится 10 секунд, веб-страница загружает следующий сегмент видео (такой же длины) и плавно вставляет его в конец уже воспроизводимого сегмента.

Причина, по которой мы должны сделать это, заключается в том, что если бы мы воспроизводили видео из стандартного HTML-тега видео, браузеру пришлось бы загрузить это видео полностью, прежде чем его можно будет просмотреть. Для коротких видео (желательно менее одной минуты) это не проблема. Но когда мы начинаем получать все больше и больше видео, этот процесс браузера становится очень трудоемким и использует слишком много ресурсов.

Загружая только короткие сегменты с определенным интервалом, мы сокращаем начальное время загрузки до начала воспроизведения и значительно снижаем нагрузку на систему хранения данных.

Итак, если вы обращали внимание, у вас должны возникнуть следующие вопросы:

  1. Как браузер узнает, какой сегмент видео будет следующим?
  2. Как мы можем взять список сегментов, загрузить каждый из них и соединить их вместе?
  3. Это лучший способ? (всегда сомневайтесь в реализации, чтобы увидеть, сможете ли вы придумать что-то лучше)

Я отвечу на первый вопрос прямо сейчас, затем мы углубимся в реализацию и посмотрим, как именно мы можем автоматизировать весь этот процесс с помощью Python. Затем я расскажу о других методах и о том, почему я считаю этот метод лучшим.

Когда вы открываете любой видеоплеер, использующий эту технологию буферизации, базовый JavaScript, поддерживающий веб-страницу, загружает очень специфический файл с серверной части веб-страницы. Этот файл обычно называется «master.m3u8» и хранит информацию о расположении и порядке всех сегментов видео, которые будут воспроизводиться.

Если мы загрузим этот файл, а затем найдем в нем ссылки на сегменты, а затем соединим эти сегменты вместе с помощью программного обеспечения, такого как FFmpeg, мы успешно загрузим буферизованное видео.

Для этого мы будем использовать популярное программное обеспечение для автоматизированного тестирования Selenium. Selenium позволяет нам эмулировать весь браузер Chrome с помощью кода, поэтому для его установки используйте следующую команду.

pip install selenium

При этом вы сможете использовать обширный набор инструментов Selenium для автоматизации (при условии, что у вас установлен chromedriver, который я не буду рассматривать).

Давайте импортируем необходимые модули Selenium для этого проекта.

from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

import requests

Если сайт, с которого вы скачиваете, использует защиту Cloudflare, вы можете использовать https://pypi.org/project/undetected-chromedriver/ в качестве замены Selenium.

Далее мы импортируем веб-драйвер Selenium и создадим его экземпляр.

from selenium import webdriver

# replace with path to chromedriver
driver = webdriver.Chrome(executable_path=r"C:\path\to\chromedriver.exe")

Мой код ищет загрузку с https://animetake.tv, поэтому драйвер получит эту веб-страницу. Обязательно замените этот URL-адрес любым сайтом, с которого вы хотите загрузить. Используйте ссылку, содержащую видео, а не главную страницу сайта.

driver.get("https://animetake.tv") # get the video source

Здесь следует отметить одну вещь: если вы пытаетесь скачать с пиратского сайта, вы можете заметить, что видео воспроизводится из iFrame. Вам нужно будет перейти к источнику iFrame, прежде чем это сработает.

Для этого вы можете использовать часть моего кода из предыдущих проектов, которые я вставлю прямо здесь.

WebDriverWait(driver, timeout=5).until(EC.presence_of_element_located((By.ID, 'videowrapper_mplayer')))

# select the videos parent tag
videowrapper = driver.find_element(By.ID, 'videowrapper_mplayer')

# get the iframe displaying the video (this source iframes from another source)
iframe = videowrapper.find_element(By.TAG_NAME, 'iframe')
src = iframe.get_attribute("src")

# redirect to that iframe
driver.get(src)

# find another iframe that leads to ACTUAL source
iframe = driver.find_element(By.TAG_NAME, 'iframe')
link = iframe.get_attribute("src")

# redirect to that link
driver.get(link)

Теперь, с Selenium и JavaScript, нам нужно проверить сетевые запросы и найти файл «master.m3u8», который управляет технологией буферизации.

JS_get_network_requests = "var performance = window.performance || window.msPerformance || window.webkitPerformance || {}; var network = performance.getEntries() || {}; return network;"
network_requests = driver.execute_script(JS_get_network_requests)

Здесь мы используем JavaScript, чтобы легко взаимодействовать с сетевыми запросами. Попытка сделать это в самом Python потребует от нас более сложного подхода. Найдите минутку, чтобы просмотреть JavaScript и понять, что именно он делает (просто получает сетевые запросы в виде словаря).

Как только браузер Selenium выполнит этот код JavaScript, мы можем просмотреть объект network_requests и найти файл «master.m3u8».

for n in network_requests:
    if "master.m3u8" in n["name"]: 
        url = n["name"]

Если этот код не работает, убедитесь, что вы действительно пытаетесь загрузить из буферизованного видео, а не из тега ‹video›. Если вы в этом уверены, пройдитесь по сетевым запросам сайта видеоплеера и убедитесь, что загружаемый файл m3u8 называется master. Если это не так, измените имя в коде.

Теперь, когда мы получили URL-адрес файла master.m3u8, нам нужно использовать специальную библиотеку Python, которая позволяет нам читать файл и искать имена видео.

pip install m3u8
import m3u8

r = requests.get(url) # get the master.m3u8 file
m3u8_master = m3u8.loads(r.text) # convert to a readable format
playlist_url = m3u8_master.data["playlists"][0]['uri'] # get the URL of the playlist containing all video segments
r = requests.get(playlist_url) # get the playlist file
playlist = m3u8.loads(r.text) # load into readable format

Как вы можете видеть здесь, файл m3u8 разделен на списки воспроизведения, которые мы можем просмотреть. В моем случае файл m3u8 содержал список воспроизведения для аудио и список воспроизведения для видео. Мне нужно повторно использовать этот код для обоих. Если в вашем файле m3u8 нет двух списков воспроизведения, вы можете сделать это один раз.

Мы собираемся использовать файл m3u8 для загрузки текста запроса в удобочитаемый формат Python (по сути, словарь) и искать разные списки воспроизведения. Каждый список воспроизведения содержит ссылку на другой файл m3u8, который мы можем легко использовать с запросами, чтобы затем снова загрузить m3u8.

Мы собираемся использовать запросы, чтобы получить этот playlist_url, а затем написать файл .ts, который объединяет каждый сегмент в списке воспроизведения.

r = requests.get(playlist.data['segments'][0]['uri']) # get the URL of the segments

with open('video.ts', 'wb') as f:
    for segment in playlist.data['segments']: # go through each segment and write it to the file
        url = segment['uri']
        r = requests.get(url)
        
        f.write(r.content)

Мы снова находим другой «плейлист» внутри нашего плейлиста для сегментов, затем получаем его и загружаем в словарь. Теперь мы перебираем сегменты и получаем каждый suburl.

Когда вы запустите это, вы заметите, что в вашем каталоге появится новый файл, который должен содержать загруженное видео. Если в вашем файле m3u8 есть только один список воспроизведения, вы должны были успешно загрузить видео и аудио в один файл.

Если в вашем файле m3u8 есть два списка воспроизведения, продолжайте работу с этим руководством, и мы повторим те же шаги, чтобы получить звук.

for n in network_requests: # reassign url to the master.m3u8 url
    if "master.m3u8" in n["name"]: 
        url = n["name"]

r = requests.get(url)
m3u8_master = m3u8.loads(r.text)
playlist_url = m3u8_master.data["media"][1]['uri'] # get audio playlist url
r = requests.get(playlist_url)
playlist = m3u8.loads(r.text)

r = requests.get(playlist.data['segments'][0]['uri']) # get the segments

with open('audio.ts', 'wb') as f:
    for segment in playlist.data['segments']: # write each segment to a new file
        url = segment['uri']
        r = requests.get(url)
        
        f.write(r.content)

Этот повторяющийся фрагмент кода можно экстраполировать на функцию, но ради урока я этого делать не буду.

Теперь у нас есть проблема, у нас есть два файла, которые нам нужно объединить. Лучшим решением для этого будет использование команды FFmpeg (не нужно изобретать велосипед).

Загрузите FFmpeg и добавьте его в PATH (или вашу виртуальную среду), чтобы мы могли использовать его в нашем коде.

import subprocess

# subprocess will allow us to execute a command

subprocess.run(['ffmpeg', '-i', 'video.ts', '-i', 'audio.ts', '-c', 'copy', 'output.ts'])
subprocess.run(['ffmpeg', '-i', 'output.ts', f'{file_name}.mp4'])

Этот фрагмент кода объединяет два наших файла .ts и преобразует этот вывод в файл mp4.

Давайте очистим и удалим ненужные файлы, которые мы скачали.

import os

os.remove("video.ts")
os.remove("audio.ts")
os.remove("output.ts")

На этом наш код завершен и должен полностью функционировать для загрузки видео с любого веб-сайта!

Теперь я отвечу на третий вопрос, почему это лучшая реализация.

Если бы мы записывали видео на экран, мы бы не смогли использовать машину на протяжении всего видео. С помощью этого метода коду требуется несколько минут, чтобы загрузить все видео, и вы можете перевести Selenium в безголовый режим, что позволит вам также использовать машину, пока код загружается в фоновом режиме.

Используя код, мы также открываем новые способы оптимизации и автоматизации. Мы можем перерабатывать код и использовать его для других проектов, а также постоянно его улучшать.

На этом я оставляю вас с тем, ради чего вы пришли сюда. Удачного кодирования!