parsehub 2.2.0__tar.gz → 2.2.1__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.1}/PKG-INFO +2 -2
- {parsehub-2.2.0 → parsehub-2.2.1}/pyproject.toml +2 -2
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/douban.py +20 -6
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/douban.py +4 -2
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/xhs.py +13 -6
- {parsehub-2.2.0 → parsehub-2.2.1/src/parsehub.egg-info}/PKG-INFO +2 -2
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub.egg-info/requires.txt +1 -1
- {parsehub-2.2.0 → parsehub-2.2.1}/LICENSE +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/README.md +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/setup.cfg +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/cli.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/config/config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/errors.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/bilibili.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/instagram.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/zhihu.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/zhihu.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/post.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/types/result.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/test/test_cli.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/test/test_cli_config.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/test/test_core_offline.py +0 -0
- {parsehub-2.2.0 → parsehub-2.2.1}/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.1
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pydantic>=1.10.19
|
|
|
28
28
|
Requires-Dist: markdownify>=1.1.0
|
|
29
29
|
Requires-Dist: markdown>=3.7
|
|
30
30
|
Requires-Dist: requests
|
|
31
|
-
Requires-Dist: httpx>=0.24.1
|
|
31
|
+
Requires-Dist: httpx[socks]>=0.24.1
|
|
32
32
|
Requires-Dist: pillow>=12.1.0
|
|
33
33
|
Requires-Dist: python-slugify[unidecode]>=8.0.4
|
|
34
34
|
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "parsehub"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = "轻量、异步、开箱即用的社交媒体聚合解析库"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12.0"
|
|
@@ -31,7 +31,7 @@ dependencies = [
|
|
|
31
31
|
"markdownify>=1.1.0",
|
|
32
32
|
"markdown>=3.7",
|
|
33
33
|
"requests",
|
|
34
|
-
"httpx>=0.24.1",
|
|
34
|
+
"httpx[socks]>=0.24.1",
|
|
35
35
|
"pillow>=12.1.0",
|
|
36
36
|
"python-slugify[unidecode]>=8.0.4",
|
|
37
37
|
"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"]
|
|
@@ -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
|
|
|
@@ -10,6 +10,8 @@ from typing import Any, cast
|
|
|
10
10
|
import httpx
|
|
11
11
|
from bs4 import BeautifulSoup
|
|
12
12
|
|
|
13
|
+
from ..errors import ParseError
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class XHSAPI:
|
|
15
17
|
def __init__(self, proxy: str | None = None, cookie: dict | None = None):
|
|
@@ -17,8 +19,13 @@ class XHSAPI:
|
|
|
17
19
|
self.cookie = cookie
|
|
18
20
|
|
|
19
21
|
async def __fetch_html(self, url: str) -> str:
|
|
20
|
-
async with httpx.AsyncClient(proxy=self.proxy, cookies=self.cookie) as client:
|
|
21
|
-
|
|
22
|
+
async with httpx.AsyncClient(proxy=self.proxy, cookies=self.cookie, follow_redirects=True) as client:
|
|
23
|
+
result = await client.get(url, timeout=30)
|
|
24
|
+
if "/login" in str(result.url):
|
|
25
|
+
raise ParseError("该帖子需要登录后查看")
|
|
26
|
+
elif "/404" in str(result.url):
|
|
27
|
+
raise ParseError("帖子不存在")
|
|
28
|
+
return result.text
|
|
22
29
|
|
|
23
30
|
@staticmethod
|
|
24
31
|
async def __extract_data(html: str) -> dict[str, Any]:
|
|
@@ -27,7 +34,7 @@ class XHSAPI:
|
|
|
27
34
|
script for script in soup.find_all("script") if script.text.lstrip().startswith("window.__INITIAL_STATE__")
|
|
28
35
|
]
|
|
29
36
|
if not scripts:
|
|
30
|
-
raise
|
|
37
|
+
raise ParseError("No data found")
|
|
31
38
|
script = scripts[0].text
|
|
32
39
|
json_data = script.replace("window.__INITIAL_STATE__=", "")
|
|
33
40
|
json_data = re.sub(r"\bundefined\b", "null", json_data) # 清理js对象
|
|
@@ -36,11 +43,11 @@ class XHSAPI:
|
|
|
36
43
|
|
|
37
44
|
def __parse(self, data: dict[str, Any]) -> XHSPost:
|
|
38
45
|
if not data.get("note"):
|
|
39
|
-
raise
|
|
46
|
+
raise ParseError("该帖子需要登录后查看")
|
|
40
47
|
first_note_id = data["note"]["firstNoteId"]
|
|
41
48
|
note = data["note"]["noteDetailMap"][first_note_id]["note"]
|
|
42
49
|
if not note:
|
|
43
|
-
raise
|
|
50
|
+
raise ParseError("未获取到内容, 该帖子可能需要登录后查看")
|
|
44
51
|
|
|
45
52
|
title = note["title"]
|
|
46
53
|
desc = note["desc"]
|
|
@@ -79,7 +86,7 @@ class XHSAPI:
|
|
|
79
86
|
stream = media["stream"]
|
|
80
87
|
selected_stream = self.__select_stream(stream)
|
|
81
88
|
if not selected_stream:
|
|
82
|
-
raise
|
|
89
|
+
raise ParseError("未获取到视频流")
|
|
83
90
|
stream = selected_stream[0]
|
|
84
91
|
media_list.append(
|
|
85
92
|
XHSMedia(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parsehub
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: 轻量、异步、开箱即用的社交媒体聚合解析库
|
|
5
5
|
Author-email: 梓澪 <zilingmio@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pydantic>=1.10.19
|
|
|
28
28
|
Requires-Dist: markdownify>=1.1.0
|
|
29
29
|
Requires-Dist: markdown>=3.7
|
|
30
30
|
Requires-Dist: requests
|
|
31
|
-
Requires-Dist: httpx>=0.24.1
|
|
31
|
+
Requires-Dist: httpx[socks]>=0.24.1
|
|
32
32
|
Requires-Dist: pillow>=12.1.0
|
|
33
33
|
Requires-Dist: python-slugify[unidecode]>=8.0.4
|
|
34
34
|
Requires-Dist: opencv-python-headless>=4.13.0.92
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|