python-library-ff14-news 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. python_library_ff14_news-0.0.0/.cursor/skills/ff14_news-package-changelog/SKILL.md +17 -0
  2. python_library_ff14_news-0.0.0/.cursor/skills/ff14_news-package-design/SKILL.md +33 -0
  3. python_library_ff14_news-0.0.0/.cursor/skills/ff14_news-package-preload/SKILL.md +25 -0
  4. python_library_ff14_news-0.0.0/.gitignore +17 -0
  5. python_library_ff14_news-0.0.0/PKG-INFO +8 -0
  6. python_library_ff14_news-0.0.0/README.md +110 -0
  7. python_library_ff14_news-0.0.0/_debug_jp_list.py +11 -0
  8. python_library_ff14_news-0.0.0/_debug_jp_list2.py +17 -0
  9. python_library_ff14_news-0.0.0/_debug_jp_list3.py +13 -0
  10. python_library_ff14_news-0.0.0/_jp_detail.html +1345 -0
  11. python_library_ff14_news-0.0.0/_jp_list_ctx.txt +10 -0
  12. python_library_ff14_news-0.0.0/_jp_news__list_banner.txt +1 -0
  13. python_library_ff14_news-0.0.0/_jp_news__list_header.txt +9 -0
  14. python_library_ff14_news-0.0.0/_jp_topics_snippet.html +0 -0
  15. python_library_ff14_news-0.0.0/_probe_jp_detail.py +15 -0
  16. python_library_ff14_news-0.0.0/_probe_jp_list_only.py +29 -0
  17. python_library_ff14_news-0.0.0/_probe_jp_lodestone.py +31 -0
  18. python_library_ff14_news-0.0.0/_probe_jp_lodestone2.py +30 -0
  19. python_library_ff14_news-0.0.0/_probe_jp_lodestone3.py +48 -0
  20. python_library_ff14_news-0.0.0/example/__init__.py +0 -0
  21. python_library_ff14_news-0.0.0/example/__main__.py +105 -0
  22. python_library_ff14_news-0.0.0/example/ensure_browser.py +54 -0
  23. python_library_ff14_news-0.0.0/example/export_feed.py +140 -0
  24. python_library_ff14_news-0.0.0/example/weibo_cookie.txt.example +3 -0
  25. python_library_ff14_news-0.0.0/example.bat +11 -0
  26. python_library_ff14_news-0.0.0/ff14_news/__init__.py +25 -0
  27. python_library_ff14_news-0.0.0/ff14_news/channel_protocol.py +41 -0
  28. python_library_ff14_news-0.0.0/ff14_news/channels/__init__.py +1 -0
  29. python_library_ff14_news-0.0.0/ff14_news/channels/cn_official/__init__.py +3 -0
  30. python_library_ff14_news-0.0.0/ff14_news/channels/cn_official/channel.py +112 -0
  31. python_library_ff14_news-0.0.0/ff14_news/channels/cn_official/constants.py +13 -0
  32. python_library_ff14_news-0.0.0/ff14_news/channels/cn_official/cqnews_client.py +112 -0
  33. python_library_ff14_news-0.0.0/ff14_news/channels/cn_official/html_content.py +11 -0
  34. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/__init__.py +3 -0
  35. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/browser_cookies.py +93 -0
  36. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/channel.py +141 -0
  37. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/constants.py +10 -0
  38. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/crawl_backend.py +129 -0
  39. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/exceptions.py +2 -0
  40. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/mblog_parser.py +161 -0
  41. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/post_adapter.py +105 -0
  42. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/profile.py +10 -0
  43. python_library_ff14_news-0.0.0/ff14_news/channels/cn_weibo/proxy_url.py +14 -0
  44. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/__init__.py +3 -0
  45. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/channel.py +110 -0
  46. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/constants.py +6 -0
  47. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/detail_parser.py +128 -0
  48. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/http_client.py +16 -0
  49. python_library_ff14_news-0.0.0/ff14_news/channels/jp_official/list_parser.py +112 -0
  50. python_library_ff14_news-0.0.0/ff14_news/common/html_blocks.py +183 -0
  51. python_library_ff14_news-0.0.0/ff14_news/common/list_feed.py +20 -0
  52. python_library_ff14_news-0.0.0/ff14_news/ff14_news.py +64 -0
  53. python_library_ff14_news-0.0.0/ff14_news/models.py +74 -0
  54. python_library_ff14_news-0.0.0/pyproject.toml +18 -0
  55. python_library_ff14_news-0.0.0/test.bat +9 -0
  56. python_library_ff14_news-0.0.0/tests/__init__.py +0 -0
  57. python_library_ff14_news-0.0.0/tests/test_cn_weibo_parser.py +79 -0
  58. python_library_ff14_news-0.0.0/tests/test_cn_weibo_post_adapter.py +21 -0
  59. python_library_ff14_news-0.0.0/tests/test_cn_weibo_profile.py +5 -0
  60. python_library_ff14_news-0.0.0/tests/test_cn_weibo_proxy_url.py +14 -0
  61. python_library_ff14_news-0.0.0/tests/test_html_content.py +25 -0
  62. python_library_ff14_news-0.0.0/tests/test_integration.py +20 -0
  63. python_library_ff14_news-0.0.0/tests/test_jp_detail_parser.py +44 -0
  64. python_library_ff14_news-0.0.0/tests/test_jp_list_parser.py +26 -0
  65. python_library_ff14_news-0.0.0/tests/test_list_feed.py +20 -0
  66. python_library_ff14_news-0.0.0/update.bat +10 -0
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: ff14-news-package-changelog
3
+ description: ff14_news 包:要求与决议;最新在上。
4
+ ---
5
+
6
+ # ff14_news · Changelog
7
+
8
+ (规则见 `~/.cursor/skills/agent-project-changelog/SKILL.md`。)
9
+
10
+ ## 2026-06-04
11
+
12
+ - **决议**:默认抓取改为**列表级**(头图、标题、摘要、链接);`fetch_articles` / `fetch_articles_by_ids` 的 **`blocks` 为空**;正文用 **`fetch_article_detail`**(`fetch_article` 别名)。国服按 ID 仍调详情 JSON 但只取 Title/Summary/HomeImagePath;日文列表解析 `news__list--banner` 摘要;微博默认不拉 statuses/show。新增 **`common/list_feed.py`**;example 仅导出头图。
13
+ - **决议**:新增渠道 **`cn_weibo`**(FF14 官方微博 `@cnff14`);`channels/cn_weibo/` 独立实现;m.weibo.cn 时间线 + statuses/show;文章 `id` 为 mblog id 字符串。
14
+ - **决议**:新增渠道 **`jp_official`**(日文 Lodestone);`channels/jp_official/` 独立实现;`article.id` 改为 `str`;`category_code` 可选。
15
+ - **决议**:多渠道架构:`channels/<id>/` 分目录;门面 `FF14News.cn_official`;`NewsFeed` / `NewsArticle` 增加 `channel_id`;example 按渠道分子目录输出。
16
+ - **决议**:实现国服渠道 `cn_official`(cqnews `CategoryCode=5310`),详情转有序 `NewsContentBlock`。
17
+ - **决议**:在 monorepo 初始化空包 **ff14_news**,并建立包级三件套(预加载、设计笔记、changelog)。
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: ff14-news-package-design
3
+ description: >-
4
+ ff14_news 包:当前有效的设计意图;变更见 ff14_news-package-changelog。
5
+ ---
6
+
7
+ # ff14_news · 设计笔记
8
+
9
+ > 变更见 `.cursor/skills/ff14_news-package-changelog/SKILL.md`;矛盾以 changelog 最新为准。
10
+
11
+ ## 设计意图
12
+
13
+ - **多渠道**:各渠道在 **`ff14_news/channels/<channel_id>/`** 独立实现,逻辑不混写;共享模型在包根 `models.py`。
14
+ - **门面 `FF14News`**:以属性暴露渠道(如 **`cn_official`**),`channel(id)` 按标识取用。
15
+ - **默认抓取粒度**:与旧 Selenium 列表一致——头图、标题、摘要、详情页链接;**`fetch_articles` / `fetch_articles_by_ids` 的 `blocks` 为空**。
16
+ - **正文**:各渠道 **`fetch_article_detail`**(`fetch_article` 别名);国服 `html_content`、日文 `detail_parser`、微博 `mblog_parser` 保留,仅默认路径不调用。
17
+ - **列表转文章**:`ff14_news/common/list_feed.py` 的 **`article_from_list_item`**。
18
+ - **渠道 `cn_official`**:列表 `newsList`;按 ID 补全用详情 JSON 的 Title/Summary/HomeImagePath,不 `html_to_blocks`。
19
+ - **渠道 `cn_weibo`**:时间线 `mblog_to_list_item`;按 ID 先扫时间线,否则 statuses/show 仅取列表级字段。
20
+ - **渠道 `jp_official`**:列表含 `news__list--banner` 摘要(纯文本 ≤200);按 ID 先扫列表,否则 `parse_detail_metadata`。
21
+ - 结构化输出:**`NewsFeed` / `NewsArticle` / `NewsContentBlock`**(Pydantic v2),均带 **`channel_id`**。
22
+
23
+ ## 硬性要求
24
+
25
+ - 包目录名 **`ff14_news`**(snake_case);PyPI 名 **`python-library-ff14-news`**。
26
+ - 新渠道须新建子目录 + 门面属性,禁止把多站逻辑堆在同一文件。
27
+ - 正文块保持 DOM 阅读顺序;去掉 `style`/`script` 与常见分享/版权噪音(仅 **`fetch_article_detail`** 路径)。
28
+ - 中文禁用词见 `~/.cursor/skills/forbidden-doc-comment-vocabulary/SKILL.md`。
29
+
30
+ ## 备忘与待定
31
+
32
+ - 第二渠道及以后:在 `channels/` 下追加子包并在 `FF14News` 注册。
33
+ - 调度、缓存由外层任务包组合各渠道的 `fetch_articles`。
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: ff14-news-package-preload
3
+ description: ff14_news 包:会话预加载;改本包源码前 Read。
4
+ ---
5
+
6
+ # ff14_news · 会话预加载
7
+
8
+ ## 初始化加载(Session preload)
9
+
10
+ 在 **`packages/ff14_news/`** 下改源码、测试或 `pyproject.toml` 时,按序 **Read**:
11
+
12
+ 1. `~/.cursor/skills/project-skill-manifest-policy/SKILL.md`
13
+ 2. `~/.cursor/skills/forbidden-doc-comment-vocabulary/SKILL.md`
14
+ 3. `~/.cursor/skills/markdown-authoring-zh/SKILL.md`
15
+ 4. `~/.cursor/skills/python-project-ai/SKILL.md`
16
+ 5. `~/.cursor/skills/python-doc-comments/SKILL.md`
17
+ 6. `~/.cursor/skills/agent-codegen-self-review/SKILL.md`
18
+ 7. `.cursor/skills/ff14_news-package-design/SKILL.md`
19
+ 8. `.cursor/skills/ff14_news-package-changelog/SKILL.md`
20
+
21
+ 整库约定见仓库根 `.cursor/skills/python-library-session-manifest/SKILL.md`。
22
+
23
+ ## 用过的 skill(追加记录)
24
+
25
+ (初始为空。)
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+
12
+ # example 抓取产物(含下载图片)
13
+ example/output/
14
+
15
+ # 微博登录 Cookie / Playwright 会话(本地私密)
16
+ example/weibo_cookie.txt
17
+ example/.weibo_browser_state.json
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-library-ff14-news
3
+ Version: 0.0.0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: crawl4weibo>=0.1.0
6
+ Requires-Dist: pydantic>=2.0
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -0,0 +1,110 @@
1
+ # python-library-ff14-news
2
+
3
+ 多渠道 FF14 新闻抓取;各渠道代码分目录实现,输出统一的 `NewsFeed` / `NewsArticle`。
4
+
5
+ ## 设计特性
6
+
7
+ ### 渠道
8
+
9
+ | 标识 | 目录 | 说明 |
10
+ | --- | --- | --- |
11
+ | `cn_official` | `ff14_news/channels/cn_official/` | [国服官网](https://ff.web.sdo.com/web8/index.html#/newstab/newslist),cqnews JSON(与 SPA 同源,非 Selenium) |
12
+ | `cn_weibo` | `ff14_news/channels/cn_weibo/` | [FF14 官方微博](https://weibo.com/cnff14),m.weibo.cn 时间线 |
13
+ | `jp_official` | `ff14_news/channels/jp_official/` | [日文 Lodestone トピックス](https://jp.finalfantasyxiv.com/lodestone/topics/),列表 `news__list--*` |
14
+
15
+ 新渠道在 `ff14_news/channels/<id>/` 下单独实现,并在门面 `FF14News` 上挂同名属性。
16
+
17
+ ### 返回结构
18
+
19
+ - `NewsFeed`:`channel_id`、列表 URL、`articles`(顺序与对应站点列表一致)。
20
+ - `NewsArticle`:列表级字段(`channel_id`、标题、摘要、头图、`source_page_url`);默认 `blocks` 为空。
21
+ - 正文块:调用各渠道 `fetch_article_detail`(`fetch_article` 为别名)后 `blocks` 才有内容。
22
+
23
+ ### 国服官网(cn_official)
24
+
25
+ - 列表栏目 `CategoryCode=5310`;详情页链接 `newscont/{id}`。
26
+ - `fetch_articles` / `fetch_articles_by_ids`:列表或详情 JSON 的标题、摘要、头图,不解析 HTML 正文。
27
+ - `fetch_article_detail`:详情 `Content` 转有序 `NewsContentBlock`。
28
+
29
+ ### 日文官网(jp_official)
30
+
31
+ - 列表:`news__list--header`(标题/时间)、`news__list--banner`(摘要,纯文本最多 200 字)、`news__list--img`。
32
+ - `fetch_articles`:仅列表;`fetch_articles_by_ids`:先扫列表,未命中再拉详情元数据(无 blocks)。
33
+ - `fetch_article_detail`:详情页 `news__detail__wrapper` 全文块。
34
+ - 文章 `id` 为 Lodestone 十六进制串(非数字)。
35
+
36
+ ### 官方微博(cn_weibo)
37
+
38
+ - 账号 `@cnff14`(uid `1784473157`);列表自 m.weibo.cn 用户微博 tab。
39
+ - 反爬:依赖 [crawl4weibo](https://pypi.org/project/crawl4weibo/) + **Playwright Chromium**(`example.bat` 默认经代理 `127.0.0.1:7897` 安装内核并取 Cookie)。
40
+ - 默认无 Cookie 时由 Playwright 打开移动端页获取会话;API 经 `add_proxy` 走同一代理。也可手动配置 `cn_weibo_cookie` 或 `example/weibo_cookie.txt`。
41
+ - `fetch_articles`:时间线列表级字段,不拉长文展开。
42
+ - `fetch_articles_by_ids`:时间线查找或单条元数据,无 blocks。
43
+ - `fetch_article_detail`:完整正文(含长文展开)。
44
+ - 文章 `id` 为微博 mblog id 字符串。
45
+
46
+ ## 配置
47
+
48
+ | 项 | 说明 |
49
+ | --- | --- |
50
+ | `FF14News().cn_official.category_code` | 列表栏目,默认 `5310` |
51
+ | `FF14News(cn_official_timeout_seconds=…)` | 国服渠道 HTTP 超时 |
52
+ | `FF14News(cn_weibo_timeout_seconds=…)` | 微博渠道 HTTP 超时(默认 60s) |
53
+ | `FF14News(cn_weibo_cookie=…)` | 微博 Cookie 整串(跳过 Playwright 取 Cookie) |
54
+ | `FF14News(cn_weibo_cookie_storage_path=…)` | Playwright 会话缓存路径 |
55
+ | `FF14News(cn_weibo_browser_headless=…)` | 自动取 Cookie 时是否无头浏览器(默认 True) |
56
+ | `FF14News(cn_weibo_proxy_url=…)` | 微博 HTTP 代理,如 `127.0.0.1:7897` |
57
+ | `FF14News(jp_official_timeout_seconds=…)` | 日文渠道 HTTP 超时(默认 120s) |
58
+
59
+ ## 使用
60
+
61
+ ```python
62
+ from ff14_news import FF14News
63
+
64
+ news = FF14News()
65
+
66
+ # 列表级(blocks 为空)
67
+ feed = news.cn_official.fetch_articles(limit=2)
68
+ feed_wb = news.cn_weibo.fetch_articles(limit=2)
69
+ feed_jp = news.jp_official.fetch_articles(limit=2)
70
+ feed = news.channel("cn_official").fetch_articles_by_ids(["387965"])
71
+
72
+ # 单篇正文
73
+ article = news.cn_official.fetch_article_detail("387965")
74
+
75
+ print(feed.model_dump_json(indent=2, ensure_ascii=False))
76
+ ```
77
+
78
+ ## 示例
79
+
80
+ `example.bat` 默认抓取**全部渠道、每渠道 2 条**,按渠道分子目录,仅下载头图:
81
+
82
+ ```
83
+ example/output/
84
+ cn_official/
85
+ cn_weibo/
86
+ jp_official/
87
+ feed.json
88
+ {id}/article.json
89
+ {id}/article.md
90
+ {id}/images/cover.*
91
+ ```
92
+
93
+ ```bat
94
+ example.bat
95
+ example.bat --proxy 127.0.0.1:7897
96
+ example.bat -c cn_official -n 5
97
+ example.bat --proxy ""
98
+ ```
99
+
100
+ 首次运行会经代理下载 Chromium(约 180MB)。代理默认 `127.0.0.1:7897`,与本地 Clash 等一致时可不改。若仍失败,可改用手动 Cookie:`example/weibo_cookie.txt`。
101
+
102
+ ## 测试
103
+
104
+ ```bat
105
+ pip install -e ".[dev]"
106
+ pytest
107
+
108
+ set FF14_NEWS_INTEGRATION=1
109
+ pytest tests/test_integration.py
110
+ ```
@@ -0,0 +1,11 @@
1
+ import re
2
+
3
+ from ff14_news.channels.jp_official.http_client import fetch_html
4
+ from ff14_news.channels.jp_official.list_parser import topics_list_url
5
+
6
+ html = fetch_html(topics_list_url(0), timeout_seconds=120)
7
+ print("topics count", html.count("news__list--topics"))
8
+ pat = r'<li class="news__list--topics[^"]*">'
9
+ print("split", len(re.split(pat, html)))
10
+ idx = html.find("news__list--topics")
11
+ print("sample", repr(html[idx : idx + 100]))
@@ -0,0 +1,17 @@
1
+ import re
2
+
3
+ from ff14_news.channels.jp_official.http_client import fetch_html
4
+ from ff14_news.channels.jp_official.list_parser import (
5
+ _parse_item_fragment,
6
+ topics_list_url,
7
+ )
8
+
9
+ html = fetch_html(topics_list_url(0), timeout_seconds=120)
10
+ pat = re.compile(r'<li class="news__list--topics[^"]*">')
11
+ frags = pat.split(html)
12
+ print("frags", len(frags))
13
+ frag = frags[1]
14
+ print("frag len", len(frag))
15
+ print("id re", bool(re.search(r"/lodestone/topics/detail/([a-f0-9]+)/", frag)))
16
+ row = _parse_item_fragment(frag)
17
+ print("row", row)
@@ -0,0 +1,13 @@
1
+ import re
2
+
3
+ from ff14_news.channels.jp_official.http_client import fetch_html
4
+ from ff14_news.channels.jp_official.list_parser import topics_list_url
5
+
6
+ html = fetch_html(topics_list_url(0), timeout_seconds=120)
7
+ pat = re.compile(r'<li class="news__list--topics[^"]*">')
8
+ frag = pat.split(html)[1]
9
+ links = re.findall(r"/lodestone/topics/detail/[^\"']+", frag)
10
+ print("links", links[:3])
11
+ # maybe uppercase
12
+ links2 = re.findall(r"/lodestone/topics/detail/([A-Fa-f0-9]+)", frag)
13
+ print("ids", links2[:3])