wechat-article-parser 0.0.7__tar.gz → 0.0.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: wechat-article-parser
3
- Version: 0.0.7
3
+ Version: 0.0.8
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wechat-article-parser"
7
- version = "0.0.7"
7
+ version = "0.0.8"
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"
@@ -35,12 +35,22 @@ _JS_DATA_TOKEN_RE = re.compile(
35
35
  # ---------------------------------------------------------------------------
36
36
 
37
37
  class _MarkdownConverter(MarkdownConverter):
38
- def convert_span(self, el, text, parent_tags):
39
- return text.strip()
38
+ def convert_br(self, el, text, parent_tags):
39
+ # 代码中的换行不需要 Markdown 硬换行所用的两个尾随空格。
40
+ if "pre" in parent_tags:
41
+ return "\n"
42
+ return super().convert_br(el, text, parent_tags)
40
43
 
41
- def convert_p(self, el, text, parent_tags):
42
- text = " ".join(text.split())
43
- return super().convert_p(el, text, parent_tags)
44
+ def convert_pre(self, el, text, parent_tags):
45
+ # BeautifulSoup 已解码  ;代码中将其还原为可直接复制的普通空格。
46
+ return super().convert_pre(el, text.replace("\xa0", " "), parent_tags)
47
+
48
+ def convert_td(self, el, text, parent_tags):
49
+ # GFM 表格内的管道符即使位于行内代码中,也必须转义。
50
+ return super().convert_td(el, text.replace("|", r"\|"), parent_tags)
51
+
52
+ def convert_th(self, el, text, parent_tags):
53
+ return super().convert_th(el, text.replace("|", r"\|"), parent_tags)
44
54
 
45
55
 
46
56
  def _to_markdown(html: str) -> str:
@@ -50,8 +60,7 @@ def _to_markdown(html: str) -> str:
50
60
  def _to_markdown_plain(html: str) -> str:
51
61
  """用于纯文本类内容(纯文本/转载/视频/轮播/全屏)的转换。
52
62
 
53
- 这些内容由 <br> 承载换行,必须保留;故使用未覆写 convert_p 的基础转换器,
54
- 避免 _MarkdownConverter 折叠空白时把换行一并吞掉。
63
+ 这些内容由 <br> 承载换行,使用基础转换器保留换行。
55
64
  """
56
65
  return MarkdownConverter().convert(html)
57
66
 
@@ -332,7 +341,8 @@ def _extract_swiper_meta(script_text: str, result: ArticleResult) -> None:
332
341
 
333
342
  def _extract_rich_media_content(content_tag: Tag, result: ArticleResult) -> None:
334
343
  """将 rich_media_content 区块转换为 Markdown。"""
335
- soup = BeautifulSoup(content_tag.prettify(), "html.parser")
344
+ # prettify 会在行内标签之间插入空白,改变代码和引用链接的内容。
345
+ soup = BeautifulSoup(str(content_tag), "html.parser")
336
346
  seen: set[str] = set()
337
347
 
338
348
  # 处理 <img> 标签
@@ -385,7 +395,7 @@ def _extract_rich_media_content(content_tag: Tag, result: ArticleResult) -> None
385
395
  for tag in to_remove:
386
396
  tag.decompose()
387
397
 
388
- result.article_markdown = _to_markdown(soup.prettify())
398
+ result.article_markdown = _to_markdown(str(soup))
389
399
 
390
400
 
391
401
  def _extract_repost_content(content_tag: Tag, result: ArticleResult) -> None:
@@ -0,0 +1,85 @@
1
+ """富文本 Markdown 转换的离线回归测试。"""
2
+
3
+ import pytest
4
+
5
+ from wechat_article_parser.parser import _parse_html
6
+
7
+
8
+ def _convert(body: str) -> str:
9
+ html = f'<div class="rich_media_content">{body}</div>'
10
+ return _parse_html("https://mp.weixin.qq.com/s/test", html).article_markdown
11
+
12
+
13
+ def test_highlighted_code_preserves_breaks_spaces_and_entities() -> None:
14
+ # 微信 mdnice 代码块:语法高亮 span 切分 token,br 换行,nbsp 占位。
15
+ body = (
16
+ '<pre data-tool="mdnice编辑器"><code>'
17
+ '<span><span># ======== 基础体验 ========</span></span><span><br></span>'
18
+ '<span><span>set</span></span><span>&nbsp;-g default-terminal&nbsp;</span>'
19
+ '<span><span>"tmux-256color"</span></span><span><br></span>'
20
+ '<span>set</span><span>&nbsp;-as terminal-features&nbsp;</span>'
21
+ '<span>",*:RGB"</span><span><br></span>'
22
+ '<span>set</span><span>&nbsp;-g mouse on &nbsp; &nbsp;</span>'
23
+ '<span># 支持鼠标操作</span><span><br></span><span><br></span>'
24
+ '<span>&nbsp;&nbsp;echo &quot;&lt;tag&gt;&quot; &amp;&amp; echo &#160;ok</span>'
25
+ '</code></pre>'
26
+ )
27
+
28
+ assert _convert(body) == (
29
+ '```\n'
30
+ '# ======== 基础体验 ========\n'
31
+ 'set -g default-terminal "tmux-256color"\n'
32
+ 'set -as terminal-features ",*:RGB"\n'
33
+ 'set -g mouse on # 支持鼠标操作\n\n'
34
+ ' echo "<tag>" && echo ok\n'
35
+ '```'
36
+ )
37
+
38
+
39
+ def test_plain_pre_preserves_indentation_and_blank_lines() -> None:
40
+ assert _convert('<pre><code>if ready:\n run()\n\n done()\n</code></pre>') == (
41
+ '```\nif ready:\n run()\n\n done()\n```'
42
+ )
43
+
44
+
45
+ @pytest.mark.parametrize("cell", ["td", "th"])
46
+ def test_table_pipes_in_text_and_inline_code_are_escaped(cell: str) -> None:
47
+ output = _convert(
48
+ f'<table><tr><{cell}>A | B</{cell}><{cell}><section>'
49
+ '<span>验证 </span><code><span>tailscale status | grep peer-relay</span></code>'
50
+ f'</section></{cell}></tr></table>'
51
+ )
52
+
53
+ assert r'A \| B' in output
54
+ assert r'`tailscale status \| grep peer-relay`' in output
55
+ # 每行仍然只有两个单元格(三个未转义的分隔符)。
56
+ assert all(line.replace(r'\|', '').count('|') == 3 for line in output.splitlines())
57
+
58
+
59
+ def test_pipe_outside_table_is_unchanged() -> None:
60
+ assert _convert('<p><code>left | right</code></p><pre>left | right</pre>') == (
61
+ '`left | right`\n\n```\nleft | right\n```'
62
+ )
63
+
64
+
65
+ def test_inline_spans_and_paragraph_breaks_preserve_spacing() -> None:
66
+ assert _convert(
67
+ '<p><span>hello </span><span>world</span><br><span>next line</span></p>'
68
+ ) == 'hello world \nnext line'
69
+
70
+
71
+ def test_bold_reference_stays_inline() -> None:
72
+ assert _convert(
73
+ '<ul><li><section><strong><span>Hermes</span><sup>'
74
+ '<a href="https://example.com/hermes">[1]</a></sup></strong>'
75
+ '<span>:Nous Research 的自主 AI Agent</span></section></li></ul>'
76
+ ) == '* **Hermes[[1]](https://example.com/hermes)**:Nous Research 的自主 AI Agent'
77
+
78
+
79
+ def test_literal_asterisks_in_source_stay_literal_and_inline() -> None:
80
+ # 这篇原文没有 strong 标签;不能把普通星号自动猜测为加粗语义。
81
+ assert _convert(
82
+ '<ul><li><section><span>**</span><span>Hermes</span>'
83
+ '<sup><span>[1]</span></sup><span>**:Nous Research 的自主 AI Agent</span>'
84
+ '</section></li></ul>'
85
+ ) == r'* \*\*Hermes[1]\*\*:Nous Research 的自主 AI Agent'