Author: eFusion

07
Авг
2020

🌐 Концепция IP-адресов на примере Python-модуля ipaddress

Рассказываем, как работать с IP-адресами классического протокола IPv4 в теории и на практике – в коде Python. Показываем, как проверить связь между группой IP-адресов и их соответствие подсетям и диапазонам частных IP-адресов.

Вводные сведения об IP-адресах

В самом грубом представлении IP-адрес – это просто число. В случае протокола IPv4 IP-адрес– это 32-разрядное целое число, используемое для представления
хоста в сети. То есть существует 232
возможных IPv4 адреса – от 0 до 4 294 967 295. IPv4-адреса записывают в виде четырех октетов – целых чисел, разделенных точками:

        220.14.9.37
    

Каждый октет – это один байт, число от 0 до 255. То есть максимальный адрес равен
255.255.255.255, а минимальный – 0.0.0.0.

Далее мы рассмотрим, как
модуль ipaddress выполняет преобразования адреса так, что нам не приходится отвлекаться на строение адреса.

Примечание
Модуль ipaddress добавлен в стандартную библиотеку в версии Python 3.3, примеры туториала проверены на примере Python 3.8. Пользователь matyushkin любезно адаптировал программный код статьи в виде конспективного Jupyter-блокнота. За счёт этого с кодом можно поиграть в среде Colab.

Модуль ipaddress

Получим внешний IP-адрес нашего
компьютера для работы с ним в командной строке. В Linux это делается так:

        $ curl -sS ifconfig.me/ip
220.14.9.37
    

Этот запрос узнает наш
IP-шник на сайте ifconfig.me. Сайт также выдает множество другой полезной информации о вашем сетевом подключении.

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

Теперь откроем интерпретатор Python. Чтобы создать объект Python с инкапсулированным адресом, создадим класс IPv4Address:

        >>> from ipaddress import IPv4Address
>>> addr = IPv4Address("220.14.9.37")
>>> addr
IPv4Address('220.14.9.37')
    

Передача строки "220.14.9.37"
в конструктор IPv4Address – наиболее распространенный подход, но класс может
принимать и другие типы:

        >>> IPv4Address(3691907365)   # Из целого числа
IPv4Address('220.14.9.37')

>>> IPv4Address(b"\xdc\x0e\t%")  # Из байтовой строки
IPv4Address('220.14.9.37')
    

Адрес можно распаковать в требуемую форму:

        >>> int(addr)
3691907365
>>> addr.packed
b'\xdc\x0e\t%'
    

Экземпляры IPv4Address являются хэшируемыми и могут использоваться в качестве
ключей словаря:

        >>> hash(IPv4Address("220.14.9.37"))
4035855712965130587

>>> num_connections = {
...     IPv4Address("220.14.9.37"): 2,
...     IPv4Address("100.201.0.4"): 16,
...     IPv4Address("8.240.12.2"): 4,
... }
    

Класс IPv4Address также реализует
методы, позволяющие проводить сравнения:

        >>> IPv4Address("220.14.9.37") > IPv4Address("8.240.12.2")
True

>>> addrs = (
...     IPv4Address("220.14.9.37"),
...     IPv4Address("8.240.12.2"),
...     IPv4Address("100.201.0.4"),
... )
>>> for a in sorted(addrs):
...     print(a)
...
8.240.12.2
100.201.0.4
220.14.9.37
    

Можно использовать
любой стандартный оператор сравнения целочисленных значений
адресных объектов.

IP-сети и интерфейсы

Сеть – это набор
IP-адресов. Сети описываются и отображаются как непрерывные диапазоны адресов.
Например, сеть может соответствовать диапазону 192.4.2.0192.4.2.255, т. е.
включать 256 адресов. Если нужно это отобразить в краткой форме, используется нотация CIDR.

В CIDR сеть
определяется с помощью сетевого адреса и префикса
<network_address>/<prefix>:

        >>> from ipaddress import IPv4Network
>>> net = IPv4Network("192.4.2.0/24")
>>> net.num_addresses
256

# Вывести префикс можно с помощью свойства prefixlen:
>>> net.prefixlen
24
    

В данном случае префикс равен 24. Префикс – это количество ведущих битов, соответствующих входящим в сеть адресам. Ведущие биты отсчитываются слева направо.

Пример: входит ли
адрес 192.4.2.12 в сеть 192.4.2.0/24?

Ответ: да, так как ведущие 24 бита адреса 192.4.2.12 – это первые три октета: 192.4.2. Последний октет соответствует последним 8 битам 32-битного IP-адреса.

Воспользуемся netmask для маскирования
битов
в сравниваемых
адресах.

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

        >>> net.netmask
IPv4Address('255.255.255.0')
    

На рисунке ниже показано, как сравниваются ведущие
биты, чтобы определить, является ли адрес частью сети.

Побитовое сравнение
Побитовое сравнение

Последние 8 бит в
192.4.2.12 маскируются нулем и игнорируются при сравнении.

        >>> IPv4Address("192.4.2.12") in net
True
>>> IPv4Address("192.4.20.2") in net
False
    

Рассмотрим еще один
важный тип адреса – широковещательный.

Определение
Широковещательный адрес – условный (не присвоенный никакому устройству в сети) адрес, который используется для передачи широковещательных пакетов в компьютерных сетях.

Это единственный адрес, который может использоваться
для связи со всеми хостами сети:

        >>> net.network_address
IPv4Address('192.4.2.0')
    

Чаще всего вы будете сталкиваться
с длиной префикса кратной 8.

Распространенные подсети
Распространенные подсети

Любое целое число от 0
до 32 является допустимым, но такой вариант встречается реже:

        >>> net = IPv4Network("100.64.0.0/10")
>>> net.num_addresses
4194304
>>> net.netmask
IPv4Address('255.192.0.0')
    

Перебор IP-адресов в цикле

Класс IPv4Network позволяет
перебирать отдельные адреса в цикле for:

        >>> net = IPv4Network("192.4.2.0/28")
>>> for addr in net:
...     print(addr)
...
192.4.2.0
192.4.2.1
192.4.2.2
...
192.4.2.13
192.4.2.14
192.4.2.15
    

Инструмент
net.hosts() возвращает генератор, выдающий адреса, исключая сетевые и
широковещательные:

        >>> h = net.hosts()
>>> type(h)
<class 'generator'>
>>> next(h)
IPv4Address('192.4.2.1')
>>> next(h)
IPv4Address('192.4.2.2')
    

Подсети IP-адресов

Подсеть – это часть IP-сети:

        >>> small_net = IPv4Network("192.0.2.0/28")
>>> big_net = IPv4Network("192.0.0.0/16")
>>> small_net.subnet_of(big_net)
True
>>> big_net.supernet_of(small_net)
True
    

В коде выше small_net
содержит 16 адресов, а big_net – 65 536.

Распространенный способ
разбиения на подсети – это увеличение длины префикса на 1:

Разбиение сети
Разбиение сети

К счастью, IPv4Network расчеты подсетей поддерживаются встроенным методом subnets():

        >>> for sn in net.subnets():
...     print(sn)
...
200.100.10.0/25
200.100.10.128/25
    

В передаваемом subnets() аргументе можно задать, каким должен быть новый префикс:

        >>> for sn in net.subnets(new_prefix=28):
...     print(sn)
...
200.100.10.0/28
200.100.10.16/28
200.100.10.32/28
...
200.100.10.208/28
200.100.10.224/28
200.100.10.240/28
    

Специальные диапазоны IP-адресов

Администрация адресного пространства Интернет (Internet Assigned
Numbers Authority
, IANA) совместно с Инженерном советом Интернета (Internet
Engineering Task Force
, IETF) осуществляют
надзор за распределением диапазонов адресов. Реестр подобных адресов –
важная таблица, которая описывает, для каких целей зарезервированы диапазоны IPv4-адресов.

К примеру, это частные IP-адреса, используемые для внутренней связи между
устройствами в сети, не требующей подключения к интернету:

Зарезервированные диапазоны
Зарезервированные диапазоны

