第七章主要是讲异常处理,文章结尾的练习是希望大家写一个文件读取或者完善第六章简易图书管理系统的练习。
我们这里给出一个读取txt文件的示例代码。
虽然在第七章中我们已经举了读取文件的例子,但那个还比较粗糙,而且只是一个基本的架子,这里正好细化一下。
我的想法是这样的,使用面向对象编程的思想实现一个逐行读取txt文件的类,最好能够读取大文件也没有压力,在这个过程中,我们可以让程序自动检测文件的编码格式,其中的异常可以用日志来记录。
我打算用uv工具来管理我们的项目,如果你机器上还没有下载这个工具,可以参照我们之前的文章(第五章:模块与包)来安装和了解简单的用法。
用VSCode打开一个工作目录,我们在控制台运行uv init ch7-example命令来创建一个项目,然后在控制台中切换到ch7-example这个路径下,执行uv venv创建虚拟环境,这时的项目目录结构是这样的:
ch7-example ├── .venv ├── pyproject.toml ├── README.md ├── main.py ├── .python-version └── .gitignore我们在这个项目的根目录下创建一个txt_reader.py的文件,与main.py同级目录。
为了帮助我们实现自动检测文件编码格式的功能,我打算使用chardet库(这是一个比较推荐的库),那就需要将这个库加入到我们的项目中,在控制台中将目录结构切换到ch7-example下,使用uv add chardet命令添加库。
现在我们就可以开始写代码了,先在txt_reader.py这个文件中配置一下日志,代码如下:
import loggingLOG_PATH = "txt_reader.log"logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler(LOG_PATH, encoding='utf-8') ])logger = logging.getLogger(__name__)我们在日志中定义了日志输出的文件,txt_reader.log,产生的日志都会输出到这个文件中,使用的是相对路径,所以这个文件会在项目的根目录下。另外使用format来配置日志输出的格式,这些内容是固定的:
%(asctime)s表示的是时间戳%(levelname)s表示的是日志的级别(INFO/WARNING/ERROR等)%(module)s表示模块名%(lineno)d表示代码行号,便于我们快速定位代码位置%(message)s表示日志的内容他们的格式就是这么写,我所说的固定是asctime这个词就是表示时间,你不能用其他的字母或单词代替,但他们的顺序、美化的形式可以自己增加,比如用方括号包裹。
然后定义一个TxtReader类,用于实现编码格式检测和逐行读取两个功能,代码如下:
from typing import Iterator, Optionalimport chardetclass TxtReader: """ 读取txt文件 """ def __init__(self, file_path: str) -> None: self.file_path = file_path self.file_encoding: Optional[str] = None def _detect_encoding(self) -> None: """ 检测文件编码 :param self: self """ try: with open(self.file_path, "rb") as f: raw_data = f.read(4096) dectect_res = chardet.detect(raw_data) self.file_encoding = dectect_res.get("encoding") confidence = dectect_res.get("confidence", 0.0) if confidence < 0.7: logger.warning("文件%s编码检测置信度较低,置信度为:(%.2f)", self.file_path, confidence) except Exception as e: self.file_encoding = 'utf-8' logger.error("文件编码检测失败,使用默认编码utf-8,原因:%s,文件路径:%s", str(e), self.file_path) def read_lines(self) -> Iterator[str]: """ 逐行读取txt文件 :param self: self :return: 列表迭代器 :rtype: Iterator[str] """ try: with open(self.file_path, 'r', encoding=self.file_encoding, errors="replace") as f: for _, line in enumerate(f, 1): yield line.rstrip('\n\r') # 删除行尾换行符 except FileNotFoundError: logger.error("文件不存在,路径为:%s", self.file_path) raise except PermissionError: logger.error("文件无访问权限,路径:%s", self.file_path) raise except Exception as e: logger.error("文件读取异常,原因:%s,路径:%s", str(e), self.file_path) raise def __iter__(self) -> Iterator[str]: return self.read_lines()_detect_encoding方法是一个保护方法,我们不希望对外暴露,所以以下划线开头,这个方法主要实现编码检测,我们使用了chardet来辅助处理,内部还使用了Exception异常,在第七章的最佳实践中说捕获异常要具体,这里使用了比较通用的Exception,是因为这里只做编码检测,不具体处理异常,所以用了最广的范围,在read_lines方法中我们也是用了Exception,这是为了兜底。
最佳实践依然是不变的,异常如果是具体和清晰的,我们还是要捕获具体的异常而非宽泛的,但也要注意灵活应用。
在read_lines方法中我们使用生成器来实现逐行读取,这样做的好处在于即便读取大文件占用的内存也不会很高。
errors="replace"参数是为了保证文件能完整读取。
我们实现了__iter__魔术方法,是为了让TxtReader对象直接可遍历。这样我们在写测试方法的时候就可以这样用:
if __name__ == "__main__": try: reader = TxtReader('.gitignore') for line in reader: # 直接迭代 print(line) except Exception as e: logger.error("文件读取异常:%s", str(e))好了,到这里练习示例就结束了。你在练习时遇到了哪些问题?我们可以一起来讨论。