Приложение принимает список ссылок, параллельно открывает каждую в
stealth-Chrome (undetected-chromedriver поверх Selenium), дожидается полной
загрузки страницы и снимает полностраничный скриншот. Ход работы виден в
Streamlit в реальном времени: статус, прогресс, превью по мере готовности,
пауза, стоп и история прогонов. Снимки можно посмотреть прямо в приложении и
выгрузить архивом — и из текущего прогона, и из истории.
Поддержка систем. Разработано и проверено на macOS 26 (Apple Silicon).
Код кросс-платформенный, пути, определение версии Chrome и запуск разведены
по системам — на Windows и Linux приложение должно работать, но там оно
не тестировалось. Если на Windows что-то не заведётся, первым делом
uv run python -m monitor.doctor: он показывает, на каком именно шаге ломается.
Нужны три вещи: Google Chrome, uv и git. Python ставить отдельно не
нужно — uv сам скачает 3.12, указанный в .python-version.
# 1. Homebrew — если его ещё нет
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. uv и git
brew install uv git
# 3. Google Chrome — если не установлен
brew install --cask google-chrome
# 4. Проект
git clone https://github.com/ilianovi3/monitoring.git
cd monitoring
# 5. Зависимости и Python 3.12
make install
# 6. Проверка окружения — обязательный шаг
make doctor
# 7. Запуск
make runБез Homebrew uv ставится так: curl -LsSf https://astral.sh/uv/install.sh | sh
(после этого перезапустите терминал).
В PowerShell:
# 1. uv и git
winget install --id=astral-sh.uv -e
winget install --id=Git.Git -e
# 2. Google Chrome — если не установлен
winget install --id=Google.Chrome -e
# 3. Перезапустите PowerShell, чтобы обновился PATH, затем:
git clone https://github.com/ilianovi3/monitoring.git
cd monitoring
# 4. Зависимости и Python 3.12
uv python install 3.12
uv sync
# 5. Проверка окружения — обязательный шаг
uv run python -m monitor.doctor
# 6. Запуск
.\start.ps1Если winget недоступен, uv ставится так:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Если PowerShell отказывается выполнять .\start.ps1, разрешите скрипты для
текущего пользователя: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
Останов — .\stop.ps1.
curl -LsSf https://astral.sh/uv/install.sh | sh
sudo apt install -y git google-chrome-stable # или ваш пакетный менеджер
git clone https://github.com/ilianovi3/monitoring.git
cd monitoring
make install && make doctor && make runundetected-chromedriver не обновлялся с середины 2025 года, а Chrome выходит
каждые четыре недели. Доктор прогоняет всю цепочку — версия Chrome, платформа
драйвера, скачивание, патч, подпись, запуск браузера, тестовый скриншот,
проверка на детект — и, если что-то не так, называет конкретный шаг. Без него
поломка всплывёт посреди прогона на сотне ссылок.
На macOS и Linux — через make (make help покажет всё):
| Команда | Что делает |
|---|---|
make install |
Python 3.12 и зависимости |
make run |
запустить в фоне (PID в .run/app.pid, логи в logs/app.log) |
make dev |
запустить на переднем плане, логи в терминал |
make stop |
остановить приложение и добить оставшиеся Chrome |
make restart |
перезапустить |
make status |
запущено ли, сколько Chrome работает |
make logs |
следить за логом |
make doctor |
смоук-тест окружения |
make test |
юнит-тесты |
make info |
версии Chrome/chromedriver, объём данных |
make reinstall-driver |
снести кэш драйвера и скачать заново |
make clean-history / clean-screenshots / clean |
очистки |
Порт задаётся так: make run PORT=8600.
На Windows make обычно нет, поэтому те же операции — напрямую:
| Действие | Команда |
|---|---|
| Установка | uv python install 3.12; uv sync |
| Запуск в фоне | .\start.ps1 (или .\start.ps1 -Port 8600) |
| Запуск в терминале | uv run streamlit run app.py |
| Остановка | .\stop.ps1 |
| Логи | Get-Content logs\app.log -Wait |
| Смоук-тест | uv run python -m monitor.doctor |
| Тесты | uv run pytest -q |
| Состояние окружения | uv run python -m monitor.cli info |
| Переустановить драйвер | uv run python -m monitor.cli reinstall-driver |
| Очистить историю | uv run python -m monitor.cli clear-history |
| Удалить скриншоты | uv run python -m monitor.cli clear-screenshots |
- Вставьте ссылки в текстовое поле — по одной в строке (
\nили\r\n). Кнопка «Вставить из буфера обмена» добавляет содержимое буфера. Схема дописывается автоматически, дубликаты и мусор отбрасываются с пояснением. - Задайте число воркеров (1–8). Каждый воркер — отдельное окно Chrome со своим профилем, примерно 300–500 МБ памяти.
- «Старт». Лента обновляется раз в секунду сама.
- «Пауза» останавливает выдачу новых ссылок; страница, которая уже грузится, дорабатывается до конца. «Стоп» гасит прогон, недобранные ссылки помечаются пропущенными.
Окна Chrome открываются за пределами видимой области и работать не мешают — скриншот снимается через CDP, реальный экран для этого не нужен. Режим Headless в сайдбаре быстрее, но защиты вроде Cloudflare срабатывают на него заметно чаще.
Интерфейс браузера в скриншот не попадает: Page.captureScreenshot — это
снимок рендерера, а не окна, и никакими параметрами омнибокс туда не добавить.
Снять окно целиком умеет только ОС (screencapture -l на macOS, PrintWindow
на Windows), но для этого окно должно быть видимым на экране — у нас они
специально уводятся в -4000,-4000, а в headless окон нет вовсе; вдобавок ОС
снимет ровно вьюпорт, и полностраничный снимок потеряется.
Поэтому полоса с адресом дорисовывается над снимком (src/monitor/imaging.py):
омнибокс с замочком и итоговым адресом после редиректов, кнопки окна, светлая
тема Chrome. Результат детерминированный, одинаковый на всех платформах и в
headless, и корректно живёт с полностраничным снимком любой высоты. Высота
кадра растёт примерно на 48 px (полоса масштабируется от ширины снимка), сам
контент страницы не перекрывается. Отключается тумблером «Адресная строка на
снимке» в сайдбаре.
Отдавать в браузер исходные PNG нельзя: страница высотой 16384 px весит несколько мегабайт, и десяток таких карточек кладёт и вкладку, и память. Поэтому в ленте показываются уменьшенные JPEG-копии, а полный снимок открывается по кнопке «Открыть полностью» — по одному за раз, в модальном окне, вместе с кнопкой скачивания PNG.
Превью собираются воркером сразу после снимка (интерфейс никогда не декодирует многомегабайтные файлы) и кэшируются на диск под ключом параметров, поэтому смена ширины или режима не портит уже собранное. На практике 10 МБ скриншотов превращаются примерно в 130 КБ превью.
В сайдбаре, раздел «Просмотр»:
| Настройка | Что делает |
|---|---|
| Показывать превью | выключает картинки полностью — остаётся только список результатов, в браузер не уходит ни одного изображения |
| Карточек на странице | сколько снимков рисовать за раз; остальные — через переключатель страниц |
| Что показывать в превью | «Верх страницы» — крупно и читаемо; «Вся страница» — целиком, но длинный лендинг станет узкой полоской |
| Ширина превью | 200–800 px |
Высота превью ограничена полутора ширинами, иначе одна длинная страница растянула бы карточку на несколько экранов и перекосила ряд.
Из текущего прогона — блок «Выгрузка снимков» над лентой: галочки на карточках, кнопки «Выбрать все» / «Снять выделение», затем «Подготовить архив» и «Скачать архив».
Из истории — выделите строки в таблице (чекбоксы в первой колонке), под ней появятся размер выборки, кнопки выгрузки и превью выделенных снимков.
Обрезанная копия. Галочка «Добавлять копию 1080 px» (раздел «Выгрузка»
в сайдбаре, включена по умолчанию) кладёт в архив рядом с каждым оригиналом
вторую версию — верхние 1080 px кадра, с суффиксом _1080px в имени:
run_20260819_115711/example.com__327c3fda.png 1920×4312 — оригинал
run_20260819_115711/example.com__327c3fda_1080px.png 1920×1080 — обрезка
Копия делается на лету, на диске не хранится и на историю не влияет, так что галочку можно переключать между выгрузками — в том числе для старых прогонов. Если снимок и так не выше 1080 px, в архив уходят исходные байты без перекодирования. Та же пара кнопок есть в окне «Открыть полностью».
Архив собирается только по нажатию «Подготовить архив», а не на каждую перерисовку, и складывается без сжатия: PNG уже сжат, повторное сжатие даёт единицы процентов ценой заметного времени на десятках мегабайт. Если выбрано больше 300 МБ, приложение предупредит — такой архив целиком держится в памяти.
data/screenshots/run_20260801_173022/ подпапка на каждый прогон
example.com__327c3fda.png
en.wikipedia.org_wiki_Python__b43612ed.png
data/thumbnails/crop-w420-a1.5-q80/ кэш превью, ключ — параметры показа
run_20260801_173022/example.com__327c3fda.jpg
data/history/history.jsonl история, по записи на строку
data/.drivers/uc_driver_w0 личная копия драйвера для воркера
logs/app.log лог приложения
Превью пересобираются из исходников в любой момент, так что
data/thumbnails/ можно удалять свободно.
Скриншоты могут сниматься на одной системе, а разбираться на другой, поэтому имя удовлетворяет ограничениям macOS и Windows одновременно:
- убраны символы
< > : " / \ | ? *и управляющие; - имена DOS-устройств (
CON,PRN,COM1…) экранируются — Windows запрещает их и с расширением, то естьCON.pngтоже; - нет точки и пробела в конце (Windows) и в начале (на macOS это скрытый файл);
- IDN-домены переводятся в punycode, строка нормализуется в NFC (macOS хранит имена в NFD — без нормализации имя «поедет» после переноса);
- длина обрезается до 120 байт UTF-8, не разрывая символ (лимит APFS — 255 байт);
- в конце добавляется
__и 8 символов sha1 полного URL: после санитизации и обрезки разные адреса легко сходятся в одну читаемую часть.
src/monitor/engine.py— пул потоков. Потоки, а не процессы: работа I/O-bound, GIL не мешает, общее состояние проще. Воркеры никогда не обращаются кst.*(у фоновых потоков нет ScriptRunContext); UI читает состояние черезengine.snapshot(). Движок живёт в@st.cache_resource, поэтому переживает рероны Streamlit.src/monitor/capture.py— «полная загрузка» это лестница:readyState, затем сетевая тишина, затемdocument.fonts.ready, прокрутка для ленивых картинок, ожиданиеimg.completeи выдержка. Счётчик незавершённых запросов ставится через CDP до выполнения скриптов страницы:readyStateничего не знает про XHR и fetch, которыми SPA догружают контент.- Скриншот —
Page.captureScreenshotсcaptureBeyondViewport. Сторона ограничена 16384 px (потолок Chrome); если страница выше, снимок помечается обрезанным. src/monitor/imaging.py— постобработка на Pillow: адресная строка поверх снимка и обрезанная копия для архива. Размеры в ленте и истории остаются размерами страницы: дорисованная полоса — оформление снимка, а не свойство сайта, и потолок Chrome считается тоже по ним.
Пакет заморожен: релиз на PyPI — 3.5.5 (февраль 2024), последний коммит —
июль 2025. Ставится из git по пину коммита (в master новее зависимости), а в
src/monitor/uc_patch.py и driver.py чинятся четыре поломки, без которых он
не поднимается. Если решите обновить uc — проверьте, не стали ли патчи
лишними или, наоборот, недостаточными.
- Нет Apple Silicon.
patcher.py::_set_platform_nameдля darwin жёстко ставитmac-x64и вообще не смотрит наplatform.machine()— на M-чипе тянется Intel-сборка драйвера. Chrome for Testing публикуетmac-arm64. - distutils-API на packaging.Version.
patcher.pyимпортируетpackaging.version.Version as LooseVersion, но обращается к нему как к distutils-версии:release.version[0]иversion_full.vstring. Таких атрибутов нет — скачивание драйвера падает сAttributeError. Это и есть известное «uc не работает с Chrome 138+». Ломается на всех системах. - Невалидная подпись после патча (только macOS).
patch_exe()переписывает байты прямо в бинарнике, ad-hoc подпись перестаёт сходиться с содержимым, и на Apple Silicon ядро убивает драйвер сигналом 9 ещё до первой строки вывода (Selenium показывает это какStatus code was: -9). Лечитсяcodesign --force --sign -после патча. - Гонка при параллельном старте.
Patcher.auto()без своего пути удаляет и перекачивает драйвер, а защита от параллелизма нерабочая:patcher.pyпишетwith Lock():, конструируя новыйmultiprocessing.Lockна каждый вызов. Поэтому драйвер готовится один раз последовательно, а воркерам раздаются личные копии черезdriver_executable_path.
Ещё три места, где легко наступить:
- В
ChromeOptionsнельзя передавать--headless: uc на этот аргумент выполняетoptions.headless = True, а такой сеттер удалён в Selenium 4.13+. Headless включается только черезuc.Chrome(headless=...). - macOS ужимает окно под размер дисплея (запрошенные 1920×1080 превращаются,
например, в 1728×904). Если система обрезала окно, размер вьюпорта
навязывается через
Emulation.setDeviceMetricsOverride. - На Windows
chrome.exe --versionничего не печатает в stdout, поэтому версия читается из реестра (HKCU\Software\Google\Chrome\BLBeacon), затем из метаданных файла, затем из имени версионной подпапки.
| Симптом | Что делать |
|---|---|
doctor падает на загрузке драйвера |
make reinstall-driver (Windows: uv run python -m monitor.cli reinstall-driver) |
Status code was: -9 (macOS) |
подпись драйвера слетела — переустановите драйвер |
| Chrome обновился, драйвер старый | переустановите драйвер |
Google Chrome не найден |
доктор напечатает список проверенных путей; поправьте _chrome_candidates() в src/monitor/config.py |
| Остались висеть окна Chrome | make stop / .\stop.ps1 — добивают только процессы этого приложения |
| Сайт отдаёт капчу | выключите Headless в сайдбаре, уменьшите число воркеров |
| Скриншот обрезан | страница выше 16384 px — это потолок Chrome, не приложения |
| Лента тормозит | уменьшите «Карточек на странице» или выключите «Показывать превью» в сайдбаре |
| Превью не появилось | снимок мог не сохраниться — проверьте make logs; кэш можно снести, data/thumbnails/ пересоберётся |
PowerShell не запускает .ps1 |
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned |