☰
sg2地震数据解析实战:从segdat.zip到NumPy数组的完整指南
2026/10/8 16:58:17 网站建设 项目流程

简介:这份资源面向地震学数据处理方向的学习者与研究人员,聚焦于将地震监测中常见的SG2二进制格式转换为便于阅读分析的DAT文本格式,适合需要打通不同软件与分析流程之间数据壁垒的初学者和科研人员。压缩包内共1个文件,为MATLAB脚本(.m),整体约1KB,体量轻巧,核心逻辑集中在格式解析与数据重排上。SG2格式通常紧凑存储加速度、速度或位移等原始时间序列及采样率、通道、时间戳等元数据,而DAT以ASCII码形式呈现振幅、频率特征与波形段落,更利于人工查看与二次分析。脚本大致覆盖解析SG2结构、提取关键信息、转换数据类型与排列方式、写入DAT并保留头部注释等环节,读者可据此理解二进制到文本的转换思路,并按需调整参数适配不同数据源。目前已有608人学习下载,可作为地震数据格式转换的入门参考。

1. segdat.zip 里的 sg2 地震数据:从压缩包到可读剖面的第一步

你拿到一个segdat.zip,解压后是一堆.dat文件,用文本编辑器打开全是乱码,用 Excel 打开直接卡死。这不是文件损坏,而是典型的sg2 格式地震数据——一种在石油物探、工程勘探和微地震监测里广泛使用的二进制地震记录格式。它和微信 dat 文件一样,都是“看起来像垃圾,实际有固定头结构”的二进制容器,区别在于 sg2 里装的是地震道集,不是聊天图片。

这个标题要解决的问题很具体:segdat.zip 解压出来的 sg2 地震数据,怎么识别、怎么读、怎么转成能用的格式。适合做地震资料处理、微地震监测、工程勘察的从业者,也适合需要把老地震数据迁移到现代处理流程的工程师。读完你能自己写解析脚本,知道哪些参数必须核对,哪些坑一踩就白干。

2. sg2 格式拆解:头段、道头和数据体到底怎么排

2.1 先搞清楚 sg2 不是单一格式,而是一族

sg2 这个叫法在不同软件里有不同含义。常见的有两类:一类是Sercel/Geospace 等采集系统输出的 SG2 格式,另一类是某些国产软件自定义的 sg2 变种。它们共同点是:文件由文件头 + 道头 + 数据体组成,但头段长度、字节序、采样点类型可能不同。

我一般拿到一个陌生 sg2 文件,先做三件事:

  1. 看文件大小能不能被某个记录长度整除;
  2. 用十六进制查看器看前 64 字节有没有可读的 ASCII 或规律性整数;
  3. 试几个常见头长(如 32、64、128、256 字节),看数据体部分是否呈现地震波形的连续性。
# 查看文件基本信息 ls -lh segdat.zip file segdat.zip unzip -l segdat.zip # 解压后看单个 dat 文件大小 unzip segdat.zip -d segdat_out ls -lh segdat_out/*.dat # 用 xxd 看前 256 字节,判断头段结构 xxd -l 256 segdat_out/line01.dat

逻辑说明:unzip -l先确认压缩包里文件数量和大小,避免解压出一堆碎片。xxd看前 256 字节是关键,sg2 的文件头通常包含采样率、采样点数、道数、起始时间等信息,这些值往往以 16 位或 32 位整数出现。如果前几十字节全是 0 或 FF,说明头段可能被加密或用了非标准偏移。

参数说明:-l 256限制只显示 256 字节,避免刷屏。如果你看到类似00 00 01 F4这种值,01 F4转十进制是 500,很可能就是采样率 500 Hz 或采样点数 500。

2.2 文件头里必须核对的四个字段

不管哪类 sg2,下面四个字段错了,后面全错:

字段常见偏移典型值核对方法
采样率0x10~0x14500、1000、2000 Hz和采集设计对比
采样点数0x14~0x181024、2048、4096文件大小反推
道数0x18~0x1C24、48、96总大小除以单道长度
数据格式0x1C~0x201=IEEE float,3=16位整型试读看波形是否合理
import struct def peek_sg2_header(filepath): with open(filepath, 'rb') as f: head = f.read(64) # 按小端序试读几个候选位置 candidates = { 'sample_rate_0x10': struct.unpack('<I', head[0x10:0x14])[0], 'sample_count_0x14': struct.unpack('<I', head[0x14:0x18])[0], 'trace_count_0x18': struct.unpack('<I', head[0x18:0x1C])[0], 'data_format_0x1C': struct.unpack('<I', head[0x1C:0x20])[0], } for k, v in candidates.items(): print(f"{k}: {v}") return candidates peek_sg2_header('segdat_out/line01.dat')

