Category: Инструменты

20
Сен
2020

💪📱 10 инструментов тестирования производительности мобильных приложений

Сравниваем инструменты тестирования производительности мобильных приложений, в том числе предоставляемые услуги и стоимость: Gatling, JMeter, Apptim и другие.

Пользователи не любят…

17
Сен
2020

DigitalOcean запустил новую облачную платформу для хостинга приложений. Сравниваем цены с AWS

DigitalOcean запустили вторую бета-версию своей новой платформы для хостинга приложений — App Platform. Сравнили их цены с сервисами AWS.
— Читать дальше «DigitalOcean запустил новую облачную платформу для хостинга приложений. Сравниваем цены с AWS»

16
Сен
2020

22 сентября, онлайн: вебинар «Разработка библиотеки компонентов на React»

Бесплатный вебинар, на котором участники научатся разработке презентационных компонентов на React используя Storybook.
— Читать дальше «Вебинар «Разработка библиотеки компонентов на React + Storybook»»

03
Сен
2020

15-17 сентября, Москва: конференция TestCon Moscow 2020

Специалисты поделятся инструментами и лучшими практиками для проведения тестов в целях улучшения качества программного обеспечения.
— Читать дальше «Конференция TestCon Moscow 2020»

29
Июл
2020

1 августа, онлайн: JavaGlobal Summit’20

Лучшие кейсы, семинары и живое общение в режиме вопросов и ответов с докладчиками из Oracle, Microsoft, IBM, JCP и других топовых компаний.
— Читать дальше «JavaGlobal Summit’20»

21
Июл
2020

📦 Что такое npm? Гайд по Node Package Manager для начинающих

Как установить, опубликовать и проверить JavaScript-пакеты на уязвимость с помощью npm – менеджера пакетов Node.js.

Программная платформа Node.js появилась в 2009 г., и с тех пор на ней были построены сотни тысяч приложений. Одной из причин успеха стал npm – популярный пакетный менеджер, позволяющий JS-разработчикам быстро делиться пакетами.

На момент написания статьи в npm содержится 1.3 млн пакетов с общим количеством скачиваний 16 млрд.

1. Что такое npm?

npm (Node Package Manager) – дефолтный пакетный менеджер для JavaScript, работающий на Node.js. Менеджер npm состоит из двух частей:

  • CLI (интерфейс командной строки) – средство для размещения и скачивания пакетов,
  • онлайн-репозитории, содержащие JS пакеты.

Структуру репозитория npmjs.com можно представить, как центр исполнения заказов, который получает товары (npm-пакеты) от продавцов (авторы пакетов) и распространяет эти товары среди покупателей (пользователи пакетов).

В центре исполнения заказов (npmjs.com) в качестве персональных менеджеров для каждого покупателя работает армия вомбатов (npm CLI).

Зависимости поставляются следующим образом (Рис. 1).

Рис. 1. Процесс установки пакета через <code class="inline-code">npm install</code>
Рис. 1. Процесс установки пакета через npm install

Процесс размещения пакета выглядит, как показано на Рис. 2.

Рис. 2. Процесс размещения пакета через <code class="inline-code">npm publish</code>
Рис. 2. Процесс размещения пакета через npm publish

Теперь детально рассмотрим работу вомбатов.

1.1. Файл package.json

Каждый проект в JavaScript – будь то Node.js или веб-приложение – может быть скопирован? как npm-пакет с собственным описанием и файлом package.json.

package.json можно представить, как стикеры (список пакетов нужных версий) на npm-коробке (проект). Файл генерируется командой npm init при создании JavaScript/Node.js проекта со следующими метаданными:

  • name: название JS библиотеки/проекта.
  • version: версия проекта.
  • description: описание проекта.
  • license: лицензия проекта.

1.2. Скрипты npm

В package.json включено поле scripts для автоматизации сборки, например:

        {
  "scripts": {
    "build": "tsc",
    "format": "prettier --write **/*.ts",
    "format-check": "prettier --check **/*.ts",
    "lint": "eslint src/**/*.ts",
    "pack": "ncc build",
    "test": "jest",
    "all": "npm run build && npm run format && npm run lint && npm run pack && npm test"
  }
}

    

