CoderCastrov logo
CoderCastrov
Парсер веб-страниц

Использование Node.js и Puppeteer для парсинга веб-страниц

Использование Node.js и Puppeteer для парсинга веб-страниц
просмотров
20 мин чтение
#Парсер веб-страниц

Если вы не можете получить нужные данные из веб-страницы с помощью API, вы можете использовать парсинг (или скрапинг) для получения данных. Легальность парсинга зависит от вашего местоположения и местоположения данных, но в данном случае мы будем использовать данные с сайта books.toscrape.com в тестовых целях, что не создает юридических проблем.

Мы будем парсить страницы с книгами, классифицировать их по категориям и записывать данные в JSON файл. Для этого вам потребуется установленный Node.js на вашем компьютере. Если его нет, вы можете установить его отсюда.


1. Шаг — Установка Веб-Скрэпера

После установки Node.js сначала создайте корневую папку проекта и перейдите в нее. Все команды вы будете выполнять из этой папки. Для установки пакетов вы будете использовать npm, которая уже устанавливается по умолчанию вместе с Node.js.

mkdir book-scraper
cd book-scraper

Запустите npm с помощью следующей команды, чтобы создать файл package.json. Этот файл содержит зависимости вашего проекта и метаданные проекта.

npm init

Ответьте на вопросы npm или нажмите Enter, чтобы продолжить с значениями по умолчанию. Убедитесь, что вы не меняете значения для entry point: и test command: при их запросе. Если вы не хотите этого делать, вы можете использовать команду npm init -y, чтобы npm автоматически создала все значения. В результате должно быть примерно следующее:

Output
{
  "name": "sammy_scraper",
  "version": "1.0.0",
  "description": "a web scraper",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "sammy the shark",
  "license": "ISC"
}Is this OK? (yes) yes

Введите "yes" и нажмите Enter, чтобы позволить npm сохранить этот вывод в файле package.json.

Теперь вы можете установить puppeteer с помощью следующей команды.

npm install --save puppeteer

На Linux-машине могут потребоваться дополнительные зависимости. Если вы используете Ubuntu 18.04, ознакомьтесь с этой ссылкой для получения информации о дополнительных зависимостях. Вы можете использовать следующую команду, чтобы найти отсутствующие зависимости.

ldd chrome | grep not

Вам также потребуется внести небольшие изменения в файл package.json. Поскольку вы будете запускать свое приложение с помощью команды npm run start, вам нужно добавить некоторую информацию о команде в файл package.json. В частности, вам нужно добавить одну строку под разделом scripts. Чтобы сделать это, откройте файл package.json с помощью следующей команды.

nano package.json

Найдите раздел scripts и добавьте следующие строки. Убедитесь, что вы добавляете запятую в конец строки "test". В противном случае файл package.json не будет правильно разобран, так как он не будет соответствовать формату.

Output
{
  . . .
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  . . .
  "dependencies": {
    "puppeteer": "^5.2.1"
  }
}

Зависимость puppeteer будет отображаться под разделом dependencies. Файл package.json не требует дополнительных изменений. Сохраните и закройте файл.

Теперь вы можете начать писать код для парсинга. Для этого вы можете создать экземпляр браузера и затем протестировать основные функции парсера.

2. Adım — Bir Tarayıcı Instance’ı Oluşturma

Geleneksel bir tarayıcı açtığınızda, bir butona tıklayabilir, mouse’unuzla gezinebilir, yazı yazabilir, dev tools’u açabilir ve daha başka şeyler yapabilirsiniz. Chromium gibi bir headless browser da size aynı imkanları tanır. Tek farkı ise bunun bir program kodu ile ve kullanıcı arayüzü olmaksızın gerçekleşmesidir. Bu adımda, tarayıcınızın bir instance’ını oluşturacaksınız. Uygulamanızı çalıştırdığınızda Chromium otomatik olarak başlayacak ve books.toscrape.com’a gidecektir. Bu süreç kazımanın en temel adımlarıdır.

