Ранее я разбирал DNS-трафик домашней сети и анализировал 1233 публикации Хабра, чтобы понять, за что площадка даёт рейтинг. И там, и там был вопрос, который я обходил стороной: а чем, собственно, я выгрузил эту тысячу статей?

Не руками же копировал. И не скриптом на Python, который на двухсотом запросе получил бы 429 Too Many Requests и бан по IP. Я написал расширение для Chrome, которое сохраняет статьи Хабра в Markdown — по одной, пакетом или автоматически по расписанию, — и делает это достаточно вежливо, чтобы сервер не воспринимал его как атаку.

Ниже — как оно устроено, почему выгрузка чужого сайта это в первую очередь вопрос хороших манер к серверу, и где проходит граница между «сохранил статью для себя» и «спарсил контент». Расширение с открытым кодом, ссылки в конце.

Зачем вообще выгружать статьи

Три сценария, в которых это реально нужно.

Первый — личный архив. Хорошая техническая статья живёт, пока жив её автор и его аккаунт. Статьи удаляют, аккаунты уходят в ReadOnly, площадки меняют политику. Если материал важен для работы, разумно держать его копию у себя.

Второй — оффлайн-чтение. Markdown открывается где угодно, читается без сети, ищется по локальному диску, кладётся в Obsidian или любую заметочную систему.

Третий, мой — анализ. Чтобы посчитать, что общего у высокорейтинговых статей, мне нужна была тысяча публикаций в машиночитаемом виде: не HTML со всей вёрсткой, а чистый текст с метаданными — рейтинг, хабы, теги, дата. Расширение отдаёт ровно это: Markdown с YAML-заголовком, где все поля разложены по полочкам.

Важная оговорка, с которой начинается всё остальное: во всех трёх сценариях речь о личном использовании статей, которые ты и так читаешь. Не о создании зеркала Хабра, не о перепубликации чужого контента, не о массовом сливе для перепродажи. Это разница между «сохранить книгу из библиотеки в закладки» и «отксерить весь фонд и открыть свою библиотеку». Первое законно и нормально, второе — нет.

Правовая рамка, если коротко

Раз уж речь о выгрузке чужого сайта, стоит проговорить, почему это в принципе можно.

В российском праве есть статья 1335.1 ГК — про извлечение материалов из обнародованной базы данных. Она разрешает делать это без разрешения правообладателя в личных, научных и образовательных целях в оправданном объёме, а в иных целях — в объёме, составляющем несущественную часть базы.

Ключевые слова — «личные цели» и «несущественная часть». Инструмент, который сохраняет отдельные статьи, которые пользователь читает, к нему на диск и никуда их не публикует, укладывается в это ровно. Он не покушается на базу Хабра как таковую, не создаёт конкурирующий ресурс, не лишает площадку трафика — сохранённая копия остаётся у одного человека для его нужд.

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

Главное — не быть свиньёй по отношению к серверу

Вот тут основная инженерная мысль всей статьи, и она не про парсинг HTML.

Написать код, который выкачивает страницы, — тривиально: цикл, fetch, готово. Проблема в том, что наивный цикл отправляет запросы со скоростью, с которой их может генерировать процессор, — сотни в секунду. Для сервера это неотличимо от небольшой DDoS-атаки, и любой нормальный сервер на это реагирует: сначала 429 Too Many Requests, потом временный бан по IP.

Наивный граббер упирается в защиту за секунды, вежливый — нет
Наивный граббер упирается в защиту за секунды, вежливый — нет

Поэтому первое, что заложено в расширение, — принудительный интервал между запросами. Минимум полторы секунды, и не в среднем, а гарантированно: каждый запрос ждёт своей очереди.

async function waitForSlot(minGapMs = 1500) {
  const now = Date.now();
  const wait = Math.max(0, minGapMs - (now - lastRequestAt));
  if (wait > 0) await sleep(wait);
  lastRequestAt = Date.now();
}

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

Circuit breaker: слушать, что говорит сервер

Интервала мало. Сервер может попросить притормозить и явно — ответив 429. Наивный код на это либо не смотрит вовсе, либо тупо повторяет запрос, усугубляя ситуацию.

Правильная реакция на 429перестать стучать вообще на некоторое время. Это паттерн «предохранитель» (circuit breaker): получили сигнал перегрузки — размыкаем цепь, ждём, потом пробуем снова.

if (response.status === 429) {
  openCircuit(180000);   // 3 минуты вообще не трогаем сервер
  throw new RateLimitError('HTTP 429 — Habr ограничил запросы');
}

После 429 расширение на три минуты полностью прекращает запросы — не «реже», а совсем. Сервер за это время приходит в себя, и только потом работа возобновляется.

Circuit breaker: реакция на сигнал сервера «притормози»
Circuit breaker: реакция на сигнал сервера «притормози»

Это принципиальный момент этики автоматизации: инструмент должен уважать сигналы сервера, а не игнорировать их. Разница между вежливым ботом и вредным — именно в том, слушает он 429 или долбит сквозь него.

