文件与异常:让程序"摔倒了也能爬起来"
文件与异常:让程序"摔倒了也能爬起来"
一、问题背景:跑了5分钟的脚本,第300行崩了
上个月遇到一个经典状况。
写了一个批量处理脚本,分析50个Lot的数据。跑了5分钟,到第43个Lot时崩了:
```
ZeroDivisionError: division by zero
```
原因:第43个Lot的数据文件是空的,`len(data) == 0`,然后代码做了 `sum(data) / len(data)` —— 除0了。
更崩溃的是:前42个Lot的结果也丢了,因为程序一崩啥都没保存。
这个问题的本质是:你把所有鸡蛋放在一个篮子里,篮子一摔全碎了。
学完这一篇,你能做到:
1. 文件不存在/空文件/格式不对——程序都能"优雅处理",不崩
2. 用 `try-except` 让程序跳过有问题的数据继续跑
3. 哪怕其中一段崩了,前面的结果也保存得好好的
---
二、技术原理:try-except = 安全气囊
2.1 最常见的两种报错
```python
1. 文件不存在
open("不存在的文件.csv") # FileNotFoundError
2. 除数为0
1 / 0 # ZeroDivisionError
```
报错后程序直接停止。后面的所有代码都不执行了。
2.2 try-except 解决
```python
安全气囊版
try:
file = open("data.csv")
content = file.read()
file.close()
print("文件读取成功")
except FileNotFoundError:
print("文件不存在,跳过")
print("程序继续运行!") # 即使上面出错了,这里照样执行
```
为什么这样写? `try` 块里的代码就像开车,`except` 就是安全气囊——出事了气囊弹出来,人没事(程序继续跑)。
2.3 不用try-except的隐患
```python
=== 脆弱版 ===
data = open("data.csv").readlines() # 文件不存在?直接崩
avg = sum(float(x) for x in data) / len(data) # 空文件?也崩
=== 健壮版 ===
try:
with open("data.csv", 'r') as f:
data = f.readlines()
except FileNotFoundError:
print("data.csv 不存在,使用默认数据")
data = ["1250", "1248", "1251"]
try:
nums = [float(x.strip()) for x in data]
avg = sum(nums) / len(nums)
except ZeroDivisionError:
print("数据为空,平均值为0")
avg = 0
except ValueError:
print("数据格式不对,跳过")
avg = 0
print(f"平均值为: {avg}")
```
---
三、实战案例:安全的Lot数据分析
3.1 读取CSV文件
假设每天从MES导出一个CSV文件,格式如下:
```
lot_id,thickness_1,thickness_2,thickness_3,thickness_4,thickness_5
FAB001,1250.5,1248.3,1251.2,1249.8,1250.1
FAB002,1247.5,1250.8,1249.1,1251.5,1248.2
```
```python
import csv
def read_lot_csv(file_path):
"""
安全读取Lot数据CSV
参数:
file_path: CSV文件路径
返回:
list[dict]: 解析后的数据
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
data = list(reader)
if not data:
print("⚠ 文件为空")
return []
print(f"✅ 成功读取 {len(data)} 条记录")
return data
except FileNotFoundError:
print(f"⚠ 文件不存在: {file_path}")
return []
except UnicodeDecodeError:
# 有些MES导出的文件是GBK编码
try:
with open(file_path, 'r', encoding='gbk') as f:
return list(csv.DictReader(f))
except:
print("⚠ 编码问题,无法读取")
return []
except Exception as e:
print(f"⚠ 读取失败: {e}")
return []
使用
data = read_lot_csv("mes_data.csv")
if data:
print(f"第1批: {data[0]}")
```
为什么这样写? 函数返回空列表而不是抛异常——调用者可以安全地处理 "没数据" 的情况,比如 `if not data: use_default()`。另外要注意的是,中文Windows导出的CSV经常是GBK编码,所以 `UnicodeDecodeError` 时要尝试用 `gbk` 重新打开。
3.2 安全的Lot分析
```python
def safe_analyze_lots(csv_data):
"""
安全分析多批Lot数据
跳过有问题的数据,不影响其他Lot的分析
"""
results = []
success = 0
failed = 0
for i, row in enumerate(csv_data):
try:
# 提取厚度数据(5个测量值)
thickness = []
for key in ['thickness_1', 'thickness_2', 'thickness_3',
'thickness_4', 'thickness_5']:
val = float(row.get(key, 0))
thickness.append(val)
# 计算统计量
avg = sum(thickness) / len(thickness)
# Cpk计算
usl = 1253 # 规格上限
lsl = 1247 # 规格下限
std = (sum((x - avg)**2 for x in thickness) /
(len(thickness) - 1)) ** 0.5
cpk = min((usl - avg) / (3 std), (avg - lsl) / (3 std)) if std > 0 else 0
results.append({
'lot_id': row.get('lot_id', f'Unknown-{i}'),
'avg': round(avg, 2),
'std': round(std, 2),
'cpk': round(cpk, 3),
'pass': cpk >= 1.0
})
success += 1
except (ValueError, ZeroDivisionError, KeyError) as e:
print(f"⚠ 第{i+2}行数据异常: {e},已跳过")
failed += 1
continue # 跳过这一行,继续分析下一个
print(f"分析完成: 成功{success}个, 跳过{failed}个")
return results
使用
results = safe_analyze_lots(data)
for r in results[:3]:
status = "✅" if r['pass'] else "⚠"
print(f"{status} {r['lot_id']}: avg={r['avg']}, Cpk={r['cpk']}")
```
最关键的技巧:`try` 只包裹可能出错的部分(提取数值和计算),`except` 里用 `continue` 跳过有问题的 Lot。这样就算第10个Lot数据坏了,前9个和后40个都正常分析。
3.3 把结果保存回去
```python
def save_results(results, output_path="analysis_result.csv"):
"""
保存分析结果
如果写入失败,不崩溃,只报错
"""
try:
with open(output_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['lot_id', 'avg', 'std', 'cpk', 'pass'])
writer.writeheader()
writer.writerows(results)
print(f"✅ 结果已保存: {output_path}")
return True
except PermissionError:
print(f"⚠ 无权限写入: {output_path}")
return False
except Exception as e:
print(f"⚠ 保存失败: {e}")
return False
完整流程
data = read_lot_csv("mes_20260115.csv")
if data:
results = safe_analyze_lots(data)
save_results(results)
```
---
四、效果对比
| 情况 | 不加异常处理 | 加异常处理 |
|------|------------|-----------|
| 文件不存在 | 程序直接崩溃 | 提示"文件不存在",用默认数据 |
| 空文件 | 除0错误崩溃 | 返回空列表,继续往下走 |
| 某行数据格式错 | 第10行崩了,前面9行白干 | 跳过第10行,继续后面的 |
| 编码问题 | UnicodeDecodeError | 自动切换gbk编码 |
| 无写入权限 | PermissionError崩溃 | 打印错误,数据不丢 |
一句话总结:不加异常处理是"全部或全不"——要么全对,要么全崩。加异常处理是"尽力而为"——好的留下,坏的跳过。
---
五、自己动手
```python
练习:读取你自己的数据
import csv
假设你有一个数据文件 data.csv,内容是:
id,value
1,123
2,abc
3,456
练习1:安全读取文件,处理文件不存在的异常
练习2:计算value的平均值,跳过非数字的行
练习3:用try-except保护整个过程
✏️ 下面写你的代码
```
思考题:
1. try-except 和 if-else 有什么区别?(提示:try处理"意料之外的错误",if处理"可以预见的情况")
2. 为什么不用一个大的try包住所有代码?试试看如果不知道具体哪行出错,调试是不是更困难?
3. `finally` 是用来做什么的?(提示:关闭文件、释放资源)
---
六、常见误区
| 误区 | 问题 | 正确做法 |
|------|------|---------|
| `except:` 不指定异常类型 | 捕获所有异常,包括Ctrl+C | 指定具体类型如 `except FileNotFoundError:` |
| try包一大坨代码 | 不知道哪行出的错 | 每个可能出错的步骤单独try |
| 捕获了异常但不处理 | `except: pass` 静默吞错误 | 至少打印日志 `print(f"错误: {e}")` |
| 只用 `else` 不用 `finally` | 资源可能没释放 | 用 `finally` 或 `with` 语句 |
---
> 💬 你写代码时有没有"跑了一个小时崩了"的经历?评论区聊聊
> 📚 收藏+点赞,下一篇开始搞数据分析 👆





