commit 6d9b6fd1da6b3595b849299fee5e9607a17f0e57 Author: hblu Date: Sun Dec 7 04:48:18 2025 +0800 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fac36a --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# 作业 3:文件批量处理工具 + +## 任务 +- 在 `src/file_tool.py` 中完成 `FileTool` 类:实现文件列出、筛选、批量重命名、按类型整理、统计等功能。 +- 处理各种边界情况:隐藏文件、无扩展名文件、重命名冲突、权限错误等。 +- 通过公开测试与隐藏测试;提交 `REPORT.md` 反思报告。 + +⚠️ **安全提示**:本作业涉及文件操作,请始终在测试目录中操作,先用 `dry_run` 模式预览。 + +## 环境与依赖 +- Python 3.11+ +- 安装依赖:`pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple` + +## 本地运行 +```bash +python -m pytest -v +``` + +## 提交要求 +- 提交信息需包含关键字"完成作业"以触发评分。 +- 确保 `REPORT.md` 已填写。 + +## 评分构成(总分 20) +- Core 测试:10 分 +- Edge 测试:5 分 +- REPORT.md:5 分 + diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..c9a61cb --- /dev/null +++ b/REPORT.md @@ -0,0 +1,42 @@ +# 作业 3 反思报告 + +## 1. 安全意识 + +在测试批量重命名功能时,你是如何确保不会误操作重要文件的? + +- 你使用了 dry_run 模式吗? +- 你是在哪个目录测试的? +- 如果代码有 bug,最坏情况会发生什么? + +> [在此处回答] + +## 2. 冲突处理的决策 + +当重命名遇到冲突(目标文件已存在)时,可能的处理方式有: +- A. 直接覆盖 +- B. 跳过 +- C. 添加数字后缀(如 file_1.txt) +- D. 抛出异常 + +你选择了哪种方式?为什么?这个决策有什么权衡? + +> [在此处回答] + +## 3. AI 代码审查 + +AI 生成的文件操作代码,你发现了哪些潜在风险? + +- AI 初版代码是否考虑了权限问题? +- AI 初版代码是否考虑了冲突问题? +- 你做了哪些修改来让代码更安全? + +> [在此处回答] + +## 4. 责任思考 + +如果这个工具要给其他人使用,你会添加什么安全措施? + +(提示:确认提示、日志记录、备份机制、撤销功能等) + +> [在此处回答] + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4950232 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pytest>=7.0.0 + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/file_tool.py b/src/file_tool.py new file mode 100644 index 0000000..36af730 --- /dev/null +++ b/src/file_tool.py @@ -0,0 +1,245 @@ +""" +文件批量处理工具 + +你的任务是实现 FileTool 类,批量处理目录下的文件。 + +功能要求: +1. 列出目录下所有文件(支持递归、支持隐藏文件筛选) +2. 按扩展名筛选文件 +3. 批量重命名(添加前缀/后缀,支持 dry_run 预览模式) +4. 按文件类型整理到子目录 +5. 统计文件大小分布 + +⚠️ 安全注意事项: +- 文件操作是危险的,错误的代码可能导致数据丢失 +- 始终在测试目录中操作,不要在重要目录测试 +- 先用 dry_run 模式预览,确认无误后再执行 +- 重命名冲突时绝不应该静默覆盖已有文件 + +边界情况处理: +- 隐藏文件(以.开头):默认不列出,可选包含 +- 无扩展名文件:extension 字段返回空字符串,不应崩溃 +- 重命名冲突:不覆盖已有文件,应跳过或报错 +- 权限错误:无权限文件应跳过,不崩溃 +- 符号链接:特殊处理,不跟随链接 + +示例用法: +tool = FileTool("/path/to/directory") +files = tool.list_files(recursive=True) +result = tool.batch_rename("*.txt", prefix="2024_", dry_run=True) # 先预览 +result = tool.batch_rename("*.txt", prefix="2024_") # 确认后执行 +stats = tool.get_size_stats() + +提示: +- 使用 pathlib 库处理路径(比 os.path 更现代) +- 使用 fnmatch 或 glob 进行模式匹配 +- 妥善处理各种异常:PermissionError, FileExistsError, FileNotFoundError +""" + +from pathlib import Path +from typing import List, Dict, Optional +import fnmatch + + +class FileTool: + """ + 文件批量处理工具 + + 提供文件列出、筛选、重命名、整理、统计等功能。 + """ + + def __init__(self, root_path: str): + """ + 初始化,设置工作目录 + + Args: + root_path: 工作目录路径 + """ + self.root = Path(root_path) + # TODO: 你可以添加其他属性来辅助实现 + + def list_files(self, recursive: bool = False, + include_hidden: bool = False) -> List[Dict]: + """ + 列出目录内容 + + Args: + recursive: 是否递归子目录 + include_hidden: 是否包含隐藏文件(以.开头的文件) + + Returns: + 文件信息列表,每个元素包含: + - 'name': 文件名 + - 'path': 完整路径(字符串) + - 'size': 文件大小(字节),目录为 0 + - 'type': 'file' 或 'directory' + - 'extension': 扩展名(如 '.txt'),无扩展名为空字符串 + + 示例返回: + [ + {'name': 'file.txt', 'path': '/path/file.txt', 'size': 1024, + 'type': 'file', 'extension': '.txt'}, + {'name': 'subdir', 'path': '/path/subdir', 'size': 0, + 'type': 'directory', 'extension': ''}, + ] + """ + # TODO: 在此实现你的代码 + pass + + def filter_by_extension(self, extensions: List[str]) -> List[Dict]: + """ + 按扩展名筛选文件 + + Args: + extensions: 扩展名列表,如 ['.txt', '.py', '.md'] + + Returns: + 匹配的文件列表(格式同 list_files) + + 注意: + - 扩展名比较应不区分大小写(.TXT 和 .txt 应该匹配) + - 只返回文件,不返回目录 + """ + # TODO: 在此实现你的代码 + pass + + def batch_rename(self, pattern: str, + prefix: str = '', + suffix: str = '', + dry_run: bool = False) -> Dict: + """ + 批量重命名 + + Args: + pattern: 文件名匹配模式(如 '*.txt', 'report_*.csv') + prefix: 添加前缀 + suffix: 添加后缀(在扩展名之前) + dry_run: 如果为 True,只返回预览结果,不实际重命名 + + Returns: + { + 'success': [{'old': 'file.txt', 'new': 'prefix_file_suffix.txt'}, ...], + 'failed': [{'file': 'xxx', 'reason': '目标文件已存在'}, ...], + 'skipped': [{'file': 'xxx', 'reason': '无权限'}, ...] + } + + 重要: + - dry_run=True 时只返回预览,不实际修改任何文件 + - 重命名冲突时(目标文件已存在)不应覆盖,应放入 failed 或 skipped + - 无权限时应跳过,不崩溃 + + 示例: + # 先预览 + result = tool.batch_rename("*.txt", prefix="2024_", dry_run=True) + print(result) # 查看会重命名哪些文件 + + # 确认后执行 + result = tool.batch_rename("*.txt", prefix="2024_") + """ + # TODO: 在此实现你的代码 + pass + + def organize_by_type(self, type_mapping: Optional[Dict[str, str]] = None, + dry_run: bool = False) -> Dict: + """ + 按文件类型分类到子目录 + + Args: + type_mapping: 扩展名到目录的映射,如: + {'.txt': 'documents', '.py': 'code', '.jpg': 'images', '.png': 'images'} + 如果为 None,使用默认映射 + dry_run: 预览模式 + + Returns: + { + 'moved': [{'file': 'xxx', 'from': '...', 'to': '...'}, ...], + 'failed': [{'file': 'xxx', 'reason': '...'}, ...] + } + + 默认映射: + - documents: .txt, .doc, .docx, .pdf, .md + - code: .py, .js, .java, .c, .cpp, .h + - images: .jpg, .jpeg, .png, .gif, .bmp + - data: .csv, .json, .xml, .yaml, .yml + - other: 其他未匹配的文件 + """ + # TODO: 在此实现你的代码 + pass + + def get_size_stats(self) -> Dict: + """ + 统计文件大小分布 + + Returns: + { + 'total_files': 100, # 文件总数 + 'total_size': 1024000, # 总大小(字节) + 'by_size': { # 按大小分布 + '<1KB': 20, + '1KB-100KB': 50, + '100KB-1MB': 20, + '>1MB': 10 + }, + 'by_type': { # 按类型分布 + '.txt': {'count': 30, 'size': 102400}, + '.py': {'count': 20, 'size': 51200}, + '': {'count': 5, 'size': 1024}, # 无扩展名 + ... + } + } + + 注意: + - 只统计文件,不统计目录 + - 无扩展名文件的 key 为空字符串 '' + """ + # TODO: 在此实现你的代码 + pass + + +if __name__ == "__main__": + # 测试你的实现 + import tempfile + import os + + # 创建临时测试目录 + with tempfile.TemporaryDirectory() as tmp_dir: + # 创建测试文件 + Path(tmp_dir, "file1.txt").write_text("hello") + Path(tmp_dir, "file2.py").write_text("print('hi')") + Path(tmp_dir, "report_01.txt").write_text("report 1") + Path(tmp_dir, "report_02.txt").write_text("report 2") + Path(tmp_dir, ".hidden").write_text("hidden file") + Path(tmp_dir, "README").write_text("no extension") + Path(tmp_dir, "subdir").mkdir() + + tool = FileTool(tmp_dir) + + # 测试列出文件 + print("=== 列出文件(不含隐藏)===") + files = tool.list_files() + for f in files: + print(f" {f['name']} ({f['type']}, {f['size']} bytes)") + + # 测试列出文件(含隐藏) + print("\n=== 列出文件(含隐藏)===") + files = tool.list_files(include_hidden=True) + for f in files: + print(f" {f['name']} ({f['type']})") + + # 测试筛选 + print("\n=== 筛选 .txt 文件 ===") + txt_files = tool.filter_by_extension(['.txt']) + for f in txt_files: + print(f" {f['name']}") + + # 测试重命名(预览) + print("\n=== 重命名预览 ===") + result = tool.batch_rename("report_*.txt", prefix="2024_", dry_run=True) + print(f" 预览结果: {result}") + + # 测试统计 + print("\n=== 文件统计 ===") + stats = tool.get_size_stats() + print(f" 总文件数: {stats.get('total_files', 'N/A')}") + print(f" 总大小: {stats.get('total_size', 'N/A')} bytes") + diff --git a/tests/test_public.py b/tests/test_public.py new file mode 100644 index 0000000..f9bdc17 --- /dev/null +++ b/tests/test_public.py @@ -0,0 +1,74 @@ +""" +公开测试 - 学生可见 +这些测试帮助你验证基本功能是否正确 +""" + +import pytest +from pathlib import Path +from src.file_tool import FileTool + + +@pytest.fixture +def sample_dir(tmp_path): + """创建测试目录结构""" + # 创建文件 + (tmp_path / "file1.txt").write_text("hello") + (tmp_path / "file2.py").write_text("print('hi')") + (tmp_path / "report_01.txt").write_text("report 1") + (tmp_path / "report_02.txt").write_text("report 2") + # 创建子目录 + (tmp_path / "subdir").mkdir() + (tmp_path / "subdir" / "nested.txt").write_text("nested") + return tmp_path + + +def test_list_files_basic(sample_dir): + """测试基本列出文件功能""" + tool = FileTool(str(sample_dir)) + files = tool.list_files() + + # 应该包含文件和目录 + names = [f['name'] for f in files] + assert "file1.txt" in names + assert "file2.py" in names + assert "subdir" in names + + +def test_list_files_info(sample_dir): + """测试文件信息是否完整""" + tool = FileTool(str(sample_dir)) + files = tool.list_files() + + # 找到 file1.txt + txt_file = next(f for f in files if f['name'] == 'file1.txt') + + assert txt_file['type'] == 'file' + assert txt_file['extension'] == '.txt' + assert txt_file['size'] > 0 + assert 'path' in txt_file + + +def test_filter_by_extension(sample_dir): + """测试按扩展名筛选""" + tool = FileTool(str(sample_dir)) + txt_files = tool.filter_by_extension(['.txt']) + + names = [f['name'] for f in txt_files] + assert "file1.txt" in names + assert "report_01.txt" in names + assert "file2.py" not in names + + +def test_batch_rename_dry_run(sample_dir): + """测试重命名预览模式""" + tool = FileTool(str(sample_dir)) + result = tool.batch_rename("report_*.txt", prefix="2024_", dry_run=True) + + # 预览模式应该返回结果 + assert 'success' in result + assert len(result['success']) == 2 + + # 但文件不应该被实际重命名 + assert (sample_dir / "report_01.txt").exists() + assert not (sample_dir / "2024_report_01.txt").exists() +