Initial commit

This commit is contained in:
hblu 2025-12-07 04:48:18 +08:00
commit 6d9b6fd1da
6 changed files with 390 additions and 0 deletions

27
README.md Normal file
View File

@ -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.md5 分

42
REPORT.md Normal file
View File

@ -0,0 +1,42 @@
# 作业 3 反思报告
## 1. 安全意识
在测试批量重命名功能时,你是如何确保不会误操作重要文件的?
- 你使用了 dry_run 模式吗?
- 你是在哪个目录测试的?
- 如果代码有 bug最坏情况会发生什么
> [在此处回答]
## 2. 冲突处理的决策
当重命名遇到冲突(目标文件已存在)时,可能的处理方式有:
- A. 直接覆盖
- B. 跳过
- C. 添加数字后缀(如 file_1.txt
- D. 抛出异常
你选择了哪种方式?为什么?这个决策有什么权衡?
> [在此处回答]
## 3. AI 代码审查
AI 生成的文件操作代码,你发现了哪些潜在风险?
- AI 初版代码是否考虑了权限问题?
- AI 初版代码是否考虑了冲突问题?
- 你做了哪些修改来让代码更安全?
> [在此处回答]
## 4. 责任思考
如果这个工具要给其他人使用,你会添加什么安全措施?
(提示:确认提示、日志记录、备份机制、撤销功能等)
> [在此处回答]

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pytest>=7.0.0

0
src/__init__.py Normal file
View File

245
src/file_tool.py Normal file
View File

@ -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")

74
tests/test_public.py Normal file
View File

@ -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()