Парсинг AliExpress с nodejs, puppeteer и RuCaptcha

0
347
views

Привет всем начинающим Node.js-разработчикам. В сегодняшней статье разберем реальный кейс парсинга на Node.js (с использованием puppeteer, cheerio и ruCaptcha). Я подробно распишу, как сделал простой парсер товаров на AliExpress. Этот пример вы сможете легко изменить для ваших задач.

Дисклеймер: все, что вы прочитаете в этой статье, написано исключительно в образовательных целях, автор статьи (как и администрация сайта techrocks.ru) не несет никакой ответственности за возможное использование информации незаконным путем.

Начнем с теории:

Что такое парсинг

Парсинг/скрапинг — автоматический сбор информации по заданному признаку. В качестве источников могут выступать социальные сети, интернет-порталы и вообще любые другие ресурсы в интернете. Информация, собранная с помощью парсинга в последующем может быть использована для наполнения собственной базы данных, анализа и т.п.

Какие задачи можно решить с помощью парсинга?

  • Сбор данных, например, сбор списка товаров, их свойств, фотографий, описаний, цен и т.п.
  • Анализ цен у конкурентов, их контент.
  • Изучение пользовательской активности на сайте.

В этой статье я буду парсить AliExpress и соберу данные о товарах заданного магазина.

Инструменты, которые мы будем использовать:

  1. Puppeteer — nodejs библиотека, которая предоставляет высокоуровневое API для программного управления браузером на движке Chromium. Подробнее о puppeteer можно узнать по ссылке.
  2. Cheerio — библиотека, которая позволяет работать с HTML по тому же принципу, что и в jQuery (доступны те же селекторы и т.п.)
  3. Chalk — библиотека, которая позволит нам красиво выводить результаты работы нашего скраппера в консоль
  4. ruCaptcha — сервис распознавания капчи. Позволит нам обходить капчи при большом количестве одновременных запросов.

Структура проекта

Структура проекта будет довольно простой — сразу мы создадим файл package.json (с помощью команды npm init). Далее создадим 3 каталога:

data — каталог, где мы будем хранить собранные данные с aliexpress

handlers — каталог, где будут храниться обработчики

helpers — каталог, где будут лежать дополнительные функции

Установим все зависимости:

npm install puppeteer cheerio chalk rucaptcha-client @babel/cli @babel/core @babel/node @babel/preset-env
Структура каталогов

Пишем код

Создадим index.js в корне проекта и начнем писать код:

import cherio from 'cherio';
import chalk from 'chalk';

import { arrayFromLength } from './helpers/common';
import { getPageContent } from './helpers/puppeteer';

const SITE = 'https://www.aliexpress.com/store/5410049/search/1.html'; // constant with link to catalog

Создаем helper для работы со страницами каталога

Мы импортировали основные зависимости, создали константу с ссылкой на первую страницу каталога, который будем парсить и импортировали нашу первую вспомогательную функцию arrayFromLength из папки helpers. Ее написанием сейчас займемся.
Функция arrayFromLength будет принимать на вход число страниц, и будет возвращать массив со значениями этих страниц. Создадим файл common.js в папке helpers и напишем следующий код:

export function arrayFromLength(number) {
    return Array.from(new Array(number).keys()).map(k => k + 1);
}

Создаем функцию для загрузки контента страницы

Для загрузки страниц с нам потребуется написать функцию getPageContent, которая будет принимать на вход url страницы и возвращать ее содержимое. Для этого будем использовать puppeteer. В папке helpers создадим файл puppeteer.js со следующим содержимым:

import puppeteer from 'puppeteer';

export const LAUNCH_PUPPETEER_OPTS = {
    args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-accelerated-2d-canvas',
        '--disable-gpu',
        '--window-size=1920x1080',
    ],
};

export const PAGE_PUPPETEER_OPTS = {
    networkIdle2Timeout: 5000,
    waitUntil: 'networkidle2',
    timeout: 3000000,
};

export async function getPageContent(url) {
    try {
        const browser = await puppeteer.launch(LAUNCH_PUPPETEER_OPTS);
        const page = await browser.newPage(PAGE_PUPPETEER_OPTS);
        await page.goto(url, PAGE_PUPPETEER_OPTS);
        const content = await page.content();
        browser.close();

        return content;
    } catch (err) {
        throw(err);
    }
}

Про конфиги puppeteer (константы LAUNCH_PUPPETEER_OPTS, PAGE_PUPPETEER_OPTS) можно почитать в документации — это тема отдельного занятия. Обратите внимание, я обернул код функции getPageContent в try-catch блок и пробрасываю ошибку наверх (в вызывающий код) — обрабатываться она будет в нашем основном методе main в index.js.

Разбираем страницу каталога

Начнем писать основной метод для разбора каталога — для этого в index.js мы создадим функцию main, которая и будет делать основную работу. Это будет самовызывающаяся функция, которая будет формировать корректные url-ы страниц для парсинга, загружать их с помощью puppeteer и с помощью cherio доставать нужные нам поля.

Функцию main мы обернули в try-catch блок, чтобы отлавливать все ошибки в одном месте и корректно их обрабатывать. В нашем случае мы будем выводить ошибку в консоль.

