Есть немало страниц, за которыми стоит следить: страница цен конкурента, статус наличия товара, политика или условия использования, доска объявлений о работе, страница с примечаниями к релизу. Информация публична, но именно изменение, это сигнал, который вам важен, а обновлять вкладку вручную не масштабируется больше чем на пару страниц. Нужен скрипт, который проверяет сам и сообщает только тогда, когда что-то изменилось.
В этом руководстве показано, как создать трекер изменений сайта на Python надёжным способом. Вы создадите небольшой работающий инструмент, который загружает страницу через Crawling API, извлекает значимый текст, вычисляет SHA-256 отпечаток, сохраняет каждый снимок на диск, сравнивает новый отпечаток с предыдущим для обнаружения изменения и запускает всю проверку по расписанию. Подход универсален: он работает с любой публичной страницей, на которую вы его направите, а не с одним конкретным сайтом.
Что вы создадите
Python-скрипт, который принимает один или несколько публичных URL, получает каждую страницу через Crawling API, сводит её к сравнимому тексту, вычисляет отпечаток и сообщает, изменилась ли страница с момента последнего запуска. Каждый компонент, небольшая функция, которую можно читать и переиспользовать. Компоненты:
- Фетчер получает HTML страницы через Crawling API, чтобы блокировки и рендеринг JavaScript обрабатывались автоматически.
- Экстрактор убирает скрипты, стили, навигацию и подвалы, оставляя читаемый текст тела.
- Отпечаток, SHA-256 хэш очищенного текста, при котором одно изменённое слово даёт полностью другое значение.
- Хранилище, JSON-файл, сопоставляющий каждый URL с последним отпечатком, плюс последний текст для диффа.
- Компаратор загружает предыдущий отпечаток, сравнивает и сообщает «изменилось» или «не изменилось».
- Планировщик, цикл с интервалом сна или запись в cron, чтобы проверка запускалась самостоятельно.
Почему обычного запроса недостаточно
Можно написать это с голым HTTP-клиентом и вообще не использовать API, на простой статической странице это даже будет работать. Проблемы начинаются с реальными целями. Многие сайты напрямую дросселируют или блокируют автоматизированные запросы: IP датацентра, обращающийся к одному URL с фиксированным интервалом, лёгкий паттерн для детектирования, а монитор по определению является автоматизированным трафиком. Другие страницы строят свой контент в браузере с помощью JavaScript, поэтому «сырой» HTML, который возвращает обычный запрос, почти пустая оболочка, и ваш отпечаток в итоге отслеживает оболочку, а не контент, который вы хотели наблюдать.
Надёжный трекер нуждается в двух вещах от каждой загрузки: IP, который сайт воспринимает как реального посетителя, и, когда страница рендерится на клиенте, браузер, запускающий скрипты перед возвратом HTML. Можно собрать это самостоятельно с headless-браузером и пулом ротирующих резидентных прокси, но поддержание этого стека в рабочем состоянии, основная часть усилий. Crawling API складывает оба требования в один вызов: отправьте URL, опционально JavaScript-токен, и он возвращает готовый HTML для отпечатка.
Есть и вторая причина, по которой само сравнение должно быть аккуратным, отдельно от способа загрузки. Сырой HTML меняется почти при каждом запросе в аспектах, которые вас не интересуют: встроенные скрипты, рекламные блоки, временные метки, CSRF-токены, динамические виджеты, всё это меняется, пока реальный контент остаётся тем же. Если хэшировать «сырой» ответ, на почти каждом запуске будет ложное срабатывание. Сначала свести страницу к читаемому тексту, вот что делает отпечаток реальным сигналом, а не шумом.
Предварительные требования
Несколько вещей нужно подготовить заранее. Ни одна из них не займёт много времени.
Базовые знания Python. Вы должны уметь писать и запускать скрипты и устанавливать пакеты через pip. Если BeautifulSoup для вас в новинку, наше руководство по использованию BeautifulSoup в Python охватывает парсинг, который предполагается в этом руководстве.
Python 3.10 или новее. Проверьте версию командой python --version. Код использует синтаксис type-hint str | None, для которого нужна версия 3.10. Если её нет, установите с python.org.
Аккаунт и токен Crawlbase. Зарегистрируйтесь, откройте дашборд и скопируйте токен со страницы документации аккаунта. Бесплатный тариф включает до 20 000 запросов, которых достаточно для тестирования трекера. Обращайтесь с токеном как с паролем и держите его вне системы контроля версий: код ниже считывает его из переменной окружения CRAWLBASE_TOKEN.
Настройка проекта
Создайте виртуальную среду, чтобы зависимости были изолированы, затем установите две сторонние библиотеки. Хэширование, хранение, планирование и диффы используют стандартную библиотеку Python (hashlib, json, time и difflib), поэтому для них ничего дополнительно устанавливать не нужно.
python --version python -m venv tracker_env source tracker_env/bin/activate pip install requests beautifulsoup4
На Windows активируйте среду командой tracker_env\Scripts\activate вместо строки с source. Две зависимости выполняют основную работу: requests отправляет HTTP-запрос к Crawling API, а beautifulsoup4 парсит возвращаемый HTML, чтобы извлечь читаемый текст.
Шаг 1: Загрузите страницу через Crawling API
Начните с подтверждения того, что страница вообще загружается. Функция ниже считывает токен, формирует URL запроса к Crawling API с целевой страницей в URL-кодировке, отправляет запрос и возвращает HTML. Проверка статуса ответа делает сбои явными, а не тихими.
import os from urllib.parse import quote import requests CRAWLBASE_API_URL = "https://api.crawlbase.com" def fetch_page(url: str, token: str | None = None) -> str: api_token = token or os.environ.get("CRAWLBASE_TOKEN", "") if not api_token: raise ValueError("Set CRAWLBASE_TOKEN or pass token=") api_url = f"{CRAWLBASE_API_URL}/?token={api_token}&url={quote(url)}" response = requests.get(api_url, timeout=30) response.raise_for_status() return response.text if __name__ == "__main__": html = fetch_page("https://example.com") print(html[:300])
Запустите с установленным токеном (export CRAWLBASE_TOKEN="your_token"), и вы должны увидеть первые несколько сотен символов реального HTML страницы. Это единственное подтверждение важно: оно доказывает, что запрос достигает страницы и возвращается с контентом до того, как вы что-то строите поверх. Вызовы timeout=30 и raise_for_status() намеренны, и раздел об обработке ошибок ниже опирается на оба. Если цель рендерит контент с JavaScript, используйте вместо стандартного JavaScript-токен, чтобы страница рендерилась перед возвратом HTML.
Тот первый вызов fetch_page вернул реальный HTML без необходимости управлять хотя бы одним прокси. Crawling API берёт на себя блокировки, дросселирование и CAPTCHA-вызовы, которые топят голый цикл запросов, и ротирует через доверенные IP на стороне сервера, чтобы долгоживущий монитор продолжал получать чистый HTML для отпечатка вместо блокировки. Добавьте JavaScript-токен, и он рендерит клиентские страницы перед их возвратом. Для начала направьте его на публичную страницу в бесплатном тарифе.
Шаг 2: Извлечение и создание отпечатка контента
Сравнение «сырого» HTML ненадёжно, поэтому следующий шаг, свести страницу к читаемому тексту, а затем хэшировать его. Экстрактор загружает HTML в BeautifulSoup, убирает элементы, которые меняются без смысловой нагрузки (script, style, nav, footer), извлекает видимый текст и схлопывает пробелы, чтобы косметические изменения форматирования не регистрировались как изменения.
import hashlib from bs4 import BeautifulSoup def extract_monitorable_text(html: str) -> str: soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "nav", "footer"]): tag.decompose() text = soup.get_text(separator=" ", strip=True) return " ".join(text.split()) def content_fingerprint(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest()
Отпечаток, это SHA-256 хэш очищенного текста. Хэш, строка фиксированной длины, производная от входных данных, и любое их изменение, даже один символ, даёт полностью другой вывод. Это свойство, именно то, что нужно трекеру: вместо хранения и байтового сравнения целых страниц вы храните одну 64-символьную строку на URL и сравниваете их. Сравнение быстрое, хранение минимальное, а мелкие правки всё равно фиксируются. Совместите это с нашим общим руководством по скрапингу на Python, если хотите расширить экстрактор для отслеживания конкретного региона страницы, а не всего тела.
Шаг 3: Хранение снимков и сравнение
Чтобы обнаружить изменение, инструмент должен помнить последний запуск. Состояние хранится в двух небольших JSON-файлах: snapshots.json сопоставляет каждый URL с последним отпечатком, а snapshots_text.json хранит последний извлечённый текст для показа человекочитаемого диффа при изменении. Функция загрузки возвращает пустой словарь при первом запуске вместо ошибки.
import json from pathlib import Path def load_json(path: str | Path) -> dict[str, str]: p = Path(path) if not p.exists(): return {} with open(p, encoding="utf-8") as f: return json.load(f) def save_json(data: dict[str, str], path: str | Path) -> None: with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) def check_for_change(url: str, current_hash: str, snapshots: dict[str, str]) -> bool: previous = snapshots.get(url) if previous is None: return True return previous != current_hash
Логика сравнения, сердце трекера, и она намеренно проста. check_for_change ищет сохранённый отпечаток URL. Если его нет, страница встречается первый раз, поэтому функция сообщает об изменении и новый отпечаток сохраняется. Если есть, возвращает, отличаются ли они. Первый запуск для любого URL всегда сообщает изменилось именно по этой причине, это ожидаемо, а не баг.
Теперь свяжите части в один запуск. Функция ниже обходит URL, загружает и вычисляет отпечаток для каждого, определяет, изменилось ли, выводит унифицированный дифф по сохранённому тексту при изменении и сохраняет обновлённое состояние в конце, чтобы следующий запуск имел что сравнивать. Дифф использует модуль стандартной библиотеки difflib, без дополнительных зависимостей.
import difflib def run_once(urls: list[str], hash_path="snapshots.json", text_path="snapshots_text.json") -> None: snapshots = load_json(hash_path) snapshot_texts = load_json(text_path) for url in urls: html = fetch_page(url) text = extract_monitorable_text(html) if not text: print(f"[warn] empty text, skipping {url}") continue fingerprint = content_fingerprint(text) if check_for_change(url, fingerprint, snapshots): print(f"[changed] {url}") old = snapshot_texts.get(url, "") diff = difflib.unified_diff( old.split(), text.split(), lineterm="", n=0) print(" ".join(diff)[:500]) else: print(f"[no change] {url}") snapshots[url] = fingerprint snapshot_texts[url] = text save_json(snapshots, hash_path) save_json(snapshot_texts, text_path)
Сохранение и отпечатка, и текста позволяет будущим запускам обнаруживать изменение и объяснять его. Отпечаток отвечает на вопрос «что-то изменилось?», а сохранённый текст позволяет difflib ответить на вопрос «что именно?». Если нужен только ответ «да/нет», можно убрать текстовый файл и оставить только карту отпечатков.
JSON-файлы идеальны для небольшого числа URL и легко проверяются вручную. Как только вы отслеживаете сотни страниц, замените функции загрузки и сохранения на SQLite через стандартный модуль sqlite3: он обрабатывает параллельные чтения, масштабируется на большие списки URL и хранит всё состояние в одном переносимом файле. Остальная часть скрипта не меняется.
Шаг 4: Запуск трекера по расписанию
Трекер изменений полезен только если запускается самостоятельно. Есть два чистых способа это сделать. Первый встроен в скрипт: опциональный интервальный цикл, перепроверяющий каждый URL каждые N секунд до остановки. Второй, позволить операционной системе запускать один проход по таймеру через cron. Ниже, точка входа CLI с режимами однократного запуска и интервального режима.
import argparse import time def main() -> None: parser = argparse.ArgumentParser(description="Website change tracker") parser.add_argument("urls", nargs="+", help="public URLs to monitor") parser.add_argument("--interval", type=float, metavar="SECONDS", help="re-check every SECONDS (e.g. 3600 for hourly); Ctrl+C to stop") args = parser.parse_args() while True: run_once(args.urls) if args.interval is None: break time.sleep(args.interval) if __name__ == "__main__": main()
Запустите одну проверку или держите цикл с почасовым интервалом:
export CRAWLBASE_TOKEN="your_token" # one pass, then exit python tracker.py https://example.com # check every hour until you stop it python tracker.py https://example.com --interval 3600
Интервальный цикл, простейший вариант, держащий процесс в одном месте, что удобно при тестировании. Для необслуживаемой production-установки cron обычно лучше: он переживает перезагрузки и не занимает терминал. Запись в crontab, запускающая один проход каждый час и добавляющая вывод в лог, выглядит так:
# run at the top of every hour 0 * * * * cd /path/to/project && \ CRAWLBASE_TOKEN=your_token \ ./tracker_env/bin/python tracker.py https://example.com >> tracker.log 2>&1
На Windows аналогом является Планировщик задач, запускающий ту же однопроходную команду по триггеру. В любом случае уберите флаг --interval, когда таймингом управляет cron или Планировщик задач, поскольку планировщик уже обрабатывает повторение.
Как выглядит вывод
Скрипт печатает одну строку на URL за каждый запуск, плюс сокращённый дифф при изменении страницы. В первый раз URL всегда сообщает об изменении, поскольку сохранённого отпечатка ещё нет, и снимок записывается для следующего раза:
# first run: no snapshot exists yet [changed] https://example.com # later run, content edited [changed] https://example.com --- +++ @@ -Old pricing copy +New pricing copy # later run, nothing moved [no change] https://example.com
Состояние на диске так же читаемо. snapshots.json, плоская карта URL к отпечатку, и это всё, что нужно для сравнения:
{ "https://example.com": "3e1f9c...a7d2", "https://example.com/pricing": "b04c88...11ef" }
Обработка ошибок и масштабирование
Долгоживущий монитор столкнётся со сбоями, и то, как он их обрабатывает, определяет, продолжит ли работу. Постоянно встречаются три случая. Таймауты: вызов requests.get(timeout=30) генерирует исключение, если API не отвечает вовремя, поэтому оберните загрузку и повторяйте с экспоненциальной задержкой, а не позволяйте одному медленному ответу остановить весь запуск. HTTP-ошибки: raise_for_status() превращает ответы 4xx и 5xx в исключения; логируйте статус и URL, затем пропускайте этот URL и продолжайте с остальными. Пустые извлечения: если extract_monitorable_text возвращает пустую строку, пропускайте сравнение и логируйте предупреждение вместо записи ложного изменения, что уже делает охрана if not text в run_once.
def fetch_with_retry(url: str, retries: int = 3, backoff: float = 2.0) -> str: for attempt in range(retries): try: return fetch_page(url) except requests.exceptions.RequestException: if attempt < retries - 1: time.sleep(backoff ** attempt) else: raise
Масштабирование с одной страницы до многих следует естественно. Отслеживать больше URL, значит просто передать более длинный список в run_once. Чтобы ускорить большие списки, загружайте параллельно через concurrent.futures.ThreadPoolExecutor, так как работа ограничена I/O. Для отслеживания сотен страниц перейдите от JSON к SQLite, как описано выше. А если какие-то целевые страницы рендерят контент на стороне клиента, используйте для загрузки JavaScript-токен, чтобы страница рендерилась до извлечения: наши заметки о скрапинге JavaScript-страниц на Python объясняют, когда это необходимо.
Ответственный мониторинг изменений
Трекер изменений, это автоматизированный трафик, поэтому запускайте его так, как хотели бы видеть его запущенным против вашего собственного сайта. Опрашивайте с периодичностью, соответствующей тому, как часто страница на самом деле меняется: каждые 15–60 минут для быстро меняющихся новостей или дашбордов, каждые несколько часов для цен и объявлений, ежедневно или еженедельно для политики и документации. Проверка статичной страницы каждую минуту добавляет расходы на запросы и нагрузку на источник без улучшения обнаружения, поэтому выбирайте наиболее медленный интервал, всё ещё фиксирующий то, что нужно.
Оставайтесь на правильной стороне источника. Отслеживайте только публичные страницы, загружаемые любым без аккаунта, и изучайте условия использования сайта и robots.txt перед запуском повторяющегося задания; относитесь к обоим как к границам собираемых данных. Держите объём запросов достаточно низким, чтобы не перегружать сервер, распределяйте проверки по целям вместо того, чтобы постоянно обращаться к одному URL, и снижайте интенсивность при ошибках или вызовах, а не повторяйте с большей настойчивостью. Если сайт предлагает официальный API или ленту изменений, предпочтите их: это путь, предназначенный сайтом для подобного использования, и он обычно более стабилен, чем парсинг HTML.
Ключевые выводы
- Отпечатки лучше прямого сравнения. Хэширование очищенного текста через SHA-256 превращает «изменилась ли страница» в быстрое сравнение двух 64-символьных строк вместо целых страниц.
- Извлекайте перед хэшированием. Удаление скриптов, стилей, навигации и подвалов и схлопывание пробелов не позволяет временным меткам и рекламным блокам вызывать ложные срабатывания.
-
Храните текст, не только хэш. Хранение последнего извлечённого текста рядом с отпечатком позволяет
difflibпоказать точно, что именно изменилось, а не только факт изменения. - Блокировки происходят на уровне загрузки. Crawling API обрабатывает рендеринг и ротацию доверенных IP, поэтому долгоживущий монитор продолжает получать чистый HTML вместо блокировки.
- Планируйте и будьте вежливы. Цикл со сном или запись в cron запускает проверку самостоятельно; соответствуйте интервалу частоте реальных изменений страницы и соблюдайте условия источника и robots.txt.
Часто задаваемые вопросы
Почему вычислять отпечаток текста, а не сравнивать «сырой» HTML?
«Сырой» HTML меняется почти в каждом запросе в аспектах, которые вас не интересуют: встроенные скрипты, рекламные блоки, временные метки и CSRF-токены смещаются, пока реальный контент остаётся тем же. Сравнение «сырого» HTML даёт ложное срабатывание почти при каждом запуске. Сначала сведение страницы к читаемому тексту, а затем его хэширование, это то, что делает отпечаток инструментом отслеживания именно контента, а не шума вокруг него.
Работает ли это на JavaScript-насыщенных сайтах?
Да, с одним изменением. Используйте JavaScript-токен с Crawling API вместо стандартного. Это рендерит полную страницу в реальном браузере перед возвратом HTML, поэтому клиентский контент присутствует при извлечении текста BeautifulSoup. Без этого клиентская страница возвращает почти пустую рамку, и ваш отпечаток в итоге отслеживает рамку, а не контент.
Можно ли отслеживать несколько страниц одновременно?
Да. Передайте несколько URL в командной строке, и скрипт обрабатывает их по очереди, храня один отпечаток на URL в файле снимков. Для больших списков загружайте параллельно через concurrent.futures.ThreadPoolExecutor, поскольку работа ограничена I/O, и рассмотрите переход на SQLite, чтобы состояние масштабировалось чисто.
Как получить оповещение при изменении?
Базовый скрипт сообщает об изменениях в stdout, что достаточно, если cron отправляет вам вывод. Для реального оповещения добавьте вызов в том месте, где check_for_change возвращает True: отправьте в вебхук Slack или Discord, вышлите email через транзакционный API или обратитесь к любому HTTP-эндпоинту. Это несколько строк, добавленных в ветку изменения в run_once.
Какой вариант хранения лучше всего подходит для отслеживания сотен URL?
Замените JSON-файлы на SQLite через стандартный модуль sqlite3. Он обрабатывает параллельные чтения, масштабируется на большие списки URL и хранит всё состояние в одном переносимом файле. Меняются только функции загрузки и сохранения; логика загрузки, извлечения, вычисления отпечатка и сравнения остаётся полностью той же.
Как часто должен запускаться трекер?
Соответствуйте интервалу частоте реальных изменений страницы. Быстро меняющиеся новости и дашборды оправдывают каждые 15–60 минут; страницы цен и товаров обычно в порядке через несколько часов; политику и документацию можно проверять ежедневно или еженедельно. Запуск намного чаще, чем меняется страница, только добавляет расходы на запросы и нагрузку на источник без фиксации чего-либо, что иначе было бы пропущено.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

