{
 "№": 78,
 "заголовок": "Парсер, который не падает, а врёт: шесть случаев из моей практики",
 "площадка": "DTF",
 "статус": "Опубликовано",
 "тип": "разбор",
 "контур": "спутник · разбор",
 "рубрика": "Франшизы",
 "кластер": "",
 "волна": "П1",
 "приоритет": "P1",
 "создана": "2026-09-17",
 "выход": null,
 "просмотры": 1644,
 "ключи": [],
 "ключи_нота": "материал писался под жанр площадки, а не под поисковый запрос: кластера у него нет",
 "ключи_кластер": null,
 "ключи_замер": null,
 "описание": null,
 "ключи_из_текста": [],
 "знаков": 8870,
 "слов": 1290,
 "файл": "завод/публикации/ixbt-01-grabli-sbora.md",
 "статус_текста": "ОПУБЛИКОВАНО на DTF 25.09.2026 руководителем, подсайт howto",
 "ссылки": {
  "опубликована": "https://dtf.ru/howto/5318976-parsing-dannykh-bez-oshibok",
  "витрина": [
   "portal-frantlas/statyi-ploshchadok/dtf-post-3.html"
  ],
  "документ": null,
  "документ_локальный": true,
  "страница": true
 },
 "картинки": [
  {
   "файл": "ixbt-01-chislo.jpg",
   "alt": "Схема: строка «сегодня в 16:32 116 просмотров» превращается выражением «цифры и пробелы подряд» в число 32115, склеенное из времени и счётчика. Иллюстрация автора",
   "подпись": null,
   "адрес": "данные/картинки/ixbt-01-chislo.jpg",
   "блок": 34,
   "обложка": false,
   "ключи": []
  },
  {
   "файл": "ixbt-01-curl.jpg",
   "alt": "Схема: один и тот же адрес каталога отвечает кодом 421 Misdirected Request консольному клиенту и кодом 200 настоящему браузеру Chromium. Иллюстрация автора",
   "подпись": null,
   "адрес": "данные/картинки/ixbt-01-curl.jpg",
   "блок": 12,
   "обложка": false,
   "ключи": []
  },
  {
   "файл": "ixbt-01-none.jpg",
   "alt": "Схема: два ряда столбиков за неделю. В первом на месте неудачного замера стоит ноль, и в истории появляется провал, которого не было. Во втором замера просто нет, а прежнее значение остаётся. Иллюстрация автора",
   "подпись": null,
   "адрес": "данные/картинки/ixbt-01-none.jpg",
   "блок": 54,
   "обложка": false,
   "ключи": []
  },
  {
   "файл": "ixbt-01-prevyu.jpg",
   "alt": "Шесть граблей при сборе данных с чужих сайтов - числа, которые парсер отдавал как настоящие, 421 вместо 200, 62 рубля вместо 12 000, 32115 вместо 116",
   "подпись": null,
   "адрес": "данные/картинки/ixbt-01-prevyu.jpg",
   "блок": null,
   "обложка": false,
   "ключи": []
  }
 ],
 "картинки_нота": null,
 "иллюстрации": {
  "слов": 1451,
  "норма": 15,
  "в_тексте": 3,
  "ключи": [],
  "повторы": []
 },
 "площадка_правила": {
  "имя": "DTF",
  "адрес": "https://dtf.ru",
  "как_публикуем": "вход через Яндекс ID или почту, публикация через /editor. Блока «таблица» в редакторе нет: доступны текст, H2 и H3, фото, видео, ссылка, цитата, список, код, разделитель, аудио, опрос и «Персона». Таблицы отдаём картинками",
  "ссылки_вес": "ссылок нет",
  "ссылки_правило": "ЗАМЕР 14.09.2026: внешних ссылок в теле статей на DTF нет как явления - просмотрено 24 чужих материала из свежей и популярной лент, ни в одном ссылки на посторонний сайт в тексте не нашлось. Это согласуется с правилами площадки, которые прямо запрещают ссылки на коммерческие сайты. Считаем, что вес отсюда не передаётся вовсе",
  "картинки": null,
  "объём": null,
  "регистрация": [
   "Яндекс ID или почта"
  ],
  "телефон": "нет",
  "аккаунт": "заведён",
  "статус_площадки": "РАБОТАЕТ, ПЯТЬ МАТЕРИАЛОВ. 14.09.2026 первый (в личный блог, без подсайта), 21.09.2026 второй - в подсайт «Офтоп»: https://dtf.ru/flood/5310879-skolko-chelovek-dolzhno-sidet-v-klube-chtoby-soshlas-obeshannaya-vyruchka-schitayu-po-cifram-samih-franshiz . 25.09.2026 третий - в подсайт howto: https://dtf.ru/howto/5318976-parsing-dannykh-bez-oshibok - технический разбор про сбор данных, писался под iXBT Live и ушёл сюда после двух отказов там. 05.10.2026 четвёртый - в подсайт «Офтоп»: https://dtf.ru/flood/5336445-samozanyatye-ili-ip-schitayu-skolko-nalogov-otdayut-strimer-frilanser-dlya-studii-i-avtor-assetov . 12.10.2026 пятый - в подсайт «Офтоп»: https://dtf.ru/flood/5349549-chto-prodavat-v-2027-godu-esli-delaete-veshi-svoimi-rukami-proveril-spros-na-figurki-keikapy-i-amigurumi . Прогрев 3 из 3 пройден, но ссылок на портал нет и не будет: правила площадки запрещают",
  "ограничения": [
   "ПРОВЕРЕНО 09.09.2026: сайт и окно входа с сервера открываются",
   "ПРОВЕРЕНО 09.09.2026: вход и регистрация через Яндекс ID или почту, телефон отдельно не спрашивают. У нас Яндекс ID проекта уже есть",
   "ПРАВИЛА ПРОЧИТАНЫ 14.09.2026 (dtf.ru/rules): реклама и ссылки на коммерческие сайты запрещены прямо, блог только ради рекламы получает ограничения. Ссылок на портал отсюда не ставим вообще, а не только на время прогрева",
   "запрещены: полное копирование чужих материалов, фейки и искажение фактов, мат в заголовках, NSFW, токсичность. Политика не должна быть основной темой",
   "наказания идут по нарастающей: 3 часа, 6, 12, 24, 48, дальше дни и пермабан. Пост могут скрыть из лент, не удаляя",
   "тематика игровая и развлекательная. Деловой текст заходит только через игровой угол: разбор про бизнес компьютерных клубов здесь живёт, пост «Как открыть компьютерный клуб на 6-10 мест» собрал 19 тыс. просмотров и 243 комментария",
   "картинки площадка просит 16:9 и предупреждает, что другая пропорция покажется уже заголовка",
   "ПРОВЕРЕНО НА ПУБЛИКАЦИИ 14.09.2026: картинки принимаются только 1920x1080, файлы шириной 1400 px редактор отклонил при том же формате и весе. Подписи под картинками площадка сохраняет полностью"
  ]
 },
 "чеклист": [
  {
   "что": "Ссылок на портал в тексте быть не должно - площадка их запрещает",
   "готово": true
  },
  {
   "что": "Заголовок совпадает с тем, что в плане",
   "готово": true
  },
  {
   "что": "Текст написан и лежит в заводе",
   "готово": true
  },
  {
   "что": "Документ для редакции собран",
   "готово": true
  },
  {
   "что": "Черновик выложен на витрину - можно показать до публикации",
   "готово": true
  },
  {
   "что": "alt у каждой картинки",
   "готово": true
  },
  {
   "что": "Дисклеймер про доход и окупаемость, если в тексте есть такие цифры",
   "готово": true
  }
 ],
 "письмо": null,
 "размещение": [
  {
   "поле": "Раздел",
   "значение": "«ИИ, сервисы и приложения» (/live/blog/sw/) - это раздел iXBT, куда материал подавался и где его не взяли"
  },
  {
   "поле": "Теги",
   "значение": "парсинг, Python, веб-скрапинг, автоматизация, данные, разработка Проверено 17.09.2026, сколько постов в каждом: автоматизация 30, данные 26, разработка 24, Python 21, парсинг 3, веб-скрапинг 3"
  }
 ],
 "анонс": null,
 "текст_html": "<blockquote> Первый материал для iXBT Live. Аудитория техническая, поэтому тема не про франшизы, а про инструмент: грабли, на которые я наступила, собирая данные с чужих сайтов браузером.  <b>Чего нет намеренно:</b> названия портала, названия каталога, откуда шёл сбор, и любых ссылок. Правила площадки запрещают рекламу прямо и обещают бан не только автору, но и упомянутой компании.  <b>Иллюстрации свои</b>, в каждом alt стоит «Иллюстрация автора» - площадка отклонила первую версию именно за отсутствие подписей.  <b>ПЕРЕРАБОТКА 18.09.2026 после отказа.</b> Что изменено против первой версии: - добавлено семь блоков реального кода из наших скриптов. Для технической площадки код - главный признак, что автор руками делал то, о чём пишет; - точные сообщения вместо пересказа: 421 Misdirected Request, «227 | ★1» превратились в «2221», настоящие строки отчёта о пропусках; - названа цена каждой ошибки: месяц работы по архивным данным, полный пересбор 107 значений, три склейки за один день; - ритм сломан намеренно: где-то абзац в одну строку, где-то длинный разбор. Прежний текст был ровным шаблоном из шести одинаковых пунктов; - заголовок сменился с перечислительного «Шесть граблей» на утверждение «Парсер, который не падает, а врёт»; - пометка об ИИ переформулирована точнее: текст писал человек, ИИ-редактор использовался для вычитки. Так и есть, и так меньше поводов придраться.  <b>Пометку об ИИ убирать нельзя</b> - она обязательна по правилам площадки, даже при том, что именно из-за неё модератор ищет следы генерации.</blockquote>\n<h2>Парсер, который не падает, а врёт: шесть случаев из моей практики</h2>\n<p>За полтора месяца я собрал браузером около двухсот карточек товаров, счётчики просмотров с одиннадцати площадок и содержимое пары десятков объявлений. Данные нужны были не для продажи, а для собственной аналитики: я веду небольшой сайт и считаю по нему цифры.</p>\n<p>Все шесть случаев ниже объединяет одно свойство. Код не упал. Он отработал, записал результат в базу, и всё выглядело правильным ровно до того момента, пока я не полез проверять руками.</p>\n<h3>421 от сервера, который в браузере открывается</h3>\n<p>Первое, на что я потратил месяц.</p>\n<p>Каталог, из которого я собирал карточки, на обычный запрос из скрипта отвечал <code>421 Misdirected Request</code>. Не 403, не 429, а именно 421 - код, который обычно означает, что запрос ушёл не на тот сервер.</p>\n<p>Я решил, что нас забанили по адресу, и месяц собирал данные из веб-архива. Это отдельный вид мучения: архив режет частоту и после двух десятков страниц начинает отдавать отказы, а числа в нём августовские.</p>\n<p>Потом от отчаяния я открыл тот же адрес в headless Chromium через Playwright. Ответ 200, страница целиком, за полторы секунды.</p>\n<p>Заголовки я подделывал и раньше - User-Agent, Accept-Language, Referer. Не помогало. Дело оказалось не в них: сервер смотрит на то, как устроено само TLS-соединение и HTTP/2-рукопожатие. Порядок расширений, набор шифров, окна и приоритеты потоков у консольного клиента складываются в отпечаток, который не совпадает ни с одним живым браузером. Подделать это заголовками нельзя, проще взять настоящий браузер.</p>\n<p>Вывод для себя записала короткий: прежде чем считать сайт закрытым, открой его headless-браузером. Одна проверка - и месяц работы по протухшим данным не случился бы.</p>\n<figure class=\"статья-кадр\" data-файл=\"ixbt-01-curl.jpg\"><img src=\"данные/картинки/ixbt-01-curl.jpg\" alt=\"Схема: один и тот же адрес каталога отвечает кодом 421 Misdirected Request консольному клиенту и кодом 200 настоящему браузеру Chromium. Иллюстрация автора\" loading=\"lazy\"><figcaption>Схема: один и тот же адрес каталога отвечает кодом 421 Misdirected Request консольному клиенту и кодом 200 настоящему браузеру Chromium. Иллюстрация автора</figcaption></figure>\n<h3>Значение, которое пришло из бокового меню</h3>\n<p>Дальше было хуже, потому что эта ошибка тихая.</p>\n<p>Мне нужно было снять со страницы товара величину, подписанную словом «Роялти». Первый вариант кода был такой: разбираем текст страницы построчно, находим строку, равную метке, берём следующую.</p>\n<p>```python МЕТКА = re.compile(r\"^роялти\\s*:?\\s*$\", re.I)</p>\n<p>for н, строка in enumerate(строки): if МЕТКА.match(строка.strip()): return строки[н + 1] ```</p>\n<p>На ста страницах отработало нормально. Потом в собранных данных мне попалась карточка, где ежемесячный платёж составлял 62 рубля.</p>\n<p>Шестьдесят два рубля. Я пошёл смотреть страницу глазами.</p>\n<p>В боковом меню каталога есть фильтр, и один из его пунктов называется «Без роялти». На части страниц этот блок оказывается в разметке раньше карточки товара, и мой цикл честно находил первое совпадение - в меню.</p>\n<p>Лечится сужением области. Я перестал искать по всему тексту и начал брать самый маленький элемент, внутри которого одновременно встречаются все нужные поля:</p>\n<p>``<code>python блок = страница.evaluate(\"\"\"() =&gt; { const годится = [...document.querySelectorAll('div,section,ul,table')] .filter(э =&gt; { const т = э.innerText || ''; return /Роялти/i.test(т) &amp;&amp; /инвестиц/i.test(т) &amp;&amp; т.length &lt; 600; }) .map(э =&gt; ({э, n: э.innerText.length})) .sort((a, b) =&gt; a.n - b.n); return годится.length ? годится[0].э.innerText : null; }\"\"\") </code>``</p>\n<p>Ограничение по длине тут не для красоты: без него в выборку попадает <code>body</code>, в котором тоже есть оба слова.</p>\n<p>Сбор пришлось повторить с нуля. Доверия к 107 уже снятым значениям не осталось - я не мог сказать, какие из них из карточки, а какие из фильтра.</p>\n<h3>Пробел, который склеил время со счётчиком</h3>\n<p>Это классика, и я на неё наступил трижды за один день.</p>\n<p>Числа на русскоязычных сайтах пишут с разделителем тысяч, часто неразрывным пробелом. Поэтому в регулярке появляется что-то вроде <code>([\\d\\s]+)</code>. А потом попадается страница, где рядом стоит время публикации:</p>\n<p>``<code> сегодня в 16:32  116 просмотров </code>``</p>\n<p>Из этой строки жадное выражение достаёт <code>32115</code>. Формально всё верно: цифры и пробелы подряд, как просили.</p>\n<p>В тот день у меня так склеились время с числом просмотров, счётчик с рейтингом (<code>227 | ★1</code> превратились в <code>2221</code>) и просмотры с номером страницы. Три разных парсера, одна и та же причина.</p>\n<p>Рабочая версия выглядит так:</p>\n<p>```python</p>\n<h2>пробел допустим только как разделитель тысяч, то есть если после него</h2>\n<h2>ровно три цифры; слева не должно стоять ни цифры, ни двоеточия</h2>\n<p>ПРОСМОТРЫ = re.compile(r\"(?&lt;![\\d:])(\\d{1,3}(?:[  ]\\d{3})*)\\s*просмотр\") ```</p>\n<p>Обратите внимание на символы внутри класса: там обычный пробел и неразрывный <code> </code>. Если оставить только обычный, половина сайтов перестанет разбираться, и это тоже выяснится не сразу.</p>\n<figure class=\"статья-кадр\" data-файл=\"ixbt-01-chislo.jpg\"><img src=\"данные/картинки/ixbt-01-chislo.jpg\" alt=\"Схема: строка «сегодня в 16:32 116 просмотров» превращается выражением «цифры и пробелы подряд» в число 32115, склеенное из времени и счётчика. Иллюстрация автора\" loading=\"lazy\"><figcaption>Схема: строка «сегодня в 16:32 116 просмотров» превращается выражением «цифры и пробелы подряд» в число 32115, склеенное из времени и счётчика. Иллюстрация автора</figcaption></figure>\n<h3>Страница отдаёт 404 и показывает чужие цифры</h3>\n<p>Эту ловушку я знаю не по своему опыту, и рассказываю именно поэтому: она дороже всех остальных вместе взятых.</p>\n<p>Когда пост на крупной площадке снимают, страница отвечает кодом 404. Но тело у неё не пустое: сайт рисует ленту рекомендаций, и в разметке оказываются чужие посты со своими счётчиками.</p>\n<p>Парсер, который ищет «первый попавшийся элемент с классом счётчика», находит его у чужого материала. Знакомый рассказывал, как в их проекте после блокировки аккаунта пять статей трое суток показывали одинаковые 1,4 миллиона просмотров, растущие каждый час. Сумма в отчёте выросла с семидесяти тысяч до семи миллионов, и никто не усомнился, пока не открыл страницу руками.</p>\n<p>С тех пор у меня два обязательных условия в каждом сборщике. Первое: проверять код ответа и при 400 и выше выходить сразу, ничего не разбирая. Второе: искать счётчик не «где-нибудь», а внутри своего объекта по идентификатору:</p>\n<p>``<code>python м = re.search(r\"_(\\d+)/?$\", адрес.rstrip(\"/\")) область = f'.story[data-story-id=\"{м.group(1)}\"]' if м else \".story\" </code>``</p>\n<h3>Число, которого ещё нет в узле</h3>\n<p>Счётчики почти везде рисует клиентский скрипт. Узел в разметке уже есть, а внутри пусто: значение приезжает отдельным запросом через неопределённое время.</p>\n<p>Наивный код ставит паузу на полторы секунды. У меня примерно в половине случаев этого хватало, а в половине возвращалась пустая строка, которая дальше превращалась в ноль.</p>\n<p>Ждать нужно не время, а состояние:</p>\n<p>``<code>python await страница.wait_for_function(\"\"\"(scope) =&gt; { const root = document.querySelector(scope) || document; const v = root.querySelector('.story__views-count'); return !v || v.textContent.trim().length &gt; 0; }\"\"\", arg=область, timeout=8000) </code>``</p>\n<p>Сюда же относится ленивая загрузка картинок. Если снимать адреса изображений сразу после <code>domcontentloaded</code>, вместо ссылок получите прозрачные заглушки в один пиксель, закодированные прямо в атрибуте. Помогает прокрутка до конца страницы перед сбором - у меня это цикл из восьми колёсиков с паузой в 350 миллисекунд.</p>\n<h3>Ноль вместо пустого значения</h3>\n<p>Последнее правило самое скучное и самое дорогое.</p>\n<p>Когда замер не удался, у кода есть соблазн вернуть ноль. Он того же типа, что и нормальный результат, с ним ничего не надо делать дальше, и функция получается красивой.</p>\n<p>Но ноль в базе читается как «никто не смотрел». Если вы потом строите динамику, один неудачный замер навсегда останется провалом на графике, которого не было. И восстановить это нельзя: данные за вчера уже не собрать.</p>\n<p>Поэтому у меня неудача возвращает <code>None</code> и не пишет строку вовсе. Прежнее значение остаётся жить, а в отчёт попадает причина:</p>\n<p>``<code> ПРОПУСК  Spark   Сравнил цены входа   challenge DDoS-Guard, замер пропущен ПРОПУСК  Pikabu  Проверил 13 компаний  код 404, пост снят или недоступен </code>``</p>\n<p>Читать второй список неприятно, зато в данных нет вранья.</p>\n<figure class=\"статья-кадр\" data-файл=\"ixbt-01-none.jpg\"><img src=\"данные/картинки/ixbt-01-none.jpg\" alt=\"Схема: два ряда столбиков за неделю. В первом на месте неудачного замера стоит ноль, и в истории появляется провал, которого не было. Во втором замера просто нет, а прежнее значение остаётся. Иллюстрация автора\" loading=\"lazy\"><figcaption>Схема: два ряда столбиков за неделю. В первом на месте неудачного замера стоит ноль, и в истории появляется провал, которого не было. Во втором замера просто нет, а прежнее значение остаётся. Иллюстрация автора</figcaption></figure>\n<h3>Что из этого стоит забрать</h3>\n<p>Если коротко, то вот список, который я теперь держу в голове перед каждым новым сборщиком.</p>\n<p>Проверить сайт настоящим браузером, прежде чем решать, что он закрыт.</p>\n<p>Искать значение внутри блока, где лежат все нужные поля, а не по всей странице: меню и фильтры воруют совпадения.</p>\n<p>Разрешать пробел в числе только перед тремя цифрами и не забывать про неразрывный.</p>\n<p>Смотреть код ответа и привязываться к идентификатору своего объекта.</p>\n<p>Ждать появления данных, а не фиксированное время.</p>\n<p>И никогда не подменять неудачу нулём.</p>\n<p>Первые пять пунктов стоили мне по дню каждый, второй - ещё и полного пересбора. Шестой не стоил ничего, потому что я вовремя прочитал чужую историю про полтора миллиона просмотров у снятой статьи.</p>\n<p>---</p>\n<p>Все иллюстрации к посту нарисованы мной по собственным данным. Текст я писала сама, ИИ-редактор использовала для вычитки и проверки формулировок.</p>",
 "есть_текст": true
}