Случайным образом выберем
адрес – 10.243.156.214. Относится ли этот адрес к приватным? Для этого проверим, попадает ли он в диапазон cети 10.0.0.0/8:

        >>> IPv4Address("10.243.156.214") in IPv4Network("10.0.0.0/8")
True
    

Другой специальный тип
адреса – это локальный адрес связи, состоящий из блока 169.254.0.0/16. Примером
может служить
Amazon Time Sync Service, доступный для инстансов AWS EC2 по адресу
169.254.169.123. Данный пул также использует
Windows для
выдачи адресов сетевым адаптерам при отсутствии интернета от провайдера.

        >>> timesync_addr = IPv4Address("169.254.169.123")
>>> timesync_addr.is_link_local
True
    

Модуль ipaddress
предоставляет
набор свойств
для проверки того, относится ли адрес к специальным:

        >>> IPv4Address("10.243.156.214").is_private
True
>>> IPv4Address("127.0.0.1").is_loopback
True

>>> [i for i in dir(IPv4Address) if i.startswith("is_")]
['is_global',
 'is_link_local',
 'is_loopback',
 'is_multicast',
 'is_private',
 'is_reserved',
 'is_unspecified']
    

Вот еще несколько
зарезервированных сетей:

Что происходит внутри ipaddress

В дополнение к хорошо документированному
API, исходный
код CPython
и класс
IPv4Address
показывают некоторые отличные идеи, как улучшить собственный код.

Компоновщик

Модуль ipaddress
использует преимущества шаблона проектирования «Компоновщик». Класс IPv4Address представляет собой компоновщик, который оборачивает обычное целое число.

Каждый экземпляр
IPv4Address имеет атрибут _ip, число типа int. Многие свойства и методы класса определяются
значением этого атрибута:

        >>> addr = IPv4Address("220.14.9.37")
>>> addr._ip
3691907365
    

Атрибут _ip отвечает
за создание int(addr). Цепочка вызовов выглядит следующим образом:

Цепочка вызовов в компоновщике
Цепочка вызовов в компоновщике

Продемонстрируем силу ._ip путем расширения класса IPv4Address:

        from ipaddress import IPv4Address

class MyIPv4(IPv4Address):
    def __and__(self, other: IPv4Address):
        if not isinstance(other, (int, IPv4Address)):
            raise NotImplementedError
        return self.__class__(int(self) & int(other))
    

Добавление .__and__()
позволяет использовать бинарный оператор &, чтобы применять маску к
IP-адресу:

        >>> addr = MyIPv4("100.127.40.32")
>>> mask = MyIPv4("255.192.0.0")  # Соответствует префиксу /10

>>> addr & mask
MyIPv4('100.64.0.0')

>>> addr & 0xffc00000  # hex-литерал для 255.192.0.0
MyIPv4('100.64.0.0')
    

Метод __and__()
позволяет использовать либо другой IPv4Address, либо непосредственно
int в
качестве маски. Поскольку MyIPv4 является подклассом IPv4Address, проверка
isinstance() в данном случае вернет
True.

Помимо перегрузки
оператора, есть возможность добавить новые свойства:

        import re
from ipaddress import IPv4Address

class MyIPv4(IPv4Address):
    @property
    def binary_repr(self, sep=".") -> str:
        """Представляет IPv4 в виде 4 блоков по 8 бит."""
        return sep.join(f"{i:08b}" for i in self.packed)  # 8 строка

    @classmethod
    def from_binary_repr(cls, binary_repr: str):
        """Создает IPv4 из двоичного представления."""
        i = int(re.sub(r"[^01]", "", binary_repr), 2)  # 14 строка
        return cls(i)
    

В методе binary_repr (строка 8), используется .packed для преобразования IP-адреса в массив байтов, который
затем форматируется, как строковое представление бинарной формы.

В from_binary_repr, вызов int(re.sub(r"[^01]", "", binary_repr), 2) (строка 14) состоит
из двух частей:

  • удаление из входящей строки всего, кроме нулей и единиц;
  • анализ результата с помощью int(<string>, 2).

Методы binary_repr() и from_binary_repr() позволяют проводить двустороннюю конвертацию:

        >>> MyIPv4("220.14.9.37").binary_repr
'11011100.00001110.00001001.00100101'
>>> MyIPv4("255.255.0.0").binary_repr  # Маска для префикса /16 
'11111111.11111111.00000000.00000000'

>>> MyIPv4.from_binary_repr("11011100 00001110 00001001 00100101")
MyIPv4('220.14.9.37')
    

Таким образом, мы разобрали несколько способов
использования преимуществ шаблона IP-as-integer, который может помочь расширить
функциональность IPv4Address с небольшим количеством дополнительного кода.

Заключение

Если вам нравится язык Python и вы хотите детально овладеть стандартной библиотекой, у нас есть множество родственных публикаций:

03
Авг
2020

⚔ Vue vs React vs Angular: какой фронтенд-фреймворк выбрать?

Существует множество фронтенд-фреймворков. В этой статье мы рассмотрим три самых популярных: Angular, React и Vue. В том числе расскажем о преимуществах и недостатках их использования для различных веб-проектов.

27
Июл
2020

🔌 Чем занимается DevOps-инженер? Обзор базовых практик

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

Традиционная IT-команда крупного проекта состоит
из трех подгрупп:

  1. Разработчики (Dev),
  2. Тестировщики (QA),
  3. Группа эксплуатации (оперативная группа, Ops).

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

Иллюстрация, показывающая представление DevOps как пересечения разработки, эксплуатации и тестирования
Иллюстрация, показывающая представление DevOps как пересечения разработки, эксплуатации и тестирования

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

Зачем нужны DevOps-инженеры?

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


В методологии DevOps
используются различные инструменты:

  • контейнеризация (Docker, Kubernetes)
  • непрерывная интеграция (Jenkins)
  • развертывание сред по шаблону (Terraform, Puppet, Ansible)
  • мониторинг служб и сетей (Nagios, Grafana, Prometheus, Splunk)

Но одних инструментов мало, нужен набор практик.

Непрерывная интеграция (CI)


Непрерывная интеграция (Continuous Integration, CI) – это практика
разработки ПО, которая заключается в постоянном слиянии рабочих копий в общую основную ветвь разработки и выполнении автоматизированных сборок проекта для скорейшего выявления потенциальных дефектов и решения интеграционных проблем.

В традиционных проектах стадия интеграции является заключительной, что приводит к задержке окончания работ. Переход к непрерывной интеграции позволяет снизить трудоёмкость интеграции и сделать её более предсказуемой за счёт раннего обнаружения и устранения ошибок и противоречий. Но главным преимуществом является сокращение стоимости исправления дефекта за счёт его раннего выявления. Решаем проблемы, «не отходя от кассы».

Непрерывное развертывание и доставка


Процесс развертывания
цикличен: разработка → сборка → проверка → управление
версиями сборки → деплой и т. д. Идея непрерывного процесса развертывания
заключается в автоматическом развертывании созданного кода в производственную
среду после того, как сборка пройдет все этапы QA-staging-beta, интеграцию, пользовательское
тестирование и т. д.

Такие инструменты, как Spinnaker, Jenkins, Harness,
Ansible, Chef, Puppet, позволяют команде DevOps настраивать
автоматизированные конвейеры для деплоя в нескольких средах с минимальным
вмешательством человека.

Непрерывная доставка (continuous delivery) – это
практика DevOps, при которой новый код тестируется QA-командой на различных
этапах автоматизированных и ручных циклов QA. Если код проходит цикл QA и это
одобрено командой, всё деплоится в продакшен. Именно так с помощью DevOps команда
может создавать, тестировать и выпускать релизы быстрее и чаще, разделяя весь
процесс на короткие циклы. Это позволяет организациям выпускать больше релизов,
сокращать ручное развертывание и минимизировать риск сбоев.

Конфигурационное управление (CM)

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

Менеджмент конфигураций
дает ряд преимуществ: упрощение настройки новой среды, снижение рисков в
продакшене и экономия времени на разработку ПО вместо
инвестирования усилий в инициирование новых служб с использованием
практики «инфраструктура как код».

