☰
HTRI二次开发教程(06):Automation Server 探测(下)——对象树测绘与探测清单
2026/9/30 11:22:23 网站建设 项目流程

HTRI二次开发教程(06):Automation Server 探测(下)——对象树测绘与探测清单

版本与事实声明

  • 版本锚点:当前Xchanger Suite 9.4;Automation Server 为随软件安装的 OLE 自动化服务器。
  • 仍不写任何具体 ProgID/类名/方法名:所有标识符来自本机探测,示例用占位符。官方对象模型文档在会员门户内,公开不可得。
  • 文中所有数值均为示例性建模,不代表任何标准规定。

一句话结论:第 05 篇拿到的是"平面成员清单",第 06 篇要把它变成"可导航的对象树"——用 pywin32 早期绑定对象沿case → panel → field递归dir(),把属性/方法/子对象分类导出成 JSON 测绘报告,再用它回填数据字典的real_identifier列;这份测绘报告是后续所有自动化寻址的唯一合法地图。

〇、本篇要解决的认知问题

  • Q1:为什么"成员清单"不够用,必须升级成"对象树"?
  • Q2:怎么区分一个成员是属性、方法,还是子对象?
  • Q3:递归遍历时,哪些成员是"脚手架"(不该深入)、哪些是"业务节点"(应该深入)?
  • Q4:怎么安全地探索未知成员,不至于在遍历时把程序搞崩?
  • Q5:测绘结果怎么与第 04 篇的datadict.csv对接,形成"规范路径 → 真实标识符"的映射?

一、机制解析

1.1 从清单到树

为什么这对你重要:自动化寻址是路径,不是名字。第 05 篇的probe_members.txt告诉你"有个叫 X 的成员",但没告诉你"X 挂在谁下面"。而第 04 篇的心智模型要求我们沿case → 面板 → 字段走路径——overall_u可能只在outputs.summary下存在,而process下不存在。同名成员出现在不同父节点下,语义完全不同。

对象树测绘要回答三个问题:

  1. 这棵树的根是什么(Application/Case?);
  2. 每一层有哪些子对象(可继续深入);
  3. 每个子对象下有哪些叶子字段(不可深入、可直接读写)。

1.2 属性、方法与子对象

在 pywin32 早期绑定对象上,dir()返回的每个名字属于三类之一:

类别判据处理方式
属性/字段取值返回标量(数/字符串/布尔)或简单结构登记为叶子
方法可调用(callable(...)),调用后可能改变状态登记为动作(如 run/save)
子对象取值返回另一个 OLE 对象(有_oleobj_等)递归深入
脚手架名前缀_(_oleobj_、_comobj_、_ApplyTypes_、_NewEnum…)跳过,不深入

反直觉点:_NewEnum常常是实现"集合遍历"的钩子——如果某个节点是"案例集合"或"面板集合",_NewEnum才是你遍历它的正道,但它的名字带下划线。所以"跳过_前缀"是默认策略,但对集合节点要例外处理(先看该节点是否像集合)。

1.3 安全探索:只读、限额、容错

遍历一个你并不完全了解的 COM 对象树,工程上有三条护栏:

  1. 只读优先:优先访问"看起来像属性"的成员;绝不在探测阶段调用"看起来像动作"的方法(run/save/delete 之类)。
  2. 深度与广度限额:设置max_depth、max_children,避免对象图有环或极深时把探测脚本挂死。
  3. 异常吞掉并记录:某个成员取值抛异常(需要参数、权限不足),记"error"并继续,不要让整棵树测绘中断。

最佳实践:探测阶段先从"最小可用动作"开始——只dir()不调用,确认根对象稳定后,再逐层深入一层、验证一层。"一次遍历到底"是把程序搞崩的经典方式。

1.4 测绘报告与数据字典对接

第 04 篇的datadict.csv有规范路径(geometry.exchanger.shell_id)与空的real_identifier列。第 06 篇的测绘报告给出真实树的节点名。对接方式:

  • 规范路径geometry.exchanger.shell_id→ 在测绘报告里找"父链像 exchanger、叶子像 shell id"的节点 → 把该叶子的真实节点名填进映射表;
  • 若找不到对应节点,标记"待确认",不猜。

1.5 测绘产物如何进入工程

测绘不是一次性动作,而是平台的一个层(第 20 篇的 L1 探测层)。它的产物在工程里的三条用法:

  1. 生成数据字典的实名列:把mapping_suggest.csv中人工确认的accepted_real_identifier回填datadict.csv的real_identifier——业务代码从此只引用规范路径(第 04 篇);
  2. 当"字段存在性"的事实源:跨模块核对(第 11 篇)、导入核对(第 13 篇)都要回答"这个字段在本机存不存在",测绘报告就是答案;
  3. 当"版本漂移"的探测器:升级 Xchanger Suite 后重测,对比新旧object_tree.json的节点差异——哪些字段新增、哪些改名,一目了然(9.4 新增了温度有效度等输出,正是这类差异)。