逻辑说明:这段代码不是万能解析器,而是侦察工具。它按小端序读几个常见偏移,打印出来让你判断哪个值像采样率、哪个像道数。如果读出来是0或4294967295,说明偏移不对或字节序是大端。

参数说明:<I表示小端无符号 32 位整数。如果你怀疑是大端,把<换成>再试。head[0x10:0x14]是 Python 切片,取第 16 到 19 字节。

2.3 道头和道数据的分离逻辑

sg2 的每条道通常有道头(几十字节)和道数据(采样点数组)。道头里存道号、炮检距、坐标等。解析时最容易翻车的地方是:道头长度不固定,有的系统用 32 字节,有的用 64 字节,还有的用 240 字节。

我一般用“总大小反推法”:

import os def infer_trace_layout(filepath, file_header_len=64): total = os.path.getsize(filepath) body = total - file_header_len # 假设采样点数 1024,float32 每点 4 字节 for trace_header_len in [32, 64, 128, 240]: for n_samples in [512, 1024, 2048, 4096]: trace_bytes = trace_header_len + n_samples * 4 if body % trace_bytes == 0: n_traces = body // trace_bytes print(f"trace_header={trace_header_len}, " f"n_samples={n_samples}, n_traces={n_traces}") return infer_trace_layout('segdat_out/line01.dat')

逻辑说明:这段代码暴力枚举道头长度和采样点数的组合,看哪个组合能让文件体整除。整除的那个组合大概率就是真实布局。如果多个组合都整除,优先选道数接近采集设计值的那个。

参数说明:file_header_len=64是文件头长度,需要根据上一步xxd结果调整。n_samples * 4是因为假设数据是 float32;如果是 16 位整型,改成* 2。

提示:不要一上来就写完整解析器。先用上面两段代码把文件“摸”清楚,再动手写正式读取函数,能省掉大量返工。

3. 用 Python 把 sg2 读成 NumPy 数组:最小可用解析器

3.1 读取流程和关键参数

确认布局后,读取就简单了:跳过文件头,循环读每条道的道头和数据。下面是一个最小可用解析器,支持 float32 和 int16 两种常见数据格式。

import numpy as np import struct def read_sg2(filepath, file_header_len=64, trace_header_len=64, n_samples=1024, data_format='float32', endian='<'): """ 读取 sg2 文件,返回 (n_traces, n_samples) 的 NumPy 数组 data_format: 'float32' 或 'int16' endian: '<' 小端,'>' 大端 """ dtype_map = { 'float32': np.dtype(endian + 'f4'), 'int16': np.dtype(endian + 'i2'), } dt = dtype_map[data_format] traces = [] with open(filepath, 'rb') as f: f.seek(file_header_len) while True: thead = f.read(trace_header_len) if len(thead) < trace_header_len: break data = np.frombuffer(f.read(n_samples * dt.itemsize), dtype=dt) if len(data) < n_samples: break traces.append(data) return np.array(traces) # 使用示例 data = read_sg2('segdat_out/line01.dat', file_header_len=64, trace_header_len=64, n_samples=1024, data_format='float32') print(data.shape) # (n_traces, n_samples)

逻辑说明:f.seek(file_header_len)跳过文件头。循环里先读道头,再读固定长度的数据。np.frombuffer直接把字节转成数组,比struct.unpack快得多。如果读出来的data.shape是(0,)或道数明显不对,回去检查trace_header_len和n_samples。

参数说明:trace_header_len必须和上一步推断一致。data_format如果选错,波形会变成噪声或直线。判断方法:float32 读出来幅值通常在1e-6到1e3之间;int16 读出来是-32768到32767的整数。

3.2 快速验证读出来的对不对

读完后不要急着做处理,先画一条道看看。

import matplotlib.pyplot as plt def quick_check(data, trace_index=0): plt.figure(figsize=(12, 3)) plt.plot(data[trace_index]) plt.title(f"Trace {trace_index}") plt.xlabel("Sample") plt.ylabel("Amplitude") plt.tight_layout() plt.savefig("trace_check.png", dpi=150) print("saved trace_check.png") quick_check(data, trace_index=0)

逻辑说明:正常地震道应该看到初至波、反射同相轴或至少是连续振荡。如果是一条直线,可能是数据格式选错(比如把 int16 当 float32 读)。如果是随机噪声,可能是字节序反了。

参数说明:trace_index=0看第一道。如果第一道是坏道,换trace_index=10再看。

3.3 转成 SEG-Y 或 NumPy 保存

读成 NumPy 后,可以存成.npy方便后续处理,也可以转成 SEG-Y 给专业软件用。转 SEG-Y 需要额外写头段,这里给一个存.npy的最小示例。

def save_as_npy(data, outpath): np.save(outpath, data) print(f"saved {outpath}, shape={data.shape}") save_as_npy(data, 'line01.npy') # 后续读取 loaded = np.load('line01.npy') print(loaded.shape)

逻辑说明:.npy是 NumPy 原生格式,读写快,适合在 Python 流程里传递。如果要给 GeoEast、Promax 等软件用,需要按 SEG-Y 标准写 3200 字节文本头 + 400 字节二进制头 + 道头,那是另一个话题。

参数说明:np.save会自动加.npy后缀。如果outpath已经带.npy,不会重复加。

4. 避坑与排查:sg2 解析翻车的五个典型场景

4.1 现象:读出来全是零或全是同一个值

原因:文件头长度猜错,把数据体当文件头跳过了;或者数据格式选错,比如实际是 int16 却按 float32 读。

解决:用xxd重新看前 256 字节,确认文件头结束位置。然后试data_format='int16'再读一次。如果还是不对,检查字节序,把endian='<'改成'>'。

4.2 现象:道数对不上,最后一条道总是残缺

原因:文件末尾有填充字节,或者道头长度不是固定值。有些 sg2 变种在最后一条道后面加了校验块。

解决:在循环里加判断,如果剩余字节不够一条道,直接break。不要试图读残缺道。另外用os.path.getsize算一下理论道数,和实际读出的道数对比。

4.3 现象:波形看起来像被“压扁”了,幅值全在 0 附近

原因:数据格式选错。比如实际是 24 位整型,你按 16 位读,高位丢失,幅值就变得很小。

解决:sg2 常见数据格式有 16 位整型、32 位整型、32 位浮点。如果 16 位和 32 位浮点都不对,试 32 位整型(np.int32),然后除以一个增益系数。

4.4 现象:多个 dat 文件读出来采样率不一致

原因:segdat.zip 里可能混了不同采集参数的文件,或者文件名相似但来自不同测线。

解决:不要批量用一个参数读所有文件。先对每个文件单独跑peek_sg2_header,把采样率、采样点数、道数打印出来,按参数分组再批量处理。

4.5 现象:内存不够,大文件读一半就崩

原因:一次性把所有道读进内存。一个 1 GB 的 sg2 文件,读成 float32 数组可能占 2 GB 以上。

解决:用生成器逐道读取,或者用np.memmap做内存映射。下面是一个生成器版本:

def iter_sg2_traces(filepath, file_header_len=64, trace_header_len=64, n_samples=1024, data_format='float32', endian='<'): dtype_map = { 'float32': np.dtype(endian + 'f4'), 'int16': np.dtype(endian + 'i2'), } dt = dtype_map[data_format] with open(filepath, 'rb') as f: f.seek(file_header_len) while True: thead = f.read(trace_header_len) if len(thead) < trace_header_len: break buf = f.read(n_samples * dt.itemsize) if len(buf) < n_samples * dt.itemsize: break yield np.frombuffer(buf, dtype=dt) # 逐道处理,不占大内存 for i, trace in enumerate(iter_sg2_traces('segdat_out/line01.dat')): if i % 100 == 0: print(f"trace {i}, max={trace.max():.3f}")

逻辑说明:yield让函数变成生成器,每次只返回一条道。处理完一条丢一条,内存占用恒定。适合大文件或批量处理。

参数说明:i % 100 == 0每 100 道打印一次,避免刷屏。trace.max()看幅值是否合理。

5. 批量转换与参数扫描:把 segdat.zip 里所有文件跑通

5.1 自动扫描最佳解析参数

面对一堆 dat 文件,手动试参数太慢。写一个扫描函数,对每个文件自动找最可能的布局。

import os import numpy as np def scan_sg2(filepath, file_header_len=64): total = os.path.getsize(filepath) body = total - file_header_len results = [] for thl in [32, 64, 128, 240]: for ns in [512, 1024, 2048, 4096]: for fmt, size in [('float32', 4), ('int16', 2)]: trace_bytes = thl + ns * size if body % trace_bytes == 0: n_traces = body // trace_bytes results.append((thl, ns, fmt, n_traces)) return results for fname in sorted(os.listdir('segdat_out')): if fname.endswith('.dat'): fpath = os.path.join('segdat_out', fname) print(f"\n{fname}:") for r in scan_sg2(fpath): print(f" trace_header={r[0]}, n_samples={r[1]}, " f"fmt={r[2]}, n_traces={r[3]}")

逻辑说明:对每个文件枚举道头长度、采样点数、数据格式的组合,输出所有能整除的候选。然后人工挑一个最合理的——通常道数接近采集设计值、采样率符合工区参数的那个。

参数说明:file_header_len=64是默认值,如果某些文件头更长,需要单独调整。body % trace_bytes == 0是整除判断,余数为零说明布局自洽。

5.2 批量转成 .npy 并记录元数据

确定参数后,批量转换并写一个简单的元数据日志。

