文件内容复制工具 - 技术文档
概述
file_content_copy.py 是一个用于批量处理项目文件的Python工具,能够将指定目录下多种文件类型的內容整合到单个输出文件中,同时生成清晰的项目目录结构。
功能特性
核心功能
多格式文件收集: 支持同时处理多种文件扩展名(.py, .txt, .md, .js, .css, .html等)
智能目录排除: 自动排除常见的开发环境目录和缓存目录
目录结构生成: 自动生成树形结构的项目目录概览
编码自动检测: 支持多种编码格式(UTF-8, GBK, Latin-1)
文件大小限制: 可配置最大文件处理大小,避免处理过大文件
安全特性
权限处理: 完善的异常处理,避免因权限问题导致程序崩溃
路径安全: 使用相对路径检查,确保文件在项目目录内
隐藏文件过滤: 自动跳过以
.开头的隐藏文件和目录
技术架构
模块结构
file_content_copy.py ├── get_directory_structure() # 目录树生成器 ├── copy_files_by_extension() # 主处理函数 └── 主程序入口
依赖库
os: 操作系统接口glob: 文件模式匹配pathlib: 面向对象的文件系统路径操作
核心函数详解
1. get_directory_structure(directory, all_files, exclude_dirs=None)
功能: 生成格式化的目录结构树
参数:
directory: 目标目录路径all_files: 要包含的文件列表exclude_dirs: 排除目录列表
2. copy_files_by_extension(directory, extensions, exclude_dirs, output_dirname, max_file_size)
主处理函数参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
directory | str | 必需 | 要处理的根目录路径 |
extensions | list | 必需 | 文件扩展名列表 ['.py', '.txt'] |
exclude_dirs | list | [] | 排除目录列表 |
output_dirname | str | "output_files" | 输出目录名称 |
max_file_size | int | 10MB | 最大文件大小限制(字节) |
文件命名规则
每种文件类型生成独立的输出文件
命名格式:
all_{ext_name}_files.txt示例:
all_py_files.txt,all_md_files.txt
错误处理机制
异常类型处理
文件编码错误: 尝试多种编码方案
权限错误: 跳过无法访问的文件/目录
路径错误: 检查文件是否在项目目录内
大小限制: 跳过超过设定大小的文件
日志输出
处理进度实时显示
错误和警告信息明确
最终统计报告
应用场景
代码审查
快速收集项目所有源代码文件
生成完整的代码快照
项目文档
自动生成项目结构文档
代码片段收集和整理
备份归档
选择性备份特定类型文件
创建轻量级项目快照
性能考虑
优化措施
使用集合(Set)进行快速文件查找
递归目录遍历避免重复扫描
文件大小预检查避免内存溢出
限制配置
默认10MB文件大小限制
可调整的排除规则
灵活的扩展名配置
扩展建议
可能的增强功能
压缩输出: 自动压缩输出文件
模式匹配: 支持正则表达式文件匹配
增量处理: 只处理修改过的文件
远程存储: 支持输出到云存储
这个工具特别适合需要快速收集和分析项目文件结构的开发场景,如代码审查、项目迁移或文档生成等任务。
附:代码
import os
import glob
from pathlib import Path
def get_directory_structure(directory, all_files, exclude_dirs=None):
"""
生成目录结构树
"""
if exclude_dirs is None:
exclude_dirs = []
project_root = Path(directory).resolve()
structure = ["#" * 50 + "
",
"# 项目目录结构:
",
"#" * 50 + "
"]
# 安全地创建文件路径集合
file_set = set()
for f in all_files:
file_path = Path(f)
try:
relative_path = file_path.relative_to(project_root)
# 检查文件是否在排除目录中
skip = any(
str(relative_path) == str(Path(exclude_dir)) or
str(relative_path).startswith(str(Path(exclude_dir)) + os.sep)
for exclude_dir in exclude_dirs
)
if skip:
continue
file_set.add(relative_path)
except ValueError:
print(f"警告: 文件 {f} 不在项目目录内,已跳过")
continue
def build_tree(current_path, prefix=""):
"""递归构建目录树"""
# 检查当前目录是否在排除列表中
current_rel_path = current_path.relative_to(project_root)
if any(
str(current_rel_path) == str(Path(exclude_dir)) or
str(current_rel_path).startswith(str(Path(exclude_dir)) + os.sep)
for exclude_dir in exclude_dirs
):
return
items = []
try:
for item in sorted(current_path.iterdir()):
if item.name.startswith('.'): # 跳过隐藏文件/目录
continue
# 检查是否在排除目录中
item_rel_path = item.relative_to(project_root)
skip_item = any(
str(item_rel_path) == str(Path(exclude_dir)) or
str(item_rel_path).startswith(str(Path(exclude_dir)) + os.sep)
for exclude_dir in exclude_dirs
)
if skip_item:
continue
if item.is_dir():
# 检查该目录下是否有目标文件
has_target_files = any(
str(file_path).startswith(str(item.relative_to(project_root)))
for file_path in file_set
)
if has_target_files:
items.append((item, 'dir'))
else:
rel_path = item.relative_to(project_root)
if rel_path in file_set:
items.append((item, 'file'))
except (PermissionError, OSError) as e:
print(f"无法访问目录 {current_path}: {e}")
return
for index, (item, item_type) in enumerate(items):
is_last = index == len(items) - 1
connector = "└── " if is_last else "├── "
if item_type == 'dir':
structure.append(f"{prefix}{connector}{item.name}/
")
new_prefix = prefix + (" " if is_last else "│ ")
build_tree(item, new_prefix)
else: # file
structure.append(f"{prefix}{connector}{item.name}
")
# 从项目根目录开始构建
structure.append(f"{project_root.name}/
")
build_tree(project_root, "")
structure.append("
" + "#" * 50 + "
")
return "".join(structure)
def copy_files_by_extension(directory, extensions, exclude_dirs=None,
output_dirname="output_files", max_file_size=10 * 1024 * 1024):
"""
将目录下指定后缀的文件内容复制到单个文件中,每种后缀生成一个文件
Args:
directory: 要处理的根目录路径
extensions: 要处理的文件扩展名列表
exclude_dirs: 要排除的目录列表
output_dirname: 输出目录名称
max_file_size: 最大文件大小限制(字节)
"""
if exclude_dirs is None:
exclude_dirs = []
directory = os.path.abspath(directory)
project_root = Path(directory).resolve()
# 验证目录存在
if not project_root.exists():
print(f"错误: 目录 {directory} 不存在")
return
# 创建输出目录
output_dir = project_root / output_dirname
output_dir.mkdir(exist_ok=True)
# 将输出目录添加到排除列表中
if output_dirname not in exclude_dirs:
exclude_dirs.append(output_dirname)
# 获取所有指定后缀的文件
all_files = []
for ext in extensions:
pattern = f"**/*{ext}" if not ext.startswith('.') else f"**/*{ext}"
ext_files = [str(path) for path in project_root.glob(pattern)]
all_files.extend(ext_files)
if not all_files:
print(f"未找到任何 {extensions} 文件")
return
print(f"找到 {len(all_files)} 个目标文件")
# 为每种后缀创建一个输出文件
for ext in extensions:
pattern = f"**/*{ext}" if not ext.startswith('.') else f"**/*{ext}"
ext_files = [str(path) for path in project_root.glob(pattern)]
# 过滤排除目录中的文件
filtered_files = []
for file_path in ext_files:
file_obj = Path(file_path)
try:
relative_path = file_obj.relative_to(project_root)
# 检查排除条件
skip = any(
str(relative_path) == str(Path(exclude_dir)) or
str(relative_path).startswith(str(Path(exclude_dir)) + os.sep)
for exclude_dir in exclude_dirs
)
if not skip:
# 检查文件大小
try:
file_size = file_obj.stat().st_size
if file_size > max_file_size:
print(f"跳过大文件: {relative_path} ({file_size // 1024}KB)")
continue
filtered_files.append(file_path)
except OSError as e:
print(f"无法获取文件大小 {relative_path}: {e}")
except ValueError:
print(f"警告: 文件 {file_path} 不在项目目录内,已跳过")
continue
if not filtered_files:
print(f"未找到任何 {ext} 文件(排除后)")
continue
# 创建输出文件
ext_name = ext.lstrip('.')
output_file = output_dir / f"all_{ext_name}_files.txt"
try:
with open(output_file, 'w', encoding='utf-8') as outfile:
# 写入目录结构
try:
directory_structure = get_directory_structure(directory, all_files, exclude_dirs)
outfile.write(directory_structure)
except Exception as e:
print(f"生成目录结构时出错: {e}")
outfile.write("目录结构生成失败
")
# 写入文件内容
processed_files = 0
for file_path in filtered_files:
file_obj = Path(file_path)
try:
relative_path = file_obj.relative_to(project_root)
except ValueError:
relative_path = file_obj
# 写入文件分隔符
outfile.write(f"
{'#' * 80}
")
outfile.write(f"# 文件: {relative_path}
")
outfile.write(f"{'#' * 80}
")
try:
# 尝试多种编码
encodings = ['utf-8', 'gbk', 'latin-1']
content = None
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as infile:
content = infile.read()
break
except UnicodeDecodeError:
continue
if content is not None:
outfile.write(content)
outfile.write("
")
print(f"已处理: {relative_path}")
processed_files += 1
else:
outfile.write(f"无法读取文件: 编码问题
")
print(f"读取失败(编码问题): {relative_path}")
except Exception as e:
outfile.write(f"读取文件失败: {e}
")
print(f"读取失败: {relative_path} - {e}")
print(f"成功处理 {processed_files}/{len(filtered_files)} 个 {ext} 文件,结果保存在: {output_file}")
except Exception as e:
print(f"创建输出文件失败 {output_file}: {e}")
print(f"
所有输出文件已保存到: {output_dir}")
# 使用示例
if __name__ == "__main__":
# 指定要处理的根目录路径
root_directory = r"E:PythonProjectmysql_backup" # 可以修改为任何目录路径
# 指定要处理的后缀列表
extensions = ['.py', '.txt', '.md', '.js', '.css', '.html']
# 指定要排除的目录列表(相对于根目录的路径)
exclude_directories = [
"__pycache__", # Python缓存目录
".git", # Git目录
"node_modules", # Node.js依赖目录
".venv", # Python虚拟环境
"dist", # 构建输出目录
"build", # 构建目录
".vscode", # VSCode配置目录
".idea", # PyCharm配置目录
"project_files" # 输出目录自身
]
# 输出目录名称
output_directory = ".project_files"
# 调用函数处理文件
copy_files_by_extension(
directory=root_directory,
extensions=extensions,
exclude_dirs=exclude_directories,
output_dirname=output_directory,
max_file_size=10 * 1024 * 1024 # 10MB限制
)