refactor(yandexweather): rework yandexweather parser

This commit is contained in:
2026-09-17 20:59:29 +03:00
parent ece1a58a27
commit 2e9d04c10e
3 changed files with 176 additions and 102 deletions
+6 -22
View File
@@ -1,9 +1,7 @@
import datetime import datetime
import json
import logging import logging
from aiocache import cached from aiocache import cached
from bs4 import BeautifulSoup
from gallery.sketch.source import ApiSource from gallery.sketch.source import ApiSource
from gallery.sketch.weather.api import WeatherApi from gallery.sketch.weather.api import WeatherApi
@@ -14,7 +12,7 @@ from gallery.sketch.weather.model import (
from gallery.sketch.weather.util import merge_weather_values from gallery.sketch.weather.util import merge_weather_values
from gallery.util import TimeUnit from gallery.util import TimeUnit
from .parser import parse_article from .parser import YandexForecastParser, YandexLocationParser
logger = logging.getLogger("yandexweather") logger = logging.getLogger("yandexweather")
@@ -22,6 +20,9 @@ logger = logging.getLogger("yandexweather")
class YandexWeatherApi(WeatherApi): class YandexWeatherApi(WeatherApi):
PROVIDER = "yandex" PROVIDER = "yandex"
forecast_parser = YandexForecastParser()
location_parser = YandexLocationParser()
def _build_source(self) -> ApiSource: def _build_source(self) -> ApiSource:
return ApiSource("https://yandex.ru") return ApiSource("https://yandex.ru")
@@ -33,29 +34,12 @@ class YandexWeatherApi(WeatherApi):
async def _get_location_forecast(self, location_id: str) -> list[WeatherResponse]: async def _get_location_forecast(self, location_id: str) -> list[WeatherResponse]:
endpoint = f"pogoda/ru/{location_id}/details" endpoint = f"pogoda/ru/{location_id}/details"
content = await self.source.request(endpoint, mode="browser") content = await self.source.request(endpoint, mode="browser")
soup = BeautifulSoup(content, features="html.parser") return self.forecast_parser.parse(content)
location = soup.select_one("[class^=Suggest_placeholder]").text.strip()
articles = soup.select("article[class^=AppForecastDay]")
return [parse_article(article, location) for article in articles]
async def find_locations(self, query: str) -> list[Location]: async def find_locations(self, query: str) -> list[Location]:
endpoint = f"weather/api/suggest?part={query}&lang=ru&type=weather" endpoint = f"weather/api/suggest?part={query}&lang=ru&type=weather"
response = await self.source.request(endpoint) response = await self.source.request(endpoint)
data = json.loads(response) return self.location_parser.parse(response)
return [
Location(
id=str(item["geoId"]),
name=item["title"],
provider=self.provider,
lat=item["coords"]["lat"],
lon=item["coords"]["lon"],
country="ru",
country_code="ru",
district=item["description"],
subdistrict="",
)
for item in data
]
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
forecast = await self._get_location_forecast(location_id) forecast = await self._get_location_forecast(location_id)
+140 -50
View File
@@ -1,29 +1,70 @@
import datetime
import json
import logging import logging
from enum import Enum
import dateparser from bs4 import BeautifulSoup, Tag
from bs4 import Tag
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WeatherResponse from gallery.sketch.parse.core import Parser
from gallery.sketch.weather.parser import ParseError, parse_wind_direction from gallery.sketch.parse.date import parse_date
from gallery.sketch.weather.util import build_weather_value from gallery.sketch.parse.html import TableParser, TableValueParser, TagParser
from gallery.sketch.weather.model import (
Cloudness,
Location,
Precipitation,
Sky,
WeatherResponse,
WeatherValue,
WindDirection,
)
from gallery.sketch.weather.parser import parse_wind_direction
logger = logging.getLogger("yandexweather") logger = logging.getLogger("yandexweather")
class DayPart(Enum): class LocationNameParser(TagParser[str]):
MORNING = ("Утром", 6) selector = "[class^=Suggest_placeholder]"
AFTERNOON = ("Днём", 12)
EVENING = ("Вечером", 18)
NIGHT = ("Ночью", 0)
def __init__(self, title: str, hour: int): def extract(self, data: Tag) -> str:
super().__init__() return data.text.strip()
self.title = title
self.hour = hour
def parse_sky(sky: str) -> Sky: class DateParser(TagParser[datetime.date]):
selector = "[class^=AppForecastDayHeader_dayTitle]"
def extract(self, data: Tag) -> datetime.date:
return parse_date(data.text.split(",")[-1].strip())
class TimeParser(TableValueParser[datetime.datetime]):
selector = "[style$='-part']"
model_key = "date"
HOUR_MAP = {
"Утром": 6,
"Днём": 12,
"Вечером": 18,
"Ночью": 0,
}
def extract(self, data: Tag) -> datetime.datetime:
hour = self.HOUR_MAP[data.text.strip()]
return datetime.datetime.min.replace(hour=hour)
class TemperatureParser(TableValueParser[list[int]]):
selector = "[style$='-temp']"
model_key = "temperature"
def extract(self, data: Tag) -> list[int]:
return [int(data.text.strip().rstrip("°"))]
class SkyParser(TableValueParser[Sky]):
selector = "[style$='-text']"
model_key = "sky"
def extract(self, data: Tag) -> Sky:
sky = data.text.strip()
cloudness = Cloudness.CLEAR cloudness = Cloudness.CLEAR
precipitation = Precipitation.NO precipitation = Precipitation.NO
thunder = False thunder = False
@@ -52,40 +93,89 @@ def parse_sky(sky: str) -> Sky:
) )
def parse_article(article: Tag, location: str) -> WeatherResponse: class WindSpeedParser(TableValueParser[int]):
date_str = article.select_one("[class^=AppForecastDayHeader_dayTitle]").text.split(",")[-1].strip() selector = "[style$='-wind']:not([class*=AppForecastDayPart_wind_formatted])"
date = dateparser.parse(date_str, languages=["ru"]) model_key = "wind.speed"
if date is None:
raise ParseError.invalid_date(date_str) def extract(self, data: Tag) -> int:
values = [] return int(data.text.strip())
for day_part in DayPart:
value = build_weather_value(date.replace(hour=day_part.hour))
day_part_tag = article.find(string=day_part.title) class WindDirectionParser(TableValueParser[WindDirection | None]):
temperature_tag = day_part_tag.next_element selector = "[style$='-dir']"
temperature = int(temperature_tag.text.strip().rstrip("°")) model_key = "wind.direction"
value.temperature.append(temperature)
sky_tag = temperature_tag.next_element.next_element.next_element def extract(self, data: Tag) -> WindDirection | None:
sky_str = sky_tag.text.strip() return parse_wind_direction(data.text.strip())
value.sky = parse_sky(sky_str)
temperature_tag = sky_tag.next_element.next_element
temperature = int(temperature_tag.text.strip().rstrip("°")) class HumidityParser(TableValueParser[int]):
value.temperature.append(temperature) selector = "[style$='-hum']"
wind_speed_tag = temperature_tag.next_element.next_element model_key = "humidity"
wind_speed = int(wind_speed_tag.text.strip())
value.wind.speed = wind_speed def extract(self, data: Tag) -> int:
wind_direction_tag = wind_speed_tag.next_element.next_element.next_element.next_element return int(data.text.strip().rstrip("%"))
wind_direction_str = wind_direction_tag.text.strip()
value.wind.direction = parse_wind_direction(wind_direction_str)
humidity_tag = wind_direction_tag.next_element.next_element.next_element.next_element class PressureParser(TableValueParser[list[int]]):
humidity = int(humidity_tag.text.strip().rstrip("%")) selector = "[style$='-press']"
value.humidity = humidity model_key = "pressure"
pressure_tag = humidity_tag.next_element.next_element
pressure = int(pressure_tag.text.strip()) def extract(self, data: Tag) -> list[int]:
value.pressure.append(pressure) return [int(data.text.strip().rstrip("%"))]
values.append(value)
return WeatherResponse(
class YandexTableParser(TableParser[WeatherValue]):
selector = ""
value_parsers = [
TimeParser(),
TemperatureParser(),
SkyParser(),
WindSpeedParser(),
WindDirectionParser(),
HumidityParser(),
PressureParser(),
]
def create_model(self, data: dict) -> WeatherValue:
return WeatherValue.model_validate(data)
class YandexForecastParser(Parser[str, list[WeatherResponse]]):
table_parser = YandexTableParser()
date_parser = DateParser()
location_parser = LocationNameParser()
def parse(self, data: str) -> list[WeatherResponse]:
soup = BeautifulSoup(data, features="html.parser")
location = self.location_parser.parse(soup)
articles = soup.select("article[class^=AppForecastDay]")
return [
WeatherResponse(
location=location, location=location,
date=date, date=self.date_parser.parse(item),
period="day", period="day",
values=values, values=self.table_parser.parse(item),
) )
for item in articles
]
class YandexLocationParser(Parser[str, list[Location]]):
def parse(self, data: str) -> list[Location]:
content = json.loads(data)
return [
Location(
id=str(item["geoId"]),
name=item["title"],
provider="yandex", # TODO: provider module const
lat=item["coords"]["lat"],
lon=item["coords"]["lon"],
country="ru",
country_code="ru",
district=item["description"],
subdistrict="",
)
for item in content
]
+1 -1
View File
@@ -13,7 +13,7 @@ class TagParser(Parser[Tag, R], Generic[R]):
raise NotImplementedError raise NotImplementedError
def parse(self, data: Tag) -> R: def parse(self, data: Tag) -> R:
tag = data.select_one(self.selector) tag = data.select_one(self.selector) if self.selector else data
if tag is None: if tag is None:
raise ParseError("Tag not found", self.selector) raise ParseError("Tag not found", self.selector)
return self.extract(tag) return self.extract(tag)