最佳实践:把object_tree.json纳入版本管理。这样"从哪个版本开始多了温度有效度输出""哪个版本改了某面板结构"都能在 git log 里查到——测绘报告不只是一张图,它是对象树的时间机器。

一条纪律:测绘报告只记录结构与类型,不记录业务值(1.3 节已落实在代码里)。案例里可能含业主工艺数据,探测产物绝不能成为数据外泄的渠道;交付或共享探测报告前,仍应人工过一遍。

1.6 测绘与"自动化寻址"的关系

为什么这对你重要:很多人以为测绘只是"看一眼对象有哪些方法"。其实测绘的真正产出,是自动化寻址的路径表。

把三件事串起来看:

  • 第 04 篇给出了"应然"的路径(规范路径,如geometry.exchanger.shell_id);
  • 本篇给出了"实然"的路径(本机对象树里的真实节点链);
  • 映射表(accepted_real_identifier)把两者连起来。

于是业务代码的寻址变成一句:“按规范路径查映射表,按映射表的分段名逐层下降”(第 07 篇set_field里的for part in entry["ident"].split(".")就是这个动作)。探测的价值,最终凝结在这条"逐层下降"的循环里。

一条反直觉结论:“测绘得越全"不等于"代码写得越顺”。测绘给你的是候选空间,而代码需要的是最小必要路径。真正高效的自动化代码,往往只用了对象树里很小的一部分节点——关键不是把树摸清,而是准确定位到你需要的那几个叶子。

二、完整代码与逐行剖析

代码 6-1:对象树递归测绘(占位 ProgID,只读)

# -*- coding: utf-8 -*-""" probe_object_tree.py —— 递归测绘 Automation Server 对象树 用法:python probe_object_tree.py 【重要】PROGID 必须替换为本机探测所得真实值。 只读探测:不调用任何看起来像"动作"的方法(run/save/delete)。 """importjsonimportdatetimeimportwin32com.clientaswc PROGID="<HTRIAutomationServer.ProgID(本机枚举所得)>"MAX_DEPTH=3# 最大递归深度(先浅后深)MAX_CHILDREN=40# 每层最多展开的子节点数SKIP_PREFIX="_"# 脚手架前缀deflooks_like_object(v):"""判断一个取值是否像 OLE 子对象。"""returnhasattr(v,"_oleobj_")orhasattr(v,"_comobj_")defis_collection(node):"""集合节点通常实现 _NewEnum,可用 for ... in 遍历。"""returnhasattr(node,"_NewEnum")defclassify(v):iflooks_like_object(v):return"object"ifcallable(v):return"method"return"value"defwalk(name,node,depth,report,path=""):"""递归测绘;report 为可变列表,收集节点记录。"""ifdepth>MAX_DEPTH:returnmembers=[mformindir(node)ifnotm.startswith(SKIP_PREFIX)]forminsorted(members)[:MAX_CHILDREN]:child_path=f"{path}.{m}"ifpathelsemtry:v=getattr(node,m)# 只取值;不调用exceptExceptionase:# noqa: BLE001 —— 探测阶段吞异常并记录report.append({"path":child_path,"kind":"error","detail":f"{type(e).__name__}:{e}","depth":depth})continuekind=classify(v)rec={"path":child_path,"kind":kind,"depth":depth}ifkind=="value":# 只记录类型与是否标量,不记录可能敏感的业务内容rec["type"]=type(v).__name__ report.append(rec)ifkind=="object":walk(m,v,depth+1,report,child_path)defmain():ifPROGID.startswith("<"):print("[停止] PROGID 仍是占位符。请先完成第 05 篇探测并替换。")returnapp=wc.gencache.EnsureDispatch(PROGID)report=[]walk("root",app,1,report)out={"progid":PROGID,"probed_at":datetime.datetime.now().isoformat(timespec="seconds"),"max_depth":MAX_DEPTH,"nodes":report,}withopen("object_tree.json","w",encoding="utf-8")asf:json.dump(out,f,ensure_ascii=False,indent=2)by_kind={}forrinreport:by_kind[r["kind"]]=by_kind.get(r["kind"],0)+1print(f"测绘完成:{len(report)}个节点 -> object_tree.json")print("节点类别统计:",by_kind)if__name__=="__main__":main()

