feat(python进阶): 新增文件与目录操作课程
This commit is contained in:
4
02_python进阶/2_2_文件与目录操作/.gitignore
vendored
Normal file
4
02_python进阶/2_2_文件与目录操作/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# 本课示例和练习运行时生成的数据。
|
||||
# 这些内容可以重新生成,不需要纳入 Git 版本管理。
|
||||
runtime_data/
|
||||
practice_data/
|
||||
405
02_python进阶/2_2_文件与目录操作/README.md
Normal file
405
02_python进阶/2_2_文件与目录操作/README.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# 第 2-2 课:文件与目录操作
|
||||
|
||||
## 一、本课目标
|
||||
|
||||
完成本课后,你将能够:
|
||||
|
||||
1. 解释文件、目录和路径分别是什么;
|
||||
2. 使用 `Path` 表示和组合路径;
|
||||
3. 创建练习目录;
|
||||
4. 安全地写入、追加和读取文本文件;
|
||||
5. 检查路径是否存在,以及它是不是文件;
|
||||
6. 遍历目录中的文件;
|
||||
7. 说明文件操作可能带来的副作用。
|
||||
|
||||
## 二、前置知识
|
||||
|
||||
学习本课前,需要掌握:
|
||||
|
||||
- 字符串、变量和布尔值;
|
||||
- 条件判断与循环;
|
||||
- 函数、参数和返回值;
|
||||
- 模块、导入和程序入口判断。
|
||||
|
||||
上一课学习了模块。本课将从 Python 标准库中导入 `pathlib` 模块提供的 `Path`。
|
||||
|
||||
## 三、文件、目录和路径是什么
|
||||
|
||||
文件(File)是保存在计算机中的一份数据,例如:
|
||||
|
||||
- Python 程序 `practice.py`;
|
||||
- 文本文件 `learning_notes.txt`;
|
||||
- 图片文件 `photo.png`。
|
||||
|
||||
目录(Directory)也常被称为文件夹,用于组织文件和其他目录。
|
||||
|
||||
路径(Path)用于描述文件或目录在计算机中的位置。例如:
|
||||
|
||||
```text
|
||||
02_python进阶/2_2_文件与目录操作/runtime_data/learning_notes.txt
|
||||
```
|
||||
|
||||
可以把路径理解为从一个位置找到目标文件所经过的路线。
|
||||
|
||||
## 四、为什么使用 `Path`
|
||||
|
||||
不同操作系统表示路径的方式可能不同。Windows 常见反斜杠 `\`,其他系统常见正斜杠 `/`。手工拼接路径容易写错。
|
||||
|
||||
`pathlib` 是 Python 标准库中的路径处理模块。`Path` 是它提供的路径对象:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
lesson_dir = Path(__file__).parent
|
||||
data_dir = lesson_dir / "runtime_data"
|
||||
```
|
||||
|
||||
对象(Object)可以暂时理解为“同时保存数据和操作能力的值”。面向对象课程会详细讲解。
|
||||
|
||||
这里:
|
||||
|
||||
- `__file__` 是 Python 自动提供的特殊变量,表示当前文件的位置;
|
||||
- `Path(__file__)` 把位置转换成路径对象;
|
||||
- `.parent` 得到当前文件所在的目录;
|
||||
- `/ "runtime_data"` 在原路径后面组合新的目录名。
|
||||
|
||||
这里的 `/` 不是数学除法。`Path` 对象把它设计成了清晰的路径组合写法。
|
||||
|
||||
## 五、相对路径与绝对路径
|
||||
|
||||
相对路径(Relative Path)表示相对于当前工作位置的路线:
|
||||
|
||||
```text
|
||||
runtime_data/learning_notes.txt
|
||||
```
|
||||
|
||||
绝对路径(Absolute Path)从磁盘或系统根位置开始,完整描述目标位置。在 Windows 中可能类似:
|
||||
|
||||
```text
|
||||
F:\PyCharm\PythonLearn\02_python进阶\2_2_文件与目录操作
|
||||
```
|
||||
|
||||
本课通过 `Path(__file__).parent` 取得课程目录。这样无论从项目根目录还是课程目录启动示例,数据都会写入本课自己的 `runtime_data`。
|
||||
|
||||
## 六、创建目录
|
||||
|
||||
```python
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
```
|
||||
|
||||
`mkdir()` 表示创建目录:
|
||||
|
||||
- `parents=True`:需要时一并创建缺少的上级目录;
|
||||
- `exist_ok=True`:目录已经存在时不报错。
|
||||
|
||||
如果没有 `exist_ok=True`,第二次运行程序时可能出现:
|
||||
|
||||
```text
|
||||
FileExistsError
|
||||
```
|
||||
|
||||
中文解释:程序想创建的文件或目录已经存在。
|
||||
|
||||
## 七、写入文本文件
|
||||
|
||||
```python
|
||||
notes_file.write_text(
|
||||
"第一条:学习使用 Path 表示路径。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
```
|
||||
|
||||
`write_text()` 会创建文件并写入文本。如果文件已经存在,它会覆盖原有内容。
|
||||
|
||||
覆盖意味着旧内容会被替换,可能难以恢复。因此:
|
||||
|
||||
- 练习时只操作专用数据目录;
|
||||
- 写入前确认路径是否正确;
|
||||
- 不要把重要文件路径交给练习程序;
|
||||
- 中文文本明确使用 `encoding="utf-8"`。
|
||||
|
||||
编码(Encoding)是文字与计算机数据之间的转换规则。UTF-8 是常用的文字编码,可以保存中文和多种语言。
|
||||
|
||||
字符串中的 `\n` 表示换行。
|
||||
|
||||
## 八、追加文本文件
|
||||
|
||||
覆盖写入会清空旧内容。希望在文件末尾增加内容时,可以使用追加模式:
|
||||
|
||||
```python
|
||||
with notes_file.open("a", encoding="utf-8") as file:
|
||||
file.write("第二条:学习读取和追加文件。\n")
|
||||
```
|
||||
|
||||
打开模式(File Mode)`"a"` 中的字母来自 append,表示追加。
|
||||
|
||||
`with` 语句会在代码块结束后帮助关闭文件。即使以后文件操作发生错误,也更容易正确释放文件资源。
|
||||
|
||||
本课先记住固定结构:
|
||||
|
||||
```python
|
||||
with path.open("a", encoding="utf-8") as file:
|
||||
file.write("需要追加的内容")
|
||||
```
|
||||
|
||||
## 九、读取文本文件
|
||||
|
||||
```python
|
||||
content = notes_file.read_text(encoding="utf-8")
|
||||
print(content)
|
||||
```
|
||||
|
||||
`read_text()` 会读取整个文本文件,并返回一个字符串。
|
||||
|
||||
如果文件不存在,可能出现:
|
||||
|
||||
```text
|
||||
FileNotFoundError
|
||||
```
|
||||
|
||||
中文解释:Python 在指定路径没有找到需要读取的文件。
|
||||
|
||||
可以先检查文件是否存在:
|
||||
|
||||
```python
|
||||
if notes_file.exists():
|
||||
content = notes_file.read_text(encoding="utf-8")
|
||||
```
|
||||
|
||||
## 十、检查路径
|
||||
|
||||
常用检查方法:
|
||||
|
||||
```python
|
||||
path.exists()
|
||||
path.is_file()
|
||||
path.is_dir()
|
||||
```
|
||||
|
||||
- `exists()`:路径是否存在;
|
||||
- `is_file()`:路径是否存在并且是文件;
|
||||
- `is_dir()`:路径是否存在并且是目录。
|
||||
|
||||
这些方法返回布尔值 `True` 或 `False`,可以与条件判断配合使用。
|
||||
|
||||
## 十一、遍历目录
|
||||
|
||||
```python
|
||||
for path in data_dir.iterdir():
|
||||
if path.is_file():
|
||||
print(path.name)
|
||||
```
|
||||
|
||||
`iterdir()` 会提供目录中的每一项。每一项仍然是 `Path` 对象:
|
||||
|
||||
- `path.name` 表示文件或目录的名称;
|
||||
- `path.is_file()` 判断它是否是文件。
|
||||
|
||||
本课只遍历当前目录,不递归进入更深层目录。递归表示一个操作继续处理内部的子目录,属于后续扩展内容。
|
||||
|
||||
## 十二、示例文件与数据影响
|
||||
|
||||
本课文件结构:
|
||||
|
||||
```text
|
||||
02_python进阶/2_2_文件与目录操作/
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── file_directory_example.py
|
||||
└── practice.py
|
||||
```
|
||||
|
||||
运行示例后会产生:
|
||||
|
||||
```text
|
||||
runtime_data/
|
||||
└── learning_notes.txt
|
||||
```
|
||||
|
||||
运行练习后应产生:
|
||||
|
||||
```text
|
||||
practice_data/
|
||||
└── diary.txt
|
||||
```
|
||||
|
||||
这两个数据目录都被本课 `.gitignore` 忽略,不会加入 Git。程序不会操作课程目录之外的文件。
|
||||
|
||||
每次运行完整示例时,`learning_notes.txt` 会先被写入初始内容,再追加第二条记录,因此结果不会无限重复增加。
|
||||
|
||||
## 十三、运行方法
|
||||
|
||||
在 PowerShell 中进入项目根目录:
|
||||
|
||||
```powershell
|
||||
cd F:\PyCharm\PythonLearn
|
||||
```
|
||||
|
||||
运行完整示例:
|
||||
|
||||
```powershell
|
||||
python .\02_python进阶\2_2_文件与目录操作\file_directory_example.py
|
||||
```
|
||||
|
||||
完成练习后运行:
|
||||
|
||||
```powershell
|
||||
python .\02_python进阶\2_2_文件与目录操作\practice.py
|
||||
```
|
||||
|
||||
## 十四、运行结果
|
||||
|
||||
完整示例的预期结果类似:
|
||||
|
||||
```text
|
||||
数据目录已准备:runtime_data
|
||||
初始内容已写入:learning_notes.txt
|
||||
新的学习记录已追加。
|
||||
学习记录内容:
|
||||
第一条:学习使用 Path 表示路径。
|
||||
第二条:学习读取和追加文件。
|
||||
数据目录中的文件:
|
||||
- learning_notes.txt
|
||||
```
|
||||
|
||||
然后可以打开 `runtime_data/learning_notes.txt`,确认文件内容与终端输出一致。
|
||||
|
||||
## 十五、关键代码执行顺序
|
||||
|
||||
运行 `file_directory_example.py` 时:
|
||||
|
||||
1. 导入 `Path`;
|
||||
2. 根据当前文件位置准备课程目录、数据目录和文件路径;
|
||||
3. 定义文件操作函数;
|
||||
4. 入口判断成立,调用 `main()`;
|
||||
5. 创建 `runtime_data`;
|
||||
6. 覆盖写入初始记录;
|
||||
7. 在文件末尾追加一条记录;
|
||||
8. 读取并输出完整内容;
|
||||
9. 遍历数据目录并输出文件名。
|
||||
|
||||
先创建目录再写文件非常重要。目录不存在时,直接写入内部文件会失败。
|
||||
|
||||
## 十六、常见错误
|
||||
|
||||
### 16.1 手工拼接路径
|
||||
|
||||
不推荐:
|
||||
|
||||
```python
|
||||
file_path = lesson_dir + "/runtime_data/notes.txt"
|
||||
```
|
||||
|
||||
`lesson_dir` 是 `Path` 对象,不能直接与普通字符串这样相加。使用:
|
||||
|
||||
```python
|
||||
file_path = lesson_dir / "runtime_data" / "notes.txt"
|
||||
```
|
||||
|
||||
### 16.2 忘记创建目录
|
||||
|
||||
在 `runtime_data` 不存在时直接写入内部文件,会出现 `FileNotFoundError`。应先调用 `mkdir()`。
|
||||
|
||||
### 16.3 忘记指定中文编码
|
||||
|
||||
读取或写入中文时未指定 UTF-8,可能出现乱码或编码错误。请在读写两端都使用 `encoding="utf-8"`。
|
||||
|
||||
### 16.4 混淆覆盖和追加
|
||||
|
||||
- `write_text()` 会覆盖原内容;
|
||||
- `open("a")` 会在末尾追加内容。
|
||||
|
||||
操作重要文件前必须确认自己需要哪一种行为。
|
||||
|
||||
### 16.5 忘记关闭文件
|
||||
|
||||
手动调用 `open()` 后如果忘记关闭文件,可能造成资源占用。本课使用 `with` 自动管理文件。
|
||||
|
||||
### 16.6 路径指向项目外部
|
||||
|
||||
练习时不要使用系统目录、桌面文件或其他项目文件。把所有练习数据限制在本课的 `practice_data` 中。
|
||||
|
||||
## 十七、课堂练习
|
||||
|
||||
打开 `practice.py`,依次完成:
|
||||
|
||||
1. 准备练习目录和日记文件路径;
|
||||
2. 创建 `practice_data`;
|
||||
3. 写入第一条日记;
|
||||
4. 追加第二条日记;
|
||||
5. 读取并输出完整内容;
|
||||
6. 检查文件是否存在并遍历目录。
|
||||
|
||||
每完成一部分就运行一次。不要直接复制参考答案。
|
||||
|
||||
## 十八、参考答案
|
||||
|
||||
请先独立完成练习,再展开查看。
|
||||
|
||||
<details>
|
||||
<summary>查看参考答案</summary>
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
lesson_dir = Path(__file__).parent
|
||||
practice_dir = lesson_dir / "practice_data"
|
||||
diary_file = practice_dir / "diary.txt"
|
||||
|
||||
practice_dir.mkdir(parents=True, exist_ok=True)
|
||||
print("练习目录已准备。")
|
||||
|
||||
first_entry = "今天学习了文件与目录。\n"
|
||||
diary_file.write_text(first_entry, encoding="utf-8")
|
||||
print("日记已写入。")
|
||||
|
||||
with diary_file.open("a", encoding="utf-8") as file:
|
||||
file.write("我会使用 Path 处理路径。\n")
|
||||
|
||||
print("日记已追加。")
|
||||
|
||||
content = diary_file.read_text(encoding="utf-8")
|
||||
print("日记内容:")
|
||||
print(content, end="")
|
||||
|
||||
if diary_file.exists():
|
||||
print("日记文件存在。")
|
||||
|
||||
print("练习目录中的文件:")
|
||||
|
||||
for path in practice_dir.iterdir():
|
||||
if path.is_file():
|
||||
print(path.name)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 十九、本课小结
|
||||
|
||||
- 文件保存数据,目录用于组织文件,路径描述它们的位置;
|
||||
- `Path` 可以清晰地表示和组合路径;
|
||||
- `mkdir()` 可以创建目录;
|
||||
- `write_text()` 可以写入文本,但会覆盖原内容;
|
||||
- `open("a")` 可以追加内容;
|
||||
- `read_text()` 可以读取整个文本文件;
|
||||
- `with` 可以帮助正确关闭文件;
|
||||
- `exists()`、`is_file()` 和 `is_dir()` 可以检查路径;
|
||||
- `iterdir()` 可以遍历目录;
|
||||
- 文件操作存在副作用,必须限制操作范围并确认目标路径。
|
||||
|
||||
## 二十、验收标准
|
||||
|
||||
完成本课时,应满足:
|
||||
|
||||
- 可以解释文件、目录、相对路径和绝对路径;
|
||||
- 可以使用 `Path(__file__).parent` 定位当前课程目录;
|
||||
- 可以使用 `/` 组合 `Path` 路径;
|
||||
- 可以创建已经存在也不会报错的目录;
|
||||
- 可以正确写入、追加和读取中文文本;
|
||||
- 可以解释覆盖写入与追加写入的区别;
|
||||
- 可以使用 `with` 打开文件;
|
||||
- 可以检查路径并遍历目录中的文件;
|
||||
- 练习数据只出现在 `practice_data` 中;
|
||||
- 运行两次练习,程序都不会崩溃;
|
||||
- 最终日记内容不会因为重复运行而无限增加。
|
||||
71
02_python进阶/2_2_文件与目录操作/file_directory_example.py
Normal file
71
02_python进阶/2_2_文件与目录操作/file_directory_example.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# 第 2-2 课完整示例:文件与目录操作
|
||||
#
|
||||
# 本程序只会在当前课程目录的 runtime_data 文件夹中创建和修改文件。
|
||||
# runtime_data 已被本课的 .gitignore 忽略,不会污染 Git 变更记录。
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# __file__ 表示当前 Python 文件的位置。
|
||||
# parent 表示当前文件所在的目录。
|
||||
LESSON_DIR = Path(__file__).parent
|
||||
DATA_DIR = LESSON_DIR / "runtime_data"
|
||||
NOTES_FILE = DATA_DIR / "learning_notes.txt"
|
||||
|
||||
|
||||
def create_data_directory():
|
||||
"""创建保存示例数据的目录。"""
|
||||
# parents=True 表示需要时一并创建上级目录。
|
||||
# exist_ok=True 表示目录已经存在时不报错。
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
print(f"数据目录已准备:{DATA_DIR.name}")
|
||||
|
||||
|
||||
def write_initial_notes():
|
||||
"""写入初始学习记录。"""
|
||||
notes = "第一条:学习使用 Path 表示路径。\n"
|
||||
|
||||
# write_text() 会覆盖文件原有内容。
|
||||
# encoding="utf-8" 可以正确保存中文。
|
||||
NOTES_FILE.write_text(notes, encoding="utf-8")
|
||||
print(f"初始内容已写入:{NOTES_FILE.name}")
|
||||
|
||||
|
||||
def append_note():
|
||||
"""在文件末尾追加一条学习记录。"""
|
||||
# open() 打开文件后,需要在操作结束时关闭文件。
|
||||
# with 语句会帮助我们自动关闭文件。
|
||||
# 模式 "a" 表示追加,不会清空原有内容。
|
||||
with NOTES_FILE.open("a", encoding="utf-8") as file:
|
||||
file.write("第二条:学习读取和追加文件。\n")
|
||||
|
||||
print("新的学习记录已追加。")
|
||||
|
||||
|
||||
def read_notes():
|
||||
"""读取并输出全部学习记录。"""
|
||||
content = NOTES_FILE.read_text(encoding="utf-8")
|
||||
print("学习记录内容:")
|
||||
print(content, end="")
|
||||
|
||||
|
||||
def show_directory_contents():
|
||||
"""遍历并显示数据目录中的内容。"""
|
||||
print("数据目录中的文件:")
|
||||
|
||||
for path in DATA_DIR.iterdir():
|
||||
if path.is_file():
|
||||
print(f"- {path.name}")
|
||||
|
||||
|
||||
def main():
|
||||
"""按照安全顺序运行文件与目录示例。"""
|
||||
create_data_directory()
|
||||
write_initial_notes()
|
||||
append_note()
|
||||
read_notes()
|
||||
show_directory_contents()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
45
02_python进阶/2_2_文件与目录操作/practice.py
Normal file
45
02_python进阶/2_2_文件与目录操作/practice.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# 第 2-2 课课堂练习:文件与目录操作
|
||||
#
|
||||
# 请先阅读讲义并运行完整示例,再按照顺序完成练习。
|
||||
# 所有练习数据只能保存在当前课程目录的 practice_data 文件夹中。
|
||||
# 不要使用本课尚未要求的删除操作,也不要修改项目中的其他文件。
|
||||
|
||||
|
||||
# 第一部分:准备路径
|
||||
# 1. 从 pathlib 导入 Path。
|
||||
# 2. 使用 Path(__file__).parent 得到当前课程目录。
|
||||
# 3. 准备 practice_data 目录路径。
|
||||
# 4. 准备 practice_data/diary.txt 文件路径。
|
||||
|
||||
# 第二部分:创建目录
|
||||
# 1. 使用 mkdir(parents=True, exist_ok=True) 创建 practice_data。
|
||||
# 2. 输出“练习目录已准备。”。
|
||||
# 3. 连续运行两次,确认第二次运行不会报错。
|
||||
# 第三部分:写入文件
|
||||
# 1. 准备字符串“今天学习了文件与目录。\n”。
|
||||
# 2. 使用 write_text() 写入 diary.txt。
|
||||
# 3. 明确指定 encoding="utf-8"。
|
||||
# 4. 输出“日记已写入。”。
|
||||
# 第四部分:追加内容
|
||||
# 1. 使用 with 和 open("a", encoding="utf-8") 打开 diary.txt。
|
||||
# 2. 追加“我会使用 Path 处理路径。\n”。
|
||||
# 3. 输出“日记已追加。”。
|
||||
# 第五部分:读取内容
|
||||
# 1. 使用 read_text(encoding="utf-8") 读取 diary.txt。
|
||||
# 2. 输出标题“日记内容:”。
|
||||
# 3. 输出读取到的全部内容。
|
||||
|
||||
# 第六部分:检查和遍历
|
||||
# 1. 使用 exists() 检查 diary.txt 是否存在。
|
||||
# 2. 存在时输出“日记文件存在。”。
|
||||
# 3. 使用 iterdir() 遍历 practice_data。
|
||||
# 4. 只输出文件,并通过 name 属性显示文件名。
|
||||
|
||||
# 完成后自查:
|
||||
# 1. 是否始终使用 Path 组合路径,而不是手工拼接斜杠;
|
||||
# 2. 是否把练习数据限制在 practice_data 中;
|
||||
# 3. 写入和读取中文时是否指定 utf-8;
|
||||
# 4. 是否知道 write_text() 会覆盖原内容;
|
||||
# 5. 是否知道追加模式 "a" 不会清空原内容;
|
||||
# 6. 是否能解释 with 为什么可以帮助关闭文件;
|
||||
# 7. 是否能使用 exists()、is_file() 和 iterdir()。
|
||||
26
README.md
26
README.md
@@ -189,12 +189,12 @@ Python/
|
||||
## 当前学习进度
|
||||
|
||||
- 当前阶段:第二阶段——Python 进阶。
|
||||
- 当前课程:`2_1_模块与包`。
|
||||
- 当前状态:正在学习模块与包。
|
||||
- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`。
|
||||
- 学习中的课程:`2_1_模块与包`。
|
||||
- 已创建课程目录:第一阶段全部课程,以及 `02_python进阶/2_1_模块与包/`。
|
||||
- 下一步:完成模块与包练习,然后学习文件和目录操作。
|
||||
- 当前课程:`2_2_文件与目录操作`。
|
||||
- 当前状态:正在学习文件与目录操作。
|
||||
- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`,以及 `2_1_模块与包`。
|
||||
- 学习中的课程:`2_2_文件与目录操作`。
|
||||
- 已创建课程目录:第一阶段全部课程,以及 `02_python进阶/2_1_模块与包/`、`02_python进阶/2_2_文件与目录操作/`。
|
||||
- 下一步:完成文件与目录操作练习,然后学习异常处理。
|
||||
|
||||
## 建议环境
|
||||
|
||||
@@ -209,12 +209,12 @@ Python/
|
||||
|
||||
## 下一步
|
||||
|
||||
当前正在学习模块与包。本课将学习:
|
||||
上一课“模块与包”已经完成。当前正在学习文件与目录操作,本课将学习:
|
||||
|
||||
1. 模块是什么,以及为什么要拆分文件;
|
||||
2. 使用 `import` 导入模块;
|
||||
3. 使用 `from...import...` 导入指定内容;
|
||||
4. 理解 `__name__ == "__main__"`;
|
||||
5. 创建和使用自己的模块与包。
|
||||
1. 文件、目录和路径的基本含义;
|
||||
2. 使用 `Path` 表示和组合路径;
|
||||
3. 创建练习目录;
|
||||
4. 写入、追加和读取文本文件;
|
||||
5. 检查路径并遍历目录内容。
|
||||
|
||||
完成本课练习并确认继续后,下一课将学习文件和目录操作。
|
||||
完成本课练习并确认继续后,下一课将学习异常处理。
|
||||
|
||||
Reference in New Issue
Block a user