eslint, prettier, ncc, jest могут быть установлены глобально или локально для проекта внутри node_modules/.bin/.

1.3. dependencies и devDependencies

dependencies и devdependencies представляют собой словари с именами npm-библиотек (ключ) и их семантические версии (значение). Пример из шаблона TypeScript Action:

        {
  "dependencies": {
    "@actions/core": "^1.2.3",
    "@actions/github": "^2.1.1"
  },
  "devDependencies": {
    "@types/jest": "^25.1.4",
    "@types/node": "^13.9.0",
    "@typescript-eslint/parser": "^2.22.0",
    "@zeit/ncc": "^0.21.1",
    "eslint": "^6.8.0",
    "eslint-plugin-github": "^3.4.1",
    "eslint-plugin-jest": "^23.8.2",
    "jest": "^25.1.0",
    "jest-circus": "^25.1.0",
    "js-yaml": "^3.13.1",
    "prettier": "^1.19.1",
    "ts-jest": "^25.2.1",
    "typescript": "^3.8.3"
  }
}
    

Эти зависимости устанавливаются командной npm install с флагами --save и --save-dev. Они предназначены соответственно для использования в продакшене и разработке.

О версионировании:

  • ^: последний минорный релиз. Например, ^1.0.4 установит версию 1.3.0, если это последний минорный релиз в серии 1 мажорного релиза.
  • ~: последний патч-релиз. ~1.0.4 установит 1.0.7, если эта последняя минорная версия в серии минорных релизов 1.0.

Все версии пакетов будут отображены в сгенерированном файле package-lock.json.

1.4. Файл package-lock.json

Файл package-lock.json описывает версии пакетов, используемые в JavaScript-проекте. Если package.json включает общее описание зависимостей (название товара), то package-lock.json более детальный – всё дерево зависимостей.

package-lock.json генерируется командой npm install и читается npm CLI, чтобы обеспечить воспроизведение окружения для проекта через npm ci.

2. Установка пакетов

Так как пользователи чаще скачивают пакеты (16 млрд скачиваний против 13 млн публикаций), хорошо бы разобраться, как их устанавливать.

2.1. npm install

npm install команда, устанавливающая пакеты.

По умолчанию npm install <package-name> со знаком ^ установит последнюю версию пакета. npm install скачает пакет в папку проекта node_modules в соответствии с конфигурацией в файле package.json, обновив версию пакета везде, где это возможно (и, в свою очередь, обновив package-lock.json). При необходимости установки пакета глобально можно указать флаг -g .

npm сделал установку пакетов JavaScript настолько простой, что команда часто используется некорректно и в сообществе разрабов появились мемы на эту тему:

Рис. 3. Мем про node_modules
Рис. 3. Мем про node_modules

При добавлении флага --production установятся только нужные для работы приложения зависимости из dependencies, не раздувая node_modules.

2.2. npm ci

Если npm install --production оптимален для продакшена, существует ли аналогичная команда для локальной разработки? Да, она называется npm ci.

Как и раньше, если package-lock.json еще не существует в проекте, он будет сгенерирован при вызове npm install. npm ci обращается к Lock-файлу для загрузки точной версии пакетов. Таким образом, на разных машинах набор пакетов останется неизменным.

2.3. npm audit

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

Пример аудита для пакета static-eval
Пример аудита для пакета static-eval

Если исправления доступны в следующих версиях пакета, npm audit fix автоматически обновит версии затронутых зависимостей.

3. Размещение пакетов

Перейдем от потребления пакетов к их размещению.

3.1. npm publish

Отправить пакет в npmjs.com очень просто – нужно набрать в консоли npm publish. Важная часть, которой пренебрегают авторы – версионирование. Вот набор эмпирические правил semver.org, указывающих, когда следует увеличить номер версии:

  • Мажорная версия: когда сделаны обратно несовместимые изменения API.
  • Минорная версия: когда вы добавляете новую функциональность, не нарушая обратной совместимости.
  • Патч-версия: когда вы делаете обратно совместимые исправления.

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

Заключение

В этой публикации мы познакомились со структурой npm и узнали:

  • в каких файлах хранятся данные о зависимостях (package.json, package-lock.json).
  • как установить пакеты в продакшен (npm install), на локальной машине (npm ci) и провести аудит пакетов (npm audit).
  • как добавить пакет в репозиторий (npm publish).
