Глава 1. Реализация приложения прогноза погоды

Нашим первым приложением в этой книге мы собираемся выбрать приложение веб поиска, которое вычищает информацию прогноза погоды из https:/​/​weather.​com и предоставляет её в терминале. Мы добавим некоторые опции, которые будут передаваться в качестве аргументов в данное приложение, такие как:

  • Единицы измерения температуры (градусы Цельсия или Фаренгейта)

  • Область в которой вы желаете получать прогноз погоды

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

  • Способы комплектования вывода дополнительной информацией, такой как скорость ветра и влажность

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

В данной главе мы изучим как:

  • Пользоваться концепциями объектно- ориентированного программирования в приложениях Python

  • Выскребать данные с вебсайтов с помощью пакета BeautifulSoup

  • Получать аргументы командной строки

  • Применять модуль inspect

  • Динамически загружать модули Python

  • Применять обозримость Python

  • Использовать Selenium для запроса вебстраницы и инспекции элементов DOM

Прежде чем мы приступим, важно отметить, что при разработке приложений веб анализа вам следует иметь в виду, что эти приложения чувствительны к изменениям. Если сами разработчики того сайта, с которого вы получает данные изменяют название класс CSS или структуру DOM HTML, ваше приложение перестаёт работать. Кроме того, если изменяется URL того сайта, с которого мы получаем необходимые данные, наше приложение не сможет отправлять запросы.

Установка среды

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

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

Установка Python поступает с модулем, имеющим название venv, который вы можете применять для создания виртуальных сред; его синтаксис достаточно прямолинеен. Наше приложение, которое мы собираемся создавать называется weatherterm (weather terminal), поэтому мы создадим некую виртуальную среду с таким же названием для упрощения.

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


$ python3 -m venv weatherterm
		

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


$ . weatherterm/bin/activate
		
[Совет]Совет

Я рекомендую установить и применять virtualenvwrapper, который является неким расширением данного инструмента virtualenv. Это сделает очень простыми управление, создание и удаление виртуальных сред, а также быстрое переключение между ними. Если вы желаете исследовать это дополнительно, посетите https:/​/virtualenvwrapper.​readthedocs.​io/​en/​latest/.

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

[Замечание]Замечание

Я настраиваю среду и запускаю все примеры в установленной машине с Debian 9.2 и на момент написания я запускал самую последнюю версию Python (3.6.2). Если вы являетесь пользователем Mac, это не должно сильно отличаться; однако, если вы работаете в Windows, приводимые шаги могут быть слегка иными, однако нге сложно найти информацию о том как установить виртуальную среду и в нём {Прим. пер.: например, ознакомьтесь с Создание виртуальной среды при помощи Python 3.6.x и PEP 405 в нашем переводе отдельных глав "Программирования MQTT на Python" Гастона К. Хайляра, май 2018, Packt Publishing} .

Перейдите в соответствующий только что созданный вами каталог проекта и создайте файл с названием requirements.txt, со следующим содержимым:


beautifulsoup4==4.6.0
selenium==3.6.0
 	   

Это все требующиеся для вашего проекта зависимости:

  • BeautifulSoup: Этот пакет служит для синтаксического анализа файлов HTML и XML. Мы будем применять его для синтаксического разбора HTML, которые мы будем вытаскивать с сайтов погоды для получения данных о погоде, которые понадобятся нам в терминале. Он очень прост в исполнении и имеет великолепную документацию по ссылке: http:/​/​beautiful-​soup-​4.​readthedocs.​io/en/​latest/ {Прим. пер.: также рекомендуем 2е издание Web Scraping with Python (апрель 2018) Райана Митчелла и вышедший в ДМК Пресс перевод её первого издания Скрапинг веб-сайтов с помощью Python.}

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

Для установки этих необходимых пакетов в нашей виртуальной среде вы можете исполнить такую команду:


pip install -r requirements.txt
		
[Совет]Совет

Как всегда, хорошей мыслью будет применять управление версиями подобным GIT или Mercurial. Это очень полезно для контроля за изменением, проверки истории, отката обратно изменений и тому подобного. Если вы не знакомы ни с какими из этих инструментов, в Интернете существует множество руководств. Вы можете начать с проверки документации на самом Git. {Прим. пер.: также обращаем внимание на вышедшее в июне 2018 2е издание GitHub Essentials Achilleas Pipinellis и сделанный в издательстве Питер в 2017 перевод книги С. Чакона и Б. Штрауба Git для профессионального программиста (9785496017633)}.

Нашим последним устанавливаемым средством является PhantomJS.

После его выгрузки раскройте его содержимое внутри своего каталога weatherterm и переименуйте его папку в phantomjs.

Получив в свои руки поднятую виртуальную среду и установленный PhantomJS мы готовы начать кодировать!

Функциональность ядра

Давайте начнём с создания каталога вашего модуля. Внутри каталога корня вашего проекта создайте подкаталог с названием weatherterm. Именно в этом подкаталоге weatherterm и будет обитать наш модуль. Каталогу нашего модуля понадобятся два подкаталога - core и parsers. Структура каталога проектов должна выглядеть так:


weatherterm
├── phantomjs
└── weatherterm
   ├── core
   ├── parsers
 	   

Динамическая загрузка программ анализа синтаксиса

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

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

  • Имя этого файла должно завершаться на parser, например, weather_com_parser.py

  • название имени файла не должно начинаться с удвоенного символа подчёркивания

Сказав всё это, давайте продвинемся далее и создадим свой загрузчик синтаксического анализатора. Создайте файл с названием parser_loader.py внутри каталога weatherterm/core и добавьте в него следующее содержимое:


import os
import re
import inspect

def _get_parser_list(dirname):
    files = [f.replace('.py', '')
             for f in os.listdir(dirname)
            if not f.startswith('__')]

    return files

def _import_parsers(parserfiles):

    m = re.compile('.+parser$', re.I)

   _modules = __import__('weatherterm.parsers',
                         globals(),
                         locals(),
                         parserfiles,
                         0)

    _parsers = [(k, v) for k, v in inspect.getmembers(_modules)
                if inspect.ismodule(v) and m.match(k)]

    _classes = dict()

    for k, v in _parsers:
        _classes.update({k: v for k, v in inspect.getmembers(v)
                         if inspect.isclass(v) and m.match(k)})

    return _classes

def load(dirname):
    parserfiles = _get_parser_list(dirname)
    return _import_parsers(parserfiles)
 	   

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

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

Допустим, что у нас уже имеется некий анализатор в каталоге weatherterm/parsers, возвращаемое inspect.getmembers(_modules) значение будет выглядеть как- то так:


[('WeatherComParser',
  <class
'weatherterm.parsers.weather_com_parser.WeatherComParser'>),
  ...]
 	   
[Замечание]Замечание

inspect.getmembers(_module) возвращает намного больше элементов, однако они должны быть опущены, так как они не имеют отношения к тому, что мы обсуждаем сейчас.

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

Создание модели приложения

Давайте приступим к созданию модели, которая будет представлять всю информацию, которую наше приложение вытащит из своего вебсайта прогноза погоды. Самый первый элемент, который мы собираемся добавить является перечисление для представления каждого варианта прогноза погоды, которые мы будем предоставлять пользователям приложения. Создайте файл с названием forecast_type.py в каталоге weatherterm/core со следующим содержимым:


from enum import Enum, unique

@unique
class ForecastType(Enum):
    TODAY = 'today'
    FIVEDAYS = '5day'
    TENDAYS = '10day'
    WEEKEND = 'weekend'
 	   