逐行剖析:

  • MAX_DEPTH=3起步、MAX_CHILDREN=40:先浅后深。对象图可能有环(父节点引用子节点、子节点又指回父),不限深会无限递归。第一轮只测绘三层,确认结构后再调大。
  • SKIP_PREFIX="_"与is_collection:默认跳过脚手架,但提供集合判定函数(1.2 节的例外)。第一版先不展开集合,避免一次拉入成百上千项。
  • getattr(node, m)只取值、不调用:classify里callable(v)只判断可调用性,探测阶段绝不v()——一旦调了run或save,你的案例就被改了。
  • 异常处理记"error"节点并continue:有些属性需要参数(如按索引取项)或权限,取值会抛异常;吞掉并记录,保证测绘完整。
  • 隐私与安全考虑:叶子只记录type(v).__name__,不记录值本身——案例里可能含业主数据与工艺参数,探测产物不应成为数据泄露渠道。
  • 落盘object_tree.json带progid与probed_at:与第 05 篇探测清单同源,构成完整的"版本—探测—测绘"证据链。

代码 6-2:把测绘结果回填进数据字典

# -*- coding: utf-8 -*-""" map_to_datadict.py —— 用对象树测绘结果辅助回填 datadict.csv 的 real_identifier 用法:python map_to_datadict.py 策略:按"叶子规范名"在测绘报告中做模糊匹配,给出候选真实节点名; 不做自动断言——只生成"候选映射建议表"供人工确认。 """importjsonimportcsvimportredefload_tree(path="object_tree.json"):withopen(path,encoding="utf-8")asf:returnjson.load(f)deftokenize(name):returnset(re.split(r"[._\s\-]+",name.lower()))defsuggest(leaf_norm,report):"""返回若干候选(按 token 重合度排序)。"""want=tokenize(leaf_norm)scored=[]forrinreport["nodes"]:ifr["kind"]!="value":continuegot=tokenize(r["path"].split(".")[-1])score=len(want&got)ifscore:scored.append((score,r["path"]))scored.sort(reverse=True)return[pfor_,pinscored[:3]]defmain():report=load_tree()try:rows=list(csv.DictReader(open("datadict.csv",encoding="utf-8-sig")))exceptFileNotFoundError:print("未找到 datadict.csv,请先运行第 04 篇 gen_datadict.py。")returnout_rows=[]forrowinrows:norm=f'{row["group"]}.{row["field"]}'cands=suggest(norm,report)out_rows.append({"norm_path":norm,"unit":row["unit"],"real_identifier_candidates":" || ".join(cands),"accepted_real_identifier":"",# 人工确认后填写})withopen("mapping_suggest.csv","w",newline="",encoding="utf-8-sig")asf:w=csv.DictWriter(f,fieldnames=list(out_rows[0].keys()))w.writeheader()w.writerows(out_rows)print(f"已生成 mapping_suggest.csv({len(out_rows)}行候选映射)")print("请人工确认 accepted_real_identifier 列;未确认的条目不得用于编码。")if__name__=="__main__":main()

逐行剖析:

  • 匹配用token 集合交集而不是字符串相等:真实节点名(可能含前缀、大小写、分隔符不同)与规范名很难字面相等,按语义 token 比对更鲁棒。
  • 只取kind == "value"的叶子:规范路径指向字段,不该匹配到"对象"或"方法"。
  • 输出accepted_real_identifier留空:脚本只给候选,人工确认才算数——"机器建议、人定案"是本系列处理不确定性的一贯原则(呼应底账 U2)。
  • 打印明确纪律:“未确认的条目不得用于编码”。把红线写在脚本输出里,比只写在文档里更有效。
  • 与datadict.csv用utf-8-sig同编码读写,避免中文乱码。

三、常见报错与排查

报错 3-1:递归测绘把内存吃满 / 长时间不返回。
现象:脚本卡住、内存飙升。根因:对象图有环或节点极多,MAX_DEPTH/MAX_CHILDREN没设或设太大。解法:把MAX_DEPTH从 1 开始逐层加,MAX_CHILDREN压到 20 以内先看结构;对集合节点先不展开。

报错 3-2:某个成员getattr抛异常,测绘中断。
现象:遍历到一半崩了。根因:属性需要参数(按索引取项)、或权限不足、或该成员是"按需生成"。解法:代码 6-1 已用try/except记录"error"节点并继续;若你自行改写,务必保留异常吞掉。

报错 3-3:误调用了动作方法,案例被改。
现象:探测后打开案例发现输入变了。根因:把"像属性其实要调用"的成员调了,或对method类成员做了调用。解法:探测阶段只 getattr 不 call;classify只判断callable。若已误改,从备份恢复案例——探测前务必备份或使用副本。

报错 3-4:_NewEnum明明存在,却遍历不到集合项。
现象:想遍历案例集合却拿不到元素。根因:_前缀默认被跳过,而集合遍历恰恰靠_NewEnum。解法:对判定为集合的节点(is_collection为真)例外处理,用 Python 的for item in node:触发_NewEnum,但仍要限额(先取前 N 个)。

