wechat-article-parser 0.0.16__tar.gz → 0.0.17__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.
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/PKG-INFO +2 -1
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/README.md +1 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/docs/api.md +3 -2
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/pyproject.toml +1 -1
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/models.py +1 -2
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_article_urls.py +11 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_models.py +7 -4
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_parser.py +2 -1
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/.gitignore +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/LICENSE +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/docs/content-rules.md +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/docs/development.md +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/__init__.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/parser.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/conftest.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_fullscreen_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_markdown.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_miniprogram_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_paid_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_product_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_profile_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_redpacket_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_repost_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_svg_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_swiper_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_type_routing.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_user_name.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_video_content.py +0 -0
- {wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_vote_content.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: wechat-article-parser
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.17
|
|
4
4
|
Summary: WeChat MP article parser - extract metadata and content from WeChat public account articles
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -117,6 +117,7 @@ print(result.raw_html)
|
|
|
117
117
|
- 明确识别的 SVG 快速逐帧动画仅保留首帧;轮播及无法确认的帧序列保留可提取图片。具体规则见[正文转换规则](docs/content-rules.md)。
|
|
118
118
|
- 视频、音频分享可以没有文字附言。付费预览及部分卡片可能只返回文字提示;`is_valid=True` 不代表已获取付费全文、媒体文件或小程序内部内容。
|
|
119
119
|
- 页面结构变化或内容不可访问时,可能返回不完整结果。调用方应检查 `is_valid`;长链接的 `article_id` 为空属于正常情况。
|
|
120
|
+
- **自 2026-11-02 起,微信短链接文章页不再提供文章签名 `sn`,返回的 `article_sn` 为空字符串,不影响 `is_valid` 判断。**
|
|
120
121
|
- 检测到微信人机验证时抛出 `WeChatVerifyError`;HTTP 状态异常和网络错误通过 `httpx` 异常向外传播。接口不自动重试,处理示例见 [API 参考](docs/api.md#异常与不完整结果)。
|
|
121
122
|
|
|
122
123
|
## 文档
|
|
@@ -102,6 +102,7 @@ print(result.raw_html)
|
|
|
102
102
|
- 明确识别的 SVG 快速逐帧动画仅保留首帧;轮播及无法确认的帧序列保留可提取图片。具体规则见[正文转换规则](docs/content-rules.md)。
|
|
103
103
|
- 视频、音频分享可以没有文字附言。付费预览及部分卡片可能只返回文字提示;`is_valid=True` 不代表已获取付费全文、媒体文件或小程序内部内容。
|
|
104
104
|
- 页面结构变化或内容不可访问时,可能返回不完整结果。调用方应检查 `is_valid`;长链接的 `article_id` 为空属于正常情况。
|
|
105
|
+
- **自 2026-11-02 起,微信短链接文章页不再提供文章签名 `sn`,返回的 `article_sn` 为空字符串,不影响 `is_valid` 判断。**
|
|
105
106
|
- 检测到微信人机验证时抛出 `WeChatVerifyError`;HTTP 状态异常和网络错误通过 `httpx` 异常向外传播。接口不自动重试,处理示例见 [API 参考](docs/api.md#异常与不完整结果)。
|
|
106
107
|
|
|
107
108
|
## 文档
|
|
@@ -86,7 +86,7 @@ result = parse(
|
|
|
86
86
|
| `article_url_type` | `ArticleURLType` | 输入链接类型:`short`、`long` 或 `unknown`(默认);可从包顶层导入枚举,支持字符串比较和 JSON 序列化 |
|
|
87
87
|
| `article_msg_id` | `int` | 文章所在的群发消息 ID |
|
|
88
88
|
| `article_idx` | `int` | 群发图文中的位置(从 1 开始) |
|
|
89
|
-
| `article_sn` | `str` |
|
|
89
|
+
| `article_sn` | `str` | 文章签名(防伪校验);页面未提供时为空字符串,不影响 `is_valid` |
|
|
90
90
|
| `article_title` | `str` | 文章标题 |
|
|
91
91
|
| `article_cover_image` | `str` | 文章封面图链接 |
|
|
92
92
|
| `article_description` | `str` | 文章摘要 |
|
|
@@ -99,7 +99,8 @@ result = parse(
|
|
|
99
99
|
| `images` | `list[str]` | 最终保留的正文图片链接,按顺序去重;不包含被省略的动画帧、小程序封面和头像、红包封面预览图 |
|
|
100
100
|
| `is_valid` | `bool` | 关键字段是否全部解析成功(属性) |
|
|
101
101
|
|
|
102
|
-
`is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_msg_id`、`article_idx`、`
|
|
102
|
+
`is_valid` 为 `True` 的条件:`mp_id`、`mp_name`、`article_msg_id`、`article_idx`、`article_title`、`article_publish_time` 均不为空/零,且 `article_markdown` 非空或 `article_display_type in ("video", "audio")`。
|
|
103
|
+
`article_sn` 为非必需字段,页面未提供签名时仍可返回有效结果;这不改变输入长链接的参数要求,详见[链接识别](#链接识别)。
|
|
103
104
|
此外,`article_url_type == "long"` 时允许 `article_id` 为空;短链接和未识别链接仍要求 `article_id` 非空。
|
|
104
105
|
视频和音频分享允许没有文字正文,仍须满足其他关键字段要求;有效性不表示已获取视频或音频文件。
|
|
105
106
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wechat-article-parser"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.17"
|
|
8
8
|
description = "WeChat MP article parser - extract metadata and content from WeChat public account articles"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/models.py
RENAMED
|
@@ -98,14 +98,13 @@ class ArticleResult:
|
|
|
98
98
|
|
|
99
99
|
@property
|
|
100
100
|
def is_valid(self) -> bool:
|
|
101
|
-
"""
|
|
101
|
+
"""检查关键字段是否解析成功;签名可为空,视频和音频分享允许正文为空。"""
|
|
102
102
|
return bool(
|
|
103
103
|
self.mp_id
|
|
104
104
|
and self.mp_name
|
|
105
105
|
and (self.article_id or self.article_url_type == ArticleURLType.LONG)
|
|
106
106
|
and self.article_msg_id
|
|
107
107
|
and self.article_idx
|
|
108
|
-
and self.article_sn
|
|
109
108
|
and self.article_title
|
|
110
109
|
and (
|
|
111
110
|
self.article_markdown
|
|
@@ -50,6 +50,17 @@ def test_long_url_parses_without_article_id(scheme: str, suffix: str) -> None:
|
|
|
50
50
|
assert result.is_valid
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
@pytest.mark.parametrize("url", [_SHORT_URL, _LONG_URL])
|
|
54
|
+
@pytest.mark.parametrize("declaration", ['var sn = "" || "" || "";', ""])
|
|
55
|
+
def test_page_without_signature_is_valid(url: str, declaration: str) -> None:
|
|
56
|
+
html = _HTML.replace(f'var sn = "{_PARAMS["sn"]}";', declaration)
|
|
57
|
+
result = parser._parse_html(url, html)
|
|
58
|
+
|
|
59
|
+
assert result.article_sn == ""
|
|
60
|
+
assert result.article_markdown == "正文"
|
|
61
|
+
assert result.is_valid
|
|
62
|
+
|
|
63
|
+
|
|
53
64
|
@pytest.mark.parametrize("key", _PARAMS)
|
|
54
65
|
@pytest.mark.parametrize("value", [None, "", " \t\n"])
|
|
55
66
|
def test_long_url_requires_each_parameter_with_value(key: str, value: str | None) -> None:
|
|
@@ -22,11 +22,13 @@ _COMPLETE = ArticleResult(
|
|
|
22
22
|
|
|
23
23
|
@pytest.mark.parametrize("display_type", list(ArticleDisplayType))
|
|
24
24
|
@pytest.mark.parametrize("markdown", ["", "正文"])
|
|
25
|
+
@pytest.mark.parametrize("signature", ["", "signature"])
|
|
25
26
|
def test_only_video_and_audio_can_be_valid_without_markdown(
|
|
26
|
-
display_type: ArticleDisplayType, markdown: str,
|
|
27
|
+
display_type: ArticleDisplayType, markdown: str, signature: str,
|
|
27
28
|
) -> None:
|
|
28
29
|
result = replace(
|
|
29
30
|
_COMPLETE, article_display_type=display_type, article_markdown=markdown,
|
|
31
|
+
article_sn=signature,
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
assert result.is_valid is (
|
|
@@ -42,7 +44,6 @@ def test_only_video_and_audio_can_be_valid_without_markdown(
|
|
|
42
44
|
("article_id", ""),
|
|
43
45
|
("article_msg_id", 0),
|
|
44
46
|
("article_idx", 0),
|
|
45
|
-
("article_sn", ""),
|
|
46
47
|
("article_title", ""),
|
|
47
48
|
("article_publish_time", 0),
|
|
48
49
|
],
|
|
@@ -103,12 +104,14 @@ def test_paid_field_serializes_as_boolean(is_paid: bool) -> None:
|
|
|
103
104
|
@pytest.mark.parametrize("url_type", list(ArticleURLType))
|
|
104
105
|
@pytest.mark.parametrize("display_type", list(ArticleDisplayType))
|
|
105
106
|
@pytest.mark.parametrize("markdown", ["", "正文"])
|
|
107
|
+
@pytest.mark.parametrize("signature", ["", "signature"])
|
|
106
108
|
def test_only_long_url_can_omit_article_id(
|
|
107
|
-
url_type: ArticleURLType, display_type: ArticleDisplayType, markdown: str,
|
|
109
|
+
url_type: ArticleURLType, display_type: ArticleDisplayType, markdown: str, signature: str,
|
|
108
110
|
) -> None:
|
|
109
111
|
result = replace(
|
|
110
112
|
_COMPLETE, article_id="", article_url_type=url_type,
|
|
111
113
|
article_display_type=display_type, article_markdown=markdown,
|
|
114
|
+
article_sn=signature,
|
|
112
115
|
)
|
|
113
116
|
assert result.is_valid is (
|
|
114
117
|
url_type == ArticleURLType.LONG
|
|
@@ -120,7 +123,7 @@ def test_only_long_url_can_omit_article_id(
|
|
|
120
123
|
("field_name", "empty_value"),
|
|
121
124
|
[
|
|
122
125
|
("mp_id", 0), ("mp_name", ""), ("article_msg_id", 0),
|
|
123
|
-
("article_idx", 0), ("
|
|
126
|
+
("article_idx", 0), ("article_title", ""),
|
|
124
127
|
("article_publish_time", 0),
|
|
125
128
|
],
|
|
126
129
|
)
|
|
@@ -57,6 +57,8 @@ TEST_URLS = [
|
|
|
57
57
|
"https://mp.weixin.qq.com/s/CgWnsf3lyBijzy3L5Y7Xtg",
|
|
58
58
|
# foreignObject 定位且帧间距大于图片尺寸的 SVG 背景帧序列。
|
|
59
59
|
"https://mp.weixin.qq.com/s/6pT1xECveLM7iTqiYvjH-g",
|
|
60
|
+
# 页面中的文章签名为空,但正文和其他关键元数据完整。
|
|
61
|
+
"https://mp.weixin.qq.com/s/skgpeibg1sbv2QFFdQeFxA",
|
|
60
62
|
]
|
|
61
63
|
|
|
62
64
|
|
|
@@ -131,7 +133,6 @@ def _assert_result(result: ArticleResult, url: str) -> None:
|
|
|
131
133
|
assert result.article_id, "article_id should not be empty"
|
|
132
134
|
assert result.article_msg_id > 0, "article_msg_id should be positive"
|
|
133
135
|
assert result.article_idx > 0, "article_idx should be positive"
|
|
134
|
-
assert result.article_sn, "article_sn should not be empty"
|
|
135
136
|
assert result.article_title, "article_title should not be empty"
|
|
136
137
|
if result.article_display_type not in ("video", "audio"):
|
|
137
138
|
assert result.article_markdown, "article_markdown should not be empty for non-video/audio articles"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/__init__.py
RENAMED
|
File without changes
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/src/wechat_article_parser/parser.py
RENAMED
|
File without changes
|
|
File without changes
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_fullscreen_content.py
RENAMED
|
File without changes
|
|
File without changes
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_miniprogram_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wechat_article_parser-0.0.16 → wechat_article_parser-0.0.17}/tests/test_redpacket_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|