new version

This commit is contained in:
2026-08-03 21:44:03 +03:00
parent 1bde8e8ca3
commit a441fa106a
2 changed files with 349 additions and 118 deletions
+205 -82
View File
@@ -2,22 +2,69 @@
Скрипт для рекурсивного скачивания большой публичной папки Яндекс Диска, защищённой паролем.
Скрипт использует cookie `passToken`, скачивает каждый файл отдельно и не пытается сформировать единый ZIP-архив.
Он получает список объектов через веб-API Яндекс Диска, сохраняет структуру каталогов и скачивает каждый файл отдельно через `wget`.
## Возможности
* скачивание защищённой публичной папки;
* рекурсивный обход вложенных каталогов;
* сохранение исходной структуры папок;
* скачивание файлов по одному;
* продолжение прерванной загрузки через `wget --continue`;
* автоматический пропуск полностью скачанных файлов;
* проверка размера скачанного файла;
* автоматическая пагинация списка файлов;
* остановка при появлении CAPTCHA;
* сохранение ссылки CAPTCHA в текстовый файл;
* паузы между запросами для снижения вероятности блокировки;
* повторный запуск без потери уже скачанных данных.
- чтение всех пользовательских настроек из файла `.env` рядом со скриптом;
- использование готового cookie `passToken` либо автоматическое получение токена по паролю через Chromium и Playwright;
- постоянный профиль Chromium с повторным использованием cookies;
- рекурсивный обход вложенных каталогов;
- двухэтапная работа: сначала полный список, затем скачивание;
- сохранение полного списка в `yandex-download-manifest.jsonl`;
- исключение служебного элемента текущего каталога из расчёта `offset`;
- сохранение исходной структуры каталогов;
- продолжение прерванной загрузки через `wget --continue`;
- автоматический пропуск файлов, размер которых уже совпадает с серверным;
- проверка итогового размера каждого скачанного файла;
- паузы между запросами прямых ссылок и периодические длительные паузы;
- повторные HTTP-запросы при временных ошибках `429`, `500`, `502`, `503` и `504`;
## Как работает двухэтапная загрузка
### Этап 1 — получение полного списка
Скрипт быстро обходит страницы каталога:
```text
0 → 40 → 80 → 120 → ... → completed=True
```
На этом этапе:
- `download-url` не запрашивается;
- `wget` не запускается;
- файлы не скачиваются;
- для каждого файла сохраняются серверные метаданные и относительный каталог;
- вложенные каталоги также обходятся полностью;
- служебное описание текущей папки не учитывается при увеличении `offset`.
После успешного завершения обхода создаётся:
```text
OUTPUT_DIR/yandex-download-manifest.jsonl
```
Manifest записывается атомарно: сначала создаётся временный файл `.tmp`, после чего он заменяет предыдущий manifest.
Пример одной строки:
```json
{"relative_directory":".","item":{"name":"FL_9J1915234BP_1654_40013103010803_V001_S.frf","type":"file","size":793122,"path":"public_hash:/ODIS/Flashdaten/Brand-A/FL_9J1915234BP_1654_40013103010803_V001_S.frf"}}
```
### Этап 2 — скачивание файлов
После получения `completed=True` скрипт начинает скачивание по полностью собранному плану:
1. проверяет наличие локального файла;
2. сравнивает его размер с серверным;
3. пропускает полностью скачанный файл;
4. для отсутствующего или неполного файла получает временный `download-url`;
5. запускает `wget --continue`;
6. проверяет размер после завершения.
Текущая версия сохраняет manifest для контроля и последующего анализа, но при новом запуске всё равно заново выполняет этап 1 и получает актуальный список с Яндекса.
## Требования
@@ -29,38 +76,36 @@
sudo apt install -y python3 python3-requests wget
```
Playwright и Chromium для автоматического получения `passToken`:
```bash
python3 -m pip install --user playwright
python3 -m playwright install chromium
```
## Настройка
Все основные параметры находятся в начале файла:
Все основные параметры находятся в файле sample.env
```python
TARGET_URL = (
"https://disk.yandex.ru/d/"
"cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-V"
)
### `TARGET_URL`
PASS_TOKEN = ""
Полная публичная ссылка на нужный каталог:
OUTPUT_DIR = "/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V"
```dotenv
TARGET_URL="https://disk.yandex.ru/d/cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-A"
```
### TARGET_URL
Для вложенного каталога необходимо указывать полный путь после идентификатора публичной ссылки.
Полная ссылка на скачиваемую папку Яндекс Диска:
### `PUBLIC_LINK_PASSWORD`
```python
TARGET_URL = "https://disk.yandex.ru/d/cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-V"
Пароль публичной ссылки:
```dotenv
PUBLIC_LINK_PASSWORD="реальный пароль публичной ссылки"
```
### PASS_TOKEN
Значение cookie `passToken`, полученное после ввода пароля в браузере.
Можно указать только значение:
```python
PASS_TOKEN = "значение_cookie_passToken"
```
Если оставить значение пустым, программа попытается использовать cookies из постоянного профиля Chromium. Если действующего `passToken` нет и запуск производится из терминала, пароль будет запрошен без отображения введённых символов.
### OUTPUT_DIR
@@ -71,31 +116,131 @@ OUTPUT_DIR = "/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V"
```
Каталог будет создан автоматически, если у пользователя есть права на его создание.
Внутри него также могут появиться:
```text
yandex-download-manifest.jsonl
yandex-captcha-url.txt
```
### Настройки Playwright
```dotenv
PLAYWRIGHT_PROFILE_DIR="./playwright-profile"
PLAYWRIGHT_HEADLESS="false"
PLAYWRIGHT_TIMEOUT="120"
```
- `PLAYWRIGHT_PROFILE_DIR` — постоянный профиль Chromium;
- `PLAYWRIGHT_HEADLESS=false` — показывает окно браузера и позволяет вручную пройти CAPTCHA;
- `PLAYWRIGHT_HEADLESS=true` — запускает Chromium без окна; при CAPTCHA программа остановится;
- `PLAYWRIGHT_TIMEOUT` — максимальное время ожидания получения `passToken`, в секундах.
Относительный путь профиля вычисляется относительно каталога, где находится `.env` и скрипт.
### Задержки между страницами
```dotenv
PAGE_DELAY_MIN="0.5"
PAGE_DELAY_MAX="1.5"
```
Эти задержки используются только на этапе 1 между запросами `fetch-list`.
После перехода на двухэтапную схему страницы перечисляются подряд и больше не разделяются длительным скачиванием файлов. Поэтому состояние пагинации живёт значительно меньше времени.
### Задержки между файлами
```dotenv
FILE_DELAY_MIN="1.5"
FILE_DELAY_MAX="3.5"
```
Случайная пауза перед каждым запросом `download-url` на этапе 2.
### Длительные паузы
```dotenv
COOLDOWN_EVERY_FILES="100"
COOLDOWN_MIN="60"
COOLDOWN_MAX="120"
```
После каждых 100 запросов `download-url` программа дополнительно ждёт от 60 до 120 секунд.
Чтобы отключить длительные паузы:
```dotenv
COOLDOWN_EVERY_FILES="0"
COOLDOWN_MIN="0"
COOLDOWN_MAX="0"
```
### HTTP-таймауты и повторы
```dotenv
CONNECT_TIMEOUT="30"
READ_TIMEOUT="120"
HTTP_RETRIES="5"
```
- `CONNECT_TIMEOUT` — таймаут установки соединения;
- `READ_TIMEOUT` — таймаут чтения ответа API;
- `HTTP_RETRIES` — число автоматических повторов временных ошибок.
## Пример вывода
### Получение токена
```text
[AUTH] Открываю защищённую ссылку в Chromium...
[AUTH] Используется passToken из сохранённого профиля.
```
Либо:
```text
[AUTH] Поле пароля найдено (input[type="password"]).
[AUTH] Ввожу пароль публичной ссылки...
[AUTH] Пароль введён; отправляю форму...
[AUTH] passToken получен.
```
### Этап 1
```text
[INFO] Открываю исходную ссылку...
[INFO] Папка открыта.
[INFO] Корневой hash: public_hash:
[INFO] Корневой hash: public_hash:/ODIS/Flashdaten/Brand-A
[INFO] Этап 1/2: получаю полный список файлов...
[DIR ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V
[GET ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V/038997016C__7000.sgo (1.75 MiB)
[WAIT] 2.4 с перед запросом ссылки
2026-07-19 URL:https://downloader.disk.yandex.ru/... [1835008/1835008]
[PAGE] получено=41, следующий offset=41, completed=False
[WAIT] 4.2 с перед следующей страницей
[DIR ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A
[PAGE] получено API=41, дочерних=40, служебных=1, offset=0->40, completed=False
[WAIT] 0.8 с перед следующей страницей
[PAGE] получено API=10, дочерних=9, служебных=1, offset=12080->12089, completed=True
[INFO] Полный список получен: 12089 файлов.
[INFO] Manifest сохранён: /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/yandex-download-manifest.jsonl
```
После завершения выводится статистика:
### Этап 2
```text
[INFO] Этап 2/2: начинаю скачивание по сохранённому списку...
[SKIP] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/FL_0DL300014F_3123_idMA_sw.frf (2.14 MiB)
[GET ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/FL_9J1915234BP_1654_40013103010803_V001_S.frf (774.53 KiB)
[WAIT] 2.6 с перед запросом ссылки
2026-08-03 URL:https://s684vla.storage.yandex.net/rdisk/... [793122/793122]
```
### Итоговая статистика
```text
================ РЕЗУЛЬТАТ ================
Найдено файлов: 4500
Скачано сейчас: 250
Уже было скачано: 4250
Найдено файлов: 12089
Скачано сейчас: 529
Уже было скачано: 11560
Ошибок: 0
Известный общий объём: 115.40 GiB
Известный общий объём: 49.04 GiB
===========================================
```
@@ -129,48 +274,26 @@ https://disk.yandex.ru/showcaptcha?...
/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V/yandex-captcha-url.txt
```
## Настройка задержек
## Ошибка `409 Conflict`
### FILE_DELAY_MIN и FILE_DELAY_MAX
Внутренний API `/public/api/fetch-list` может вернуть `409 Conflict`, если серверное состояние пагинации изменилось или стало недействительным.
Случайная задержка перед запросом прямой ссылки на каждый файл:
Двухэтапная схема значительно снижает вероятность такой ошибки, потому что все страницы каталога запрашиваются до начала скачивания файлов.
```python
FILE_DELAY_MIN = 1.5
FILE_DELAY_MAX = 3.5
`409` не следует считать признаком конца каталога. Если он всё же возник на этапе 1:
1. не удаляйте уже скачанные файлы;
2. повторно запустите программу;
3. скрипт получит новый `sk` и начнёт перечисление заново;
4. на этапе 2 существующие файлы будут пропущены.
Полным считается только обход, завершившийся серверным признаком:
```text
completed=True
```
Скрипт будет ждать от 1.5 до 3.5 секунды перед каждым запросом `download-url`.
### PAGE_DELAY_MIN и PAGE_DELAY_MAX
Задержка между страницами списка файлов:
```python
PAGE_DELAY_MIN = 3.0
PAGE_DELAY_MAX = 6.0
```
### COOLDOWN_EVERY_FILES
Количество запросов файлов, после которого выполняется длинная пауза:
```python
COOLDOWN_EVERY_FILES = 100
```
При значении `100` длинная пауза выполняется после каждых 100 запросов.
### COOLDOWN_MIN и COOLDOWN_MAX
Диапазон длительной паузы:
```python
COOLDOWN_MIN = 60.0
COOLDOWN_MAX = 120.0
```
После каждых 100 запросов скрипт будет ждать от 60 до 120 секунд.
## Коды завершения
+144 -36
View File
@@ -1161,6 +1161,10 @@ class YandexProtectedFolderDownloader:
self.visited_directories: set[str] = set()
self.seen_files: set[str] = set()
self.planned_file_hashes: set[str] = set()
self.planned_files: list[
tuple[dict[str, Any], Path]
] = []
self.file_count = 0
self.downloaded_count = 0
@@ -1883,17 +1887,10 @@ class YandexProtectedFolderDownloader:
self.seen_files.add(resource_hash)
self.file_count += 1
expected_size = self.get_file_size(
item
)
if expected_size is not None:
self.total_known_bytes += (
expected_size
)
destination_directory = (
self.output_dir
/ relative_directory
@@ -2016,14 +2013,11 @@ class YandexProtectedFolderDownloader:
relative_directory: Path,
) -> list[tuple[str, str]]:
"""
Обработать страницу.
Добавить файлы страницы в план скачивания.
Возвращает список вложенных каталогов:
[
(hash_каталога, локальное_имя),
...
]
На первом этапе прямые ссылки не запрашиваются
и wget не запускается. Метод только сохраняет
описание файлов и возвращает вложенные каталоги.
"""
subdirectories: list[
@@ -2064,35 +2058,126 @@ class YandexProtectedFolderDownloader:
continue
if item_type == "file":
try:
self.download_file(
item,
relative_directory,
)
if item_type != "file":
continue
except CaptchaRequired:
# CAPTCHA должна остановить
# весь процесс, а не один файл.
raise
resource_hash = (
item.get("path")
or item.get("hash")
)
except Exception as exc:
local_path = (
self.output_dir
/ relative_directory
/ name
)
if (
not isinstance(resource_hash, str)
or not resource_hash
):
print(
"[WARN] У файла "
f"{name!r} отсутствует path/hash.",
file=sys.stderr,
)
self.failed_count += 1
continue
print(
f"[ERR ] {local_path}: "
f"{exc}",
file=sys.stderr,
)
if resource_hash in self.planned_file_hashes:
continue
self.failed_count += 1
self.planned_file_hashes.add(
resource_hash
)
self.planned_files.append(
(
item,
relative_directory,
)
)
self.file_count += 1
expected_size = self.get_file_size(
item
)
if expected_size is not None:
self.total_known_bytes += (
expected_size
)
return subdirectories
def save_manifest(self) -> Path:
"""Атомарно сохранить полный план скачивания в JSONL."""
manifest_path = (
self.output_dir
/ "yandex-download-manifest.jsonl"
)
temporary_path = manifest_path.with_suffix(
manifest_path.suffix + ".tmp"
)
with temporary_path.open(
"w",
encoding="utf-8",
) as manifest_file:
for item, relative_directory in self.planned_files:
record = {
"relative_directory": (
relative_directory.as_posix()
),
"item": item,
}
manifest_file.write(
json.dumps(
record,
ensure_ascii=False,
separators=(",", ":"),
)
+ "\n"
)
temporary_path.replace(
manifest_path
)
return manifest_path
def download_planned_files(self) -> None:
"""Скачать файлы из полностью собранного плана."""
for item, relative_directory in self.planned_files:
name = safe_component(
str(
item.get("name")
or "unnamed-file"
)
)
try:
self.download_file(
item,
relative_directory,
)
except CaptchaRequired:
raise
except Exception as exc:
local_path = (
self.output_dir
/ relative_directory
/ name
)
print(
f"[ERR ] {local_path}: {exc}",
file=sys.stderr,
)
self.failed_count += 1
def split_fetch_list_items(
self,
items: list[dict[str, Any]],
@@ -2451,11 +2536,34 @@ class YandexProtectedFolderDownloader:
f"{self.root_hash}"
)
print(
"[INFO] Этап 1/2: получаю полный список файлов..."
)
self.crawl_directory(
self.root_hash,
Path("."),
)
manifest_path = self.save_manifest()
print(
"[INFO] Полный список получен: "
f"{self.file_count} файлов."
)
print(
"[INFO] Manifest сохранён: "
f"{manifest_path}"
)
print(
"[INFO] Этап 2/2: начинаю скачивание "
"по сохранённому списку..."
)
self.download_planned_files()
self.print_summary()
def print_summary(self) -> None: