parsehub 2.3.5__tar.gz → 2.3.7__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.3.5/src/parsehub.egg-info → parsehub-2.3.7}/PKG-INFO +1 -1
- {parsehub-2.3.5 → parsehub-2.3.7}/pyproject.toml +1 -1
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/bilibili.py +4 -1
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/tiktok.py +2 -2
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/twitter.py +1 -3
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/bilibili.py +3 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/tiktok.py +68 -106
- {parsehub-2.3.5 → parsehub-2.3.7/src/parsehub.egg-info}/PKG-INFO +1 -1
- {parsehub-2.3.5 → parsehub-2.3.7}/LICENSE +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/README.md +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/setup.cfg +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/cli.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/cli_config.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/config/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/config/config.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/errors.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/base/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/base/base.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/base/ytdlp.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/bangumi.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/coolapk.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/douban.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/douyin.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/facebook.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/instagram.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/kuaishou.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/medium.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/pipix.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/snapchat.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/threads.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/tieba.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/toutiao.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/weibo.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/weixin.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/wxchannels.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/xhs.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/xiaoheihe.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/youtube.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/zhihu.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/parsers/parser/zuiyou.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/bangumi.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/coolapk.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/douban.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/douyin.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/instagram.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/kuaishou.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/medium.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/pipix.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/threads.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/tieba.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/toutiao.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/twitter.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/weibo.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/weixin.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/wxchannels.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/xhs.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/xiaoheihe.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/zhihu.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/provider_api/zuiyou.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/__init__.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/callback.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/media_file.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/media_ref.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/platform.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/post.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/types/result.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/utils/downloader.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/utils/helpers.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub/utils/media_info.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub.egg-info/SOURCES.txt +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub.egg-info/dependency_links.txt +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub.egg-info/entry_points.txt +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub.egg-info/requires.txt +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/src/parsehub.egg-info/top_level.txt +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/test/test_cli.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/test/test_cli_config.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/test/test_core_offline.py +0 -0
- {parsehub-2.3.5 → parsehub-2.3.7}/test/test_downloader.py +0 -0
|
@@ -31,7 +31,10 @@ class BiliParse(BaseParser):
|
|
|
31
31
|
__redirect_keywords__ = ["b23.tv", "bili2233.cn"]
|
|
32
32
|
|
|
33
33
|
async def _do_parse(self, raw_url: str) -> YtVideoParseResult | BiliVideoParseResult | ImageParseResult:
|
|
34
|
-
|
|
34
|
+
parsed = urlparse(raw_url)
|
|
35
|
+
if parsed.hostname == "mall.bilibili.com":
|
|
36
|
+
raise ParseError("不支持解析商品")
|
|
37
|
+
if parsed.path.startswith("/bangumi/"):
|
|
35
38
|
raise ParseError("不支持解析番剧和影视")
|
|
36
39
|
if await self.is_dynamic(raw_url):
|
|
37
40
|
dynamic = await self.get_dynamic_info(raw_url)
|
|
@@ -235,8 +235,8 @@ class TikTokApiResult:
|
|
|
235
235
|
image_list.append(
|
|
236
236
|
ImageRef(
|
|
237
237
|
url=url,
|
|
238
|
-
height=as_int(display_image.get("height") or
|
|
239
|
-
width=as_int(display_image.get("width") or
|
|
238
|
+
height=as_int(display_image.get("height") or image.get("imageHeight")),
|
|
239
|
+
width=as_int(display_image.get("width") or image.get("imageWidth")),
|
|
240
240
|
)
|
|
241
241
|
)
|
|
242
242
|
|
|
@@ -44,9 +44,7 @@ class TwitterParser(BaseParser):
|
|
|
44
44
|
try:
|
|
45
45
|
return await Twitter(self.proxy, cookie=cookie).fetch_tweet(url)
|
|
46
46
|
except Exception as e2:
|
|
47
|
-
raise ParseError(
|
|
48
|
-
f"Twitter 账号无权限或已被封禁: {e2}\n\n使用的 Cookie: {self.cookie}"
|
|
49
|
-
) from e2
|
|
47
|
+
raise ParseError(f"{e2}\n\n使用的 Cookie: {self.cookie}") from e2
|
|
50
48
|
|
|
51
49
|
stages.append(FallbackStage("cookie", fetch_cookie, cookie=True))
|
|
52
50
|
|
|
@@ -57,6 +57,9 @@ class BiliAPI:
|
|
|
57
57
|
headers |= {
|
|
58
58
|
"referer": f"https://t.bilibili.com/{dyn_id}",
|
|
59
59
|
}
|
|
60
|
+
# buvid3 失效会触发 -352 风控,每次现取一枚匿名指纹,覆盖配置里的旧值
|
|
61
|
+
b3, b4 = await self.get_buvid()
|
|
62
|
+
cookie = {**(cookie or {}), "buvid3": b3, "buvid4": b4}
|
|
60
63
|
response = await self._get_client().get(
|
|
61
64
|
"https://api.bilibili.com/x/polymer/web-dynamic/v1/detail",
|
|
62
65
|
headers=headers,
|
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
2
4
|
import html
|
|
3
5
|
import json
|
|
4
6
|
import re
|
|
5
7
|
from typing import Any, NamedTuple, cast
|
|
6
|
-
from urllib.parse import
|
|
8
|
+
from urllib.parse import urlparse
|
|
7
9
|
|
|
8
10
|
import httpx
|
|
9
11
|
|
|
12
|
+
from ..errors import ContentUnavailableError
|
|
10
13
|
from ..utils.helpers import UA
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
TIKTOK_ITEM_PAGE = "https://www.tiktok.com/@_/video/{aweme_id}"
|
|
13
16
|
|
|
14
|
-
FACEBOOK_EXTERNAL_HIT_UA = "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)"
|
|
15
17
|
UNIVERSAL_DATA_RE = re.compile(
|
|
16
18
|
r'<script[^>]+id=["\']__UNIVERSAL_DATA_FOR_REHYDRATION__["\'][^>]*>(?P<json>.*?)</script>',
|
|
17
19
|
re.DOTALL,
|
|
18
20
|
)
|
|
19
21
|
|
|
20
|
-
TIKTOK_HEADERS = {
|
|
21
|
-
"User-Agent": UA,
|
|
22
|
-
"Referer": "https://www.tiktok.com/",
|
|
23
|
-
"x-ladon": "Hello From Evil0ctal!",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
TIKTOK_WEB_HEADERS = {
|
|
27
23
|
"User-Agent": UA,
|
|
28
24
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
@@ -48,7 +44,7 @@ class TikTokWebCrawler:
|
|
|
48
44
|
max_retries: int = 3,
|
|
49
45
|
timeout: int = 15,
|
|
50
46
|
):
|
|
51
|
-
self.headers = dict(
|
|
47
|
+
self.headers = dict(TIKTOK_WEB_HEADERS)
|
|
52
48
|
if user_agent:
|
|
53
49
|
self.headers["User-Agent"] = user_agent
|
|
54
50
|
self.cookies = httpx.Cookies()
|
|
@@ -58,30 +54,16 @@ class TikTokWebCrawler:
|
|
|
58
54
|
self.max_retries = max_retries
|
|
59
55
|
self.timeout = timeout
|
|
60
56
|
|
|
61
|
-
async def parse(self, url: str) -> dict:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
item_ref = self.extract_item_ref_from_url(resolved_url)
|
|
68
|
-
if not item_ref:
|
|
69
|
-
raise ValueError(f"无法从链接中提取作品 ID: {resolved_url}")
|
|
70
|
-
return await self.fetch_one_video(item_ref.aweme_id)
|
|
71
|
-
except Exception as exc:
|
|
72
|
-
primary_error = exc
|
|
73
|
-
|
|
74
|
-
if item_ref and item_ref.media_type == "photo":
|
|
75
|
-
raise RuntimeError(f"获取 TikTok 图文作品失败: {primary_error}") from primary_error
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
return await self.fetch_video_from_web(url, expected_aweme_id=item_ref.aweme_id if item_ref else None)
|
|
79
|
-
except Exception as web_error:
|
|
80
|
-
raise RuntimeError(f"获取 TikTok 作品失败: feed={primary_error}; web={web_error}") from web_error
|
|
57
|
+
async def parse(self, url: str) -> dict[str, Any]:
|
|
58
|
+
resolved_url = await self.resolve_url(url)
|
|
59
|
+
item_ref = self.extract_item_ref_from_url(resolved_url)
|
|
60
|
+
if not item_ref:
|
|
61
|
+
raise ValueError(f"无法从链接中提取作品 ID: {resolved_url}")
|
|
62
|
+
return await self.fetch_item(item_ref.aweme_id)
|
|
81
63
|
|
|
82
|
-
def _client(self
|
|
64
|
+
def _client(self) -> httpx.AsyncClient:
|
|
83
65
|
return httpx.AsyncClient(
|
|
84
|
-
headers=
|
|
66
|
+
headers=self.headers,
|
|
85
67
|
timeout=self.timeout,
|
|
86
68
|
follow_redirects=True,
|
|
87
69
|
proxy=self.proxy,
|
|
@@ -120,85 +102,24 @@ class TikTokWebCrawler:
|
|
|
120
102
|
raise ValueError("TikTok 页面不可用,可能是地区、代理或链接问题")
|
|
121
103
|
return resolved
|
|
122
104
|
|
|
123
|
-
async def
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return url
|
|
127
|
-
|
|
128
|
-
headers = dict(TIKTOK_WEB_HEADERS)
|
|
129
|
-
headers["User-Agent"] = FACEBOOK_EXTERNAL_HIT_UA
|
|
130
|
-
async with self._client(headers=headers) as client:
|
|
131
|
-
response = await client.head(url)
|
|
132
|
-
if response.status_code >= 400:
|
|
133
|
-
response = await client.get(url)
|
|
134
|
-
response.raise_for_status()
|
|
135
|
-
return str(response.url)
|
|
136
|
-
|
|
137
|
-
async def fetch_one_video(self, aweme_id: str) -> dict[str, Any]:
|
|
138
|
-
params = {
|
|
139
|
-
"iid": 7318518857994389254,
|
|
140
|
-
"device_id": 7318517321748022790,
|
|
141
|
-
"channel": "googleplay",
|
|
142
|
-
"app_name": "musical_ly",
|
|
143
|
-
"version_code": "300904",
|
|
144
|
-
"device_platform": "android",
|
|
145
|
-
"device_type": "SM-ASUS_Z01QD",
|
|
146
|
-
"os_version": "9",
|
|
147
|
-
"aweme_id": aweme_id,
|
|
148
|
-
}
|
|
149
|
-
endpoint = f"{TIKTOK_APP_FEED}?{urlencode(params)}"
|
|
150
|
-
last_error: Exception | None = None
|
|
151
|
-
|
|
152
|
-
for attempt in range(self.max_retries):
|
|
153
|
-
try:
|
|
154
|
-
async with self._client() as client:
|
|
155
|
-
response = await client.get(endpoint)
|
|
156
|
-
response.raise_for_status()
|
|
157
|
-
payload = response.json()
|
|
158
|
-
|
|
159
|
-
aweme_list = payload.get("aweme_list") or []
|
|
160
|
-
for item in aweme_list:
|
|
161
|
-
if str(item.get("aweme_id")) == str(aweme_id):
|
|
162
|
-
return cast(dict[str, Any], item)
|
|
163
|
-
|
|
164
|
-
if aweme_list:
|
|
165
|
-
first_id = aweme_list[0].get("aweme_id")
|
|
166
|
-
raise RuntimeError(f"返回作品 ID 不匹配: expected={aweme_id}, got={first_id}")
|
|
167
|
-
|
|
168
|
-
status_msg = payload.get("status_msg") or payload.get("statusMessage") or payload
|
|
169
|
-
raise RuntimeError(f"接口未返回 aweme_list: {status_msg}")
|
|
170
|
-
except Exception as exc:
|
|
171
|
-
last_error = exc
|
|
172
|
-
if attempt + 1 < self.max_retries:
|
|
173
|
-
await asyncio.sleep(1)
|
|
174
|
-
|
|
175
|
-
raise RuntimeError(f"获取 TikTok 作品失败: {last_error}")
|
|
176
|
-
|
|
177
|
-
async def fetch_video_from_web(self, url_or_text: str, expected_aweme_id: str | None = None) -> dict[str, Any]:
|
|
178
|
-
url = await self.resolve_web_url(url_or_text)
|
|
179
|
-
item_ref = self.extract_item_ref_from_url(url)
|
|
180
|
-
if not item_ref:
|
|
181
|
-
raise ValueError(f"无法从链接中提取作品 ID: {url}")
|
|
182
|
-
if item_ref.media_type == "photo":
|
|
183
|
-
raise ValueError("TikTok 图文作品不支持 Web hydration fallback")
|
|
184
|
-
|
|
185
|
-
webpage = await self.download_webpage(url)
|
|
105
|
+
async def fetch_item(self, aweme_id: str) -> dict[str, Any]:
|
|
106
|
+
# 图文也要请求 /video/ 页面, /photo/ 页面里没有 webapp.video-detail
|
|
107
|
+
webpage = await self.download_webpage(TIKTOK_ITEM_PAGE.format(aweme_id=aweme_id))
|
|
186
108
|
universal_data = self._search_universal_data(webpage)
|
|
187
109
|
if not universal_data:
|
|
188
110
|
raise RuntimeError("无法从页面提取 __UNIVERSAL_DATA_FOR_REHYDRATION__")
|
|
189
111
|
|
|
190
112
|
item = self._extract_web_item(universal_data)
|
|
191
113
|
item_id = str(item.get("aweme_id") or item.get("id") or "")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
raise RuntimeError(f"返回作品 ID 不匹配: expected={expected_id}, got={item_id}")
|
|
114
|
+
if item_id and item_id != aweme_id:
|
|
115
|
+
raise RuntimeError(f"返回作品 ID 不匹配: expected={aweme_id}, got={item_id}")
|
|
195
116
|
if item_id and not item.get("aweme_id"):
|
|
196
117
|
item["aweme_id"] = item_id
|
|
197
118
|
return item
|
|
198
119
|
|
|
199
120
|
async def download_webpage(self, url: str) -> str:
|
|
200
|
-
async with self._client(
|
|
201
|
-
|
|
121
|
+
async with self._client() as client:
|
|
122
|
+
webpage = ""
|
|
202
123
|
for attempt in range(self.max_retries):
|
|
203
124
|
response = await client.get(url)
|
|
204
125
|
if urlparse(str(response.url)).path == "/login":
|
|
@@ -207,10 +128,51 @@ class TikTokWebCrawler:
|
|
|
207
128
|
webpage = response.text
|
|
208
129
|
if self._search_universal_data(webpage):
|
|
209
130
|
return webpage
|
|
210
|
-
|
|
131
|
+
if self._solve_challenge(client, webpage):
|
|
132
|
+
continue
|
|
211
133
|
if attempt + 1 < self.max_retries:
|
|
212
134
|
await asyncio.sleep(1)
|
|
213
|
-
return
|
|
135
|
+
return webpage
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _html_class(webpage: str, element_id: str) -> str | None:
|
|
139
|
+
match = re.search(rf'<[^>]+\bid="{element_id}"[^>]*>', webpage)
|
|
140
|
+
if not match:
|
|
141
|
+
return None
|
|
142
|
+
class_match = re.search(r'\bclass="([^"]*)"', match.group(0))
|
|
143
|
+
return class_match.group(1) if class_match else None
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _solve_challenge(cls, client: httpx.AsyncClient, webpage: str) -> bool:
|
|
147
|
+
"""解 WAF 的 sha256 计算挑战并写入 cookie, 做法同 yt-dlp"""
|
|
148
|
+
encoded = cls._html_class(webpage, "cs")
|
|
149
|
+
cookie_name = cls._html_class(webpage, "wci")
|
|
150
|
+
if not encoded or not cookie_name:
|
|
151
|
+
return False
|
|
152
|
+
try:
|
|
153
|
+
challenge = json.loads(base64.b64decode(f"{encoded}==="))
|
|
154
|
+
expected_digest = base64.b64decode(challenge["v"]["c"])
|
|
155
|
+
base_hash = hashlib.sha256(base64.b64decode(challenge["v"]["a"]))
|
|
156
|
+
except (ValueError, KeyError, TypeError):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
for i in range(1_000_001):
|
|
160
|
+
number = str(i).encode()
|
|
161
|
+
test_hash = base_hash.copy()
|
|
162
|
+
test_hash.update(number)
|
|
163
|
+
if test_hash.digest() == expected_digest:
|
|
164
|
+
challenge["d"] = base64.b64encode(number).decode()
|
|
165
|
+
break
|
|
166
|
+
else:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
cookie_value = base64.b64encode(json.dumps(challenge, separators=(",", ":")).encode()).decode()
|
|
170
|
+
client.cookies.set(cookie_name, cookie_value, domain=".tiktok.com")
|
|
171
|
+
rci_name = cls._html_class(webpage, "rci")
|
|
172
|
+
rci_value = cls._html_class(webpage, "rs")
|
|
173
|
+
if rci_name and rci_value:
|
|
174
|
+
client.cookies.set(rci_name, rci_value, domain=".tiktok.com")
|
|
175
|
+
return True
|
|
214
176
|
|
|
215
177
|
@staticmethod
|
|
216
178
|
def _search_universal_data(webpage: str) -> dict[str, Any]:
|
|
@@ -224,7 +186,7 @@ class TikTokWebCrawler:
|
|
|
224
186
|
data = json.loads(raw)
|
|
225
187
|
except json.JSONDecodeError:
|
|
226
188
|
return {}
|
|
227
|
-
return data.get("__DEFAULT_SCOPE__") or {}
|
|
189
|
+
return cast(dict[str, Any], data.get("__DEFAULT_SCOPE__") or {})
|
|
228
190
|
|
|
229
191
|
@staticmethod
|
|
230
192
|
def _extract_web_item(universal_data: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -236,10 +198,10 @@ class TikTokWebCrawler:
|
|
|
236
198
|
status = 0
|
|
237
199
|
item = ((detail.get("itemInfo") or {}).get("itemStruct")) or {}
|
|
238
200
|
if item:
|
|
239
|
-
return item
|
|
201
|
+
return cast(dict[str, Any], item)
|
|
240
202
|
if status in (10216, 10222):
|
|
241
203
|
raise RuntimeError("这个 TikTok 内容需要登录或无权访问")
|
|
242
|
-
if status == 10204:
|
|
243
|
-
raise RuntimeError("当前 IP 被 TikTok 阻止访问这个内容")
|
|
244
204
|
status_msg = detail.get("statusMsg") or detail.get("statusMessage") or status
|
|
205
|
+
if status == 10204:
|
|
206
|
+
raise ContentUnavailableError(f"TikTok 作品不存在或不可见: {status_msg}")
|
|
245
207
|
raise RuntimeError(f"页面中没有作品详情,status={status_msg}")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|