***

Если самостоятельная работа с npm-пакетами вызывает трудности, и вам требуется помощь наставника, мы советуем обратить внимание на курс факультета Веб-разработки GeekBrains, где вы получите готовую базу навыков и необходимую поддержку. Вы не только освоите работу с Node.js, но и научитесь целиком разрабатывать безопасные веб-приложения.

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

13
Июл
2020

16 июля, онлайн: Java community meetup

Поговорят про Apache Ignite, инструменты Spring для тестирования отдельных компонентов и о возможности его совместного использования с TestContainers.
— Читать дальше «Java community meetup»

01
Июл
2020

Программируем лучше с ESLint, Prettier и TypeScript

Рассказ о том, как правильно управлять инструментами ESLint и Prettier, которые позволяют писать более читабельный и продуктивный код.
— Читать дальше «Программируем лучше с ESLint, Prettier и TypeScript»

01
Июл
2020

Java Webinar «Gitlab CI – бесплатно, без регистрации и смс»

Вебинар, посвящённый Gitlab CI, на котором покажут сам инструмент, а также расскажут о том, как построить и настроить CI/CD для ваших проектов.
— Читать дальше «Java Webinar «Gitlab CI – бесплатно, без регистрации и смс»»

26
Июн
2020

2–4 июля, онлайн: интенсив «Станьте хакером на Python за 3 дня»

Изучите основы кодинга на Python, разберётесь с файлами и типами данных, научитесь проводить атаки методом грубой силы и добавите проект в портфолио.
— Читать дальше «Интенсив «Станьте хакером на Python за 3 дня»»

19
Июн
2020

👨‍💻️ Как выбрать сервер для 1С

Рассказываем, как выбрать сервер для продуктов 1С в зависимости от числа активных пользователей и других требований к системе.

Итак, вам нужно определиться с оборудованием для 1С. Для начала требуется определить параметры серверного и клиентского оборудования. Обсудим, что влияет на их выбор:

  • Количество одновременно работающих в системе пользователей.
  • Размер базы данных с учетом потенциального роста.
  • Особенности нагрузки, требования к отказоустойчивости.

Обычно число пользователей системы является ключевым параметром, определяющим остальные.

В зависимости от требований необходимо выбрать:

  • Дисковый массив: объем, пропускная способность при считывании и записи данных.
  • Процессор: количество ядер и частота.
  • Оперативная память: объём и частота.
  • Требование к скорости выполнения ключевых операций. Например: проверка остатков в панели менеджера за 0.3 секунды, формирование супер-отчета не более чем за минуту, расчет данных для утренней выписки путевых листов за ночь и т. п.

Расчёт параметров доступен на сайте 1С. В частности, там приведена следующая таблица зависимости характеристик от числа пользователей:

Рекомендуемые параметры с сайта 1С
Рекомендуемые параметры с сайта 1С

Вячеслав Гилёв в посте Рекомендации по процессорам при подборе серверного оборудования для 1С указывает на важность выбора процессора, в частности обращая внимание, что одним из ключевых факторов является частота:

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

При этом при покупке сервера стоимость процессора составляет примерно 10% от всего сервера, а вклад в общую производительность может достигать 50%.

Инженер Дмитрий Малышев указывает, что системные требования на сайте 1С являются минимальными и для работы рекомендует несколько отличающийся набор характеристик оборудования для серверов базы данных и 1С.

Рекомендации Дмитрия Малышева
Рекомендации Дмитрия Малышева

Общие рекомендации Малышева следующие:

  • Между серверами для обеспечения отказоустойчивости использовать сетевые интерфесы 2х10Гб.
  • На сервере СУБД не должно быть никаких других задач и ролей, кроме сервера СУБД.
  • На сервере 1С не устанавливать роль терминального сервера.
  • Все сервера должны находиться в одной подсети. Виртуализацию оборудования нужно по возможности избегать.
  • Файлы, от которых зависит максимальная производительность системы (а это файлы и логи БД, файлы tempdb для MS SQL или pg_xlog для PostgreSQL, файлы кластера 1С (srvinfo), папка временных файлов профиля пользователя службы 1С) необходимо размещать на PCIe SSD и/или NVMe.

