parsehub 2.2.0__tar.gz → 2.2.2__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.
- {parsehub-2.2.0/src/parsehub.egg-info → parsehub-2.2.2}/PKG-INFO +25 -24
- {parsehub-2.2.0 → parsehub-2.2.2}/README.md +22 -22
- {parsehub-2.2.0 → parsehub-2.2.2}/pyproject.toml +3 -2
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/douban.py +20 -6
- parsehub-2.2.2/src/parsehub/parsers/parser/weixin.py +40 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/zhihu.py +2 -2
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/douban.py +4 -2
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/weibo.py +1 -0
- parsehub-2.2.2/src/parsehub/provider_api/weixin.py +153 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/xhs.py +19 -6
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/zhihu.py +29 -0
- {parsehub-2.2.0 → parsehub-2.2.2/src/parsehub.egg-info}/PKG-INFO +25 -24
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub.egg-info/requires.txt +2 -1
- {parsehub-2.2.0 → parsehub-2.2.2}/test/test_core_offline.py +1 -0
- parsehub-2.2.0/src/parsehub/parsers/parser/weixin.py +0 -20
- parsehub-2.2.0/src/parsehub/provider_api/weixin.py +0 -62
- {parsehub-2.2.0 → parsehub-2.2.2}/LICENSE +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/setup.cfg +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/cli.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/config/config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/errors.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/bilibili.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/instagram.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/post.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/types/result.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/test/test_cli.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/test/test_cli_config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.2}/test/test_downloader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parsehub
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,8 +27,9 @@ Requires-Dist: lxml>=5.3.0
|
|
|
27
27
|
Requires-Dist: pydantic>=1.10.19
|
|
28
28
|
Requires-Dist: markdownify>=1.1.0
|
|
29
29
|
Requires-Dist: markdown>=3.7
|
|
30
|
+
Requires-Dist: json5>=0.12
|
|
30
31
|
Requires-Dist: requests
|
|
31
|
-
Requires-Dist: httpx>=0.24.1
|
|
32
|
+
Requires-Dist: httpx[socks]>=0.24.1
|
|
32
33
|
Requires-Dist: pillow>=12.1.0
|
|
33
34
|
Requires-Dist: python-slugify[unidecode]>=8.0.4
|
|
34
35
|
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
@@ -72,28 +73,28 @@ Dynamic: license-file
|
|
|
72
73
|
|
|
73
74
|
## 🌐 支持平台
|
|
74
75
|
|
|
75
|
-
| 平台 | 视频 | 图文 | 其他
|
|
76
|
-
|
|
77
|
-
| **Twitter / X** | ✅ | ✅ | 📝 文章
|
|
78
|
-
| **Instagram** | ✅ | ✅ |
|
|
79
|
-
| **YouTube** | ✅ | | 🎵 音乐
|
|
80
|
-
| **Facebook** | ✅ | |
|
|
81
|
-
| **Threads** | ✅ | ✅ |
|
|
82
|
-
| **Bilibili** | ✅ | | 📝 动态
|
|
83
|
-
| **抖音** | ✅ | ✅ | ☀️日常
|
|
84
|
-
| **TikTok** | ✅ | ✅ |
|
|
85
|
-
| **微博** | ✅ | ✅ |
|
|
86
|
-
| **小红书** | ✅ | ✅ |
|
|
87
|
-
| **贴吧** | ✅ | ✅ |
|
|
88
|
-
| **微信公众号** | | ✅ |
|
|
89
|
-
| **快手** | ✅ | ✅ |
|
|
90
|
-
| **酷安** | | ✅ |
|
|
91
|
-
| **皮皮虾** | ✅ | ✅ |
|
|
92
|
-
| **最右** | ✅ | ✅ |
|
|
93
|
-
| **小黑盒** | ✅ | ✅ |
|
|
94
|
-
| **Snapchat** | ✅ | |
|
|
95
|
-
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏,
|
|
96
|
-
| **豆瓣** | ✅ | ✅ | 👥 小组话题
|
|
76
|
+
| 平台 | 视频 | 图文 | 其他 |
|
|
77
|
+
|-----------------|:----:|:----:|---------------------------|
|
|
78
|
+
| **Twitter / X** | ✅ | ✅ | 📝 文章 |
|
|
79
|
+
| **Instagram** | ✅ | ✅ | |
|
|
80
|
+
| **YouTube** | ✅ | | 🎵 音乐 |
|
|
81
|
+
| **Facebook** | ✅ | | |
|
|
82
|
+
| **Threads** | ✅ | ✅ | |
|
|
83
|
+
| **Bilibili** | ✅ | | 📝 动态 |
|
|
84
|
+
| **抖音** | ✅ | ✅ | ☀️日常 |
|
|
85
|
+
| **TikTok** | ✅ | ✅ | |
|
|
86
|
+
| **微博** | ✅ | ✅ | |
|
|
87
|
+
| **小红书** | ✅ | ✅ | |
|
|
88
|
+
| **贴吧** | ✅ | ✅ | |
|
|
89
|
+
| **微信公众号** | | ✅ | |
|
|
90
|
+
| **快手** | ✅ | ✅ | |
|
|
91
|
+
| **酷安** | | ✅ | |
|
|
92
|
+
| **皮皮虾** | ✅ | ✅ | |
|
|
93
|
+
| **最右** | ✅ | ✅ | |
|
|
94
|
+
| **小黑盒** | ✅ | ✅ | |
|
|
95
|
+
| **Snapchat** | ✅ | | |
|
|
96
|
+
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏, 圈子, 日报 |
|
|
97
|
+
| **豆瓣** | ✅ | ✅ | 👥 小组话题 |
|
|
97
98
|
|
|
98
99
|
## 📦 安装
|
|
99
100
|
|
|
@@ -30,28 +30,28 @@
|
|
|
30
30
|
|
|
31
31
|
## 🌐 支持平台
|
|
32
32
|
|
|
33
|
-
| 平台 | 视频 | 图文 | 其他
|
|
34
|
-
|
|
35
|
-
| **Twitter / X** | ✅ | ✅ | 📝 文章
|
|
36
|
-
| **Instagram** | ✅ | ✅ |
|
|
37
|
-
| **YouTube** | ✅ | | 🎵 音乐
|
|
38
|
-
| **Facebook** | ✅ | |
|
|
39
|
-
| **Threads** | ✅ | ✅ |
|
|
40
|
-
| **Bilibili** | ✅ | | 📝 动态
|
|
41
|
-
| **抖音** | ✅ | ✅ | ☀️日常
|
|
42
|
-
| **TikTok** | ✅ | ✅ |
|
|
43
|
-
| **微博** | ✅ | ✅ |
|
|
44
|
-
| **小红书** | ✅ | ✅ |
|
|
45
|
-
| **贴吧** | ✅ | ✅ |
|
|
46
|
-
| **微信公众号** | | ✅ |
|
|
47
|
-
| **快手** | ✅ | ✅ |
|
|
48
|
-
| **酷安** | | ✅ |
|
|
49
|
-
| **皮皮虾** | ✅ | ✅ |
|
|
50
|
-
| **最右** | ✅ | ✅ |
|
|
51
|
-
| **小黑盒** | ✅ | ✅ |
|
|
52
|
-
| **Snapchat** | ✅ | |
|
|
53
|
-
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏,
|
|
54
|
-
| **豆瓣** | ✅ | ✅ | 👥 小组话题
|
|
33
|
+
| 平台 | 视频 | 图文 | 其他 |
|
|
34
|
+
|-----------------|:----:|:----:|---------------------------|
|
|
35
|
+
| **Twitter / X** | ✅ | ✅ | 📝 文章 |
|
|
36
|
+
| **Instagram** | ✅ | ✅ | |
|
|
37
|
+
| **YouTube** | ✅ | | 🎵 音乐 |
|
|
38
|
+
| **Facebook** | ✅ | | |
|
|
39
|
+
| **Threads** | ✅ | ✅ | |
|
|
40
|
+
| **Bilibili** | ✅ | | 📝 动态 |
|
|
41
|
+
| **抖音** | ✅ | ✅ | ☀️日常 |
|
|
42
|
+
| **TikTok** | ✅ | ✅ | |
|
|
43
|
+
| **微博** | ✅ | ✅ | |
|
|
44
|
+
| **小红书** | ✅ | ✅ | |
|
|
45
|
+
| **贴吧** | ✅ | ✅ | |
|
|
46
|
+
| **微信公众号** | | ✅ | |
|
|
47
|
+
| **快手** | ✅ | ✅ | |
|
|
48
|
+
| **酷安** | | ✅ | |
|
|
49
|
+
| **皮皮虾** | ✅ | ✅ | |
|
|
50
|
+
| **最右** | ✅ | ✅ | |
|
|
51
|
+
| **小黑盒** | ✅ | ✅ | |
|
|
52
|
+
| **Snapchat** | ✅ | | |
|
|
53
|
+
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏, 圈子, 日报 |
|
|
54
|
+
| **豆瓣** | ✅ | ✅ | 👥 小组话题 |
|
|
55
55
|
|
|
56
56
|
## 📦 安装
|
|
57
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "parsehub"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.2"
|
|
4
4
|
description = "轻量、异步、开箱即用的社交媒体聚合解析库"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12.0"
|
|
@@ -30,8 +30,9 @@ dependencies = [
|
|
|
30
30
|
"pydantic>=1.10.19",
|
|
31
31
|
"markdownify>=1.1.0",
|
|
32
32
|
"markdown>=3.7",
|
|
33
|
+
"json5>=0.12",
|
|
33
34
|
"requests",
|
|
34
|
-
"httpx>=0.24.1",
|
|
35
|
+
"httpx[socks]>=0.24.1",
|
|
35
36
|
"pillow>=12.1.0",
|
|
36
37
|
"python-slugify[unidecode]>=8.0.4",
|
|
37
38
|
"opencv-python-headless>=4.13.0.92",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Union
|
|
2
3
|
|
|
3
4
|
from ...provider_api.douban import IMAGE_REFERER, Douban, DoubanError, DoubanPhoto, DoubanVideo
|
|
4
5
|
from ...types import (
|
|
5
6
|
AniRef,
|
|
6
7
|
AnyMediaRef,
|
|
7
8
|
DownloadResult,
|
|
9
|
+
ImageParseResult,
|
|
8
10
|
ImageRef,
|
|
9
11
|
ParseError,
|
|
12
|
+
ParseResult,
|
|
10
13
|
Platform,
|
|
11
14
|
ProgressCallback,
|
|
12
15
|
RichTextParseResult,
|
|
@@ -22,7 +25,7 @@ class DoubanParser(BaseParser):
|
|
|
22
25
|
__match__ = r"^(http(s)?://)?(((www|m)\.)?douban\.com/((group/)?topic/\d+|doubanapp/dispatch)|douc\.cc/.+)"
|
|
23
26
|
__redirect_keywords__ = ["douc.cc", "doubanapp/dispatch"]
|
|
24
27
|
|
|
25
|
-
async def _do_parse(self, raw_url: str) -> "DoubanRichTextParseResult":
|
|
28
|
+
async def _do_parse(self, raw_url: str) -> Union["DoubanRichTextParseResult", "DoubanImageParseResult"]:
|
|
26
29
|
try:
|
|
27
30
|
topic = await Douban(proxy=self.proxy, cookie=self.cookie.get_value()).parse(raw_url)
|
|
28
31
|
except DoubanError as e:
|
|
@@ -30,11 +33,16 @@ class DoubanParser(BaseParser):
|
|
|
30
33
|
except Exception as e:
|
|
31
34
|
raise ParseError("豆瓣解析失败: 未知错误") from e
|
|
32
35
|
|
|
36
|
+
photos = [self.to_media_ref(p) for p in topic.photos]
|
|
37
|
+
|
|
38
|
+
if topic.image_layout == "horizontal":
|
|
39
|
+
return DoubanImageParseResult(title=topic.title, photo=photos, content=topic.text_content)
|
|
40
|
+
|
|
33
41
|
# 图片和视频在正文里有位置关系, 交给 RichText 由 markdown 保留顺序
|
|
34
|
-
media: list[AnyMediaRef] = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
media: list[AnyMediaRef] = []
|
|
43
|
+
if topic.video:
|
|
44
|
+
media.append(self.to_video_ref(topic.video))
|
|
45
|
+
media.extend(photos)
|
|
38
46
|
return DoubanRichTextParseResult(title=topic.title, media=media, markdown_content=topic.markdown_content)
|
|
39
47
|
|
|
40
48
|
@staticmethod
|
|
@@ -59,7 +67,7 @@ class DoubanParser(BaseParser):
|
|
|
59
67
|
)
|
|
60
68
|
|
|
61
69
|
|
|
62
|
-
class
|
|
70
|
+
class DoubanParseResult(ParseResult):
|
|
63
71
|
async def _do_download(
|
|
64
72
|
self,
|
|
65
73
|
*,
|
|
@@ -83,4 +91,10 @@ class DoubanRichTextParseResult(RichTextParseResult):
|
|
|
83
91
|
)
|
|
84
92
|
|
|
85
93
|
|
|
94
|
+
class DoubanImageParseResult(ImageParseResult, DoubanParseResult): ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DoubanRichTextParseResult(RichTextParseResult, DoubanParseResult): ...
|
|
98
|
+
|
|
99
|
+
|
|
86
100
|
__all__ = ["DoubanParser", "DoubanRichTextParseResult"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from ...provider_api.weixin import WX, WXItemShowType
|
|
2
|
+
from ...types import AnyParseResult, ImageParseResult, ImageRef, ParseError, Platform, RichTextParseResult
|
|
3
|
+
from ..base.base import BaseParser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WXParser(BaseParser):
|
|
7
|
+
__platform__ = Platform.WEIXIN
|
|
8
|
+
__supported_type__ = ["图文"]
|
|
9
|
+
__match__ = r"^(http(s)?://)mp.weixin.qq.com/s/.*"
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _refs(urls: list[str]) -> list[ImageRef]:
|
|
13
|
+
"""图片地址带 wx_fmt 参数标明实际格式, 缺失时才用默认的 jpg."""
|
|
14
|
+
refs = []
|
|
15
|
+
for url in urls:
|
|
16
|
+
fmt = url.partition("wx_fmt=")[2].partition("&")[0]
|
|
17
|
+
refs.append(ImageRef(url=url, ext=fmt or "jpg"))
|
|
18
|
+
return refs
|
|
19
|
+
|
|
20
|
+
async def _do_parse(self, raw_url: str) -> AnyParseResult:
|
|
21
|
+
wx = await WX.parse(raw_url, self.proxy)
|
|
22
|
+
|
|
23
|
+
match wx.item_show_type:
|
|
24
|
+
case WXItemShowType.ARTICLE:
|
|
25
|
+
return RichTextParseResult(
|
|
26
|
+
title=wx.title,
|
|
27
|
+
media=self._refs(wx.imgs),
|
|
28
|
+
markdown_content=wx.markdown_content,
|
|
29
|
+
)
|
|
30
|
+
case WXItemShowType.IMAGE | WXItemShowType.TEXT:
|
|
31
|
+
return ImageParseResult(
|
|
32
|
+
title=wx.title,
|
|
33
|
+
photo=self._refs(wx.photos),
|
|
34
|
+
content=wx.text_content,
|
|
35
|
+
)
|
|
36
|
+
case _:
|
|
37
|
+
raise ParseError(f"不支持的内容类型: {wx.item_show_type!r}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["WXParser"]
|
|
@@ -13,8 +13,8 @@ from ..base.base import BaseParser
|
|
|
13
13
|
|
|
14
14
|
class ZhihuParser(BaseParser):
|
|
15
15
|
__platform__ = Platform.ZHIHU
|
|
16
|
-
__supported_type__ = ["问答", "专栏", "圈子"]
|
|
17
|
-
__match__ = r"^(http(s)?://)?(www|zhuanlan).zhihu.com/(pin|question|p)/.*"
|
|
16
|
+
__supported_type__ = ["问答", "专栏", "圈子", "日报"]
|
|
17
|
+
__match__ = r"^(http(s)?://)?(www|zhuanlan|daily).zhihu.com/(pin|question|p|story)/.*"
|
|
18
18
|
|
|
19
19
|
async def _do_parse(
|
|
20
20
|
self, raw_url: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from typing import Any, cast
|
|
3
|
+
from typing import Any, Literal, cast
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
from bs4 import BeautifulSoup
|
|
@@ -80,6 +80,7 @@ class DoubanTopic:
|
|
|
80
80
|
title: str
|
|
81
81
|
markdown_content: str
|
|
82
82
|
text_content: str
|
|
83
|
+
image_layout: Literal["horizontal", "vertical"] # vertical 图文混排, horizontal 图集
|
|
83
84
|
video: DoubanVideo | None = None
|
|
84
85
|
photos: list[DoubanPhoto] = field(default_factory=list)
|
|
85
86
|
|
|
@@ -95,7 +96,8 @@ class DoubanTopic:
|
|
|
95
96
|
markdown_content=markdown_content,
|
|
96
97
|
text_content=text_content,
|
|
97
98
|
video=parse_video(video_info) if video_info else None,
|
|
98
|
-
photos=[p for photo in data.get("photos"
|
|
99
|
+
photos=[p for photo in data.get("photos", []) if (p := parse_photo(photo))],
|
|
100
|
+
image_layout=data.get("image_layout", "horizontal"),
|
|
99
101
|
)
|
|
100
102
|
|
|
101
103
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from html import unescape
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import json5
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
from markdownify import MarkdownConverter
|
|
11
|
+
|
|
12
|
+
from ..errors import ParseError
|
|
13
|
+
from ..utils.helpers import UA
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WXItemShowType(IntEnum):
|
|
17
|
+
"""页面数据里的 ``item_show_type``, 决定正文与媒体的取值位置."""
|
|
18
|
+
|
|
19
|
+
ARTICLE = 0
|
|
20
|
+
"""公众号文章"""
|
|
21
|
+
IMAGE = 8
|
|
22
|
+
"""图文"""
|
|
23
|
+
TEXT = 10
|
|
24
|
+
"""纯文本"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_TAG_RE = re.compile(r"</?[a-zA-Z][^>]*>")
|
|
28
|
+
_BREAK_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
|
|
29
|
+
_IMG_SRC_RE = re.compile(r"<img[^>]*?\bdata-src\s*=\s*[\"']([^\"']+)[\"']", re.IGNORECASE)
|
|
30
|
+
#: 页面数据用 '0' * 1 这类写法做隐式类型转换, 不是合法的 JSON5 值
|
|
31
|
+
_IMPLICIT_INT_RE = re.compile(r"('[^'\n]*'|\"[^\"\n]*\")\s*\*\s*1(?![.\d])")
|
|
32
|
+
#: 对象以 "};" 结束后还跟着 try/catch 与 IIFE 收尾
|
|
33
|
+
_OBJECT_END_RE = re.compile(r"}\s*;.*$", re.DOTALL)
|
|
34
|
+
|
|
35
|
+
_PAYLOAD_NAME = "cgiDataNew"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _load_payload(html: str) -> dict[str, Any]:
|
|
39
|
+
"""解析 ``window.cgiDataNew`` 为字典.
|
|
40
|
+
|
|
41
|
+
数据在 script 标签里, 是 JS 对象字面量 (键无引号、单引号字符串、尾随逗号),
|
|
42
|
+
用 json5 解析, 只需去掉两处 JS 语法.
|
|
43
|
+
"""
|
|
44
|
+
scripts = [s.text for s in BeautifulSoup(html, "lxml").find_all("script") if _PAYLOAD_NAME in s.text]
|
|
45
|
+
if not scripts:
|
|
46
|
+
raise ParseError(f"未找到 {_PAYLOAD_NAME} 数据")
|
|
47
|
+
|
|
48
|
+
text = scripts[0].split(_PAYLOAD_NAME, 1)[1]
|
|
49
|
+
text = _IMPLICIT_INT_RE.sub(r"\1", text)
|
|
50
|
+
text = text[text.index("{") :]
|
|
51
|
+
return cast(dict[str, Any], json5.loads(_OBJECT_END_RE.sub("}", text, count=1)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _to_text(source: Any) -> str:
|
|
55
|
+
"""正文片段转纯文本, 兼容本身就是纯文本的情况."""
|
|
56
|
+
if not isinstance(source, str) or not source:
|
|
57
|
+
return ""
|
|
58
|
+
return unescape(_TAG_RE.sub("", _BREAK_RE.sub("\n", unescape(source)))).strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _collect_photos(data: dict[str, Any]) -> list[str]:
|
|
62
|
+
"""图片消息的图片地址, 取原始图 ``watermark_info.cdn_url``, 缺失时退回 ``cdn_url``."""
|
|
63
|
+
entries = data.get("picture_page_info_list") or []
|
|
64
|
+
return [
|
|
65
|
+
url
|
|
66
|
+
for e in entries
|
|
67
|
+
if isinstance(e, dict)
|
|
68
|
+
for url in [(e.get("watermark_info") or {}).get("cdn_url") or e.get("cdn_url")]
|
|
69
|
+
if url
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WXConverter(MarkdownConverter):
|
|
74
|
+
"""把正文 HTML 转为 markdown, 图片地址取自 ``data-src``."""
|
|
75
|
+
|
|
76
|
+
def convert_img(self, el: Any, text: Any, parent_tags: Any) -> str:
|
|
77
|
+
alt = el.attrs.get("alt", None) or ""
|
|
78
|
+
src = el.attrs.get("data-src", None) or el.attrs.get("src", None) or ""
|
|
79
|
+
title = el.attrs.get("title", None) or ""
|
|
80
|
+
title_part = ' "{}"'.format(title.replace('"', r"\"")) if title else ""
|
|
81
|
+
options = cast(dict[str, Any], getattr(self, "options")) # noqa: B009
|
|
82
|
+
if "_inline" in parent_tags and el.parent.name not in options["keep_inline_images_in"]:
|
|
83
|
+
return alt
|
|
84
|
+
|
|
85
|
+
return f""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class WX:
|
|
90
|
+
"""微信文章的解析结果.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
item_show_type: 内容形态
|
|
94
|
+
title: 标题
|
|
95
|
+
markdown_content: markdown 正文, 仅图文形态有值
|
|
96
|
+
imgs: 正文内的图片地址
|
|
97
|
+
text_content: 纯文本正文
|
|
98
|
+
photos: 图片消息的图片地址, 仅图片形态有值
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
item_show_type: WXItemShowType
|
|
102
|
+
title: str
|
|
103
|
+
markdown_content: str
|
|
104
|
+
imgs: list[str]
|
|
105
|
+
text_content: str
|
|
106
|
+
photos: list[str]
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
async def parse(url: str, proxy: str | None = None) -> "WX":
|
|
110
|
+
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
111
|
+
response = await client.get(url, headers={"User-Agent": UA})
|
|
112
|
+
return WX._parse_data(response.text)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _parse_data(cls, html: str) -> "WX":
|
|
116
|
+
data = _load_payload(html)
|
|
117
|
+
try:
|
|
118
|
+
item_show_type = WXItemShowType(int(data["item_show_type"]))
|
|
119
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
120
|
+
raise ParseError(f"不支持的内容类型: {data.get('item_show_type')!r}") from e
|
|
121
|
+
|
|
122
|
+
match item_show_type:
|
|
123
|
+
case WXItemShowType.ARTICLE:
|
|
124
|
+
source = data.get("content_noencode") or ""
|
|
125
|
+
return cls(
|
|
126
|
+
item_show_type=item_show_type,
|
|
127
|
+
title=data.get("title") or "",
|
|
128
|
+
markdown_content=WXConverter(heading_style="ATX").convert(source),
|
|
129
|
+
imgs=_IMG_SRC_RE.findall(source),
|
|
130
|
+
text_content=_to_text(source),
|
|
131
|
+
photos=[],
|
|
132
|
+
)
|
|
133
|
+
case WXItemShowType.TEXT:
|
|
134
|
+
return cls(
|
|
135
|
+
item_show_type=item_show_type,
|
|
136
|
+
title="",
|
|
137
|
+
markdown_content="",
|
|
138
|
+
imgs=[],
|
|
139
|
+
text_content=_to_text((data.get("text_page_info") or {}).get("content_noencode")),
|
|
140
|
+
photos=[],
|
|
141
|
+
)
|
|
142
|
+
case WXItemShowType.IMAGE:
|
|
143
|
+
return cls(
|
|
144
|
+
item_show_type=item_show_type,
|
|
145
|
+
title=data.get("title") or "",
|
|
146
|
+
markdown_content="",
|
|
147
|
+
imgs=[],
|
|
148
|
+
text_content=_to_text(data.get("content_noencode")),
|
|
149
|
+
photos=_collect_photos(data),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = ["WX", "WXConverter", "WXItemShowType"]
|
|
@@ -10,6 +10,10 @@ from typing import Any, cast
|
|
|
10
10
|
import httpx
|
|
11
11
|
from bs4 import BeautifulSoup
|
|
12
12
|
|
|
13
|
+
from parsehub.utils.helpers import UA
|
|
14
|
+
|
|
15
|
+
from ..errors import ParseError
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
class XHSAPI:
|
|
15
19
|
def __init__(self, proxy: str | None = None, cookie: dict | None = None):
|
|
@@ -17,8 +21,17 @@ class XHSAPI:
|
|
|
17
21
|
self.cookie = cookie
|
|
18
22
|
|
|
19
23
|
async def __fetch_html(self, url: str) -> str:
|
|
20
|
-
async with httpx.AsyncClient(proxy=self.proxy, cookies=self.cookie) as client:
|
|
21
|
-
|
|
24
|
+
async with httpx.AsyncClient(proxy=self.proxy, cookies=self.cookie, follow_redirects=True) as client:
|
|
25
|
+
result = await client.get(
|
|
26
|
+
url,
|
|
27
|
+
headers={"User-Agent": UA},
|
|
28
|
+
timeout=30,
|
|
29
|
+
)
|
|
30
|
+
if "/login" in str(result.url):
|
|
31
|
+
raise ParseError("该帖子需要登录后查看")
|
|
32
|
+
elif "/404" in str(result.url):
|
|
33
|
+
raise ParseError("帖子不存在")
|
|
34
|
+
return result.text
|
|
22
35
|
|
|
23
36
|
@staticmethod
|
|
24
37
|
async def __extract_data(html: str) -> dict[str, Any]:
|
|
@@ -27,7 +40,7 @@ class XHSAPI:
|
|
|
27
40
|
script for script in soup.find_all("script") if script.text.lstrip().startswith("window.__INITIAL_STATE__")
|
|
28
41
|
]
|
|
29
42
|
if not scripts:
|
|
30
|
-
raise
|
|
43
|
+
raise ParseError("No data found")
|
|
31
44
|
script = scripts[0].text
|
|
32
45
|
json_data = script.replace("window.__INITIAL_STATE__=", "")
|
|
33
46
|
json_data = re.sub(r"\bundefined\b", "null", json_data) # 清理js对象
|
|
@@ -36,11 +49,11 @@ class XHSAPI:
|
|
|
36
49
|
|
|
37
50
|
def __parse(self, data: dict[str, Any]) -> XHSPost:
|
|
38
51
|
if not data.get("note"):
|
|
39
|
-
raise
|
|
52
|
+
raise ParseError("该帖子需要登录后查看")
|
|
40
53
|
first_note_id = data["note"]["firstNoteId"]
|
|
41
54
|
note = data["note"]["noteDetailMap"][first_note_id]["note"]
|
|
42
55
|
if not note:
|
|
43
|
-
raise
|
|
56
|
+
raise ParseError("未获取到内容, 该帖子可能需要登录后查看")
|
|
44
57
|
|
|
45
58
|
title = note["title"]
|
|
46
59
|
desc = note["desc"]
|
|
@@ -79,7 +92,7 @@ class XHSAPI:
|
|
|
79
92
|
stream = media["stream"]
|
|
80
93
|
selected_stream = self.__select_stream(stream)
|
|
81
94
|
if not selected_stream:
|
|
82
|
-
raise
|
|
95
|
+
raise ParseError("未获取到视频流")
|
|
83
96
|
stream = selected_stream[0]
|
|
84
97
|
media_list.append(
|
|
85
98
|
XHSMedia(
|
|
@@ -208,8 +208,24 @@ class ZhihuAPI:
|
|
|
208
208
|
return await self.parse_zl(raw_url)
|
|
209
209
|
if "/pin/" in raw_url:
|
|
210
210
|
return await self.parse_pin(raw_url)
|
|
211
|
+
if "daily.zhihu.com" in raw_url or "/story/" in raw_url:
|
|
212
|
+
return await self.parse_daily(raw_url)
|
|
211
213
|
raise ValueError("不支持的类型")
|
|
212
214
|
|
|
215
|
+
async def parse_daily(self, raw_url: str) -> ZhihuQA | ZhihuZhuanLan | ZhihuPin:
|
|
216
|
+
story_id = self._get_daily_id(raw_url)
|
|
217
|
+
result = await self._daily(story_id)
|
|
218
|
+
body = result.get("body", "")
|
|
219
|
+
soup = BeautifulSoup(body, "lxml")
|
|
220
|
+
origin_url: str | None = None
|
|
221
|
+
for a in soup.find_all("a"):
|
|
222
|
+
if "查看知乎原文" in a.get_text():
|
|
223
|
+
origin_url = str(a.get("href"))
|
|
224
|
+
break
|
|
225
|
+
if not origin_url:
|
|
226
|
+
raise ValueError("未在知乎日报中找到原文链接")
|
|
227
|
+
return await self.parse(origin_url)
|
|
228
|
+
|
|
213
229
|
async def parse_qa(self, raw_url: str) -> ZhihuQA:
|
|
214
230
|
qid, aid = self._get_qa_id(raw_url)
|
|
215
231
|
if aid:
|
|
@@ -293,6 +309,12 @@ class ZhihuAPI:
|
|
|
293
309
|
r = await client.get(url, headers=headers, params=query, cookies=self.cookie)
|
|
294
310
|
return dict(r.json())
|
|
295
311
|
|
|
312
|
+
async def _daily(self, story_id: int | str) -> dict:
|
|
313
|
+
url = f"https://daily.zhihu.com/api/7/story/{story_id}"
|
|
314
|
+
async with httpx.AsyncClient(proxy=self.proxy) as client:
|
|
315
|
+
r = await client.get(url)
|
|
316
|
+
return dict(r.json())
|
|
317
|
+
|
|
296
318
|
@staticmethod
|
|
297
319
|
def _get_qa_id(raw_url: str) -> tuple[str, str | None]:
|
|
298
320
|
"""返回问题和回答 id, 没有回答 id 时返回 None"""
|
|
@@ -315,6 +337,13 @@ class ZhihuAPI:
|
|
|
315
337
|
raise ValueError("从链接中提取 圈子 id 错误")
|
|
316
338
|
return r.group(1)
|
|
317
339
|
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _get_daily_id(raw_url: str) -> str:
|
|
342
|
+
r = re.search(r"/story/(\d+)", raw_url)
|
|
343
|
+
if not r:
|
|
344
|
+
raise ValueError("从链接中提取 日报 id 错误")
|
|
345
|
+
return r.group(1)
|
|
346
|
+
|
|
318
347
|
|
|
319
348
|
# --- SM4 constants (extracted from other.js VM) ---------------------------------
|
|
320
349
|
SM4_ZB = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parsehub
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,8 +27,9 @@ Requires-Dist: lxml>=5.3.0
|
|
|
27
27
|
Requires-Dist: pydantic>=1.10.19
|
|
28
28
|
Requires-Dist: markdownify>=1.1.0
|
|
29
29
|
Requires-Dist: markdown>=3.7
|
|
30
|
+
Requires-Dist: json5>=0.12
|
|
30
31
|
Requires-Dist: requests
|
|
31
|
-
Requires-Dist: httpx>=0.24.1
|
|
32
|
+
Requires-Dist: httpx[socks]>=0.24.1
|
|
32
33
|
Requires-Dist: pillow>=12.1.0
|
|
33
34
|
Requires-Dist: python-slugify[unidecode]>=8.0.4
|
|
34
35
|
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
@@ -72,28 +73,28 @@ Dynamic: license-file
|
|
|
72
73
|
|
|
73
74
|
## 🌐 支持平台
|
|
74
75
|
|
|
75
|
-
| 平台 | 视频 | 图文 | 其他
|
|
76
|
-
|
|
77
|
-
| **Twitter / X** | ✅ | ✅ | 📝 文章
|
|
78
|
-
| **Instagram** | ✅ | ✅ |
|
|
79
|
-
| **YouTube** | ✅ | | 🎵 音乐
|
|
80
|
-
| **Facebook** | ✅ | |
|
|
81
|
-
| **Threads** | ✅ | ✅ |
|
|
82
|
-
| **Bilibili** | ✅ | | 📝 动态
|
|
83
|
-
| **抖音** | ✅ | ✅ | ☀️日常
|
|
84
|
-
| **TikTok** | ✅ | ✅ |
|
|
85
|
-
| **微博** | ✅ | ✅ |
|
|
86
|
-
| **小红书** | ✅ | ✅ |
|
|
87
|
-
| **贴吧** | ✅ | ✅ |
|
|
88
|
-
| **微信公众号** | | ✅ |
|
|
89
|
-
| **快手** | ✅ | ✅ |
|
|
90
|
-
| **酷安** | | ✅ |
|
|
91
|
-
| **皮皮虾** | ✅ | ✅ |
|
|
92
|
-
| **最右** | ✅ | ✅ |
|
|
93
|
-
| **小黑盒** | ✅ | ✅ |
|
|
94
|
-
| **Snapchat** | ✅ | |
|
|
95
|
-
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏,
|
|
96
|
-
| **豆瓣** | ✅ | ✅ | 👥 小组话题
|
|
76
|
+
| 平台 | 视频 | 图文 | 其他 |
|
|
77
|
+
|-----------------|:----:|:----:|---------------------------|
|
|
78
|
+
| **Twitter / X** | ✅ | ✅ | 📝 文章 |
|
|
79
|
+
| **Instagram** | ✅ | ✅ | |
|
|
80
|
+
| **YouTube** | ✅ | | 🎵 音乐 |
|
|
81
|
+
| **Facebook** | ✅ | | |
|
|
82
|
+
| **Threads** | ✅ | ✅ | |
|
|
83
|
+
| **Bilibili** | ✅ | | 📝 动态 |
|
|
84
|
+
| **抖音** | ✅ | ✅ | ☀️日常 |
|
|
85
|
+
| **TikTok** | ✅ | ✅ | |
|
|
86
|
+
| **微博** | ✅ | ✅ | |
|
|
87
|
+
| **小红书** | ✅ | ✅ | |
|
|
88
|
+
| **贴吧** | ✅ | ✅ | |
|
|
89
|
+
| **微信公众号** | | ✅ | |
|
|
90
|
+
| **快手** | ✅ | ✅ | |
|
|
91
|
+
| **酷安** | | ✅ | |
|
|
92
|
+
| **皮皮虾** | ✅ | ✅ | |
|
|
93
|
+
| **最右** | ✅ | ✅ | |
|
|
94
|
+
| **小黑盒** | ✅ | ✅ | |
|
|
95
|
+
| **Snapchat** | ✅ | | |
|
|
96
|
+
| **知乎** | ✅ | ✅ | 🐶 问答, 专栏, 圈子, 日报 |
|
|
97
|
+
| **豆瓣** | ✅ | ✅ | 👥 小组话题 |
|
|
97
98
|
|
|
98
99
|
## 📦 安装
|
|
99
100
|
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from ...provider_api.weixin import WX
|
|
2
|
-
from ...types import ImageRef, Platform, RichTextParseResult
|
|
3
|
-
from ..base.base import BaseParser
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class WXParser(BaseParser):
|
|
7
|
-
__platform__ = Platform.WEIXIN
|
|
8
|
-
__supported_type__ = ["图文"]
|
|
9
|
-
__match__ = r"^(http(s)?://)mp.weixin.qq.com/s/.*"
|
|
10
|
-
|
|
11
|
-
async def _do_parse(self, raw_url: str) -> "RichTextParseResult":
|
|
12
|
-
wx = await WX.parse(raw_url, self.proxy)
|
|
13
|
-
return RichTextParseResult(
|
|
14
|
-
title=wx.title,
|
|
15
|
-
media=[ImageRef(url=i) for i in wx.imgs],
|
|
16
|
-
markdown_content=wx.markdown_content,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
__all__ = ["WXParser"]
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Any, cast
|
|
3
|
-
|
|
4
|
-
import httpx
|
|
5
|
-
from bs4 import BeautifulSoup, Tag
|
|
6
|
-
from markdown import markdown
|
|
7
|
-
from markdownify import MarkdownConverter
|
|
8
|
-
|
|
9
|
-
from ..types import ParseError
|
|
10
|
-
from ..utils.helpers import UA
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class WXConverter(MarkdownConverter):
|
|
14
|
-
def convert_img(self, el: Any, text: Any, parent_tags: Any) -> str:
|
|
15
|
-
alt = el.attrs.get("alt", None) or ""
|
|
16
|
-
src = el.attrs.get("data-src", None) or ""
|
|
17
|
-
title = el.attrs.get("title", None) or ""
|
|
18
|
-
title_part = ' "{}"'.format(title.replace('"', r"\"")) if title else ""
|
|
19
|
-
options = cast(dict[str, Any], getattr(self, "options")) # noqa: B009
|
|
20
|
-
if "_inline" in parent_tags and el.parent.name not in options["keep_inline_images_in"]:
|
|
21
|
-
return alt
|
|
22
|
-
|
|
23
|
-
return f""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass
|
|
27
|
-
class WX:
|
|
28
|
-
title: str
|
|
29
|
-
imgs: list[str]
|
|
30
|
-
markdown_content: str
|
|
31
|
-
text_content: str
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
async def parse(url: str, proxy: str | None = None) -> "WX":
|
|
35
|
-
async with httpx.AsyncClient(proxy=proxy) as client:
|
|
36
|
-
response = await client.get(url, headers={"User-Agent": UA})
|
|
37
|
-
html = response.text
|
|
38
|
-
return WX._parse_html(html)
|
|
39
|
-
|
|
40
|
-
@classmethod
|
|
41
|
-
def _parse_html(cls, html: str) -> "WX":
|
|
42
|
-
soup = BeautifulSoup(html, "lxml")
|
|
43
|
-
title_tag = soup.find("h1", {"class": "rich_media_title"})
|
|
44
|
-
title = title_tag.text.strip() if isinstance(title_tag, Tag) else ""
|
|
45
|
-
wxc = WXConverter(heading_style="ATX")
|
|
46
|
-
if isinstance(rich_media_content := soup.find("div", {"class": "rich_media_content"}), Tag):
|
|
47
|
-
imgs = [str(i.get("data-src") or "") for i in rich_media_content.find_all("img", {"class": "rich_pages"})]
|
|
48
|
-
|
|
49
|
-
markdown_content = wxc.convert(str(rich_media_content))
|
|
50
|
-
text_content = "".join(BeautifulSoup(markdown(markdown_content), "lxml").find_all(string=True))
|
|
51
|
-
return cls(title, imgs, markdown_content, text_content)
|
|
52
|
-
elif isinstance(share_content_page := soup.find("div", {"class": "share_content_page"}), Tag):
|
|
53
|
-
imgs = [str(i.get("data-src") or "") for i in share_content_page.find_all("div", {"class": "swiper_item"})]
|
|
54
|
-
|
|
55
|
-
description = soup.find("meta", {"name": "description"})
|
|
56
|
-
if not isinstance(description, Tag):
|
|
57
|
-
raise ParseError("获取内容失败")
|
|
58
|
-
markdown_content = wxc.convert(str(description.get("content") or ""))
|
|
59
|
-
text_content = "".join(BeautifulSoup(markdown(markdown_content), "lxml").find_all(string=True))
|
|
60
|
-
return cls(title, imgs, markdown_content, text_content)
|
|
61
|
-
else:
|
|
62
|
-
raise ParseError("获取内容失败")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|