Введение: Эпоха после 12ft.io и рождение WallHop

Представьте: пятничный вечер, вы кликаете по ссылке из интересного твида про новую архитектуру баз данных, а в ответ — глухая стена из формы подписки на 50 долларов в месяц. Знакомо? Когда утренний поиск нужного гайда превращается в квест по сбору бесплатных просмотров, разработчик начинает думать не о коде, а о мести системе. Раньше у нас был 12ft.io — изящная «лестница», позволявшая перешагнуть через любой пейвол за счет кэшей поисковиков (иногда проще поднять свой Kubernetes, чем купить все подписки на медиа).

Однако время шло, издатели учились защищаться. Под давлением News/Media Alliance и правообладателей популярный инструмент окончательно ушел в офлайн. Сообщество разработчиков осталось без привычного решения быстрого доступа к текстовому контенту. Именно тогда передо мной встала классическая инженерная задача: если готового решения больше нет, его нужно спроектировать и написать самостоятельно. Так появился WallHop (wallhop.io) — мой собственный открытый инструмент, созданный на базе проекта everywall/ladder (лицензия GPL-3.0), но глубоко модифицированный и переосмысленный с учетом современных реалий веб-разработки.

В этой статье я подробно расскажу об архитектуре WallHop, технических вызовах, с которыми мне пришлось столкнуться, принципах работы с headless-браузерами и о том, как устроен пайплайн очистки HTML-контента от мусора.

Архитектурный выбор: Почему простые HTTP-запросы больше не работают

Когда я только задумывал WallHop, моим первым порывом было написать простой скрипт на Node.js с использованием библиотек axios и cheerio. Зачем городить сложную инфраструктуру, если можно просто отправить GET-запрос к целевой странице, притворившись поисковым роботом Googlebot, и забрать чистый HTML?

Практика показала всю наивность этого подхода. Современные медиаресурсы и новостные гиганты используют многоуровневую защиту:

  • Cloudflare Bot Management и Under Attack Mode: Простые скрипты отсекаются еще на этапе TLS-рукопожатия или с помощью сложных JavaScript-челленджей, требующих выполнения кода в настоящем браузере.
  • Client-Side Rendering (CSR): Большинство контента не зашито в исходный HTML-код страницы. Сервер отдает пустой шаблон с React- или Vue-приложением, которое подгружает текст статьи через защищенные API-эндпоинты.
  • Динамические пейволы: Скрипты проверки подписки выполняются асинхронно, накладывая оверлей поверх текста уже средствами браузера.

Эволюция кода: От простого прокси к умному пайплайну

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

const puppeteer = require('puppeteer');
const TurndownService = require('turndown');

async function fetchCleanArticle(targetUrl) {
    const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
    const page = await browser.newPage();
    
    // Эмулируем реального пользователя и отключаем лишние ресурсы
    await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
    await page.setRequestInterception(true);
    
    page.on('request', (req) => {
        if (['image', 'stylesheet', 'font'].includes(req.resourceType())) {
            req.abort();
        } else {
            req.continue();
        }
    });

    await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 30000 });
    
    // Удаляем элементы пейволов и навигации DOM
    await page.evaluate(() => {
        const selectors = ['.paywall', 'header', 'footer', '.cookie-ban