benchmark/springf_project.py

479 lines
15 KiB
Python
Raw 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.

import requests
import os
import time
import datetime
import base64
import json
import xml.etree.ElementTree as ET
import subprocess
import re
import yaml
# 路径配置
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(SCRIPT_DIR, "data")
REPO_DIR = os.path.join(DATA_DIR, "repos1")
LOG_DIR = os.path.join(SCRIPT_DIR, "logs")
OUTPUT_JSON = os.path.join(DATA_DIR, "collected_repos.json")
CLONE_FAIL_FILE = os.path.join(LOG_DIR, "clone_fail.json")
RATE_LIMIT_LOG = os.path.join(LOG_DIR, "rate_limit.log")
MISSING_REPO_LOG = os.path.join(LOG_DIR, "missing_repo.log")
SKIPPED_REPOS_FILE = os.path.join(LOG_DIR, "skipped_repos.json")
COLLECTED_REPOS_FILE = os.path.join(LOG_DIR, "collected_repos.json")
for d in [DATA_DIR, REPO_DIR, LOG_DIR]:
os.makedirs(d, exist_ok=True)
# 读取配置文件
def load_config():
config_path = os.path.join(SCRIPT_DIR, ".env.yml")
if not os.path.exists(config_path):
print(f"Config file not found: {config_path}")
print(f"Please create it based on .env.example.yml")
exit(1)
with open(config_path, "r") as f:
return yaml.safe_load(f)
# 加载配置
config = load_config()
# GitHub 配置
GITHUB_TOKENS = config.get("github_tokens", [])
# 兼容旧版配置
if not GITHUB_TOKENS and config.get("github_token"):
GITHUB_TOKENS = [config.get("github_token")]
# Token轮训索引
current_token_index = 0
# 获取当前Token并轮换
def get_next_token():
global current_token_index
if not GITHUB_TOKENS:
return None
token = GITHUB_TOKENS[current_token_index]
# 轮换到下一个Token
current_token_index = (current_token_index + 1) % len(GITHUB_TOKENS)
return token
BASE_URL = config.get("base_url", "https://api.github.com")
MIN_STARS = config.get("min_stars", 20)
REQUEST_INTERVAL = config.get("request_interval", 0.8)
MAX_RETRY_5XX = config.get("max_retry_5xx", 3)
# clone 控制参数
CLONE_RETRY = config.get("clone_retry", 2)
CLONE_TIMEOUT = config.get("clone_timeout", 180)
# 日志工具
def log_to_file(path, msg):
with open(path, "a", encoding="utf-8") as f:
f.write(msg + "\n")
# 加载已处理的仓库列表(包括已收集和已跳过的)
def load_processed_repos():
"""加载所有已处理的仓库列表,包括已收集和已跳过的,用于后续运行时跳过这些仓库"""
processed = set()
# 加载已跳过的仓库
if os.path.exists(SKIPPED_REPOS_FILE):
try:
with open(SKIPPED_REPOS_FILE, "r", encoding="utf-8") as f:
skipped = json.load(f)
processed.update(skipped)
except Exception as e:
print(f"Failed to load skipped repos: {e}")
# 加载已收集的仓库
if os.path.exists(OUTPUT_JSON):
try:
with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
collected = json.load(f)
processed.update(repo["repo"] for repo in collected)
except Exception as e:
print(f"Failed to load collected repos: {e}")
return processed
# 保存已跳过的仓库列表
def save_skipped_repos(skipped_repos):
"""保存已跳过的仓库列表,以便后续运行时跳过这些仓库"""
try:
with open(SKIPPED_REPOS_FILE, "w", encoding="utf-8") as f:
json.dump(list(skipped_repos), f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Failed to save skipped repos: {e}")
# 保存已收集的仓库列表
def save_collected_repos(collected_repos):
"""保存已收集的仓库列表,以便后续运行时跳过这些仓库"""
try:
with open(COLLECTED_REPOS_FILE, "w", encoding="utf-8") as f:
json.dump(list(collected_repos), f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Failed to save collected repos: {e}")
# 加载克隆失败的仓库列表
def load_clone_failures():
"""加载克隆失败的仓库列表"""
if not os.path.exists(CLONE_FAIL_FILE):
return []
try:
with open(CLONE_FAIL_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Failed to load clone failures: {e}")
return []
# 保存克隆失败的仓库列表
def save_clone_failures(clone_failures):
"""保存克隆失败的仓库列表"""
try:
with open(CLONE_FAIL_FILE, "w", encoding="utf-8") as f:
json.dump(clone_failures, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Failed to save clone failures: {e}")
# GitHub GET
def github_get(url, params=None):
retry_5xx = 0
while True:
# 获取当前请求的Token和Headers
token = get_next_token()
headers = {"Accept": "application/vnd.github+json"}
if token:
headers["Authorization"] = f"token {token}"
r = requests.get(url, headers=headers, params=params)
if r.status_code == 200:
time.sleep(REQUEST_INTERVAL)
return r
elif r.status_code == 403:
reset = r.headers.get("X-RateLimit-Reset")
if reset:
sleep_time = int(reset) - int(time.time()) + 1
log_to_file(RATE_LIMIT_LOG, f"403 rate limit → sleep {sleep_time}s, url={url}")
time.sleep(max(sleep_time, 1))
continue
return None
elif r.status_code == 404:
log_to_file(MISSING_REPO_LOG, f"404 missing {url}")
return None
elif 500 <= r.status_code < 600:
retry_5xx += 1
if retry_5xx <= MAX_RETRY_5XX:
time.sleep(2)
continue
return None
else:
return None
# 搜索仓库(按日期)
def search_repositories_by_stars(start_date, end_date):
query = f"language:Java stars:>={MIN_STARS} created:{start_date}..{end_date}"
repos = []
for page in range(1, 11):
r = github_get(
f"{BASE_URL}/search/repositories",
params={
"q": query,
"sort": "stars",
"order": "desc",
"per_page": 100,
"page": page
},
)
if not r:
break
items = r.json().get("items", [])
if not items:
break
repos.extend(items)
return repos
# 检查文件是否存在于仓库中并返回相对路径(支持任意路径)
def check_file_exists(repo, filename):
"""
使用 GitHub 搜索 API 检查文件是否存在于仓库的任何位置
使用模式repo:{repo} path:{filename}
返回:如果存在返回文件的相对路径,否则返回 None
"""
search_url = f"{BASE_URL}/search/code"
params = {
"q": f"repo:{repo} path:{filename}",
"per_page": 1 # 只需要知道第一个匹配结果
}
r = github_get(search_url, params=params)
if not r:
return None
try:
result = r.json()
if result.get("total_count", 0) > 0:
# 提取第一个匹配结果的相对路径
item = result.get("items", [])[0]
return item.get("path", None)
return None
except Exception:
return None
# 检查仓库中是否包含@Test注解支持任意路径
def check_has_test_annotations(repo):
"""
使用 GitHub 搜索 API 检查仓库中是否包含@Test注解
使用模式repo:{repo} @Test
"""
search_url = f"{BASE_URL}/search/code"
params = {
"q": f"repo:{repo} @Test",
"per_page": 1 # 只需要知道是否存在,不需要所有结果
}
r = github_get(search_url, params=params)
if not r:
return False
try:
result = r.json()
return result.get("total_count", 0) > 0
except Exception:
return False
# 检查仓库中是否包含 Spring Framework 导入语句(支持任意路径)
def check_has_spring_imports(repo):
"""
使用 GitHub 搜索 API 检查仓库中是否包含 Spring Framework 导入语句
使用模式repo:{repo} import org.springframework
"""
search_url = f"{BASE_URL}/search/code"
params = {
"q": f'repo:{repo} "import org.springframework"',
"per_page": 1 # 只需要知道是否存在,不需要所有结果
}
r = github_get(search_url, params=params)
if not r:
return False
try:
result = r.json()
return result.get("total_count", 0) > 0
except Exception:
return False
# 获取文件内容(用于获取构建文件内容进行分析,支持任意路径)
def get_file_content(repo, path):
r = github_get(f"{BASE_URL}/repos/{repo}/contents/{path}")
if not r:
return None
c = r.json().get("content")
if not c:
return None
return base64.b64decode(c).decode("utf-8", errors="ignore")
# Maven 解析
def analyze_pom(pom_text):
uses_test = False
uses_spring = False
try:
root = ET.fromstring(pom_text)
except ET.ParseError:
return False, False
for dep in root.findall(".//dependencies/dependency"):
gid = dep.findtext("groupId")
if not gid:
continue
gid = gid.strip()
# 测试框架
if gid in {"junit", "org.junit.jupiter", "org.testng"}:
uses_test = True
# Spring Framework
if gid == "org.springframework":
uses_spring = True
return uses_test, uses_spring
# Gradle 解析(正则)
SPRING_RE = re.compile(r'["\']org\.springframework:([^"\']+)["\']')
JUNIT_RE = re.compile(r'["\'](junit:junit|org\.junit\.jupiter:[^"\']+)["\']')
def analyze_gradle(gradle_text):
uses_spring = bool(SPRING_RE.search(gradle_text))
uses_test = bool(JUNIT_RE.search(gradle_text))
return uses_test, uses_spring
# 克隆仓库
def clone_repo(repo_full_name, clone_failures):
local_name = repo_full_name.replace("/", "_")
target = os.path.join(REPO_DIR, local_name)
if os.path.exists(target):
print(f" [SKIP] {repo_full_name} already cloned")
return target
for attempt in range(1, CLONE_RETRY + 1):
print(f" [CLONE] {repo_full_name} (attempt {attempt}/{CLONE_RETRY})")
try:
result = subprocess.run(
["git", "clone", "--depth", "1",
f"git@github.com:{repo_full_name}.git", target],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=CLONE_TIMEOUT
)
except subprocess.TimeoutExpired:
msg = f"timeout after {CLONE_TIMEOUT}s"
print(f"{msg}")
if attempt == CLONE_RETRY:
# 添加到克隆失败列表
clone_failures.append({
"repo": repo_full_name,
"error": msg,
"timestamp": datetime.datetime.now().isoformat()
})
# 立即保存
save_clone_failures(clone_failures)
continue
if result.returncode == 0:
return target
error_msg = result.stderr.strip() or "unknown error"
print(f" ↳ git error: {error_msg}")
if attempt == CLONE_RETRY:
# 添加到克隆失败列表
clone_failures.append({
"repo": repo_full_name,
"error": error_msg,
"timestamp": datetime.datetime.now().isoformat()
})
# 立即保存
save_clone_failures(clone_failures)
time.sleep(2)
return None
# 分析仓库
def analyze_repository(repo):
name = repo["full_name"]
stars = repo.get("stargazers_count", 0)
# 使用 GitHub 搜索 API 检查是否包含 Spring Framework 导入语句
uses_spring = check_has_spring_imports(name)
if not uses_spring:
print(f" [SKIP] {name} no Spring Framework imports detected")
return None
# 使用 GitHub 搜索 API 检查是否包含 @Test 注解
uses_test = check_has_test_annotations(name)
if not uses_test:
print(f" [SKIP] {name} no junit/testng detected")
return None
print(f" [OK] {name} passed all filters")
return {
"repo": name,
"stars": stars
}
def main():
results = []
# 加载已跳过的仓库列表
skipped_repos = set()
if os.path.exists(SKIPPED_REPOS_FILE):
try:
with open(SKIPPED_REPOS_FILE, "r", encoding="utf-8") as f:
skipped_repos = set(json.load(f))
except Exception as e:
print(f"Failed to load skipped repos: {e}")
# 加载已收集的仓库列表
collected_repos = set()
if os.path.exists(OUTPUT_JSON):
try:
with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
collected = json.load(f)
collected_repos = set(repo["repo"] for repo in collected)
except Exception as e:
print(f"Failed to load collected repos: {e}")
# 加载克隆失败的仓库列表
clone_failures = load_clone_failures()
# 提取克隆失败的仓库名集合,用于跳过已失败的仓库
failed_repos = set(failure["repo"] for failure in clone_failures)
# 合并为所有已处理的仓库(包括克隆失败的)
processed_repos = skipped_repos.union(collected_repos).union(failed_repos)
print(f"Loaded {len(processed_repos)} processed repositories")
print(f"Loaded {len(clone_failures)} clone failures")
start_date = datetime.date(2025, 2, 8)
end_date = datetime.date(2025, 6, 30)
delta = datetime.timedelta(days=1)
current_date = start_date
while current_date <= end_date:
next_date = current_date + delta
print(f"\n Searching date: {current_date}")
repos = search_repositories_by_stars(
current_date.isoformat(),
(next_date - datetime.timedelta(seconds=1)).isoformat()
)
print(f" found {len(repos)} repos")
for repo in repos:
repo_full_name = repo["full_name"]
# 检查是否已处理此仓库(无论是收集还是跳过)
if repo_full_name in processed_repos:
print(f" [SKIP] {repo_full_name} already processed in previous run")
continue
info = analyze_repository(repo)
if info:
results.append(info)
# 每次成功添加结果后立即保存
with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
# 更新已处理和已收集的仓库列表
processed_repos.add(repo_full_name)
collected_repos.add(repo_full_name)
# 立即保存已收集的仓库列表,确保数据不丢失
save_collected_repos(collected_repos)
else:
# 更新已处理和已跳过的仓库列表
processed_repos.add(repo_full_name)
skipped_repos.add(repo_full_name)
# 立即保存跳过的仓库列表,确保数据不丢失
save_skipped_repos(skipped_repos)
current_date = next_date
print(f"\n Total collected: {len(results)} repositories")
print(f" Saved to {OUTPUT_JSON}")
print(f" Clone failed repos saved to {CLONE_FAIL_FILE}")
print(f" Skipped {len(skipped_repos)} repositories")
print(f" Skipped repos saved to {SKIPPED_REPOS_FILE}")
if __name__ == "__main__":
main()