parsehub 2.0.34__tar.gz → 2.0.35__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.0.34/src/parsehub.egg-info → parsehub-2.0.35}/PKG-INFO +1 -1
- {parsehub-2.0.34 → parsehub-2.0.35}/pyproject.toml +1 -1
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/instagram.py +3 -3
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/xhs.py +8 -8
- {parsehub-2.0.34 → parsehub-2.0.35/src/parsehub.egg-info}/PKG-INFO +1 -1
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub.egg-info/SOURCES.txt +2 -1
- {parsehub-2.0.34 → parsehub-2.0.35}/test/test_core_offline.py +1 -0
- parsehub-2.0.35/test/test_downloader.py +161 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/LICENSE +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/README.md +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/setup.cfg +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/cli.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/config/config.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/errors.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/bilibili.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/tiktok.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/twitter.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/bilibili.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/tiktok.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/post.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/types/result.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub.egg-info/requires.txt +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/test/test_cli.py +0 -0
- {parsehub-2.0.34 → parsehub-2.0.35}/test/test_cli_config.py +0 -0
|
@@ -9,7 +9,7 @@ from ..base.base import BaseParser
|
|
|
9
9
|
class InstagramParser(BaseParser):
|
|
10
10
|
__platform__ = Platform.INSTAGRAM
|
|
11
11
|
__supported_type__ = ["视频", "图文"]
|
|
12
|
-
__match__ = r"^(http(s)?://)(www\.|)instagram\.com/(p|
|
|
12
|
+
__match__ = r"^(http(s)?://)(www\.|)instagram\.com/(p|reels|share|.*/p)/.*"
|
|
13
13
|
__redirect_keywords__ = ["share"]
|
|
14
14
|
|
|
15
15
|
async def _do_parse(self, raw_url: str) -> VideoParseResult | ImageParseResult | MultimediaParseResult:
|
|
@@ -67,12 +67,12 @@ class InstagramParser(BaseParser):
|
|
|
67
67
|
text = f"Instagram 账号可能已被封禁\n\n使用的Cookie: {cookie}"
|
|
68
68
|
else:
|
|
69
69
|
text = str(e)
|
|
70
|
-
raise ParseError(f"
|
|
70
|
+
raise ParseError(f"无法获取帖子内容(可能为私人内容): {text}") from e
|
|
71
71
|
|
|
72
72
|
@staticmethod
|
|
73
73
|
def get_short_code(url: str) -> str | None:
|
|
74
74
|
url = url.removesuffix("/")
|
|
75
|
-
shortcode = re.search(r"/(share|p|
|
|
75
|
+
shortcode = re.search(r"/(share|p|reels|.*/p)/(.*)", url)
|
|
76
76
|
return shortcode.group(2).split("/")[0] if shortcode else None
|
|
77
77
|
|
|
78
78
|
|
|
@@ -122,14 +122,14 @@ class XHSAPI:
|
|
|
122
122
|
|
|
123
123
|
@staticmethod
|
|
124
124
|
def get_trace_id(img_url: str) -> str:
|
|
125
|
-
|
|
126
|
-
if
|
|
127
|
-
return "
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if
|
|
131
|
-
return
|
|
132
|
-
return
|
|
125
|
+
match = re.search(r"/(spectrum|note_pre_post_uhdr|notes_pre_post|notes_uhdr)/([^/!]+)(?:!.*)?$", img_url)
|
|
126
|
+
if match:
|
|
127
|
+
return f"{match.group(1)}/{match.group(2)}"
|
|
128
|
+
|
|
129
|
+
match = re.search(r"/([^/!]+)(?:!.*)?$", img_url)
|
|
130
|
+
if match:
|
|
131
|
+
return match.group(1)
|
|
132
|
+
return img_url
|
|
133
133
|
|
|
134
134
|
def get_raw_image_url(self, ime_url: str) -> str:
|
|
135
135
|
"""拼接无水印图片链接"""
|
|
@@ -238,6 +238,7 @@ class TestPlatformUrlMatching(unittest.TestCase):
|
|
|
238
238
|
"https://www.instagram.com/share/BAexample/",
|
|
239
239
|
"https://www.instagram.com/user.name/p/C0example/",
|
|
240
240
|
"https://www.instagram.com/user.name/reel/C0example/",
|
|
241
|
+
"https://www.instagram.com/reels/DaGI8bPS3ed/",
|
|
241
242
|
],
|
|
242
243
|
Platform.KUAISHOU: [
|
|
243
244
|
"https://www.kuaishou.com/short-video/3xexample",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import threading
|
|
3
|
+
import unittest
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from parsehub.errors import DownloadError
|
|
10
|
+
from parsehub.utils.downloader import download
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RangeTestHandler(BaseHTTPRequestHandler):
|
|
14
|
+
content: ClassVar[bytes] = b""
|
|
15
|
+
support_range: ClassVar[bool] = True
|
|
16
|
+
fail_all: ClassVar[bool] = False
|
|
17
|
+
requests: ClassVar[list[tuple[str, str | None]]] = []
|
|
18
|
+
|
|
19
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
def do_HEAD(self) -> None:
|
|
23
|
+
self.__class__.requests.append(("HEAD", self.headers.get("Range")))
|
|
24
|
+
if self.fail_all:
|
|
25
|
+
self.send_response(500)
|
|
26
|
+
self.end_headers()
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
self.send_response(200)
|
|
30
|
+
self.send_header("Content-Length", str(len(self.content)))
|
|
31
|
+
self.send_header("Accept-Ranges", "bytes" if self.support_range else "none")
|
|
32
|
+
self.end_headers()
|
|
33
|
+
|
|
34
|
+
def do_GET(self) -> None:
|
|
35
|
+
range_header = self.headers.get("Range")
|
|
36
|
+
self.__class__.requests.append(("GET", range_header))
|
|
37
|
+
if self.fail_all:
|
|
38
|
+
self.send_response(500)
|
|
39
|
+
self.end_headers()
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if range_header and self.support_range:
|
|
43
|
+
start, end = self._parse_range(range_header)
|
|
44
|
+
if start >= len(self.content):
|
|
45
|
+
self.send_response(416)
|
|
46
|
+
self.send_header("Content-Range", f"bytes */{len(self.content)}")
|
|
47
|
+
self.end_headers()
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
end = min(end, len(self.content) - 1)
|
|
51
|
+
body = self.content[start : end + 1]
|
|
52
|
+
self.send_response(206)
|
|
53
|
+
self.send_header("Content-Length", str(len(body)))
|
|
54
|
+
self.send_header("Content-Range", f"bytes {start}-{end}/{len(self.content)}")
|
|
55
|
+
self.send_header("Accept-Ranges", "bytes")
|
|
56
|
+
self.end_headers()
|
|
57
|
+
self.wfile.write(body)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self.send_response(200)
|
|
61
|
+
self.send_header("Content-Length", str(len(self.content)))
|
|
62
|
+
self.send_header("Accept-Ranges", "none")
|
|
63
|
+
self.end_headers()
|
|
64
|
+
self.wfile.write(self.content)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _parse_range(header: str) -> tuple[int, int]:
|
|
68
|
+
prefix = "bytes="
|
|
69
|
+
if not header.startswith(prefix):
|
|
70
|
+
return 0, 0
|
|
71
|
+
start_text, end_text = header.removeprefix(prefix).split("-", 1)
|
|
72
|
+
return int(start_text), int(end_text)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextlib.contextmanager
|
|
76
|
+
def range_server(*, content: bytes, support_range: bool = True, fail_all: bool = False):
|
|
77
|
+
class Handler(RangeTestHandler):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
Handler.content = content
|
|
81
|
+
Handler.support_range = support_range
|
|
82
|
+
Handler.fail_all = fail_all
|
|
83
|
+
Handler.requests = []
|
|
84
|
+
|
|
85
|
+
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
86
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
87
|
+
thread.start()
|
|
88
|
+
try:
|
|
89
|
+
yield f"http://127.0.0.1:{server.server_port}/file.bin", Handler
|
|
90
|
+
finally:
|
|
91
|
+
server.shutdown()
|
|
92
|
+
server.server_close()
|
|
93
|
+
thread.join(timeout=5)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DownloaderTest(unittest.IsolatedAsyncioTestCase):
|
|
97
|
+
async def test_download_uses_range_parts_when_server_supports_range(self):
|
|
98
|
+
content = bytes(range(251)) * 20
|
|
99
|
+
progresses: list[tuple[int, int]] = []
|
|
100
|
+
|
|
101
|
+
async def progress(current: int, total: int) -> None:
|
|
102
|
+
progresses.append((current, total))
|
|
103
|
+
|
|
104
|
+
with TemporaryDirectory() as tmp, range_server(content=content, support_range=True) as (url, handler):
|
|
105
|
+
target = Path(tmp) / "video.bin"
|
|
106
|
+
|
|
107
|
+
path = await download(
|
|
108
|
+
url,
|
|
109
|
+
target,
|
|
110
|
+
progress=progress,
|
|
111
|
+
connections=4,
|
|
112
|
+
min_split_size=512,
|
|
113
|
+
chunk_size=128,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.assertEqual(Path(path), target)
|
|
117
|
+
self.assertEqual(target.read_bytes(), content)
|
|
118
|
+
self.assertEqual(progresses[-1], (len(content), len(content)))
|
|
119
|
+
range_gets = [range_header for method, range_header in handler.requests if method == "GET" and range_header]
|
|
120
|
+
self.assertGreaterEqual(len(range_gets), 2)
|
|
121
|
+
self.assertFalse(list(Path(tmp).glob(".*.parsehub-tmp")))
|
|
122
|
+
|
|
123
|
+
async def test_download_falls_back_to_single_request_when_range_is_ignored(self):
|
|
124
|
+
content = b"fallback-body" * 100
|
|
125
|
+
|
|
126
|
+
with TemporaryDirectory() as tmp, range_server(content=content, support_range=False) as (url, handler):
|
|
127
|
+
target = Path(tmp) / "image.bin"
|
|
128
|
+
|
|
129
|
+
await download(url, target, connections=4, min_split_size=10, chunk_size=32)
|
|
130
|
+
|
|
131
|
+
self.assertEqual(target.read_bytes(), content)
|
|
132
|
+
self.assertIn(("GET", "bytes=0-0"), handler.requests)
|
|
133
|
+
self.assertIn(("GET", None), handler.requests)
|
|
134
|
+
self.assertFalse(list(Path(tmp).glob(".*.parsehub-tmp")))
|
|
135
|
+
|
|
136
|
+
async def test_download_keeps_existing_file_when_request_fails(self):
|
|
137
|
+
with TemporaryDirectory() as tmp, range_server(content=b"new", support_range=True, fail_all=True) as (url, _):
|
|
138
|
+
target = Path(tmp) / "video.bin"
|
|
139
|
+
target.write_bytes(b"old")
|
|
140
|
+
|
|
141
|
+
with self.assertRaises(DownloadError):
|
|
142
|
+
await download(url, target, connections=4, max_retries=0)
|
|
143
|
+
|
|
144
|
+
self.assertEqual(target.read_bytes(), b"old")
|
|
145
|
+
self.assertFalse(list(Path(tmp).glob(".*.parsehub-tmp")))
|
|
146
|
+
|
|
147
|
+
async def test_connections_one_uses_single_request(self):
|
|
148
|
+
content = b"single" * 200
|
|
149
|
+
|
|
150
|
+
with TemporaryDirectory() as tmp, range_server(content=content, support_range=True) as (url, handler):
|
|
151
|
+
target = Path(tmp) / "single.bin"
|
|
152
|
+
|
|
153
|
+
await download(url, target, connections=1, min_split_size=10)
|
|
154
|
+
|
|
155
|
+
self.assertEqual(target.read_bytes(), content)
|
|
156
|
+
range_gets = [range_header for method, range_header in handler.requests if method == "GET" and range_header]
|
|
157
|
+
self.assertEqual(range_gets, [])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
unittest.main()
|
|
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
|