Initial commit
This commit is contained in:
commit
6d9b6fd1da
27
README.md
Normal file
27
README.md
Normal 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.md:5 分
|
||||
|
||||
42
REPORT.md
Normal file
42
REPORT.md
Normal 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
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
pytest>=7.0.0
|
||||
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
245
src/file_tool.py
Normal file
245
src/file_tool.py
Normal 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
74
tests/test_public.py
Normal 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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user