Если есть повышенные требования к отказоустойчивости системы, надо подумать о резервировании узлов. Что и как резервировать – определяется напрямую требованиями и финансовыми возможностями бизнеса.

При выборе сервера для размещения конфигурации 1С уже не стоит выбор – физический сервер или виртуальный. Значимых отличий по скорости нет, а удобство и отказоустойчивость при использовании виртуальных машин очевидны даже ярым скептикам.

Однако выбор самой конфигурации сервера и необходимых выделяемых ресурсов часто бывает нетривиальной задачей так как. отсутствует официальная методика расчета нагрузки от компании разработчика «1С». Казалось бы, что сложного – выбираем объем усредненных ресурсов и умножаем на количества одновременно работающих пользователей. Но тут есть ряд моментов.

База может работать как в файловом режиме, где на скорость влияет в большей степени отклик дисковой подсистемы сервера, так и в клиент-серверном режиме, где также добавляется неизвестные в виде неоптимизированных запросов к СУБД, схемы размещения самого сервера предприятия 1С и тонких/толстых клиентов.

Также присутствует тонкости в виде взаимных блокировок при одновременной работе нескольких пользователей в одной базе, ограничений платформы 1С и тщательной настройки физических серверов для оптимальной производительности виртуальных машин.

На наш взгляд, наиболее оптимальном вариантом являться поднятие тестового сервера с примерными показателями такими как 2 Гб оперативной памяти в расчете на одного работающего пользователя, использование для размещения баз SSD дисков Enterprise уровня и выделение 2-4 виртуальных процессоров с высокой тактовой частотой. После чего производится сравнение скорости работы 1С в файловом и клиент-серверном варианте и при необходимости изменение выделяемых ресурсов.

Алексеев Михаил, CIO, группа компаний «УниКС», http://unics.online/

Конечно, можно строить систему самостоятельно. Но если нужно надёжное готовое решение, достаточно арендовать физический выделенный сервер для 1С в одном из защищенных дата-центров. В качестве примера рассмотрим конфигурации серверов 1С, предлагаемых компанией Selectel.

Результаты анализа <a href="https://proglib.io/w/4c221cb5" target="_blank">предложений Selectel</a>
Результаты анализа предложений Selectel
Всё больше компаний выбирают использование облачных решений вместо обычных серверов. Cloud-решения обычно обходятся дешевле, не нужно заниматься поддержкой «железа», к тому же можно оплачивать ежемесячную подписку вместо покупки серверов, что в том числе позволяет легко масштабироваться.

Однако в случае с корпоративными системами, содержащими конфиденциальную информацию, такими как CRM, ERP, учетными системами – в частности, 1С – компании всё же предпочитают использовать серверные решения как более безопасные и привычные.

Александр Смирнов, консультант по управлению проектами, http://ibtraining.ru/

Для нагрузки свыше 100 пользователей Selectel поможет выбрать конфигурацию под ваши требования. На соответствующей странице можно ввести сведения о требуемой системе (число пользователей, база данных, лицензии), и сайт автоматически рассчитает стоимость.

16
Июн
2020

Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://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);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

Выясним, какие данные необходимо
извлечь. Нас интересует бренд, название продукта, ссылки на продукт и его изображение, и, наконец, стоимость:

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        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 < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 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_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: 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
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

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');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        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());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

Мы рассмотрели всего лишь один конкретный случай
использования Puppeteer. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на
ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

16
Июн
2020

Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://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);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

Выясним, какие данные необходимо
извлечь. Нас интересует бренд, название продукта, ссылки на продукт и его изображение, и, наконец, стоимость:

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        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 < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 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_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: 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
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

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');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        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());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

Мы рассмотрели всего лишь один конкретный случай
использования Puppeteer. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на
ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

16
Июн
2020

🕵 Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://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);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

Выясним, какие данные необходимо
извлечь. Нас интересует бренд, название продукта, ссылки на продукт и его изображение, и, наконец, стоимость:

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        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 < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 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_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: 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
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

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');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        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());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

Мы рассмотрели всего лишь один конкретный случай
использования Puppeteer. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на
ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

16
Июн
2020

🕵 Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://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);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