Перечисления стали стандартной библиотекой Python начиная с версии 3.4 и их можно создавать при помощи синтаксиса создания класса. Просто создайте класс, наследуемый из enum.Enum, содержащий набор уникальных свойств, установленных в постоянные значения. Здесь у нас имеются значения для четырёх типов прогнозов, которые будет предоставлять данное приложение и где могут быть доступны такие значения как ForecastType.TODAY, ForecastType.WEEKEND и так далее.

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

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

Для начала давайте включим базовое перечисление. Создадим некий файл с название base_enum.py в каталоге weatherterm/core со следующим содержимым:


from enum import Enum

class BaseEnum(Enum):
    def _generate_next_value_(name, start, count, last_value):
        return name
 	   

BaseEnum является очень простым классом, наследуемым из Enum. Единственный момент, который мы хотим тут переопределить, это метод _generate_next_value_, с тем, чтобы всякое перечисление, которое наследуется из BaseEnum и имеет свойства со значением, установленным в auto(), автоматически получали бы те же самые значения, что и само название этого свойства.

Теперь мы можем создать некое перечисление для единиц измерения температуры. Создайте некий файл с названием unit.py в нашем каталоге weatherterm/core с таким содержимым:


from enum import auto, unique

from .base_enum import BaseEnum

@unique
class Unit(BaseEnum):
    CELSIUS = auto()
    FAHRENHEIT = auto()
 	   

Этот класс наследуется из BaseEnum, который мы только что создали, причём все свойства установлены в auto(), что означает, что величина значения для каждого элемента в этом перечислении будет установлена для нас автоматически. Поскольку наш класс Unit наследуется из BaseEnum, при каждом вызове метода auto() в BaseEnum будет вызываться _generate_next_value_ и возвращать значение названия самого данного свойства.

Прежде чем попробовать всё это вывести, создайте некий файл с названием __init__.py в нашем каталоге weatherterm/core и импортируйте то перечисление, которое мы только что создали:


from .unit import Unit
 	   

Если вы загрузите этот класс в REPL Python и проверите его значение произойдёт следующее:


Python 3.6.2 (default, Sep 11 2017, 22:31:28)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from weatherterm.core import Unit
>>> [value for key, value in Unit.__members__.items()]
[<Unit.CELSIUS: 'CELSIUS'>, <Unit.FAHRENHEIT: 'FAHRENHEIT'>]
		

Другой элемент, который мы также хотим добавить в модуль основного ядра своего приложения это класс для представления самих данных прогноза погоды, которые возвращает наш синтаксический анализатор. Давайте пройдём ещё вперёд и создадим файл с названием forecast.py в нашем каталоге weatherterm/core со следующим содержимым:


from datetime import date

from .forecast_type import ForecastType

class Forecast:
    def __init__(
            self,
            current_temp,
	        humidity,
	        wind,
	        high_temp=None,
	        low_temp=None,
	        description='',
	        forecast_date=None,
	        forecast_type=ForecastType.TODAY):
	    self._current_temp = current_temp
	    self._high_temp = high_temp
	    self._low_temp = low_temp
	    self._humidity = humidity
	    self._wind = wind
	    self._description = description
	    self._forecast_type = forecast_type

        if forecast_date is None:
            self.forecast_date = date.today()
        else:
            self._forecast_date = forecast_date

@property
def forecast_date(self):
    return self._forecast_date

@forecast_date.setter
def forecast_date(self, forecast_date):
    self._forecast_date = forecast_date.strftime("%a %b %d")

@property
def current_temp(self):
    return self._current_temp

@property
def humidity(self):
    return self._humidity

@property
def wind(self):
    return self._wind

@property
def description(self):
    return self._description

def __str__(self):
    temperature = None
    offset = ' ' * 4
    if self._forecast_type == ForecastType.TODAY:
        temperature = (f'{offset}{self._current_temp}\xb0\n'
                       f'{offset}High {self._high_temp}\xb0 / '
                       f'Low {self._low_temp}\xb0 ')
    else:
        temperature = (f'{offset}High {self._high_temp}\xb0 / '
                       f'Low {self._low_temp}\xb0 ')

return(f'>> {self.forecast_date}\n'
    f'{temperature}'
    f'({self._description})\n'
    f'{offset}Wind: '
    f'{self._wind} / Humidity: {self._humidity}\n')
 	   

В своём классе Forecast мы определим свойства для всех данных, для которых мы намереваемся выполнять синтаксический разбор:

Таблица 1-1.
Свойство Описание

current_temp

Представляет значение текущей температуры. Доступно только при получении состояния погоды на сегодня.

humidity

Процентное значение влажности на указанную дату

wind

Информация о сегодняшнем текущем уровне ветренности

high_temp

Наивысшая температура за указанную дату

low_temp

Наинизшая температура за указанную дату

description

Описание погодных условий, например Частично облачно

forecast_date

Дата прогноза погоды; если не указана, всё будет относиться к сегодняшнему дню.

forecast_type

Любое из значений перечисления ForecastType (TODAY, FIVEDAYS, TENDAYS или WEEKEND).

Мы также можем реализовать два метода с названием forecast_date с соответствующими декораторами @property и @forecast_date.setter. Декоратор @property превращает этот метод в механизм сборки (getter) для свойства _forecast_date из класса Forecast, а @forecast_date.setter преобразует данный метод в механизм установки (setter). Данный механизм установки определяется здесь потому что всякий раз, когда нам следует устанавливать значение даты в некотором экземпляре Forecast, нам следует быть уверенным что оно будет представлено в необходимом формате. В данном механизме установки мы вызываем метод strftime, передавая значения кодов формата %a (сокращённое название дня недели), %b (сокращённое название месяца) и %d (день месяца).

[Замечание]Замечание

Значения кода форматов %a и %b будут применять локальные настройки той машины, в которой исполняется данный код.

И последнее, мы переписываем основной метод __str__ с тем, чтобы он допускал форматирование нашего вывода таким образом, чтобы мы могли по своему желанию применять стандартные функции print, format и str.

По умолчанию применяемой в weather.com единицей изменения температуры является шкала Fahrenheit и мы желаем дать всем пользователям нашего приложения вариант применения вместо неё градусов Цельсия. Пэтому пройдём далее и создадим ещё один файл в своём каталоге weatherterm/core с названием unit_converter.py и следующим содержимым:


from .unit import Unit

class UnitConverter:
    def __init__(self, parser_default_unit, dest_unit=None):
        self._parser_default_unit = parser_default_unit
        self.dest_unit = dest_unit

        self._convert_functions = {
        Unit.CELSIUS: self._to_celsius,
        Unit.FAHRENHEIT: self._to_fahrenheit,
    }

    @property
    def dest_unit(self):
        return self._dest_unit

    @dest_unit.setter
    def dest_unit(self, dest_unit):
        self._dest_unit = dest_unit

    def convert(self, temp):
        try:
            temperature = float(temp)
        except ValueError:
            return 0

        if (self.dest_unit == self._parser_default_unit or
                self.dest_unit is None):
            return self._format_results(temperature)

        func = self._convert_functions[self.dest_unit]
        result = func(temperature)

        return self._format_results(result)

    def _format_results(self, value):
        return int(value) if value.is_integer() else f'{value:.1f}'

    def _to_celsius(self, fahrenheit_temp):
        result = (fahrenheit_temp - 32) * 5/9
        return result

    def _to_fahrenheit(self, celsius_temp):
        result = (celsius_temp * 9/5) + 32
        return result
 	   

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