И третий предохранитель — лимит на цикл. В режиме автоматического слежения расширение за один проход делает не больше восьми запросов, даже если новых статей вышло сто. Остальное подождёт следующего цикла. Никаких всплесков нагрузки.

Как это устроено технически

Теперь собственно механика, коротко, потому что после темы вежливости она уже простая.

Расширение работает на Manifest V3 — актуальном стандарте Chrome. Права запрошены минимальные и ровно по делу: доступ к активной вкладке, к загрузкам, к хранилищу настроек и к планировщику для режима слежения. Область действия ограничена одним доменом:

"host_permissions": ["https://habr.com/*"]

Никакого доступа ко всем сайтам, никакой отправки данных на сторонние серверы — расширение вообще не имеет своего бэкенда. Всё происходит локально в браузере, файлы пишутся только в папку загрузок.

Парсинг. Со страницы статьи достаётся тело публикации и метаданные — заголовок, автор, рейтинг, хабы, теги, дата, сложность. HTML тела прогоняется через Turndown (это опенсорсная библиотека HTML→Markdown) с плагином для таблиц и кода. На выходе — чистый Markdown с YAML-заголовком:

---
url: "https://habr.com/ru/articles/123456/"
title: "Заголовок статьи"
author: "username"
rating: 42
hubs: ["Python", "Data Mining"]
tags: ["парсинг", "markdown"]
---

Именно этот YAML-заголовок и сделал возможным анализ тысячи статей: рейтинг, хабы и теги уже разложены по полям, парсить повторно ничего не надо.

Тихие загрузки. По умолчанию Chrome на каждое скачивание показывает диалог «Сохранить как». При выгрузке сотни статей это сто окон. Расширение использует API загрузок так, чтобы файлы падали в заданную подпапку без диалога — иначе пакетный режим был бы пыткой.

Четыре режима выгрузки — от одного клика до автоархива
Четыре режима выгрузки — от одного клика до автоархива

Почему парсинг чужой вёрстки — это боль

Отдельно стоит сказать про хрупкость, потому что это главная головная боль любого граббера и причина, по которой они регулярно ломаются.

Расширение достаёт данные из HTML по CSS-селекторам вроде .tm-article-presenter__meta или .tm-publication-hub__link. Проблема в том, что это внутренние классы вёрстки Хабра, не публичный контракт. Площадка может в любой момент переименовать класс при редизайне — и парсер, который вчера работал, сегодня возвращает пустоту.

Защита от этого — не полагаться на один селектор. Для каждого поля заложена цепочка фолбэков: сначала пробуем основной способ, если не вышло — запасной. Например, хабы сначала ищутся в специальном блоке публикации, а если его нет — в общем списке метаданных по ключевому слову «Хабы»:

function getHubs(doc) {
  const hubs = [...doc.querySelectorAll('.tm-publication-hub__link')]
    .map(link => link.querySelector('span')?.textContent.trim())
    .filter(Boolean);
  return hubs.length ? hubs : getMetaList(doc, 'Хабы');  // фолбэк
}

То же со сложностью — она берётся то из отдельного поля, то вычисляется из класса вроде complexity-medium. Рейтинг ищется сразу по нескольким возможным селекторам счётчика голосов, потому что на разных типах публикаций он размечен по-разному.

Это неблагодарная, но необходимая работа: граббер чужого сайта живёт ровно до следующего редизайна, и единственное, что продлевает ему жизнь, — запасные пути на каждый чих.

Имя файла: мелочь, без которой архив превращается в помойку

Когда статей одна-две, имя файла неважно. Когда их тысяча — критично, иначе в папке каша из article(1).md, article(2).md.

Поэтому имя собирается по настраиваемому шаблону из метаданных: id публикации, слаг заголовка, дата, автор, тип. По умолчанию {id}_{title}, но можно задать своё:

const template = settings.filenameTemplate || '{id}_{title}';
let name = template
  .replace(/\{id\}/g, id)
  .replace(/\{title\}/g, slugify(meta.title, 30))
  .replace(/\{date\}/g, formatDate(meta.published))
  .replace(/\{author\}/g, sanitizePathPart(meta.author));

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

Четыре режима

Кратко, что умеет на уровне пользователя.

Кнопка на статье — самый частый сценарий. Прямо в шапке публикации появляется кнопка .md, клик — статья на диске. Кнопка помнит уже скачанное и показывает «Уже скачано», чтобы не плодить дубли.

Пакет — вставляешь список ссылок или загружаешь .txt, расширение выкачивает их по очереди в фоне, с тем самым интервалом в полторы секунды. Так я и собрал выборку для анализа.

Слежение — периодическая проверка лент и хабов, скачивается только новое. Для тех, кто ведёт архив по конкретной теме и хочет, чтобы он пополнялся сам.

Фильтры — поверх слежения: отбирать только статьи выше нужного рейтинга, из определённых хабов, с нужными тегами, или наоборот исключать. Чтобы в архив падало не всё подряд, а то, что действительно интересно.