Выясним, какие данные необходимо
извлечь. Нас интересует бренд, название продукта, ссылки на продукт и его изображение, и, наконец, стоимость:

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        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 < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 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_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: 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
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

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');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        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());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

Мы рассмотрели всего лишь один конкретный случай
использования Puppeteer. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на
ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

10
Июн
2020

29 июня – 7 августа, онлайн: летняя ИТ-школа и онлайн-марафон по разработке

Принимайте участие в одном из направлений Летней IT-школы КРОК или в команде Java и .NET разработчиков решайте реальные задачи в CROC DEV CHALLENGE.
— Читать дальше «Летняя ИТ-школа и онлайн-марафон по разработке от КРОК»

08
Июн
2020

Android Studio 4.0: новые инструменты работы с макетами, улучшенное профилирование и анализ билдов

Рассказываем, чем нас порадовала четвёртая версия Android Studio: редактор движения объектов, инспектор макетов, улучшенный CPU Profiler, интеллектуальный редактор и анализатор сборки.

08
Июн
2020

📱Android Studio 4.0: новые инструменты работы с макетами, улучшенное профилирование и анализ билдов

Рассказываем, чем нас порадовала четвёртая версия Android Studio: редактор движения объектов, инспектор макетов, улучшенный CPU Profiler, интеллектуальный редактор и анализатор сборки.

01
Май
2020

3 инструмента для Python, которые упростят работу с кодом

Разбор полезных инструментов для Python с функциями статической типизации, умного ИИ автодополнения и динамического анализа кода.
— Читать дальше «3 инструмента для Python, которые упростят работу с кодом»

28
Апр
2020

Встраиваем видеоролики YouTube в Android-приложение

Пошаговая инструкция, как настроить YouTube API, чтобы интегрировать воспроизведение видео в Android-приложение на примере Kotlin.

YouTube позволяет встраивать функции
воспроизведения видео в Android-приложения. API определяет методы загрузки
и воспроизведения видео и плейлистов, а также настройки и управления процессом
воспроизведения. С помощью API можно управлять воспроизведением программно. Например, искать нужную точку в загруженном видео.

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

Прежде чем перейти к коду, нужно выполнить настройку API YouTube в приложении.

Создаем новый проект

1. Создаем проект с Empty Activity


  1. Перейдём в Files;
  2. Выбираем New Project;
  3. Выбираем Project Template – Basic Activity.

2. Настройка библиотек YouTube и Manifest

Настроим клиентскую
библиотеку YouTube API для нашего проекта. Перейдем на страницу YouTube Android Player API и загрузим jar-файл YouTube Android Player.

Страница с загрузкой zip-папки
Страница с загрузкой zip-папки

Извлекаем jar-файл и добавляем в папку libs.

Папка libs с добавленным jar-файлом
Папка libs с добавленным jar-файлом

Интеграция YouTube
Android Player с помощью Kotlin

1. Переходим в
консоль разработчика Google

Переходим в консоль разработчика. Включаем YouTube Data API.


2. Переходим в
раздел “Help me choose”

Раскрытый список с необходимым разделом
Раскрытый список с необходимым разделом

3. Выбираем Credentials


В поле Name вводим имя пакета.


Создаём сертификат SHA-1, запускаем команду signingReport.



Копируем сгенерированный сертификат и помещаем его, как показано ниже.



Ниже представлены XML-файл и YouTubeActivity.Kt, которые можно использовать в качестве базовых примеров для работы с YouTube API в приложении на Kotlin.

AndroidManifest.xml
            


    
    
        
            
                

                
            
        
        
            
                

                
            
        
    

        
YouTubeActivity.Kt
            package com.example.youtubeplayer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.youtube.player.YouTubeBaseActivity
import com.google.android.youtube.player.YouTubeInitializationResult
import com.google.android.youtube.player.YouTubePlayer
import com.google.android.youtube.player.YouTubePlayerView
import com.google.android.youtube.player.internal.t
const val YOUTUBE_VIDEO_ID = "Evfe8GEn33w"
const val YOUTUBE_PLAYLIST = "UCU3jy5C8MB-JvSw_86SFV2w"

class YoutubeActivity : YouTubeBaseActivity(), YouTubePlayer.OnInitializedListener {
    private val TAG = "YoutubeActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val layout = layoutInflater.inflate(R.layout.activity_youtube, null) as ConstraintLayout
        setContentView(layout)