Наш метод convert получает только один параметр, значение температуры. Здесь значение температуры является строковой величиной, поэтому первое что нам требуется сделать, это попытаться преобразовать её в значение с плавающей точкой (float); если попытка завершается неудачей, она непосредственно возвратит нулевое значение.

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

Если вам требуется выполнять преобразование, вы можете рассмотреть словарь _convert_functions для обнаружения функции conversion, которую нам следует запустить, мы вызываем её и возвращаем отформатированное значение.

Приводимый ниже код отображает сам метод _format_results, который и является методом инструмента, который отформатирует значение температуры для нас:


return int(value) if value.is_integer() else f'{value:.1f}'
 	   

Метод _format_results проверяет является ли данное число целым; результат value.is_integer() вернёт True если значение, к примеру, равно 10.0. В случае True мы применим для преобразования функцию int для преобразования значения в 10; в противном случае полученное значение возвращается в виде числа с фиксированной точкой с точностью 1. В Python значением точности по умолчанию является 6. Наконец, имеются два метода, которые выполняют преобразование температуры, _to_celsius и _to_fahrenheit.

Теперь нам только требуется изменить файл __init__.py в своём каталоге weatherterm/core и включить следующие операторы импорта:


from .base_enum import BaseEnum
from .unit_converter import UnitConverter
from .forecast_type import ForecastType
from .forecast import Forecast
 	   

Извлечение данных с веб сайта погоды

Мы собираемся добавить класс с названием Request, который будет отвечать за сбор данных с вебсайта погоды. Давайте добавим файл с названием request.py в свой каталог weatherterm/core со следующим содержимым:


import os
from selenium import webdriver

class Request:
    def __init__(self, base_url):
        self._phantomjs_path = os.path.join(os.curdir, 'phantomjs/bin/phantomjs')
        self._base_url = base_url
        self._driver = webdriver.PhantomJS(self._phantomjs_path)

    def fetch_data(self, forecast, area):
        url = self._base_url.format(forecast=forecast, area=area) 
		self._driver.get(url)

        if self._driver.title == '404 Not Found':
            error_message = ('Could not find the area that you ' 
                             'searching for')
            raise Exception(error_message)

        return self._driver.page_source
 	   

Этот класс очень простой; его инициализатор определяет значение базового URL и создаёт некий драйвер PhantomJS, применяя путь, в котором установлен PhantomJS. Метод fetch_data форматирует это драйвер, добавляя необходимые опции предсказания и искомую область. После этого webdriver выполняет запрос и возвращает полученную исходную страницу. Если полученный заголовок возвращает 404 Not Found, будет возбуждена исключительная ситуация. К сожалению, Selenium не предоставляет надлежащего метода получения значения кода состояния HTTP; это было бы намнгого лучше нежели сравнение со строкой.

[Замечание]Замечание

Вы можете заметить, что я устанавливаю для некоторых свойств своего класса символ подчерка в качестве префикса. Обычно я применяю это с тем, чтобы показать что данное свойство является частным (private) и не должно устанавливаться вне данного класса. В Python нет нужды в этом, так как нет способа для установки частных или общедоступных (public) свойств; тем не менее мне нравится так поступать всегда, потому что это позволяет более чётко указывать мои намерения.

Теперь мы можем импортировать его в свой файл __init__.py в нашем каталоге weatherterm/core:


from .request import Request
 	   

Теперь у нас имеется загрузчик синтаксического анализатора для загрузки всех анализаторов, которые мы набросаем в свой каталог weatherterm/parsers, у нас есть класс, представляющий модель прогноза. Существующее перечисление ForecastType представляет единицы измерения температуры и инструментальные функции для преобразования температур из Fahrenheit в Celsius и из Celsius в Fahrenheit. Поэтому теперь нам следует приготовиться к созданию точки входа нашего приложения для получения всех необходимых аргументов, передаваемых нашим пользователем, исполнения синтаксического анализа и предоставления полученных данных в терминале.

Получение ввода пользователя при помощи ArgumentParser

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

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

Звучит несколько вызывающе, не так ли?

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

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


$ python -m weatherterm
		

Ещё одним вариантом является заархивировать (zip) весь каталог приложения и исполнить Python, передав ему вместо самого названия такой файл ZIP. Это некий лёгкий, быстрый и простой способ распространения наших программ Python.

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

Сказав всё это, давайте создадим некий файл __main__.py внутри своего каталога weatherterm со следующим содержимым:


import sys
from argparse import ArgumentParser

from weatherterm.core import parser_loader
from weatherterm.core import ForecastType
from weatherterm.core import Unit

def _validate_forecast_args(args):
    if args.forecast_option is None:
        err_msg = ('One of these arguments must be used: '
                   '-td/--today, -5d/--fivedays, -10d/--tendays, -w/--weekend')
        print(f'{argparser.prog}: error: {err_msg}', file=sys.stderr)
        sys.exit()

parsers = parser_loader.load('./weatherterm/parsers')

argparser = ArgumentParser(
    prog='weatherterm',
    description='Weather info from weather.com on your terminal')

required = argparser.add_argument_group('required arguments')

required.add_argument('-p', '--parser',
                      choices=parsers.keys(),
                      required=True,
                      dest='parser',
                      help=('Specify which parser is going to be used to '
                            'scrape weather information.'))

unit_values = [name.title() for name, value in
Unit.__members__.items()]

argparser.add_argument('-u', '--unit',
                       choices=unit_values,
                       required=False,
                       dest='unit',
                       help=('Specify the unit that will be used to display '
                             'the temperatures.'))

required.add_argument('-a', '--areacode',
                      required=True,
                      dest='area_code',
                      help=('The code area to get the weather
                       broadcast from. '
                            'It can be obtained at https://weather.com'))

argparser.add_argument('-v', '--version',
                       action='version',
                       version='%(prog)s 1.0')

argparser.add_argument('-td', '--today',
                       dest='forecast_option',
                       action='store_const',
                       const=ForecastType.TODAY,
                       help='Show the weather forecast for the current day')

args = argparser.parse_args()

_validate_forecast_args(args)

cls = parsers[args.parser]

parser = cls()
results = parser.run(args)

for result in results:
    print(results)
 	   

Варианты прогноза погоды (сегодня, пять дней, десять дней и прогноз на выходные), которые наше приложение может принимать не являются обязательными; однако по крайней мере один параметр должен быть представлен в нашей командной строке, поэтому мы создаём некую простую функцию с названием _validate_forecast_args для выполнения для нас такой проверки. Эта функция покажет сообщение подсказки и покинет наше приложение.

Во- первых мы получим все доступные в нашем каталоге weatherterm/parsers процедуры синтаксического разбора. Список таких процедур анализа будет применяться как допустимые значения для входных параметров процедур анализа.

Именно имеющийся объект ArgumentParser будет тем, что создаёт задание определения необходимых параметров, разбора их значений и отображения подсказки, поэтому мы создадим некий экземпляр ArgumentParser и также создадим группу аргументов для всех необходимых параметров. Это сделает вывод нашей подсказки выглядящем намного более привлекательным и более упорядоченным.

Чтобы создать все параметры и сделать вывод нашей подсказки более организованным, мы собираемся создать некую группу внутри своего объекта ArgumentParser. Эта группа будет содержать все необходимые параметры, требующиеся нашему приложению. Тем самым, все пользователи нашего приложения смогут запросто увидеть какие параметры являются обязательными, а какие нет.

Мы достигаем этого воспользовавшись такой строкой:


required = argparser.add_argument_group('required arguments')
 	   

После создания такой группы аргументов для всех обязательных параметров мы получаем перечень всех участников перечисления Unit и применяем функцию title() чтобы сделать только самые первые буквы заглавными.

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

