"""统计项目代码行数,按后端/前端/测试分类"""
import os
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
GROUPS: dict[str, dict[str, list[str]]] = {
"后端": {
"dirs": ["backend"],
"exts": [".py"],
"exclude": [],
},
"前端": {
"dirs": ["frontend/src"],
"exts": [".vue", ".js", ".css"],
"exclude": [],
},
"测试": {
"dirs": ["tests"],
"exts": [".py"],
"exclude": [],
},
"工具/配置": {
"dirs": ["tools", "config", "constants"],
"exts": [".py", ".toml", ".sh", ".json"],
"exclude": ["code_spectra.db"],
},
}
def display_width(s: str) -> int:
"""计算字符串的终端显示宽度(中文等全角字符占 2 列)"""
w = 0
for ch in s:
if '\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef':
w += 2
else:
w += 1
return w
def pad_str(s: str, width: int, align: str = "left") -> str:
"""按显示宽度填充字符串"""
dw = display_width(s)
pad = width - dw
if align == "left":
return s + " " * max(pad, 0)
return " " * max(pad, 0) + s
def count_lines(path: Path) -> int:
"""统计单个文件的非空行数"""
try:
with open(path, encoding="utf-8", errors="ignore") as f:
return sum(1 for line in f if line.strip())
except Exception:
return 0
def main() -> None:
results: dict[str, dict] = {}
grand_total = 0
for group_name, cfg in GROUPS.items():
total = 0
files = 0
for d in cfg["dirs"]:
dir_path = PROJECT_ROOT / d
if not dir_path.exists():
continue
for ext in cfg["exts"]:
for fpath in dir_path.rglob(f"*{ext}"):
if fpath.name.startswith("."):
continue
if "__pycache__" in fpath.parts or "node_modules" in fpath.parts:
continue
lines = count_lines(fpath)
total += lines
files += 1
results[group_name] = {"files": files, "lines": total}
grand_total += total
print(f"{pad_str('类别', 10)} {pad_str('文件数', 6, 'right')} {pad_str('行数', 8, 'right')}")
print("-" * 26)
total_files = sum(r['files'] for r in results.values())
for group_name, stats in results.items():
print(
f"{pad_str(group_name, 10)} "
f"{pad_str(str(stats['files']), 6, 'right')} "
f"{pad_str(str(stats['lines']), 8, 'right')}"
)
print("-" * 26)
print(
f"{pad_str('合计', 10)} "
f"{pad_str(str(total_files), 6, 'right')} "
f"{pad_str(str(grand_total), 8, 'right')}"
)
if __name__ == "__main__":
main()