Про лицензию и код

Расширение выложено с открытым исходным кодом под лицензией MIT — то есть это настоящий open source в определении OSI, делайте с ним что хотите, включая коммерческое использование и форки.

Оно опубликовано в Chrome Web Store, то есть прошло модерацию Google, и его можно поставить в один клик, не возясь с режимом разработчика. Но код открыт, так что можно и собрать из исходников, и посмотреть, что именно оно делает, — учитывая, что речь о расширении с доступом к вашему браузеру, это правильная привычка.

Итог

Выгрузка статей с чужого сайта — это на 20% парсинг HTML и на 80% хорошие манеры по отношению к серверу. Наивный качатель на голом fetch пишется за пять минут и получает бан на десятой минуте. Инструмент, которым можно пользоваться постоянно, отличается тремя вещами: держит интервал между запросами, слушает 429 и замолкает, когда сервер просит, и работает с открытыми статьями для личных нужд, а не сливает базу.

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

Код открыт под MIT: github.com/ShyDamn/habr-article-downloader. Ставится из Chrome Web Store в один клик. Если пользуетесь и чего-то не хватает — issues и pull request’ы открыты.

А как вы храните важные статьи, чтобы не потерять? Закладки не в счёт — они умирают вместе со страницей.

Комментарии (10)


  1. LittleHornet
    10.08.2026 09:18

    Приятного общения с герром майором.


    1. ShyDamn Автор
      10.08.2026 09:18

      Расширение не обходит никакой защиты. Оно читает открытые статьи через сессию самого пользователя, держит полторы секунды между запросами и замолкает на три минуты по первому же 429. Сохраняет локально, с указанием автора и ссылкой на оригинал, никуда не публикует.

      По ГК на личное использование обнародованного есть 1273 и 1335.1, а соглашение Хабра само делает исключение для личного некоммерческого сохранения с сохранением атрибуции. Так что герру майору показывать особо нечего :)


  1. a3d
    10.08.2026 09:18

    Хм. неплохо, сразу же появилась идея, многие бы думаю порадовались выложенному архиву хабра на какой-то трекер в md формате без всякого г вокруг и что бы статьи которые он не выдаёт для русских ип, тоже конечно выкачались. А потом было бы неплохо еще включить механизм фильтрации от откровенного нейрослопа, ну точнее маркировка таких статей, что бы их можно выключить было из поиска и просмотра. Ну а ещё удобный поиск, сортировка, катологизация.


    1. ShyDamn Автор
      10.08.2026 09:18

      Интересная идея, спасибо, подумаю над этим на досуге :)


  1. Emelian
    10.08.2026 09:18

    А как вы храните важные статьи, чтобы не потерять?

    Наваял скрипт на Питоне, с помощью бесплатного ИИ-сервиса, который загружает мои статьи из Хабра на компьютер, причем, отображение максимально похожее, но, не требующее Интернета. Эти статьи я, постепенно (по мере отсутствия лени), выкладываю, затем, на свой сайт: https://lecole.free.nf/articles.php .


    1. ShyDamn Автор
      10.08.2026 09:18

      Прикольно сделали, спасибо, что поделились


  1. a3d
    10.08.2026 09:18

    А что насчёт картинок, md же поддерживает локальные ссылки на png/jpg. А расширение не выкачивает их?

    Упс, вот оно, кто тоже с ходу не нашёл настройка - тут. Протестировал как работает, слежение в том числе. Это великолепно читать хабр в своём любимом редакторе .md без смс и регистраци без спама и с фильтрацией тегов! Спасибо за отличный плагин.


  1. AlexGorky
    10.08.2026 09:18

    В силу возраста и плохого зрения много читать могу только "с бумаги".
    Поэтому всегда "ручками" копировал статьи в Ворд и распечатывал для дальнейшего чтения.
    За расширение - спасибо!


  1. Revertis
    10.08.2026 09:18

    В российском праве есть статья 1335.1 ГК — про извлечение материалов из обнародованной базы данных. Она разрешает делать это без разрешения правообладателя в личных, научных и образовательных целях в оправданном объёме, а в иных целях — в объёме, составляющем несущественную часть базы.

    Интересно, а поисковики как-то по-другому работают? Они ведь стараются скачать и проиндексировать существенную часть базы.


    1. ShyDamn Автор
      10.08.2026 09:18

      Поисковики на это не опираются. В сайтах зашит robots.txt - по сути это разрешение от сайта, что и в каком объёме индексировать.И поисковик не замещает источник, тк он отдаёт сниппет со ссылкой и возвращает читателя на площадку. В той же 1335.1 ключевое ограничение звучит как «необоснованное ущемление интересов изготовителя базы», а поисковик их наоборот обслуживает. Плюс у поисковиков есть отдельный статус информационного посредника (1253.1 ГК) со своим режимом ответственности, а это вообще другая правовая конструкция, чем извлечение материалов пользователем.