资讯动态

WITSML代码包实战:XSD校验、SOAP接口与增量同步

发布时间:2026/10/25 12:25:11 来源:尧图企业网站定制
简介面向C#开发者与石油天然气行业数据集成人员的WITSML实践代码包围绕井数据交换标准展开可用于理解井筒数据模型、简单对象访问协议及可扩展标记语言序列化在项目中的落地方式。压缩包共52个文件、约74KB以9个C#源码为核心配合6个配置、2个解决方案、3个可执行程序与3个动态库以及服务描述和接口文件覆盖客户端调用、网络服务端和序列化类库等模块便于梳理服务代理、端点绑定与数据传输流程。已有855人学习下载说明该主题在井筒数据共享与系统对接场景中具有稳定需求。代码中可参考井、钻柱等实体的类表示可扩展标记语言序列化与反序列化示例以及简单对象访问协议服务代理的创建方式适合需要快速建立该标准集成认知、对照调试接口与补充项目结构思路的中级C#开发者。1. 拿到 WITSML 代码包先别急着找 API它其实是三件套拼起来的上周有个做井场数据采集的朋友把一份日报 XML 拖进编辑器标签一眼都认识改完往服务端一提交返回里suppMsgOut写着 success结果库里一条没进。这类翻车在 WITSML 相关代码包里几乎天天发生它看着像一套 XML 模板实际是三件东西叠在一起——一组用 XSD 钉死的对象模型、一套 SOAP 风格的服务接口、外加一门“填了值就当过滤条件”的查询模板语法。资源里的 schema 目录、示例报文和调用样例正好对应这三层。做钻井日报入库、测井曲线对接、井眼轨迹同步的工程师拿到它最大的价值不是多一份样例而是能把校验、构造、调用这条链在本机跑通一遍再去连真实服务端。适合已经会读写 XML、但还没被uidWell链和单位属性折磨过的人。2. 数据对象与 XSD 校验把 well/wellbore/log 的父子关系钉死WITSML 的数据不是平铺的表格而是一棵树。理解这棵树的引用方式比背字段名重要得多。1.4.1 这一代用的是“逐级 uid 引用”顶层对象只有自己的uid往下每挂一层就多带一个祖先的 uid比如 log 必须同时写uidWell、uidWellbore、uid。2.0 那一代换成了全局uuid加链接关系对象名首字母大写还多了 JSON 表达。代码包里如果两种版本的样例混在一起第一件事是按目录分开别在同一个校验脚本里跑。2.1 对象模型四个层级与引用链先把常见对象和它们的挂载位置列清楚后面写查询模板时就是照着这张表填 uid。对象挂载层级关键字段典型用途well顶层name、country、timeZone井主数据uid 全局唯一wellborewell 下uidWell、name、statusWellbore井筒、侧钻分支logwellbore 下indexType、indexCurve、logCurveInfo、logData测井、随钻、录井曲线trajectorywellbore 下trajectoryStation(md/incl/azi/tvd)井眼轨迹tubularwellbore 下tubularComponent管柱结构、钻具组合messagewell 或 wellbore 下messageText、dTim井场事件、日报条目reportwell 或 wellbore 下日报表单字段日报数据注意uid是服务端内唯一的字符串不是自增数字。很多团队图省事用1、2当 uid第一次合并两家服务商的数据就撞车血泪经验uid 里带上来源前缀比如SRC-W-0001后期做数据融合会轻松很多。2.2 用 XSD 先把样例报文验一遍schema 目录是这份资源里最不该被跳过的东西。它决定了你拼出来的报文到底合不合法而服务端的报错往往只说“invalid query”不会告诉你哪一行错了。本机先验一遍能省掉大半天来回。常见做法是用 lxml 把每个 xsd 编译成XMLSchema对象再对样例逐个校验。from pathlib import Path from lxml import etree SCHEMA_DIR Path(witsml/schemas) # schema 目录1.4.1 与 2.0 建议分开放 DOC_DIR Path(witsml/samples) # 待校验的样例报文目录 # 1) 预先编译所有 schema避免每个文件重复解析 schemas {} for xsd in SCHEMA_DIR.glob(*.xsd): try: schemas[xsd.name] etree.XMLSchema(etree.parse(str(xsd))) except etree.XMLSchemaParseError as e: print(f[SCHEMA-ERR] {xsd.name}: {e}) # 2) 逐个校验样例按根节点名匹配对象类型 for doc in sorted(DOC_DIR.glob(*.xml)): tree etree.parse(str(doc)) root tree.getroot() obj root.tag.split(})[-1] # 命名空间被剥掉得到 logs / wells 这类复数根 if not obj.endswith(s): print(f[SKIP] {doc.name}: 根节点 {obj} 不是集合类型) continue cand [n for n in schemas if obj[:-1] in n] # obj_log.xsd 这类命名 if not cand: print(f[SKIP] {doc.name}: 未找到对应 schema) continue schema schemas[cand[0]] if schema.validate(tree): print(f[OK] {doc.name}) else: print(f[FAIL] {doc.name}) for err in schema.error_log: print(f line {err.line}: {err.message})代码逻辑先用XMLSchema(etree.parse(...))预编译因为 xsd 之间会互相 import编译一次复用比每次现编快一个量级obj[:-1]把logs削成log再去匹配文件名error_log里的line是相对样例文件的行号直接跳过去看就行。参数说明SCHEMA_DIR要放原始 xsd 全集缺一个 import 就会在编译阶段抛XMLSchemaParseError这时候看报错里的文件名就知道缺哪个如果你的代码包把 1.3.1 和 1.4.1 混在同一目录cand可能匹配到错版本建议按版本号建子目录。校验通过不代表服务端一定接受但校验不通过的一定会被拒。2.3 校验失败时先看哪三行报错信息量经常很大按优先级看三处就够了。第一看行号附近的元素名是不是多了一层不该有的包裹节点第二看uom属性1.4.1 里数值型元素基本都要求带单位缺了直接判不合法第三看日期格式dTim这类字段要求带时区的 ISO8601写成2024-05-01 08:00:00一定过不去得写成2024-05-01T08:00:00Z或带08:00偏移。还有一种不报错但很坑的情况样例文件里带了xsi:schemaLocation指向某个在线地址lxml 离线时会尝试拉取然后失败。解决办法是校验前把schemaLocation属性删掉或者用etree.XMLParser(no_networkTrue)强制离线。我一般会写个小工具函数pop掉这个属性再编译省得每次都手动改。3. 报文构造与解析用 Python 拼一份能过校验的 log 对象校验脚本跑通之后第二步是反过来——从数据库里取数拼成合法报文。这一步最容易出问题的地方不在数值而在结构顺序logCurveInfo的排列顺序和logData/data里每一列的排列顺序必须严格一致错一列整条曲线就串了而且服务端不会报错。3.1 indexType 决定 log 对象的骨架log 的索引类型只有两类思路按深度measured depth、true vertical depth和按时间date time。索引类型一换startIndex/endIndex的语义、单位、以及下游怎么画图全变。按深度索引的曲线startIndex和endIndex是深度值单位通常是m或ft按时间索引的曲线这两个字段是时间戳单位留空或写date time。indexCurve字段填索引曲线自己的助记符比如DEPTH这个助记符必须同时出现在logCurveInfo的第一条里。曾经见过有人把indexCurve写成MD、把logCurveInfo里写成DEPTH校验通过、入库成功但下游软件一律读不出来——典型的黑匣子问题。3.2 用 ElementTree 拼一份能过校验的 log下面的写法用 lxml 的nsmap把默认命名空间挂在根节点上子元素统一走q()拼标签避免手写命名空间前缀写到崩溃。from pathlib import Path from lxml import etree NS http://www.witsml.org/schemas/1series VERSION 1.4.1.1 # 与 schema 版本对应别乱填 def q(tag: str) - str: return f{{{NS}}}{tag} # 根节点挂默认命名空间子元素会自动继承 logs etree.Element(q(logs), nsmap{None: NS}, versionVERSION) log etree.SubElement(logs, q(log)) log.set(uidWell, SRC-W-0001) log.set(uidWellbore, SRC-WB-0001) log.set(uid, SRC-LOG-0001) def sub(parent, tag, textNone, **attrs): el etree.SubElement(parent, q(tag)) for k, v in attrs.items(): el.set(k, v) if text is not None: el.text str(text) return el sub(log, nameWell, 示例井-1) sub(log, nameWellbore, 示例井筒-1) sub(log, name, 随钻曲线-深度索引) sub(log, indexType, measured depth) sub(log, startIndex, 1000.0, uomm) sub(log, endIndex, 1002.0, uomm) sub(log, indexCurve, DEPTH) # 必须与第一条 logCurveInfo 的助记符一致 CURVES [(DEPTH, m), (GR, gAPI), (RHOB, g/cm3)] for mnem, unit in CURVES: lci etree.SubElement(log, q(logCurveInfo), uidfLCI-{mnem}) sub(lci, mnemonic, mnem) sub(lci, unit, unit) rows [(1000.0, 45.2, 2.31), (1001.0, 46.7, 2.29), (1002.0, 47.1, 2.28)] data etree.SubElement(etree.SubElement(log, q(logData)), q(data)) data.text \n.join(,.join(f{v:.3f} for v in r) for r in rows) Path(out).mkdir(exist_okTrue) Path(out/log_001.xml).write_bytes( etree.tostring(logs, pretty_printTrue, xml_declarationTrue, encodingUTF-8) )逻辑说明nsmap{None: NS}让根节点用默认命名空间序列化出来就是干净的logs xmlns...不会带ns0:前缀。log元素的三个 uid 属性是引用链缺一个服务端就无法定位。曲线元数据先写logData最后写顺序和 schema 里的元素顺序一致。参数说明version属性填的是完整的 schema 版本号1.4.1 系列常见写法是1.4.1.1和你SCHEMA_DIR里的 xsd 对上就行uom用行业通用单位符号gAPI、g/cm3、m这些都别自己造data.text里逗号分隔不要空格不要结尾换行某些服务端解析结尾空行会多出一列 NaN。拼完之后务必回到第 2 章的脚本再跑一次校验把“构造”和“校验”串成一个流程比事后到服务端排错快得多。3.3 把返回的 Result 解析成行列表WMLS_GetFromStore的返回里真正的数据在Result元素里而且是一段被转义过的 XML 字符串需要二次解析。直接拿正则抠标签是最容易埋雷的做法。from lxml import etree def parse_logs_result(result_xml: str): 把 WMLS_GetFromStore 的 Result 字段解析成结构化行数据 root etree.fromstring(result_xml.encode(utf-8)) ns {w: http://www.witsml.org/schemas/1series} out [] for log in root.findall(.//w:log, ns): mnems [e.text for e in log.findall(.//w:logCurveInfo/w:mnemonic, ns)] units [e.text for e in log.findall(.//w:logCurveInfo/w:unit, ns)] rows [] for block in log.findall(.//w:logData/w:data, ns): for line in (block.text or ).strip().splitlines(): vals [v.strip() for v in line.split(,)] if len(vals) ! len(mnems): # 列数对不上就是丢列了宁可抛错也别静默塞 None raise ValueError(f列数不匹配: {len(vals)} vs {len(mnems)}) rows.append(dict(zip(mnems, vals))) out.append({uid: log.get(uid), units: dict(zip(mnems, units)), mnemonics: mnems, rows: rows}) return out逻辑说明findall(.//w:log)能一次拿到多个对象因为一个返回包里可能有多口井的曲线先取助记符列表再逐行zip保证字典的键和单位一一对应。列数校验是关键服务端返回带缺失值的行时容易出现空列静默处理会让后续的单位换算全部错位。参数说明命名空间必须和请求的版本一致查 1.4.1 就用1series查 2.0 要换成对应的 2.0 命名空间w:前缀只是本地别名写什么都行但xmlns必须对。4. SOAP 接口对接WMLS_GetFromStore 的请求模板与 optionsIn 调参接口这一层WITSML 1.4.1 走的是 SOAP over HTTP所有操作都是 POST 到同一个端点靠SOAPAction头区分动作。认证常见做法是 HTTP Basic Auth少数服务端支持把凭证放进专门的头部元素先问清楚再动手。4.1 SOAP 信封与 SOAPAction 的对应关系请求体结构很固定一个信封包一个 BodyBody 里是WMLS_GetFromStore这类动作元素里面几个必填子元素——WMLtypeIn填对象类型log、well、trajectoryQueryIn放模板 XML 字符串OptionsIn放逗号分隔的选项CapabilitiesIn一般留空。SOAPAction和命名空间里的版本号要一致。1.4.1 系常见是http://www.witsml.org/action/140/Store.WMLS_GetFromStore1.3.1 系是120。这两个数字对不上服务端会回一个看起来像 500 但其实什么都没干的响应非常容易误判成数据不存在。4.2 一个能直接跑通的调用import requests from lxml import etree from requests.auth import HTTPBasicAuth ENDPOINT https://witsml.example.com/witsml/store # 以服务端 GetCap 给出的地址为准 AUTH HTTPBasicAuth(user, pass) SOAP_ACTION http://www.witsml.org/action/140/Store.WMLS_GetFromStore def build_envelope(query_xml: str, options: str, wml_type: str log) - str: # QueryIn 是字符串类型把模板 XML 转义后放进去别直接嵌原始标签 esc (query_xml.replace(, amp;).replace(, lt;).replace(, gt;)) return f?xml version1.0 encodingUTF-8? soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ soapenv:Body WMLS_GetFromStore xmlnshttp://www.witsml.org/wsdl/140/witsml WMLtypeIn{wml_type}/WMLtypeIn QueryIn{esc}/QueryIn OptionsIn{options}/OptionsIn CapabilitiesIn/ /WMLS_GetFromStore /soapenv:Body /soapenv:Envelope QUERY logs xmlnshttp://www.witsml.org/schemas/1series version1.4.1.1 log uidWellSRC-W-0001 uidWellboreSRC-WB-0001 uidSRC-LOG-0001/ /logs body build_envelope(QUERY, returnElementsall) resp requests.post(ENDPOINT, databody.encode(utf-8), headers{Content-Type: text/xml;charsetUTF-8, SOAPAction: SOAP_ACTION}, authAUTH, timeout(5, 120)) resp.raise_for_status() env etree.fromstring(resp.content) ns {s: http://schemas.xmlsoap.org/soap/envelope/, w: http://www.witsml.org/wsdl/140/witsml} body_el env.find(.//s:Body, ns) fault body_el.find(s:Fault, ns) if fault is not None: raise RuntimeError(f服务端 Fault: {fault.findtext(faultstring)}) node body_el.find(.//w:WMLS_GetFromStoreResponse, ns) result node.findtext(Result, default, namespacesns) supp node.findtext(SuppMsgOut, default, namespacesns) print(补充信息:, supp) # 1 表示有数据0 或负数通常意味着没命中逻辑说明先手工做 XML 转义再塞进QueryIn比用 CDATA 稳妥因为部分服务端的解析器对 CDATA 处理不一致。响应要先查Fault有些实现会用 HTTP 200 返回一个 Fault 体raise_for_status()拦不住。Result拿到字符串后再交给第 3 章的解析函数。参数说明timeout拆成连接和读取两段井数多的时候读取超时给到 120 秒不算多WMLtypeIn必须和模板根节点对应查logs就填单数logSuppMsgOut是服务端的补充返回码出现异常值时优先看它而不是看数据量。4.3 optionsIn 四个常用参数的取舍OptionsIn是调优的主战场全部塞returnElementsall在对象少的时候没事井一多就等着超时。参数常用取值实际影响returnElementsall / id-only / requested决定返回全部字段、仅标识、还是模板里显式声明的字段maxReturnNodes整数如 200单次返回对象数上限超了会截断必须配合分页requestObjectSelectionCapabilitytrue探测服务端支持哪些对象对接新服务端时先跑一次returnHeadertrue / false是否需要返回对象头信息做增量同步时有用实践里的组合是:第一次接触某台服务端先用returnElementsid-only拉一遍 uid 列表确认连通性和数据规模写业务逻辑时改成requested只在模板里声明真正要的字段能显著压缩响应体批量导出才用all并且务必带上maxReturnNodes。id-only返回的字段很少别指望它带出曲线数据这是新手最容易误解的一档。5. 避坑与排查五个让报文静默失败的坑这些坑的共同点是服务端不报错、返回码正常、数据却没有。排查顺序建议从报文层到接口层逐层缩小。5.1 报文层面的三个静默失败现象一入库成功但下游读不出曲线。原因indexCurve的值和第一条logCurveInfo的mnemonic不一致。解决写一个断言构造完报文后立刻校验二者相等把检查卡在发送之前别等下游反馈。现象二校验通过服务端回 invalid query。原因命名空间版本和version属性不匹配比如模板里写 1.4.1 的命名空间却标了version1.3.1。解决把命名空间、version属性、SOAPAction里的版本号三者做成同一个常量从配置读不要三处手写。现象三时间字段全被拒。原因dTim不带时区。解决统一用 UTC 的Z后缀输出服务端再按timeZone字段做本地化展示。我一般会在数据出口加一个格式化函数任何时间字段都强制走它避免有人顺手str(datetime.now())。5.2 接口层面的两个超时与认证坑现象四响应 200、body 里是 Fault日志里只记了状态码。原因把 HTTP 状态码当成业务结果。解决封装一个统一的响应处理函数先解析 Fault再解析SuppMsgOut最后才看Result三层都过了才算成功。现象五单井数据能拉整批就超时或截断。原因没设maxReturnNodes一次请求把整库对象都往回调。解决按maxReturnNodes加 uid 游标分页每页拉完立即落盘再做下一批同时把读取超时调大别用默认的几秒。还有一条容易被忽略不同服务端对QueryIn里“填了值就当过滤条件”的支持程度不一样。同一个模板A 服务端返回精确命中的一条B 服务端返回全表。对接新服务端时先跑requestObjectSelectionCapabilitytrue摸清能力再决定是客户端过滤还是服务端过滤。6. 增量同步与曲线对齐把 dTimLastChange 用起来的两个技巧全量拉取在开发阶段够用上了生产就得换增量。WITSML 的多数可更新对象都带一个由服务端维护的dTimLastChange它是做增量最省事的抓手。第一种做法是服务端过滤在查询模板的对象里把commonData/dTimLastChange填上一个起始时间语义就是“只要这个时间之后变更过的”。注意这条依赖服务端实现先在测试环境用已知的修改记录验证一次别直接上线。QUERY_INCR logs xmlnshttp://www.witsml.org/schemas/1series version1.4.1.1 log uidWellSRC-W-0001 uidWellboreSRC-WB-0001 commonDatadTimLastChange2024-05-01T00:00:00Z/dTimLastChange/commonData /log /logs第二种做法是客户端比对先用returnElementsid-only拉全量 uid 和变更时间本地做 diff只对变化的 uid 发详细查询。这种方式对服务端实现依赖小代价是每次多一次轻量请求。井数上千的场景我一般选第二种把 diff 结果落一张本地表失败重试也好做。曲线对齐是另一个高频问题。两条曲线来自不同服务商索引单位一个是m一个是ft或者采样点密度的差异导致同一深度上有多个值。落地检查就两条一是统一索引单位后再合并别在原始单位上做插值二是同一索引值出现多条记录时按dTimLastChange取最新而不是简单去重取第一条。单位换算写成显式函数并记录换算系数到日志里出问题时能一眼看出是哪一步引入的偏差。还有个实用技巧把每次请求的WMLtypeIn、OptionsIn、SuppMsgOut、返回对象数打成一行结构化日志出问题时不用复现翻日志就知道当时发了什么。从那以后我每次接新服务端第一件事都是先跑一遍版本查询和能力查询把版本号和对象支持清单打进日志再动手写业务查询——这条习惯帮我省掉过好几次“明明昨天还能跑”的深夜排查。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