Как да създадете потребителски API от всеки уебсайт с помощта на Puppeteer

Често се случва да попаднете на уебсайт и да сте принудени да извършите набор от действия, за да получите най-накрая някои данни. След това се изправяте пред дилема: как правите тези данни достъпни във форма, която лесно може да бъде използвана от вашето приложение?

В такъв случай на помощ идва остъргването. И изборът на подходящия инструмент за работата е доста важен.

Puppeteer: Не просто поредната библиотека за изстъргване

Puppeteer е библиотека Node.js, поддържана от екипа на Chrome Devtools в Google. Той основно изпълнява екземпляр на Chromium или Chrome (може би по-разпознаваемото име) по безглав (или конфигурируем) начин и излага набор от API на високо ниво.

От официалната си документация кукловодът обикновено се използва за множество процеси, които не се ограничават до следното:

  • Генериране на екранни снимки и PDF файлове
  • Обхождане на SPA и генериране на предварително изобразено съдържание (т.е. визуализация от страна на сървъра)
  • Тестване на разширения за Chrome
  • Тестване за автоматизация на уеб интерфейси
  • Диагностициране на проблеми с производителността чрез техники като улавяне на времевата следа на уебсайт

За нашия случай трябва да имаме достъп до уебсайт и да картографираме данните във форма, която лесно може да бъде използвана от нашето приложение.

Звучи просто? Изпълнението също не е толкова сложно. Да започваме.

Нанизване на кода

Любовта ми към продуктите на Amazon ме подтиква да използвам една от техните страници с продуктови списъци като пример тук. Ще приложим нашия случай на употреба в две стъпки:

  • Извличайте данни от страницата и ги картографирайте в лесно консумираща се форма JSON
  • Добавете малко доза автоматизация, за да улесните живота ни малко

Можете да намерите пълния код в това хранилище.

Ще извличаме данните от тази връзка: //www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2 (списък на най-търсените ризи, както е показано на изображението) в подлежаща на API форма.

Преди да започнем да използваме широко кукловод в този раздел, трябва да разберем двата основни класа, предоставени от него.

  • Браузър: стартира екземпляр на Chrome, когато използваме puppeteer.launchили puppeteer.connect. Това работи като проста емулация на браузъра.
  • Страница: прилича на отделен раздел в браузър Chrome. Той предоставя изчерпателен набор от методи, които можете да използвате с определен екземпляр на страница и се извиква, когато се обадим browser.newPage. Точно както можете да създадете множество раздели в браузъра, можете по същия начин да създадете множество екземпляри на страници едновременно в кукловод.

Настройване на кукловод и навигиране до целевия URL адрес

Започваме да настройваме кукловод, като използваме предоставения модул npm. След като инсталираме кукловод, създаваме екземпляр на браузъра и класа на страницата и се придвижваме до целевия URL адрес.