        val playerView = YouTubePlayerView(this)
        playerView.layoutParams = ConstraintLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        layout.addView(playerView)

        playerView.initialize(getString(R.string.GOOGLE_API_KEY), this)
    }

    override fun onInitializationSuccess(provider: YouTubePlayer.Provider?, youTubePlayer: YouTubePlayer?,
                                         wasRestored: Boolean) {
        Log.d(TAG, "onInitializationSuccess: provider is ${provider?.javaClass}")
        Log.d(TAG, "onInitializationSuccess: youTubePlayer is ${youTubePlayer?.javaClass}")
        Toast.makeText(this, "Initialized Youtube Player successfully", Toast.LENGTH_SHORT).show()

        youTubePlayer?.setPlayerStateChangeListener(playerStateChangeListener)
        youTubePlayer?.setPlaybackEventListener(playbackEventListener)

        if (!wasRestored) {
            youTubePlayer?.cueVideo(YOUTUBE_VIDEO_ID)
        }
    }

    override fun onInitializationFailure(provider: YouTubePlayer.Provider?,
                                         youTubeInitializationResult: YouTubeInitializationResult?) {
        val REQUEST_CODE = 0

        if (youTubeInitializationResult?.isUserRecoverableError == true) {
            youTubeInitializationResult.getErrorDialog(this, REQUEST_CODE).show()
        } else {
            val errorMessage = "There was an error initializing the YoutubePlayer ($youTubeInitializationResult)"
            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
        }
    }

    private val playbackEventListener = object: YouTubePlayer.PlaybackEventListener {
        override fun onSeekTo(p0: Int) {
        }

        override fun onBuffering(p0: Boolean) {
        }

        override fun onPlaying() {
            Toast.makeText(this@YoutubeActivity, "Good, video is playing ok", Toast.LENGTH_SHORT).show()
        }

        override fun onStopped() {
            Toast.makeText(this@YoutubeActivity, "Video has stopped", Toast.LENGTH_SHORT).show()
        }

        override fun onPaused() {
            Toast.makeText(this@YoutubeActivity, "Video has paused", Toast.LENGTH_SHORT).show()
        }
    }

    private val playerStateChangeListener = object: YouTubePlayer.PlayerStateChangeListener {
        override fun onAdStarted() {
            Toast.makeText(this@YoutubeActivity, "Click Ad now, make the video creator rich!", Toast.LENGTH_SHORT).show()
        }

        override fun onLoading() {
        }

        override fun onVideoStarted() {
            Toast.makeText(this@YoutubeActivity, "Video has started", Toast.LENGTH_SHORT).show()
        }

        override fun onLoaded(p0: String?) {
        }

        override fun onVideoEnded() {
            Toast.makeText(this@YoutubeActivity, "Congratulations! You've completed another video.", Toast.LENGTH_SHORT).show()
        }

        override fun onError(p0: YouTubePlayer.ErrorReason?) {
        }
    }
}
        

Теперь вы можете запустить код и посмотреть видео на
эмуляторе.

Результат запуска в эмуляторе
Результат запуска в эмуляторе

Код целиком доступен в GitHub-репозитории.

Другие материалы по теме

23
Мар
2020

10 примеров использования wget

Знакомимся на практике с wget – популярной утилитой командной строки *nix-систем для извлечения содержимого из интернета.

Что такое wget?

Wget – свободная утилита, предоставляющая неинтерактивный способ загрузки файлов из интернета
по HTTPS, HTTP, FTP и даже HTTP-прокси.

Вы можете запустить wget в фоновом режиме, а он доделает
всю назначенную работу. Обычно утилиту используют для получения всего веб-сайта,
перехода по ссылкам на страницах XHTML/HTML и создания локальной версии сайта.
Синтаксис wget выглядит так:

            wget [option] [URL]
        

1. Скачивание страницы

Попробуем загрузить
страницу github.com:

            wget github.com
        

Если все подключилось,
то wget загрузит домашнюю страницу и покажет результат, как показано ниже:

            wget github.com
URL transformed to HTTPS due to an HSTS policy
--2020-02-23 10:45:52--  https://github.com/
Resolving github.com (github.com)... 140.82.118.3
Connecting to github.com (github.com)|140.82.118.3|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: ‘index.html’