Управление изменениями

Управление изменениями
это процесс запроса, планирования, внедрения и оценки изменений, необходимых
для удовлетворения новых требований. Если в процессе работы в существующей
системе или инфраструктуре появляются/требуются какие-либо изменения – вот тут
и «включается» в игру управление изменениями. Оперативные группы должны следить
и представлять свои отчеты с причинами и последствиями изменений, которые могут
возникнуть на более широком уровне, включая другие системы, на которые могут повлиять новые
изменения.

Держитесь вместе

DevOps работает с несколькими отделами. Очень важны коммуникативные навыки: чтобы избежать конфликтов,
несогласованности и недопонимания в командах, используйте общий
ресурс внутренней связи. Активное вовлекайте в обсуждения членов
всех команд.

Автоматизация тестирования


Автоматизированное
тестирование
помогает выполнять больше тестов, увеличить частоту тестирования и
экономить время, затрачиваемое на ручное QA. Этот процесс обеспечивает раннее
обнаружение и исправление ошибок, повышает общее качество софта.
Существует несколько доступных инструментов, легко
интегрируемых с инструментами DevOps: Selenium, Robot Framework, Appium,
XCUITest, JUnit и т. д.

Непрерывный мониторинг (CM)

Непрерывный мониторинг
предполагает мониторинг и отображение отчетов в подробном графическом формате всех
систем и инфраструктуры с помощью нескольких инструментов:

  • дашборды и оповещения;
  • анализ в реальном времени метрик, влияющих на ПО: производительность системы, количество тестов, частота успехов/отказов;
  • состояние развертывания;
  • журналы ошибок.

Сравнение соответствующих инструментов мы сделали в публикации «10 лучших инструментов облачного мониторинга».

Заключение

DevOps – перспективное направление, которое продолжает активно развиваться. Универсальность DevOps специалистов высоко ценится, а постоянное активное обучение, новые практики и подходы не дадут вам заскучать на рабочем месте.

***

Данный материал подготовлен при поддержке компании GeekBrains – нашего партнёра, предоставляющего помощь в освоении DevOps-технологий. Если вы хотите освоить DevOps, не тратя лишнее время и силы на поиск знаний, инструментов и привыкание к разному стилю чтения курсов, обратите внимание на факультет DevOps. Программа и преподаватели имеют высокие оценки учащихся, а при успешном прохождении курса онлайн-университет гарантирует не только диплом, но и трудоустройство.

24
Июл
2020

⌨️ Инструкция: как создать форму в React

Показываем на примерах JS-кода, как создать форму с помощью контролируемых или неконтролируемых компонентов React и обработать состояние с помощью React хуков.

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

Вот пример простой HTML-формы:

        <form>  
  <label for="name">Name:</label><br>  
  <input type="text" id="name" name="name" value="John"><br>  
  <input type="submit" value="Submit">  
</form>
    

Давайте рассмотрим, как создаются формы в React.

Как создать форму в React с помощью контролируемых компонентов

Мы знаем, что такие HTML-элементы, как input, запоминают то, что мы вводим. Таким же образом можно использовать состояние компонента React для хранения данных элементов формы. Когда
данные обрабатываются компонентом React, он называется
контролируемым, а функционирование в нем завязано на состоянии компонента, а не
на элементе DOM.

Давайте реализуем форму с помощью контролируемых компонентов, используя следующие элементы HTML: input, radio button, select dropdown.

Создадим компонент с
тегом формы и один input:

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input type="text" placeholder="enter student name" />  
        </form>  
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent; 
    

Выглядеть это будет
так:

Контролируемый компонент формы
Контролируемый компонент формы

Создадим состояние в компоненте и повесим событие onChange на input, чтобы изменить
состояние компонента при изменении значения input.

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  
  constructor(props) {  
    super(props);  
    this.state = {  
      studentName: ""  
    };  
    this.onNameChangeHandler = this.onNameChangeHandler.bind(this);  
  }  
  
  onNameChangeHandler(e){  
    this.setState({  
      studentName: e.target.value  
    })  
  }  
  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input   
              type="text"   
              placeholder="enter student name"  
              onChange={this.onNameChangeHandler} />  
        </form>   
        <hr/>  
        <p>State of Component</p>        
          <pre>{JSON.stringify(this.state, null, 2) }</pre>       
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent;  
    

Мы создали состояние и
добавили studentName в качестве свойства. После этого добавили обработчик
onChange, который изменит состояние и забиндит его с событием onChange у input.

Таким же образом можно
добавить еще один HTML-элемент и привязать его к состоянию компонента.

Добавим на нашу форму radio button и два новых свойства: пол и штат.

        <label>Gender: </label>  
          <label>Male</label>  
          <input  
            type="radio"  
            name="gender"  
            value="male"  
            checked={this.state.gender === "male"}  
            onChange={this.onChangeHandler}  
          />  
          <label>Female</label>  
          <input  
            type="radio"  
            name="gender"  
            value="female"  
            checked={this.state.gender === "female"}  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>State: </label>  
          <select  
            name="state"  
            value={this.state.state}  
            onChange={this.onChangeHandler}  
          >  
            <option value="Maharashtra">Maharashtra</option>  
            <option value="Madhya Pradesh">Madhya Pradesh</option>  
            <option value="Karnataka">Karnataka</option>  
            <option value="West Bengal">West Bengal</option>  
          </select>  
    

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

        onChangeHandler(e){  
    this.setState({  
      [e.target.name]: e.target.value  
    })  
  } 
    

Вот так выглядит
готовый компонент:

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      studentName: "",  
      gender: "",  
      state: "Maharashtra"  
    };  
    this.onChangeHandler = this.onChangeHandler.bind(this);  
  }  
  onChangeHandler(e) {  
    this.setState({  
      [e.target.name]: e.target.value  
    });  
  }  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input  
            type="text"  
            name="studentName"  
            placeholder="enter student name"  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>Gender: </label>  
          <label>Male</label>  
          <input  
            type="radio"  
            name="gender"  
            value="male"  
            checked={this.state.gender === "male"}  
            onChange={this.onChangeHandler}  
          />  
          <label>Female</label>  
          <input  
            type="radio"  
            name="gender"  
            value="female"  
            checked={this.state.gender === "female"}  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>State: </label>  
          <select  
            name="state"  
            value={this.state.state}  
            onChange={this.onChangeHandler}  
          >  
            <option value="Maharashtra">Maharashtra</option>  
            <option value="Madhya Pradesh">Madhya Pradesh</option>  
            <option value="Karnataka">Karnataka</option>  
            <option value="West Bengal">West Bengal</option>  
          </select>  
        </form>  
        <br />  
        <hr />  
        <p>State of Component</p>  
        <pre>{JSON.stringify(this.state, null, 2)}</pre>  
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent;  
    
Компонент в действии
Компонент в действии

Использование хуков React

Состояние формы можно
обрабатывать и с помощью хуков. Чтобы хранить состояние в компоненте, существует useState(). Создадим простую форму
с одним инпутом и обработаем ее с помощью хука:

        import React, { useState } from 'react';  
  
export default function ControlledFormWithHook() {  
  const [name, setName] = useState('');  
  
  return (  
    <div>  
      <form>  
        <label>Name:</label>  
        <input type="text" onChange={(e) => setName(e.target.value)} />  
      </form>  
      <br />  
      Name is: {name}  
    </div>  
  );  
}  
    
Форма с использованием хуков
Форма с использованием хуков

Как создать форму в React с помощью неконтролируемых компонентов

Мы знаем, что
HTML-элементы сохраняют собственное состояние и обновляют его при изменении
входного значения. То есть мы можем получить прямой доступ к значению
HTML-элемента без сохранения состояния компонента. Когда данные обрабатываются
элементами DOM, это можно назвать неконтролируемым компонентом. React позволяет напрямую получить ссылку на элемент DOM и хранить в нем состояние компонента.