(async function main() {
    try {
        for(const page of arrayFromLength(CATALOG_PAGE_LENGTH)) {
            const url = `${SITE}/${page}.html`;
            const pageContent = await getPageContent(url);
            const $ = cherio.load(pageContent);
            const items = [];

            $('#node-gallery .items-list .item').each((i, item) => {
                console.log(i);
                const url = $('.detail h3 a', header).attr('href');
                const title = $('.detail h3 a', header).attr('title');
                const price = $('.cost b', header).text();
                items.push({
                    url,
                    title,
                    price
                });
            });

            await listItemsHandler(items);
        }
    } catch (err) {
        console.log(chalk.red('An error has occurred'));
        console.log(err);
    }
})();

Функция main проходится по страницам каталога и загружает каждую из них. После этого, с помощью cherio мы разбираем содержимое страницы, проходимся по всем карточкам товаров каталога и  получаем url страниц с подробным описанием, название товара и его цену).

Пишем обработчик listItemsHandler для разбора страницы товара

Далее нам необходимо написать функцию-обработчик listItemsHandler, которая будет получать массив данных с товарами, дополнять их необходимыми полями и сохранять в файл (в рамках текущей статьи мы рассмотрим вариант с сохранением в файл, на практике вероятнее всего вы заходите сохранять результаты в базу данных). Для этого создадим файл с названием listItemsHandler.js в папке handlers и напишем следующий код:

import cherio from 'cherio';
import chalk from 'chalk';
import { getPageContent } from '../helpers/puppeteer';

export default function listItemsHandler(data) {
    try {
        for (const initialData of data) {
            console.log(chalk.green('Getting data from: ' + chalk.green.bold(initialData.url)));
            const detailContent = await getPageContent(initialData.url);
            const $ = cherio.load(detailContent);

            const productTitle = $('.product-main .product-info .product-title-text').text();
            const productPrice = $('.product-main .product-info .product-price-value').text();
            const productSizes = $('.product-main .product-info .product-sku .sku-property-list').map(
                (i, item) => $('.sku-property-item .sku-property-text span', item).text()
            );
            const productImageUrl = $('.product-main-wrap .image-cover .maginfier-image').attr('src');

            await saveData({
                initialData.url,
                productTitle,
                productPrice,
                productSizes,
                productImageUrl,
            });
        }
    } catch (err) {
        throw err;
    }
}

В этом методе мы проходимся по массиву товаров, загружаем страницу с детальным описанием этого товара, с помощью cheerio получаем дополнительные данные (в нашем случае, цену, название, размеры и URL изображения) и сохраняем полученный объект в файл. Осталось написать функцию saveData, которая и будет отвечать за сохранение.

Сохраняем разобранные данные в файл

Создадим файл saver.js в папке handlers со следующим содержимым:

import path from 'path';
import fs from 'fs';
import chalk from 'chalk';

export default async function saveData(data) {
    const { title } = data;
    const fileName = `${title}.json`;
    const savePath = path.join(__dirname, '..', 'data', fileName);

    return new Promise( (resolve, reject ) => {
        fs.writeFile(savePath, data, err => {
            if (err) {
                return reject(err);
            }

            console.log(chalk.blue('File was saved successfully: ' + chalk.blue.bold(fileName) + '\n'));

            resolve();
        });
    });
};

Подключаем ruCaptcha

На данном этапе у нас получился полностью готовый несложный парсер на node.js, но есть одна существенная проблема, которая не позволит вам собрать данные. Все дело в капче, которую показывает AliExpress (и, кстати, не только AliExpres) при большом количестве отправленных запросов. Эту проблему удобнее всего решить, используя RuCaptcha — сервис ручного распознавания капчи с решениями для разных языков программирования (в том числе и для JS). Подробную документацию можно посмотреть по ссылке.

Для работы с капчами я буду использовать npm-пакет rucaptcha-client.
Создадим файл captchaSolver.js в папке handlers и напишем следующий код:

import Rucaptcha from 'rucaptcha-client';
 
export async function solveCaptcha(imageUrl) {
    try {
        const rucaptcha = new Rucaptcha(xxxxxxxxxxxxxxx); // ваш API-ключ
 
        // Если ключ API был указан неверно, выбросит RucaptchaError с кодом
        // ERROR_KEY_DOES_NOT_EXIST. Полезно вызывать этот метод сразу после
        // инициализации, чтобы убедиться, что ключ API указан верно.
        const balance = await rucaptcha.getBalance();
        const answer = await rucaptcha.solve(imageUrl);
        return answer.text;
    } catch (err) {
        throw err;
    }
};

Работает RuCaptcha очень просто:

  • Вы получаете персональный API-ключ в настройках вашего аккаунта.
  • Отправляете запрос на сервер ruCaptcha с вашим API-ключом и параметрами вашей капчи. Список поддерживаемых параметров можно посмотреть здесь.
  • В ответ получаем ID задачи (Captcha ID)
  • Отправляем на сервер ruCaptcha полученный Captcha ID и получаем решенную капчу.

В нашем случае npm-пакет инкапсулирует некоторые из этих шагов и нам достаточно дождаться выполнения метода rucaptcha.solve

Вот и все! Простейший парсер на Node.js готов — вы можете адаптировать код для своих нужд и под свои проекты. Если у вас есть предложения по улучшению статьи или вы хотите поделиться своими собственными примерами парсера, пишите на contact@techrocks.ru.

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here