index.html                                       [ <=>                                                                                        ] 131.96K  --.-KB/s    in 0.04s

2020-02-23 10:45:52 (2.89 MB/s) - ‘index.html’ saved [135126]
        

2. Скачивание нескольких
файлов

Круто, если можно
загрузить сразу несколько файлов. Это, кстати, идея для написания какого-нибудь
скрипта для автоматизации загрузки файлов, подумайте на досуге.

Попробуем скачать
архивы с Python 3.8.1 и 3.5.1:

            wget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz https://www.python.org/ftp/python/3.5.1/Python-3.5.1.tgz
        

Как вы уже догадались,
синтаксис такой:

            wget URL1 URL2 URL3
        

Не забывайте про пробел
между ссылками!

3. Ограничение скорости
загрузки

Ограничить скорость загрузки полезно,
когда стоит задача проверить, сколько времени занимает загрузка файла на разной
пропускной способности.

Используя опцию --
limit-rate
, вы можете ограничить скорость загрузки.

Вот результат загрузки
файла Node.js:

            wget https://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.xz
--2020-02-23 10:59:58--  https://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.xz
Resolving nodejs.org (nodejs.org)... 104.20.23.46, 104.20.22.46, 2606:4700:10::6814:162e, ...
Connecting to nodejs.org (nodejs.org)|104.20.23.46|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 14591852 (14M) [application/x-xz]
Saving to: ‘node-v12.16.1-linux-x64.tar.xz’

node-v12.16.1-linux-x64.tar.xz               100%[===========================================================================================>]  13.92M  --.-KB/s    in 0.05s

2020-02-23 10:59:58 (272 MB/s) - ‘node-v12.16.1-linux-x64.tar.xz’ saved [14591852/14591852]
        

Потребовалось 0.05 с, чтобы скачать 13.92 Мб. Теперь попробуем ограничить скорость до 500 Кб/с:

            wget --limit-rate=500k https://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.xz
--2020-02-23 11:00:18--  https://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.xz
Resolving nodejs.org (nodejs.org)... 104.20.23.46, 104.20.22.46, 2606:4700:10::6814:162e, ...
Connecting to nodejs.org (nodejs.org)|104.20.23.46|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 14591852 (14M) [application/x-xz]
Saving to: ‘node-v12.16.1-linux-x64.tar.xz.1’

node-v12.16.1-linux-x64.tar.xz.1             100%[===========================================================================================>]  13.92M   501KB/s    in 28s

2020-02-23 11:00:46 (500 KB/s) - ‘node-v12.16.1-linux-x64.tar.xz.1’ saved [14591852/14591852]
        

Скорость загрузки упала
до 28 с. Представьте себе ситуацию – юзеры жалуются на медленную загрузку.
Вы можете быстро поиграться с limit-rate для имитации проблемы.

4. Загрузка в фоне

Загрузка больших файлов
может занять некоторое время, но что если вы не хотите смотреть в терминал?

Можно использовать ключ
-b, чтобы запустить wget в фоновом режиме:

            wget -b https://slack.com
Continuing in background, pid 25430.
Output will be written to ‘wget-log.1’.
        

5. Игнорирование ошибки
сертификата

Игнорирование ошибки сертификата удобно, когда вам
нужно обратиться к узлу во внешней сети без соответствующего сертификата. Если сертификат не является валидным, по
умолчанию wget выдаст ошибку.

            wget https://expired.badssl.com/
--2020-02-23 11:24:59--  https://expired.badssl.com/
Resolving expired.badssl.com (expired.badssl.com)... 104.154.89.105
Connecting to expired.badssl.com (expired.badssl.com)|104.154.89.105|:443... connected.
ERROR: cannot verify expired.badssl.com's certificate, issued by ‘CN=COMODO RSA Domain Validation Secure Server CA,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB’:
  Issued certificate has expired.