Самый первый создаваемый нами аргумент это --parser или -p:


required.add_argument('-p', '--parser',
                      choices=parsers.keys(),
                      required=True,
                      dest='parser',
                      help=('Specify which parser is going to be used to '
                            'scrape weather information.'))
 	   

Давайте рассмотрим каждый параметр вызова add_argument из применяемых при создании данного флага синтаксического разбора:

  • Самые первые два параметра являются самим флагом. В нашем случае пользователь передаёт некое значение для данного аргумента применяя либо -p, либо --parser в своей командной строке, например, --parser WeatherComParser.

  • Следующий параметр choices определяет некий перечень допустимых ключей для данного параметра, который мы можем создавать. В нашем случае мы применяем parsers.keys(), которая возвратит список разобранных имён. Преимуществом такой реализации будет автоматическое добавление к такому перечню, причём не требуются никакие изменения в данном файле.

  • Параметр required, как и сообщает его название, определяет будет обязательным данный аргумент или нет.

  • Наш параметр dest определит собственно название данного атрибута, подлежащее добавлению в полу3чаемый в конечном счёте объект всех аргументов нашего синтаксического анализа. Этот объект возвращается parser_args(), который будет содержать некий атрибут, вызываемый parser со значением, которое будет передаваться в этот аргумент из вызывающей командой строки.

  • Наконец, наш параметр help является аргументом строки текста подсказки, отображаемым с помощью флага -h или -help.

Перейдём к аргументу -today:


argparser.add_argument('-td', '--today',
                       dest='forecast_option',
                       action='store_const',
                       const=ForecastType.TODAY,
                       help='Show the weather forecast for the current day')
 	   

Здесь у нас имеются два ключевых слова аргумента, которые мы не видели раньше, action и const.

Действия могут быть ограничены создаваемыми нами аргументами и они могут выполнять много чего. Наш модуль argparse содержит большое множество действий, однако если вам необходимо делать что- то определённое, вы можете создавать своё собственное действие, которое будет удовлетворять вашим потребностям. Большинство определяемых в модуле argparse действий предназначаются для запоминания значений и синтаксического анализа результатов атрибутов объекта.

В предыдущем фрагменте кода мы применили действие store_const, которое сохранит некое постоянное значение в какой- то атрибут нашего объекта, возвращаемого parse_args().

Мы также применяем ключевое слово аргумента const, которое определяет значение этой константы по умолчанию при использовании данного флага в командной строке.

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


unit_values = [name.title() for name, value in
Unit.__members__.items()]

required.add_argument('-u', '--unit',
                      choices=unit_values,
                      required=False,
                      dest='unit',
                      help=('Specify the unit that will be used to display '
                          'the temperatures.'))
 	   

Возвращаемый parse_args() объект будет содержать некий атрибут с названием unit {единицы измерения} со строковым значением (Celsius или Fahrenheit), однако это не совсем то что нам нужно. Было бы не плохо вместо этого иметь данное значение как некий элемент перечисления, не так ли? Мы можем изменить существующий порядок вещей, создав некое индивидуальное действие.

Во- первых, добавим некий новый файл с названием set_unit_action.py в свой каталог weatherterm/core с таким содержимым:


from argparse import Action

from weatherterm.core import Unit

class SetUnitAction(Action):

    def __call__(self, parser, namespace, values, option_string=None):
        unit = Unit[values.upper()]
        setattr(namespace, self.dest, unit)
 	   

Данный класс действия чрезвычайно простой; он всего лишь выполняет наследование из argparse.Action и переписывает имеющийся метод __call__, который будет вызываться при разборе значения данного аргумента. Это будет применено для установки значения атрибута назначения.

Наш параметр parser будет неким экземпляром ArgumentParser. Его пространством имён является argparser.Namespace и это всего лишь некий простой класс, содержащий все необходимые атрибуты, определяемые в нашем объекте ArgumentParser. Если вы изучите этот параметр в отладчике, вы обнаружите нечто аналогичное следующему:


Namespace(area_code=None, fields=None, forecast_option=None, parser=None, unit=None)
 	   

Величиной параметра values является то значение, которое передал сам пользователь с своей командной строке; в нашем случае это может быть либо Celsius, либо Fahrenheit. Наконец, наш параметр option_string является тем флагом, который определяет данный аргумент. Для нашего аргумента unit значением для option_string будет -u.

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


Unit[values.upper()]
 	   

Проверив это в REPL Python мы получаем:


>>> from weatherterm.core import Unit
>>> Unit['CELSIUS']
<Unit.CELSIUS: 'CELSIUS'>
>>> Unit['FAHRENHEIT']
<Unit.FAHRENHEIT: 'FAHRENHEIT'>
		

После получения правильного участника перечисления мы устанавливаем его значение, определяя его надлежащим образом с помощью self.dest в самом пространстве имён объекта. Это намного понятнее и у нас нет нужды применять магические строки.

Поместив на своё место индивидуальное действие, нам понадобится добавить оператор импорта в свой файл __init__.py в основном каталоге weatherterm/core :


from .set_unit_action import SetUnitAction
 	   

Просто включите эту приведённую выше строку в самый конец указанного файла. Затем нам понадобится импортировать её в свой файл __main__.py таким манером:


from weatherterm.core import SetUnitAction
 	   

И мы собираемся также добавить ключевое слово action в самом определении данного аргумента unit и установить его в SetUnitAction так:


required.add_argument('-u', '--unit',
                      choices=unit_values,
                      required=False,
                      action=SetUnitAction,
                      dest='unit',
                      help=('Specify the unit that will be used to display '
                          'the temperatures.'))
 	   

Таким образом, когда пользователь нашего приложения воспользуется флагом -u для Celsius, полученным из parse_args() значением для атрибута unit в данном объекте будет:


<Unit.CELSIUS: 'CELSIUS'>
 	   

Оставшийся код достаточно прост; мы вызываем свою функцию parse_args для синтаксического разбора полученных аргументов и устанавливаем её результаты в значении переменной args. Затем мы применяем значение args.parser (название выбранного синтаксического анализатора) и осуществляем доступ к такому элементу словаря этого разборщика. Помните, что его значением является тип класса, поэтому мы создаём некий экземпляр данного разборщика и после этого вызываем соотвествующий метод на исполнение, что и будет толчком к вытаскиванию данных с вебсайта.

Создание программы синтаксического анализа

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

Давайте продвинемся далее и создадим некий файл с названием weather_com_parser.py в своём каталоге weatherterm/parsers. Чтобы сделать это более протсым, мы намереваемся создать только необходимые методы и единственная вещь, которую мы собираемся делать при запуске методов состоит в возбуждении NotImplementedError:


from weatherterm.core import ForecastType

class WeatherComParser:

def __init__(self):
    self._forecast = {
        ForecastType.TODAY: self._today_forecast,
        ForecastType.FIVEDAYS: self._five_and_ten_days_forecast,
        ForecastType.TENDAYS: self._five_and_ten_days_forecast,
        ForecastType.WEEKEND: self._weekend_forecast,
        }

def _today_forecast(self, args):
    raise NotImplementedError()

def _five_and_ten_days_forecast(self, args):
    raise NotImplementedError()

def _weekend_forecast(self, args):
    raise NotImplementedError()

def run(self, args):
    self._forecast_type = args.forecast_option
    forecast_function = self._forecast[args.forecast_option]
    return forecast_function(args)
 	   
ForecasType, а его значением является тот метод,который привязывается к этим опциям. Наше приложение будет способно представлять прогноз погоды на сегодня, на пять дней, на десять дней и на выходные, поэтому мы реализуем все четыре метода.

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

