Задача была прикладная: мы переносим интернет‑магазины на свой движок и хотели понять, сколько в зоне .ru магазинов на каждой платформе и как их находить. Готовые сервисы вроде BuiltWith продают выгрузки по подписке, но нам нужен был воспроизводимый обход, который можно гонять когда угодно и по своим правилам.
Написали свой пробер, обошли 53 305 доменов и получили распределение платформ. Заодно собрали коллекцию ловушек, на каждой из которых успели ошибиться. Про них и статья — цифры без описания граблей мало чего стоят.
Что считаем футпринтом
Определение CMS по странице — это поиск строк, которые платформа неизбежно оставляет в разметке. У нас девять наборов:
FOOTPRINTS = { "insales": ["insales-cdn.com", "cdn.insales-cdn.com", "static.insales-cdn.com"], "bitrix": ["/bitrix/js/main/core", "/bitrix/templates/", "/bitrix/components/", "BX.message("], "woocommerce": ["wp-content/plugins/woocommerce", "woocommerce-page", "wc-add-to-cart"], "opencart": ["catalog/view/theme", "Powered By OpenCart", "ocStore"], "cs-cart": ["Powered by CS-Cart", "/design/themes/responsive", "CS-Cart"], "shop-script": ["/wa-data/", "Powered by Webasyst", "Shop-Script"], "advantshop": ["advantshop"], "shopify": ["cdn.shopify.com", "Shopify.theme"], "tilda": ["tildacdn.com", "tilda.cc"], }
Принцип отбора строк: брать то, что платформа кладёт в разметку по своей механике. Всё, что владелец сайта может убрать руками, идёт вторым эшелоном. /bitrix/js/main/core — это путь к ядру, он есть, пока сайт работает. А Powered by из футера снимается за пять минут, поэтому такие строки идут вторыми, для добора.
Обход — асинхронный, с двумя независимыми потолками: --concurrency 16 ограничивает число запросов в полёте, --delay 0.2 держит минимальный интервал между старта́ми. Второе важнее: именно delay задаёт реальную скорость (5 запросов в секунду), а concurrency только не даёт очереди распухнуть. Читаем не больше 300 КБ с страницы — футпринт всегда в первых килобайтах, дальше только трафик.
Результат
53 305 доменов, 77 522 HTTP‑запроса. Срез из Tranco и Majestic по зоне .ru.
Сначала честный знаменатель, потому что без него проценты врут:
Статус | Доменов | Доля |
|---|---|---|
| 35 463 | 66,5% |
| 6 511 | 12,2% |
| 3 979 | 7,5% |
| 3 768 | 7,1% |
| 1 599 | 3,0% |
| 1 211 | 2,3% |
| 567 | 1,1% |
прочее | 207 | 0,4% |
Треть доменов из ранкингов просто не отвечает. Мёртвые, припаркованные, за клаудфларой, с развалившимся TLS. Это первое, что стоит знать, если вы собираетесь считать доли платформ «по зоне.ru»: ваша выборка — не зона, а её отвечающая часть.
Из 35 463 ответивших футпринт нашёлся у 7 073 (13,3% от всех, 19,9% от ответивших):
Платформа | Доменов |
|---|---|
1С‑Битрикс | 4 600 |
Tilda | 1 195 |
WooCommerce | 730 |
OpenCart / ocStore | 310 |
Shop‑Script (Webasyst) | 161 |
CS‑Cart | 59 |
InSales | 30 |
AdvantShop | 21 |
Shopify | 1 |
Сумма по строкам — 7 107, а доменов 7 073, и разница в 34 попадания раскладывается не так, как хочется арифметике: несколько футпринтов дали 32 домена — у 30 по две платформы, у двух по три. Три платформы сразу — это inetlinks.ru (CS‑Cart + OpenCart + Shop‑Script) и digitalkassa.ru (AdvantShop + CS‑Cart + Shop‑Script); оба продают и обслуживают сами эти движки, поэтому их названия рассыпаны по разметке. Остальное — лендинг на Tilda и магазин на движке под одним доменом либо остатки старой платформы.
Если ваш код пишет платформу в одно поле, такие случаи молча теряются. И считать домены как «сумма попаданий» тоже нельзя: я на этом успел ошибиться на два домена, пока не пересчитал по записям.
Битрикс с большим отрывом первый, и это не новость. Интереснее хвост: Shopify в зоне .ru — один домен. Не «мало», а буквально один на 53 тысячи.
Ловушка 1: футпринт платформы ≠ работающий магазин
Самая дорогая ошибка. wp-content/plugins/woocommerce в исходнике означает, что WooCommerce установлен. Он не означает, что перед вами магазин.
Хуже: wp-content/uploads — это просто медиатека WordPress, она есть у любого блога. Если ослабить футпринт до wp-content, WooCommerce “найдётся” у половины интернета.
У нас был живой случай. Домен gtdv.ru пробер отдал как WooCommerce. При проверке: заголовок x-powered-by: Nuxt, фронт на Vue, wp-login.php и /wp-json/wc/v3 отдают 404. WordPress там когда‑то стоял и остался раздавать картинки из wp-content/uploads, а сам сайт давно переписан. Мы это поймали только потому, что дальше проверяли платформу отдельно и по исходнику.
Сначала это выглядело как единичный курьёз. Потом мы проверили руками ещё несколько сотен доменов — открывали исходник и подтверждали платформу живым признаком — и получили измеренную долю ошибки. Она оказалась неравномерной по платформам, и это само по себе полезнее общей цифры:
Платформа | Проверено вручную | Платформа не подтвердилась | Доля |
|---|---|---|---|
WooCommerce | 188 | 10 | 5,3% |
InSales | 137 | 4 | 2,9% |
OpenCart | 179 | 1 | 0,6% |
Всего | 504 | 15 | 3,0% |
Про выборку сразу: домены Woo и OpenCart взяты из этого же прохода, а InSales — из отдельной выгрузки по футпринту, потому что в ранкинговом срезе их всего 30. Складывать их в одну долю можно только как оценку ошибки метода. Свойством зоны .ru это число не является.
У WooCommerce ошибка на порядок чаще, и причина в семи случаях из десяти одна и та же: CSS‑классы woocommerce- от темы оформления. Тема ставит их независимо от того, установлен ли плагин. В этих семи случаях в исходнике не было ни wp-content/plugins/woocommerce, ни рабочего роута — только классы: wp-json/wp/v2/product отдавал 404, /shop/ тоже. То есть футпринт, который выглядит специфичным для магазина, на деле принадлежит шаблону.
У OpenCart и InSales так не выходит, потому что их признаки механические: OCSESSID в куках и index.php?route= — это работа движка, а insales-cdn.com в URL картинок — работа их CDN. Ни то, ни другое не остаётся от одной лишь темы. Отсюда практический вывод: надёжность футпринта зависит не от того, насколько строку трудно подделать, а от того, кладёт ли её ядро платформы или оформление. Если строку может поставить тема — считайте её вторичной, чем бы она ни выглядела.
Оставшиеся три ошибки по Woo — другой природы: заброшенный WordPress под переписанным фронтом (gtdv.ru), WordPress вообще без WooCommerce, и домен, который редиректит на другой сайт. По InSales и OpenCart картина похожая поштучно: два сайта на Tilda и один на WordPress, ошибочно зачтённые как InSales; домен, перепроданный под онлайн‑казино и сохранивший старую разметку; и статичный лендинг‑сателлит без CMS, зачтённый как OpenCart (akb-v-spb.ru: ни catalog/view, ни route=, ни OCSESSID — только счётчики и перелинковка на восемь своих же доменов).
Надёжнее спросить у самой платформы, через её живой API:
# WooCommerce активен, а не просто «когда-то ставили WordPress» import json, urllib.request def woo_alive(domain): try: with urllib.request.urlopen(f"https://{domain}/wp-json/", timeout=10) as r: ns = json.load(r).get("namespaces", []) return any(n.startswith("wc/") for n in ns) # wc/v3, wc/store except Exception: return False
Плюс признаки, которые появляются только у работающей витрины: класс woocommerce-page на странице каталога, ссылки ?add-to-cart=<id>, meta generator с версией. Один из наших магазинов определился именно по namespace wc/v3 в /wp-json/ — в разметке главной футпринтов не было вовсе, каталог рисовался скриптом.
Отдельно: мы прогоняли найденные домены вторым проходом, который ищет на сайте цену и корзину. Из доменов с футпринтом платформы реальными магазинами оказалась примерно треть:
Срез | Прогнано | Магазин (цена + корзина) | Каталог без цен | Не магазин |
|---|---|---|---|---|
Битрикс, верх ранкингов | 2 355 | 882 (37,5%) | 131 | 1 342 |
WooCommerce | 480 | 160 (33,3%) | 55 | 265 |
Что оказалось в отбраковке — по платформам заметно разное.
У Битрикса это институции и крупные организации: банк, энергосбыт, патентное бюро, палата, ассоциация телерадиовещателей, учебный центр, медлаборатория, городская больница, музей, центр качества образования, сетевая компания. Плюс заводы с сайтом‑визиткой без продаж и франчайзи самого Битрикса.
У WooCommerce отбраковка другая по природе: цифровые товары с курсами (PDF‑выкройки вместе с мастер‑классами), изделия на заказ без цен, сайты производителей, продающих через дилеров, и просто блоги. WordPress берут те, кто продаёт что‑то нестандартное — или не продаёт вовсе.
То есть по футпринту платформы вы не отличите магазин от больницы. Второй проход обязателен, и он съест две трети выборки.
Ловушка 2: бюджет запросов и гейт на страницы контактов
Нам нужны были ещё и контактные адреса. Наивный подход — забирать /contacts/, /kontakty/, /about/ у каждого домена. Считаем: 35 463 ответивших × 3 страницы ≈ 106 тысяч дополнительных запросов, то есть обход раздувается втрое.
Мы поставили гейт: страницы контактов забираются только если на главной адреса не нашлось И платформа определена. Второе условие ключевое — если футпринта нет, домен нам не интересен, и тратить на него запросы бессмысленно.
items = extract_emails(body, page_url, "homepage") if (not items and self.cfg.email_pages and not self.cfg.offline_cache and base.get("status") == "ok" and base.get("platforms")): self.counts["l1_domains"] += 1 got, used = await self.level1(base["domain"], page_url, body) items = merge_emails(items, got)
Что дал гейт на реальном прогоне. Без адреса на главной остались 19 325 доменов; из них платформа определена у 2 545. То есть уровень 1 отработал по 2 545 доменам и стоил 5 285 запросов вместо ~58 000 — экономия 91%.
Внутри уровня 1 ещё одна мелочь, которая экономит половину его бюджета: останавливаемся на первой странице, где нашёлся адрес. Остальные две не запрашиваем. И неудачный запрос (404, таймаут) считаем расходом бюджета, но не ошибкой домена — иначе один битый /contacts/ выкидывает живой магазин.
Итого адреса нашлись у 17 391 домена (16 138 с главной, 1 253 со страниц контактов), принято 30 803 адреса.
Ловушка 3: в темах платформ живут плейсхолдеры
Регулярка на email соберёт вам мусор, который выглядит как настоящий контакт. Реальные находки из нашего прогона:
mail@domen.com,info@domen.com— заглушка из темы, встретилась на трёх разных сайтах;callmeback@asap.com— заглушка в темах InSales;rating@mail.ru— это не контакт, а счётчик Mail.ru; попался у трёх доменов;example@example.ru,your@mail.com,email@email.ru,example@mail.com,example@site.com— шаблонные примеры со страниц франшизы и оптовых разделов;адреса технических доменов:
@sentry-cdn.com,@wixpress.com,@w3.org,@schema.org,@1c-bitrix.ru,@tildacdn.com.
Отдельный класс — адрес есть в разметке, но не в видимом тексте. У одного домена контакт нашёлся только внутри скрытого мета‑тега верификации каталога (cataloxy-verification), на странице его не видно. Формально строка на странице присутствует, и любая программная сверка её подтвердит. Такой адрес брать нельзя — он не предназначен для входящей почты.
Ещё: mailto: на иконке в футере без видимого текста. Технически адрес есть, для человека его на странице нет.
Ловушка 4: домены‑редиректы прячут магазины
Пробер обходит домен из списка и записывает результат по нему. Если домен редиректит, вы получите платформу целевого сайта, но в выгрузке останется исходное имя.
У нас так вышло с stabiline.ru: он редиректит на nizkovoltka.ru — работающий магазин электротехники с 2000+ наименований и складами в четырёх регионах. Самого nizkovoltka.ru в выгрузке нет вообще: в ранкинги он не попал и виден только через редирект с другого домена. Мы нашли его случайно, при ручной проверке.
Мораль: если пишете такой обход, сохраняйте final_url отдельно от запрошенного домена и сверяйте их. У нас поле есть, но сопоставлением мы занялись поздно. Сколько магазинов спрятано за редиректами — не знаем, и это честный пробел в наших данных.
Главное ограничение: ранкинги смещают выборку
Это про то, чего цифрами выше не видно.
Tranco и Majestic ранжируют домены по трафику и ссылочному профилю. Значит любой срез из них это выборка популярных сайтов. Всех остальных в ней нет. Для платформы, чьи магазины сидят ниже топ-1M, обход по ранкингам её просто не увидит.
У нас есть замер, который это показывает численно. Мы гоняли тот же пробер по зоне .kz: 2 634 домена, 214 попаданий, из них InSales — 58. Сравнили с ручными выгрузками BuiltWith по той же зоне: пробер добавил ровно 1 новый домен InSales из 58. Остальные 57 уже были в выгрузках. И наоборот — 66 из 69 доменов из выгрузок в ранкингах отсутствовали вовсе.
Отсюда и InSales - 30 в таблице по .ru. Это не значит, что в России 30 магазинов на InSales. Это значит, что выше топ-1M их 30, а основная масса живёт ниже и обходом по ранкингам не достаётся.
Вывод, который стоит забрать: обход по ранкингам и выгрузки по футпринту — не конкуренты, а разные инструменты. Ранкинги хороши для платформ, которые стоят у крупных сайтов (Битрикс), и слепы к платформам массового малого магазина. Чтобы копать ниже топ-1M, нужен другой сид: логи Certificate Transparency (crt.sh), отраслевые каталоги, площадки‑агрегаторы.
Побочное наблюдение про 1С
Отдельно и с оговоркой: это не статистика по популяции, а результат ручной проверки — мы открывали страницы и смотрели исходник глазами. Выборка неслучайная: это домены, которые прошли предыдущие фильтры, то есть смещённая в сторону работающих магазинов часть.
По вручную проверенным доменам явный признак обмена с 1С (упоминание 1С/УТ/выгрузки, модуль обмена в исходнике, оптовый кабинет с ценами по договору) распределился так:
Платформа | Проверено вручную | Признак 1С | Доля |
|---|---|---|---|
1С‑Битрикс | 240 | 174 | 72,5% |
WooCommerce | 188 | 0 | 0% |
InSales | 137 | 0 | 0% |
OpenCart | 179 | 2 | 1,1% |
Снаружи 1С видна практически только на Битриксе. На трёх других платформах — 2 случая на 504 домена. Оговорка обязательна: это признак, видимый снаружи. Факта наличия учётной системы он не доказывает. Магазин вполне может выгружаться из 1С через закрытый API, и на страницах этого не будет.
Но и в таком виде наблюдение полезно, потому что асимметрия слишком велика, чтобы быть артефактом критерия. Похоже, дело в том, что связка с 1С на Битриксе делается штатным модулем обмена от самого вендора и оставляет следы в разметке, а на остальных платформах её ставят сторонним плагином или пишут интеграцию мимо витрины.
И грабля в самом критерии, чисто механическая: поиск подстроки «1с» ловит артикулы. У поставщика трубопроводной арматуры сработало на коде товара 11с10фт (Broen Ballomax) — пришлось снимать вручную. Признак из двух символов будет находить мусор в любой номенклатуре: нужен либо контекст вокруг вхождения, либо словарь исключений.
Оба случая на OpenCart, кстати, нашлись по побочным следам, прямых упоминаний там не было: у одного магазина картинки лежали в /image/cache/catalog/1c/ с GUID‑именами (так выгружается номенклатура), у другого скрипт темы декодировал описания «если 1С выгрузила их». Автоматически такое не найти.
Зато у восьми Woo‑доменов из 188 обнаружился плагин обмена с другой системой: amoCRM (трижды), Битрикс24 (трижды), МойСклад и RetailCRM — видно по путям плагинов в исходнике (woocommerce-amocrm-integration, woocommerce-bitrix24-integration, woo-retailcrm). То есть учёт есть, но не 1С, а облачный.
Важная деталь про критерий: у WooCommerce артикулы (SKU) есть у всех товаров по умолчанию, поэтому «есть артикулы и остатки» про интеграцию с 1С не говорит ничего. На Битриксе этот признак работает, на Woo — нет. Мы на этом ошиблись: сначала зачли оптовый кабинет с ценами после логина как признак 1С, потом сняли — кабинет означает какую‑то учётную систему, но у Woo это обычно облако.
И ещё, из той же ручной проверки: 2 сайта из 188 на WordPress оказались взломаны — казино‑спам в блоге свежими датами и десятки исходящих ссылок на игорные домены. Выборка маленькая, обобщать не буду, но если вы обходите WordPress‑сайты, добавьте проверку на доорвей‑контент: это одновременно и мусор в данных, и признак, что почтовая настройка домена может быть не под контролем владельца.
Что мы не мерили
Чтобы не создавать ложного впечатления полноты:
Не мерили динамику. Один прогон, один срез по времени. Сколько магазинов переезжает с платформы на платформу — по этим данным сказать нельзя.
Не считали долю рынка. У нас доли внутри отвечающей части ранкингового среза, и это не то же самое, что доли по зоне.
Не обходили ниже топ-1M. См. раздел про смещение — это главный пробел.
Не разбирали 28 390 доменов, которые ответили, но футпринтов не дали. Там и самописные движки, и headless, и платформы, для которых мы не завели наборы, и сайты, отдающие пустую оболочку под JS.
Не проверяли robots.txt в этом прогоне (
respect_robots: false), обход шёл с User‑Agent с контактом и лимитом 5 запросов в секунду на весь прогон. Если повторяете — решайте это сами, у вас может быть другая политика.
И отдельно про то, кому вся эта методика не подходит. Если задача собрать одну нишу, ранкинговый срез её не даст: внутри ниши в нём окажется только верхушка, а весь длинный хвост останется за бортом. Тогда сид берётся из отраслевого каталога или из поисковой выдачи по запросам, и пробер идёт уже по этому списку. Ловушки ниже по тексту останутся в силе, а вот распределение платформ придётся считать заново — оно будет другим.
Если хотите повторить
Порядок, который у нас в итоге сложился:
Взять сид (ранкинги — только если целевая платформа стоит у крупных сайтов).
Обойти главные, писать в JSONL всё: статус,
http_status,final_url, запрошенные URL, найденные футпринты. Отдельным полем — ошибку.Не выбрасывать неответивших: треть выборки в них, и по распределению статусов видно качество сида.
Платформу подтверждать живым признаком: API‑namespace, класс на странице каталога,
add-to-cart. Наличие файлов само по себе не значит ничего.Второй проход — «а магазин ли это»: цена плюс корзина. Готовьтесь потерять две трети.
Адреса добирать со страниц контактов только под гейтом, и с чёрным списком плейсхолдеров.
Сверять
final_urlс запрошенным доменом — за редиректами прячутся сайты, которых в сиде нет.
Обход 53 тысяч доменов занял одну ночь и уложился в 77,5 тысяч запросов. Самой дорогой частью оказалась не сеть, а понимание того, что найденный футпринт и работающий магазин — разные вещи.