To connect to expired.badssl.com insecurely, use `--no-check-certificate'.

        

На примере выше результат истекшего срока действия сертификата. Предлагается использовать
--no-check-certificate для игнорирования любой проверки сертификата. Сравним результат с добавленным ключом:

            wget https://untrusted-root.badssl.com/ --no-check-certificate
--2020-02-23 11:33:45--  https://untrusted-root.badssl.com/
Resolving untrusted-root.badssl.com (untrusted-root.badssl.com)... 104.154.89.105
Connecting to untrusted-root.badssl.com (untrusted-root.badssl.com)|104.154.89.105|:443... connected.
WARNING: cannot verify untrusted-root.badssl.com's certificate, issued by ‘CN=BadSSL Untrusted Root Certificate Authority,O=BadSSL,L=San Francisco,ST=California,C=US’:
  Self-signed certificate encountered.
HTTP request sent, awaiting response... 200 OK
Length: 600 [text/html]
Saving to: ‘index.html.6’

index.html.6   100%[===========================================================================================>]     600  --.-KB/s    in 0s

2020-02-23 11:33:45 (122 MB/s) - ‘index.html.6’ saved [600/600]
        

6. Заголовок HTTP ответа

С помощью ключа -S в
терминал будет выведен заголовок, как это произошло с Яндексом:

             wget -S --no-check-certificate https://www.yandex.ru
--2020-03-20 02:52:16--  https://www.yandex.ru/
Resolving www.yandex.ru (www.yandex.ru)... 149.5.244.14, 80.239.201.15
Connecting to www.yandex.ru (www.yandex.ru)|149.5.244.14|:443... connected.
HTTP request sent, awaiting response...
   HTTP/1.1 200 Ok
  Accept-CH: Viewport-Width, DPR, Device-Memory, RTT, Downlink, ECT
  Accept-CH-Lifetime: 31536000
  Cache-Control: no-cache,no-store,max-age=0,must-revalidate
  Content-Length: 199540 
  Content-Type: text/html; charset=UTF-8
  Date: Thu, 19 Mar 2020 23:52:49 GMT
  Expires: Thu, 19 Mar 2020 23:52:50 GMT
  Last-Modified: Thu, 19 Mar 2020 23:52:50 GMT
  P3P: policyref="/w3c/p3p.xml", CP="NON DSP ADM DEV PSD IVDo OUR IND STP PHY PR                                                     E NAV UNI"
  Set-Cookie: yp=1587253970.ygu.1; Expires=Sun, 17-Mar-2030 23:52:49 GMT; Domain                                                     =.yandex.ru; Path=/
  Set-Cookie: mda=0; Expires=Fri, 17-Jul-2020 23:52:49 GMT; Domain=.yandex.ru; P                                                     ath=/
  Set-Cookie: yandex_gid=213; Expires=Sat, 18-Apr-2020 23:52:49 GMT; Domain=.yan                                                     dex.ru; Path=/
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  X-Yandex-Sdch-Disable: 1

        

7. Манипуляции с
User-Agent

Бывает так, что мы хотим открыть сайт с помощью другого user-agent. Это можно сделать, указав ключ --user-agent:

            wget https://gf.dev --user-agent="MyCustomUserAgent"
        

8. Заголовок хоста

Когда приложение находится
в разработке и есть необходимость что-то потестить, но нет подходящего
URL-адреса или мы хотим протестировать домен с использованием IP-адреса – в обеих ситуациях будет полезен --header.

Для примера возьмем
http://10.10.10.1 и application.com в качестве заголовка:

            wget --header="Host: application.com" http://10.10.10.1
        

9. Подключение через
прокси

Если вы работаете в
среде DMZ, вас может не выпускать в интернет. Воспользуйтесь преимуществами
прокси для подключения:

            wget -e use_proxy=yes http_proxy=$PROXYHOST:PORT http://externalsite.com
        

Не забудьте в
переменной $PROXYHOST:PORT указать реальные значения.

10. Подключение через TLS

Обычно рекомендуют
использовать OpenSSL для тестирования протокола TLS. Но можете использовать и
wget.

            wget --secure-protocol=TLSv1_2 https://example.com
        

Вышеизложенное заставит
wget подключиться через TLS 1.2.

Заключение

Если вам интересно подробнее разобраться в утилите, имеется перевод документации wget на русский. В некоторых случаях хорошей альтернативой wget является cURL, а для создания зеркал сайтов (чего cURL не умеет) обычно используется rsync.

Еще вам могут быть интересны следующие публикации: