2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM 每小時自動更新 中文 EN
LIVE 快訊
← 返回網站更新紀錄 Site Changelog

提交原始紀錄

加入 Aerospace Global News 資料來源 新增主動抓取來源、原站新聞解析器與回歸測試。

SHA
228aef5a95a2c480cda8ce067071be537e398c30
提交時間
作者
nnneeeooo-nnneeeooo
提交者
web-flow

變更檔案 (3)

  • pipeline/common.py modified · +12 / −0
  • pipeline/fetch.py modified · +54 / −0
  • tests/test_aerospace_global_news.py added · +96 / −0

完整差異

@@ -243,6 +243,18 @@ def group_has_material(group: dict) -> bool:\n             "en": "Global incident tracking",\n         },\n     },\n+    "aerospaceglobalnews": {\n+        "name": "Aerospace Global News",\n+        "kind": "media",\n+        "fmt": "HTML",\n+        "url": "https://aerospaceglobalnews.com/",\n+        "endpoint": "https://aerospaceglobalnews.com/latest-news/",\n+        "type": "html",\n+        "cover": {\n+            "zh": "全球航太、民航、國防、太空與技術新聞",\n+            "en": "Global aerospace, air transport, defence, space & technology news",\n+        },\n+    },\n     "flightaware": {\n         "name": "FlightAware",\n         "kind": "data",\n@@ -386,6 +386,59 @@ def _parse_eurocontrol(soup: BeautifulSoup, base: str) -> list[dict]:\n     return items\n \n \n+def _agn_date_after(anchor, base: str) -> datetime | None:\n+    """Find the date following an AGN listing link without crossing cards."""\n+    current_url = urljoin(base, str(anchor.get("href") or ""))\n+    for index, node in enumerate(anchor.next_elements):\n+        if index >= 60:\n+            break\n+        if getattr(node, "name", None) == "a":\n+            href = str(node.get("href") or "")\n+            next_url = urljoin(base, href)\n+            if (re.fullmatch(r"/news/[a-z0-9][a-z0-9-]+/?",\n+                             urlsplit(next_url).path)\n+                    and next_url != current_url):\n+                break\n+        if isinstance(node, str):\n+            published = _date_from_text(node)\n+            if published is not None:\n+                return published\n+    return None\n+\n+\n+def _parse_aerospaceglobalnews(soup: BeautifulSoup, base: str) -> list[dict]:\n+    """Aerospace Global News latest-news listing.\n+\n+    The page repeats some article links for the image and headline.  Keep one\n+    dated /news/<slug>/ entry per URL and ignore navigation, topic and\n+    undated recommendation links.\n+    """\n+    items, seen = [], set()\n+    path_re = re.compile(r"^/news/[a-z0-9][a-z0-9-]+/?$")\n+    base_host = urlsplit(base).netloc.lower()\n+    for anchor in soup.find_all("a", href=True):\n+        url = urljoin(base, str(anchor["href"]))\n+        parts = urlsplit(url)\n+        if parts.netloc.lower() != base_host or not path_re.fullmatch(parts.path):\n+            continue\n+        title = _collapse(anchor.get("title") or anchor.get_text(" ", strip=True))\n+        if url in seen or len(title) < 12 or title.lower() == "news":\n+            continue\n+        published = _agn_date_after(anchor, base)\n+        if published is None:\n+            continue\n+        seen.add(url)\n+        image = anchor.find("img", src=True)\n+        items.append({\n+            "title": title,\n+            "url": url,\n+            "published": published,\n+            "summary": "",\n+            "image": urljoin(base, image["src"]) if image else None,\n+        })\n+    return items\n+\n+\n def _parse_airbus(soup: BeautifulSoup, base: str) -> list[dict]:\n     """Airbus newsroom cards: official stories and press releases.\n \n@@ -428,6 +481,7 @@ def _parse_airbus(soup: BeautifulSoup, base: str) -> list[dict]:\n     "faa": _parse_faa,\n     "icao": _parse_icao,\n     "eurocontrol": _parse_eurocontrol,\n+    "aerospaceglobalnews": _parse_aerospaceglobalnews,\n     "airbus": _parse_airbus,\n }\n \n@@ -0,0 +1,96 @@\n+import sys\n+import unittest\n+from pathlib import Path\n+\n+from bs4 import BeautifulSoup\n+\n+ROOT = Path(__file__).resolve().parent.parent\n+sys.path.insert(0, str(ROOT / "pipeline"))\n+\n+import common  # noqa: E402\n+import fetch  # noqa: E402\n+\n+\n+AGN_HTML = """\n+<main>\n+  <article>\n+    <a href="/news/ga-ats-gives-the-do228-a-new-lease-of-life-with-nxt-launch/">\n+      GA-ATS gives the Do228 a new lease of life with NXT launch\n+    </a>\n+    <a href="/topic/do228-nxt/">Do228 NXT</a>\n+    <span>July 29, 2026</span>\n+  </article>\n+  <article>\n+    <a href="/news/ga-ats-gives-the-do228-a-new-lease-of-life-with-nxt-launch/">\n+      GA-ATS gives the Do228 a new lease of life with NXT launch\n+    </a>\n+    <span>July 29, 2026</span>\n+  </article>\n+  <article>\n+    <a href="/news/china-c919-high-altitude-variant-completes-maiden-flight/">\n+      China C919 high-altitude variant completes maiden flight\n+    </a>\n+    <span>July 28, 2026</span>\n+  </article>\n+  <aside>\n+    <a href="/news/undated-recommendation/">Undated recommendation article</a>\n+  </aside>\n+</main>\n+"""\n+\n+\n+class AerospaceGlobalNewsTests(unittest.TestCase):\n+    def test_source_is_public_direct_html_feed(self):\n+        source = common.SOURCES["aerospaceglobalnews"]\n+\n+        self.assertEqual(source["name"], "Aerospace Global News")\n+        self.assertEqual(source["kind"], "media")\n+        self.assertEqual(source["fmt"], "HTML")\n+        self.assertEqual(\n+            source["endpoint"],\n+            "https://aerospaceglobalnews.com/latest-news/",\n+        )\n+        self.assertEqual(source["type"], "html")\n+        self.assertTrue(source.get("public", True))\n+        self.assertIs(\n+            fetch.HTML_PARSERS["aerospaceglobalnews"],\n+            fetch._parse_aerospaceglobalnews,\n+        )\n+\n+    def test_latest_news_parser_keeps_unique_dated_articles(self):\n+        items = fetch._parse_aerospaceglobalnews(\n+            BeautifulSoup(AGN_HTML, "lxml"),\n+            "https://aerospaceglobalnews.com/latest-news/",\n+        )\n+\n+        self.assertEqual(\n+            [item["title"] for item in items],\n+            [\n+                "GA-ATS gives the Do228 a new lease of life with NXT launch",\n+                "China C919 high-altitude variant completes maiden flight",\n+            ],\n+        )\n+        self.assertEqual(\n+            [item["url"] for item in items],\n+            [\n+                (\n+                    "https://aerospaceglobalnews.com/news/"\n+                    "ga-ats-gives-the-do228-a-new-lease-of-life-with-nxt-launch/"\n+                ),\n+                (\n+                    "https://aerospaceglobalnews.com/news/"\n+                    "china-c919-high-altitude-variant-completes-maiden-flight/"\n+                ),\n+            ],\n+        )\n+        self.assertEqual(\n+            [item["published"].isoformat() for item in items],\n+            [\n+                "2026-07-29T00:00:00+00:00",\n+                "2026-07-28T00:00:00+00:00",\n+            ],\n+        )\n+\n+\n+if __name__ == "__main__":\n+    unittest.main()

你發現了隱藏主題!

8-bit 主題已解鎖。重新整理後,就能在主題選單選擇 8-bit。