К содержимому

Документация

Страница Antibot: сохранить результат проверки (TypeScript)

Получить и проверить контрольную страницу ScrapingCourse, затем сохранить понятный логический результат. Этот пример удобно использовать как отдельный шаг перед чтением данных.

Выберите язык TypeScript, вставьте полный исходник из этой статьи и задайте параметры запуска ниже. Источник для всей подборки: ScrapingCourse. Разбор относится к указанной демонстрационной странице.

Настройки примера

{
  "path": "/antibot-challenge",
  "mode": "challenge",
  "expected": 1
}

path выбирает страницу, mode выбирает ветку алгоритма, expected задаёт контрольное число записей для этого примера. Значение expected не является лимитом тарифа. Если набор данных источника меняется, сверяйте ожидаемое количество с ним.

Как работает алгоритм

  1. Настраиваем /antibot-challenge и режим challenge. Полный исходник остаётся тем же, меняются параметры выбранной страницы.
  2. Открываем страницу через браузерный SDK с настройкой auto_captcha.
  3. Читаем текст тела страницы и нормализуем пробельные символы. Это делает сравнение независимым от переносов строк в HTML.
  4. Проверяем наличие контрольной фразы демонстрационной площадки. Один HTTP-статус не заменяет эту проверку содержимого.
  5. При совпадении создаём одну запись: name содержит путь, passed равен true, ключ равен пути страницы.
  6. Закрываем сессию в finally. Сборщик товаров и проверка количества карточек в этой ветке не вызываются.

На что обратить внимание

Для своей контрольной страницы условие должно отражать её содержимое. В этом примере регулярное выражение соответствует странице ScrapingCourse и не является универсальным признаком доступа к любому сайту.

Особенности TypeScript

В TypeScript ctx: Akmelo.Context даёт подсказки методов SDK, Promise<void> описывает результат run, а Set<string> хранит строковые ключи. Эти типы не меняют порядок действий: сетевые и браузерные вызовы по-прежнему требуют await. collect(html: string): number возвращает число карточек текущей страницы.

Что получится

Результат этой ветки: одна запись с полями name и passed. name равен /antibot-challenge, passed равен true только после совпадения контрольного текста. Отдельная проверка seen в конце для режима challenge пропускается.

Полный код сценария

Исходник общий для всей подборки. Для этого примера в нём выполняется ветка challenge, выбранная параметрами выше. Блок можно целиком скопировать в редактор TypeScript; реквизиты доступа и ключи сервисов в него добавлять не требуется.

async function run(ctx: Akmelo.Context): Promise<void> {
  const origin = 'https://www.scrapingcourse.com';
  const path = String(ctx.param('path', '/pagination'));
  const mode = String(ctx.param('mode', 'http'));
  const auto = ctx.param<boolean>('auto_captcha', true) !== false;
  const seen = new Set<string>();
  const collect = (html: string): number => {
    const dom = ctx.dom(html);
    for (const card of dom.qa('.product-item, li.product')) {
      const name = card.q('.product-name, .woocommerce-loop-product__title')?.text().trim() ?? '';
      const key = card.q('a')?.attr('href') ?? name;
      if (!name || seen.has(key)) continue;
      const price = ctx.text().money(card.q('.product-price, .price')?.text() ?? '');
      const image = card.q('img')?.attr('src') ?? '';
      seen.add(key);
      ctx.emit({name, price, image}, key);
    }
    return dom.qa('.product-item, li.product').length;
  };
  if (mode === 'http') {
    let url: string | null = origin + path;
    const visited = new Set<string>();
    while (url && visited.size < 50) {
      if (visited.has(url)) throw new Error('Пагинация вернула уже обработанную страницу');
      visited.add(url);
      const response = await ctx.http().withoutCache().get(url, auto);
      if (!response.ok()) throw new Error('Источник не открылся: ' + response.status);
      const html = response.body;
      collect(html);
      const next = ctx.dom(html).attr('a[rel="next"], a.next.page-numbers', 'href');
      url = next ? ctx.url().absolutize(url, next) : null;
    }
    if (url) throw new Error('Превышен защитный предел пагинации');
  } else {
    const page = await ctx.browser().open(origin + path, ['*/ajax/products*'], {}, auto);
    try {
      if (mode === 'login') {
        const dom = ctx.dom(await page.content());
        const hint = dom.qa('p').map(node => node.text()).find(text => text.includes('Demo Credentials:')) ?? '';
        const password = hint.match(/Password:\s*([^\s|]+)/i)?.[1];
        let email = hint.match(/Email:\s*([^\s|]+@[^\s|]+)/i)?.[1];
        const encoded = dom.attr('[data-cfemail]', 'data-cfemail');
        if (!email && encoded && encoded.length > 2) {
          const key = parseInt(encoded.slice(0, 2), 16);
          email = '';
          for (let i = 2; i < encoded.length; i += 2) email += String.fromCharCode(parseInt(encoded.slice(i, i + 2), 16) ^ key);
        }
        if (!email || !password) throw new Error('Страница не содержит учебных реквизитов');
        await page.type('#email', email);
        await page.type('#password', password);
        await page.click('button[type="submit"]');
      }
      if (mode === 'challenge') {
        const text = (ctx.dom(await page.content()).text('body') ?? '').replace(/\s+/g, ' ');
        if (!/You (?:successfully )?bypassed|successfully bypassed|Congratulations/i.test(text)) throw new Error('Защищённое содержимое не получено');
        ctx.emit({name: path, passed: true}, path);
      } else {
        await page.waitFor('.product-name:not(:empty)', 15000);
        if (mode === 'button') {
          let complete = false;
          for (let i = 0; i < 30; i++) {
            const before = ctx.dom(await page.content());
            collect(await page.content());
            const button = before.q('#load-more-btn');
            if (!button || /display:\s*none/.test(button.attr('style') ?? '')) { complete = true; break; }
            const offset = button.attr('data-offset');
            const count = before.qa('.product-item').length;
            await page.click('#load-more-btn');
            const response = await page.waitForResponse('*offset=' + offset, 15000) as Akmelo.Row | null;
            if (!response || (Number(response.status) < 200 || Number(response.status) >= 300) || response.truncated || response.error) throw new Error('Не удалось загрузить следующую порцию');
            if (String(response.body ?? '').trim() === '') { complete = true; break; }
            await page.waitFor('.product-item:nth-child(' + (count + 1) + ')');
          }
          if (!complete) throw new Error('Превышен защитный предел загрузки');
        }
        if (mode === 'scroll') {
          let stable = 0;
          let previous = 0;
          for (let i = 0; i < 40 && stable < 3; i++) {
            await page.scroll(3, 400);
            const count = collect(await page.content());
            stable = count === previous ? stable + 1 : 0;
            previous = count;
          }
          if (stable < 3) throw new Error('Лента продолжает загружаться: увеличьте предел прокрутки');
        }
        collect(await page.content());
      }
    } finally { await page.close(); }
  }
  const expected = Number(ctx.param('expected', 1));
  if (mode !== 'challenge' && seen.size !== expected) throw new Error('Неполный результат: ' + seen.size + ' вместо ' + expected);
}

Куда посмотреть дальше

Общий алгоритм и параметры · HTTP-запросы · Действия браузера · Поиск элементов HTML