Kazıyıcınız dört adet .js dosyasına ihtiyaç duyar. Bunlar browser.js, index,js, pageController.js, ve pageScraper.js dosyalarıdır. Bu adımda bahsettiğimiz bu dosyaları oluşturacağız. browser.js dosyası tarayıcınızı başlatacaktır.

Projenizin kök dizininde aşağıdaki komutla browser.js dosyasını oluşturun.

nano browser.js

İlk önce Puppeteer’ı import edersiniz. Daha sonra ise startBrowser() fonksiyonunu async olarak oluşturursunuz. Bu fonksiyon tarayıcıyı başlatır ve bir instance’ını döndürür. Aşağıdaki kod browser.js dosyasıdır.

const puppeteer = require('puppeteer');async function startBrowser(){
    let browser;
    try {
        console.log("Tarayıcı açılıyor......");
        browser = await puppeteer.launch({
            headless: false,
            args: ["--disable-setuid-sandbox"],
            'ignoreHTTPSErrors': true
        });
    } catch (err) {
        console.log("Tarayıcı instance'ı oluşturulamadı => : ", err);
    }
    return browser;
}module.exports = {
    startBrowser
};

Puppeteer’ın .launch metodu vardır. Bahsedilen tarayıcı instance’ı bu metod ile oluşturulur. Bu metod, bir Promise döndürür. Bu nedenle döndürülen bu Promise’in .then veya .await bloğuyla çözüldüğünden emin olmalısınız.

Burada await kullanılarak Promise’in çözüldüğünden emin oluyor, tarayıcının bir instance’ını döndürüyor ve tüm bunları try-catch bloğu ile sarmalıyoruz.

.launch() metodu aşağıdaki gibi çeşitli değerler alan bir JSON parametresi alır.

  • headless: false seçerseniz tarayıcı bir arayüz üzerinden çalışacaktır. Böylelikle kodunuzun nasıl çalıştığını görebilirsiniz. Öte yandan true seçmeniz halinde tarayıcı headless olarak çalışıp size bir arayüz sunmayacaktır. Eğer kodunuzu cloud’da çalıştırmak istiyorsanız headless’ı true seçmelisiniz. Çünkü birçok sanal makina headless’dır ve size bir kullanıcı arayüzü sunmaz. headful ihtiyacınız ise sadece kodunuzu test etmek için olacaktır. Kodunuzu deploy ettikten sonra headless’ı true yapmalısınız.
  • ignoreHTTPSErrors: true olduğunda “https olmayan” siteleri ziyaret edebilmenize imkan verir. ayrıca https ile ilgili hatalara takılmamanızı sağlar.

Dosyanızı kaydedip kapatın.

Aşağıdaki komutla ikici .js dosyanız olan index.js’i oluşturun.

nano index.js

Bu aşamada require ile browser.js ve pageController.js dosyalarını import etmeniz gerekiyor. Sonra startBrowser() ile tarayıcı instance’ımızı page controller’ımıza atayarak browser’ı yönetebilir hale geleceğiz. Aşağıdaki kod index.js dosyasıdır.

const browserObject = require('./browser');
const scraperController = require('./pageController');//Tarayıcıyı başlatıyor ve bir tarayıcı instance'ı oluşturuyoruz. 
let browserInstance = browserObject.startBrowser();// Tarayıcı instance'ını scraper controller'a atıyoruz. 
scraperController(browserInstance)

Dosyayı kaydedip kapatın.

Çünkü dosyanız olan pageController.js’i aşağıdaki komutla yaratın.

nano pageController.js