报错 3-5:测绘报告里的路径与本机不一致,映射对不上。
现象:mapping_suggest.csv候选全是空。根因:测绘深度不够(字段在更深层),或该字段确实不存在于当前模块/模式。解法:加深MAX_DEPTH重测;若确认不存在,按铁律 1不猜,标记"待确认"并检查模块与模式是否正确。

报错 3-6:测绘报告被误当作"可用字段全集"直接编码。
现象:代码引用了报告里存在、但语义无关的节点。根因:测绘报告是"结构与类型清单",不是"业务字段契约"。解法:把确认后的标识符回填datadict.csv的real_identifier,业务代码只引用规范路径;报告本身只作事实源,不作编码字典。

四、动手练习

  • 练习 1(首轮测绘):用真实 ProgID 运行代码 6-1(MAX_DEPTH=1)。判定:生成object_tree.json,且打印出节点类别统计;确认其中没有kind == "method"的节点被调用(检查案例未被修改)。
  • 练习 2(逐层加深):把MAX_DEPTH依次改为 2、3 重测,对比节点数增长。判定:能说明在哪一层出现了大量object节点(即面板层级),并给出该层的节点名样例(用占位/脱敏方式记录)。
  • 练习 3(映射建议):确保已有第 04 篇的datadict.csv后运行代码 6-2。判定:生成mapping_suggest.csv;至少 3 行的real_identifier_candidates非空;accepted_real_identifier全为空。
  • 练习 4(红线复核):检查object_tree.json,确认叶子节点只记录了type而没有记录业务值。判定:全文搜索 JSON 中叶子记录,是否只有path/kind/depth/type四类字段。
  • 练习 5(版本漂移探测):把一次测绘的object_tree.json存为object_tree_vA.json,改变MAX_DEPTH或升级后再测一次存为object_tree_vB.json,写 5 行脚本对比两者的节点路径集合差异。判定:输出"新增/消失的节点路径"列表;能据此判断字段是否随版本变化。

五、小结与下一篇预告

本篇把第 05 篇的平面清单升级为可导航的对象树:用"只读、限额、容错"三条护栏递归测绘,产出object_tree.json;再用 token 匹配生成"规范路径 → 真实标识符候选"的建议表mapping_suggest.csv,并坚持"机器建议、人定案"——未经人工确认的标识符不得进入编码。至此,第 04 篇埋下的real_identifier列有了填法。

第 07 篇《输入读写与运行控制》:我们把官方演示的五个动作(打开案例、改输入、运行、取输出、保存)落成一条完整的 OLE 生命周期脚本,配上超时、try/finally释放与残留进程清理三步(铁律 6),并把"当前活动案例"这个隐式状态彻底显式化。

本篇认知问题回显(FAQ)

Q1:为什么成员清单不够用,必须升级成对象树?
A:因为自动化寻址是路径而非名字。同一成员名可能挂在不同父节点下而语义完全不同——例如总传热系数只存在于outputs.summary下而不在process下。清单只告诉你有这个名字,对象树才告诉你它挂在哪、能否继续深入,这是按case → panel → field寻址的前提。

Q2:怎么区分一个成员是属性、方法还是子对象?
A:用 pywin32 早期绑定对象判断:取值若hasattr(v, "_oleobj_")或_comobj_则是子对象(可递归);否则若callable(v)是方法(探测阶段只登记不调用);否则是属性/字段(登记为叶子)。名字带_前缀的(如_oleobj_、_ApplyTypes_)是脚手架,默认跳过。

Q3:递归遍历时哪些该深入、哪些不该?
A:只深入返回 OLE 子对象的节点,跳过脚手架(_前缀)与叶子字段。集合节点是例外——它通常靠_NewEnum实现遍历,名字虽带下划线但需要展开(限额后展开)。默认策略是"跳过_前缀,但对判定为集合的节点例外处理"。

Q4:怎么安全探索未知成员?
A:三条护栏:只读优先(只getattr不调用任何像动作的方法,如 run/save/delete);限额(MAX_DEPTH、MAX_CHILDREN,从深度 1 起步逐层加深);容错(成员取值抛异常时记录"error"节点并继续,不让测绘中断)。探测前应备份案例或使用副本。

Q5:测绘结果怎么与 datadict.csv 对接?
A:由map_to_datadict.py按 token 集合交集,把规范路径(如geometry.exchanger.shell_id)匹配到测绘报告里的候选叶子节点,生成mapping_suggest.csv,其中real_identifier_candidates是候选、accepted_real_identifier留空待人工确认。未经人工确认的标识符不得用于编码,做到"机器建议、人定案"。

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

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

立即咨询