Теперь это приложение наконец готово для исполнения в самый первый раз если вы исполните данную команду в своей командной строке:


$ python -m weatherterm --help
		

Вы должны увидеть опции в подсказке нашего приложения:


usage: weatherterm [-h] -p {WeatherComParser} [-u {Celsius,Fahrenheit}] -a AREA_CODE [-v] [-td] [-5d] [-10d] [-w]

Weather info from weather.com on your terminal

optional arguments:
  -h, --help show this help message and exit
  -u {Celsius,Fahrenheit}, --unit {Celsius,Fahrenheit}
                        Specify the unit that will be used to display 
                        the temperatures.
  -v, --version show program's version number and exit
  -td, --today Show the weather forecast for the current day require arguments:
  -p {WeatherComParser}, --parser {WeatherComParser}
                        Specify which parser is going to be used to scrape
                        weather information.
  -a AREA_CODE, --areacode AREA_CODE
                        The code area to get the weather broadcast from. It
                        can be obtained at https://weather.com
		

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

Отметим, что наш аргумент -p уже выдаёт вам вариант выбора WeatherComParser. Его не требовалось кодировать в явном виде неким образом, так как наш загрузчик синтаксических анализаторов сделал эту работу за нас. Наш флаг -u (--unit) также содержит необходимые элементы перечисления Unit (единиц измерения). Если когда- либо вы пожелаете расширить данное приложение и добавить новые единицы измерения, единственная вещь которая вам понадобится, это добавить такие новые элементы в данное перечисление и они автоматически закрепятся и будут включаться в виде вариантов для данного флага -u.

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


$ python -m weatherterm -u Celsius -a SWXX2372:1:SW -p WeatherComParser -td
		

Вы получите некую исключительную ситуацию подобную такой:

 

Рисунок 1-1



Не стоит волноваться - это именно то что мы хотели получить! Если вы отследите весь свой стек, вы сможете увидеть что всё работает именно так, как мы и предполагали. Когда вы запускаете свой код, мы вызываем свой метод run в выбранном нами анализаторе из своего файла __main__.py, затем мы выбираем тот метод, который связан с данным вариантом прогноза, в данном случае это _today_forecast, и наконец сохраняем полученный результат в своей переменной forecast_function.

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

Получения прогноза погоды на сегодня

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

Поскольку сам я из Швеции, я буду применять код области SWXX2372:1:SW (Стокгольм, Швеция); однако, вы можете воспользоваться любым кодом по своему выбору. Для получения своего кода пройдите на ​weather.​com и отыщите код того региона, который нужен вам. После выбора этой области будет отображён прогноз погоды на текущий день. Отметим ключевые особенности изменения в URL, скажем,когда мы отыскиваем Стокгольм, Швеция, наш URL изменяется на https:/​/​weather.​com/​weather/​today/​l/​SWXX2372:1:SW.

Для Сан- Пало, Бразилия он будет выглядеть так: https:/​/​weather.​com/​weather/​today/​l/​BRXX0232:1:BR.

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

Добавление методов помощника

Чтобы начать нам потребуется импортировать некоторые пакеты:


import re

from weatherterm.core import Forecast
from weatherterm.core import Request
from weatherterm.core import Unit
from weatherterm.core import UnitConverter
 	   

А в инициализаторе мы собираемся добавить следующий код:


self._base_url = 'http://weather.com/weather/{forecast}/l/{area}'
self._request = Request(self._base_url)

self._temp_regex = re.compile('([0-9]+)\D{,2}([0-9]+)')
self._only_digits_regex = re.compile('[0-9]+')

self._unit_converter = UnitConverter(Unit.FAHRENHEIT)
 	   

В своём инициализаторе мы определяем значение шаблона URL, который мы собираемся применять для выполнения запросов с вебсайта прогноза погоды; затем мы создаём некий объект Request. Именно этот объект будет выполнять для нас запросы.

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

Также мы определяем объект UnitConverter и устанавливаем единицей измерения по умолчанию Fahrenheit.

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


def _get_data(self, container, search_items):
    scraped_data = {}

    for key, value in search_items.items():
        result = container.find(value, class_=key)
        data = None if result is None else result.get_text()
        if data is not None:
            scraped_data[key] = data

    return scraped_data
 	   

Основная идея данного метода состоит в поиске элементов с содержимым, которое соответствует неким критериям. Сам container это всего лишь элемент DOM в HTML, а search_items является неким словарём, в котором значением ключа является некий класс CSS, а его значением является сам тип данного элемента HTML. Это может быть DIV, SPAN или любой иной, в котором вы желаете получить искомое значение.

Он начинается с цикла по search_items.items() и применяет метод find для поиска необходимого элемента внутри данного контейнера. Если данный элемент не найден, мы применяем get_text для выделения значения текста данного элемента DOM и добавления его в свой словарь, который мы будем возвращать когда не останется больше элементов поиска.

Наш второй реализуемый метод это метод _parse. Он применяет тот _get_data, который мы только что реализовали:


def _parse(self, container, criteria):
    results = [self._get_data(item, criteria)
               for item in container.children]

    return [result for result in results if result]
 	   

Здесь мы также получаем container и criteria как и в _get_data. Нашим контейнером является некий элемент DOM, а критерием является некий словарь узлов, которые мы желаем отыскать. Самый первый охват получает все контейнеры дочерних элементов и передаёт их в наш метод _get_data.

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

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


def _clear_str_number(self, str_number):
  result = self._only_digits_regex.match(str_number)
  return '--' if result is None else result.group()
 	   

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

А последний метод, который нам необходимо реализовать это метод _get_additional_info:


def _get_additional_info(self, content):
    data = tuple(item.td.span.get_text()
                 for item in content.table.tbody.children)
    return data[:2]
 	   

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

Реализация прогноза погоды на сегодня

Пора начать добавлять реализацию нашего метода _today_forecast, тем не менее, нам вначале следует импортировать BeautifulSoup. В самом верху своего файла добавьте следующий оператор импорта:


from bs4 import BeautifulSoup
 	   

Теперь мы можем добавить нужный нам метод _today_forecast:


def _today_forecast(self, args):
    criteria = {
        'today_nowcard-temp': 'div',
        'today_nowcard-phrase': 'div',
        'today_nowcard-hilo': 'div',
        }

    content = self._request.fetch_data(args.forecast_option.value,
                                       args.area_code)

    bs = BeautifulSoup(content, 'html.parser')

    container = bs.find('section', class_='today_nowcard-container')

    weather_conditions = self._parse(container, criteria)

    if len(weather_conditions) < 1:
        raise Exception('Could not parse weather foreecast for today.')

    weatherinfo = weather_conditions[0]

    temp_regex = re.compile(('H\s+(\d+|\-{,2}).+'
                             'L\s+(\d+|\-{,2})'))
    temp_info = temp_regex.search(weatherinfo['today_nowcard-hilo']) 
    high_temp, low_temp = temp_info.groups()

    side = container.find('div', class_='today_nowcard-sidecar')
    humidity, wind = self._get_additional_info(side)

    curr_temp = self._clear_str_number(weatherinfo['today_nowcardtemp'])
    self._unit_converter.dest_unit = args.unit
    td_forecast = Forecast(self._unit_converter.convert(curr_temp),
                           humidity,
                           wind,
                           high_temp=self._unit_converter.convert(high_temp),
                           low_temp=self._unit_converter.convert(low_temp),
                           description=weatherinfo['today_nowcardphrase'])
    return [td_forecast]
 	   

