feat: +fl.ru

This commit is contained in:
2026-07-14 15:04:31 +07:00
parent 8b0e1ffffa
commit a0f1f1ade8
6 changed files with 97 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ class Settings:
telegram_proxy_url: str | None
scheduler_interval_minutes: int
kwork_category_ids: list[int]
fl_rss_url: str | None
keywords_include: list[str]
keywords_exclude: list[str]
min_price_amount: int | None
@@ -33,6 +34,7 @@ def load_settings() -> Settings:
telegram_proxy_url=_parse_optional_text(os.getenv("TELEGRAM_PROXY_URL", "")),
scheduler_interval_minutes=int(os.getenv("SCHEDULER_INTERVAL_MINUTES", "5")),
kwork_category_ids=_parse_int_list(os.getenv("KWORK_CATEGORY_IDS", "41")),
fl_rss_url=_parse_optional_text(os.getenv("FL_RSS_URL", "https://www.fl.ru/rss/?category=5")),
keywords_include=_parse_text_list(os.getenv("KEYWORDS_INCLUDE", "")),
keywords_exclude=_parse_text_list(os.getenv("KEYWORDS_EXCLUDE", "")),
min_price_amount=_parse_optional_int(os.getenv("MIN_PRICE_AMOUNT", "")),

View File

@@ -1,4 +1,5 @@
from app.sources.base import OfferSource
from app.sources.fl import FlSource
from app.sources.kwork import KworkSource
__all__ = ["OfferSource", "KworkSource"]
__all__ = ["OfferSource", "FlSource", "KworkSource"]

72
app/sources/fl.py Normal file
View File

@@ -0,0 +1,72 @@
from __future__ import annotations
import html
from typing import Final
from urllib.parse import urlparse
from xml.etree import ElementTree
import requests
from app.models import Offer
from app.sources.base import OfferSource
class FlSource(OfferSource):
source_name = "fl"
_default_category_id: Final[int] = 5
def __init__(self, rss_url: str, timeout_seconds: int) -> None:
self._rss_url = rss_url
self._timeout_seconds = timeout_seconds
self._session = requests.Session()
self._session.headers.update(
{
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
)
}
)
def fetch_offers(self) -> list[Offer]:
response = self._session.get(self._rss_url, timeout=self._timeout_seconds)
response.raise_for_status()
root = ElementTree.fromstring(response.content)
offers: list[Offer] = []
for item in root.findall("./channel/item"):
url = _get_item_text(item, "link")
external_id = _extract_external_id(url)
if external_id is None:
continue
offers.append(
Offer(
source=self.source_name,
external_id=external_id,
title=_get_item_text(item, "title", "Без названия"),
description=_get_item_text(item, "description"),
price="Не указан",
price_amount=None,
url=url,
category_id=self._default_category_id,
published_at=_get_item_text(item, "pubDate"),
)
)
return offers
def _get_item_text(item: ElementTree.Element, tag_name: str, default: str = "") -> str:
element = item.find(tag_name)
if element is None or element.text is None:
return default
return html.unescape(element.text.strip())
def _extract_external_id(url: str) -> str | None:
path_parts = [part for part in urlparse(url).path.split("/") if part]
if len(path_parts) < 2 or path_parts[0] != "projects":
return None
return path_parts[1]