В некоторых случаях мы
должны использовать неконтролируемый компонент, например, когда хотим добавить выбор файла: <input type="file"/>.

Возьмем тот
же пример и немного
переделаем с помощью React.createRef() API.

        import React, { Component } from "react";  
  
export default function UncontrolledFormComponent() {  
  let inputRef = React.createRef();  
  let name = "";  
  const handleClick = e => {  
    e.preventDefault();  
    alert("Name is: " + inputRef.current.value);  
  };  
  
  return (  
    <div>  
      <h3>Uncontrolled Form Component</h3>  
      <form>  
        <input type="text" ref={inputRef} />  
        <button style={{ margin: "8px" }} onClick={handleClick}>  
          Submit  
        </button>  
      </form>  
    </div>  
  );  
} 
    

Когда вы нажимаете на Submit, открывается alert со значением, которое было
введено в текстовом поле.

Работа с неконтролируемым компонентом
Работа с неконтролируемым компонентом
***

Итак, мы рассмотрели
формы, а также обсудили различные способы их обработки в React. Если вас привлекает веб-разработка, мы советуем пройти курс факультета Веб-разработки GeekBrains и получить готовую базу навыков.

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

20
Июл
2020

🐳 Виды контейнеров: когда какой использовать

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

Системный контейнер – один из старых примеров технологии контейнеров. Системный контейнер похож
на виртуальную машину: идеален для хранения операционной системы и запуска нескольких процессов. Системные контейнеры используются для
традиционных и монолитных приложений – в одном контейнере архитектура, инструменты и конфигурации.

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

Схема: контейнер приложения (слева) и контейнер системы
Схема: контейнер приложения (слева) и контейнер системы

Говоря о контейнерах, часто имеют в виду технологию Docker. Большинство облачных вендоров предлагают контейнеры приложений
с Docker-ом внутри каждой виртуалки. Такая машина включает гостевую операционную систему с
памятью, процессором и дисковым пространством. Обычно Docker работает внутри системных контейнеров в пределах одного
ядра и совместно использует ресурсы хост-системы. Хотя эти вложенные
контейнеры являются более легкими, чем виртуальные машины, они изолированы и безопасны.

Сравнение системного контейнера и виртуальной машины. Как видите, они во многом похожи
Сравнение системного контейнера и виртуальной машины. Как видите, они во многом похожи

Ещё один вариант – использование системных контейнеров Virtuozzo и контейнеров
приложений Docker во вложенной архитектуре. Внутри платформы различные типы
контейнеров могут использоваться для разных целей:

  • сертифицированные управляемые контейнеры;
  • elastic VPS;
  • кастомные Docker-контейнеры;
  • Docker Engine CE;
  • кластер Kubernetes.

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

Сертифицированные управляемые контейнеры

Самый распространенный
вариант. Обычно предлагается несколько предварительно сконфигурированных и
управляемых программных стеков, позволяющих создавать гибкие топологии с
необходимым софтом (Java, PHP, Node.js, Ruby, Python или Go), балансировщик
нагрузки, база данных и прочее.

Панель управляемого контейнера
Панель управляемого контейнера

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

Взаимодействие оркестратора с образами
Взаимодействие оркестратора с образами

Данные контейнеры выгоднее
и удобнее таких решений, как автоматизированная конфигурация с масштабированием
ресурсов, автоматизированная установка SSL-сертификатов, авторазвертывание
приложений, управляемая доставка и т. д.

Виртуальные частные серверы (VPS)

Наиболее простым
примером реализации системного контейнера является VPS-контейнер с
предустановленными ОС: CentOS, Ubuntu и Debian. Это «
чистый» контейнер без
каких-либо дополнительных настроек или установленного ПО. Его можно
рассматривать как вариант контейнеризации устаревших приложений – почти не требует настройки при миграции из виртуальных машин.

Настройка под Elastic VPS образа с CentOS 7.7
Настройка под Elastic VPS образа с CentOS 7.7

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

Пользовательские Docker-контейнеры

Это образ Docker,
развернутый внутри системного контейнера, что делает его совместимым с
большинством (но не со всеми) отличительными чертами платформы, например, встроенным вертикальным и горизонтальным масштабированием. Другими словами,
файловая система такого образа распаковывается внутри среды выполнения
системного контейнера.

Работа оркестратора с кастомными образами
Работа оркестратора с кастомными образами

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

Настройка пользовательского контейнера
Настройка пользовательского контейнера

Docker Engine CE

Docker Engine Community
Edition работает внутри системных контейнеров, но в то же время имеет полную
совместимость с нативной экосистемой Docker.

Взаимодействие оркерстратора с образами Docker Engine
Взаимодействие оркерстратора с образами Docker Engine

Такая интеграция
позволяет работать с основными инструментами контейнерной технологии Docker:

  • Docker Engine – работает с Dockerfile и запускает pre-built образы контейнеров.
  • Docker Registry – хранит и предоставляет доступ к многочисленным общедоступным и частным образам, предназначенным для развертывания в Docker Engine.
  • Docker Compose – помогает собирать приложения, состоящие из нескольких компонентов, где все необходимые конфигурации объявляются в одном compose-файле.
  • Docker Swarm – представляет собой несколько независимых Docker узлов, объединенных в кластер.
Настройка окружений для Docker Engine CE и Docker Swarm Cluster
Настройка окружений для Docker Engine CE и Docker Swarm Cluster

Kubernetes

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


Иначе говоря, Kubernetes
обеспечивает максимальную совместимость всех проектов. Основным преимуществом его
использования является усовершенствованная модель масштабирования и
запуска контейнеров, что в разы облегчает управление и мониторинг, делает все
это «предприятие» экономически выгодным.

Заключение

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

***

Данный материал подготовлен при поддержке компании GeekBrains — нашего партнёра, предоставляющего помощь в освоении DevOps-технологий. Если вы хотите освоить DevOps, не тратя лишнее время и силы на поиск знаний, инструментов и привыкание к разному стилю чтения курсов, обратите внимание на факультет DevOps.

В рамках курса данного факультета вы освоите технологии Docker, Kubernetes, работу с виртуальными машинами, облачные технологии и микросервисную архитектуру.

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

09
Июл
2020

📝 «Помедленнее, я записываю»: туториал по системным логам Linux

Из этой статьи вы узнаете, что такое журналы Linux, какие инструменты их генерируют и где эти журналы хранятся. Рассмотрим, как и зачем искать и читать результаты journald и syslog, а также о том, как собрать логи нескольких серверов в одном месте.

👨‍🏫️ Что такое логи?

Логи (журнал сервера, англ. server log) – это записываемые
фрагменты данных, описывающие то, что в конкретный момент времени делает сервер, ядро, службы и приложения. Вот пример лога SSH из /var/log/auth.log:

        May 5 08:57:27 ubuntu-bionic sshd[5544]: pam_unix(sshd:session): session opened for user vagrant by (uid=0)
    

Обратите внимание, что непосредственно
перед сообщением лог содержит несколько полей: метка времени, имя хоста, инициатор
события и идентификатор процесса.

Логи в Linux поступают
из разных источников. Ниже перечислены основные.

Подсистема systemd. Большинство дистрибутивов Linux для управления службами имеют в своём составе systemd. Подсистема инициализации и управления ловит
выходные данные служб и записывает их в журнал. Для работы с логами systemd используется система журналирования journalctl (шпаргалка по работе с journalctl):

        $ journalctl
...
May 05 08:57:27 ubuntu-bionic sshd[5544]: pam_unix(sshd:session): session opened for user vagrant by (uid=0)
...
    

Сообщения процессов по стандарту syslog. При отсутствии systemd такие процессы, как SSH, могут записывать данные в UNIX-сокет
в формате syslog. Демон syslog, например, rsyslog, выбирает сообщение, анализирует и по умолчанию
записывает его в /var/log.

Ядро Linux пишет собственные логи в особый буфер. Подсистемы systemd или syslog могут считывать
журналы из этого буфера, а затем записывать их в свои журналы или файлы – обычно /var/log/kern.log. Чтобы посмотреть логи ядра, воспользуйтесь dmesg:

        $ dmesg -T
