项目启动一直失败,错误提示看得我一头雾水,查了半天才发现问题居然在配置文件里
昨天帮同事部署一个数据处理脚本,启动的时候直接给我来了个报错,当时就傻眼了
$ python main.py
Traceback (most recent call last):
File "main.py", line 12, in <module>
config = yaml.safe_load(f)
File "/usr/lib/python3.10/site-packages/yaml/__init__.py", line 162, in load
return loader.get_single_data()
yaml.scanner.ScannerError: mapping values are not allowed here
in "config.yaml", line 15, column 14
报的是YAML语法错误,在第15行第14列
我打开配置文件一看,第15行正好是个注释 # 数据库连接配置
这就有意思了,注释也能导致报错
项目背景
先说说这是个什么项目
我们组在做一个自动化数据清洗的脚本,主要是从MySQL数据库里拉数据,做一些ETL处理,然后写入数据仓库
项目结构大概是这样的:
data-pipeline/
├── main.py
├── config.yaml
├── src/
│ ├── __init__.py
│ ├── extractor.py
│ ├── transformer.py
│ └── loader.py
└── requirements.txt
config.yaml 里配置了数据库连接信息、任务调度参数、字段映射关系啥的
配置大概是这个样子的(简化版):
# 数据清洗配置文件
database:
host:localhost
port:3306
user:etl_user
password:"s3cr3t_p@ssw0rd"
database:data_warehouse
etl:
batch_size:5000
timeout:300
retry_times:3
# 数据源配置
sources:
-name:user_activity
table:user_activity_log
incremental:true
key_column:id
-name:order_detail
table:orders
incremental:false
# 目标表配置
target:
table:dw_fact_table
mode:append
看起来很正常对吧
注释写得规规矩矩的
但就是加载失败
排查过程
报错信息说第15行有问题
我数了一下,第15行刚好是 # 数据库连接配置 这个注释
我第一反应是:**编码问题
**
因为之前遇到过Windows创建的YAML文件带BOM头,导致解析失败的情况
我试着检查了一下文件编码:
$ file config.yaml
config.yaml: UTF-8 Unicode text, with very long lines
$ hexdump -C config.yaml | head -20
00000000 23 20 23 20 23 20 23 20 23 20 23 20 23 20 23 |# ### ### ### ### ### ##|
看起来是UTF-8编码,没问题啊
开头也没有BOM
**那是不是YAML语法有问题
**
我仔细检查了缩进
YAML对缩进要求很高,空格和混用Tab都不行
我用cat -A检查了一下:
$ cat -A config.yaml | head -20
# 数据清洗配置文件$
database:$
host: localhost$
port: 3306$
user: etl_user$
password: "s3cr3t_p@ssw0rd"$
database: data_warehouse$
$
etl:$
batch_size: 5000$
timeout: 300$
retry_times: 3$
$
# 数据库连接配置$
sources:$
嗯,都是正常的行尾换行符,没有混Tab
缩进看起来也是用空格实现的
**会不会是PyYAML版本问题
**
我检查了一下安装的版本:
$ pip show pyyaml
Name: PyYAML
Version: 6.0
Summary: YAML parser and emitter for Python
6.0版本,应该比较稳定
我试着降低版本试试:
$ pip install pyyaml==5.4.1
$ python main.py
还是同样的错误
更诡异的事情来了——
我把报错那行注释删掉,再跑一次:
$ python main.py
Traceback (most recent recent call last):
File "main.py", line 12, in <module>
config = yaml.safe_load(f)
File "/usr/lib/python32/site-packages/yaml/__init__.py", line 162, in load
return loader.get_single_data()
yaml.scanner.ScannerError: mapping values are not allowed here
in"config.yaml", line 18, column 14
现在报的是第18行了
删除一个注释,报错行号变了,但问题还在
这说明问题不是某一行注释本身,而是注释导致的某种连锁反应
终于找到真凶
我开始怀疑是不是YAML解析器对注释的处理有什么特殊规则
于是我做了个实验:
把配置文件里的注释全部删掉,再跑:
$ python main.py
2024-01-15 10:23:45 - INFO - Configuration loaded successfully
2024-01-15 10:23:45 - INFO - Starting ETL pipeline...
居然成功了
问题真的出在注释上
但是,为啥注释会导致YAML解析失败呢
我又做了另一个实验——逐行恢复注释,看看到底是哪一行有问题
恢复第一组注释(文件头部的# 数据清洗配置文件):
$ python main.py # 成功
加上数据库配置下的注释(# 数据库连接配置):
$ python main.py
# 报错了!
找到问题了
就是 # 数据库连接配置 这行注释导致的
但这行注释看起来完全正常啊,#开头,后面跟中文,没有任何问题
我把这行注释改成英文:
# database config
sources:
再跑一次:
$ python main.py
2024-01-15 10:23:45 - INFO - Configuration loaded successfully
居然成功了
**居然是中文注释导致的问题
**
但这也不对啊,文件编码是UTF-8,中文应该没问题
而且之前我也经常在YAML里用中文注释,从没遇到过问题
我把这行注释改成其他中文试试:
# 数据库配置
sources:
成功
# 数据库连接配置
sources:
失败
我去,这到底是什么鬼
# 数据库连接配置这9个字里,有什么是不能出现在注释里的
我试着一个个字尝试:
# 数据库连
sources:# 成功
# 数据库连接
sources:# 成功
# 数据库连接配
sources:# 成功
# 数据库连接配置
sources:# 失败!
问题出在"置"这个字上
让我再试试:
# 数据库连接配xx
sources:# 成功
# 数据库连接配置x
sources:# 失败
好嘛,加个字就失败,去个字就成功
# 数据库连接配置这个字符串里,多的那个"置"或者"置x"会导致YAML解析失败
这也太邪门了
真相大白
我决定用Python直接测试一下,看看这个字符串到底有什么魔力:
import yaml
test_cases = [
"# 数据库连接配置",
"# 数据库连接配置x",
"# 数据库连接配置a",
"# test 配置",
"# 测试配置",
]
for i, comment inenumerate(test_cases, 1):
yaml_content = f"""
database:
host: localhost
port: 3306
{comment}
sources:
- name: test
"""
try:
result = yaml.safe_load(yaml_content)
print(f"Case {i}: OK - {comment!r}")
except Exception as e:
print(f"Case {i}: FAILED - {comment!r}")
print(f" Error: {e}")
运行结果:
Case 1: FAILED - '# 数据库连接配置'
Error: mapping values are not allowed here
Case 2: FAILED - '# 数据库连接配置x'
Error: mapping values are not allowed here
Case 3: OK - '# 数据库连接配置a'
Case 4: OK - '# test 配置'
Case 5: OK - '# 测试配置'
这也太奇怪了
# 数据库连接配置会失败,但# 数据库连接配置a反而成功
让我再试试更多组合:
test_cases = [
"# 数据库连接配置",
"# 数据库连接配置1",
"# 数据库连接配置2",
"# 数据库连接配置a",
"# 数据库连接配置b",
"# 数据库连接配置ab",
"# 数据库连接配置ba",
"# 数据库连接配置abc",
]
# 运行测试...
结果更离谱了:
这完全没有规律啊
真正的元凶:Tab字符
就在我快要崩溃的时候,同事过来问我进展怎么样了
我给他看了这个奇怪的现象,他也懵了
他说:“会不会是配置文件里混入了不可见字符
比如Tab
”
对啊
我一直检查的是注释那一行本身,但问题可能出在缩进上
YAML要求使用空格缩进,禁止使用Tab
但有时候编辑器配置不当,或者复制代码的时候,会把Tab带进来
让我重新检查配置文件的缩进:
$ cat -A config.yaml | head -30
...(省略前面部分)
database: data_warehouse$
$
etl:$
batch_size: 5000$
timeout: 300$
retry_times: 3$
$
^I# 数据库连接配置$
sources:$
看到了吗
^I
^I就是Tab字符
在#之前有一个Tab
也就是说,配置文件里是这样的:
⇥# 数据库连接配置
sources:
而不是这样的:
# 数据库连接配置
sources:
YAML的注释必须从行首开始(忽略前导空格),但如果注释前面有Tab,而这个Tab恰好出现在某个结构后面,解析器就会犯糊涂
让我验证一下:
import yaml
# Tab在注释前面(错误)
yaml_content = """
database:
host: localhost
\t# 这是一个注释
sources:
- name: test
"""
try:
yaml.safe_load(yaml_content)
print("OK")
except Exception as e:
print(f"FAILED: {e}")
输出:
FAILED: mapping values are not allowed here
找到了
**问题不是注释本身,而是注释前面有一个Tab字符
**
因为cat -A显示的是原始字符,而我在编辑器里看的时候,Tab被显示成空格,所以我一直以为缩进是正常的
完整复现和解决方案
现在让我完整复现这个问题,并给出解决方案
问题复现代码
# test_yaml_tab.py
import yaml
# 场景1:注释前有Tab(错误)
yaml_with_tab = """
database:
host: localhost
port: 3306
\t# 数据库连接配置
sources:
- name: test
"""
# 场景2:正常注释(正确)
yaml_normal = """
database:
host: localhost
port: 3306
# 数据库连接配置
sources:
- name: test
"""
print("=== 测试1: 注释前有Tab ===")
try:
config = yaml.safe_load(yaml_with_tab)
print("加载成功")
except yaml.YAMLError as e:
print(f"加载失败: {e}")
print("\n=== 测试2: 正常注释 ===")
try:
config = yaml.safe_load(yaml_normal)
print("加载成功")
except yaml.YAMLError as e:
print(f"加载失败: {e}")
运行结果:
=== 测试1: 注释前有Tab ===
加载失败: mapping values are not allowed here
in "<unicode string>", line 5, column 2
=== 测试2: 正常注释 ===
加载成功
解决方案
方法1:使用空格代替Tab
这是最根本的解决办法
在编辑器里设置Tab转换为空格:
# vim 中设置
:set expandtab
:set tabstop=4
:set shiftwidth=4
# 或者在 .vimrc 中添加
set expandtab
方法2:用命令行转换
# 把Tab转换成4个空格
sed -i 's/\t/ /g' config.yaml
# 或者用 expand 命令
expand -t 4 config.yaml > config_fixed.yaml
方法3:在Python中预处理
如果不想修改配置文件,也可以在加载YAML之前预处理:
import yaml
import re
defload_yaml_without_tab(filepath):
withopen(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 替换Tab为空格(4个空格)
content = content.replace('\t', ' ')
# 使用safe_load解析
return yaml.safe_load(content)
# 使用
config = load_yaml_without_tab('config.yaml')
这件事带来的思考
为什么之前方案不行
回看我的排查过程:
- 1. 检查编码 - 方向错了,文件确实是UTF-8
- 2. 检查YAML语法 - 方向也错了,表面上看语法完全正确
- 3. 换PyYAML版本 - 完全无效,问题不在版本
- 4. 怀疑中文注释 - 差一点就接近真相了,但被错误的思路带偏了
最核心的问题是:我以为编辑器显示的就是真实内容
其实很多编辑器默认把Tab显示成4个空格的视觉效果,导致我完全没有意识到Tab的存在
经验教训
- • 用
cat -A、hexdump等命令检查原始字符 - • 特别是当你排查"明明看起来没问题但就是报错"的问题时
- • 建议在项目里加一个CI检查,禁止Tab字符进入仓库
- •
mapping values are not allowed here 这个错误信息其实已经提示了方向 - • 是"映射值"出了问题,通常是缩进、语法错误导致的
怎么避免类似的坑
现在我的项目里都加了这样的Git hooks:
# .git/hooks/pre-commit
#!/bin/bash
# 检查YAML文件是否包含Tab
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\.yaml$'); do
if grep -q $'\t'"$file"; then
echo"Error: $file contains tab characters. Use spaces instead."
exit 1
fi
done
这样在提交之前就会检查,如果有Tab就拒绝提交,从源头解决问题
总结
这个bug卡了我整整一个下午,最后发现居然是因为配置文件里有一个不起眼的Tab字符
你说这个问题严重吧,其实就是手误;说不严重吧,确实能让人折腾半天
我现在每次配置YAML文件,都会习惯性地用cat -A检查一下
宁可现在多花10秒钟检查,也不要以后花半天时间排查
**代码无小事,细节是魔鬼
** 与大家共勉