Python文件操作与异常处理:从新手踩坑到生产级鲁棒代码 1. 项目概述为什么文件操作与异常处理是Python新手真正的分水岭你刚学完变量、循环、函数甚至能写个猜数字小游戏心里大概觉得“Python也不过如此”。但当你第一次尝试把程序运行结果保存下来或者从一个Excel里读点数据做分析却卡在FileNotFoundError: [Errno 2] No such file or directory: data.csv这行报错上反复检查路径——明明文件就在桌面上为什么Python就是找不到这时候你就站在了Python初学者的真正分水岭前语法会写不等于工程能跑能跑不等于鲁棒可用。这篇内容讲的不是“又一种语法”而是让你从“写玩具代码”跨入“写真实工具”的关键两块基石文件操作与异常处理。它面向的是所有已经能写if/else和for循环但还没在真实场景中读过一行日志、没处理过一次用户误输入、没保存过一份配置的新手。我带过上百个转行学员几乎所有人踩的第一个深坑都和这两件事有关——不是不会写open()而是不知道with语句为什么非用不可不是不懂try...except而是搞不清什么时候该捕获ValueError什么时候该让KeyError直接崩掉程序。这篇文章不讲概念定义只讲你明天就要用上的实操逻辑比如为什么f open(log.txt); f.write(start); f.close()这种写法在服务器上跑三天后必然丢数据比如为什么你写的爬虫脚本一遇到网络抖动就整个退出而不是跳过那一页继续抓再比如当用户在命令行里输错文件名你是该打印一长串红色traceback吓退对方还是优雅地提示“请检查文件路径是否正确并确认文件存在”。这些细节决定了你的代码是玩具还是别人愿意放进生产环境里的工具。2. 文件操作的核心设计逻辑为什么“打开-读写-关闭”这个三步曲必须被重构2.1 传统三步曲的致命缺陷资源泄漏与数据丢失的真实代价很多教程开头就教f open(data.txt, r)接着content f.read()最后f.close()。看起来干净利落但这是教科书式的理想模型。真实世界里问题永远出在“最后”之前。比如你在f.read()之后、f.close()之前程序因为内存不足崩溃了或者你在写日志时f.write(user login success)执行了但f.close()根本没来得及跑——这时候会发生什么答案是数据可能永久丢失。原因在于操作系统对文件的写入并非实时落盘。当你调用write()数据先写进内存里的缓冲区buffer等缓冲区满了或者你显式调用flush()或者你调用close()系统才把缓冲区内容真正写入磁盘。如果程序在close()前意外终止缓冲区里那几KB的日志就永远消失了。我曾经维护过一个电商后台的订单导出脚本它用的就是这种三步曲写法。上线两周后运营同事反馈“昨天导出的3000单怎么只有2997单记录在文件里”查日志发现每次导出到第2998单时服务器恰好触发了自动内存清理进程被killclose()没执行最后三单的写入全在缓冲区里蒸发了。这不是玄学是每个Python新手迟早要面对的硬伤。2.2with语句不是语法糖而是资源管理的工业级标准with语句解决的不是“写起来简不简洁”的问题而是“资源能否被100%可靠释放”的工程底线。它的底层机制叫上下文管理协议Context Management Protocol核心是两个魔法方法__enter__()和__exit__()。当你写with open(data.txt) as f:Python在进入with块时自动调用open()返回对象的__enter__()通常就是返回文件对象本身在离开with块时——无论你是正常执行完、遇到return还是抛出异常——都会无条件调用__exit__()。而__exit__()的职责就是确保close()被执行。这才是它不可替代的价值异常安全exception-safe。我们来对比一段真实代码# ❌ 危险写法一旦中间出错f.close()永不执行 f open(config.json, r) config json.load(f) # 如果这里抛出JSONDecodeError下面的close()就跳过了 f.close() # ✅ 安全写法即使json.load()崩溃__exit__仍会调用close() with open(config.json, r) as f: config json.load(f) # 这里崩了没关系f.close()已安排更关键的是with支持嵌套。比如你要读一个CSV解析后写入数据库同时还要记日志# ✅ 多资源安全嵌套Python 3.10 with ( open(input.csv) as f_in, open(output.log, a) as f_log, sqlite3.connect(db.sqlite) as conn ): # 所有资源都在这里被安全管理 data csv.reader(f_in) for row in data: try: conn.execute(INSERT INTO users VALUES (?, ?), row) f_log.write(fSuccess: {row}\n) except Exception as e: f_log.write(fFailed: {row} - {e}\n) # 退出with块时f_in.close(), f_log.close(), conn.close() 全部自动触发这不再是“推荐写法”而是你在写任何涉及文件、数据库连接、网络套接字的代码时必须刻进DNA的肌肉记忆。它背后是二十年软件工程沉淀下来的共识手动管理资源给未来埋雷。2.3 文件模式详解不只是r和w那些你忽略的细节决定成败open()的第二个参数mode远比r读、w写、a追加复杂。新手常犯的错误是以为w只是“覆盖写”却忽略了它对文件权限和编码的隐含影响。我们拆解几个高频陷阱wvsa的本质区别w模式在打开文件时会立即清空文件全部内容然后将文件指针定位到开头。而a模式则强制将文件指针定位到文件末尾无论你之前用seek()怎么移动过指针。这意味着with open(log.txt, w) as f: f.write(First line\n) with open(log.txt, w) as f: # 再次用w打开 f.write(Second line\n) # 结果log.txt只剩这一行第一行彻底消失x模式唯一能避免覆盖的“创建专属”开关当你需要确保文件绝对不存在才写入比如生成唯一配置文件w不够安全——它会静默覆盖。x模式则不同如果文件已存在open()直接抛出FileExistsError绝不妥协。这是防止误操作的最后防线。try: with open(backup_config.json, x) as f: json.dump(default_config, f) except FileExistsError: print(警告备份配置文件已存在未覆盖)二进制模式的必要性处理图片、PDF、Excel等非文本文件时必须用rb读二进制或wb写二进制。用文本模式r去读一张JPG会因编码解码失败而报UnicodeDecodeError用w去写会因换行符自动转换\n→\r\n而损坏文件头。二进制模式绕过所有文本处理原样搬运字节流。模式读写同开的双刃剑r允许你在同一个文件对象上既读又写但写入默认从当前位置开始且不会自动扩展文件。如果你读到文件末尾再write()新内容会覆盖原有末尾字节。安全做法是显式调用f.seek(0, 2)定位到文件末尾再写或者直接用a模式它保证写入总在末尾。提示永远显式指定encoding参数。open(file.txt, r)在Windows上默认用cp1252在Mac上用utf-8同一份代码在不同机器上可能行为迥异。强制写成open(file.txt, r, encodingutf-8)是专业性的基本体现。3. 异常处理的实战哲学不是“兜底”而是“精准外科手术”3.1 为什么except Exception:是新手最大误区看到报错就加try...except Exception:这是最典型的“防御性编程”幻觉。它看似让程序不崩溃实则掩盖了所有问题让bug在黑暗中繁殖。想象一个数据清洗脚本# ❌ 毒瘤写法用Exception兜住一切 try: user_data json.load(f) age int(user_data[age]) # 可能ValueError db.insert(age) # 可能sqlite3.IntegrityError send_email(user_data[email]) # 可能smtplib.SMTPRecipientsRefused except Exception: print(出错了跳过这条数据) # 你根本不知道错在哪这段代码的问题在于ValueError年龄字段不是数字、IntegrityError数据库主键冲突、SMTPRecipientsRefused邮箱格式错误——三种完全不同的业务含义却被同一段print糊弄过去。结果是你永远不知道是数据源脏了还是数据库约束变了还是邮件服务挂了。真正的异常处理是按错误类型分诊对症下药。3.2 精准捕获从OSError到ValueError的分级诊疗Python的异常类构成一棵继承树顶层是BaseException但我们日常打交道的绝大多数是它的子类Exception。关键是要理解各子类的语义边界OSError及其子类FileNotFoundError,PermissionError,IsADirectoryError系统级错误源于操作系统。它们告诉你“事情在物理层面无法完成”。比如FileNotFoundError意味着路径根本不存在PermissionError意味着你没有读取权限IsADirectoryError意味着你试图用open()打开一个文件夹。这类错误通常不应被静默吞掉而应转化为用户可理解的提示try: with open(user_input_path, r, encodingutf-8) as f: return f.read() except FileNotFoundError: print(f❌ 错误找不到文件 {user_input_path}。请检查路径是否正确。) return None except PermissionError: print(f❌ 错误无权访问 {user_input_path}。请检查文件权限。) return None except IsADirectoryError: print(f❌ 错误{user_input_path} 是一个文件夹不是文件。) return NoneValueError,TypeError,KeyError数据/逻辑错误源于你的代码或用户输入。ValueError表示值不合适如int(abc)KeyError表示字典里没有这个键TypeError表示类型不匹配如len(42)。这类错误往往可以通过预检查规避但捕获它们是为了提供友好反馈try: age int(input(请输入年龄: )) if not (0 age 150): raise ValueError(年龄必须在0-150之间) except ValueError as e: print(f❌ 输入错误{e}。请输入一个有效的数字。) # 注意这里捕获的是具体的ValueError不是宽泛的ExceptionConnectionError,TimeoutError外部依赖错误源于网络、数据库等外部服务。它们的特点是暂时性。一个HTTP请求超时重试一次可能就成功了数据库连接断开等待几秒再连可能就恢复了。这类错误最适合用指数退避重试exponential backoffimport time import random def fetch_data_with_retry(url, max_retries3): for i in range(max_retries): try: response requests.get(url, timeout5) response.raise_for_status() return response.json() except (ConnectionError, TimeoutError) as e: if i max_retries - 1: # 最后一次重试也失败 raise e wait_time (2 ** i) random.uniform(0, 1) # 1s, 3s, 7s... print(f⚠️ 网络错误{wait_time:.1f}秒后重试 ({i1}/{max_retries})...) time.sleep(wait_time)3.3else和finally被严重低估的异常处理黄金搭档try...except结构还有两个常被忽略的兄弟else和finally。它们的存在让异常处理逻辑变得无比清晰else子句仅在try块无异常时执行。这是放置“成功后逻辑”的完美位置避免把本不该被异常捕获的代码塞进try里。比如try: data load_config(config.yaml) # 可能FileNotFoundError, yaml.YAMLError except FileNotFoundError: data default_config except yaml.YAMLError as e: print(f配置文件格式错误: {e}) data default_config else: # ✅ 只有配置加载成功才执行校验 validate_config(data) # 这里抛出的异常不会被上面的except捕获 print(✅ 配置加载并校验成功)finally子句无论成败必定执行。这是清理资源的终极保险。虽然with语句已极大减少了对finally的需求但在某些场景它不可替代。比如你手动管理了一个需要close()的C库资源resource allocate_c_resource() # 假设这是一个C扩展函数 try: process(resource) except CriticalError: log_error(处理失败) raise # 重新抛出让上层知道 finally: # ✅ 无论如何资源必须释放 free_c_resource(resource) # 防止内存泄漏注意finally里的代码如果抛出异常会覆盖try块中原本的异常。所以finally里应尽量只做清理避免复杂逻辑。4. 文件与异常的协同作战构建一个鲁棒的CSV数据处理器4.1 需求拆解一个真实场景的完整链条假设你要写一个脚本从用户指定的CSV文件中读取销售数据包含product_id,quantity,price三列计算每行的total quantity * price然后将结果写入新的sales_report.csv。这个看似简单的任务暗藏至少6个故障点用户输入的CSV路径不存在FileNotFoundErrorCSV文件被其他程序占用无法读取PermissionErrorCSV文件编码不是UTF-8UnicodeDecodeError某行数据缺失quantity列KeyErrorquantity字段是字符串abc而非数字ValueError磁盘空间不足无法写入新文件OSError一个健壮的实现必须对每个点给出明确、差异化的响应。4.2 分步实现从骨架到血肉第一步安全读取CSV处理I/O异常import csv from pathlib import Path def safe_read_csv(filepath: str) - list[dict]: 安全读取CSV返回字典列表 path Path(filepath) # 预检查路径是否存在且为文件 if not path.exists(): raise FileNotFoundError(f文件不存在: {filepath}) if not path.is_file(): raise IsADirectoryError(f路径不是文件: {filepath}) # 尝试用UTF-8读取 try: with open(path, r, encodingutf-8) as f: reader csv.DictReader(f) return list(reader) except UnicodeDecodeError: # UTF-8失败尝试gbk常见于中文Windows try: with open(path, r, encodinggbk) as f: reader csv.DictReader(f) return list(reader) except UnicodeDecodeError as e: raise ValueError(f文件编码无法识别请确保是UTF-8或GBK格式: {e}) except PermissionError: raise PermissionError(f无权读取文件: {filepath}) # 测试调用 try: data safe_read_csv(sales_2023.csv) except (FileNotFoundError, PermissionError, ValueError) as e: print(f❌ 读取文件失败: {e}) exit(1)第二步逐行计算精准捕获数据异常def calculate_sales_total(row: dict) - float: 计算单行销售总额严格校验数据 try: # 检查必需字段 if quantity not in row: raise KeyError(缺少必需字段 quantity) if price not in row: raise KeyError(缺少必需字段 price) # 转换并校验数值 quantity float(row[quantity].strip()) price float(row[price].strip()) if quantity 0: raise ValueError(f数量不能为负数: {quantity}) if price 0: raise ValueError(f价格不能为负数: {price}) return quantity * price except ValueError as e: # 数值转换失败或业务规则失败 raise ValueError(f数据格式错误 - 行 {row.get(product_id, unknown)}: {e}) except KeyError as e: # 字段缺失 raise KeyError(f字段缺失 - 行 {row.get(product_id, unknown)}: {e}) # 对数据列表进行处理 processed_data [] for i, row in enumerate(data, 1): try: total calculate_sales_total(row) # 添加计算结果到原字典 row[total] f{total:.2f} processed_data.append(row) except (ValueError, KeyError) as e: print(f⚠️ 跳过第{i}行: {e}) # 友好提示不中断整个流程 continue # 继续处理下一行第三步安全写入报告处理输出异常def safe_write_report(data: list[dict], output_path: str): 安全写入销售报告CSV if not data: print(⚠️ 没有数据可写入跳过生成报告。) return # 确保输出目录存在 output_dir Path(output_path).parent output_dir.mkdir(parentsTrue, exist_okTrue) try: with open(output_path, w, newline, encodingutf-8) as f: # 使用第一行数据的键作为表头确保顺序 fieldnames list(data[0].keys()) writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data) print(f✅ 报告已成功生成: {output_path}) except PermissionError: raise PermissionError(f无权写入文件: {output_path}) except OSError as e: # 捕获磁盘满、路径过长等系统错误 raise OSError(f写入文件失败: {e}) # 执行写入 try: safe_write_report(processed_data, output/sales_report.csv) except (PermissionError, OSError) as e: print(f❌ 生成报告失败: {e}) exit(1)4.3 完整整合与用户体验优化把以上模块组合成一个完整的main()函数并加入命令行参数支持import argparse import sys def main(): parser argparse.ArgumentParser(description销售数据CSV处理器) parser.add_argument(input, help输入CSV文件路径) parser.add_argument(-o, --output, defaultoutput/sales_report.csv, help输出报告路径 (默认: output/sales_report.csv)) args parser.parse_args() print( 开始处理销售数据...) # 步骤1读取 try: data safe_read_csv(args.input) print(f✅ 成功读取 {len(data)} 行数据) except Exception as e: print(f❌ 读取失败: {e}) sys.exit(1) # 步骤2处理 processed_data [] errors 0 for i, row in enumerate(data, 1): try: total calculate_sales_total(row) row[total] f{total:.2f} processed_data.append(row) except Exception as e: print(f⚠️ 第{i}行处理失败: {e}) errors 1 if errors 0: print(f⚠️ 共 {errors} 行数据因错误被跳过) # 步骤3写入 try: safe_write_report(processed_data, args.output) except Exception as e: print(f❌ 写入失败: {e}) sys.exit(1) print( 处理完成) if __name__ __main__: main()现在你可以这样运行它# 正常情况 python sales_processor.py data/sales.csv # 指定输出路径 python sales_processor.py data/sales.csv -o reports/2023_q1.csv # 当输入文件有问题时你会得到清晰的错误定位 # ❌ 读取失败: 文件不存在: data/sales.csv # ⚠️ 第5行处理失败: 数据格式错误 - 行 P1001: 数量不能为负数: -5.0这个脚本的价值不在于它多炫酷而在于它把每一个可能的失败点都转化成了用户可理解、可操作的信息。这才是专业Python开发者的日常。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 “文件找不到”问题的终极排查清单FileNotFoundError是新手最高频的报错但90%的人只会在代码里打印一句“文件不存在”然后陷入无意义的路径检查。以下是我总结的系统化排查流程排查步骤操作方法为什么有效实例1. 确认当前工作目录在报错前加import os; print(当前目录:, os.getcwd())Python的相对路径是相对于当前工作目录不是脚本所在目录你双击运行script.py工作目录可能是桌面用IDE运行工作目录可能是项目根目录2. 检查路径拼写与大小写Path(filepath).resolve()获取绝对路径看是否符合预期Windows不区分大小写Linux/macOS严格区分路径中多一个空格就失败data\input.csv在Windows上可能被解释为data\\input.csv反斜杠转义3. 验证文件是否真的存在Path(filepath).exists()和Path(filepath).is_file()分开检查区分“路径存在但它是文件夹”和“路径根本不存在”两种情况Path(logs).exists()返回True但Path(logs).is_file()返回False4. 检查隐藏字符用编辑器显示所有字符如VS Code的CtrlShiftP→Toggle Render Whitespace从网页复制的路径可能包含不可见的零宽空格U200Bconfig.json\u200b看起来一样实际是不同字符串实操心得永远在打开文件前用Path(filepath).resolve()打印出绝对路径。这是我带新人的第一条铁律。看到绝对路径你才能真正判断问题出在路径构造还是文件本身。5.2 编码错误的“万能解法”与陷阱UnicodeDecodeError让人头疼因为错误信息只告诉你“无法解码”却不告诉你该用什么编码。网上流传的“试试gbk、试试utf-8-sig、试试latin-1”是懒人方案。专业做法是latin-1不是万能而是“保命”latin-1能解码任意字节序列0-255不会报错但它会把非ASCII字节变成乱码字符。它唯一的用途是读取文件内容然后用chardet库检测真实编码import chardet # 先用latin-1暴力读取前10000字节 with open(unknown.txt, rb) as f: # 注意是rb raw_data f.read(10000) # 检测编码 detected chardet.detect(raw_data) print(f检测到编码: {detected[encoding]} (置信度: {detected[confidence]:.2f})) # 用检测到的编码重新读取 with open(unknown.txt, r, encodingdetected[encoding]) as f: content f.read()utf-8-sig的真相它不是一种独立编码而是utf-8的变体能自动跳过BOM字节顺序标记。如果你的文件是Excel另存为CSV时生成的大概率带BOM用utf-8-sig就能读通。但如果你用notepad保存为UTF-8它默认不带BOM此时utf-8-sig反而会把开头三个字节EF BB BF当成内容读出来。5.3 异常处理中的“幽灵错误”被吞掉的原始异常这是一个极其隐蔽的坑你在except块里做了日志但忘了raise导致原始异常信息丢失。# ❌ 危险原始异常被吃掉 try: risky_operation() except ValueError as e: logger.error(f操作失败: {e}) # ❌ 这里没有raise也没有return程序继续往下走 # 后面的代码可能基于一个无效状态运行产生更诡异的错误 # ✅ 正确记录后重新抛出 try: risky_operation() except ValueError as e: logger.error(f操作失败: {e}) raise # 重新抛出让调用者决定如何处理 # ✅ 或者记录后返回默认值明确意图 try: result risky_operation() except ValueError as e: logger.warning(f使用默认值 - {e}) result DEFAULT_VALUE判断标准很简单如果except块里没有raise或return并且try块之后还有代码那你就是在制造“幽灵错误”。5.4 性能陷阱readlines()vsfor line in file新手常以为readlines()是读取文件的标准方式。但它有一个致命缺点把整个文件一次性加载进内存。一个1GB的日志文件readlines()会瞬间吃掉1GB内存还可能触发OOM内存溢出。# ❌ 危险大文件杀手 with open(huge_log.txt) as f: all_lines f.readlines() # 全部载入内存 for line in all_lines: process(line) # ✅ 安全内存友好的迭代器 with open(huge_log.txt) as f: for line in f: # f本身就是一个迭代器一次只读一行 process(line)for line in f的底层原理是Python的文件对象实现了__iter__()方法每次迭代只从操作系统读取一个缓冲区通常是8KB的数据处理完就丢弃内存占用恒定。这是处理大文件的唯一正确姿势。6. 实战进阶用上下文管理器封装自定义资源6.1 为什么你需要自己写上下文管理器with语句的强大不仅在于open()更在于它可以被任何遵循上下文管理协议的对象使用。当你需要管理数据库连接、网络连接、锁、临时文件等资源时手动写try...finally既啰嗦又易错。自定义上下文管理器是把“获取-使用-释放”这个模式封装成可复用、可测试的组件。6.2 两种实现方式类 vs 装饰器方式一用类实现最直观from contextlib import contextmanager class DatabaseConnection: def __init__(self, db_url): self.db_url db_url self.connection None def __enter__(self): print(f 正在连接数据库: {self.db_url}) # 这里是真实的连接逻辑 self.connection create_real_connection(self.db_url) return self.connection def __exit__(self, exc_type, exc_value, traceback): print( 正在关闭数据库连接) if self.connection: self.connection.close() # 如果exc_type不为None说明发生了异常 if exc_type is not None: print(f⚠️ 连接期间发生异常: {exc_type.__name__}: {exc_value}) # 返回False表示不抑制异常让异常继续向上抛 return False # 使用 with DatabaseConnection(sqlite:///app.db) as conn: conn.execute(INSERT INTO logs VALUES (startup))方式二用contextmanager装饰器更简洁from contextlib import contextmanager contextmanager def temporary_directory(suffix, prefixtmp): 创建并自动清理临时目录的上下文管理器 import tempfile import shutil # __enter__ 部分 temp_dir tempfile.mkdtemp(suffixsuffix, prefixprefix) print(f 创建临时目录: {temp_dir}) try: yield temp_dir # 将temp_dir传给with块 finally: # __exit__ 部分 print(f️ 清理临时目录: {temp_dir}) shutil.rmtree(temp_dir) # 使用 with temporary_directory(prefixsales_) as tmp: # 在tmp目录下进行所有操作 with open(f{tmp}/data.csv, w) as f: f.write(id,name\n1,apple) # 退出with块时tmp目录自动被删除6.3 真实案例一个防崩溃的配置加载器结合文件操作和异常处理我们封装一个生产级的配置加载器import json from pathlib import Path from contextlib import contextmanager contextmanager def safe_config_loader(config_path: str, default_config: dict None): 安全配置加载器上下文管理器 - 成功yield 解析后的配置字典 - 失败yield 默认配置并记录警告 config_path Path(config_path) config default_config or {} if not config_path.exists(): print(f⚠️ 配置文件不存在: {config_path}使用默认配置) yield config return try: with open(config_path, r, encodingutf-8) as f: loaded json.load(f) # 深度合并用loaded覆盖default_config的同名键 merged {**config, **loaded} print(f✅ 成功加载配置: {config_path}) yield merged except json.JSONDecodeError as e: print(f⚠️ 配置文件JSON格式错误: {config_path} - {e}) yield config except UnicodeDecodeError as e: print(f⚠️ 配置文件编码错误: {config_path} - {e}) yield config except Exception as e: print(f⚠️ 加载配置时发生未知错误: {config_path} - {e}) yield config # 使用示例 DEFAULT_CONFIG {debug: False, timeout: 30} with safe_config_loader(config.json, DEFAULT_CONFIG) as config: DEBUG config[debug] TIMEOUT config[timeout] # 后续代码直接使用DEBUG和TIMEOUT无需担心config为空这个管理器的价值在于它把“配置加载”这个可能失败的操作变成了一个确定性的、有默认兜底的资源获取过程。调用方代码变得无比干净所有错误处理逻辑都被封装在管理器内部。7. 个人经验总结从踩坑到建立直觉的三年心路我第一次在生产环境里栽跟头是因为一个日志写入脚本用了f open(); f.write(); f.close()。那天下班前提交了代码第二天早上运维报警过去24小时有37%的日志行丢失。我花了整整一个上午才在close()被跳过的边缘case里找到原因。那天起我强迫自己任何涉及open()的地方第一反应不是写open()而是写with open()。这已经成了我的条件反射。后来带团队我发现新手最大的认知偏差是把异常处理当成“让程序不崩溃”的补丁。其实恰恰相反好的异常处理是让程序在崩溃前尽可能多地告诉你“它为什么想崩溃”。except Exception:就像给病人吃止痛片却不做检查except ValueError as e