argo-search 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/backends/domain_profiles.json +364 -0
- package/backends/engine_registry.yaml +505 -0
- package/backends/quota_profiles.json +395 -0
- package/bin/argo.js +54 -0
- package/config.yaml +554 -0
- package/package.json +43 -0
- package/scripts/adaptive.py +179 -0
- package/scripts/benchmark.py +124 -0
- package/scripts/cache.py +374 -0
- package/scripts/clarify.py +689 -0
- package/scripts/config.py +262 -0
- package/scripts/crawl.py +73 -0
- package/scripts/engines.py +386 -0
- package/scripts/evidence.py +381 -0
- package/scripts/extract.py +69 -0
- package/scripts/fetch.py +118 -0
- package/scripts/health_check.py +437 -0
- package/scripts/health_probe.py +218 -0
- package/scripts/mcp_diag.py +81 -0
- package/scripts/mcp_server.py +488 -0
- package/scripts/query_rewriter.py +278 -0
- package/scripts/quota.py +196 -0
- package/scripts/research.py +499 -0
- package/scripts/route.py +341 -0
- package/scripts/search.py +508 -0
- package/scripts/search_types.py +72 -0
- package/scripts/tfidf_router.py +312 -0
- package/sub-skills/local-search/SKILL.md +104 -0
- package/sub-skills/local-search/config.yaml +328 -0
- package/sub-skills/local-search/engine_registry.py +298 -0
- package/sub-skills/local-search/health_check.py +347 -0
- package/sub-skills/local-search/local_search_adapter.py +56 -0
- package/sub-skills/local-search/parse_maps.yaml +184 -0
- package/sub-skills/local-search/search_v3.py +558 -0
- package/sub-skills/local-search/smart_router.py +215 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""health_check.py — local-search 轻量健康探针
|
|
3
|
+
|
|
4
|
+
对每个启用引擎发送 canary 查询,检测 HTTP 状态、延迟、解析成功与反爬拦截,
|
|
5
|
+
将结果持久化到 ~/.cache/unified-search/local_search_health.json。
|
|
6
|
+
|
|
7
|
+
判定规则(与设计方案一致):
|
|
8
|
+
- 连续 2 次失败 / 单次延迟 > 8s / HTTP 4xx/5xx → 标记 unavailable
|
|
9
|
+
- 成功 1 次即可恢复 available
|
|
10
|
+
- 启动时读取缓存(TTL 5 分钟),避免每次查询都发探针
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import concurrent.futures
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import urllib.request
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from engine_registry import EngineRegistry, get_registry, update_availability
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("local_search.health_check")
|
|
30
|
+
if not logger.handlers:
|
|
31
|
+
logger.setLevel(logging.WARNING)
|
|
32
|
+
logger.addHandler(logging.StreamHandler())
|
|
33
|
+
|
|
34
|
+
# 常见反爬/拦截标记(大小写不敏感)
|
|
35
|
+
ANTI_BOT_MARKERS = [
|
|
36
|
+
"captcha", "recaptcha", "robot", "robots", "cloudflare", "challenge",
|
|
37
|
+
"blocked", "verification", "please verify", "access denied",
|
|
38
|
+
"too many requests", "rate limit", "forbidden", "unauthorized",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# HTTP 状态码分组
|
|
42
|
+
RETRYABLE_STATUS = {429, 503, 502, 504}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _now() -> float:
|
|
46
|
+
return time.time()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _detect_anti_bot(text: str, status: int | None = None) -> str | None:
|
|
50
|
+
"""检测是否命中验证码/拦截页面。"""
|
|
51
|
+
if status == 429:
|
|
52
|
+
return "rate_limited"
|
|
53
|
+
if status and 500 <= status < 600:
|
|
54
|
+
return f"http_{status}"
|
|
55
|
+
lowered = text.lower()
|
|
56
|
+
for marker in ANTI_BOT_MARKERS:
|
|
57
|
+
if marker in lowered:
|
|
58
|
+
return f"anti_bot:{marker}"
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_canary_url(spec: dict[str, Any], query: str, n: int = 1) -> tuple[str, dict[str, str]]:
|
|
63
|
+
"""构造 canary 请求的 URL 与 headers。"""
|
|
64
|
+
url = spec.get("url", "")
|
|
65
|
+
qp = spec.get("query_param", "q")
|
|
66
|
+
method = spec.get("method", "GET")
|
|
67
|
+
extra = spec.get("extra_params", {})
|
|
68
|
+
headers = spec.get("headers", {})
|
|
69
|
+
|
|
70
|
+
resolved_url = url.replace("{query}", urllib.parse.quote(query)).replace("{n}", str(n))
|
|
71
|
+
if method == "GET":
|
|
72
|
+
params: dict[str, str] = {qp: query}
|
|
73
|
+
for k, v in extra.items():
|
|
74
|
+
params[k] = str(v).replace("{query}", query).replace("{n}", str(n))
|
|
75
|
+
sep = "&" if "?" in resolved_url else "?"
|
|
76
|
+
full_url = f"{resolved_url}{sep}{urllib.parse.urlencode(params)}"
|
|
77
|
+
else:
|
|
78
|
+
full_url = resolved_url
|
|
79
|
+
|
|
80
|
+
resolved_headers = {k: str(v).replace("{n}", str(n)) for k, v in headers.items()}
|
|
81
|
+
return full_url, resolved_headers
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _fetch_probe(
|
|
85
|
+
url: str,
|
|
86
|
+
headers: dict[str, str],
|
|
87
|
+
method: str = "GET",
|
|
88
|
+
timeout: float = 8,
|
|
89
|
+
user_agent: str = "Mozilla/5.0 (compatible; unified-search-local/1.0.1; +https://local)",
|
|
90
|
+
) -> tuple[int | None, float, str, str | None]:
|
|
91
|
+
"""发送探针请求,返回 (status, latency_ms, text, fail_reason)。"""
|
|
92
|
+
req_headers = dict(headers)
|
|
93
|
+
if user_agent and "User-Agent" not in req_headers:
|
|
94
|
+
req_headers["User-Agent"] = user_agent
|
|
95
|
+
req_headers.setdefault("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
|
96
|
+
req_headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
|
97
|
+
|
|
98
|
+
req = urllib.request.Request(url, headers=req_headers, method=method)
|
|
99
|
+
t0 = time.time()
|
|
100
|
+
try:
|
|
101
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
102
|
+
raw = resp.read()
|
|
103
|
+
try:
|
|
104
|
+
text = raw.decode("utf-8", errors="replace")
|
|
105
|
+
except Exception:
|
|
106
|
+
text = ""
|
|
107
|
+
status = resp.getcode()
|
|
108
|
+
except urllib.error.HTTPError as e:
|
|
109
|
+
status = e.code
|
|
110
|
+
text = ""
|
|
111
|
+
try:
|
|
112
|
+
text = e.read().decode("utf-8", errors="replace")[:500]
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
except urllib.error.URLError as e:
|
|
116
|
+
return None, round((time.time() - t0) * 1000, 2), "", f"url_error:{e.reason}"
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return None, round((time.time() - t0) * 1000, 2), "", f"exception:{type(e).__name__}:{e}"
|
|
119
|
+
|
|
120
|
+
elapsed = round((time.time() - t0) * 1000, 2)
|
|
121
|
+
fail_reason = _detect_anti_bot(text, status)
|
|
122
|
+
return status, elapsed, text, fail_reason
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_success(engine_name: str, text: str, fmt: str, registry: EngineRegistry) -> bool:
|
|
126
|
+
"""粗略判断解析是否可能成功(通过 parse_maps.yaml 与格式特征)。"""
|
|
127
|
+
if not text:
|
|
128
|
+
return False
|
|
129
|
+
if fmt == "json":
|
|
130
|
+
try:
|
|
131
|
+
json.loads(text)
|
|
132
|
+
return True
|
|
133
|
+
except json.JSONDecodeError:
|
|
134
|
+
return False
|
|
135
|
+
if fmt in ("xml", "rss"):
|
|
136
|
+
import xml.etree.ElementTree as ET
|
|
137
|
+
try:
|
|
138
|
+
ET.fromstring(text)
|
|
139
|
+
return True
|
|
140
|
+
except ET.ParseError:
|
|
141
|
+
return False
|
|
142
|
+
# html:检查 parse_maps 中 container 是否出现在文本中
|
|
143
|
+
maps = registry.parse_maps.get("html", {}).get(engine_name, {})
|
|
144
|
+
container = maps.get("container", "")
|
|
145
|
+
if container:
|
|
146
|
+
# 简单把 CSS 选择器转成正则特征
|
|
147
|
+
parts = [p.strip(". ") for p in container.replace(",", " ").split() if p.strip(". ")]
|
|
148
|
+
return any(p in text for p in parts)
|
|
149
|
+
return len(text) > 200
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_engine(
|
|
153
|
+
engine_name: str,
|
|
154
|
+
registry: EngineRegistry | None = None,
|
|
155
|
+
canary_query: str = "test",
|
|
156
|
+
n: int = 1,
|
|
157
|
+
timeout: float = 8,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""对单个引擎执行健康探针,返回状态报告(不直接修改注册表)。"""
|
|
160
|
+
reg = registry or get_registry()
|
|
161
|
+
spec = reg.get_engine(engine_name)
|
|
162
|
+
if spec is None:
|
|
163
|
+
return {"name": engine_name, "available": False, "fail_reason": "not_configured"}
|
|
164
|
+
if not spec.get("enabled", True):
|
|
165
|
+
return {"name": engine_name, "available": False, "fail_reason": "disabled"}
|
|
166
|
+
|
|
167
|
+
url, headers = _build_canary_url(spec, canary_query, n)
|
|
168
|
+
method = spec.get("method", "GET")
|
|
169
|
+
status, latency_ms, text, fail_reason = _fetch_probe(url, headers, method=method, timeout=timeout)
|
|
170
|
+
|
|
171
|
+
fmt = spec.get("format", "html")
|
|
172
|
+
parse_ok = _parse_success(engine_name, text, fmt, reg) if text else False
|
|
173
|
+
|
|
174
|
+
report: dict[str, Any] = {
|
|
175
|
+
"name": engine_name,
|
|
176
|
+
"url": url,
|
|
177
|
+
"status": status,
|
|
178
|
+
"latency_ms": latency_ms,
|
|
179
|
+
"parse_ok": parse_ok,
|
|
180
|
+
"text_sample": text[:300] if text else "",
|
|
181
|
+
"fail_reason": fail_reason,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 判定逻辑
|
|
185
|
+
http_failed = status is None or status >= 400
|
|
186
|
+
too_slow = latency_ms > 8000
|
|
187
|
+
blocked = fail_reason is not None
|
|
188
|
+
parse_failed = not parse_ok and not blocked and not http_failed
|
|
189
|
+
|
|
190
|
+
if http_failed or too_slow or blocked:
|
|
191
|
+
report["available"] = False
|
|
192
|
+
if not fail_reason:
|
|
193
|
+
if status is None:
|
|
194
|
+
report["fail_reason"] = "network_error"
|
|
195
|
+
else:
|
|
196
|
+
report["fail_reason"] = f"http_{status}"
|
|
197
|
+
elif parse_failed:
|
|
198
|
+
# 解析失败单独记录,但不直接判 unavailable(可能是页面改版)
|
|
199
|
+
report["available"] = True
|
|
200
|
+
report["parse_warning"] = True
|
|
201
|
+
else:
|
|
202
|
+
report["available"] = True
|
|
203
|
+
|
|
204
|
+
return report
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def apply_threshold(report: dict[str, Any], previous: dict[str, Any] | None = None) -> bool:
|
|
208
|
+
"""根据本次报告与上一次状态,应用可用性阈值判定。
|
|
209
|
+
|
|
210
|
+
规则:
|
|
211
|
+
- 本次成功 → available = True
|
|
212
|
+
- 本次失败且(连续失败 >=2 或 延迟>8s 或 HTTP 4xx/5xx)→ available = False
|
|
213
|
+
- 其他失败情况继承上一次状态(保守策略)
|
|
214
|
+
"""
|
|
215
|
+
available = report.get("available", False)
|
|
216
|
+
if available:
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
status = report.get("status")
|
|
220
|
+
latency_ms = report.get("latency_ms", 0)
|
|
221
|
+
consecutive = previous.get("consecutive_failures", 0) if previous else 0
|
|
222
|
+
|
|
223
|
+
http_failed = status is None or (isinstance(status, int) and status >= 400)
|
|
224
|
+
too_slow = latency_ms > 8000
|
|
225
|
+
now_consecutive = consecutive + 1
|
|
226
|
+
|
|
227
|
+
if http_failed or too_slow or now_consecutive >= 2:
|
|
228
|
+
return False
|
|
229
|
+
# 单次软性失败,继承之前状态
|
|
230
|
+
return previous.get("available", True) if previous else True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_health_check(
|
|
234
|
+
registry: EngineRegistry | None = None,
|
|
235
|
+
canary_query: str = "test",
|
|
236
|
+
n: int = 1,
|
|
237
|
+
timeout: float = 8,
|
|
238
|
+
max_parallel: int = 5,
|
|
239
|
+
ttl_minutes: float = 5,
|
|
240
|
+
engine_names: list[str] | None = None,
|
|
241
|
+
) -> dict[str, dict[str, Any]]:
|
|
242
|
+
"""运行一轮健康检查,返回每个引擎的最新报告。
|
|
243
|
+
|
|
244
|
+
若缓存 TTL 未过期,则直接返回已有健康状态。
|
|
245
|
+
传入 engine_names 可只检查指定引擎,避免对所有启用引擎全量探针。
|
|
246
|
+
"""
|
|
247
|
+
reg = registry or get_registry()
|
|
248
|
+
settings = reg.settings.get("health_check", {})
|
|
249
|
+
canary_query = settings.get("canary_query", canary_query)
|
|
250
|
+
timeout = settings.get("timeout", timeout)
|
|
251
|
+
ttl_minutes = settings.get("ttl_minutes", ttl_minutes)
|
|
252
|
+
max_parallel = settings.get("max_parallel", max_parallel)
|
|
253
|
+
|
|
254
|
+
enabled = engine_names if engine_names is not None else reg.list_engines(enabled_only=True)
|
|
255
|
+
|
|
256
|
+
now = _now()
|
|
257
|
+
# TTL 检查:在有效期内直接复用缓存
|
|
258
|
+
cached = reg._health
|
|
259
|
+
all_recent = all(
|
|
260
|
+
(now - h.get("last_checked", 0)) < ttl_minutes * 60
|
|
261
|
+
for h in cached.values() if engine_names is None or h in enabled
|
|
262
|
+
)
|
|
263
|
+
if cached and all_recent:
|
|
264
|
+
return {name: dict(h) for name, h in cached.items() if name in enabled}
|
|
265
|
+
|
|
266
|
+
reports: dict[str, dict[str, Any]] = {}
|
|
267
|
+
|
|
268
|
+
def _probe(name: str) -> tuple[str, dict[str, Any]]:
|
|
269
|
+
return name, check_engine(name, reg, canary_query=canary_query, n=n, timeout=timeout)
|
|
270
|
+
|
|
271
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(enabled), max_parallel)) as ex:
|
|
272
|
+
futures = [ex.submit(_probe, name) for name in enabled]
|
|
273
|
+
for fut in concurrent.futures.as_completed(futures):
|
|
274
|
+
name, report = fut.result()
|
|
275
|
+
reports[name] = report
|
|
276
|
+
|
|
277
|
+
# 应用阈值并更新注册表
|
|
278
|
+
for name, report in reports.items():
|
|
279
|
+
previous = reg.get_health(name)
|
|
280
|
+
final_available = apply_threshold(report, previous)
|
|
281
|
+
update_data = {
|
|
282
|
+
"available": final_available,
|
|
283
|
+
"latency_ms": report.get("latency_ms"),
|
|
284
|
+
"status": report.get("status"),
|
|
285
|
+
"parse_ok": report.get("parse_ok"),
|
|
286
|
+
"fail_reason": report.get("fail_reason"),
|
|
287
|
+
}
|
|
288
|
+
if final_available:
|
|
289
|
+
update_data["success_rate"] = 1.0
|
|
290
|
+
else:
|
|
291
|
+
update_data["success_rate"] = 0.0
|
|
292
|
+
reg.update_availability(name, final_available, **update_data)
|
|
293
|
+
|
|
294
|
+
return reports
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_available_engines(
|
|
298
|
+
registry: EngineRegistry | None = None,
|
|
299
|
+
use_cache: bool = True,
|
|
300
|
+
engine_names: list[str] | None = None,
|
|
301
|
+
) -> list[str]:
|
|
302
|
+
"""获取当前可用引擎列表;若缓存过期则自动执行健康检查。
|
|
303
|
+
|
|
304
|
+
通过 engine_names 可只检查指定引擎,避免全量探针。
|
|
305
|
+
"""
|
|
306
|
+
reg = registry or get_registry()
|
|
307
|
+
if engine_names is None:
|
|
308
|
+
engine_names = reg.list_engines(enabled_only=True)
|
|
309
|
+
if use_cache:
|
|
310
|
+
now = _now()
|
|
311
|
+
ttl = reg.settings.get("health_check", {}).get("ttl_minutes", 5) * 60
|
|
312
|
+
cached = reg._health
|
|
313
|
+
if cached and all((now - h.get("last_checked", 0)) < ttl for h in cached.values() if h in engine_names):
|
|
314
|
+
return [n for n in engine_names if reg.is_available(n)]
|
|
315
|
+
run_health_check(registry=reg, engine_names=engine_names)
|
|
316
|
+
return [n for n in engine_names if reg.is_available(n)]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
import argparse
|
|
321
|
+
|
|
322
|
+
parser = argparse.ArgumentParser(description="local-search 健康检查")
|
|
323
|
+
parser.add_argument("--engine", default=None, help="只检查单个引擎")
|
|
324
|
+
parser.add_argument("--query", default="test", help="canary 查询词")
|
|
325
|
+
parser.add_argument("--timeout", type=float, default=8)
|
|
326
|
+
parser.add_argument("--max-parallel", type=int, default=5)
|
|
327
|
+
parser.add_argument("--force", action="store_true", help="忽略 TTL 强制检查")
|
|
328
|
+
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
329
|
+
args = parser.parse_args()
|
|
330
|
+
|
|
331
|
+
reg = get_registry()
|
|
332
|
+
if args.force:
|
|
333
|
+
reg._health.clear()
|
|
334
|
+
reg._save_health()
|
|
335
|
+
|
|
336
|
+
if args.engine:
|
|
337
|
+
report = check_engine(args.engine, reg, canary_query=args.query, timeout=args.timeout)
|
|
338
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
339
|
+
else:
|
|
340
|
+
reports = run_health_check(reg, canary_query=args.query, timeout=args.timeout, max_parallel=args.max_parallel)
|
|
341
|
+
summary = {
|
|
342
|
+
"total": len(reports),
|
|
343
|
+
"available": sum(1 for r in reports.values() if r.get("available")),
|
|
344
|
+
"unavailable": sum(1 for r in reports.values() if not r.get("available")),
|
|
345
|
+
"reports": reports,
|
|
346
|
+
}
|
|
347
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""local_search_adapter.py — local-search 兼容入口
|
|
3
|
+
|
|
4
|
+
保留原 adapter 接口,内部委托 search_v3.search_engines 执行。
|
|
5
|
+
unified-search 可通过 --sub-skill local-search 或 --local-first 调用本入口。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
SKILL_DIR = Path(__file__).resolve().parent
|
|
17
|
+
sys.path.insert(0, str(SKILL_DIR))
|
|
18
|
+
|
|
19
|
+
from search_v3 import search_engines
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_engine_list(value: str) -> list[str]:
|
|
23
|
+
return [x.strip() for x in value.replace(",", ",").split(",") if x.strip()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
parser = argparse.ArgumentParser(description="local-search 兼容入口(委托 search_v3)")
|
|
28
|
+
parser.add_argument("query", nargs="?", help="搜索关键词")
|
|
29
|
+
parser.add_argument("--engine", "-e", default="", help="引擎名,多个用逗号分隔")
|
|
30
|
+
parser.add_argument("--n", type=int, default=5, help="每引擎结果数")
|
|
31
|
+
parser.add_argument("--timeout", "-t", type=float, default=None, help="超时秒数")
|
|
32
|
+
parser.add_argument("--max-parallel", type=int, default=5)
|
|
33
|
+
parser.add_argument("--no-cache", action="store_true", help="跳过缓存")
|
|
34
|
+
parser.add_argument("--mode", default="fast", choices=["fast", "auto", "deep", "budget"],
|
|
35
|
+
help="unified-search 模式透传")
|
|
36
|
+
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
if not args.query:
|
|
40
|
+
parser.error("必须提供搜索关键词")
|
|
41
|
+
|
|
42
|
+
engines = _parse_engine_list(args.engine) if args.engine else None
|
|
43
|
+
result = search_engines(
|
|
44
|
+
args.query,
|
|
45
|
+
engines=engines,
|
|
46
|
+
n=args.n,
|
|
47
|
+
timeout=args.timeout,
|
|
48
|
+
max_parallel=args.max_parallel,
|
|
49
|
+
skip_cache=args.no_cache,
|
|
50
|
+
mode=args.mode,
|
|
51
|
+
)
|
|
52
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
main()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# local-search HTML/RSS/JSON 解析映射
|
|
2
|
+
# 页面结构变化时,只需修改本文件,无需改代码。
|
|
3
|
+
|
|
4
|
+
settings:
|
|
5
|
+
max_snippet_length: 300
|
|
6
|
+
max_title_length: 200
|
|
7
|
+
resolve_relative_urls: true
|
|
8
|
+
|
|
9
|
+
html:
|
|
10
|
+
local_bing:
|
|
11
|
+
container: "li.b_algo"
|
|
12
|
+
title: "h2 a"
|
|
13
|
+
url: "h2 a"
|
|
14
|
+
snippet: ".b_caption p, .b_lineclamp2, .b_paractl"
|
|
15
|
+
url_attr: href
|
|
16
|
+
score: 0.75
|
|
17
|
+
|
|
18
|
+
local_google:
|
|
19
|
+
container: "div.g"
|
|
20
|
+
title: "h3"
|
|
21
|
+
url: "a"
|
|
22
|
+
snippet: "div.VwiC3b, div.s, span.aCOpRe"
|
|
23
|
+
url_attr: href
|
|
24
|
+
score: 0.75
|
|
25
|
+
|
|
26
|
+
local_mojeek:
|
|
27
|
+
container: "li.results-standard-list"
|
|
28
|
+
title: "a.ob"
|
|
29
|
+
url: "a.ob"
|
|
30
|
+
snippet: "p.s"
|
|
31
|
+
url_attr: href
|
|
32
|
+
score: 0.7
|
|
33
|
+
|
|
34
|
+
local_yandex:
|
|
35
|
+
container: "li.serp-item"
|
|
36
|
+
title: "a.Link"
|
|
37
|
+
url: "a.Link"
|
|
38
|
+
snippet: "div.TextContainer"
|
|
39
|
+
url_attr: href
|
|
40
|
+
score: 0.65
|
|
41
|
+
|
|
42
|
+
local_startpage:
|
|
43
|
+
container: "div.result"
|
|
44
|
+
title: "a.result-title"
|
|
45
|
+
url: "a.result-title"
|
|
46
|
+
snippet: "p.result-snippet"
|
|
47
|
+
url_attr: href
|
|
48
|
+
score: 0.7
|
|
49
|
+
|
|
50
|
+
local_duckduckgo:
|
|
51
|
+
container: ".result"
|
|
52
|
+
title: "a.result__a"
|
|
53
|
+
url: "a.result__a"
|
|
54
|
+
snippet: "a.result__snippet"
|
|
55
|
+
url_attr: href
|
|
56
|
+
score: 0.7
|
|
57
|
+
|
|
58
|
+
local_baidu:
|
|
59
|
+
container: ".result, .c-container"
|
|
60
|
+
title: "h3"
|
|
61
|
+
url: "a"
|
|
62
|
+
snippet: ".content-right_8Zs40, .c-abstract, .content-left_3d_H4"
|
|
63
|
+
url_attr: href
|
|
64
|
+
score: 0.7
|
|
65
|
+
|
|
66
|
+
local_sogou:
|
|
67
|
+
container: ".vrwrap, .result"
|
|
68
|
+
title: "h3 a, h3"
|
|
69
|
+
url: "h3 a"
|
|
70
|
+
snippet: "p.str-text, .str_info"
|
|
71
|
+
url_attr: href
|
|
72
|
+
score: 0.65
|
|
73
|
+
|
|
74
|
+
local_stackoverflow:
|
|
75
|
+
container: ".question-summary"
|
|
76
|
+
title: "a.question-hyperlink"
|
|
77
|
+
url: "a.question-hyperlink"
|
|
78
|
+
snippet: ".excerpt"
|
|
79
|
+
url_attr: href
|
|
80
|
+
score: 0.8
|
|
81
|
+
|
|
82
|
+
local_imdb:
|
|
83
|
+
container: ".findResult"
|
|
84
|
+
title: "td.result_text a"
|
|
85
|
+
url: "td.result_text a"
|
|
86
|
+
snippet: "td.result_text"
|
|
87
|
+
url_attr: href
|
|
88
|
+
score: 0.7
|
|
89
|
+
|
|
90
|
+
local_goodreads:
|
|
91
|
+
container: "tr.bookBox"
|
|
92
|
+
title: "a.bookTitle"
|
|
93
|
+
url: "a.bookTitle"
|
|
94
|
+
snippet: "span.smallText, span.greyText"
|
|
95
|
+
url_attr: href
|
|
96
|
+
score: 0.7
|
|
97
|
+
|
|
98
|
+
xml:
|
|
99
|
+
local_arxiv:
|
|
100
|
+
entry_path: ".//{http://www.w3.org/2005/Atom}entry"
|
|
101
|
+
title: "atom:title"
|
|
102
|
+
url: "atom:id"
|
|
103
|
+
snippet: "atom:summary"
|
|
104
|
+
namespaces:
|
|
105
|
+
atom: "http://www.w3.org/2005/Atom"
|
|
106
|
+
|
|
107
|
+
rss:
|
|
108
|
+
default:
|
|
109
|
+
item_path: "./channel/item"
|
|
110
|
+
title: "title"
|
|
111
|
+
url: "link"
|
|
112
|
+
snippet: "description"
|
|
113
|
+
|
|
114
|
+
json:
|
|
115
|
+
local_wikipedia:
|
|
116
|
+
items: "query.search"
|
|
117
|
+
title: "title"
|
|
118
|
+
url: null
|
|
119
|
+
snippet: "snippet"
|
|
120
|
+
url_template: "https://en.wikipedia.org/wiki/{title}"
|
|
121
|
+
|
|
122
|
+
local_wiktionary:
|
|
123
|
+
items: "query.search"
|
|
124
|
+
title: "title"
|
|
125
|
+
url: null
|
|
126
|
+
snippet: "snippet"
|
|
127
|
+
url_template: "https://en.wiktionary.org/wiki/{title}"
|
|
128
|
+
|
|
129
|
+
local_wikiquote:
|
|
130
|
+
items: "query.search"
|
|
131
|
+
title: "title"
|
|
132
|
+
url: null
|
|
133
|
+
snippet: "snippet"
|
|
134
|
+
url_template: "https://en.wikiquote.org/wiki/{title}"
|
|
135
|
+
|
|
136
|
+
local_github:
|
|
137
|
+
items: "items"
|
|
138
|
+
title: "full_name"
|
|
139
|
+
url: "html_url"
|
|
140
|
+
snippet: "description"
|
|
141
|
+
|
|
142
|
+
local_npm:
|
|
143
|
+
items: "objects"
|
|
144
|
+
title: "package.name"
|
|
145
|
+
url: "package.links.npm"
|
|
146
|
+
snippet: "package.description"
|
|
147
|
+
|
|
148
|
+
local_gitlab:
|
|
149
|
+
items: "."
|
|
150
|
+
title: "name_with_namespace"
|
|
151
|
+
url: "web_url"
|
|
152
|
+
snippet: "description"
|
|
153
|
+
|
|
154
|
+
local_stackoverflow:
|
|
155
|
+
items: "items"
|
|
156
|
+
title: "title"
|
|
157
|
+
url: "link"
|
|
158
|
+
snippet: null
|
|
159
|
+
|
|
160
|
+
local_crossref:
|
|
161
|
+
items: "message.items"
|
|
162
|
+
title: "title"
|
|
163
|
+
url: "URL"
|
|
164
|
+
snippet: "abstract"
|
|
165
|
+
|
|
166
|
+
local_semantic_scholar:
|
|
167
|
+
items: "data"
|
|
168
|
+
title: "title"
|
|
169
|
+
url: "url"
|
|
170
|
+
snippet: "abstract"
|
|
171
|
+
|
|
172
|
+
local_pubmed:
|
|
173
|
+
items: "esearchresult.idlist"
|
|
174
|
+
title: null
|
|
175
|
+
url: null
|
|
176
|
+
snippet: null
|
|
177
|
+
url_template: "https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
|
|
178
|
+
|
|
179
|
+
local_openstreetmap:
|
|
180
|
+
items: "."
|
|
181
|
+
title: "display_name"
|
|
182
|
+
url: null
|
|
183
|
+
snippet: "type"
|
|
184
|
+
url_template: "https://www.openstreetmap.org/{osm_type}/{osm_id}"
|