Files
gaojie abf2322450
Sync to site1 / sync (push) Has been cancelled
feat: add scripts for fetching arXiv papers and generating structured reviews
- Implemented `fetch_crowdroom_papers.py` to scrape arXiv papers related to CrowdRoom with customizable query topics and output options.
- Created `gen_review_from_json.py` to render structured markdown reviews from search results JSON, including statistics and insights on papers and GitHub repositories.
- Developed `search_info.py` for thematic searches on arXiv and GitHub, supporting HTTP/SOCKS5 proxies and structured output.
2026-05-21 03:35:05 +08:00

320 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""arXiv + GitHub 检索工具(支持 HTTP / SOCKS5 代理)
用法:
# 1. 直连
python3 research/search_info.py
# 2. HTTP 代理(推荐:6984 同时支持 http/socks5
HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \
python3 research/search_info.py
# 3. SOCKS5 代理(需 pip install pysocks
ALL_PROXY=socks5h://127.0.0.1:6984 python3 research/search_info.py
# 4. 命令行指定(覆盖环境变量)
python3 research/search_info.py --proxy http://127.0.0.1:6984
输出:
research/search_results.json — 结构化 JSON(按主题分组)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from typing import Any
# ──────────────────────────────────────────────────────────────────────
# 主题化查询(与 plans/camera/zed2i_iterative_framework.md 五条主线对齐)
# ──────────────────────────────────────────────────────────────────────
TOPICS: dict[str, str] = {
"stereo_matching": 'all:"stereo matching" AND (all:"foundation" OR all:"zero-shot" OR all:"transformer")',
"visual_inertial_slam": 'all:("visual inertial" OR "VIO" OR "VI-SLAM") AND cat:cs.CV',
"gaussian_splatting_slam": 'all:"gaussian splatting" AND all:"SLAM"',
"monocular_depth_foundation": 'all:("depth anything" OR "metric3d" OR "marigold" OR "UniDepth")',
"indoor_rgbd_dataset": 'all:("RGB-D dataset" OR "indoor dataset") AND all:"reconstruction"',
"world_model_video": 'all:"world model" AND (all:"video" OR all:"prediction" OR all:"generative")',
"zed_camera": 'all:("ZED 2i" OR "ZED stereo" OR "ZED-2i" OR "Stereolabs")',
"orbbec_gemini": 'all:("Orbbec" OR "Femto Mega" OR "Femto Bolt" OR "Azure Kinect") AND all:("depth" OR "RGB-D" OR "SLAM")',
"rgbd_indoor_reconstruction": 'all:("RGB-D" OR "RGBD") AND all:("indoor" OR "room") AND (all:"reconstruction" OR all:"SLAM")',
"neural_stereo_depth": 'all:("foundation stereo" OR "RAFT-Stereo" OR "IGEV") AND all:"depth"',
}
# GitHub 主题化查询
GITHUB_TOPICS: dict[str, str] = {
"stereo_matching": "stereo matching depth in:name,description,readme stars:>500",
"vio_slam": "visual inertial SLAM in:name,description,readme stars:>500",
"gaussian_splatting": "gaussian splatting in:name,description,readme stars:>1000",
"monocular_depth": "monocular depth estimation in:name,description,readme stars:>1000",
"world_model": "world model in:name,description,readme stars:>500",
}
# ──────────────────────────────────────────────────────────────────────
# 代理配置
# ──────────────────────────────────────────────────────────────────────
def build_opener(proxy: str | None) -> urllib.request.OpenerDirector:
"""构建带代理的 urllib opener。
proxy 形式:
- http://127.0.0.1:6984
- https://127.0.0.1:6984
- socks5h://127.0.0.1:6984 (需 pysocks
- None → 走环境变量 (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) 或直连
"""
if proxy and proxy.startswith(("socks5://", "socks5h://", "socks4://")):
try:
import socks # type: ignore
import socket
scheme, rest = proxy.split("://", 1)
host, port_str = rest.split(":")
port = int(port_str)
socks_type = {
"socks5": socks.SOCKS5,
"socks5h": socks.SOCKS5,
"socks4": socks.SOCKS4,
}[scheme]
socks.set_default_proxy(socks_type, host, port, rdns=(scheme == "socks5h"))
socket.socket = socks.socksocket # type: ignore[assignment]
print(f"[proxy] SOCKS5 启用: {proxy}", file=sys.stderr)
return urllib.request.build_opener()
except ImportError:
print(
"[proxy] 警告: 需要 SOCKS5 但未安装 pysocks,将回退到环境变量。\n"
" 修复: pip install pysocks",
file=sys.stderr,
)
return urllib.request.build_opener()
if proxy:
handler = urllib.request.ProxyHandler({"http": proxy, "https": proxy})
print(f"[proxy] HTTP(S) 代理启用: {proxy}", file=sys.stderr)
return urllib.request.build_opener(handler)
# 走环境变量
env_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or os.environ.get("ALL_PROXY")
if env_proxy:
print(f"[proxy] 使用环境变量代理: {env_proxy}", file=sys.stderr)
else:
print("[proxy] 直连模式(无代理)", file=sys.stderr)
return urllib.request.build_opener()
# ──────────────────────────────────────────────────────────────────────
# arXiv
# ──────────────────────────────────────────────────────────────────────
def search_arxiv(
opener: urllib.request.OpenerDirector,
query: str,
max_results: int = 20,
sort_by: str = "submittedDate",
sort_order: str = "descending",
) -> list[dict[str, Any]]:
"""调用 arXiv API 并解析返回的 ATOM XML。"""
base = "https://export.arxiv.org/api/query"
params = urllib.parse.urlencode(
{
"search_query": query,
"start": 0,
"max_results": max_results,
"sortBy": sort_by,
"sortOrder": sort_order,
}
)
url = f"{base}?{params}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (research-bot)"})
# arXiv 限速重试(429 → 指数退避)
data = ""
for attempt in range(4):
try:
with opener.open(req, timeout=30) as resp:
data = resp.read().decode("utf-8")
break
except urllib.error.HTTPError as e:
if e.code == 429:
wait = 10 * (attempt + 1)
print(
f"[arxiv][{query[:40]}...] 429 限速,等待 {wait}s 重试 ({attempt+1}/4)",
file=sys.stderr,
)
time.sleep(wait)
continue
print(f"[arxiv][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
return []
except (urllib.error.URLError, TimeoutError) as e:
print(f"[arxiv][{query[:40]}...] ERROR: {e}", file=sys.stderr)
return []
else:
print(f"[arxiv][{query[:40]}...] 持续 429,放弃", file=sys.stderr)
return []
try:
root = ET.fromstring(data)
except ET.ParseError as e:
print(f"[arxiv][{query[:40]}...] XML parse error: {e}", file=sys.stderr)
return []
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
papers: list[dict[str, Any]] = []
for entry in root.findall("atom:entry", ns):
def _text(elem_path: str) -> str:
elem = entry.find(elem_path, ns)
return (elem.text or "").replace("\n", " ").strip() if elem is not None else ""
arxiv_id_url = _text("atom:id")
arxiv_id = arxiv_id_url.rsplit("/", 1)[-1] if arxiv_id_url else ""
# 去掉版本号 e.g. 2501.09898v2 → 2501.09898
arxiv_id_clean = arxiv_id.split("v")[0] if arxiv_id else ""
authors: list[str] = []
for a in entry.findall("atom:author", ns):
name_elem = a.find("atom:name", ns)
if name_elem is not None and name_elem.text:
authors.append(name_elem.text.strip())
categories = [
c.attrib.get("term", "") for c in entry.findall("atom:category", ns)
]
summary = _text("atom:summary")
papers.append(
{
"arxiv_id": arxiv_id_clean,
"url": f"https://arxiv.org/abs/{arxiv_id_clean}" if arxiv_id_clean else arxiv_id_url,
"title": _text("atom:title"),
"published": _text("atom:published"),
"updated": _text("atom:updated"),
"authors": authors,
"categories": categories,
"summary": summary[:400] + ("..." if len(summary) > 400 else ""),
}
)
return papers
# ──────────────────────────────────────────────────────────────────────
# GitHub
# ──────────────────────────────────────────────────────────────────────
def search_github(
opener: urllib.request.OpenerDirector,
query: str,
per_page: int = 10,
) -> list[dict[str, Any]]:
base = "https://api.github.com/search/repositories"
params = urllib.parse.urlencode({"q": query, "sort": "stars", "order": "desc", "per_page": per_page})
url = f"{base}?{params}"
headers = {"User-Agent": "Mozilla/5.0 (research-bot)", "Accept": "application/vnd.github+json"}
token = os.environ.get("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
try:
with opener.open(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
# 60 req/hr 限速时 GitHub 返回 403
print(f"[github][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
return []
except (urllib.error.URLError, TimeoutError) as e:
print(f"[github][{query[:40]}...] ERROR: {e}", file=sys.stderr)
return []
repos: list[dict[str, Any]] = []
for item in data.get("items", []):
repos.append(
{
"name": item.get("full_name", ""),
"description": item.get("description", "") or "",
"stars": item.get("stargazers_count", 0),
"forks": item.get("forks_count", 0),
"language": item.get("language", "") or "",
"url": item.get("html_url", ""),
"updated_at": item.get("updated_at", ""),
}
)
return repos
# ──────────────────────────────────────────────────────────────────────
# 主入口
# ──────────────────────────────────────────────────────────────────────
def main() -> int:
parser = argparse.ArgumentParser(description="arXiv + GitHub 主题化检索")
parser.add_argument("--proxy", default=None, help="代理 URL,如 http://127.0.0.1:6984")
parser.add_argument("--max-results", type=int, default=15, help="每个主题最多论文数")
parser.add_argument("--out", default="research/data/search_results.json", help="输出 JSON 路径")
parser.add_argument(
"--topics",
nargs="+",
default=None,
help="只跑指定主题(默认全部),如:--topics stereo_matching world_model_video",
)
parser.add_argument("--no-github", action="store_true", help="跳过 GitHub 检索")
parser.add_argument("--delay", type=float, default=3.0, help="主题间延迟(秒),arXiv 建议 ≥3s")
args = parser.parse_args()
opener = build_opener(args.proxy)
selected = args.topics or list(TOPICS.keys())
results: dict[str, Any] = {
"meta": {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"proxy": args.proxy or os.environ.get("HTTPS_PROXY") or "direct",
"max_results_per_topic": args.max_results,
},
"arxiv": {},
"github": {},
}
# arXiv
for topic in selected:
if topic not in TOPICS:
print(f"[skip] 未知主题: {topic}", file=sys.stderr)
continue
print(f"[arxiv] 检索主题: {topic}", file=sys.stderr)
papers = search_arxiv(opener, TOPICS[topic], max_results=args.max_results)
results["arxiv"][topic] = papers
print(f"{len(papers)}", file=sys.stderr)
time.sleep(args.delay) # arXiv API rate limit
# GitHub
if not args.no_github:
for topic, q in GITHUB_TOPICS.items():
if args.topics and topic not in args.topics:
continue
print(f"[github] 检索主题: {topic}", file=sys.stderr)
repos = search_github(opener, q, per_page=10)
results["github"][topic] = repos
print(f"{len(repos)} 个仓库", file=sys.stderr)
time.sleep(1.0)
out_path = args.out
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
total_papers = sum(len(v) for v in results["arxiv"].values())
total_repos = sum(len(v) for v in results["github"].values())
print(f"\n✅ 完成:{total_papers} 篇 arXiv + {total_repos} 个 GitHub 仓库 → {out_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())