pageController.js dosyası kazıma sürecinizi kontrol eder. Tüm kazıma komut dosyalarının yürütüldüğü pageScraper.js dosyasını kontrol etmek için tarayıcı instance’ını kullanır. Sonuç olarak, hangi kitap kategorisini kazımayacağınızı belirtmek için kullanacaksınız. Fakat şu anda sadece Chromium’u açıp bir web sayfası açabildiğimizden emin olmalıyız. Aşağıdaki kod pageController.js dosyasıdır.

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        await pageScraper.scraper(browser);    }
    catch(err){
        console.log("Tarayıcı instance'ı resolve edilemedi => ", err);
    }
}module.exports = (browserInstance) => scrapeAll(browserInstance)

Yukarıdaki kodla bir fonksiyon export etmektedir. Bu fonksiyon, tarayıcı instance’ını almakta ve scrapeAll() isimli fonksiyona aktarmaktadır.

Dosyayı kaydedip kapatın.

En son olarak pageScraper.js isimli son .js dosyamızı oluşturacağız.

nano pageScraper.js

Aşağıda görüldüğü üzere, bir url ve scraper() metodu ile bir nesne oluşturuyoruz. Bu url, kazıyacağınız sitenin url’sidir. Öte yandan,** scraper()** metodu ise asıl kazımayı gerçekleştirecek koddur. Fakat şu an için sadece belirtilen url’ye gitmektedir. Aşağıdaki kod pageScraper.js dosyasıdır.

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        await page.goto(this.url);
    }
}module.exports = scraperObject;

Puppeteer’ın newPage() isimli bir metodu vardır. Bu metod, tarayıcıda yeni bir sayfa instance’ı oluşturur. Bu sayfa instance’ı ile epeyce çok şey yapılabilmektedir. Yukarıda yarattığınız scraper() isimli metod içinde, page isimli instance’ı page.goto() metodu ile kullanarak books.scrape.com isimli siteyi açabiliyorsunuz.

Dosyayı kaydedip kapatın.

Artık dosya yapınız tamamlandı. Projenizin yapısı aşağıdaki gibi olmalıdır.

Output
.
├── browser.js
├── index.js
├── node_modules
├── package-lock.json
├── package.json
├── pageController.js
└── pageScraper.js

Aşağıdaki komutla kazıyıcınızı çalıştırabilirsiniz.

npm run start

Programınız otomatik olarak bir Chromium instance’ını açacak ve books.toscrape.com adresine gidecektir.

Bu aşamada sadece Chromium açan ve books.toscrape.com adresine giden bir Puppeteer uygulaması yazmış oldunuz. bir sonraki adımda bu sitedeki her bir kitabı kazıyacaksınız.

3. Шаг - Парсинг данных с веб-страницы

Перед тем, как добавлять новые функции в ваш код, откройте веб-страницу books.toscrape.com в своем браузере и изучите структуру данных.

Категории книг находятся слева, а сами книги - справа. При нажатии на книгу переходите на страницу этой книги. Вам нужно будет имитировать это поведение.

При использовании инструментов разработчика вы увидите, что данные каждой книги перечислены под тегом section. Информация о странице, цене и наличии товара находится внутри тега li под тегом section.

Вы будете парсить URL-адреса этих книг и выводить список книг, которые есть в наличии. Затем вы сможете получить доступ к каждой странице книги и ее данным.

Для реализации этого вам нужно открыть файл pageScraper.js с помощью следующей команды:

nano pageScraper.js