import json def batch_convert(indir, outdir, params): os.makedirs(outdir, exist_ok=True) meta = [] for fname in sorted(os.listdir(indir)): if not fname.endswith('.dat'): continue fpath = os.path.join(indir, fname) data = read_sg2(fpath, **params) outpath = os.path.join(outdir, fname.replace('.dat', '.npy')) np.save(outpath, data) meta.append({ 'file': fname, 'shape': list(data.shape), 'params': params, }) print(f"{fname} -> {outpath}, shape={data.shape}") with open(os.path.join(outdir, 'meta.json'), 'w') as f: json.dump(meta, f, indent=2) params = { 'file_header_len': 64, 'trace_header_len': 64, 'n_samples': 1024, 'data_format': 'float32', 'endian': '<', } batch_convert('segdat_out', 'npy_out', params)

逻辑说明:meta.json记录每个文件的形状和解析参数,方便回溯。如果后面发现某个文件读错了,能快速定位是哪个参数的问题。

参数说明:params字典直接传给read_sg2,改参数不用改函数。indent=2让 JSON 可读。

5.3 用 SEG-Y 做交叉验证

如果手头有 SEG-Y 版本的同一批数据,可以读一条道对比幅值。没有 SEG-Y 的话,至少把.npy文件用其他软件打开看看。我一般会用segyio或obspy读 SEG-Y,然后和 sg2 解析结果做互相关,确认没有整体偏移。

# 如果有 segyio,可以这样对比 # import segyio # with segyio.open('reference.sgy', 'r') as f: # ref_trace = f.trace[0] # print(ref_trace[:10]) # print(data[0][:10])

逻辑说明:这段代码是注释掉的示例,因为不是每个人都有参考 SEG-Y。核心思路是:拿一条已知正确的道,和 sg2 解析出的同一条道对比前几个采样值。如果完全对不上,说明解析参数错了。

参数说明:f.trace[0]读第一条道。data[0][:10]是 sg2 解析结果的第一条道前 10 个点。

注意:批量转换前,先用一两个文件验证参数。确认无误再跑全量,否则可能白跑几个小时。

6. 从 sg2 到处理流程:几个能省时间的进阶习惯

6.1 把解析参数写成配置文件

每次手动改参数容易出错。我习惯把参数写进config.json,脚本读配置运行。

{ "file_header_len": 64, "trace_header_len": 64, "n_samples": 1024, "data_format": "float32", "endian": "<" }
import json with open('config.json') as f: params = json.load(f) data = read_sg2('segdat_out/line01.dat', **params) print(data.shape)

逻辑说明:配置和代码分离,换工区只改 JSON,不动 Python。团队协作时,配置文件可以进版本控制,方便追溯。

参数说明:**params把字典展开成关键字参数,键名必须和read_sg2的形参一致。

6.2 用内存映射处理超大文件

如果单个 sg2 文件超过 2 GB,np.memmap是更好的选择。

def memmap_sg2(filepath, file_header_len=64, trace_header_len=64, n_samples=1024, data_format='float32', endian='<'): import os dtype_map = { 'float32': np.dtype(endian + 'f4'), 'int16': np.dtype(endian + 'i2'), } dt = dtype_map[data_format] total = os.path.getsize(filepath) body = total - file_header_len trace_bytes = trace_header_len + n_samples * dt.itemsize n_traces = body // trace_bytes # 只映射数据部分,道头跳过 offset = file_header_len + trace_header_len mm = np.memmap(filepath, dtype=dt, mode='r', offset=offset, shape=(n_traces, n_samples)) return mm mm = memmap_sg2('segdat_out/line01.dat') print(mm.shape) print(mm[0, :5])

逻辑说明:np.memmap不把文件读进内存,而是按需分页加载。适合超大文件或内存受限的环境。注意offset要跳过文件头和第一条道的道头,但这样映射出来的数据里,每条道之间会混入下一条道的道头。所以memmap只适合道头长度为零或道头固定且你愿意手动跳过的场景。如果道头非零,还是用生成器逐道读更稳妥。

参数说明:mode='r'只读。shape=(n_traces, n_samples)指定二维形状。offset是数据体起始位置。

6.3 一个我踩过的坑:别信文件名里的采样率

有一次 segdat.zip 里文件名写着line01_500hz.dat,我直接按 500 Hz 解析,结果波形周期明显不对。后来用peek_sg2_header一读,文件头里写的是 1000 Hz。文件名是给人看的,文件头才是给机器看的。从那以后,我所有解析脚本都先读文件头,再决定参数,绝不从文件名猜。

另一个习惯是:解析完先存一份.npy和meta.json,再做后续处理。这样即使后面处理流程改了,原始解析结果还在,不用重新解压和解析。后悔药不好买,但多存一份中间结果,能省很多事。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询