资讯动态

XTF测井文件解析:从二进制格式到LAS输出的完整实现

发布时间:2026/10/20 11:34:58 来源:尧图企业网站定制
简介面向石油勘探与测井软件开发人员这份资源围绕ECLIPS 5700测井系统的XTF文件读取问题打包了完整的ReadXTF工程源码与配套分析文档。压缩包共三十七个文件体积仅二点零五兆涵盖C源文件、头文件、Visual C工程文件、编译中间文件、可执行程序、界面资源与图标以及《ECLIPS 5700测井系统XTF文件格式分析》PDF结构清晰且可直接在Visual C 6.0中打开调试。对照源码与文档可以逐步理解XTF文件头结构、数据记录格式以及电阻率、声波时差等测井曲线的存储与解析方式并掌握在MFC文档视图框架中实现文件打开、数据提取与显示的方法还能学习到二进制数据读取、异常处理和曲线可视化等实用技巧。目前已有二百九十九人学习下载适合需要编写专用XTF读取工具、研究测井数据格式或进行二次开发的研究人员。1. 认识XTF测井文件一个读取程序能省掉多少手工活干过5700系列仪器资料处理的人几乎都撞上过同一个麻烦甲方给了XTF格式的测井数据说曲线就在里头但常规软件要么只能看图导不出数据要么导出来的通道和文件里对不上。XTF这种格式在文件层面是“头信息数据块”的组织方式听起来简单实际字段布局常跟着采集版本走商业软件把它们封装成黑匣子一旦你要批量处理、按井重组通道或者转成自定义格式就会卡在读取这一关。readxtfprograme XTF这套程序就是为这个场景准备的它把文件头、通道表、数据块逐一拆开还原成可用的曲线数据便于深度对齐、格式转换和绘图验证。适合测井数据处理现场急需把XTF转成LAS或CSV的从业者也适合想彻底弄明白这个二进制格式里到底存了什么的人。2. 解析XTF前先弄清三件事头、通道表与数据块布局动手写代码之前最好先把XTF的磁盘布局在脑子里过一遍。不然很容易出现一种局面代码写了一大半才发现文件头读的位置就不对。XTF打开后第一段是文件头里面有版本号、井名、深度范围和通道数量这些元信息。第二段是通道表描述每个通道的ID、名称和存储类型。第三段才是真正的内容——按块组织的数据体块里装着曲线采样值、深度标记和注释信息。下面把这三部分分别说明。2.1 文件头不是纯固定结构版本兼容的字段说明我处理过的XTF文件头部的常见设计是固定256字节前64字节放主要字段后面192字节保留给不同版本扩展。主要字段按顺序是文件标识、主版本号、次版本号、采集时间戳、井名定长8字节、起始深度、结束深度、采样间隔、通道数量和第一块数据偏移。这里最容易踩坑的一点是“定长井名”——一旦井名超过8个字符文件头里就会有两种处理方式截断或者把完整井名放到文件头之后的扩展区再在头部放一个偏移指针指向它。所以读头部代码不能只按固定偏移解一次还得多做一步判断井名段尾部是否为空闲字节如果填满了就要跳转到扩展偏移去重新读。文件头的版本号字段也很关键。版本号不同后续字段含义可能调整。常见做法是先读主版本号做分支低版本文件在头部末尾没有校验和段高版本会多出一个32位校验值。解析时如果忽略这个差异读高版本文件会连通道表起始位置都算错。因此头解析的第一条原则是先解出版本号再决定后续字段的偏移表不要用一套偏移量去通吃所有文件。2.2 通道表紧跟头部变长条目与名称编码通道表在文件头之后以一个无符号整数开头通道数量接下来是若干个通道条目。每个条目的常见固定部分是通道ID4字节、通道名称32字节、单位代码4字节、采样值类型4字节合计44字节条目剩余字节为保留区。通道名称一般用ASCII或UTF-8编码但有些采集软件会用GBK写中文曲线名直接按ASCII解码就会出现乱码。解码前先按字节判断是否为合法UTF-8不是就退回GBK这一条建议直接写进解析函数。通道表里还藏着后续数据体解析的关键每个通道的采样值类型。常见情况是全部通道都是32位浮点但也有的文件把某些通道存成64位浮点或16位整数。如果解析器默认所有通道为float32遇到混合类型文件数据体读出来就是错的。所以通道表解析完后要整理出一个“通道ID→采样类型”的映射供数据块处理时使用。2.3 数据块按类型分发曲线数据、深度标记与注释块XTF的数据体不是一个大数组而是由多个数据块顺序拼接而成。每个数据块以块头开始内容按块类型区分。常见块类型有三种曲线数据块、深度标记块、注释块。曲线数据块承载实际测井值块头记录起始深度、采样点数和块总长度深度标记块记录特殊深度点比如套管深度注释块保存文本信息。曲线数据块的内部排列方式也值得注意。多通道文件通常采用“采样点交错”排列先写第1个采样点的所有通道值再写第2个采样点的所有通道值。这样在还原二维数组时按块头中记录的采样点数M和通道表数量N一次性读出M×N个值再reshape成(M, N)的二维数组即可顺序上不需要再做通道交换。有的采集软件则按通道连续存放即先存完整条通道1的M个采样值再存通道2。两种布局在文件头里可能没有明确标志位只能用一个小技巧来识别取前几个采样点和另一套已知数据做相关比对。这一点在后面的避坑章节中会展开讲。文件头、通道表、数据块三者的关系可以用一句话概括文件头告诉你文件里有什么通道数据块告诉你这些通道的数值怎么排列。解析程序不过是用代码把上面这套约定翻译过来。3. 用Python拆解XTF从文件头到曲线数组结构弄清楚了代码就好写了。这章用Python实现一个XTF解析器代码按“骨架→头解析→块读取”三步展开。这里用的字段偏移是我在新版本文件上验证过的如果你的文件是旧版本偏移量要按实际数据调整。3.1 搭一个解析器骨架XTFReader类的职责划分解析器我用一个类来实现好处是状态集中文件句柄、头部字段、通道表都在一个对象里后续做批量处理也方便。类的方法划分_parse_header负责头部字节解释_parse_channels负责通道表读取read_blocks负责数据体循环get_curve把解析结果按通道名取出。import struct import numpy as np from dataclasses import dataclass from typing import List, Dict, Optional XTF_MAGIC bXTF\x00 HEADER_SIZE 256 CHANNEL_ENTRY_SIZE 64 BLOCK_HEADER_SIZE 32 BLOCK_TYPE_CURVE 1 BLOCK_TYPE_DEPTH_MARKER 2 BLOCK_TYPE_COMMENT 3 dataclass class ChannelInfo: channel_id: int name: str unit_code: int sample_type: int # 0: float32, 1: float64 dataclass class CurveBlock: start_depth: float num_samples: int data: np.ndarray # shape (num_samples, num_channels) class XTFReader: def __init__(self, filepath: str): self.filepath filepath self._fh open(filepath, rb) self.version: tuple (0, 0) self.well_name: str self.start_depth: float 0.0 self.end_depth: float 0.0 self.sample_step: float 0.0 self.channels: List[ChannelInfo] [] self.blocks: List[CurveBlock] []这个骨架把后续要用到的属性全部初始化了。version用二元组存储主次版本号channels列表保存通道信息blocks列表保存曲线数据块。参数filepath是唯一的构造参数传入XTF文件路径即可。类定义中的类型注解方便IDE做静态检查运行时不强制。3.2 头解析代码用struct.unpack处理二进制字段文件头解析的核心是struct.unpack。这里把前64字节按格式串解出处理完跳过剩余192字节保留区。版本号读出后主版本号为2以上的文件在头部多一个偏移字段需要用条件分支处理避免低版本文件因多读8字节导致通道表定位失败。HEADER_MAIN_FORMAT 4sHHQ8sdddQQ HEADER_MAIN_SIZE struct.calcsize(HEADER_MAIN_FORMAT) def _parse_header(self): self._fh.seek(0) raw self._fh.read(HEADER_SIZE) fields struct.unpack(HEADER_MAIN_FORMAT, raw[:HEADER_MAIN_SIZE]) magic, major, minor, timestamp, \ well_raw, self.start_depth, self.end_depth, \ self.sample_step, channel_count, data_offset fields if magic ! XTF_MAGIC: raise ValueError(f文件标识不正确: {magic!r}) self.version (major, minor) # 井名区域不足8字节时按截断处理末尾空字节去掉 self.well_name well_raw.split(b\x00)[0].decode(utf-8, errorsreplace) # 低版本文件没有数据块起始偏移默认跟随通道表 if major 2: self._data_offset data_offset else: self._data_offset None这段代码有几个要点。split(b\x00)[0]是为了截断定长字节里的空填充部分比如井名实际只有“A01”三个字符后5个字节是零直接解码会得到带空字符的字符串先按\x00切分再解码就是干净的文本。errorsreplace防止个别非UTF-8字符让整个解包崩溃遇到无法解码的字符会替换为占位符后面再根据井名可能存在的乱码情况做二次处理。data_offset在低版本文件里可能为零标记为None后后续定位数据块首地址时回退到“文件头结束位置通道表总长度”的计算方式而不是文件里写的偏移值。3.3 数据块循环读取CurveBlock到二维数组的还原通道表解析后进入数据体循环。循环条件很简单只要当前位置距离文件尾还够一个块头就读取块头判断类型按类型处理。曲线数据块的核心是把字节缓冲区转成二维数组先按“采样点数×通道数”申请内存再填充。BLOCK_HEADER_FORMAT IIdIdI # 字段顺序: 块类型(4) 块长度(4) 起始深度(8) # 采样点数(4) 时间戳(8) 校验和(4) def _read_curve_block(self, block_offset: int) - CurveBlock: self._fh.seek(block_offset) raw_header self._fh.read(BLOCK_HEADER_SIZE) btype, blen, start_depth, num_samples, timestamp, checksum \ struct.unpack(BLOCK_HEADER_FORMAT, raw_header) if btype ! BLOCK_TYPE_CURVE: raise ValueError(f块类型不匹配: {btype}) # 数据区大小 块总长度 - 块头大小 data_size blen - BLOCK_HEADER_SIZE expected num_samples * len(self.channels) * 4 if data_size expected: raise ValueError( f曲线数据长度不足: 块头声称{data_size}字节, f实际需要{expected}字节 ) raw_data self._fh.read(expected) arr np.frombuffer(raw_data, dtypenp.float32) arr arr.reshape(num_samples, len(self.channels)) return CurveBlock( start_depthstart_depth, num_samplesnum_samples, dataarr )这里的np.frombuffer比np.array(list(...))高效得多它直接复用字节缓冲区内存不产生Python列表中间对象。reshape(num_samples, len(self.channels))把一维数组解释成二维矩阵行对应采样点列对应通道顺序与通道表一致。data_size expected这个校验很关键文件末尾残缺的块经常在这暴露问题块头声称的长度比实际需要的数据长说明文件没写完整如果比需要的数据短说明通道表信息与数据体不匹配这就要回到通道表解析里找原因。主循环逻辑放在read_blocks方法中循环里维护当前偏移量遇到非曲线块就跳过并继续遇到曲线块就解析并追加到self.blocks。跳过非曲线块时要注意块类型是深度标记时块头里记录的“块长度”仍然有效照常按长度跳过即可不要额外加偏移否则下个块头位置就错位了。主循环代码def read_blocks(self): if self._data_offset is not None: offset self._data_offset else: # 低版本兜底: 文件头 通道表总长 offset HEADER_SIZE 4 len(self.channels) * CHANNEL_ENTRY_SIZE self._fh.seek(offset) while True: pos self._fh.tell() remaining self._fh.seek(0, 2) - pos if remaining BLOCK_HEADER_SIZE: break self._fh.seek(pos) raw_header self._fh.read(BLOCK_HEADER_SIZE) btype, blen, start_depth, num_samples, timestamp, checksum \ struct.unpack(BLOCK_HEADER_FORMAT, raw_header) if blen 0 or blen remaining: raise ValueError( f块长度异常: pos{pos}, len{blen}, remaining{remaining} ) if btype BLOCK_TYPE_CURVE: self._fh.seek(pos) block self._read_curve_block(pos) self.blocks.append(block) else: # 非曲线块整个跳过 self._fh.seek(pos blen)remaining 32判断文件剩余字节数是否还能容纳一个块头避免struct.unpack在读文件尾巴时报错。“块长度大于剩余字节数”说明文件被截断这时候直接抛错比把错误数据读进来更安全。非曲线块的跳过方式是“回到块头位置再走blen个字节”这样的写法逻辑更直观不容易漏掉块头自身长度。参数说明补充一句blen是“包含块头在内的总长度”。初次写解析器的人最容易在这个字段上栽跟头——把blen当成数据区长度去计算下一个偏移结果每个块都往后错32字节读出来的曲线全是乱的。这里的代码统一以pos blen作为下一块起始偏移。4. 读XTF文件避坑指南六条真实踩坑记录这章全部来自实际处理文件时的翻车经历。每一条都按“现象→原因→解决”来说明建议读者在调整自己解析器时对照排查。4.1 元信息解析三坑乱码、负数和错位第一条坑井名乱码。现象是解析出的井名显示为“锟斤拷”或一堆替换符文件在商业软件里显示的却是正常的汉字。原因是采集系统用GBK或GB2312编码写入通道名和井名Python默认按UTF-8解码必然出问题。解决方式是先按UTF-8解码失败后用gbk再解一次。实际代码里可以封装一个decode_name函数先尝试utf-8如果出现UnicodeDecodeError回退到gbk。第二条坑起始深度为负数。现象是文件头解出的start_depth是个极大的负数比如-9.4e-18或者深度上下限完全颠倒。原因通常是采样间隔字段读错位置把浮点位当成整数位解了或者文件头里深度字段用的单位与预期不一致。解决方式是先打印前64字节的每个字段值和软件显示的元数据逐项对比定位哪个字段偏移错位。我遇到过一次是因为没注意文件是“小端序”struct.unpack用了格式串所有数值全反了。第三条坑通道数量对不上。现象是商业软件显示12条曲线通道表读出来只有11条最后一条曲线在数据体重组时整列丢失。原因是其中一条通道的条目长度为96字节而代码里固定按64字节读跳过了半个条目。解决方式是读通道表时先解析每个条目的“条目长度”字段再按这个值跳转。有的版本在通道条目里带变长名称段条目实际长度需要重新计算。4.2 数据体处理三坑块校验、通道顺序和内存第四条坑校验和不匹配。现象是数据块读完后用块头里的校验和验证数据区算出来对不上但数据在软件里显示正常。原因有两个方向一是校验和的计算范围包含块头的部分字段而不仅仅是数据区需要先确定具体算法二是有些文件里的校验和字段根本没被填写永远是0属于“占位符”。解决方式是把“校验和不等于0”作为检查条件只有非零时才验证。这样既照顾了合法文件也能抓出数据被改动的问题。第五条坑通道顺序与预期不一致。现象是解析出的数组列数正确但画曲线时发现天然伽马和井径曲线位置互换和LAS文件里约定顺序不同。原因是文件头里通道表顺序是采集系统按硬件通道编号排的而现场工程师约定俗成的顺序是按曲线类型分组。解决方式是在通道表解析后加一个“重映射”步骤维护一个通道ID到逻辑列号的映射表。后续读取曲线块时按映射表把列重新排列。第六条坑大文件内存溢出。现象是文件三五百兆解析器运行到一半进程被系统杀掉。原因是read_blocks方法把所有曲线块都追加到self.blocks列表里整个文件的数据全部驻留在内存。解决方式是在read_blocks里增加回调函数on_block_read每解析完一个块就立即回调处理比如写盘或者画图并随时释放本地引用。这样内存占用峰值从“文件总大小”降到“单个块大小”大文件处理才稳定。def read_blocks(self, on_block_readNone): # ... 循环逻辑和上面一致 ... block self._read_curve_block(pos) if on_block_read is not None: on_block_read(block) else: self.blocks.append(block)这段代码体现了回调模式的核心效果调用方如果传入on_block_read数据块不缓存直接用回调消费不传回调则维持旧行为把块收集在列表里。参数on_block_read是函数对象签名是(CurveBlock) - None这样设计既能兼容小文件的旧调用方式又给大文件处理留了出路。5. 解析完怎么验证输出LAS标准格式并用Matplotlib交叉检查解析器写完不代表万事大吉下一步是把结果放到另一个软件里交叉验证这一步做得越早后面返工越少。验证方法有两条一条是把数据写成LAS标准格式用通用测井软件打开比对另一条是直接用Matplotlib画曲线和同事手里已有的PDF图对比形态。5.1 把结果写进LAS文件最小字段集与单位映射LAS是测井领域通用的文本交换格式。写LAS时不需要把所有字段都填全但版本、井名、深度曲线、通道名和单位这几项必须正确。最实用的做法是只写“必需块”和“曲线块”注释块可省。def write_las(filepath: str, curve_names: List[str], start_depth: float, step: float, data_matrix: np.ndarray): 把解析后的数据矩阵写成LAS 2.0文件 with open(filepath, w, encodingutf-8) as f: f.write(~VERSION 2.0\n) f.write(f WELL NAME: {curve_names[0]}\n) f.write(~WELL\n) f.write(f DEPTH: 起始深度 {start_depth}\n) f.write(~CURVE\n) f.write( DEPTH DEPTH m\n) for name in curve_names: f.write(f {name:24s} RAW \n) f.write(~A\n) for row in data_matrix: line .join(f{v:.4f} for v in row) f.write(f{row[0]:.4f} {line}\n)这段代码生成的LAS文件第一列是深度值后续列是曲线值。~VERSION块声明LAS版本~WELL块写井相关信息~CURVE块定义每列的“助记符、代码、单位”~A块之后是数据体。data_matrix的每一行对应一个采样深度点第一列是深度。单位这里简写为m和空字符串如果你解析的通道表里包含单位代码把对应单位填进~CURVE块里软件才能正确显示轴标签。写出LAS文件后用商业测井软件或开源工具打开看曲线量级、形态和深度是否与原始图一致。如果同一条曲线在两个软件里量级差100倍多半是单位换算没做如果曲线形态完全一样但深度移了一段回块头查起始深度和采样间隔。5.2 曲线图交叉验证深度对齐和曲线形态检查在没有商业软件可对比的情况下Matplotlib绘图是快速的验证方式。画图时选两条特征明显的曲线比如天然伽马和电阻率检查形态是否与同事给的PDF图吻合。import matplotlib.pyplot as plt def plot_curves(depths: np.ndarray, data: np.ndarray, channel_names: List[str], fig_path: str): 绘制测井曲线, 深度作为纵轴, 曲线从浅到深排列 n_curves data.shape[1] fig, axes plt.subplots( 1, n_curves, figsize(2.5 * n_curves, 10), shareyTrue ) for ax, i in zip(axes, range(n_curves)): ax.plot(data[:, i], depths, linewidth0.8) ax.set_title(channel_names[i], fontsize9) ax.grid(True, alpha0.3) ax.set_xlabel(Value) axes[0].set_ylabel(Depth (m)) axes[0].invert_yaxis() plt.tight_layout() plt.savefig(fig_path, dpi150)这个函数把每列曲线画在单独的子图里深度作为纵轴并倒置符合测井图惯例。调用前把depths按start_depth np.arange(num_points) * step生成即可。画出来如果曲线边缘毛刺特别多要怀疑数据体里混入了标记位的值如果曲线呈现周期的锯齿状要回看通道表的采样值类型很可能把float64按float32读了。我自己就碰上过一次这种怪象检查成锯齿后回头确认是通道表里有一条声波曲线是64位浮点修正后曲线立刻平滑了。验证这一步最怕的是“看着差不多就放过去”。曲线数量和深度点数完全一致但数值整体偏移这种错误最隐蔽靠肉眼看曲线形态很难发现。稳妥的做法是在LAS输出时额外输出一个_QA.txt文件记录每条曲线的均值、最小值和最大值拿这个和现场采集记录比对。数值范围对不上肯定有问题。6. 从单文件走向批量加个循环和日志工具才算完成解析单文件跑通后实际工作场景往往是几十口井的XTF文件等着处理。这时候除了批量循环还要做三件事异常隔离、日志记录和结果汇总。异常隔离保证一个坏文件不拖垮整套流程日志记录让失败文件可追溯结果汇总方便事后检查。import logging import glob import traceback logging.basicConfig( filenamextf_batch.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def process_single(xtf_path: str, out_dir: str) - bool: 处理单个XTF文件, 成功返回True, 失败返回False try: reader XTFReader(xtf_path) reader.read_blocks() # 生成深度数组和矩阵 n_points sum(b.num_samples for b in reader.blocks) data np.row_stack([b.data for b in reader.blocks]) depths float(reader.start_depth) np.arange(n_points) * float(reader.sample_step) # 输出LAS names [c.name for c in reader.channels] out_las out_dir / os.path.basename(xtf_path).replace(.xtf, .las) write_las(out_las, names, float(reader.start_depth), float(reader.sample_step), data) logging.info(fOK {xtf_path} - {out_las}) return True except Exception: logging.error(fFAIL {xtf_path}\n{traceback.format_exc()}) return False def batch_processing(glob_pattern: str, out_dir: str): 批量入口, 支持通配符匹配多个文件 paths sorted(glob.glob(glob_pattern)) ok_count 0 for p in paths: if process_single(p, out_dir): ok_count 1 logging.info(fDONE total{len(paths)} ok{ok_count} fail{len(paths)-ok_count})批处理代码有三个值得注意的参数。glob_pattern支持shell通配符比如./data/*.xtf写死在代码里也行但做成参数灵活些。out_dir是输出目录建议按“处理日期项目名”新建避免覆盖上一版结果。logging的级别设成INFO单文件成功后写一行OK失败写FAIL并带上完整堆栈方便凌晨批量处理跑完后查看失败原因。做完错得多的那一批文件我养成了一个习惯批量任务结束后先打开xtf_batch.log搜一遍FAIL再去翻失败的原始文件。日志里堆栈打印的往往是ValueError原因是块长度异常说明文件写到一半就断了。这类文件商业软件可能也打不开只能回头找甲方重新导出一份。从那以后我每次批量处理前都会先跑一遍单文件解析确认格式版本没问题再开全量流程省下的返工时间远比测试时间多。希望这套从格式解读到批量落地的思路能帮到你毕竟测井数据处理的复杂度从来不在算法而在那些不写进文档的格式细节里。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