Добавьте следующий код, выделенный жирным шрифтом, в файл pageScraper.js. С помощью этого кода мы добавим еще один блок await внутрь блока await page.goto(this.url);. Ниже приведен код для файла pageScraper.js:

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Переход на страницу ${this.url}...`);
        // Переходим на выбранную страницу
        await page.goto(this.url);
        **// Ожидаем рендеринга требуемого DOM браузером
        await page.waitForSelector('.page_inner');
        // Получаем ссылки на все необходимые книги
        let urls = await page.$$eval('section ol > li', links => {
            // Убедитесь, что книга, которую вы хотите спарсить, есть в наличии
            links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
            // Извлекаем ссылки из данных
            links = links.map(el => el.querySelector('h3 > a').href)
            return links;
        });
        console.log(urls);**
    }
}module.exports = scraperObject;

В этом коде вызывается метод page.waitForSelector(). Этот код ожидает, пока DOM-элемент, содержащий данные о книге, будет отрендерен внутри DOM. После этого вызывается метод page.$$eval(), который получает URL-элемент с помощью селектора section ol li. Убедитесь, что page.$eval() и page.$$eval() всегда возвращают только строку или число.

У каждой книги есть два состояния: "в наличии" и "нет в наличии". Вам нужно спарсить только книги, которые есть в наличии. Поскольку page.$$eval() возвращает массив всех совпадающих элементов, вы фильтруете все книги в наличии. Фильтрацию вы выполняете, ища класс .instock.availability и используя его. Затем вы отображаете свойство href ссылки на книгу с помощью метода map и возвращаете его из функции.

Сохраните и закройте файл. Запустите приложение с помощью следующей команды:

npm run start

Браузер откроется, начнется навигация по веб-странице, а затем браузер закроется, когда задача будет завершена. Проверьте консоль. Вы увидите спарсенные URL-адреса.

Output
> book-scraper@1.0.0 start /Users/sammy/book-scraper
> node index.jsOpening the browser......
Navigating to http://books.toscrape.com...
[
  'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html',
  'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html',
  'http://books.toscrape.com/catalogue/soumission_998/index.html',
  'http://books.toscrape.com/catalogue/sharp-objects_997/index.html',
  'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html',
  'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html',
  'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html',
  'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html',
  'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html',
  'http://books.toscrape.com/catalogue/the-black-maria_991/index.html',
  'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html',
  'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html',
  'http://books.toscrape.com/catalogue/set-me-free_988/index.html',
  'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html',
  'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html',
  'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html',
  'http://books.toscrape.com/catalogue/olio_984/index.html',
  'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html',
  'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html',
  'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html'
]

Это хороший старт, но вы хотите парсить не только URL-адреса книг, а все данные на этих страницах. Теперь вы будете использовать каждый из этих URL-адресов, чтобы получить данные о каждой странице книги, такие как название, автор, цена, наличие, описание и изображение.

Откройте файл pageScraper.js с помощью следующей команды:

nano pageScraper.js

Ниже приведен выделенный жирным код, который будет выполняться для каждой ссылки. С помощью этого кода вы открываете новый экземпляр страницы для каждой ссылки и извлекаете требуемые данные. Ниже приведен код для файла pageScraper.js:

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Переход на страницу ${this.url}...`);
        // Переходим на выбранную страницу
        await page.goto(this.url);
        // Ожидаем рендеринга требуемого DOM браузером
        await page.waitForSelector('.page_inner');
        // Получаем ссылки на все необходимые книги
        let urls = await page.$$eval('section ol > li', links => {
            // Убедитесь, что книга, которую вы хотите спарсить, есть в наличии
            links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
            // Извлекаем ссылки из данных
            links = links.map(el => el.querySelector('h3 > a').href)
            return links;
        });
        let pagePromise = (link) => new Promise(async(resolve, reject) => {
            let dataObj = {};
            let newPage = await browser.newPage();
            await newPage.goto(link);
            dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
            dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
            dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                // Удаляем новые строки и табуляцию
                text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                // Получаем количество товара в наличии
                let regexp = /^.*\((.*)\).*$/i;
                let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                return stockAvailable;
            });
            dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
            dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
            dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
            resolve(dataObj);
            await newPage.close();
        });
        for(link in urls){
            let currentPageData = await pagePromise(urls[link]);
            console.log(currentPageData);
        }
    }
}module.exports = scraperObject;