const puppeteer = require('puppeteer'); const url = '//www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2'; async function fetchProductList(url) { const browser = await puppeteer.launch({ headless: true, // false: enables one to view the Chrome instance in action defaultViewport: null, // (optional) useful only in non-headless mode }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); ... } fetchProductList(url); 

Използваме networkidle2като стойност за waitUntilопцията, докато навигираме до URL адреса. Това гарантира, че състоянието на зареждане на страницата се счита за окончателно, когато има не повече от 2 връзки, работещи за поне 500ms.

Забележка: Не е необходимо да имате инсталиран Chrome или негов екземпляр на вашата система, за да работи кукловод. Вече се доставя с олекотена версия, свързана с библиотеката.

Методи на страници за извличане и картографиране на данни

DOM вече се е заредил в създадения екземпляр на страницата. Ще продължим и ще използваме page.evaluate()метода за запитване към DOM.

Преди да започнем, трябва да разберем точните точки от данни, които трябва да извлечем. В текущата проба всеки от продуктовите обекти ще изглежда по следния начин.

{ brand: 'Brand Name', product: 'Product Name', url: '//www.amazon.in/url.of.product.com/', image: '//www.amazon.in/image.jpg', price: '₹599', }

Изложихме структурата, която искаме да постигнем. Време е да започнете да проверявате DOM за идентификаторите. Проверяваме за селектори, които се появяват в целия обект, който трябва да бъде картографиран. Ние ще използваме най-вече document.querySelectorи document.querySelectorAllза преминаване през DOM.

... async function fetchProductList(url) { ... await page.waitFor('div[data-cel-widget^="search_result_"]'); const result = await page.evaluate(() => { // counts total number of products let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length; let productsList = []; for (let i = 1; i  0 ? onlyProduct = true : emptyProductMeta = true; } let productsDetails = productNodes.map(el => el.innerText); if (!emptyProductMeta) { product.brand = onlyProduct ? '' : productsDetails[0]; product.product = onlyProduct ? productsDetails[0] : productsDetails[1]; } // traverse for product image let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`); product.image =rawImage ? rawImage.src : ''; // traverse for product url let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`); product.url = rawUrl ? rawUrl.href : ''; // traverse for product price let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`); product.price = rawPrice ? rawPrice.innerText : ''; if (typeof product.product !== 'undefined') { !product.product.trim() ? null : productsList = productsList.concat(product); } } return productsList; }); ... } ...

// траверс за имена на марки и продукти

След проучване на DOM, виждаме, че всеки изброен елемент е затворен под елемент с селектора div[data-cel-widget^="search_result_"]. Този конкретен селектор търси всички divтагове с атрибута data-cel-widget, чиято стойност започва с search_result_.

По същия начин картографираме селекторите за параметрите, които изискваме, както са изброени. Ако искате да научите повече за DOM обхода, можете да разгледате тази информативна статия от Zell.

  • общо изброени елементи:div[data-cel-widget^="search_result_"]
  • марка:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base ( iозначава номера на възела в total listed items)
  • product:div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base  или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal( iозначава номера на възела в total listed items)
  • url:div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal ( iозначава номера на възела в total listed items)
  • изображение:div[data-cel-widget="search_result_${i}"] .s-image ( iозначава номера на възела в total listed items)
  • цена:div[data-cel-widget="search_result_${i}"] span.a-offscreen ( iозначава номера на възела в total listed items)
Забележка: Изчакваме div[data-cel-widget^="search_result_"]елементите с име на селектор да бъдат налични на страницата с помощта на page.waitForметода.

След като page.evaluateметодът бъде извикан, можем да видим данните, които се изискват, регистрирани.

Adding Automation to Ease Flow

So far we are able to navigate to a page, extract the data we need, and transform it into an API-ready form. That sounds all hunky-dory.

However, consider for a moment a case where you have to navigate to one URL from another by performing some actions – and then try to extract the data you need.

Would that make your life a little trickier? Not at all. Puppeteer can easily imitate user behavior. Time to add some automation to our existing use case.

Unlike in the previous example, we will go to the amazon.in homepage and search for 'Shirts'. It will take us to the products listing page and we can extract the data required from the DOM. Easy peasy. Let's look at the code.

... async function fetchProductList(url, searchTerm) { ... await page.goto(url, { waitUntil: 'networkidle2' }); await page.waitFor('input[name="field-keywords"]'); await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm); await page.click('div.nav-search-submit.nav-sprite'); // DOM traversal and data mapping logic // returns a productsList array ... } fetchProductList('//amazon.in', 'Shirts'); 

We can see that we wait for the search box to be available and then we add the searchTerm passed using page.evaluate. We then navigate to the products listing page by emulating the 'search button' click action and exposing the DOM.

The complexity of automation varies from use case to use case.

Some Notable Gotchas: A Minor Heads Up

Puppeteer's API is pretty comprehensive but there are a few gotchas I came across while working with it. Remember, not all of these gotchas are directly related to puppeteer but tend to work better along with it.

  • Puppeteer creates a Chrome browser instance as already mentioned. However, it is likely that some existing websites might block access if they suspect bot activity. There is this package called user-agents which can be used with puppeteer to randomize the user-agent for the browser.
Забележка: Изстъргването на уебсайт се намира някъде в сивите зони на законово приемане. Бих препоръчал да го използвате с повишено внимание и да проверите правилата, където живеете.
const puppeteer = require('puppeteer'); const userAgent = require('user-agents'); ... const browser = await puppeteer.launch({ headless: true, defaultViewport: null }); const page = await browser.newPage(); await page.setUserAgent(userAgent.toString()); ...
  • Попаднахме defaultViewport: nullпри стартирането на нашия екземпляр на Chrome и го бях посочил като незадължителен. Това е така, защото е полезно само когато разглеждате стартирания екземпляр на Chrome. Той предотвратява влиянието на ширината и височината на уебсайта, когато се визуализира.
  • Puppeteer не е най-доброто решение по отношение на производителността. Вие, като разработчик, ще трябва да го оптимизирате, за да увеличите ефективността на неговата производителност чрез действия като ограничаване на анимации на сайта, позволяващи само съществени мрежови разговори и т.н.
  • Remember to always end a puppeteer session by closing the Browser instance by using browser.close. (I happened to miss out on it in the first try) It helps end a running Browser Session.
  • Certain common JavaScript operations like console.log() will not work within the scope of the page methods. The reason being that the page context/browser context differs from the node context in which your application is running.

These are some of the gotchas I noticed. If you have more, feel free to reach out to me with them. I would love to learn more.

Done? Let's run the application.

Website to Your API: Bringing it All Together

The application is run in non-headless mode so you can witness what exactly happens. We will automate the navigation to the product listing page from which we obtain the data.

There. You have your own API consumable data setup from the website of your choice. All you need to do now is to wire this up with a server side framework like express and you are good to go.

Conclusion

There is so much you can do with Puppeteer. This is just one particular use case. I would recommend that you spend some time to read the official documentation. I will be doing the same.

Puppeteer is used extensively in some of the largest organizations for automation tasks like testing and server side rendering, among others.

There is no better time to get started with Puppeteer than now.

If you have any questions or comments, you can reach out to me on LinkedIn or Twitter.

In the meantime, keep coding.