astrocrawl 0.1.0__py3-none-any.whl
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.
- astrocrawl/__init__.py +7 -0
- astrocrawl/__main__.py +6 -0
- astrocrawl/_constants.py +376 -0
- astrocrawl/_json_compat.py +26 -0
- astrocrawl/_packaged.py +54 -0
- astrocrawl/_path_strategy.py +148 -0
- astrocrawl/_retry_strategy.py +64 -0
- astrocrawl/_startup.py +99 -0
- astrocrawl/_types.py +255 -0
- astrocrawl/_version.py +6 -0
- astrocrawl/ai/__init__.py +88 -0
- astrocrawl/ai/_client.py +426 -0
- astrocrawl/ai/_config.py +146 -0
- astrocrawl/ai/_constraint.py +23 -0
- astrocrawl/ai/_errors.py +61 -0
- astrocrawl/ai/_observability.py +69 -0
- astrocrawl/ai/_profile.py +61 -0
- astrocrawl/ai/_provider.py +88 -0
- astrocrawl/ai/_provider_registry.py +92 -0
- astrocrawl/ai/_rate_limiter.py +152 -0
- astrocrawl/ai/_types.py +121 -0
- astrocrawl/ai/_usage_tracker.py +48 -0
- astrocrawl/ai/providers/__init__.py +7 -0
- astrocrawl/ai/providers/anthropic.py +333 -0
- astrocrawl/ai/providers/google.py +298 -0
- astrocrawl/ai/providers/openai.py +427 -0
- astrocrawl/browser/__init__.py +5 -0
- astrocrawl/browser/_device_caps.py +23 -0
- astrocrawl/browser/_domain_memory.py +54 -0
- astrocrawl/browser/_preview.py +299 -0
- astrocrawl/browser/_preview_inject.js +480 -0
- astrocrawl/browser/_retry.py +41 -0
- astrocrawl/browser/_slot_pool.py +279 -0
- astrocrawl/browser/browser_pool.py +848 -0
- astrocrawl/browser/context_pool.py +179 -0
- astrocrawl/browser/navigation.py +38 -0
- astrocrawl/browser/page_pool.py +79 -0
- astrocrawl/cli/__init__.py +5 -0
- astrocrawl/cli/_i18n.py +58 -0
- astrocrawl/cli/main.py +1771 -0
- astrocrawl/config.py +348 -0
- astrocrawl/crawler/__init__.py +16 -0
- astrocrawl/crawler/_url_gate.py +82 -0
- astrocrawl/crawler/engine.py +2148 -0
- astrocrawl/crawler/liveness.py +53 -0
- astrocrawl/crawler/outcomes.py +381 -0
- astrocrawl/crawler/progress.py +204 -0
- astrocrawl/crawler/signals.py +89 -0
- astrocrawl/crawler/supervisors.py +112 -0
- astrocrawl/diagnostics.py +256 -0
- astrocrawl/gui/__init__.py +1 -0
- astrocrawl/gui/_ai_profile_page.py +750 -0
- astrocrawl/gui/_animated_bar.py +206 -0
- astrocrawl/gui/_delegates.py +132 -0
- astrocrawl/gui/_i18n.py +70 -0
- astrocrawl/gui/_preview_panel.py +455 -0
- astrocrawl/gui/_preview_session.py +209 -0
- astrocrawl/gui/_proxy_endpoint_dialog.py +215 -0
- astrocrawl/gui/_proxy_profile_page.py +861 -0
- astrocrawl/gui/_route_settings_page.py +347 -0
- astrocrawl/gui/_style.py +105 -0
- astrocrawl/gui/_table_page.py +228 -0
- astrocrawl/gui/_tokens.py +40 -0
- astrocrawl/gui/advanced_dialog.py +611 -0
- astrocrawl/gui/completion_dialog.py +194 -0
- astrocrawl/gui/crawl_session.py +211 -0
- astrocrawl/gui/main_window.py +967 -0
- astrocrawl/gui/proxy_health_bar.py +159 -0
- astrocrawl/gui/rules_dialog.py +2798 -0
- astrocrawl/gui/theme.py +176 -0
- astrocrawl/gui/theme_dialog.py +279 -0
- astrocrawl/gui/thread.py +80 -0
- astrocrawl/gui/title_bar.py +61 -0
- astrocrawl/gui/translations/astrocrawl_gui_zh_CN.qm +0 -0
- astrocrawl/gui/worker_status_bar.py +76 -0
- astrocrawl/health.py +58 -0
- astrocrawl/health_monitor.py +175 -0
- astrocrawl/main.py +107 -0
- astrocrawl/network/__init__.py +16 -0
- astrocrawl/network/_fetch.py +152 -0
- astrocrawl/network/robots.py +319 -0
- astrocrawl/network/sitemap.py +554 -0
- astrocrawl/network/throttling.py +206 -0
- astrocrawl/proxy/__init__.py +57 -0
- astrocrawl/proxy/_config.py +292 -0
- astrocrawl/proxy/_consumers.py +15 -0
- astrocrawl/proxy/_hook.py +44 -0
- astrocrawl/proxy/_probe.py +46 -0
- astrocrawl/proxy/_proxy.py +451 -0
- astrocrawl/proxy/_session.py +232 -0
- astrocrawl/resilience.py +107 -0
- astrocrawl/rules/__init__.py +104 -0
- astrocrawl/rules/_ai.py +294 -0
- astrocrawl/rules/_chatml.py +47 -0
- astrocrawl/rules/_extractor.py +274 -0
- astrocrawl/rules/_html_preprocess.py +103 -0
- astrocrawl/rules/_io.py +239 -0
- astrocrawl/rules/_lifecycle.py +170 -0
- astrocrawl/rules/_loader.py +389 -0
- astrocrawl/rules/_markdown.py +25 -0
- astrocrawl/rules/_matcher.py +157 -0
- astrocrawl/rules/_prompt_template_position.txt +42 -0
- astrocrawl/rules/_prompt_template_type.txt +114 -0
- astrocrawl/rules/_schema.py +471 -0
- astrocrawl/rules/_source.py +652 -0
- astrocrawl/rules/_state.py +171 -0
- astrocrawl/rules/_template.py +292 -0
- astrocrawl/rules/_transform.py +122 -0
- astrocrawl/storage/__init__.py +21 -0
- astrocrawl/storage/_protocol.py +89 -0
- astrocrawl/storage/db.py +728 -0
- astrocrawl/storage/writer.py +117 -0
- astrocrawl/utils/__init__.py +17 -0
- astrocrawl/utils/_atomic.py +61 -0
- astrocrawl/utils/html.py +227 -0
- astrocrawl/utils/logging.py +106 -0
- astrocrawl/utils/preferences.py +579 -0
- astrocrawl/utils/url.py +118 -0
- astrocrawl-0.1.0.dist-info/METADATA +1327 -0
- astrocrawl-0.1.0.dist-info/RECORD +124 -0
- astrocrawl-0.1.0.dist-info/WHEEL +5 -0
- astrocrawl-0.1.0.dist-info/entry_points.txt +7 -0
- astrocrawl-0.1.0.dist-info/licenses/LICENSE +202 -0
- astrocrawl-0.1.0.dist-info/top_level.txt +1 -0
astrocrawl/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from astrocrawl._version import __version__, __version_info__
|
|
4
|
+
from astrocrawl.config import DEFAULT_CONFIG, CrawlerConfig
|
|
5
|
+
from astrocrawl.crawler.engine import AsyncCrawler
|
|
6
|
+
|
|
7
|
+
__all__ = ["__version__", "__version_info__", "CrawlerConfig", "AsyncCrawler", "DEFAULT_CONFIG"]
|
astrocrawl/__main__.py
ADDED
astrocrawl/_constants.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# ── 解析规则引擎 ──────────────────────────────────────────────────────────────
|
|
4
|
+
# 使用者: rules/_schema.py / rules/_loader.py / rules/_extractor.py
|
|
5
|
+
MAX_RULES_TOTAL = 2000 # N11: 规则总数上限
|
|
6
|
+
MAX_FALLBACK_DEPTH = 3 # S22: fallback 链最大深度(主 + 2 层)
|
|
7
|
+
MAX_FIELDS_PER_RULE = 50 # 单规则最大字段数
|
|
8
|
+
MAX_FIELD_NAME_LENGTH = 64 # 字段名最大长度
|
|
9
|
+
MULTIPLE_MAX_ITEMS = 1000 # S26: multiple:true 最大返回项数
|
|
10
|
+
SELECTOR_TIMEOUT_PER_RULE = 5 # N18: 单规则选择器执行超时 (秒)
|
|
11
|
+
MAX_RULE_FILE_SIZE = 64 * 1024 # S14: 单规则文件最大 64KB
|
|
12
|
+
MAX_RULES_CACHE_SIZE = 50 * 1024 * 1024 # S16: 规则缓存总量 ≤ 50MB
|
|
13
|
+
MAX_JSON_DEPTH = 20 # S24: JSON 最大嵌套深度
|
|
14
|
+
RULES_TMP_MAX_AGE_HOURS = 24 # S15: 过期 .tmp 文件清理阈值 (小时)
|
|
15
|
+
SOURCE_PRIORITY = {"pip": 0, "remote": 1, "user": 2} # 规则源优先级,使用者: rules/_loader.py rules/_matcher.py
|
|
16
|
+
|
|
17
|
+
# ── 远程规则源 ──────────────────────────────────────────────────────────────
|
|
18
|
+
# 使用者: rules/_source.py SourceManager
|
|
19
|
+
MANIFEST_MAX_BYTES = 1 * 1024 * 1024 # S10: Manifest 响应体最大 1MB
|
|
20
|
+
SOURCE_DOWNLOAD_TIMEOUT = 30 # S08: 下载超时 (秒)
|
|
21
|
+
SOURCE_DAILY_UPDATE_LIMIT = 12 # N9: 单源每日更新次数上限
|
|
22
|
+
DOWNLOAD_CONCURRENCY_GLOBAL = 5 # S38: 全局下载并发上限
|
|
23
|
+
DOWNLOAD_CONCURRENCY_PER_SOURCE = 2 # S38: 单源下载并发上限
|
|
24
|
+
MAX_REDIRECTS = 5 # S03: HTTP 重定向最大次数
|
|
25
|
+
MAX_SOURCE_URL_LENGTH = 2048 # URL 长度上限
|
|
26
|
+
SOURCE_DEGRADED_COOLDOWN = 600 # M2: Degraded 源冷却时间 (秒),对标代理 30s
|
|
27
|
+
|
|
28
|
+
# ── AI 生成 ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
RULE_NAME_MAX_LENGTH = 64 # S11: 规则 name 最大长度
|
|
31
|
+
RULE_NAME_PATTERN = r"^[a-z0-9_-]+$" # S11: 规则 name 合法字符
|
|
32
|
+
ATTR_NAME_PATTERN = r"^[a-zA-Z0-9_-]+$" # S29: attr 名合法字符
|
|
33
|
+
CURRENCY_SYMBOLS = frozenset({"¥", "$", "€", "£", "₹", "₩", "₽", "₺", "R$"})
|
|
34
|
+
TRANSFORM_MEMORY_MULTIPLIER = 5 # N104: replace 内存放大倍数上限
|
|
35
|
+
|
|
36
|
+
# ── Worker 与队列 ────────────────────────────────────────────────────────────
|
|
37
|
+
# 使用者: crawler/engine.py _worker() / _pop_domain_aware() / run() 主循环
|
|
38
|
+
WORKER_IDLE_SLEEP = 0.5 # queue 空时 worker 休眠间隔(避免忙等待)
|
|
39
|
+
|
|
40
|
+
RUN_EMPTY_QUEUE_WAIT = 1 # 队列首次空等待秒数(队列瞬时排空不立即退出)
|
|
41
|
+
RUN_EMPTY_QUEUE_CONFIRM = 5 # 队列二次确认等待秒数(排除网络延迟导致的重入队)
|
|
42
|
+
|
|
43
|
+
# ── 关闭与生命周期 ───────────────────────────────────────────────────────────
|
|
44
|
+
# 使用者: crawler/engine.py _run_worker_loop() finally / gui/thread.py CrawlerThread.run()
|
|
45
|
+
SHUTDOWN_PENDING_TIMEOUT = 15 # event loop 清理 pending tasks 超时
|
|
46
|
+
SHUTDOWN_EXECUTOR_TIMEOUT = 10 # default executor shutdown 超时
|
|
47
|
+
SHUTDOWN_ASYNCGEN_TIMEOUT = 10 # async generator shutdown 超时
|
|
48
|
+
IN_FLIGHT_DRAIN_TIMEOUT = 30 # 停止后等待 in_flight 自然排空超时
|
|
49
|
+
|
|
50
|
+
# ── Sitemap 与 robots.txt ────────────────────────────────────────────────────
|
|
51
|
+
# 使用者: network/sitemap.py SitemapDiscovery / network/robots.py RobotsCache
|
|
52
|
+
# 注: 重试次数与退避基数已统一使用 CrawlerConfig.max_retries / retry_backoff_base
|
|
53
|
+
SITEMAP_FETCH_TIMEOUT = 8 # sitemap 单次抓取超时 (秒)
|
|
54
|
+
SITEMAP_MAX_CONTENT_SIZE = 50 * 1024 * 1024 # 50 MiB per sitemap fetch
|
|
55
|
+
SITEMAP_MAX_DECOMPRESSED = 100 * 1024 * 1024 # 100 MiB decompressed
|
|
56
|
+
ROBOTS_FETCH_TIMEOUT = 5 # robots.txt 抓取超时 (秒)
|
|
57
|
+
ROBOTS_FETCH_MAX_CONCURRENT = 8 # robots.txt 并发抓取上限
|
|
58
|
+
ROBOTS_MAX_SIZE = 500 * 1024 # RFC 9309: 500 KiB 上限
|
|
59
|
+
|
|
60
|
+
# ── 浏览器上下文与页面管理 ───────────────────────────────────────────────────
|
|
61
|
+
# 使用者: browser/context_pool.py ContextPool / browser/page_pool.py PagePool
|
|
62
|
+
PAGE_CREATE_RETRIES = 3 # 页面创建最大重试
|
|
63
|
+
PAGE_CREATE_BACKOFF = 1.0 # 页面创建重试退避基数 (秒)
|
|
64
|
+
PAGE_CREATE_TIMEOUT = 15 # context.new_page() asyncio 超时 (秒)
|
|
65
|
+
PAGE_CLOSE_TIMEOUT = 5 # page.close() / context.close() asyncio 超时 (秒)
|
|
66
|
+
ABOUT_BLANK_ASYNCIO_TIMEOUT = 5.0 # remove_broken 中 about:blank goto 超时 (秒)
|
|
67
|
+
SLOT_CREATE_RETRIES = 3 # 槽位创建最大重试
|
|
68
|
+
SLOT_CREATE_BACKOFF = 1.0 # 槽位创建重试退避基数 (秒)
|
|
69
|
+
CONTEXT_CREATE_TIMEOUT = 30 # browser.new_context() asyncio 超时 (秒)
|
|
70
|
+
RELEASE_UNROUTE_TIMEOUT = 3 # 错误恢复时 page.unroute_all() 超时 (秒)
|
|
71
|
+
BROWSER_LAUNCH_TIMEOUT = 60 # Chromium 浏览器启动超时 (秒)
|
|
72
|
+
HARD_CLEANUP_TIMEOUT = 15 # finally 块中资源释放的最硬超时 (秒),不可被 asyncio.shield 绕过
|
|
73
|
+
|
|
74
|
+
# ── 纵深防御超时 ─────────────────────────────────────────────────────────────
|
|
75
|
+
# 使用者: crawler/engine.py _fetch_url() / browser/browser_pool.py / pipeline processors
|
|
76
|
+
# Playwright 原生超时 (page.goto timeout) 通过 cfg.page_timeout 配置。
|
|
77
|
+
# 以下 asyncio.wait_for 超时作为第二层兜底——当 Playwright 因 TCP SYN 挂起、
|
|
78
|
+
# CDP 通道中断等原因不触发自身超时时,asyncio 层强制终止。
|
|
79
|
+
CONTENT_READ_TIMEOUT = 15 # page.content() asyncio 超时 (秒)
|
|
80
|
+
PROCESS_URL_TIMEOUT = 240 # pipeline 单 URL 处理整体超时 (含 prefer_direct 代理回退余量)
|
|
81
|
+
FETCH_PROCESSOR_OVERHEAD = 15 # 非 fetch processor 的时间预算 + 安全余量 (秒)
|
|
82
|
+
DOMAIN_CONCURRENCY_ACQUIRE_TIMEOUT = 60 # 域并发 semaphore 获取超时 (秒)
|
|
83
|
+
|
|
84
|
+
# ── 卡死检测与上下文健康 ────────────────────────────────────────────────────
|
|
85
|
+
# 使用者: crawler/engine.py 主循环 / browser/browser_pool.py _health_check_loop()
|
|
86
|
+
WORKER_STUCK_TIMEOUT = 300 # 所有 worker 进度停滞阈值 (秒),超时强制退出
|
|
87
|
+
STUCK_IN_FLIGHT_TIMEOUT = 300 # queue=0 但 in_flight>0 的悬挂判定阈值 (秒)
|
|
88
|
+
CONTEXT_HEALTH_CHECK_INTERVAL = 30 # 空闲 slot 健康检查间隔 (秒)
|
|
89
|
+
BROWSER_GOTO_BUFFER_S = 5.0 # asyncio.wait_for 超时超过 Playwright 超时的缓冲(秒)
|
|
90
|
+
BROWSER_RESTART_TIMEOUT_MULT = 3 # Browser 重启超时为 HARD_CLEANUP_TIMEOUT 的倍数
|
|
91
|
+
|
|
92
|
+
# ── 代理健康管理 ─────────────────────────────────────────────────────────────
|
|
93
|
+
# 使用者: proxy/_proxy.py ProxyHealthTracker / ProxyManager
|
|
94
|
+
# 断路器模式 (Circuit Breaker): CLOSED → OPEN → HALF_OPEN → CLOSED
|
|
95
|
+
PROXY_FAILURE_THRESHOLD = 3 # 连续失败 → OPEN (完全熔断) 的阈值
|
|
96
|
+
PROXY_COOLDOWN = 30.0 # OPEN → HALF_OPEN 最短冷却时间 (秒)
|
|
97
|
+
PROXY_COOLDOWN_MAX = 120.0 # 冷却时间指数增长上界 (秒)
|
|
98
|
+
PROXY_HALF_OPEN_MIN_DURATION = 15.0 # HALF_OPEN 最短考察窗口 (秒)
|
|
99
|
+
PROXY_HALF_OPEN_MAX_FAILURES = 2 # HALF_OPEN 窗口内最大容忍失败数
|
|
100
|
+
PROXY_DECAY_SECONDS = 120.0 # 故障记录过期时间 (秒),超时后该次失败不再计入 health_score
|
|
101
|
+
PROXY_PROBE_INTERVAL = 5.0 # 后台 TCP 探测间隔 (秒)
|
|
102
|
+
PROXY_PROBE_TIMEOUT = 2.0 # 单次 TCP 连接超时 (秒)
|
|
103
|
+
PROXY_SCORE_WINDOW = 15.0 # health_score 故障密度窗口 (秒)
|
|
104
|
+
PROXY_SCORE_SUCCESS_DECAY = 30.0 # 成功奖励线性衰减 (秒)
|
|
105
|
+
PROXY_HEALTH_BAR_REFRESH = 3.0 # GUI 健康条刷新间隔 (秒)
|
|
106
|
+
|
|
107
|
+
# ── 域管理 ───────────────────────────────────────────────────────────────────
|
|
108
|
+
# 使用者: network/throttling.py / crawler/engine.py run()
|
|
109
|
+
|
|
110
|
+
DOMAIN_CLEANUP_AGE = 3600 # 域限流条目闲置后清理阈值 (秒)
|
|
111
|
+
DNS_CACHE_TTL = 300 # aiohttp DNS 缓存 TTL (秒)
|
|
112
|
+
|
|
113
|
+
# ── 后台维护周期 ─────────────────────────────────────────────────────────────
|
|
114
|
+
# 使用者: crawler/engine.py run() — HealthMonitor 调度
|
|
115
|
+
RETRY_MONITOR_INTERVAL = 10 # 失败重试监视器轮询间隔 (秒)
|
|
116
|
+
RETRY_MONITOR_BATCH = 50 # 每批原子重试回收最大数量
|
|
117
|
+
RATE_LIMITER_CLEANUP_INTERVAL = 600 # 域速率限制器过期条目清理间隔 (秒)
|
|
118
|
+
CONCURRENT_LIMITER_CLEANUP_INTERVAL = 300 # 域并发限制器过期条目清理间隔 (秒)
|
|
119
|
+
RESOURCE_MONITOR_INTERVAL = 60 # 资源监控 (内存/DB/队列) 间隔 (秒)
|
|
120
|
+
HASH_CLEANUP_INTERVAL = 1800 # 内容哈希表过期条目清理间隔 (秒)
|
|
121
|
+
HASH_MAX_AGE = 86400.0 # 内容哈希条目最大保留时间 (秒),24h
|
|
122
|
+
|
|
123
|
+
# ── 连接器 ───────────────────────────────────────────────────────────────────
|
|
124
|
+
# 使用者: crawler/engine.py run() — aiohttp TCPConnector
|
|
125
|
+
CONNECTOR_LIMIT = 100 # aiohttp 总连接数上限
|
|
126
|
+
CONNECTOR_LIMIT_PER_HOST = 10 # aiohttp 单主机连接数上限
|
|
127
|
+
|
|
128
|
+
# ── 诊断 ───────────────────────────────────────────────────────────────────────
|
|
129
|
+
# 使用者: diagnostics.py CrawlDiagnostics
|
|
130
|
+
HTTP_READ_LINE_TIMEOUT = 10.0 # HTTP 健康端点 readline() 超时 (秒)
|
|
131
|
+
|
|
132
|
+
# ── 日志 ─────────────────────────────────────────────────────────────────────
|
|
133
|
+
# 使用者: utils/logging.py / gui/main_window.py / browser/browser_pool.py (MAX_ERROR_MESSAGE_LENGTH)
|
|
134
|
+
FILE_LOG_MAX_BYTES = 10 * 1024 * 1024 # RotatingFileHandler 单文件最大字节数
|
|
135
|
+
FILE_LOG_BACKUP_COUNT = 3 # RotatingFileHandler 历史备份文件数
|
|
136
|
+
MAX_LOG_ITEMS = 500 # GUI 日志列表最大条目数 (超出时清理)
|
|
137
|
+
MAX_ERROR_MESSAGE_LENGTH = 500 # 错误消息截断长度 (防止日志溢出)
|
|
138
|
+
|
|
139
|
+
# ── GUI 控件物理极限 ──────────────────────────────────────────────────────────
|
|
140
|
+
QSPINBOX_MAX = 2147483647 # QSpinBox int32 上界,非业务上限
|
|
141
|
+
QDOUBLESPINBOX_MAX = 1.7976931348623157e308 # QDoubleSpinBox double 上界,非业务上限
|
|
142
|
+
QLINEEDIT_MAX = 32767 # QLineEdit Qt 内部默认 maxLength,非业务上限
|
|
143
|
+
|
|
144
|
+
# ── 下载候选扩展名 (对齐 ADR-0002 下载预设 + Scrapy LinkExtractor) ──────────────
|
|
145
|
+
# 使用者: utils/html.py _is_download_candidate() → extract_links_from_soup()
|
|
146
|
+
# 链接提取时按扩展名识别非 HTML 资源,当前阶段记录为 DOWNLOAD_CANDIDATE drop,
|
|
147
|
+
# 下载模块 (ADR-0002) 落地后路由到下载管线。
|
|
148
|
+
DOWNLOAD_EXTENSIONS = frozenset(
|
|
149
|
+
{
|
|
150
|
+
# archives
|
|
151
|
+
"tar",
|
|
152
|
+
"gz",
|
|
153
|
+
"tgz",
|
|
154
|
+
"bz2",
|
|
155
|
+
"xz",
|
|
156
|
+
"z",
|
|
157
|
+
"7z",
|
|
158
|
+
"zip",
|
|
159
|
+
"rar",
|
|
160
|
+
"jar",
|
|
161
|
+
"iso",
|
|
162
|
+
"dmg",
|
|
163
|
+
"exe",
|
|
164
|
+
"pkg",
|
|
165
|
+
"deb",
|
|
166
|
+
"rpm",
|
|
167
|
+
"apk",
|
|
168
|
+
"msi",
|
|
169
|
+
"msix",
|
|
170
|
+
"sigstore",
|
|
171
|
+
# data
|
|
172
|
+
"csv",
|
|
173
|
+
"tsv",
|
|
174
|
+
"json",
|
|
175
|
+
"xml",
|
|
176
|
+
"yaml",
|
|
177
|
+
"yml",
|
|
178
|
+
"sql",
|
|
179
|
+
"sqlite",
|
|
180
|
+
"db",
|
|
181
|
+
"dat",
|
|
182
|
+
"bin",
|
|
183
|
+
"log",
|
|
184
|
+
# ebooks
|
|
185
|
+
"epub",
|
|
186
|
+
"mobi",
|
|
187
|
+
"azw",
|
|
188
|
+
"azw3",
|
|
189
|
+
# documents
|
|
190
|
+
"pdf",
|
|
191
|
+
"doc",
|
|
192
|
+
"docx",
|
|
193
|
+
"xls",
|
|
194
|
+
"xlsx",
|
|
195
|
+
"ppt",
|
|
196
|
+
"pptx",
|
|
197
|
+
"odt",
|
|
198
|
+
"ods",
|
|
199
|
+
"odp",
|
|
200
|
+
"ps",
|
|
201
|
+
"pages",
|
|
202
|
+
"key",
|
|
203
|
+
"numbers",
|
|
204
|
+
# images (scrapy reference)
|
|
205
|
+
"mng",
|
|
206
|
+
"pct",
|
|
207
|
+
"bmp",
|
|
208
|
+
"gif",
|
|
209
|
+
"jpg",
|
|
210
|
+
"jpeg",
|
|
211
|
+
"png",
|
|
212
|
+
"pst",
|
|
213
|
+
"psp",
|
|
214
|
+
"tif",
|
|
215
|
+
"tiff",
|
|
216
|
+
"ai",
|
|
217
|
+
"drw",
|
|
218
|
+
"dxf",
|
|
219
|
+
"eps",
|
|
220
|
+
"svg",
|
|
221
|
+
"cdr",
|
|
222
|
+
"ico",
|
|
223
|
+
"webp",
|
|
224
|
+
# audio
|
|
225
|
+
"mp3",
|
|
226
|
+
"wma",
|
|
227
|
+
"ogg",
|
|
228
|
+
"wav",
|
|
229
|
+
"ra",
|
|
230
|
+
"aac",
|
|
231
|
+
"mid",
|
|
232
|
+
"au",
|
|
233
|
+
"aiff",
|
|
234
|
+
"flac",
|
|
235
|
+
# video
|
|
236
|
+
"3gp",
|
|
237
|
+
"asf",
|
|
238
|
+
"asx",
|
|
239
|
+
"avi",
|
|
240
|
+
"mov",
|
|
241
|
+
"mp4",
|
|
242
|
+
"mpg",
|
|
243
|
+
"qt",
|
|
244
|
+
"rm",
|
|
245
|
+
"swf",
|
|
246
|
+
"wmv",
|
|
247
|
+
"webm",
|
|
248
|
+
"mkv",
|
|
249
|
+
# web (not HTML)
|
|
250
|
+
"css",
|
|
251
|
+
"js",
|
|
252
|
+
"rss",
|
|
253
|
+
"atom",
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# ── 资源阻断与 Chromium 启动 ─────────────────────────────────────────────────
|
|
258
|
+
# 使用者: browser/browser_pool.py start() + route / crawler/engine.py run()
|
|
259
|
+
# 资源阻断已收编入下载子系统 (Download)。启用下载后由下载预设的 resource_type
|
|
260
|
+
# 门控 (Layer 1) 统一控制放行/阻断;未启用下载时全部阻断(向后兼容)。
|
|
261
|
+
BLOCKED_RESOURCE_TYPES = frozenset({"image", "font", "media", "websocket", "prefetch", "manifest"})
|
|
262
|
+
CHROMIUM_LAUNCH_ARGS = (
|
|
263
|
+
"--disable-blink-features=AutomationControlled",
|
|
264
|
+
"--disable-dev-shm-usage",
|
|
265
|
+
"--no-sandbox",
|
|
266
|
+
"--disable-gpu",
|
|
267
|
+
"--log-level=3", # 仅 ERROR 级日志,防止代理凭据在 Chromium 调试输出中泄漏
|
|
268
|
+
)
|
|
269
|
+
__all__ = [
|
|
270
|
+
# 解析规则引擎
|
|
271
|
+
"MAX_RULES_TOTAL",
|
|
272
|
+
"MAX_FALLBACK_DEPTH",
|
|
273
|
+
"MAX_FIELDS_PER_RULE",
|
|
274
|
+
"MAX_FIELD_NAME_LENGTH",
|
|
275
|
+
"MULTIPLE_MAX_ITEMS",
|
|
276
|
+
"SELECTOR_TIMEOUT_PER_RULE",
|
|
277
|
+
"MAX_RULE_FILE_SIZE",
|
|
278
|
+
"MAX_RULES_CACHE_SIZE",
|
|
279
|
+
"MAX_JSON_DEPTH",
|
|
280
|
+
"RULES_TMP_MAX_AGE_HOURS",
|
|
281
|
+
"SOURCE_PRIORITY",
|
|
282
|
+
"RULE_NAME_MAX_LENGTH",
|
|
283
|
+
"RULE_NAME_PATTERN",
|
|
284
|
+
"ATTR_NAME_PATTERN",
|
|
285
|
+
"CURRENCY_SYMBOLS",
|
|
286
|
+
"TRANSFORM_MEMORY_MULTIPLIER",
|
|
287
|
+
"MANIFEST_MAX_BYTES",
|
|
288
|
+
"SOURCE_DOWNLOAD_TIMEOUT",
|
|
289
|
+
"SOURCE_DAILY_UPDATE_LIMIT",
|
|
290
|
+
"SOURCE_DEGRADED_COOLDOWN",
|
|
291
|
+
"DOWNLOAD_CONCURRENCY_GLOBAL",
|
|
292
|
+
"DOWNLOAD_CONCURRENCY_PER_SOURCE",
|
|
293
|
+
"MAX_REDIRECTS",
|
|
294
|
+
"MAX_SOURCE_URL_LENGTH",
|
|
295
|
+
# Worker 与队列
|
|
296
|
+
"WORKER_IDLE_SLEEP",
|
|
297
|
+
"RUN_EMPTY_QUEUE_WAIT",
|
|
298
|
+
"RUN_EMPTY_QUEUE_CONFIRM",
|
|
299
|
+
# 关闭与生命周期
|
|
300
|
+
"SHUTDOWN_PENDING_TIMEOUT",
|
|
301
|
+
"SHUTDOWN_EXECUTOR_TIMEOUT",
|
|
302
|
+
"SHUTDOWN_ASYNCGEN_TIMEOUT",
|
|
303
|
+
"IN_FLIGHT_DRAIN_TIMEOUT",
|
|
304
|
+
# Sitemap 与 robots.txt
|
|
305
|
+
"SITEMAP_FETCH_TIMEOUT",
|
|
306
|
+
"SITEMAP_MAX_CONTENT_SIZE",
|
|
307
|
+
"SITEMAP_MAX_DECOMPRESSED",
|
|
308
|
+
"ROBOTS_FETCH_TIMEOUT",
|
|
309
|
+
"ROBOTS_FETCH_MAX_CONCURRENT",
|
|
310
|
+
"ROBOTS_MAX_SIZE",
|
|
311
|
+
# 浏览器上下文与页面管理
|
|
312
|
+
"PAGE_CREATE_RETRIES",
|
|
313
|
+
"PAGE_CREATE_BACKOFF",
|
|
314
|
+
"PAGE_CREATE_TIMEOUT",
|
|
315
|
+
"PAGE_CLOSE_TIMEOUT",
|
|
316
|
+
"ABOUT_BLANK_ASYNCIO_TIMEOUT",
|
|
317
|
+
"SLOT_CREATE_RETRIES",
|
|
318
|
+
"SLOT_CREATE_BACKOFF",
|
|
319
|
+
"CONTEXT_CREATE_TIMEOUT",
|
|
320
|
+
"RELEASE_UNROUTE_TIMEOUT",
|
|
321
|
+
"BROWSER_LAUNCH_TIMEOUT",
|
|
322
|
+
"HARD_CLEANUP_TIMEOUT",
|
|
323
|
+
# 纵深防御超时
|
|
324
|
+
"CONTENT_READ_TIMEOUT",
|
|
325
|
+
"PROCESS_URL_TIMEOUT",
|
|
326
|
+
"FETCH_PROCESSOR_OVERHEAD",
|
|
327
|
+
"DOMAIN_CONCURRENCY_ACQUIRE_TIMEOUT",
|
|
328
|
+
# 卡死检测与上下文健康
|
|
329
|
+
"WORKER_STUCK_TIMEOUT",
|
|
330
|
+
"STUCK_IN_FLIGHT_TIMEOUT",
|
|
331
|
+
"CONTEXT_HEALTH_CHECK_INTERVAL",
|
|
332
|
+
"BROWSER_GOTO_BUFFER_S",
|
|
333
|
+
"BROWSER_RESTART_TIMEOUT_MULT",
|
|
334
|
+
# 代理健康管理
|
|
335
|
+
"PROXY_FAILURE_THRESHOLD",
|
|
336
|
+
"PROXY_COOLDOWN",
|
|
337
|
+
"PROXY_COOLDOWN_MAX",
|
|
338
|
+
"PROXY_HALF_OPEN_MIN_DURATION",
|
|
339
|
+
"PROXY_HALF_OPEN_MAX_FAILURES",
|
|
340
|
+
"PROXY_DECAY_SECONDS",
|
|
341
|
+
"PROXY_PROBE_INTERVAL",
|
|
342
|
+
"PROXY_PROBE_TIMEOUT",
|
|
343
|
+
"PROXY_SCORE_WINDOW",
|
|
344
|
+
"PROXY_SCORE_SUCCESS_DECAY",
|
|
345
|
+
"PROXY_HEALTH_BAR_REFRESH",
|
|
346
|
+
# 域管理
|
|
347
|
+
"DOMAIN_CLEANUP_AGE",
|
|
348
|
+
"DNS_CACHE_TTL",
|
|
349
|
+
# 后台维护周期
|
|
350
|
+
"RETRY_MONITOR_INTERVAL",
|
|
351
|
+
"RETRY_MONITOR_BATCH",
|
|
352
|
+
"RATE_LIMITER_CLEANUP_INTERVAL",
|
|
353
|
+
"CONCURRENT_LIMITER_CLEANUP_INTERVAL",
|
|
354
|
+
"RESOURCE_MONITOR_INTERVAL",
|
|
355
|
+
"HASH_CLEANUP_INTERVAL",
|
|
356
|
+
"HASH_MAX_AGE",
|
|
357
|
+
# 诊断
|
|
358
|
+
"HTTP_READ_LINE_TIMEOUT",
|
|
359
|
+
# 连接器
|
|
360
|
+
"CONNECTOR_LIMIT",
|
|
361
|
+
"CONNECTOR_LIMIT_PER_HOST",
|
|
362
|
+
# 日志
|
|
363
|
+
"FILE_LOG_MAX_BYTES",
|
|
364
|
+
"FILE_LOG_BACKUP_COUNT",
|
|
365
|
+
"MAX_LOG_ITEMS",
|
|
366
|
+
"MAX_ERROR_MESSAGE_LENGTH",
|
|
367
|
+
# GUI 控件物理极限
|
|
368
|
+
"QSPINBOX_MAX",
|
|
369
|
+
"QDOUBLESPINBOX_MAX",
|
|
370
|
+
"QLINEEDIT_MAX",
|
|
371
|
+
# 下载候选扩展名
|
|
372
|
+
"DOWNLOAD_EXTENSIONS",
|
|
373
|
+
# 资源阻断与 Chromium 启动
|
|
374
|
+
"BLOCKED_RESOURCE_TYPES",
|
|
375
|
+
"CHROMIUM_LAUNCH_ARGS",
|
|
376
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""JSON 序列化兼容层 — 优先 orjson 加速,回退标准库 json。
|
|
2
|
+
|
|
3
|
+
策略: _json_dumps 始终返回 bytes 并以 \n 结尾(JSONL 格式)。
|
|
4
|
+
调用方可直接写入 io.BytesIO,无需 str→bytes 编解码。
|
|
5
|
+
机制: orjson 为可选加速后端([fast] extra),未安装时透明回退 stdlib json。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("astrocrawl.json_compat")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import orjson as _json_mod
|
|
18
|
+
|
|
19
|
+
def _json_dumps(obj: Any) -> bytes:
|
|
20
|
+
return _json_mod.dumps(obj, option=_json_mod.OPT_APPEND_NEWLINE)
|
|
21
|
+
|
|
22
|
+
except ImportError:
|
|
23
|
+
logger.debug("event=orjson_unavailable dumper=stdlib")
|
|
24
|
+
|
|
25
|
+
def _json_dumps(obj: Any) -> bytes:
|
|
26
|
+
return (json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8")
|
astrocrawl/_packaged.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""打包环境检测与路径适配。
|
|
2
|
+
|
|
3
|
+
打包工具(如 Nuitka)将 Python 应用编译为独立可执行文件。
|
|
4
|
+
此模块在应用入口最早被调用,负责检测运行环境并设置
|
|
5
|
+
PLAYWRIGHT_BROWSERS_PATH,使 Playwright 能找到捆绑的浏览器。
|
|
6
|
+
|
|
7
|
+
零外部依赖,可安全在 main() 中作为首条语句导入。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_packaged() -> bool:
|
|
18
|
+
"""任意打包环境。"""
|
|
19
|
+
return getattr(sys, "frozen", False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_bundle_dir() -> Path:
|
|
23
|
+
"""返回打包应用的根目录。
|
|
24
|
+
|
|
25
|
+
--standalone 模式:可执行文件所在目录。
|
|
26
|
+
--onefile 模式:临时解压目录(sys._MEIPASS)。
|
|
27
|
+
开发模式:项目根目录(向上两级)。
|
|
28
|
+
"""
|
|
29
|
+
if not is_packaged():
|
|
30
|
+
return Path(__file__).resolve().parent.parent
|
|
31
|
+
|
|
32
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
33
|
+
if meipass:
|
|
34
|
+
return Path(meipass)
|
|
35
|
+
|
|
36
|
+
return Path(sys.executable).resolve().parent
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def setup() -> None:
|
|
40
|
+
"""设置打包环境:配置路径与环境变量。
|
|
41
|
+
|
|
42
|
+
必须在任何 Playwright 相关代码之前调用。
|
|
43
|
+
非打包环境下为空操作。
|
|
44
|
+
"""
|
|
45
|
+
if not is_packaged():
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
bundle = get_bundle_dir()
|
|
49
|
+
|
|
50
|
+
# Playwright 浏览器路径
|
|
51
|
+
# 构建时通过 --include-data-dir 将 ~/.cache/ms-playwright/ 映射到此目录
|
|
52
|
+
browsers_path = bundle / "playwright_browsers"
|
|
53
|
+
if browsers_path.is_dir():
|
|
54
|
+
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(browsers_path)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""路径路由策略 — 主/回退双路径决策(ADR-0010 决策 11a)。
|
|
2
|
+
|
|
3
|
+
纯策略对象,零内部依赖(仅从 _types 导入 FetchErrorCategory 枚举)。
|
|
4
|
+
消费者按需取用:proxy/ 底座 + _path_strategy.py 自行组装回退逻辑。
|
|
5
|
+
|
|
6
|
+
对标: _retry_strategy.py 模式 — kernel 层独立文件,纯策略,消费者按需引入。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from astrocrawl._types import FetchErrorCategory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PathSwitch:
|
|
17
|
+
"""封装主路径与回退路径的通用策略。
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
main: 主抓取路径 ("proxy" | "direct")
|
|
21
|
+
fallback: 主路径耗尽时的次路径 (None | "proxy" | "direct")
|
|
22
|
+
trigger: 触发回退的条件 (None | "all_proxies_dead" | "connectivity_error")
|
|
23
|
+
scope: 回退生效的层级 ("slot" | "url")
|
|
24
|
+
on_exhausted: 次路径也耗尽时的行为 ("pause" | "fail")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# 四种 proxy_mode 的预置配置。for_mode() 据此查找模式 → 构造 PathSwitch。
|
|
28
|
+
# 增删模式只需修改此 dict,_VALID_MODES 和 for_mode() 自动同步。
|
|
29
|
+
_MODE_CONFIGS = {
|
|
30
|
+
"direct_only": {"main": "direct"},
|
|
31
|
+
"proxy_only": {"main": "proxy", "on_exhausted": "pause"},
|
|
32
|
+
"prefer_direct": {"main": "direct", "fallback": "proxy", "trigger": "connectivity_error", "scope": "url"},
|
|
33
|
+
"prefer_proxy": {"main": "proxy", "fallback": "direct", "trigger": "all_proxies_dead", "scope": "url"},
|
|
34
|
+
}
|
|
35
|
+
_VALID_MODES = frozenset(_MODE_CONFIGS.keys())
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
main: str,
|
|
40
|
+
fallback: Optional[str] = None,
|
|
41
|
+
trigger: Optional[str] = None,
|
|
42
|
+
scope: str = "url",
|
|
43
|
+
on_exhausted: str = "fail",
|
|
44
|
+
) -> None:
|
|
45
|
+
if main not in ("proxy", "direct"):
|
|
46
|
+
raise ValueError(f"main must be 'proxy' or 'direct', got {main!r}")
|
|
47
|
+
if fallback not in (None, "proxy", "direct"):
|
|
48
|
+
raise ValueError(f"fallback must be 'proxy', 'direct', or None, got {fallback!r}")
|
|
49
|
+
if trigger not in (None, "all_proxies_dead", "connectivity_error"):
|
|
50
|
+
raise ValueError(f"invalid trigger: {trigger!r}")
|
|
51
|
+
if scope not in ("slot", "url"):
|
|
52
|
+
raise ValueError(f"scope must be 'slot' or 'url', got {scope!r}")
|
|
53
|
+
if on_exhausted not in ("pause", "fail"):
|
|
54
|
+
raise ValueError(f"on_exhausted must be 'pause' or 'fail', got {on_exhausted!r}")
|
|
55
|
+
|
|
56
|
+
self.main = main
|
|
57
|
+
self.fallback = fallback
|
|
58
|
+
self.trigger = trigger
|
|
59
|
+
self.scope = scope
|
|
60
|
+
self.on_exhausted = on_exhausted
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def for_mode(cls, mode: str) -> "PathSwitch":
|
|
64
|
+
"""从 proxy_mode 配置字符串创建。
|
|
65
|
+
|
|
66
|
+
direct_only: 单路径直连
|
|
67
|
+
proxy_only: 单路径代理,全死暂停
|
|
68
|
+
prefer_direct: 直连优先,连通性错误时升级代理
|
|
69
|
+
prefer_proxy: 代理优先,全死时降级直连
|
|
70
|
+
"""
|
|
71
|
+
config = cls._MODE_CONFIGS.get(mode)
|
|
72
|
+
if config is None:
|
|
73
|
+
raise ValueError(f"Unknown proxy_mode: {mode!r}, valid: {sorted(cls._VALID_MODES)}")
|
|
74
|
+
return cls(**config)
|
|
75
|
+
|
|
76
|
+
# ── 属性查询 ────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def main_is_proxy(self) -> bool:
|
|
80
|
+
"""新槽位默认是否使用代理。"""
|
|
81
|
+
return self.main == "proxy"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def has_fallback(self) -> bool:
|
|
85
|
+
"""是否存在次路径。"""
|
|
86
|
+
return self.fallback is not None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def fallback_is_proxy(self) -> bool:
|
|
90
|
+
"""次路径是否为代理 (prefer_direct 升级)。"""
|
|
91
|
+
return self.fallback == "proxy"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def fallback_is_direct(self) -> bool:
|
|
95
|
+
"""次路径是否为直连 (prefer_proxy 降级)。"""
|
|
96
|
+
return self.fallback == "direct"
|
|
97
|
+
|
|
98
|
+
# ── 决策方法 ────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def should_fallback_for_error(self, category: FetchErrorCategory) -> bool:
|
|
101
|
+
"""URL 级决策:给定的抓取错误是否应触发回退。
|
|
102
|
+
|
|
103
|
+
仅 prefer_direct 需要:直连连通性错误 → 升级代理。
|
|
104
|
+
"""
|
|
105
|
+
if not self.has_fallback or self.trigger != "connectivity_error":
|
|
106
|
+
return False
|
|
107
|
+
return category in _CONNECTIVITY_ERRORS
|
|
108
|
+
|
|
109
|
+
def should_fallback_for_proxy_exhaustion(self, category: Optional[FetchErrorCategory] = None) -> bool:
|
|
110
|
+
"""prefer_proxy: 代理池耗尽时触发直连回退。
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
category: 错误分类 (可选)。传入时排除确定非代理的错误类别
|
|
114
|
+
(DNS/SSL/HTTP_4XX/redirects — 这些与代理状态无关)。
|
|
115
|
+
"""
|
|
116
|
+
if not self.has_fallback or self.trigger != "all_proxies_dead":
|
|
117
|
+
return False
|
|
118
|
+
if category is not None and category in _NON_PROXY_ERRORS:
|
|
119
|
+
return False
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_CONNECTIVITY_ERRORS = frozenset(
|
|
124
|
+
{
|
|
125
|
+
FetchErrorCategory.CONNECTION_REFUSED,
|
|
126
|
+
FetchErrorCategory.TIMEOUT,
|
|
127
|
+
FetchErrorCategory.CONNECTION_RESET,
|
|
128
|
+
FetchErrorCategory.GENERIC,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 确定非代理错误——prefer_proxy 在这些错误上回退到直连无意义
|
|
133
|
+
_NON_PROXY_ERRORS = frozenset(
|
|
134
|
+
{
|
|
135
|
+
FetchErrorCategory.DNS,
|
|
136
|
+
FetchErrorCategory.SSL,
|
|
137
|
+
FetchErrorCategory.HTTP_4XX,
|
|
138
|
+
FetchErrorCategory.DOWNLOAD,
|
|
139
|
+
FetchErrorCategory.TOO_MANY_REDIRECTS,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = [
|
|
145
|
+
"PathSwitch",
|
|
146
|
+
"_CONNECTIVITY_ERRORS",
|
|
147
|
+
"_NON_PROXY_ERRORS",
|
|
148
|
+
]
|