У вас есть массив URL-адресов. Вам нужно перебрать этот массив с помощью цикла. Для каждого URL-адреса вы открываете новую страницу и парсите данные. Затем вы закрываете страницу и повторяете этот процесс для каждого нового URL-адреса в массиве. Обратите внимание, что вы обернули этот код в Promise. Это необходимо, чтобы дождаться завершения каждой операции в цикле. Promise позволяет вам сделать это. Поэтому каждый Promise открывает новый URL и не заканчивается, пока не будет спарсена вся информация на этой странице.

Если вы внимательно посмотрите на функцию pagePromise, вы увидите, что ваш парсер сначала создает новую страницу для каждого URL. Затем вы выбираете селекторы, чтобы выбрать данные, которые вы хотите спарсить, с помощью функции page.$eval(). Некоторые из текстов могут содержать пробелы, табуляцию, переносы строк и другие неалфавитно-цифровые символы. Для доступа к ним вам нужно использовать регулярные выражения. Затем вы присваиваете спарсенные данные объекту и можете использовать их.

Сохраните и закройте файл.

Запустите код снова с помощью следующей команды:

npm run start

Браузер откроется, затем откроются страницы каждой книги и их данные будут выведены в консоль. Вывод в вашей консоли будет выглядеть следующим образом:

