{
 "№": 54,
 "заголовок": "Как мы измеряли рекламные обещания регулярками и почему первая выгрузка соврала вдвое",
 "площадка": "Habr",
 "статус": "Опубликовано",
 "тип": "разбор",
 "контур": "спутник · разбор",
 "рубрика": "Франшизы",
 "кластер": "",
 "волна": "П1",
 "приоритет": "P1",
 "создана": "2026-09-01",
 "выход": null,
 "просмотры": 8602,
 "ключи": [],
 "ключи_нота": "материал писался под жанр площадки, а не под поисковый запрос: кластера у него нет",
 "ключи_кластер": null,
 "ключи_замер": null,
 "описание": null,
 "ключи_из_текста": [],
 "знаков": 15205,
 "слов": 2171,
 "файл": "завод/публикации/habr-01-izmerit-obeshchaniya-regulyarkami.md",
 "статус_текста": "ОПУБЛИКОВАНО на Habr 16.09.2026 - Песочница пройдена, модерация",
 "ссылки": {
  "опубликована": "https://habr.com/ru/articles/1083070/",
  "витрина": [],
  "документ": null,
  "документ_локальный": false,
  "страница": true
 },
 "картинки": [
  {
   "файл": "habr-01-slovo-i-smysl.jpg",
   "alt": "Шаблон находит слово, смысл он не проверяет: одна цитата со страницы и две её трактовки",
   "подпись": null,
   "адрес": null,
   "блок": 2,
   "обложка": false,
   "ключи": []
  }
 ],
 "картинки_нота": null,
 "иллюстрации": {
  "слов": 2173,
  "норма": 22,
  "в_тексте": 1,
  "ключи": [],
  "повторы": []
 },
 "площадка_правила": {
  "имя": "Habr",
  "адрес": "https://habr.com/ru/",
  "как_публикуем": "руками",
  "ссылки_вес": null,
  "ссылки_правило": null,
  "картинки": null,
  "объём": null,
  "регистрация": [
   "почта",
   "никнейм",
   "пароль дважды",
   "капча",
   "согласие с соглашением и политикой"
  ],
  "телефон": "нет",
  "аккаунт": "заведён",
  "статус_площадки": "РАБОТАЕТ, ЧЕТЫРЕ МАТЕРИАЛА. 16.09.2026 первый (регулярки, 8500 просмотров - лучший результат среди всех наших площадок), 21.09.2026 второй («Парсер, который не падает, а врёт», 6200), 30.09.2026 третий - «Источник данных, который закрыт и для меня, и для веб-архива»: https://habr.com/ru/articles/1088562/ , 13.10.2026 четвёртый - «Пять тысяч строк одного списка: собираю франчайзинговую сеть 1С по открытым данным и сверяю с ЕГРЮЛ»: https://habr.com/ru/articles/1093722/ . Отказов не было ни разу, второй, третий и четвёртый материалы вышли сразу в ленту, без песочницы. Ссылку на портал здесь не ставим вовсе - не из-за прогрева, а по правилам площадки: самопиар разрешён только в корпоративном блоге",
  "ограничения": [
   "для публикации нужна карма или инвайт - новый аккаунт пишет в песочницу, оттуда выпускают модераторы",
   "аудитория враждебна к маркетинговым текстам, наказывает кармой",
   "тематика франшиз ей чужая: заходить только с разборами с цифрами и без продающего тона",
   "есть платный корпоративный блог - отдельная статья расходов, решение руководителя",
   "проверено чужим опытом: у PR-завода Adonis четыре статьи подряд не прошли модерацию, площадку припарковали как неподходящую по теме (созвон 27.08.2026, материалы/разбор-видео-27.08.md)",
   "ПРОВЕРЕНО 09.09.2026, страница о Песочнице: полноправный аккаунт даётся через Песочницу, первая статья проходит модерацию и ждёт приглашения. Точно не пройдут: пресс-релизы, рекламные материалы, СГЕНЕРИРОВАННЫЕ материалы, тексты из списков более чем наполовину и статьи, слабо относящиеся к тематике сайта. Уникальность ниже 75% - почти отказ",
   "ПРОВЕРЕНО 09.09.2026: форма на account.habr.com - почта, никнейм, пароль дважды, капча и два чекбокса. Телефон не спрашивают",
   "ЧАСТОТА ПУБЛИКАЦИЙ, из справки Хабра 17.09.2026 (раздел «Публикации»). Формальный лимит мягкий: «если пользователь опубликовал три материала с интервалами менее 24 часов, то следующий он сможет опубликовать не ранее чем через 24 часа с момента последнего». То есть до трёх статей в сутки, дальше пауза сутки. Отдельно: в хаб «Я пиарюсь» нельзя две и более публикации подряд - но мы туда и не ходим. РЕАЛЬНОЕ ОГРАНИЧЕНИЕ НЕ В ЛИМИТЕ, А В КАРМЕ. Публиковать без модерации может только полноправный аккаунт с положительной кармой, а карму ставят читатели. Аудитория наказывает кармой за маркетинговый тон, и при уходе в минус публиковать станет нельзя. Поэтому темп держим по качеству темы, а не по лимиту: одна сильная техническая статья в неделю-полторы"
  ]
 },
 "чеклист": [
  {
   "что": "Ссылка на портал в тексте",
   "готово": false
  },
  {
   "что": "Заголовок совпадает с тем, что в плане",
   "готово": true
  },
  {
   "что": "Текст написан и лежит в заводе",
   "готово": true
  },
  {
   "что": "alt у каждой картинки",
   "готово": true
  },
  {
   "что": "Дисклеймер про доход и окупаемость, если в тексте есть такие цифры",
   "готово": false
  }
 ],
 "письмо": null,
 "размещение": [
  {
   "поле": "Хабы",
   "значение": "Python, Data Mining, Статистика в IT, Открытые данные (проверено 14.09.2026: хабов «Парсинг» и «Регулярные выражения» на площадке нет, только теги)"
  },
  {
   "поле": "Направление",
   "значение": "Инфраструктура и данные -> Системный и бизнес-анализ"
  }
 ],
 "анонс": null,
 "текст_html": "<h2>Как мы измеряли рекламные обещания регулярками и почему первая выгрузка соврала вдвое</h2>\n<figure class=\"статья-кадр статья-кадр--нет\" data-файл=\"habr-01-slovo-i-smysl.jpg\"><div>нет файла: <code>habr-01-slovo-i-smysl.jpg</code></div></figure>\n<p>Нам понадобились свои данные о том, как устроены страницы, продающие франшизы. Не мнение, не подборка скриншотов, а числа, которые можно пересчитать через полгода и сравнить. Готовых датасетов по этой нише нет, поэтому мы написали скрипт: он забирает шестнадцать страниц, ищет на них набор признаков и складывает результат в JSON.</p>\n<p>Первая выгрузка выглядела убедительно. Потом мы открыли страницы глазами и обнаружили, что по одному признаку скрипт ошибся больше чем вдвое. Эта статья про то, где именно регулярное выражение находит слово, но не находит смысл, и что с этим делать, если полностью уйти от регулярок нельзя.</p>\n<h3>Что мы вообще пытались измерить</h3>\n<p>Соблазн при разборе рекламных страниц - оценивать: честная страница или нечестная, адекватная цифра или завышенная. Такая оценка не воспроизводится: через полгода другой человек с тем же файлом получит другие числа.</p>\n<p>Поэтому мы сузили задачу до наличия. Скрипт отвечает только на вопрос «есть ли на странице такой блок или такое слово», и ничего не говорит о качестве. Признаков получилось шестнадцать, они делятся на три группы:</p>\n<ul><li>деньги: обещание прибыли рядом с числом, слово «окупаемость», сумма</li></ul>\n<p>вложений «от», паушальный взнос, роялти;</p>\n<ul><li>приёмы страницы: форма заявки, калькулятор или квиз, счётчик точек,</li></ul>\n<p>отзывы франчайзи, «под ключ», «без опыта»;</p>\n<ul><li>обещание и его страховка: слово «гарантия», оговорка к цифрам, упоминание</li></ul>\n<p>договора концессии или товарного знака.</p>\n<p>Разница между «страница обещает много» и «на странице есть слово \"окупаемость\"» кажется формальной, пока вы не начнёте считать. Она и стала главным источником ошибок.</p>\n<h3>Как набиралась выборка</h3>\n<p>Выборку мы собирали руками, и это её главное ограничение. Страницы искались поиском по типовым запросам вида «франшиза пекарни», «купить франшизу барбершопа», после чего в список попадала посадочная страница franchise с сайта сети. Получилось шестнадцать страниц в шести нишах: пекарни и кофейни, барбершопы, детские центры, доставка еды, маникюр, автомойка самообслуживания, плюс пункт выдачи заказов крупного маркетплейса.</p>\n<p>Ниши выбраны не случайно: это то, что чаще всего показывают человеку без опыта, когда он первый раз интересуется франшизой. Внутри ниши мы брали первые страницы выдачи и не отбирали их по содержанию: ни одна страница не выброшена из-за того, что показалась нам слишком агрессивной или слишком скромной.</p>\n<p>Это по-прежнему верхушка выдачи, а не рынок. Сети без рекламного бюджета в такую выборку не попадают вовсе, и любое обобщение на «рынок франшиз» из этих данных будет неправдой. Мы называем выборку выборкой, а не рынком, в каждом месте, где приводим доли.</p>\n<h3>Сбор: curl, кэш и порог в тысячу знаков</h3>\n<p>Собственно загрузка устроена скучно, и это намеренно. Страница берётся через curl с браузерным user-agent, ответ кладётся в кэш, дальше скрипт работает с кэшем:</p>\n<p>``<code>python def скачать(url, файл, из_кэша): if из_кэша and файл.exists(): return 200, файл.read_text(encoding=\"utf-8\", errors=\"replace\") готово = subprocess.run( [\"curl\", \"-sL\", \"--max-time\", \"25\", \"-A\", БРАУЗЕР, \"-w\", \"\\n===КОД:%{http_code}\", url], capture_output=True, text=True, timeout=40) </code>``</p>\n<p>Кэш здесь не оптимизация, а условие проверяемости. Рекламная страница меняется чаще, чем статья в блоге: за неделю на ней может смениться цифра, исчезнуть счётчик точек, появиться новая акция. Если у вас нет копии страницы на дату замера, ваши числа невозможно перепроверить, и спорить о них бессмысленно. Все спорные срабатывания мы потом разбирали именно по кэшу, а не по живому сайту.</p>\n<p>Текст из HTML достаётся в три прохода: выкидываем скрипты, стили и комментарии, потом теги, потом схлопываем пробелы и переводим в нижний регистр.</p>\n<p>``<code>python def текст_страницы(html_): т = re.sub(r\"&lt;script.<i>?&lt;/script&gt;|&lt;style.</i>?&lt;/style&gt;|&lt;!--.*?--&gt;\", \" \", html_, flags=re.S | re.I) т = re.sub(r\"&lt;[^&gt;]+&gt;\", \" \", т) т = re.sub(r\"&amp;nbsp;?|&amp;#160;?\", \" \", т) return re.sub(r\"\\s+\", \" \", т).lower() </code>``</p>\n<p>И сразу первое решение, о котором мы пожалели бы, если бы его не приняли: страницы, с которых пришло меньше тысячи знаков текста, не попадают в знаменатель.</p>\n<p>``<code>python МИНИМУМ_ЗНАКОВ = 1000 </code>``</p>\n<p>Одностраничник, который целиком рисуется скриптами после загрузки, отдаёт пустой каркас. Формально это строка в выборке, фактически измерять там нечего. Если оставить такую страницу в знаменателе, все доли поедут вниз: признак «не найден» не потому, что его нет, а потому, что нет текста. По этому порогу из шестнадцати страниц выпала одна, дальше мы считали по пятнадцати.</p>\n<h3>Словарь признаков</h3>\n<p>Каждый признак - регулярное выражение. Вот характерный фрагмент словаря:</p>\n<p>```python НАЧАЛО_СЛОВА = r\"(?&lt;![а-яёa-z])\" ЧИСЛО = r\"\\d[\\d\\s .,]<i>(?:\\d|млн|тыс|т\\.р|тысяч|миллион\\w</i>)\"</p>\n<p>ПРИЗНАКИ = { \"обещание_прибыли\": rf\"(?:прибыл[ьи]|доход\\w*|зарабат\\w+|выручк\\w+)\\D{{0,25}}{ЧИСЛО}\", \"слово_окупаемость\": НАЧАЛО_СЛОВА + r\"окупа\\w+\", \"паушальный_взнос\": r\"паушальн\\w+\", \"роялти\": r\"роялти\", \"договор_или_знак\": r\"концесси\\w+|товарн\\w+ знак|роспатент\", } ```</p>\n<p>Отдельно живут признаки, которые считаются только рядом с числом. Для них мало найти слово, нужно найти слово, у которого в окне в девяносто знаков стоит сумма или срок:</p>\n<p>``<code>python РЯДОМ_С_ЧИСЛОМ = { \"окупаемость_с_цифрой\": (НАЧАЛО_СЛОВА + r\"окупа\\w+\", ЧИСЛО), \"гарантия_у_денег\": (r\"гаранти(?:я|и|ю|ей|рован\\w*|ру\\w+)\", r\"доход\\w*|прибыл\\w*|выручк\\w*|окупа\\w+|\" + ЧИСЛО), } ОКНО = 90 </code>``</p>\n<p>Функция соседства возвращает не «да/нет», а цитату - кусок текста вокруг совпадения. Это мелочь, которая потом сэкономила нам день работы:</p>\n<p>``<code>python def рядом(текст, шаблон, сосед): \"\"\"Найти шаблон, рядом с которым в окне стоит сосед. Вернуть цитату.\"\"\" for м in re.finditer(шаблон, текст, flags=re.I): начало, конец = max(0, м.start() - ОКНО), м.end() + ОКНО кусок = текст[начало:конец] if re.search(сосед, кусок, flags=re.I): return кусок </code>``</p>\n<p>Если бы признак возвращал булево значение, проверить его можно было бы только заново открыв страницу и поискав глазами. Возвращая цитату, скрипт сам приносит доказательство, и спор о каждом срабатывании занимает секунды.</p>\n<h3>Где метод соврал</h3>\n<p>Дальше самое интересное. Мы прогнали первую версию, получили доли, и уже собирались их использовать. Спасло правило, которое мы держим для любой своей выгрузки: перед тем как публиковать число, открой десяток совпадений и посмотри на них глазами.</p>\n<p><b>Первое. «Окупаемость» нашлась там, где её нет.</b> Шаблон <code>окупа\\w+</code> честно срабатывал на слове «покупателей»: внутри него есть «окупа». Лечится проверкой на начало слова, тем самым <code>НАЧАЛО_СЛОВА</code> выше. Классическая ошибка, про которую все знают, и которая всё равно случается, когда пишешь словарь из шестнадцати шаблонов подряд.</p>\n<p><b>Второе. «Гарантия» оказалась не про деньги.</b> Здесь ошибка интереснее, потому что регулярка технически права, а метод - нет. Слово «гарантия» на странице франшизы встречается постоянно, но относится к чему угодно:</p>\n<ul><li>«это гарантирует высокий уровень профессионализма вашей команды» -</li></ul>\n<p>барбершоп, речь про обучение;</p>\n<ul><li>«как именно мы гарантируем качество и ту самую \"магию\" в каждом центре» -</li></ul>\n<p>детский центр, речь про услугу;</p>\n<ul><li>«имидж мойки самообслуживания, который гарантирует привлекательность» -</li></ul>\n<p>автомойка, речь про формат;</p>\n<ul><li>«гарантийный срок оборудования» - пекарня, речь про технику.</li></ul>\n<p>Обещание дохода из этого не следует ни в одном случае. Мы вынесли гарантийное обслуживание из шаблона отдельно, а сам признак «гарантия» разделили на два: слово где угодно и слово рядом с деньгами. Первый признак оказался почти бесполезным (он есть у половины выборки и ничего не значит), второй - редким и содержательным: гарантия рядом с доходом нашлась на двух страницах из пятнадцати.</p>\n<p><b>Третье. Слово не равно обещанию.</b> Страница, где написано «рассчитаем окупаемость вашей точки» в форме заявки, и страница, где написано «окупаемость 8 месяцев», по первой версии скрипта были одинаковыми. Отсюда правило «рядом с числом»: в свод идёт только второй случай.</p>\n<p><b>Четвёртое, и самое дорогое. Оговорка.</b> Признак «оговорка про цифры» ищет формулировки вида «не является публичной офертой», «носит информационный характер», «зависит от города и вашей работы». В первой выгрузке оговорка нашлась на семи страницах из шестнадцати. После разбора цитат осталось три из пятнадцати.</p>\n<p>Разница вдвое взялась из одного механизма: регулярка находила фразу, похожую на оговорку, но относящуюся не к обещанной цифре, а к чему-то соседнему. Живой пример из выборки доставки еды: «многое зависит от города и ваших стартовых инвестиций, поэтому мы совместно анализируем город». Шаблон <code>зависит от (?:города|...)</code> сработал. Но в контексте это не оговорка к прогнозу дохода, а описание того, как выбирают точку. Оговорки к цифрам на той странице нет.</p>\n<p>Если бы мы опубликовали первую выгрузку, главный вывод исследования поменялся бы на противоположный по смыслу: «почти половина страниц страхует свои цифры оговоркой» вместо «оговорку к цифрам даёт каждая пятая».</p>\n<h3>Ручная проверка как часть данных</h3>\n<p>Мы не стали править числа в файле молча. Каждое решение, принятое глазами, лежит отдельным файлом <code>ручная-правка.json</code>, и у каждой правки одинаковый набор полей: страница, признак, было, стало, цитата, почему.</p>\n<p>``<code>json { \"страница\": \"автомойка\", \"признак\": \"гарантия_у_денег\", \"было\": true, \"стало\": false, \"цитата\": \"имидж мойки самообслуживания, который гарантирует привлекательность автомойки\", \"почему\": \"гарантия относится к привлекательности формата, а не к доходу\" } </code>``</p>\n<p>Таких правок восемь. Рядом лежит <code>данные-до-правки.json</code> - исходная машинная выгрузка, которую никто не трогал.</p>\n<p>Это даёт две вещи. Во-первых, любой человек может не поверить нашему решению: он видит цитату и может сказать «нет, это всё-таки оговорка». Спор идёт о конкретной фразе, а не о добросовестности авторов. Во-вторых, разметку можно улучшать дальше: восемь правок - это, по сути, размеченные вручную примеры, на которых проверяется следующая версия шаблонов. Если новая регулярка после переделки снова срабатывает на «гарантирует привлекательность», значит, переделка не удалась.</p>\n<h3>Что получилось после правок</h3>\n<p>Итоговые числа по выборке из пятнадцати размеченных страниц:</p>\n<p>| Признак | Сколько страниц | Доля | |---|---|---| | форма заявки | 15 из 15 | 100% | | обещание прибыли рядом с числом | 12 из 15 | 80% | | окупаемость с цифрой | 12 из 15 | 80% | | роялти назван | 11 из 15 | 73% | | паушальный взнос назван | 10 из 15 | 67% | | калькулятор или квиз | 7 из 15 | 47% | | оговорка к цифрам | 3 из 15 | 20% | | упомянут договор или товарный знак | 3 из 15 | 20% | | гарантия рядом с деньгами | 2 из 15 | 13% |</p>\n<p>Если объединить два денежных признака, цифру дохода или окупаемости показывают тринадцать страниц из пятнадцати, а оговорку рядом с ней дают три. Две страницы выборки прогноза заработка не дают вовсе.</p>\n<p>Мы сознательно не пишем, хорошо это или плохо. Скрипт считает наличие блоков, и на этом его полномочия заканчиваются.</p>\n<h3>Почему регулярки, а не модель</h3>\n<p>Вопрос законный: задача выглядит как классификация коротких фрагментов текста, и языковая модель справилась бы с контекстом лучше любого шаблона. «Гарантируем качество обучения» и «гарантируем доход» она различит без окна в девяносто знаков.</p>\n<p>Мы всё же остались на регулярках, и вот по каким причинам.</p>\n<p>Пятнадцать страниц - это не тот масштаб, где ручная проверка становится непосильной. Весь разбор спорных срабатываний занял несколько часов, и эти часы дали нам восемь размеченных примеров, которых иначе не было бы.</p>\n<p>Регулярка объяснима до последнего символа. Когда вы публикуете долю и кто-то с ней спорит, вам нужно показать не «модель так решила», а шаблон и цитату, на которой он сработал. Ответ «мы спросили модель, она посчитала это оговоркой» в таком споре не работает: ваш собеседник не может его ни проверить, ни воспроизвести.</p>\n<p>Прогон повторяем и бесплатен. Скрипт с ключом <code>--из-кэша</code> считает те же шестнадцать страниц за секунды, сколько угодно раз, и всегда даёт тот же результат. Это важно, когда вы меняете один шаблон и хотите увидеть, что поехало.</p>\n<p>На масштабе в тысячи страниц мы бы рассуждали иначе: там ручная проверка перестаёт быть возможной, и уже регулярки становятся неприемлемо шумными. Разумный порядок, кажется, такой: регулярки как грубый фильтр, модель как второй проход по найденному, ручная проверка на случайной подвыборке - как контроль качества обоих слоёв.</p>\n<h3>Границы, которые мы записали рядом с числами</h3>\n<p>Каждое число выше верно только внутри своей выборки. Ограничения мы держим в том же файле, что и результат, чтобы их нельзя было потерять при пересказе:</p>\n<p>1. отбор не случайный, страницы пришли из верхних позиций поисковой выдачи, поэтому выборка смещена в сторону тех, кто вкладывается в продвижение; 2. признак говорит только о присутствии слова или блока и ничего - о его заметности: оговорка шестым кеглем внизу страницы и оговорка рядом с цифрой для скрипта неразличимы; 3. пятнадцать страниц описывают только эти пятнадцать страниц; чтобы говорить о нише целиком, нужен случайный отбор и объём на порядок больше; 4. часть содержимого дорисовывается скриптами после загрузки, и разметка этого не видит; 5. сервер, с которого шёл сбор, стоит за пределами страны, поэтому страница, закрытая от зарубежных адресов, могла не отдаться; 6. «цифра в начале страницы» считается по первой тысяче знаков текста в коде, это близко к первому экрану, но не равно ему; 7. спорные срабатывания проверены вручную и перечислены отдельным файлом.</p>\n<p>Пятый пункт стоит отдельного слова. География сервера - это не мелочь инфраструктуры, а ограничение выборки: она влияет на то, какие страницы вообще попали в данные.</p>\n<h3>Что мы вынесли из этой работы</h3>\n<p><b>Регулярка ищет слово, а исследование считает смысл.</b> Разрыв между ними не закрывается усложнением шаблона: чем длиннее выражение, тем труднее объяснить, что оно ловит. Дешевле оставить шаблон простым и добавить второй слой - соседство с числом, ручную проверку цитат.</p>\n<p><b>Возвращайте цитату, а не булево значение.</b> Это стоит трёх лишних строк и превращает проверку выгрузки из перечитывания страниц в чтение списка.</p>\n<p><b>Держите машинную выгрузку и выправленную версию раздельно.</b> Если файл один и он правится на месте, через месяц вы не ответите, какие числа получил скрипт, а какие поставил человек.</p>\n<p><b>Смотрите глазами на десяток совпадений, прежде чем публиковать долю.</b> Все четыре ошибки из этой статьи мы нашли именно так, а не тестами. Тест проверяет, что шаблон срабатывает на том, что вы задумали, но не покажет, на чём ещё он срабатывает в живом тексте.</p>\n<p><b>Ошибка метода не симметрична.</b> Наши шаблоны почти не пропускали признак, но регулярно находили лишнее. Для доли это значит, что машинное число - верхняя граница, и без ручной проверки его нельзя называть результатом.</p>\n<p>Скрипт, словарь признаков, обе выгрузки и файл правок мы держим рядом, одним набором: без них числа выше - просто девять строк в таблице, которым надо верить на слово.</p>",
 "есть_текст": true
}