Именно эта функция вызывается когда в вашей командной строке применяется флаг -td или --today. Давайте остановимся на этом коде чтобы вы смогли проще понимать что именно он делает. Осознание данного метода важно, так как эти метод разбирает данные и из прочих вариантов прогноза погоды (пять дней, десять дней, выходные), которые очень похожи на описываемый нами сейчас.

Подпись на данный метод достаточно проста; он получает только args, который является параметром Argument, создаваемым нашим методом __main__. Самая первая вещь, которую мы делаем в этом методе, это создание словаря criteria со всеми элементами DOM, которые мы хотим обнаружить в своей записи пожеланий:


criteria = {
    'today_nowcard-temp': 'div',
    'today_nowcard-phrase': 'div',
    'today_nowcard-hilo': 'div',
}
 	   

Как это уже отмечалось выше, ключом в данном словаре criteria является само название элемента CSS класса DOM, а его значением является сам тип элемента HTML:

  • Класс today_nowcard-temp является неким классом CSS данного элемента DOM, содержащего значение текущей темперантуры.

  • Класс today_nowcard-phrase является каким- то классом CSS элемента DOM, содержащего текст условий погоды (Cloudy - облачно, Sunny - солнечно, и тому подобное)

  • Класс today_nowcard-hilo это класс CSS определённого элемента DOM, который содержит наивысшую и минимальную температуры.

После этого мы переходим к выборке, созданию и использованию BeautifulSoup для синтаксического анализа нашего DOM^


content = self._request.fetch_data(args.forecast_option.value, args.area_code)

bs = BeautifulSoup(content, 'html.parser')

container = bs.find('section', class_='today_nowcard-container')

weather_conditions = self._parse(container, criteria)

if len(weather_conditions) < 1:
    raise Exception('Could not parse weather forecast for today.')

weatherinfo = weather_conditions[0]
 	   

Вначале мы пользуемся методом BeautifulSoup из своего класса Request чтобы создать необходимый модуль ядра и передать два параметра; причём первым является вариант прогноза, а вторым аргумент кода области, которые мы передаём своей командой строке.

После опроса этих данных мы создаём объект BeautifulSoup, передавая ему значения content и parser. Так как мы получаем обратно HTML, мы применяем html.parser.

Теперь настало время поиска элементов HTML, которые нужны нам. Помните, что мы ищем элементы, которые будут контейнерами, а функция _parse будет выполнять поиск среди дочерних элементов и пытаться отыскать элементы, которые мы определили в критериях поиска своего словаря. Для прогноза погоды на сегодня, тем элементом, который содержит все необходимые нам данные является элемент section внутри класса CSStoday_nowcard-container.

BeautifulSoup содержит свой метод find, который может применяться для поиска элементов в определённом DOM HTML с заданными критериями поиска. Отметим, что значение ключевого аргумента называется class_, а не class, так как слово class зарезервировано в Python.

Теперь когда у нас имеется необходимый элемент нашего контейнера, мы можем передать его в свой метод _parse, который вернёт некий список. Мы выполним проверку того, содержит ли данный результат по крайней мере один эле мент и возбудим некую исключительную ситуацию, если он пуст. Если он не пустой, мы всего лишь получаем его самый первый элемент и назначаем его своей переменной weatherinfo, . Значение переменной weatherinfo теперь содержит некий словарь, который включает все те элементы, которые мы пытаемся найти.

Следующий наш шаг расщепляет значения самой высокой и самой низкой температур:


vfs.zfs.vdev.cache.size=<10M>temp_regex = re.compile(('H\s+(\d+|\-{,2}).+'
                                                            'L\s+(\d+|\-{,2})'))
temp_info = temp_regex.search(weatherinfo['today_nowcard-hilo'])
high_temp, low_temp = temp_info.groups()
 	   

Мы хотим выполнит разбор полученного текста, котрый был выделен из элемента DOM с наличием класса CSS today_nowcard-hilo, и это текст должен выглядеть как- то навроде H 50 L 60, H -- L 60 и тому подобного. Более лёгкий и простой способ для выделения данного нужного нам текста состоит в использовании регулярного выражения:


H\s+(\d+|\-{,2}).L\s+(\d+|\-{,2})
 	   

Мы можем разбить это регулярное выражение на две части. В первой мы желаем получить значение наивысшей температуры - H\s+(\d+|\-{,2});, что означает, что мы помечаем некий H, за которым следуют некоторые пространства и они будут собираться в группу, которая соответствует либо численным значениям, допустимому максимуму из двух двух символов тире. После этого оно может соответствовать любому символу. наконец, поступающая вторая часть, которая в основном делает то же самое; однако она .начинает сопоставлять; тем не менее, она начинает сопоставлять некий L группы регулярных выражений, которые возвращались при помощи своих функций.

После получения метода поиска он получает группы регулярных выражений, которые были возвращены вызова функции H\s+(\d+|\-{,2});, которая в данной ситуации возвратит две функции, которые в данном положении вещей будут возвращать две группы, одну для самого высокого значения температуры, а вторую для наинизшей.

Прочая информация, которую нам следовало бы предоставлять своим пользователям, это информация о ветренности и влажности. Соответствующий элемент контейнера, который содержит эту информацию, имеет класс CSS с названием today_nowcard-sidecar:


side = container.find('div', class_='today_nowcard-sidecar')
wind, humidity = self._get_additional_info(side)
 	   

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

Наконец, самая последняя часть данного метода:


curr_temp = self._clear_str_number(weatherinfo['today_nowcard-temp'])

self._unit_converter.dest_unit = args.unit

td_forecast = Forecast(self._unit_converter.convert(curr_temp),
                       humidity,
                       wind,
                       high_temp=self._unit_converter.convert(high_temp),
                       low_temp=self._unit_converter.convert(low_temp),
                       description=weatherinfo['today_nowcard-phrase'])

return [td_forecast]
 	   

Так как значение текущей температуры содержит некий особый символ (знак градуса), который мы не хотим иметь в данный момент времени, мы применяем имеющийся у нас метод _clr_str_numberde чтобы передать имеющееся значение элемента today_nowcard-temp в свой словарьweatherinfoode.

Теперь у нас есть вся необходимая нам информация, мы построили необходимый объект span class="term">Forecast и выполнили его возврат. Отметим, что здесь мы возвращаем некий массив; это происходит по той причине, что все прочие варианты, которые мы собираемся реализовать (прогнозы на пять дней, десять дней и на выходные) будут возвращать список и чтобы делать это последовательно; а также для удобства отображения данной информации на вашем терминале, мы также возвращаем перечень.

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

Когда мы запустим свою команду вновь:


$ python -m weatherterm -u Fahrenheit -a SWXX2372:1:SW -p WeatherComParser -td
		

Вы обнаружите вывод похожий на этот:

 

Рисунок 1-2



Поздравляем! вы реализовали своё первое приложение веб поиска. Далее давайте добавим прочие варианты предсказания погоды.

Получения прогноза погоды через пять и десять дней

Тот сайт, который мы только что применяли для поиска в нём прогноза погоды (weather.com), также предоставляет прогноз погоды на пять и десять дней, поэтому в данном разделе мы также собираемся реализовать методы анализа этих вариантов.

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


def _parse_list_forecast(self, content, args):
    criteria = {
        'date-time': 'span',
        'day-detail': 'span',
        'description': 'td',
        'temp': 'td',
        'wind': 'td',
        'humidity': 'td',
}

bs = BeautifulSoup(content, 'html.parser')

forecast_data = bs.find('table', class_='twc-table')
container = forecast_data.tbody