Output
Opening the browser......
Navigating to http://books.toscrape.com...
{
  bookTitle: 'A Light in the Attic',
  bookPrice: '£51.77',
  noAvailable: '22',
  imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg',
  bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]',
  upc: 'a897fe39b1053632'
}
{
  bookTitle: 'Tipping the Velvet',
  bookPrice: '£53.74',
  noAvailable: '20',
  imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg',
  bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`,
  upc: '90fa61229261140a'
}
{
  bookTitle: 'Soumission',
  bookPrice: '£50.10',
  noAvailable: '20',
  imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg',
  bookDescription: 'Dans une France assez proche de la nôtre, [...]',
  upc: '6957f44c3847a760'
}
...

На этом этапе вы парсите данные каждой книги, но хотите добавить еще больше функциональности. Например, страницы, на которых перечислены книги, имеют несколько страниц, разделенных номерами страниц. Кроме того, на сайте есть категории книг слева. Чтобы спарсить книги в определенной категории, вам нужно добавить код в вашу программу.

4. Шаг — Парсинг данных с нескольких страниц

На сайте Books.toscrape.com страницы разбиты на несколько частей, и на каждой странице, кроме последней, есть кнопка "next" для перехода на следующую страницу.

Вы можете определить, есть ли кнопка "next", проверив ее наличие. Поскольку данные на каждой странице имеют одинаковую структуру и разметку, вам не нужно писать отдельный парсинг для каждой страницы. Вместо этого вы можете использовать один и тот же код парсинга для повторяющейся структуры страницы и разметки, поместив его в цикл для повторного использования.

Для перехода на несколько страниц вам потребуется немного изменить ваш код.

Откройте файл pagescraper.js с помощью следующей команды:

nano pagescraper.js

Вы добавите новую функцию с именем scrapeCurrentPage() в ваш метод scraper(). Эта функция будет содержать весь код парсинга для конкретной страницы и щелкать кнопку "next", если она есть. Добавьте следующий выделенный жирным код ниже. Ниже приведен код файла pageScraper.js.

const scraperObject = {
    url: 'http://books.toscrape.com',
    async scraper(browser){
        let page = await browser.newPage();
        console.log(`Navigating to ${this.url}...`);
        // Navigate to the selected page
        await page.goto(this.url);
        let scrapedData = [];
        // Wait for the required DOM to be rendered
        async function scrapeCurrentPage(){
            await page.waitForSelector('.page_inner');
            // Get the link of all books on the current page
            let urls = await page.$$eval('section ol > li', links => {
                // Make sure the book to be scraped is in stock
                links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                // Extract the links from the data
                links = links.map(el => el.querySelector('h3 > a').href)
                return links;
            });
            // Loop through each of those links, open a new page instance and get the relevant data from them
            let pagePromise = (link) => new Promise(async(resolve, reject) => {
                let dataObj = {};
                let newPage = await browser.newPage();
                await newPage.goto(link);
                dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                    // Strip new line and tab spaces
                    text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                    // Get the number of stock available
                    let regexp = /^.*\((.*)\).*$/i;
                    let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                    return stockAvailable;
                });
                dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                resolve(dataObj);
                await newPage.close();
            });
            for(link in urls){
                let currentPageData = await pagePromise(urls[link]);
                scrapedData.push(currentPageData);
            }
            // After all the data is scraped, check if there's a next button on the current page
            // If so, click the next button and call this function recursively
            let nextButtonExist = false;
            try{
                const nextButton = await page.$eval('.next > a', a => a.textContent);
                nextButtonExist = true;
            }
            catch(err){
                nextButtonExist = false;
            }
            if(nextButtonExist){
                await page.click('.next > a');
                return scrapeCurrentPage(); // Call this function recursively
            }
            await page.close();
            return scrapedData;
        }
        let data = await scrapeCurrentPage();
        console.log(data);
        return data;
    }
}
module.exports = scraperObject;

Вы устанавливаете переменную nextButtonExist в false в начале и затем проверяете наличие кнопки. Если кнопка "next" существует, вы устанавливаете переменную nextButtonExist в true, щелкаете по кнопке "next" и затем вызываете эту функцию рекурсивно.

Если переменная nextButtonExist равна false, вы возвращаете массив scrapedData.

Сохраните и закройте файл.

Запустите свой код с помощью следующей команды:

npm run start

Ваше приложение начнет парсить данные для 800 книг в течение некоторого времени. Вы можете остановить процесс, закрыв браузер или нажав CTRL-C.

Вы максимально расширили возможности своего парсера. Однако у вас теперь есть новая проблема - большой объем данных. В следующем шаге вы настроите парсинг данных по категориям книг, чтобы сделать более точную настройку.

5. Шаг — Парсинг по категориям

Для парсинга по категориям вам нужно внести некоторые изменения в файлы pageScraper.js и pageController.js.

Откройте файл pageController.js с помощью следующей команды:

nano pageController.js

Добавьте следующие выделенные участки кода в файл pageController.js, чтобы создать парсер, который парсит только категорию "Путешествия". Ниже приведен код файла pageController.js.

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        **let scrapedData = {};**
     // вызываем парсер для книг в категории "Путешествия"
        **scrapedData['Путешествия'] = await pageScraper.scraper(browser, 'Путешествия');        await browser.close();        console.log(scrapedData)**
    }
    catch(err){
        console.log("Не удалось создать экземпляр браузера => ", err);
    }
}
module.exports = (browserInstance) => scrapeAll(browserInstance)

Вы добавили два параметра в ваш метод pageScraper.scraper(). Второй параметр указывает категорию книг, которую вы хотите спарсить, и вы передали ему значение "Путешествия".

Сохраните и закройте файл.

Однако ваш файл pageScraper.js пока не распознает этот параметр. Чтобы он мог его распознать, вам нужно открыть его с помощью следующей команды и внести некоторые изменения.

nano pageScraper.js

Добавьте следующий выделенный участок кода. Этот код позволяет вам получить доступ к параметру категории. Ниже приведен код файла pageScraper.js.

const scraperObject = {
    url: '[http://books.toscrape.com'](http://books.toscrape.com'),
    **async scraper(browser, category){**
        let page = await browser.newPage();
        console.log(`Переход на страницу ${this.url}...`);
**        // Переходим на выбранную страницу **
        await page.goto(this.url);
        **// Выбираем нужную категорию **
        **let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => {****        // Ищем элемент, соответствующий вашему запросу**
            **links = links.map(a => a.textContent.replace(/(\r\n\t|\n|\r|\t|^\s|\s$|\B\s|\s\B)/gm, "") === _category ? a : null);            let link = links.filter(tx => tx !== null)[0];            return link.href;        }, category);**
**        // Переходим на выбранную категорию**
        **await page.goto(selectedCategory);**
        let scrapedData = [];
        // Ждем, пока не будет отрисован нужный DOM
        async function scrapeCurrentPage(){
            await page.waitForSelector('.page_inner');
            // Получаем ссылки на все нужные книги
            let urls = await page.$$eval('section ol > li', links => {
                // Убеждаемся, что книга в наличии
                links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                // Извлекаем ссылки из данных
                links = links.map(el => el.querySelector('h3 > a').href)
                return links;
            });
            // Перебираем каждую ссылку, открываем новый экземпляр страницы и получаем нужные данные из них
            let pagePromise = (link) => new Promise(async(resolve, reject) => {
                let dataObj = {};
                let newPage = await browser.newPage();
                await newPage.goto(link);
                dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                    // Убираем переносы строк и табуляцию
                    text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");
                    // Получаем количество доступных книг
                    let regexp = /^.*\((.*)\).*$/i;
                    let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                    return stockAvailable;
                });
                dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                resolve(dataObj);
                await newPage.close();
            });for(link in urls){
                let currentPageData = await pagePromise(urls[link]);
                scrapedData.push(currentPageData);
                // console.log(currentPageData);
            }
            // Когда все данные на этой странице получены, нажимаем кнопку "Далее" и начинаем парсить следующую страницу
            // Сначала проверяем, существует ли эта кнопка, чтобы узнать, есть ли следующая страница.
            let nextButtonExist = false;
            try{
                const nextButton = await page.$eval('.next > a', a => a.textContent);
                nextButtonExist = true;
            }
            catch(err){
                nextButtonExist = false;
            }
            if(nextButtonExist){
                await page.click('.next > a');
                return scrapeCurrentPage(); // Вызываем эту функцию рекурсивно
            }
            await page.close();
            return scrapedData;
        }
        let data = await scrapeCurrentPage();
        console.log(data);
        return data;
    }
}module.exports = scraperObject;

Этот блок кода использует переданную вам категорию, чтобы получить URL страницы, на которой находятся книги этой категории.

В приведенном выше коде используется метод page.$$eval(), который позволяет вам получить доступ к аргументам, передаваемым в $$eval() метод, и определить их в качестве третьего параметра при вызове обратного вызова. Приведена структура использования ниже.

page.$$eval('selector', function(elem, args){
    // .......
}, args)

Вы сделали то же самое в своем коде; вы передали категорию, которую вы хотите спарсить, сопоставили все категории, чтобы проверить, какая совпадает с вашим запросом, и затем вернули URL этой категории.

Этот URL затем используется с помощью метода page.goto(selectedCategory) для перехода на страницу, отображающую категорию книг, которую вы хотите спарсить.

Сохраните и закройте файл.

Запустите свое приложение с помощью следующей команды. Вы увидите, что приложение переходит к категории "Путешествия", открывает каждую книгу в этой категории и регистрирует результаты:

npm run start

На этом шаге вы спарсили данные с нескольких страниц. Затем вы спарсили данные с нескольких страниц только для определенной категории. В следующем шаге вы будете делать оба этих действия одновременно. То есть вы будете парсить все страницы для нескольких категорий одновременно. Затем вы сохраните эти данные в JSON-файл.

Шаг 6 — Парсинг данных из нескольких категорий и сохранение данных в формате JSON

На этом последнем шаге вы настроите парсер для извлечения данных из нескольких категорий и затем преобразуете полученные журналы в формат JSON и сохраните их в файле data.json.

Вы можете легко добавить больше категорий для парсинга, для этого вам нужно всего лишь добавить одну дополнительную строку для каждого типа категории.

Откройте файл pageController.js с помощью следующей команды:

nano pageController.js

Добавьте код для всех других категорий, которые вам нужны. В приведенном ниже примере мы добавляем категории Историческая фикция и Мистика к нашей текущей категории Путешествия с помощью выделенного жирным кода. Ниже приведен код файла pageController.js.

const pageScraper = require('./pageScraper');
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        let scrapedData = {};
        // Call the scraper for different set of books to be scraped
        **scrapedData['Путешествия'] = await pageScraper.scraper(browser, 'Путешествия');
        scrapedData['Историческая фикция'] = await pageScraper.scraper(browser, 'Историческая фикция');
        scrapedData['Мистика'] = await pageScraper.scraper(browser, 'Мистика');**
        await browser.close();
        console.log(scrapedData)
    }
    catch(err){
        console.log("Не удалось создать экземпляр браузера => ", err);
    }
}module.exports = (browserInstance) => scrapeAll(browserInstance)

Сохраните и закройте файл.

Запустите код снова с помощью следующей команды и наблюдайте, как он парсит данные для всех трех категорий:

npm run start

Когда парсер полностью функционирует, вашим последним шагом будет сохранение данных в формате JSON, который является более удобным форматом для использования. В приведенном ниже примере показано, как использовать модуль fs в Node.js для сохранения данных в файле JSON.

Откройте файл pageController.js с помощью следующей команды:

nano pageController.js

Добавьте выделенный жирным код ниже. Ниже приведен код файла pageController.js.

const pageScraper = require('./pageScraper');
**const fs = require('fs');**
async function scrapeAll(browserInstance){
    let browser;
    try{
        browser = await browserInstance;
        let scrapedData = {};
        // Call the scraper for different set of books to be scraped
        scrapedData['Путешествия'] = await pageScraper.scraper(browser, 'Путешествия');
        scrapedData['Историческая фикция'] = await pageScraper.scraper(browser, 'Историческая фикция');
        scrapedData['Мистика'] = await pageScraper.scraper(browser, 'Мистика');
        await browser.close();
        **fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) {
            if(err) {
                return console.log(err);
            }
            console.log("Данные были успешно спарсены и сохранены! Вы можете посмотреть их в файле './data.json'");
        });**
    }
    catch(err){
        console.log("Не удалось создать экземпляр браузера => ", err);
    }
}module.exports = (browserInstance) => scrapeAll(browserInstance)

Во-первых, вам понадобится модуль fs в Node.js в файле pageController.js. Этот модуль позволяет вам сохранять данные в файле JSON. Затем, когда парсинг завершен и браузер закрыт, ваш код создаст новый файл с именем data.json. Содержимое файла data.json будет представлять собой тип stringified JSON. Поэтому, когда вы захотите прочитать и использовать данные из файла data.json, вам потребуется разобрать данные в формате JSON.

Сохраните и закройте файл.

Теперь у вас есть веб-парсер, который парсит книги из нескольких категорий и сохраняет полученные данные в файле JSON. По мере увеличения сложности вашего приложения, вы можете захотеть хранить эти спарсенные данные в базе данных или предоставлять их через API. Вы можете использовать эти данные через API по вашему усмотрению.

Результат

В этой статье вы создали веб-парсер, который извлекает данные с нескольких страниц и сохраняет их в JSON-файл. Короче говоря, вы узнали новый способ автоматизировать сбор данных с веб-сайтов.

Puppeteer имеет много других функций, которые не рассматриваются в этом руководстве. Эта статья не описывает headless-парсер, который работает в облаке. Чтобы узнать больше о headless-парсере, вы можете прочитать статьи Using Puppeteer for Easy Control Over Headless Chrome и Puppeteer’s official documentation.

Источник: https://www.digitalocean.com/community/tutorials/how-to-scrape-a-website-using-node-js-and-puppeteer

Источник: https://www.irfansimsar.com.tr/blog/yazilim/puppeteer-ile-arayuz-testi-yazmak/