- find/listdir/r/sound/SendWX 及 WARN.mp3 移入 tools/(sound.py 相对路径同目录保持可用) - Version 死代码(import 自增 VER)归档 tools/Version - Main.py(生产入口候选)与 TODO 保留根目录待确认
210 lines
7.8 KiB
Python
210 lines
7.8 KiB
Python
import os
|
||
import argparse
|
||
import re
|
||
from collections import defaultdict
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, Alignment, Border, Side
|
||
|
||
# 配置默认参数
|
||
DEFAULT_EXTENSIONS = {'py', 'pyw', 'api', 'html', 'js', 'css'}
|
||
AUTO_EXCLUDE_DIRS = {'.env', 'layui', 'node_modules'} # 不区分大小写
|
||
AUTO_EXCLUDE_KEYWORDS = {
|
||
'font-awesome',
|
||
'bootstrap',
|
||
re.compile(r'.*\.lib\.js$', re.IGNORECASE),
|
||
re.compile(r'.*\.lib\.css$', re.IGNORECASE)
|
||
}
|
||
|
||
class CodeCounter:
|
||
def __init__(self, target_dir, exclude_paths=None, extensions=None):
|
||
self.target_dir = os.path.abspath(target_dir)
|
||
self.exclude_paths = [os.path.abspath(p) for p in exclude_paths] if exclude_paths else []
|
||
self.extensions = extensions or DEFAULT_EXTENSIONS
|
||
self.file_stats = [] # 存储每个文件的统计信息
|
||
self.total_lines = 0
|
||
self.total_chars = 0
|
||
self.total_files = 0
|
||
|
||
def _should_exclude_file(self, file_path):
|
||
"""判断文件是否需要排除"""
|
||
# 检查自动排除的目录(任何深度)
|
||
dir_parts = file_path.lower().split(os.sep)
|
||
if any(ed.lower() in dir_parts for ed in AUTO_EXCLUDE_DIRS):
|
||
return True
|
||
|
||
# 检查手动排除的路径
|
||
for ep in self.exclude_paths:
|
||
if file_path.startswith(ep):
|
||
return True
|
||
|
||
# 检查自动排除的关键字/正则
|
||
file_name = os.path.basename(file_path).lower()
|
||
for keyword in AUTO_EXCLUDE_KEYWORDS:
|
||
if isinstance(keyword, re.Pattern):
|
||
if keyword.match(file_name):
|
||
return True
|
||
else:
|
||
if keyword.lower() in file_path.lower():
|
||
return True
|
||
|
||
return False
|
||
|
||
def _get_file_ext(self, file_path):
|
||
"""获取文件扩展名(不含点)"""
|
||
ext = os.path.splitext(file_path)[1].lstrip('.').lower()
|
||
return ext
|
||
|
||
def _count_file(self, file_path):
|
||
"""统计单个文件的行数和字符数"""
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||
content = f.read()
|
||
lines = len(content.splitlines())
|
||
chars = len(content)
|
||
return lines, chars
|
||
except Exception as e:
|
||
print(f"警告:无法读取文件 {file_path} - {e}")
|
||
return 0, 0
|
||
|
||
def scan(self):
|
||
"""扫描目录并统计代码量"""
|
||
print(f"开始扫描目录:{self.target_dir}")
|
||
print(f"包含扩展名:{', '.join(self.extensions)}")
|
||
print(f"排除路径:{', '.join(self.exclude_paths) if self.exclude_paths else '无'}")
|
||
print("-" * 80)
|
||
|
||
for root, dirs, files in os.walk(self.target_dir):
|
||
# 跳过自动排除的目录(优化遍历性能)
|
||
dirs[:] = [d for d in dirs if d.lower() not in AUTO_EXCLUDE_DIRS]
|
||
|
||
for file in files:
|
||
file_path = os.path.join(root, file)
|
||
ext = self._get_file_ext(file_path)
|
||
|
||
# 检查扩展名是否符合要求
|
||
if ext not in self.extensions:
|
||
continue
|
||
|
||
# 检查是否需要排除
|
||
if self._should_exclude_file(file_path):
|
||
continue
|
||
|
||
# 统计文件
|
||
lines, chars = self._count_file(file_path)
|
||
self.file_stats.append({
|
||
'path': file_path,
|
||
'lines': lines,
|
||
'chars': chars
|
||
})
|
||
self.total_lines += lines
|
||
self.total_chars += chars
|
||
self.total_files += 1
|
||
|
||
# 实时输出进度
|
||
print(f"已统计:{file_path} | 行数:{lines} | 字符数:{chars}")
|
||
|
||
print("-" * 80)
|
||
print(f"统计完成!")
|
||
print(f"总文件数:{self.total_files}")
|
||
print(f"总行数:{self.total_lines}")
|
||
print(f"总字符数:{self.total_chars}")
|
||
|
||
def export_xlsx(self, output_path):
|
||
"""导出统计结果到XLSX文件"""
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "代码量统计"
|
||
|
||
# 设置表头
|
||
headers = ['文件路径', '行数', '字符数']
|
||
for col, header in enumerate(headers, 1):
|
||
cell = ws.cell(row=1, column=col, value=header)
|
||
cell.font = Font(bold=True, size=12)
|
||
cell.alignment = Alignment(horizontal='center')
|
||
cell.border = Border(
|
||
left=Side(style='thin'),
|
||
right=Side(style='thin'),
|
||
top=Side(style='thin'),
|
||
bottom=Side(style='thin')
|
||
)
|
||
|
||
# 填充数据
|
||
for row, stat in enumerate(self.file_stats, 2):
|
||
ws.cell(row=row, column=1, value=stat['path'])
|
||
ws.cell(row=row, column=2, value=stat['lines'])
|
||
ws.cell(row=row, column=3, value=stat['chars'])
|
||
|
||
# 设置单元格格式
|
||
for col in range(1, 4):
|
||
cell = ws.cell(row=row, column=col)
|
||
cell.border = Border(
|
||
left=Side(style='thin'),
|
||
right=Side(style='thin'),
|
||
top=Side(style='thin'),
|
||
bottom=Side(style='thin')
|
||
)
|
||
if col > 1:
|
||
cell.alignment = Alignment(horizontal='center')
|
||
|
||
# 添加总计行
|
||
total_row = len(self.file_stats) + 3
|
||
ws.cell(row=total_row, column=1, value='总计')
|
||
ws.cell(row=total_row, column=2, value=self.total_lines)
|
||
ws.cell(row=total_row, column=3, value=self.total_chars)
|
||
for col in range(1, 4):
|
||
cell = ws.cell(row=total_row, column=col)
|
||
cell.font = Font(bold=True)
|
||
cell.alignment = Alignment(horizontal='center')
|
||
cell.border = Border(
|
||
left=Side(style='thin'),
|
||
right=Side(style='thin'),
|
||
top=Side(style='thin'),
|
||
bottom=Side(style='thin')
|
||
)
|
||
|
||
# 调整列宽
|
||
ws.column_dimensions['A'].width = 80
|
||
ws.column_dimensions['B'].width = 15
|
||
ws.column_dimensions['C'].width = 15
|
||
|
||
# 保存文件
|
||
try:
|
||
wb.save(output_path)
|
||
print(f"\nXLSX文件已导出:{os.path.abspath(output_path)}")
|
||
except Exception as e:
|
||
print(f"\n导出XLSX失败:{e}")
|
||
|
||
def main():
|
||
# 解析命令行参数
|
||
parser = argparse.ArgumentParser(description='Python代码量统计工具')
|
||
parser.add_argument('dir', help='要统计的目标目录')
|
||
parser.add_argument('--exclude', nargs='+', default=[],
|
||
help='需要排除的路径(如:TempLate/cdn TempLate/AKS)')
|
||
parser.add_argument('--extensions', nargs='+', default=list(DEFAULT_EXTENSIONS),
|
||
help='要统计的文件扩展名(默认:py pyw api html js css)')
|
||
parser.add_argument('--export', help='导出XLSX文件路径(如:code_stats.xlsx)')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 处理排除路径(相对路径转绝对路径)
|
||
exclude_paths = []
|
||
for ep in args.exclude:
|
||
if os.path.isabs(ep):
|
||
exclude_paths.append(ep)
|
||
else:
|
||
exclude_paths.append(os.path.join(os.path.abspath(args.dir), ep))
|
||
|
||
# 创建统计器并执行统计
|
||
counter = CodeCounter(
|
||
target_dir=args.dir,
|
||
exclude_paths=exclude_paths,
|
||
extensions=set(args.extensions)
|
||
)
|
||
counter.scan()
|
||
|
||
# 导出XLSX(如果指定)
|
||
if args.export:
|
||
counter.export_xlsx(args.export)
|
||
|
||
if __name__ == '__main__':
|
||
main() |