return self._parse(container, criteria)
 	   

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

  • Значением date-time является элемент span, и он содержит некую строку,которая предоставляет день недели

  • Значением day-detail является элемент span и он содержит некую строку со значением даты, например, SEP 29

  • Значением description является элемент TD, который содержит условия погоды, например Cloudy (облачно)

  • temp это элемент TD и содержит информацию о температуре, например, наивысшая и самая низкая температуры

  • wind это элемент TD и он содержит информацию о ветре

  • humidity это элемент TD,который содержит информацию о влажности

Теперь когда мы получили необходимые критерии, мы создадим некий объект BeatufulSoup, передавая в него получаемое содержимое и html.parser. Все те данные, которые мы бы хотели получить содержатся в таблице с классом CSS, имеющим название twc-table. Мы отыскиваем эту таблицу и находим соответствующий элемент tbody в качестве контейнера. Наконец, мы запускаем свой метод _parse, передавая соответствующие container и criteria, которые мы только что определили. Результат этой функции выглядит как- то так:


[{'date-time': 'Today',
  'day-detail': 'SEP 28',
  'description': 'Partly Cloudy',
  'humidity': '78%',
  'temp': '60°50°',
  'wind': 'ESE 10 mph '},
 {'date-time': 'Fri',
  'day-detail': 'SEP 29',
  'description': 'Partly Cloudy',
  'humidity': '79%',
  'temp': '57°48°',
  'wind': 'ESE 10 mph '},
 {'date-time': 'Sat',
  'day-detail': 'SEP 30',
  'description': 'Partly Cloudy',
  'humidity': '77%',
  'temp': '57°49°',
  'wind': 'SE 10 mph '},
 {'date-time': 'Sun',
  'day-detail': 'OCT 1',
  'description': 'Cloudy',
  'humidity': '74%',
  'temp': '55°51°',
  'wind': 'SE 14 mph '},
 {'date-time': 'Mon',
  'day-detail': 'OCT 2',
  'description': 'Rain',
  'humidity': '87%',
  'temp': '55°48°',
  'wind': 'SSE 18 mph '}]
 	   

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


def _prepare_data(self, results, args):
    forecast_result = []

    self._unit_converter.dest_unit = args.unit

    for item in results:
        match = self._temp_regex.search(item['temp'])
        if match is not None:
            high_temp, low_temp = match.groups()

        try:
            dateinfo = item['weather-cell']
            date_time, day_detail = dateinfo[:3], dateinfo[3:]
            item['date-time'] = date_time
            item['day-detail'] = day_detail
        except KeyError:
            pass

        day_forecast = Forecast(
            self._unit_converter.convert(item['temp']),
            item['humidity'],
            item['wind'],
            high_temp=self._unit_converter.convert(high_temp),
            low_temp=self._unit_converter.convert(low_temp),
            description=item['description'].strip(),
            forecast_date=f'{item["date-time"]} {item["day-detail"]}',
            forecast_type=self._forecast_type)
        forecast_result.append(day_forecast)

    return forecast_result
 	   

Этот метод достаточно простой. Во- первых он обрабатывает в цикле полученные результаты и применяет созданный нами regex для расщепления наивысшего и наинизшего значений температуры, сохраняемых в item['temp']. Если получено соответствие, они будут сгруппированы и получат значения для high_temp и low_temp.

После этого мы создаём свой объект Forecast и добавляем в его конец некий список, который будет возвращён позднее.

Последним мы добавляем тот метод, который будет запускаться при использовании флага -5d или -10d. Создадим ещё один метод с названием _five_and_ten_days_forecast и с таким содержимым:


def _five_and_ten_days_forecast(self, args):
    content = self._request.fetch_data(args.forecast_option.value, args.area_code)
    results = self._parse_list_forecast(content, args)
    return self._prepare_data(results)
 	   

Данный метод всего лишь добивается необходимого содержимого от той страницы, которая передаёт соответствующее значение forecast_option и код искомого региона, поэтому он будет способен построить необходимый URL для выполнения соответствующего запроса. Когда возвращаются необходимые данные, мы передаём их вниз в _parse_list_forecast, который возвратит список объектов Forecast (по одному для каждого дня); наконец, мы подготовим соответствующие данные для возврата с помощью своего метода _prepare_data .

Прежде чем мы выполним эту команду, нам потребуется разрешить эту опцию в своём инструменте командной строки, который мы реализуем; пройдём в свой файл __main__.py и сразу после определения нашего флага -td добавим такой код:


argparser.add_argument('-5d', '--fivedays',
                       dest='forecast_option',
                       action='store_const',
                       const=ForecastType.FIVEDAYS,
                       help='Shows the weather forecast for the next 5 days')
 	   

Теперь выполним своё приложение вновь, но на этот раз применив флаг -5d или --fivedays:


$ python -m weatherterm -u Fahrenheit -a SWXX2372:1:SW -p WeatherComParser -5d
		

Оно выдаст следующий вывод:


>> [Today SEP 28]
    High 60° / Low 50° (Partly Cloudy)
    Wind: ESE 10 mph / Humidity: 78%

>> [Fri SEP 29]
    High 57° / Low 48° (Partly Cloudy)
    Wind: ESE 10 mph / Humidity: 79%

>> [Sat SEP 30]
    High 57° / Low 49° (Partly Cloudy)
    Wind: SE 10 mph / Humidity: 77%

>> [Sun OCT 1]
    High 55° / Low 51° (Cloudy)
    Wind: SE 14 mph / Humidity: 74%

>> [Mon OCT 2]
    High 55° / Low 48° (Rain)
    Wind: SSE 18 mph / Humidity: 87%
		

Чтобы покончить с этим разделом, давайте включим соответствующую опцию чтобы сделать прогноз погоды на следующие десять дней в своём файле __main__.py сразу после определения нашего флага -5d добавим следующий код:


argparser.add_argument('-10d', '--tendays',
                       dest='forecast_option',
                       action='store_const',
                       const=ForecastType.TENDAYS,
                       help='Shows the weather forecast for the next 10 days')
 	   

Если вы выполните ту же самую команду, которую мы применяли для получения прогноза погоды на пять дней, но на этот раз заментие свой флаг -5d на -10d следующим образом:


$ python -m weatherterm -u Fahrenheit -a SWXX2372:1:SW -p WeatherComParser -10d
		

вы должны обнаружить вывод прогноза погоды на десять дней:


>> [Today SEP 28]
    High 60° / Low 50° (Partly Cloudy)
    Wind: ESE 10 mph / Humidity: 78%

>> [Fri SEP 29]
    High 57° / Low 48° (Partly Cloudy)
    Wind: ESE 10 mph / Humidity: 79%

>> [Sat SEP 30]
    High 57° / Low 49° (Partly Cloudy)
    Wind: SE 10 mph / Humidity: 77%

>> [Sun OCT 1]
    High 55° / Low 51° (Cloudy)
    Wind: SE 14 mph / Humidity: 74%

>> [Mon OCT 2]
    High 55° / Low 48° (Rain)
    Wind: SSE 18 mph / Humidity: 87%

>> [Tue OCT 3]
    High 56° / Low 46° (AM Clouds/PM Sun)
    Wind: S 10 mph / Humidity: 84%

>> [Wed OCT 4]
    High 58° / Low 47° (Partly Cloudy)
    Wind: SE 9 mph / Humidity: 80%

>> [Thu OCT 5]
    High 57° / Low 46° (Showers)
    Wind: SSW 8 mph / Humidity: 81%

>> [Fri OCT 6]
    High 57° / Low 46° (Partly Cloudy)
    Wind: SW 8 mph / Humidity: 76%