...
[Tue May 5 08:41:31 2020] EXT4-fs (sda1): mounted filesystem with ordered data mode. Opts: (null)
...
    

Audit logs. Особый случай сообщений ядра, предназначенных для аудита событий, таких как
доступ к файлам. Обычно для прослушивания таких журналов
безопасности, существует специальная служба, например, auditd, записывающая свои сообщения в /var/log/audit/audit.log.

Журнал приложений.
Несистемные приложения имеют тенденцию записывать данные в /var/log:

  • Apache (httpd) обычно пишет в /var/log/httpd или /var/log/apache2. Журналы HTTP-доступа находятся в файле /var/log/httpd/access.log.
  • Логи MySQL обычно находятся в /var/log/mysql.log или /var/log/mysqld.log.
  • Старые версии Linux могут записывать свои логи загрузки с помощью bootlogd в /var/log/boot или /var/log/boot.log. В современных ОС об этом заботится systemd: вы можете просматривать связанные с загрузкой журналы с помощью journalctl -b. Дистрибутивы без systemd снабжены syslog-демоном, считывающим данные из буфера ядра. Таким образом, вы можете найти свои boot/reboot-журналы в /var/log/messages или /var/log/syslog.

🔍 Если коротко: где искать логи?

Как правило, вы найдете
журналы пингвиньего сервера в каталоге /var/log и подкаталогах. Это место, где
syslog-демонам даны полные права на запись. Также это то место, которое у большинства
приложений (например, Apache) указано по умолчанию, как место хранения логов.

Для systemd
расположение по умолчанию – /var/log/journal, но просматривать файлы
логов напрямую не получится – они хранятся в двоичном формате. Как же быть?

📰 Как анализировать журналы

Если ваш дистрибутив
Linux использует Systemd (как и большинство современных дистрибутивов), то все
ваши системные журналы находятся в специальной области journal. Просмотреть их можно
с помощью journalctl (наиболее важные команды
journalctl
).

Если ваш дистрибутив использует syslog, для их просмотра используются стандартные инструменты: cat, less
или
grep:

        # grep "error" /var/log/syslog | tail
