Табличный каталог: получить строки товаров (PHP)
Прочитать каталог с табличным представлением и получить привычные поля товара. Пример использует селекторы элементов страницы, а не номера колонок.
Выберите язык PHP, вставьте полный исходник из этой статьи и задайте параметры запуска ниже. Источник для всей подборки: ScrapingCourse. Разбор относится к указанной демонстрационной странице.
Настройки примера
{
"path": "/table-parsing",
"mode": "http",
"expected": 15
}
path выбирает страницу, mode выбирает ветку алгоритма, expected задаёт контрольное число записей для этого примера. Значение expected не является лимитом тарифа. Если набор данных источника меняется, сверяйте ожидаемое количество с ним.
Как работает алгоритм
- Открываем
/table-parsingс режимомhttp. Для получения исходной разметки достаточно обычного запроса. - Проверяем успешность ответа и передаём HTML в общий сборщик.
- Выбираем элементы
.product-item, li.productи читаем внутри каждого название, цену, ссылку и изображение. Табличное оформление не меняет выбранный способ поиска полей. - Приводим текст цены к числу через инструменты SDK. Идентификатором товара служит его ссылка, при отсутствии ссылки используется имя.
- Проверяем ссылку следующей страницы тем же HTTP-алгоритмом. Если её нет, завершаем чтение.
- Сверяем число уникальных строк с
expectedи получаем записи с полямиname,priceиimage.
Карточки обрабатывает courseCollect($ctx, $html, $seen). Сначала читается название. Пустое название пропускается, ссылка карточки становится ключом, а при её отсутствии используется имя. seen не даёт повторно сохранить один и тот же ключ. Это важно, когда очередное чтение HTML включает уже собранные товары.
На что обратить внимание
Это пример разбора конкретной HTML-разметки. Если у другого каталога иные классы, сначала сопоставьте его поля с селекторами сборщика; порядок видимых колонок здесь не используется.
Особенности PHP
В PHP сетевые и браузерные методы вызываются последовательно через ->. Функция courseCollect получает массив $seen и возвращает его обновлённую копию, поэтому результат каждого вызова присваивается обратно. Объект страницы закрывается в finally. Для остановки с понятным сообщением используется $ctx->fail().
Что получится
Для настроенного демонстрационного набора ожидаются 15 уникальных записей. Общая проверка в конце сравнивает фактическое число ключей с expected.
| Поле | Что содержит |
|---|---|
name |
Название товара после удаления пробелов по краям. |
price |
Цена, преобразованная из текста в число. |
image |
Адрес изображения из атрибута src карточки. |
Полный код сценария
Исходник общий для всей подборки. Для этого примера в нём выполняется ветка http, выбранная параметрами выше. Блок можно целиком скопировать в редактор PHP; реквизиты доступа и ключи сервисов в него добавлять не требуется.
<?php
function run(\Sdk\Context $ctx): void
{
$origin = 'https://www.scrapingcourse.com';
$path = (string) $ctx->param('path', '/pagination');
$mode = (string) $ctx->param('mode', 'http');
$auto = $ctx->param('auto_captcha', true) !== false;
$seen = [];
if ($mode === 'http') {
$url = $origin.$path;
$visited = [];
while ($url !== null && count($visited) < 50) {
if (isset($visited[$url])) {
$ctx->fail('Пагинация вернула уже обработанную страницу');
}
$visited[$url] = true;
$response = $ctx->http()->withoutCache()->get($url, $auto);
if (! $response->ok()) {
$ctx->fail('Источник не открылся: '.$response->status);
}
$html = $response->body;
$seen = courseCollect($ctx, $html, $seen);
$next = $ctx->dom($html)->attr('a[rel="next"], a.next.page-numbers', 'href');
$url = $next ? $ctx->url()->absolutize($url, $next) : null;
}
if ($url !== null) {
$ctx->fail('Превышен защитный предел пагинации');
}
} else {
$page = $ctx->browser()->open($origin.$path, ['*/ajax/products*'], [], $auto);
try {
if ($mode === 'login') {
$dom = $ctx->dom($page->content());
$hint = '';
foreach ($dom->qa('p') as $p) {
if (str_contains($p->text(), 'Demo Credentials:')) {
$hint = $p->text();
}
}
preg_match('/Password:\s*([^\s|]+)/i', $hint, $password);
preg_match('/Email:\s*([^\s|]+@[^\s|]+)/i', $hint, $emailMatch);
$email = $emailMatch[1] ?? '';
if ($email === '' || ! isset($password[1])) {
$ctx->fail('Страница не содержит учебных реквизитов');
}
$page->type('#email', $email)->type('#password', $password[1])->click('button[type="submit"]');
}
if ($mode === 'challenge') {
$text = preg_replace('/\s+/', ' ', $ctx->dom($page->content())->text('body') ?? '');
if (! preg_match('/You (?:successfully )?bypassed|successfully bypassed|Congratulations/i', $text)) {
$ctx->fail('Защищённое содержимое не получено');
}
$ctx->emit(['name' => $path, 'passed' => true], $path);
} else {
$page->waitFor('.product-name:not(:empty)', 15000);
if ($mode === 'button') {
$complete = false;
for ($i = 0; $i < 30; $i++) {
$html = $page->content();
$before = $ctx->dom($html);
$seen = courseCollect($ctx, $html, $seen);
$button = $before->q('#load-more-btn');
if ($button === null || preg_match('/display:\s*none/', $button->attr('style') ?? '')) {
$complete = true;
break;
}
$offset = $button->attr('data-offset');
$count = count($before->qa('.product-item'));
$page->click('#load-more-btn');
$response = $page->waitForResponse('*offset='.$offset, 15000);
if ($response === null || ($response['status'] < 200 || $response['status'] >= 300) || $response['truncated'] || $response['error']) {
$ctx->fail('Не удалось загрузить следующую порцию');
}
if (trim($response['body'] ?? '') === '') {
$complete = true;
break;
}
$page->waitFor('.product-item:nth-child('.($count + 1).')');
}
if (! $complete) {
$ctx->fail('Превышен защитный предел загрузки');
}
}
if ($mode === 'scroll') {
$stable = 0;
$previous = 0;
for ($i = 0; $i < 40 && $stable < 3; $i++) {
$page->scroll(3, 400);
$html = $page->content();
$seen = courseCollect($ctx, $html, $seen);
$count = count($ctx->dom($html)->qa('.product-item'));
$stable = $count === $previous ? $stable + 1 : 0;
$previous = $count;
}
if ($stable < 3) {
$ctx->fail('Лента продолжает загружаться: увеличьте предел прокрутки');
}
}
$seen = courseCollect($ctx, $page->content(), $seen);
}
} finally {
$page->close();
}
}
$expected = (int) $ctx->param('expected', 1);
if ($mode !== 'challenge' && count($seen) !== $expected) {
$ctx->fail('Неполный результат: '.count($seen).' вместо '.$expected);
}
}
function courseCollect(\Sdk\Context $ctx, string $html, array $seen): array
{
$dom = $ctx->dom($html);
foreach ($dom->qa('.product-item, li.product') as $card) {
$name = trim($card->q('.product-name, .woocommerce-loop-product__title')?->text() ?? '');
$key = $card->q('a')?->attr('href') ?? $name;
if ($name === '' || isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$ctx->emit(['name' => $name, 'price' => $ctx->text()->money($card->q('.product-price, .price')?->text() ?? ''),
'image' => $card->q('img')?->attr('src') ?? ''], $key);
}
return $seen;
}
Куда посмотреть дальше
Общий алгоритм и параметры · HTTP-запросы · Действия браузера · Поиск элементов HTML