Страница Antibot: сохранить результат проверки (TypeScript)
Получить и проверить контрольную страницу ScrapingCourse, затем сохранить понятный логический результат. Этот пример удобно использовать как отдельный шаг перед чтением данных.
Выберите язык TypeScript, вставьте полный исходник из этой статьи и задайте параметры запуска ниже. Источник для всей подборки: ScrapingCourse. Разбор относится к указанной демонстрационной странице.
Настройки примера
{
"path": "/antibot-challenge",
"mode": "challenge",
"expected": 1
}
path выбирает страницу, mode выбирает ветку алгоритма, expected задаёт контрольное число записей для этого примера. Значение expected не является лимитом тарифа. Если набор данных источника меняется, сверяйте ожидаемое количество с ним.
Как работает алгоритм
- Настраиваем
/antibot-challengeи режимchallenge. Полный исходник остаётся тем же, меняются параметры выбранной страницы. - Открываем страницу через браузерный SDK с настройкой
auto_captcha. - Читаем текст тела страницы и нормализуем пробельные символы. Это делает сравнение независимым от переносов строк в HTML.
- Проверяем наличие контрольной фразы демонстрационной площадки. Один HTTP-статус не заменяет эту проверку содержимого.
- При совпадении создаём одну запись:
nameсодержит путь,passedравенtrue, ключ равен пути страницы. - Закрываем сессию в
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