>> [Sat OCT 7]
    High 56° / Low 44° (Mostly Sunny)
    Wind: W 7 mph / Humidity: 80%

>> [Sun OCT 8]
    High 56° / Low 44° (Partly Cloudy)
    Wind: NNE 7 mph / Humidity: 78%

>> [Mon OCT 9]
    High 56° / Low 43° (AM Showers)
    Wind: SSW 9 mph / Humidity: 79%

>> [Tue OCT 10]
    High 55° / Low 44° (AM Showers)
    Wind: W 8 mph / Humidity: 79%

>> [Wed OCT 11]
    High 55° / Low 42° (AM Showers)
    Wind: SE 7 mph / Humidity: 79%

>> [Thu OCT 12]
    High 53° / Low 43° (AM Showers)
    Wind: NNW 8 mph / Humidity: 87%
		

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

Получения прогноза погоды на выходные

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

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

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

казав всё это, продвинемся на шаг далее и создадим новый файл в своём каталоге weatherterm/core с наименованием mapper.py и следующим содержимым:


class Mapper:

    def __init__(self):
        self._mapping = {}

    def _add(self, source, dest):
        self._mapping[source] = dest

    def remap_key(self, source, dest):
        self._add(source, dest)

    def remap(self, itemslist):
        return [self._exec(item) for item in itemslist]

    def _exec(self, src_dict):
        dest = dict()

        if not src_dict:
            raise AttributeError('The source dictionary cannot be empty or None')

        for key, value in src_dict.items():
            try:
                new_key = self._mapping[key]
	            dest[new_key] = value
            except KeyError:
                dest[key] = value
        return dest
 	   

Наш класс Mapper даёт некий список словарей и переименовывает определённые ключи, у которых мы бы хотели сменить названия. Самыми важными методами здесь являются remap_key и remap. Метод remap_key получает два параметра, source и dest. Значение source является тем ключом, который мы бы хотели переименовать, а dest является новым названием для этого ключа. Наш метод remap_key добавит его в некий внутренний словарь с наименованием _mapping, который позднее будет применён при поиске этого нового названия ключа.

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

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

Теперь нам нужно просто добавить его в свой файл __init__.py в нашем каталоге weatherterm/core:


from weatherterm.core import Mapper
 	   

А в нашем файле weather_com_parser.py из каталога weatherterm/parsers нам следует импортировать Mapper:


from weatherterm.core import Mapper
 	   

Поместив это соответствие на своё место мы можем двинуться далее и создать метод _weekend_forecast в своём файле weather_com_parser.py таким образом:


def _weekend_forecast(self, args):
    criteria = {
        'weather-cell': 'header',
        'temp': 'p',
        'weather-phrase': 'h3',
        'wind-conditions': 'p',
        'humidity': 'p',
    }

    mapper = Mapper()
    mapper.remap_key('wind-conditions', 'wind')
    mapper.remap_key('weather-phrase', 'description')

    content = self._request.fetch_data(args.forecast_option.value, args.area_code)

    bs = BeautifulSoup(content, 'html.parser')

    forecast_data = bs.find('article', class_='ls-mod')
    container = forecast_data.div.div

    partial_results = self._parse(container, criteria)
    results = mapper.remap(partial_results)

return self._prepare_data(results, args)
 	   

Этот метод начинается с определения необходимых критериев в точности так же как и в наших прочих методах; однако в этот раз структура DOM слегка отличается и некоторые названия CSS также иные:

  • weather-cell: содержит дату нашего прогноза: FriSEP 29

  • temp: содержит значения температуры (максимальное и минимальное): 57°F48°F

  • weather-phrase: содержит соответствующее значение погоды: Cloudy (облачно)

  • wind-conditions: информация о ветре

  • humidity: процентное соотношение влажности

Как вы можете видеть, чтобы успешно пустить их в дело со своим методом _prepare_data, нам понадобится переименовать нкоторые ключи в своих словарях в результирующем наборе - wind-conditions должен быть wind, а weather-phrase следует сделать description.

К своему счастью мы ввели для помощи в этом класс Mapper:


mapper = Mapper()
mapper.remap_key('wind-conditions', 'wind')
mapper.remap_key('weather-phrase', 'description')
 	   

Мы создали некий объект Mapper и сообщили об изменении соответствия wind-conditions в wind, а weather-phrase в description.


content = self._request.fetch_data(args.forecast_option.value, args.area_code)

bs = BeautifulSoup(content, 'html.parser')

forecast_data = bs.find('article', class_='ls-mod')
container = forecast_data.div.div

partial_results = self._parse(container, criteria)
 	   

Мы достаём все необходимые данные, при помощи html.parser создаём объект BeautifulSoup и отыскиваем тот элемент контейнера, который содержит интересующие нас дочерние элементы. Для нашего прогноза на выходные мы интересуемся получением элемента article с классом CSS, имеющим название ls-mod, а внутри article мы проходим вниз к самому первому дочернему элементу, которым является DIV и получаем его первый элемент, который также является DIV.

Искомый HTML должен выглядеть как- то так:


<article class='ls-mod'>
  <div>
    <div>
      <!-- этот DIV будет содержать наш элемент -->
    </div>
  </div>
</article>
 	   

Именно по этой причине мы сначала отыскиваем свой параграф (article), назначаем его forecast_data, а затем применяем forecast_data.div.div ч тем, чтобы получить тот элемент DIV, который требуется нам.

После определения данного контейнера мы передаём его в метод _parse вместе с самим элементом контейнера; когда мы получаем полученные результаты обратно, нам просто нужно запустить свой метод remap из имеющегося экземпляра Mapper, что приведёт для нас эти данные в нормальный вид перед тем как мы вызовем _prepare_data.

Теперь самая последняя детль перед запуском нашего приложения и получением прогноза погоды для выходных состоит в том,что нам требуется включить соответствующие флаги -w и --weekend в свой ArgumentParser. Откройте соответствующий файл __main__.py в нашем каталоге weatherterm и сразу после имеющегося уже флага -tenday добавьте такой код:


argparser.add_argument('-w', '--weekend',
                       dest='forecast_option',
                       action='store_const',
                       const=ForecastType.WEEKEND,
                       help=('Shows the weather forecast for the next or '
                             'current weekend'))
 	   

Отлично! Теперь запустите наше приложение применив флаг -w или --weekend:


>> [Fri SEP 29]
    High 13.9° / Low 8.9° (Partly Cloudy)
    Wind: ESE 10 mph / Humidity: 79%

>> [Sat SEP 30]
    High 13.9° / Low 9.4° (Partly Cloudy)
    Wind: SE 10 mph / Humidity: 77%

>> [Sun OCT 1]
    High 12.8° / Low 10.6° (Cloudy)
    Wind: SE 14 mph / Humidity: 74%
		

Замечу, что на этот раз я применяю флаг -u с тем чтобы выбрадь градусы Цельсия вместо Фаренгейта.

Выводы

В этой главе мы изучили необходимые основы объектно ориентированного программирования в Python; мы обсудили как создавать классы, применять наследование и использовать декораторы @property для создания получателей и установщиков (getter и setter).

Мы рассмотрели как применять обследование модуля для получения дополнительной информации о модулях, классах и функциях. Последнее, но не менее важное, мы воспользовались мощным пакетом Beautifulsoup для синтаксического разбора HTML и Selenium для выполнения запросов к вебсайту погоды.

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

Далее мы собираемся разработать небольшую обёртку вокруг Spotify Rest API и применить её для создания некоего терминала удалённого управления.