Dynamo这个系列写到第三篇,前两篇讲的是节点用法和数据处理,这次聊一个我最近用得最顺手的自动化小框架——ThunderAgent。
它的定位一句话能说清:在Revit+Dynamo的工作流里替我做那些重复到想吐的数据活。我不是专职开发,只是一个天天和BIM模型打交道的技术负责人,每天要应付甲方清单、提资表、管综统计、版本对比。这些活单个看都不难,难的是它们永远在变、永远有新的模型版本,还永远等着要结果。前前后后手动折腾了三个月之后,我决定把这些流程沉淀成一个可以反复调用的“代理”,于是有了ThunderAgent。
这篇文章算是一次完整的项目复盘,从这里你会看懂ThunderAgent到底解决什么问题、架构怎么拆、核心代码怎么写的,以及它在真实项目里踩过的几个硬坑。适合谁看?如果你正在用Dynamo做工程数据整理,面对批量提取构件参数、每天生成统计表、模型一更新就要重跑一遍流程这类场景,可以照这套思路动手搭一个属于你自己的自动处理流程。
1. 忍了三个月重复出表后,我给Dynamo写了ThunderAgent
1.1 先说三件我每天都在重复做的事
第一件,下班前统计全楼防火门的数量、规格、防火等级。项目后期甲方天天要表,今天按防火分区出,明天按楼层出,后天按供应商分,一个表的列头能改六遍。
第二件,每个新模型版本来了之后,要和上一版对比构件参数。哪些门换了型号,哪些管井尺寸变了,这种差异要我手动检查几百个构件,真的会漏。
第三件,月度提资时把所有机电管综的关键标高、管径、连接方式导成Excel总表。数据全在模型里,但我不可能让设计院同事全部手工复制粘贴。
这三类任务的共同点是:数据全部都在Revit模型里,逻辑完全固定,但每次输入文件不同、输出格式不同,执行的人只有我一个。
1.2 用Dynamo原生节点为什么最后一公里总翻车
早期我也写过一堆“一次性”节点图,从模型里抽数据,接到Excel。但用了一周就烦了:数据源一改,比如文件名变了、目录换了、甲方要求多加一个参数,就得打开那团节点连线手工调整。任何一个环节断了,节点就红泡泡报警,只能手动排查。
更让我没法忍的是,节点图几乎没有“错误边界”的概念。一个构件的某个参数没填,整个流程就崩在那里,后面所有构件都不处理了。实际项目里脏数据是常态,缺参数、空值、链接模型里的构件被重复统计,都是家常便饭。原生节点图在这种场景下,就像一个人没有任何抗压能力,一碰就碎。
1.3 ThunderAgent追求的三个目标
基于上面的痛点,我给这个框架定了三条硬指标:
- 配置外置:要提取哪些参数、从哪个模型来,全部写在外部的配置清单里,改配置的时候不动图。
- 异常隔离:单个构件数据有问题,记录警告继续跑,不允许让一个坏数据毁掉整批处理。
- 全流程留痕:每次执行都有日志、有输出、有时间戳,别人接手也能看懂这次跑的是什么。
有了这三个目标,后面的架构设计就顺理成章了。
2. ThunderAgent的架构逻辑:配置先行、执行隔离、日志留痕
2.1 配置清单:用CSV当“任务书”
设计核心是先定义“干活的指令”,再谈怎么干活。指令格式就是一个简单到不能再简单的CSV:
category,prefix,param_list,output_tag Door,D-fire,类型;防火等级;安装楼层;供应商,Door_Report Window,窗-,类型;宽度;高度;材质,Window_Report PipeAccessory,管件-,系统类型;连接方式;公称直径;标高,PipeAcc_Report你看,这一行就是一条任务。category决定用Revit API里哪个类别去过滤,prefix帮你从构件名称里筛出目标构件,param_list是要提取的参数名,output_tag决定输出文件的前缀。这个设计让换楼栋、换模型变得极其廉价——只改这个清单,Dynamo图原地不动。
2.2 执行器为什么用Python节点而不是纯节点图
有人会问,Dynamo本身不是可视化编程吗,为什么核心逻辑全塞进Python节点?
我的回答是:控制流。Dynamo的节点图非常适合做数据流程展示,但一旦遇到“循环里要try-except”“遇到异常继续往下走”“对几百个元素动态分支处理”这种需求,节点图的表达力会断崖式下降。把这些逻辑放进一个Python节点,代码里的for循环、continue、try-except都是最直白的语言。
执行器只管三件事:读配置、遍历模型、攒结果。它不负责路径拼接,也不负责最终格式化Excel,边界划分得很清楚。这样一旦出问题,定位也快——是配置写错了、模型数据不对,还是执行器代码本身有bug,三条线互不干扰。
2.3 四类输出文件:让代理自己交代干了什么
跑完一次,不该只有一个孤零零的结果文件。一个可追溯的代理,至少要有四样输出:
| 文件 | 作用 |
|---|---|
| data_xxx.csv | 本次提取到的明细数据,一构件一行 |
| summary_xxx.csv | 按配置分组后的汇总行,方便直接对总数 |
| run.log | 从开始到结束每个阶段的耗时、警告、失败计数 |
| last_run_time.txt | 记录本次运行时间,供后面挂机监控判断是否需要重跑 |
有人觉得四类太多了,真用起来你会发现,没日志的自动化流程根本不叫自动化,而叫黑箱。模型出问题的时候,日志就是你唯一的检察官。
3. 手写一个数据采集型ThunderAgent:核心Python节点与完整代码
3.1 工程目录从一开始就规划好
我习惯这样组织文件夹:
D:\ThunderAgent\ input\ config\tasks.csv output\data\ output\runlog\ script\thunder_core.pyinput放RVT模型,config放配置,output按data和runlog分开。为什么不把脚本全塞进节点里完事?因为Dynamo节点里的代码不好做版本管理,一旦改坏就很难恢复。放一个独立的.py文件,配合Git也好追踪,Dynamo节点里的代码量就能降到最低。
3.2 核心逻辑一:读取任务清单
我把Python节点中的代码大致拆成三段讲。第一段,读取tasks.csv,把每一条任务转成字典:
import csv import os def load_tasks(csv_path): tasks = [] with open(csv_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: tasks.append({ 'category': row['category'].strip(), 'prefix': row['prefix'].strip(), 'param_list': [p.strip() for p in row['param_list'].split(';')], 'output_tag': row['output_tag'].strip() }) return tasks这里用utf-8-sig是为了应对Windows下Excel直接保存CSV时带BOM的问题。实测不开这个参数,读出来的列名往往带\u0057ufeff前缀,后面做匹配全失败,很坑。
3.3 核心逻辑二:过滤器与参数提取
第二段,拿到任务之后,用集合过滤器找元素。以Door为例,映射到Revit的BuiltInCategory。之后逐个元素取参数,关键是LookupParameter配合StorageType判断:
from Autodesk.Revit.DB import * def fetch_parameters(elem, param_names): row = [] for name in param_names: p = elem.LookupParameter(name) if p is None or not p.HasValue: row.append('') continue try: if p.StorageType == StorageType.String: val = p.AsString() or '' elif p.StorageType == StorageType.Double: val = p.AsValueString() or str(p.AsDouble()) elif p.StorageType == StorageType.Integer: val = str(p.AsInteger()) else: val = p.AsValueString() or '' except Exception: val = '' row.append(val) return row这个函数的妙处在于,它不会因为某一个参数取不到就崩溃。取不到就返回空字符串,和配置里的预期不一致也没关系,后续可以在summary环节查警告。注意长度、面积这类数值参数,AsValueString会按项目单位显示,比直接AsDouble更符合出表的直觉。
3.4 核心逻辑三:让Python节点接受外部输入
第三步,很多第一次写Dynamo Python节点的人容易忽略:代码里不要写死路径,让节点通过输入端把路径传进来。下面这段是节点入口部分:
# Dynamo输入:tasks_csv(字符串), output_folder(字符串) import clr clr.AddReference('RevitAPI') clr.AddReference('RevitServices') from Autodesk.Revit.DB import * from RevitServices.Persistence import DocumentManager doc_model = DocumentManager.Instance.CurrentDBDocument tasks = load_tasks(tasks_csv) all_rows = [] warnings = [] for task in tasks: cat = builtin_category_map.get(task['category']) if cat is None: warnings.append('未知类别: ' + task['category']) continue collector = FilteredElementCollector(doc_model).OfCategory(cat).WhereElementIsNotElementType() elems = collector.ToElements() for elem in elems: if task['prefix'] and task['prefix'] not in elem.Name: continue all_rows.append([elem.Id.IntegerValue, elem.Name] + fetch_parameters(elem, task['param_list'])) warnings.append('{}: 共提取 {} 个构件'.format(task['output_tag'], len(elems)))builtin_category_map是一个你自己维护的字典,把配置里的Door、Window这些关键词和BuiltInCategory.OST_Doors、OST_Windows对应起来,后续想支持更多类别,只需要扩展这个字典。
3.5 输出到CSV与节点连线
最后把数据写出。Excel对UTF-8的支持有问题,所以编码用utf-8-sig;文件路径由节点输入。写完把路径和行数也输出成一个列表,方便Dynamo上游加个Watch节点直接看结果:
out_csv = os.path.join(output_folder, 'data_{}.csv'.format(tasks[0]['output_tag'])) if tasks else os.path.join(output_folder, 'data_empty.csv') with open(out_csv, 'w', encoding='utf-8-sig', newline='') as f: writer = csv.writer(f) writer.writerow(['ElementId', 'Name'] + tasks[0]['param_list']) writer.writerows(all_rows) log_lines = warnings + ['总行数: %d' % len(all_rows)] with open(os.path.join(output_folder, 'run.log'), 'a', encoding='utf-8') as f: f.write('\n'.join(log_lines))节点连线部分其实更简单:放一个Python节点,把代码贴进去,输入端用两个Code Block分别给路径字符串,输出端接一个Watch节点。跑之前的准备工作就两步:确认当前Revit打开的是目标模型,确认output_folder已经存在。
这是完整可跑的最小闭环。第一版我连函数都没拆,直接在节点里一把梭,后来发现改需求时无比痛苦。抽函数不是为了代码优美,是为了让下一次改动只需要改其中一段,而不是从头捋一遍。
4. 实测三连坑:链接模型、参数缺失、大批量卡死
4.1 链接模型重复统计,防不胜防
第一次用ThunderAgent跑全楼防火门,数据出来多了将近一倍。排查半天发现:主文档里的门统计了一遍,链接模型里的门又会被收集器抓一遍。链接模型在Revit项目里无处不在,这个问题非常典型。
解决办法是给收集器加限定,或者干脆在遍历时做文档判断。实测最稳的写法是:
for elem in elems: if elem.Document is not doc_model: continue这一行简单粗暴,但能从根上把链接模型的元素踢出去。如果处理的量太大,还可以配合doc_model.ActiveView.Id去限定当前视图可见的元素,进一步缩小范围。
4.2 “缺参数”不是bug,是常态
这是整个框架最受用的一个设计。实际模型里,防火门有好多“防火等级没填”“材质没填”的记录,完全正常,绝大多数时候是建模的人没来得及填。如果脚本一遇到空参数就抛异常,整个任务就没法跑。
我的处理方式是统一返回空字符串,最后在run.log里统计“空值字段Top10”,把空值参数反查表输出。哪几个参数缺失最严重,一目了然。这样代理不仅把活干了,还能顺手告诉你模型里哪些地方质量堪忧。
4.3 8000个构件卡死:逐条写盘是性能头号杀手
项目模型大概8000个构件,第一次跑的时候,脚本卡在16%左右,等多久都没反应。查了一下瓶颈在循环里反复写文件——每条构件都append一行就flush一次,磁盘IO被拖死了。
这是初版最容易犯的错。把写盘从循环里挪出来,循环只负责往内存列表里塞数据,跑完再一次性写文件。同样的数据量,性能直接提升了近十倍。代码层面的校验也很粗暴:跑完对比模型里类别总数和汇总CSV的行数,两个数字对不上,说明还有漏项。
4.4 Dynamo和Revit版本不同,代码要留个心眼
我用的是Dynamo 2.x配Revit 2021,这套代码是通的。但如果你公司还在用Revit 2018/Dynamo 1.3,有两点要注意:
- 老环境下Python节点的默认版本是CPython 2.7,open()的写入参数写法完全不一样,建议统一改用io.open。
- DocumentManager.Instance.CurrentDBDocument在一些旧模板里可能拿不到,可以用
DocumentManager.Instance.CurrentUIApplication.ActiveUIDocument.Document兜底。
写代码时多包一层兼容函数,实际跑批的时候会少很多“为什么别人能跑我不能跑”的玄学问题。
5. 把ThunderAgent从单模型扩大到整个项目目录
5.1 从“跑一次”到“挂机监控”
单文件版本跑顺之后,我开始琢磨能不能让代理自己盯着文件夹。做法很简单:借助Windows任务计划程序,每天早上八点跑一次检测脚本。脚本遍历input目录下所有rvt文件的修改时间,和output/runlog下的last_run_time.txt对比,有新版本就触发Dynamo图和Revit的批处理流程。
这个能力很实用。项目从方案到施工图,模型每天都在更新,谁也不可能每天手动开一遍Dynamo。让代理自己判断“该不该跑”,才是“代理”这个名字真正的意义。
5.2 多文件批处理的工作流骨架
多文件时,建议先把任务清单升级成带分组和开关的版本:
| task_id | enabled | file_pattern | category | prefix | param_list | output_tag |
|---|---|---|---|---|---|---|
| 101 | 1 | A栋.rvt | Door | D-fire | 类型;防火等级;安装楼层 | Door_Report |
| 102 | 0 | B栋.rvt | Window | 窗- | 类型;宽度;高度 | Window_Report |
这个扩展版本的好处是:几十个RVT文件的处理逻辑可以统一调度,单个流程有问题,把enabled改成0,暂时屏蔽,不影响其他任务。流程本质是一个循环:找到待处理文件、开Revit、跑对应Dynamo图、导出CSV、关文件、把汇总行加进总表。
要注意的细节:处理中给每个rvt加一个.lock标记,防止两个调度进程同时打开;失败次数超过三次就停止,改用人工介入。批处理的稳定性,靠的是这些“看起来无关紧要”的边界条件。
5.3 数据校验:代理跑完了,结果怎么确认
自动化最担心的,就是机器给了一个错误答案,你还信了。所以我加了三个校验层:
- 数量校验:每个类别的提取数量,对比模型里对应类别的TotalCount,不一致就报警。
- 空值校验:空值字段占比超过阈值时,run.log里会标红“请检查模型质量”。
- 抽样对比:随机抽十个构件,脚本自动把它们的参数与Revit里手动看到的值核对一次。
这三层不是复杂技术,但缺了它们,自动化就是一颗定时炸弹。
最后说说这套框架对我意味着什么。ThunderAgent这个命名,看着唬人,本质上就是给重复流程装了一位“不自作聪明的管家”:它不会替你判断模型对不对,但会把每一项流程留痕,让你知道哪里有问题、哪里要做人工复核。实际用了几个月,它帮我省下的不是写代码的时间,而是“每次都要重新进入状态”的那种心力消耗。
如果你也想搭一套,我的建议很朴素:别一上来就做多文件批量、挂机监控这些花活,先把我第三节那个单模型的脚本跑通,改一改任务清单,让你自己的项目先受益。跑顺之后,再去考虑文件夹监控、批量调度。自动化的价值,永远是从第一个跑通的小闭环开始的。