3、光谱数据格式:常见光谱文件格式、数据导入导出与元数据管理
做光谱分析这些年,我打交道最多的就是文件格式。说实话,刚入行那会儿,光是被各种后缀名就搞晕过——.spa、.csv、.txt……每个仪器厂商都有自己的“小脾气”。今天咱们就把这事儿彻底捋清楚。
3.1 常见光谱文件格式
先说说最常见的三种格式,我按使用频率排个序。
3.1.1 .spa 格式(Thermo Fisher 专属)
.spa 是 Thermo Fisher 系列红外光谱仪的“母语”。它是个二进制文件,里面不光存了光谱数据,还塞了一大堆元数据——仪器型号、采集时间、操作员、样品名、扫描次数……
特点:
- 二进制存储,体积小,读取快
- 元数据丰富,但跨平台兼容性差
- 只能用特定软件(如 OMNIC)或专用库解析
pySPA 库硬解析出来的。嗯,这里要注意——pySPA 只支持部分版本,老版本 .spa 可能读不了。
3.1.2 .csv 格式(通用之王)
.csv 说白了就是逗号分隔的文本文件。几乎所有仪器都能导出成 .csv,也是我最推荐的数据交换格式。
典型结构:
Wavenumber, Absorbance
4000.00, 0.1234
3999.00, 0.1245
...
你想想看,.csv 的好处是什么?任何语言、任何工具都能打开。Excel 能看,Python 能读,甚至记事本都能瞅一眼。
encoding='utf-8-sig' 才解决。
3.1.3 .txt 格式(自由但混乱)
.txt 格式就有点“百家争鸣”了。有的用空格分隔,有的用制表符,有的甚至用冒号。我见过最离谱的——第一行是注释,第二行是单位,第三行才开始数据。
常见变体:
- 空格分隔:
4000.00 0.1234 - 制表符分隔:
4000.00\t0.1234 - 带表头:
# Wavenumber (cm-1) Absorbance
核心建议: 我个人习惯把所有原始数据统一转成 .csv 格式再处理。为什么?因为 .txt 的解析规则太“自由”了,每次都要写不同的解析代码,太折腾。
3.2 数据导入与导出
这部分我直接上代码,都是我在项目中反复验证过的。
3.2.1 读取 .spa 文件
import numpy as np
import pandas as pd
# 方法一:使用 pySPA(推荐)
from pyspa import SpaFile
spa = SpaFile('sample.spa')
wavenumbers = spa.x
absorbance = spa.y
# 方法二:使用 brukeropus(如果装不了 pySPA)
# 注意:这个方法只适用于部分 .spa 版本
3.2.2 读取 .csv 文件
import pandas as pd
# 标准读取
df = pd.read_csv('spectrum.csv', header=0)
# 如果第一列是波数,第二列是吸光度
wavenumbers = df.iloc[:, 0].values
absorbance = df.iloc[:, 1].values
# 处理带 BOM 的文件
df = pd.read_csv('spectrum.csv', encoding='utf-8-sig')
3.2.3 读取 .txt 文件
import numpy as np
# 空格分隔,无表头
data = np.loadtxt('spectrum.txt')
# 制表符分隔,跳过前两行注释
data = np.loadtxt('spectrum.txt', delimiter='\t', skiprows=2)
# 带表头的复杂情况
with open('spectrum.txt', 'r') as f:
lines = f.readlines()
# 手动解析
data = []
for line in lines[3:]: # 跳过前3行
parts = line.strip().split()
if len(parts) == 2:
data.append([float(parts[0]), float(parts[1])])
3.2.4 导出为标准格式
import pandas as pd
# 导出为 .csv(推荐)
df = pd.DataFrame({
'Wavenumber': wavenumbers,
'Absorbance': absorbance
})
df.to_csv('output_spectrum.csv', index=False)
# 导出为 .txt(空格分隔)
np.savetxt('output_spectrum.txt',
np.column_stack([wavenumbers, absorbance]),
fmt='%.4f %.6f',
header='Wavenumber Absorbance')
header 或 columns 参数。为什么?因为半年后你再看这个文件,没有表头你根本不知道哪列是哪列。别问我怎么知道的……
3.3 元数据管理
元数据,说白了就是“关于数据的数据”。它记录了光谱是怎么来的、谁测的、什么条件。这部分容易被忽略,但出了问题全靠它溯源。
3.3.1 元数据包含什么
| 类别 | 具体字段 | 示例 |
|---|---|---|
| 样品信息 | 样品名称、批号、制备方法 | PMMA薄膜、20240301-01、热压法 |
| 仪器参数 | 仪器型号、分辨率、扫描次数 | Nicolet iS50、4 cm⁻¹、32次 |
| 采集条件 | 采集时间、温度、湿度 | 2024-03-01 14:30、25°C、45%RH |
| 处理记录 | 基线校正方法、平滑参数 | ALS基线校正、Savitzky-Golay 15点 |
3.3.2 如何管理元数据
我个人习惯用两种方式:
方式一:嵌入文件名
# 命名规则:样品名_日期_条件_序号
PMMA_20240301_32scan_001.spa
PMMA_20240301_32scan_002.spa
方式二:独立元数据文件
# metadata.csv
filename, sample, date, scans, resolution
PMMA_001.spa, PMMA薄膜, 2024-03-01, 32, 4
PMMA_002.spa, PMMA薄膜, 2024-03-01, 32, 4
我的建议: 别把元数据只放在文件名里。文件名会变,但独立的元数据文件不会丢。我曾经因为重命名文件丢了批次信息,后来花了三天重新核对——血的教训。
3.3.3 用 Python 管理元数据
import json
from datetime import datetime
# 创建元数据字典
metadata = {
'sample': 'PMMA薄膜',
'batch': '20240301-01',
'instrument': 'Nicolet iS50',
'resolution': 4,
'scans': 32,
'acquisition_time': datetime.now().isoformat(),
'operator': 'Zhang',
'notes': '室温25°C,湿度45%'
}
# 保存为 JSON 文件
with open('metadata_PMMA_001.json', 'w') as f:
json.dump(metadata, f, indent=2)
# 读取时自动关联
def load_spectrum_with_metadata(spa_file):
# 假设元数据文件与光谱文件同名,后缀不同
json_file = spa_file.replace('.spa', '.json')
with open(json_file, 'r') as f:
metadata = json.load(f)
# 读取光谱数据
# ... (读取代码)
return wavenumbers, absorbance, metadata
3.4 本章知识体系
下面这张图是我自己整理的,把本章的核心逻辑串起来了:
这张图你看懂了吗?从左到右,就是从原始数据到标准化管理的完整链路。我个人觉得,元数据管理是这里面最容易被忽视、但最关键的一环——没有它,你的数据就是一堆孤立的数字。
公众号:蓝海资料掘金营,微信deep3321