Я пытаюсь создать Node.js API, который очищает веб-сайт (я начал только с Goodreads как веб-сайта, который нужно очистить, и буду расширять его дальше, когда я впервые оптимизирую подход) и предоставляю очищенные данные конечному пользователю, что будет использовать мой API.
Мой первоначальный подход заключался в планировании структуры API, решении использовать puppeteer, а затем начать создавать. При успешном создании первой конечной точки я заметил кое-что: в Postman требуется около 2-3 секунд для завершения запроса, что очень медленно.
Вот мой код:
scraper-handler.ts
import { NextFunction, Request, Response } from "express";
import { MOST_POPULAR_LISTS } from "../utils/api/urls-endpoints.js";
import { listScraper } from "./spec-scrapers/list-scraper.js";
import { lists } from "../utils/api/full-urls.js";
import puppeteer from "puppeteer";
import { GOODREADS_POPULAR_LISTS_URL } from "../utils/goodreads/urls.js";
export const scraperHandler = async (
req: Request,
res: Response,
next: NextFunction
) => {
const browser = await puppeteer.launch({
// headless: false,
// defaultViewport: null,
});
const pages = await browser.pages();
await pages[0].setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36"
);
switch (req.url) {
case `/${MOST_POPULAR_LISTS}`: {
const result = await listScraper(
browser,
pages[0],
GOODREADS_POPULAR_LISTS_URL,
1,
".cell",
".listTitle",
".listTitle"
);
res.status(200).json({
status: "success",
data: result,
});
break;
}
default: {
next();
break;
}
}
};
А вот case /${MOST_POPULAR_LISTS}:
list-scraper.ts
import puppeteer, { Page } from "puppeteer";
import { Browser } from "puppeteer";
export const listScraper = async (
browser: Browser,
page: Page,
url: string,
pageI = 1,
main: string,
title = "",
ref = ""
) => {
// const page = await browser.newPage();
await page.goto(url, {
waitUntil: "domcontentloaded",
});
const books = await page.evaluate(
(mainSelector, titleSelector, refSelector) => {
// const nextLink = document.querySelector('a[rel = "next"]');
// console.info(nextLink);
const elements = document.querySelectorAll(mainSelector);
return Array.from(elements)
.slice(0, 3)
.map((element) => {
const title =
titleSelector.length > 0 &&
(element.querySelector(titleSelector) as HTMLElement | null)
?.innerText;
const ref =
refSelector.length > 0 &&
(element.querySelector(refSelector) as HTMLAnchorElement | null)
?.href;
return { title, ref };
});
},
main,
title,
ref
);
// await page.click(".pagination > a");
await browser.close();
return books;
};
Вот URL-адрес Goodreads, с которого я начал:
export const GOODREADS_POPULAR_LISTS_URL = "https://www.goodreads.com/list/popular_lists";
Итак, мой вопрос: как я могу оптимизировать свой подход и какие методы я могу использовать, чтобы ускорить очистку и, таким образом, радикально улучшить производительность моего API?
Я искал разные посты и многие предлагали какие-то манипуляции с процессором, но я не понял, как это можно использовать в моем случае. Также довольно много раз предлагался дочерний процесс в Node.js.
Заранее спасибо!
Вот оно: Export const GOODREADS_POPULAR_LISTS_URL = "goodreads.com/list/popular_lists ";
Спасибо. Я думаю, что данные доступны статически, поэтому попробуйте просто использовать fetch и Cheerio вместо Puppeteer, или хотя бы отключите JS и заблокируйте все возможные запросы. Совет по кэшированию в ответе тоже кажется хорошим - я сомневаюсь, что эти данные меняются так часто, вероятно, кеширование в течение часа или хотя бы получаса в большинстве случаев сэкономит массу работы или даже ежечасное выполнение фонового задания, поэтому вы может всегда предоставлять данные мгновенно.
Я полностью согласен, я видел, что он статичен, и, вероятно, cherio может быть ответом на этот вопрос, но puppeteer кажется более мощным и подходит для динамических веб-сайтов, таких как SPA. Целесообразен ли гибридный подход?
Если он статический, то добавление накладных расходов на кукловода является спорным. Если это не спа, зачем использовать инструмент, предназначенный для спа? Зачем гибридизироваться, если это не спа?
Я имел в виду, что в будущем, когда я создам парсинг для большего количества веб-сайтов, в том числе и динамических.
Я также обсуждаю это в своем блоге. Я почти не вижу вариантов использования для объединения Puppeteer и fetch/Cheerio - если данные находятся там статически и сервер пропускает вас, используйте более простой вариант выборки. Если данные невозможно получить с помощью более простого варианта, вам придется использовать более сложный подход, такой как Puppeteer. Прочтите ее — я думаю, она стоит вашего времени и, по сути, ответит на все ваши вопросы.
Если потенциально этот API в какой-то момент превратится в API парсинга, каким будет подход — для статических веб-сайтов это будет «cheerio + fetch», а для динамических — «кукловод»?
Конечно, но имейте в виду, что в парсинге нет серебряной пули, поэтому это действительно зависит от сайтов, которые вы парсите, а также от варианта использования и характера данных, которые вы хотите получить. Это не так просто, как твердое правило, это просто практические правила. Ваши рекомендации хороши, но существует бесконечное количество исключений, поэтому приготовьтесь к большой работе, если вы пытаетесь написать общий API для парсинга, если только вы не можете принять некоторые ограничения, например ограниченный набор сайтов, захватывающий только простые ответов или принять довольно высокий уровень неудач. Трудно сказать больше без подробностей.
Я думаю, очевидно, что я новичок в мире парсинга, так что я просто пытаюсь понять, как это сделать, а затем перейду к более сложным задачам, таким как тот более общий API, о котором мы говорим. Но, например, если я хочу очистить данные книг с нескольких веб-сайтов, и есть как статические, так и SPA, как будет выглядеть этот «общий API» в этом примере. И что вы имеете в виду, говоря «например, ограниченный набор сайтов, собирающих только простые ответы или допускающих довольно высокий уровень отказов».
Есть несколько вещей, на которые можно обратить внимание, чтобы улучшить скорость.
puppeteer
при каждом запросе. Мы можем создать модуль по шаблону Singleton.// browser_instance.ts
import puppeteer from 'puppeteer';
let browserInstance = null;
export async function getBrowser() {
if (!browserInstance) {
browserInstance = await puppeteer.launch({ ... });
}
return browserInstance;
}
Затем в своем коде попытайтесь получить экземпляр
import { getBrowser } from './browser-instance.js';
....
export const scraperHandler = async (req: Request, res: Response, next: NextFunction) => {
const browser = await getBrowser();
// ... use the browser instance for scraping
};
Promise.all
, чтобы дождаться одновременного завершения всех запросов.const results = await Promise.all(listUrls.map(async (url) => {
const page = await browser.newPage();
// ... scraping logic for each page ...
}));
Вы можете сделать гораздо больше оптимизаций. Подумайте о том, как найти медленные точки в вашем приложении, и проведите исследование о том, как оптимизировать процесс.
Где мне следует использовать Promise.all? :)
Мне ваш подход кажется излишним. Необходимые данные находятся в статическом HTML-коде, поэтому одного запроса к странице, похоже, достаточно, чтобы получить данные с помощью встроенного в Node API-интерфейса выборки и простого статического анализатора HTML Cheerio:
const cheerio = require("cheerio"); // ^1.0.0-rc.12
const url = "<Your URL>";
fetch(url)
.then(res => {
if (!res.ok) {
throw Error(res.statusText);
}
return res.text();
})
.then(html => {
const $ = cheerio.load(html);
const data = [...$(".listTitle")].map(e => ({
title: $(e).text(),
ref: $(e).attr("href"),
}));
console.info(data);
})
.catch(err => console.error(err));
Для сравнения вот оптимизированная версия Puppeteer:
const puppeteer = require("puppeteer"); // ^22.6.0
const url = "<Your URL>";
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.setJavaScriptEnabled(false);
const ua =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36";
await page.setUserAgent(ua);
await page.setRequestInterception(true);
page.on("request", req => {
if (req.url() === url) {
req.continue();
} else {
req.abort();
}
});
await page.goto(url, {waitUntil: "domcontentloaded"});
const data = await page.$$eval(".listTitle", els => els.map(el => ({
title: el.textContent,
ref: el.href,
})));
console.info(data);
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
Вот (по сути) ваша версия — неоптимизированная, кроме "domcontentloaded"
, что является хорошим началом:
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
const ua =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36";
await page.setUserAgent(ua);
await page.goto(url, {waitUntil: "domcontentloaded"});
const data = await page.$$eval(".listTitle", els => els.map(el => ({
title: el.textContent,
ref: el.href,
})));
console.info(data);
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
Неоптимизированный Кукловод:
real 0m1.806s
user 0m0.801s
sys 0m0.204s
Оптимизированный Кукловод:
real 0m1.251s
user 0m0.651s
sys 0m0.107s
Получить + Приветствие:
real 0m0.836s
user 0m0.251s
sys 0m0.035s
Использование fetch и Cheerio обеспечивает более чем двукратное ускорение, более простой код и отсутствие зависимости от такой сложной библиотеки, как Puppeteer. Основные причины использования Puppeteer для парсинга — это когда вам необходимо выполнять сложные взаимодействия, такие как клики, перехватывать запросы, работать с файлами cookie, избегать блокировок, ждать загрузки SPA и т. д. (некоторые из этих случаев могут применяться здесь, но сначала добавьте пользователя агент на запрос на получение, если вас заблокировали).
Независимо от того, какой подход к очистке вы используете, кэширование ответа — хорошая идея. Я сомневаюсь, что данные так сильно изменяются, поэтому вы можете повторно получать их только раз в час или около того, возможно, в фоновом задании, и мгновенно передавать их всем из кеша через свой прокси.
См. эту запись в моем блоге для получения подробной информации о методах, которые я использовал для ускорения этого сценария.
Какой URL-адрес? Возможно, вы сможете отключить JS или использовать
fetch
вместо Puppeteer, или, по крайней мере, заблокировать ресурсы, такие как таблицы стилей, изображения, шрифты, сценарии отслеживания и т. д., которые не имеют отношения к извлекаемым вами данным. Вы также можете обойти сайт и напрямую обратиться к API или, по крайней мере, перехватить ответ сети. Все это зависит от сайта.