Mar 31 09:48:02 ubuntu-bionic rsyslogd: unexpected GnuTLS error -53 - this could be caused by a broken connection. GnuTLS reports: Error in the push function. [v8.2002.0 try https://www.rsyslog.com/e/2078 ]
...
    

Если для управления журналами вы используете auditd, всё найдётся в файле /var/log/audit.log. В поиске и анализе поможет ausearch.

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

🎯 Централизация логов в Linux


Системные журналы могут
находиться в двух местах: в systemd или в обычных текстовых файлах, записанных
демоном syslog. В некоторых дистрибутивах, например, Ubuntu, есть и то, и
другое: journald настроен на пересылку в syslog. Это осуществляется путем
установки ForwardToSyslog=Yes в конфиге journald.conf.

Централизация журналов с помощью Journald

Если в ваш дистрибутив включён systemd, для централизации журналов мы рекомендуем использовать
journal-upload.

Централизация журналов с помощью syslog

Существует несколько случаев,
в которых подойдет централизация с применением syslog:

  • Если в ваш дистрибутив не включён journald. Это означает, что системные журналы направляются непосредственно в syslog-демон.
  • Когда необходимо собирать и анализировать журналы приложений. Например, в случае с журналами для Apache через rsyslog и Elasticsearch.
  • Если вы хотите перенаправить записи – ForwardToSyslog=Yes. Для этого в качестве транспорта следует использовать syslog-протокол. Однако подход приведет к потере некоторых структурированных данных journald т. к. он пересылает только поля syslog-specific.
  • Когда вы настроили syslog-демон для чтения из журналов (как это делает journalctl). Такой подход не приводит к потере структурированных данных, но более чувствителен к ошибкам (например, в случае повреждения журнала) и увеличивает накладные расходы.

Во всех перечисленных ситуациях информация будут проходить через демон syslog, а оттуда их можно
отправить в любое место и использовать на своё усмотрение.

Большинство
дистрибутивов Linux поставляются с rsyslog. Чтобы пересылать
данные на другой сервер через TCP, добавьте следующую строку в
/etc/rsyslog.conf:

        *.* @@logsene-syslog-receiver.example.com
    

Эта строка будет заворачивать
данные на сервер
example.com. Вы можете заменить logsene-syslog-receiver.[…..]
именем своего syslog-хоста.

Некоторые демоны могут
выводить данные в Elasticsearch через HTTP/HTTPS. Одним из них является наш rsyslog.
Например, если вы юзаете rsyslog на Ubuntu, сначала установите модуль
Elasticsearch:

        sudo apt-get install rsyslog-elasticsearch
    

Затем в
конфигурационном файле вам потребуется поправить два элемента: ш
аблон JSON для Elasticsearch:

        template(name="LogseneFormat" type="list" option.json="on") {
 constant(value="{")
 constant(value="\"@timestamp\":\"")
 property(name="timereported" dateFormat="rfc3339")
 constant(value="\",\"message\":\"")
 property(name="msg")
 constant(value="\",\"host\":\"")
 property(name="hostname")
 constant(value="\",\"severity\":\"")
 property(name="syslogseverity-text")
 constant(value="\",\"facility\":\"")
 property(name="syslogfacility-text")
 constant(value="\",\"syslog-tag\":\"")
 property(name="syslogtag")
 constant(value="\",\"source\":\"")
 property(name="programname")
 constant(value="\"}")
}
    

и action, который пересылает данные в Elasticsearch, используя указанный выше шаблон:

        module(load="omelasticsearch")
action(type="omelasticsearch"
 template="LogseneFormat" # шаблон,объявленный ранее
 searchIndex="LOGSENE_APP_TOKEN_GOES_HERE"
 server="logsene-receiver.example.com"
 serverport="443"
 usehttps="on"
 bulkmode="on"
 queue.dequeuebatchsize="100" # сколько сообщений отправлять за раз
 action.resumeretrycount="-1") # буфер сообщений
    

В приведенном примере показано, как отправлять сообщения в API Elasticsearch на example.com. Настройте action на ваш локальный Elasticsearch:

  • searchIndex – будет вашим алиасом;
  • server – имя хоста (ноды) Elasticsearch;
  • serverport может быть 9200 или кастомным, главное, чтобы на нем слушал Elasticsearch;
  • usehttps= "off" – отправление данных по http.

Независимо от того,
используете ли вы syslog-протокол или что-то еще, лучше перенаправлять данные непосредственно из демона, чем искать проблемы в отдельных файлах из /var/log.

Это не значит, что
файлы в /var/log бесполезны. Они пригодятся в следующих случаях:

  • приложения пишут туда свои логи, например, HTTP, FTP, MySQL и т. д.,
  • требуется обработать системные журналы, например, с помощью grep.

❗Важные файлы журналов для мониторинга

Здесь мы рассмотрим
ключевые файлы логов, какую информацию они хранят, как настраивается rsyslog для записи и как посмотреть информацию с помощью journalctl.

Журнал /var/log/syslog или /var/log/messages

Это «всеохватывающий» системный лог:

        # logger "this is a test"
# tail -1 /var/log/syslog
May 7 15:33:11 ubuntu-bionic test-user: this is a test
    

Вы найдёте здесь все
сообщения: ошибки, информационные сообщения и все другие
серьёзности. Исключением является stop action.

Если в /var/log/syslog
или /var/log/messages пусто, скорее всего, journald не перенаправляет данные в
syslog. Все те же данные можно просмотреть, вызвав journalctl
без параметров.

        # journalctl --no-pager | grep "this is a test"
May 07 15:33:11 ubuntu-bionic test-user[7526]: this is a test
    

Журналы /var/log/kern.log или /var/log/dmesg

Сюда по умолчанию
отправляются сообщения ядра:

        Apr 17 16:47:28 ubuntu-bionic kernel: [ 0.004000] console [tty1] enabled
    

И снова, если у вас нет
syslog (или файл пустой/отсутствует) – используйте journalctl:

        kern.* /var/log/kern.log
    

Журналы /var/log/auth.log или /var/log/secure

Здесь вы найдете
сообщения об аутентификации, генерируемые такими службами, как sshd:

        May 7 15:03:09 ubuntu-bionic sshd[1202]: pam_unix(sshd:session): session closed for user vagrant
    

Вот ещё один фильтр по значениям auth и authpriv:

        auth,authpriv.* /var/log/auth.log
    

Вы можете использовать
такие фильтры в journalctl, используя
числовые
уровни объектов
:

        # journalctl SYSLOG_FACILITY=4 SYSLOG_FACILITY=10
...
May 7 15:03:09 ubuntu-bionic sshd[1202]: pam_unix(sshd:session): session closed for user vagrant
...
    

Журнал /var/log/cron.log

Сюда отправляются ваши
cron-сообщения (jobs-ы, выполняемые регулярно):

        May 06 08:19:01 localhost.localdomain anacron[1142]: Job `cron.daily' started
    

Пример фильтра:

        cron.* /var/log/cron
    

С journalctl можно
сделать так:

        # journalctl SYSLOG_FACILITY=9
    

Журнал /var/log/mail.log или /var/log/maillog

Практически все демоны
(такие как Postfix,
cron и т. д.) обычно пишут свои логи в syslog. Затем rsyslog
раскладывает эти логи по файлам:

        mail.* /var/log/mail.log
    

С помощью journald просматривать
журналы можно так:

        # journalctl SYSLOG_FACILITY=2
    

📄 Подведём итоги

  • Расположение и формат системных журналов Linux зависят от того, как настроен дистрибутив.
  • Большинство дистрибутивов имеют systemd, и все логи «живут» там. Чтобы что-то просмотреть и найти, используйте journalctl.
  • Некоторые дистрибутивы передают системные журналы в syslog, либо напрямую, либо через journal. В этом случае у вас, скорее всего, есть логи, записанные в отдельные файлы в /var/log.
  • Если вы управляете несколькими серверами, вам потребуется централизовать журналирование с помощью специального ПО или использовать собственный ELK-стек.

Логгирование
событий невероятно важная и серьёзная штука в любой сфере администрирования и
ОС. Рекомендуем отнестись ответственно к данной теме – она будет полезна
при дебагинге, разработке и просто в управлении инфраструктурой.

28
Июн
2020

💸 12 способов зарабатывать с помощью программного кода

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

23
Июн
2020

⚛️ 12 бесплатных ресурсов для изучения React

React – одна из самых популярных JavaScript-библиотек для разработки пользовательских интерфейсов. В этой статье вы найдете подборку бесплатных ресурсов о React: открытые курсы, подкасты, ёмкие конспекты и плейлисты YouTube.

21
Июн
2020

🚴 Паттерны Go-кода на все случаи жизни

Cортировки и битовые маски, обработка ошибок и создание изображений, генерация перестановок и работа с хэш-суммами, запуск HTTP-сервера, юнит-тесты и другие распространенные задачи, решаемые с помощью Go.

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. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на
ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

11
Июн
2020

⏳ 10 способов ускорить загрузку вашего сайта

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

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

1. Используйте CDN

CDN (Content Delivery Network) – географически распределённая сетевая инфраструктура, которая оптимизирует доставку контента конечным пользователям, давая доступ к сотням серверов по всему миру, размещающих копию
вашего сайта.

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

2. Включите gzip-сжатие

В некоторых CDN gzip-сжатие можно включить флажком Enable
compression
. Сжатие файлов обеспечит более быструю загрузку контента пользователями сайта.

3. Используйте оптимизацию изображений

Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться
Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться

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

Если у вас сайт на WordPress, можно использовать плагины, которые уменьшат размер автоматически в процессе загрузки.

4. Уменьшите количество
запросов, совершаемых страницей

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

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

5. Избегайте
перенаправлений


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

Некоторые редиректы
неизбежны, например, www -> root domain или root domain -> www, но
основная часть трафика не должна поступать через перенаправления.

6. Сократите время до первого байта

Время до первого байта (TTFB) –
это время, которое браузер тратит на ожидание данных с сервера после отправки запроса на ресурс.

На этот показатель влияют два параметра:

  1. Время, потраченное на сервере.
  2. Время, потраченное на отправку данных.

Первый параметр можно уменьшить, оптимизировав работу на стороне сервера: запросы к базам данных, вызовы API, балансировку нагрузки и т. д..

О втором параметре мы уже поговорили – на него мы можем повлиять, используя CDN.

7. Решите вопрос блокировки рендеринга JavaScript

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

        https://example.com/external.js
    

Можно отложить
загрузку сценариев до тех пор, пока пользователь не начнёт совершать
активные действия:

        window.addEventListener(
  'scroll',
  () =>
    setTimeout(() => {
      //insert marketing snippets here
    }, 1000),
  { once: true }
);
    

8. Минимизируйте CSS и JavaScript


Минимизация подразумевает
использование инструментов для удаления пробелов, символов перевода строки и
сокращения длины имён переменных. Как правило, это делается автоматически в рамках
процесса сборки. Есть специальные инструменты: например,
UglifyJS для JavaScript или cssnano для CSS.

9. Удалите
неиспользуемый код CSS и JavaScript

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

Начиная с Chrome 59, в Chrome DevTools
можно анализировать использование JavaScript и CSS.
Для этого откройте DevTools, перейдите во вкладку Console, нажмите на три точки и
откройте Coverage. При нажатии кнопки со значком перезагрузки будет проведен аудит использования CSS и JavaScript. Помеченные красным блоки кода загружаются, но не используются.

Аудит использования CSS и JS
Аудит использования CSS и JS
Примечание
Об использовании средств DevTools для ускорения загрузки сайта читайте в нашем подробном мануале «Ускоряем загрузку сайта с помощью Chrome DevTools».

10. Регулярно
отслеживайте скорость загрузки сайта

Если вы возьмете за
правило обращать внимание на проверку скорости загрузки, это станет гораздо меньшей проблемой.

Есть бесплатные
инструменты для мониторинга скорости, например,
WebPageTest и Google Lighthouse. Но нужно не забывать запускать их до и после внесения изменений. Для автоматизации запуска GoogleLighthouse можно использовать PerfBeacon.

Заключение

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

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

08
Июн
2020

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

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

08
Июн
2020

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

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

02
Июн
2020

В чём разница между Docker и Kubernetes?

Виртуализация и контейнеризация – будущее крупных проектов. В этой статье обсуждаем и сравниваем ☸️ Kubernetes, 🐋 Docker и инструмент оркестровки 🐳 Docker Swarm.

Что такое Docke…

28
Май
2020

💫 Инструментирование в Go

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

76331ef0-7c12-4d04-9ce9-fc153ee…

26
Май
2020

Безопасная загрузка изображений в веб-приложении на Django

🖼️ 💾 Почти в любом веб-приложении есть необходимость принимать от пользователей картинки. В Django это можно делать лаконично и безопасно, используя ImageField и Pillow.

Загрузка изображений в
Django

3411d48a-91e2-4b1b-8d23-b701155f3593В Django есть два поля, позволяющих загружать картинки: FileField и ImageField. Второй вариант – специализированная версия FileField, использующая подтверждение от библиотеки Pillow, что файл является изображением.

dcf6fa4c-fcb2-4441-97ca-a7102649a58cНачнем с
создания моделей. Создадим файл models.py и поместим в него следующее содержимое:

models.py
            from django.db import models

class Image(models.Model):
    title = models.CharField(max_length=200)
    image = models.ImageField(upload_to='images')

    def __str__(self):
        return self.title
        

0ba04c44-3fdc-4eda-b4e4-a7750d38f502Переменная image – это
поле ImageField, которое работает с API хранилища, который обеспечивает способ
хранения/извлечения, а также чтения/записи файлов.

5a38b4c6-df6e-45e9-908c-c7481e611bbaПараметр upload_to
указывает путь, где будут храниться изображения. Для этой модели он будет соответствовать MEDIA_ROOT/images/

64094f69-8bc4-4e98-a12b-addaedfb2649Также возможна
установка динамических путей для изображений:

            image = models.ImageField(upload_to='users/%Y/%m/%d/', blank=True)
        

babb3c17-94c7-4b60-9ee6-e3ee8b02b3ccЭто позволит хранить
изображения в каталогах вида MEDIA_ROOT/users/2020/05/26.

1f77f8b1-eecc-425b-87ce-8c0173639338Установим Pillow, выполнив следующую команду:

            pip install Pillow
        

aa7d5d86-8f11-4550-aa7e-9199ab4124ffЧтобы Django
обслуживал медиафайлы, загруженные пользователями, добавим следующие настройки
в файл settings.py вашего проекта:

            # Основной url для управления медиафайлами
MEDIA_URL = '/media/'

# Путь хранения картинок
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
        

330875b3-4003-4fd1-ace7-ffa6f40a70baMEDIA_URL – это
URL-адрес, который будет обслуживать медиафайлы, а MEDIA_ROOT – это путь к
корневому каталогу, в котором хранятся файлы.

ad33a632-b4bb-41a2-958b-42cf044d9367Добавим следующий код
в urls.py:

urls.py
            from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    ...]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)
        

2234025d-7599-43d3-b003-8cccee895b3aТеперь нам нужно создать форму
для модели изображения. В файл forms.py добавим следующий код:

forms.py
            from django import forms
from .models import Image


class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ('title', 'image')
        

68f12e0c-5b94-4ef5-8275-037610e36726Это создаст форму с
полями title и image, которые будут отображаться в шаблонах. А сейчас создадим
шаблон для загрузки файлов. В index.html внесем следующее:

            <form method="post" enctype="multipart/form-data">
  {% csrf_token %}
   {{ form.as_p }}
  <button type="submit">Upload</button>
</form>

{% if img_obj %}
  <h3>Succesfully uploaded : {{img_obj.title}}</h3>
  <img src="{{ img_obj.image.url}}" alt="connect" style="max-height:300px">
{% endif %}
        

2a8dfb39-55b7-4438-a3f8-e55a209a7399Включение свойства enctype для формы обеспечивает правильное прикрепление загруженного файла к запросу.

753ed0d4-88fa-4d51-b539-49ff7c23455bНапишем обработчик формы в views.py:

            from django.shortcuts import render
from .forms import ImageForm


def image_upload_view(request):
    """Process images uploaded by users"""
    if request.method == 'POST':
        form = ImageForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            # Get the current instance object to display in the template
            img_obj = form.instance
            return render(request, 'index.html', {'form': form, 'img_obj': img_obj})
    else:
        form = ImageForm()
    return render(request, 'index.html', {'form': form})
        

ef51f263-dc45-4576-81c1-e450c638dcf1Django делает всю
работу сам, а мы просто прогоняем форму через валидацию и сохраняем ее при
успешной загрузке файла. Теперь, когда обработчик готов, сопоставим все
с URL-адресом в urls.py:

            urlpatterns = [
    ......
    path('upload/', views.image_upload_view)
    ......
]
        

038a29b9-408f-4259-baaa-9036db34326aСохраним файлы,
запустим сервер и проверим работу программы.


Заключение

75ddcc17-ed61-402b-9365-691684711a8dМы рассмотрели
простейший вариант безопасной загрузки картинки на сервер с помощью Django.
Вариант прост в реализации и понятен не только для специалистов с опытом. Удачи
в обучении.

11
Май
2020

Как сделать галерею в стиле Instagram

Галереи картинок с горизонтальной прокруткой выглядят современно и узнаваемо. Рассказываем, как с помощью CSS и JS перенести знакомую пользователям механику в ваши веб-приложения.

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

Теория: горизонтальные списки и галерея

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

Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)
Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)

Горизонтальный список со снэппингом – почти то же самое, но текущий элемент списка при тапе заменяется на следующий.

Горизонтальный список со снэппингом (snapping horizontal lists)
Горизонтальный список со снэппингом (snapping horizontal lists)

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

Третий рассматриваемый элемент – галерея. Она похожа на snapping-списки, но каждый элемент занимает всё поле, как в обычном посте Instagram из нескольких фотографий. Под картинками расположен ряд точек – по одной для каждого изображения. Пользователю понятно, что есть другие изображения и какое из них видно в данный момент.

Пример галереи
Пример галереи

Практическая часть

1. Горизонтальные списки со свободной прокруткой

Для горизонтального списка используем flexbox, которому разрешим горизонтальный скроллинг. Размер элементов списка
укажем явно, как и зазор между ними. Поля установим бо́льшими, чем зазор, чтобы видеть, когда мы докрутили до начала или конца списка.

            .list {
  display: flex;
  padding: 20px;
  overflow-x: scroll;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) { margin-right: 10px; }
        

Интерактивное представление и полный код доступны в в
сэндбоксе:


Работает, но
можно улучшить.

Поведение при прокрутке влево. В некоторых браузерах свайп влево повторяет действие кнопки «Назад». Такое поведение можно предотвратить, установив для overscroll-behavior значение contain.

Полосу прокрутки можно скрыть, установив overflow-x в auto. Однако при скроллинге полоса вновь появится. Чтобы скрыть полностью, можно установить scrollbar-width в none. На момент написания статьи это работало только в Firefox. Для других браузеров придется добавить трюкачества в CSS:

            .list { -ms-overflow-style: none; }
.list::-webkit-scrollbar { display: none; }
        

Плавная прокрутка. В iOS не хватает
стандартной плавной прокрутки (momentum scrolling). Потребуется добавить -webkit-overflow-scrolling:
touch;
.

Предотвращение
вертикальной прокрутки
. Мы можем прокручивать
страницу по вертикали. Эту возможность лучше
отключить для пользователей мобильных устройств, добавив к списку touch-action:
pan-x
. Однако если список покрывает всю видимую область просмотра, а на странице есть что-то ещё, эти элементы станут недоступны. То есть такой вариант надо использовать с осторожностью.

В результате наших преобразований соответствующая часть CSS теперь будет иметь следующий вид:

            .list {
  display: flex;
  overflow-x: scroll;
  padding: 20px;

  /* Предотвращает случайное использование "Назад"*/
  overscroll-behavior: contain;

  /* Скрывает полосу прокрутки */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Плавная прокрутка на iOS */
  -webkit-overflow-scrolling: touch;

  /* Отключает вертикальный скроллинг для тач-устройств */
  touch-action: pan-x;
}

/* Скрывает полосу прокрутки  */
.list::-webkit-scrollbar {
  display: none;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) {
  margin-right: 10px;
}

/* фикс для отступов в конце списка */
.item:last-child {
  position: relative;
}

.item:last-child::after {
  position: absolute;
  left: 100%;
  height: 1px;
  width: 20px;
  display: block;
  content: "";
}

        

А результат выглядит вот так (там же полный код примера):


2. Горизонтальный список со снэппингом

Для описания этого типа списков отталкиваемся от списка со свободной прокруткой. Сначала сообщаем списку, когда нужно останавливаться:

            .list {
  scroll-snap-type: x mandatory;
}
        

Элементам списка добавляем опцию scroll-snap-align: start. Чтобы дать подсказку, следующий элемент может немного «выглядывать» из-за края экрана. Для этого достаточно установить отступ прокрутки: scroll-padding-inline-start:
20px
. Если хочется свайпнуть
одним движением несколько элементов,
можно добавить scroll-snap-stop: always к элементам списка, но не все браузеры пока
такое поддерживают.

Код полностью также выложен на Codesandbox:


3. Галерея в стиле Instagram

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


Но как мы поясняли в теоретической части, эти маленькие точки – полезный графический приём. Получается, что нам нужно следить за тем, какой из элементов списка сейчас виден. Наиболее простой способ – использовать IntersectionObserver.

Составим список
точек, каждая из которых соответствует одной из картинок. Когда она пролистывается в списке, мы получаем индекс элемента и устанавливаем
индикаторную точку с соответствующим индексом в активное положение.

Вот как это будет
выглядеть:

            // ссылки на элементы DOM
const list = document.querySelector('.list');
const items = Array.from(document.querySelectorAll('.item'));
const indicators = Array.from(document.querySelectorAll('.indicator'));

// создание наблюдателя
const observer = new IntersectionObserver(onIntersectionObserved, {
  root: list,
  threshold: 0.6
});

// наблюдаем за каждым элементом
items.forEach(item => {
  observer.observe(item);
});

// когда observer обнаруживает изменение записи
// (пункт, входящий в список)
// и эта запись пересекается,
// получаем индекс пересекающегося элемента
// устанавливаем нужный индикатор в активное положение
function onIntersectionObserved(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const intersectingIndex = items.indexOf(entry.target);
      activateIndicator(intersectingIndex);
    }
  });
}

function activateIndicator(index) {
  indicators.forEach((indicator, i) => {
    indicator.classList.toggle('active', i === index);
  });
}
        

Полный результат также доступен в сэндбоксе:


Примечание
Мы установили пороговое значение 0.6. То есть если 60% элемента находится в поле зрения, то мы считаем его «пересекающимся». Если установим значение равным 1, то пересекаемым будет считать только полностью видимый элемент.

Заключение

Мы разобрали несколько
вариаций построения горизонтальных списков изображений. У приведённых решений есть и свои недостатки, и преимущества. Поскольку используется нативная
прокрутка, нет возможности настроить способ движения, мы не можем
контролировать «липкость» ползунка (в этом случае правильнее было бы использовать решение на
JavaScript). Кроме того, в нашем решении мы использовали не самые стандартные подходы в CSS. Однако объём кода невелик, и он работает даже в старых браузерах.

11
Май
2020

📸 Как сделать галерею в стиле Instagram

Галереи картинок с горизонтальной прокруткой выглядят современно и узнаваемо. Рассказываем, как с помощью CSS и JS перенести знакомую пользователям механику в ваши веб-приложения.

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

Теория: горизонтальные списки и галерея

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

Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)
Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)

Горизонтальный список со снэппингом – почти то же самое, но текущий элемент списка при тапе заменяется на следующий.

Горизонтальный список со снэппингом (snapping horizontal lists)
Горизонтальный список со снэппингом (snapping horizontal lists)

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

Третий рассматриваемый элемент – галерея. Она похожа на snapping-списки, но каждый элемент занимает всё поле, как в обычном посте Instagram из нескольких фотографий. Под картинками расположен ряд точек – по одной для каждого изображения. Пользователю понятно, что есть другие изображения и какое из них видно в данный момент.

Пример галереи
Пример галереи

Практическая часть

1. Горизонтальные списки со свободной прокруткой

Для горизонтального списка используем flexbox, которому разрешим горизонтальный скроллинг. Размер элементов списка
укажем явно, как и зазор между ними. Поля установим бо́льшими, чем зазор, чтобы видеть, когда мы докрутили до начала или конца списка.

            .list {
  display: flex;
  padding: 20px;
  overflow-x: scroll;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) { margin-right: 10px; }
        

Интерактивное представление и полный код доступны в в
сэндбоксе:


Работает, но
можно улучшить.

Поведение при прокрутке влево. В некоторых браузерах свайп влево повторяет действие кнопки «Назад». Такое поведение можно предотвратить, установив для overscroll-behavior значение contain.

Полосу прокрутки можно скрыть, установив overflow-x в auto. Однако при скроллинге полоса вновь появится. Чтобы скрыть полностью, можно установить scrollbar-width в none. На момент написания статьи это работало только в Firefox. Для других браузеров придется добавить трюкачества в CSS:

            .list { -ms-overflow-style: none; }
.list::-webkit-scrollbar { display: none; }
        

Плавная прокрутка. В iOS не хватает
стандартной плавной прокрутки (momentum scrolling). Потребуется добавить -webkit-overflow-scrolling:
touch;
.

Предотвращение
вертикальной прокрутки
. Мы можем прокручивать
страницу по вертикали. Эту возможность лучше
отключить для пользователей мобильных устройств, добавив к списку touch-action:
pan-x
. Однако если список покрывает всю видимую область просмотра, а на странице есть что-то ещё, эти элементы станут недоступны. То есть такой вариант надо использовать с осторожностью.

В результате наших преобразований соответствующая часть CSS теперь будет иметь следующий вид:

            .list {
  display: flex;
  overflow-x: scroll;
  padding: 20px;

  /* Предотвращает случайное использование "Назад"*/
  overscroll-behavior: contain;

  /* Скрывает полосу прокрутки */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Плавная прокрутка на iOS */
  -webkit-overflow-scrolling: touch;

  /* Отключает вертикальный скроллинг для тач-устройств */
  touch-action: pan-x;
}

/* Скрывает полосу прокрутки  */
.list::-webkit-scrollbar {
  display: none;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) {
  margin-right: 10px;
}

/* фикс для отступов в конце списка */
.item:last-child {
  position: relative;
}

.item:last-child::after {
  position: absolute;
  left: 100%;
  height: 1px;
  width: 20px;
  display: block;
  content: "";
}

        

А результат выглядит вот так (там же полный код примера):


2. Горизонтальный список со снэппингом

Для описания этого типа списков отталкиваемся от списка со свободной прокруткой. Сначала сообщаем списку, когда нужно останавливаться:

            .list {
  scroll-snap-type: x mandatory;
}
        

Элементам списка добавляем опцию scroll-snap-align: start. Чтобы дать подсказку, следующий элемент может немного «выглядывать» из-за края экрана. Для этого достаточно установить отступ прокрутки: scroll-padding-inline-start:
20px
. Если хочется свайпнуть
одним движением несколько элементов,
можно добавить scroll-snap-stop: always к элементам списка, но не все браузеры пока
такое поддерживают.

Код полностью также выложен на Codesandbox:


3. Галерея в стиле Instagram

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


Но как мы поясняли в теоретической части, эти маленькие точки – полезный графический приём. Получается, что нам нужно следить за тем, какой из элементов списка сейчас виден. Наиболее простой способ – использовать IntersectionObserver.

Составим список
точек, каждая из которых соответствует одной из картинок. Когда она пролистывается в списке, мы получаем индекс элемента и устанавливаем
индикаторную точку с соответствующим индексом в активное положение.

Вот как это будет
выглядеть:

            // ссылки на элементы DOM
const list = document.querySelector('.list');
const items = Array.from(document.querySelectorAll('.item'));
const indicators = Array.from(document.querySelectorAll('.indicator'));

// создание наблюдателя
const observer = new IntersectionObserver(onIntersectionObserved, {
  root: list,
  threshold: 0.6
});

// наблюдаем за каждым элементом
items.forEach(item => {
  observer.observe(item);
});

// когда observer обнаруживает изменение записи
// (пункт, входящий в список)
// и эта запись пересекается,
// получаем индекс пересекающегося элемента
// устанавливаем нужный индикатор в активное положение
function onIntersectionObserved(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const intersectingIndex = items.indexOf(entry.target);
      activateIndicator(intersectingIndex);
    }
  });
}

function activateIndicator(index) {
  indicators.forEach((indicator, i) => {
    indicator.classList.toggle('active', i === index);
  });
}
        

Полный результат также доступен в сэндбоксе:


Примечание
Мы установили пороговое значение 0.6. То есть если 60% элемента находится в поле зрения, то мы считаем его «пересекающимся». Если установим значение равным 1, то пересекаемым будет считать только полностью видимый элемент.

Заключение

Мы разобрали несколько
вариаций построения горизонтальных списков изображений. У приведённых решений есть и свои недостатки, и преимущества. Поскольку используется нативная
прокрутка, нет возможности настроить способ движения, мы не можем
контролировать «липкость» ползунка (в этом случае правильнее было бы использовать решение на
JavaScript). Кроме того, в нашем решении мы использовали не самые стандартные подходы в CSS. Однако объём кода невелик, и он работает даже в старых браузерах.

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-репозитории.

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