简介:ISO 10303-21:2002 是产品数据交换领域的重要国际标准,专门定义STEP物理文件(p21文件)的明文编码格式,面向CAD/CAM/CAE系统集成、制造业数据交换与标准研究人员。该PDF文档共1个文件,约1.12MB,收录标准正式英文全文,涵盖范围、规范性引用、术语定义及交换结构明文编码规则等核心章节。读者可据此掌握STEP文件的ASCII结构、内码机制与数据表示方式,理解.stp/.step文件如何按照应用协议组织几何、材料与工程参数等信息。目前已有2027人学习下载,是解析STEP格式、进行跨系统数据交换或二次开发时值得对照的权威依据。文档虽以可读文本形式呈现,但内容专业性强,建议具备一定工业自动化或CAD使用背景后再深入研读。
1. 一份 2002 年的标准 PDF,为什么今天还在决定 .stp 文件的生死
ISO10303-21-2002.pdf 定义了 STEP 物理文件的组织语法,你每天在 .stp / .step 文件里看到的#1 = CARTESIAN_POINT(...)就是按它写出来的。多数工程师没有打开过原文,但所有主流 CAD、CAE、PLM 系统之间交换产品数据时,底层认的都是这版规则。对要写转换脚本、做数据迁移、排查「为什么这文件打不开」的人来说,它就是现场语法表。下文不逐页翻译 PDF,而是按解析器作者的视角,把文件分区、实体语法、转义机制和最容易翻车的边界情况拆开讲。读完你能从零写一个能处理真实文件的 .step 解析器,也知道该去哪查标准原文。
2. ISO 10303-21 到底规定什么:物理文件的分区与语法骨架
2.1 三个区段:HEADER 装元数据,DATA 装实体
PART 21 物理文件按顺序由三部分组成:头部区段、数据区段和结尾标记。文件第一行必须是ISO-10303-21;,这个分号经常被手写处理逻辑的人忽略,但它不是装饰。头部区段以HEADER;开头,以ENDSEC;结尾;数据区段以DATA;开头,同样以ENDSEC;结尾。文件最后一行为END-ISO-10303-21;。一个最小文件长这样:
ISO-10303-21; HEADER; FILE_DESCRIPTION(('示例文件'),'2;1'); FILE_NAME('demo.stp','2024-06-01T10:00:00',(''),(''),'demo','demo',''); FILE_SCHEMA(('CONFIG_CONTROL_DESIGN')); ENDSEC; DATA; #1 = CARTESIAN_POINT('P1',(0.,0.,0.)); #2 = CARTESIAN_POINT('P2',(1.,2.,3.)); ENDSEC; END-ISO-10303-21;头部区段里这三条记录被列为标准头部实体,实际文件几乎必然出现。解析头部不需要额外语法,它们本身就是实体记录,只需要按同一种语法读完,再单独存成元数据对象。
| 头部实体 | 参数 | 作用 |
|---|---|---|
| FILE_DESCRIPTION | (description_list, implementation_level) | 文件说明文本;'2;1' 表示实现等级 2 的子级 1,支持无界字符串与多字节字符 |
| FILE_NAME | (name, time_stamp, author[], organization[], preprocessor_version, originating_system, authorisation) | 传输文件名、时间戳、作者与来源系统 |
| FILE_SCHEMA | (schema_name[]) | 该文件遵循的 EXPRESS 模式名,如 CONFIG_CONTROL_DESIGN |
实现等级'2;1'里的 2 意味着字符串可以跨行、可以包含 ISO 10646 字符,这直接关系到后面要讲的转义序列;FILE_SCHEMA 里的模式名告诉解析器,这个文件里的实体类型定义去哪份字典里查。对纯物理层解析器来说,这两个字段一个是能力开关,一个是语义入口,都不能跳过去。
2.2 实体实例与引用:整个数据区是一张图
数据区是文件的主体,产品模型的所有实例都以实体实例形式出现。语法是固定的三元组:#编号 = 类型名(参数列表);。编号是实例 ID,在整个文件内唯一;类型名对应 EXPRESS 模式里的实体类型;参数列表里可以放数值、字符串、枚举、子列表,也可以放另一个实例的引用,比如#10 = LINE('L1', #8, #9);里的#8和#9。
参数里的引用让数据区变成一张有向图:一个装配体,顶层是 PRODUCT 或 ASSEMBLY 之类的实例,往下引用到形状表示,再往下引用到 CARTESIAN_POINT、LINE、CIRCLE 这些具体几何。这也是新手最容易把两步混在一起然后被嵌套引用绕晕的地方。只做格式转换时,先把每条记录读成{id: (类型名, 原始参数字符串)}就够了;要做拓扑重建或模型校验,必须再递归地按 EXPRESS 模式定义解析参数、建立对象图。两个阶段分开,调试时才不会互相污染。
2.3 2002 版为何是分水岭:字符转义体系
1994 年首版已经奠定了文件骨架,2002 年修订版把字符串与字符编码规则重新收紧,统一到 ISO 10646 上。对今天的解析器来说,2002 版意味着:字符串字面量用单引号包裹,里面不能直接出现不可打印字符,也不能假设文件就是某种本地编码。所有特殊字符都必须写成转义序列,常见几类如下表。
| 转义写法 | 语义 | 例子 |
|---|---|---|
\\ | 反斜杠本身 | 'a\\b'-> a\b |
\' | 单引号 | 'it\'s'-> it's |
\N\ | 换行 | 'a\N\b'-> a 换行 b |
\F\ | 换页 | |
\S\+ 一个字符 | 拉丁-1 特殊字符 | \S\A-> Ä |
\X\hhhh\ | 16 位码位(4 位十六进制) | \X\00C4\-> Ä |
\X2\hhhhhhhh\ | 16 位码位(8 位十六进制) | \X2\000000C4\-> Ä |
\X4\hhhhhhhh\ | 32 位码位,补充平面字符 | \X4\0001F600\-> 😀 |
这张表值得存下来。后面写解析器时的分词、还原、校验三条路径,全部围绕它展开。\S\后的字符表在标准附录里有完整对照;\X系列统一用int(hex, 16)转换。2002 版把这些序列的语义钉死之后,主流 CAD 系统的 .step 导出才真正变得彼此兼容,这也是为什么「2002 版」至今仍是各种解析器默认支持的基线。
3. 从零写一个 STEP 解析器:分词、记录解析与转义还原
3.1 第一道工序:用引号状态机按顶层分号切块
真实文件里,字符串字面量可以包含分号、括号甚至换行,所以「按分号 split」或「按行读」都会翻车。正确做法是一个带引号开关的状态机:只有在字符串外面遇到分号才切块。
def split_top_level(text, sep=';'): parts, buf = [], [] in_string = False i = 0 while i < len(text): c = text[i] if in_string: buf.append(c) if c == '\\': if i + 1 < len(text): buf.append(text[i + 1]) i += 1 elif c == "'": in_string = False else: if c == "'": in_string = True buf.append(c) elif c == sep: parts.append(''.join(buf).strip()) buf = [] else: buf.append(c) i += 1 if buf: parts.append(''.join(buf).strip()) return parts这个函数有三个关键点:字符串定界符是单引号而不是双引号;遇到反斜杠要连带下一个字符一起吞掉,避免把转义序列里的内容当成结束符;切出来的块统一去掉首尾空白,方便下一步处理。对数据区段调用一次split_top_level(data_block)就能得到所有实体记录。唯一的性能警告是它逐字符扫描,Python 处理上百 MB 的文件会明显变慢,生产环境可以改用 C 扩展或流式读取,但逻辑骨架不变。
3.2 第二道工序:拆出实例 ID、类型名与参数串
拿到一条像#1 = CARTESIAN_POINT('P1',(0.,0.,0.));的记录后,要把它拆成(id, cls, arg_str)三元组。
import re def parse_record(record): record = record.strip() m = re.match(r'^#(\d+)\s*=\s*([A-Za-z0-9_]+)\s*\((.*)\)$', record, re.DOTALL) if not m: return None return int(m.group(1)), m.group(2), m.group(3).strip()正则写法简洁,但有两个地方要在生产代码里替换:一是(.*)贪婪匹配会把极长的参数串整段复制,内存翻倍;二是它假设记录以最后一个右括号结尾,单条记录里字符串含右括号时没问题,但遇到不规范的换行拼接要多测。稳妥做法是扫描到第一个顶层左括号后做括号计数,配平的那个右括号才是记录真正的结束。返回值里类型名保留原始大小写,不要在这里顺手转大写,后面有专门环节处理大小写匹配。
3.3 第三道工序:字符串转义还原
参数串是原始文本,不能直接当最终数据用。字符串字面量必须经过转义还原,再交给下游做映射或建对象图。
def decode_step_string(raw): out = [] i, n = 0, len(raw) while i < n: c = raw[i] if c != '\\': out.append(c) i += 1 elif i + 1 >= n: out.append('\\') i += 1 else: kind = raw[i + 1] if kind in ('\\', "'"): out.append(kind) i += 2 elif kind in ('N', 'F') and i + 2 < n and raw[i + 2] == '\\': out.append('\n' if kind == 'N' else '\f') i += 3 elif kind == 'S' and i + 3 < n and raw[i + 2] == '\\': special = {'A': 'Ä', 'a': 'ä', 'D': 'Ö', 'd': 'ö', 'U': 'Ü', 'u': 'ü', 's': 'ß', 'Y': '°'} out.append(special.get(raw[i + 3], raw[i + 3])) i += 4 elif kind == 'X': if (i + 3 < n and raw[i + 2] in ('2', '4') and raw[i + 3] == '\\'): start = i + 4 else: start = i + 2 end = raw.find('\\', start) if end == -1: end = n try: out.append(chr(int(raw[start:end], 16))) except ValueError: out.append(raw[i]) i = end + 1 else: out.append(c) i += 1 return ''.join(out)decode 顺序对应标准的三层转义:先处理\\和\';再处理\N\、\F\;最后是\X系列的十六进制分支。\S\按定长一个字符取,映射表只列了常见几个,生产环境要补全标准附录的对照。\X系列一律先找结尾反斜杠,找不到就退回按剩余全部字符截断——这是我从真实文件里学到的容错:某些导出器写的\X\会漏掉结尾。真正调用时,必须先定位字符串字面量再解码,不能对整段参数串直接跑:
def decode_strings_in_args(arg_str): out = [] i, n = 0, len(arg_str) while i < n: c = arg_str[i] if c != "'": out.append(c) i += 1 continue j = i + 1 backslash_count = 0 while j < n: if arg_str[j] == '\\': backslash_count += 1 j += 1 elif arg_str[j] == "'" and backslash_count % 2 == 0: break else: backslash_count = 0 j += 1 raw = arg_str[i + 1:j] out.append("'" + decode_step_string(raw) + "'") i = j + 1 return ''.join(out)内层循环用「反斜杠数量的奇偶」来判断结束引号,\'这种转义会被正确跳过,\\'这种「转义反斜杠后跟结束引号」也不会误判。这也是为什么解析 STEP 字符串不能只看前一个字符,要统计连续反斜杠的个数。写完解码层,解析器骨架就齐了。
3.4 这个解析器的边界与取舍
三个组件可以合成一个最小可用的物理层加载函数:
def load_step(path): with open(path, 'r', encoding='utf-8', errors='replace') as f: content = f.read() _, data = content.split('DATA;', 1) data = data.split('ENDSEC;', 1)[0] records = {} for chunk in split_top_level(data): parsed = parse_record(chunk) if parsed: eid, cls, arg_str = parsed records[eid] = (cls, decode_strings_in_args(arg_str)) return records这个实现是「物理层解析器」,能处理绝大多数实际导出文件,但不校验语义。比如#3 = LINE('L', #1)少一个参数,物理层照样读出来,语义错误要等映射层按 EXPRESS 模式检查。另一个边界是引用还原:现在#8在参数串里还是一个普通文本,要真正得到对象图,需要在解析参数时把#\d+转成对记录表的查找,同时处理几何数据里常见的循环引用。性能上,逐字符扫描的 Python 实现适合几十 MB 以内的单文件;几百 MB 的大型装配,建议按块读取并在分块边界维护 in_string 状态,或者直接换编译型语言实现。文件读取统一用 UTF-8 加 errors='replace',避免遇到残留字节直接中断。
4. 解析 STEP 文件最容易踩的坑:5 个翻车现场排查清单
下面 5 条都是我在真实文件里各自见过至少一次的翻车场景,统一按「现象 -> 原因 -> 解决」写,可以直接当排查手册用。
4.1 字符串里的分号把记录切碎
现象:用data_block.split(';')切出来的块数量对不上实体总数,报错信息定位在第 N 条记录,但实体数怎么数都不对。原因:字符串字面量里有分号,朴素切分把它当成了记录边界。解决:用 3.1 的引号状态机切分。排查时先数文件里真正的顶层分号数量,再数实体数,差得越多越说明切分有问题。顺手在切块后加一道#\d+\s*=的正则校验,不匹配的块就是脏数据,直接暴露在哪一行。
4.2 文件尾部没有 END-ISO-10303-21
现象:解析器用正则匹配END-ISO-10303-21;找不到就抛异常,但同一个文件在 CAD 里能正常打开。原因:部分传输环节把尾部截掉了,或某个导出器写完ENDSEC;后直接收尾。解决:把数据区段的边界定位到 DATA 之后的第一个ENDSEC;,文件末尾的结束标记只做完整性警告,不当作解析失败条件。我在生产代码里把「缺尾部标记」放进 warning 列表,和「实体引用缺失」一起输出,而不是直接中断。
4.3 实体类型名大小写不敏感,精确匹配就漏判
现象:用if cls == 'CARTESIAN_POINT':做判断,某个导出的文件里一条都匹配不到。原因:EXPRESS 模式里的类型标识符不区分大小写,部分工具按小写或混合大小写输出。解决:解析时保留原始类名,匹配时统一cls.upper();如果要把数据写回 STEP,再用回原始字面量。血泪经验是字典 key 上存原始名,另建一份大写 key 的索引,两套并存不要互盖。
4.4 转义序列没还原,中文注释变乱码
现象:从 .step 读出的中文注释是一长串\X2\6D4B\或\X\组合,直接入库后全是乱码。原因:漏了转义还原环节,或还原函数没处理\X2\/\X4\分支。解决:按「找字符串 token -> decode_step_string -> 输出 UTF-8」三层做,不要去猜整个文件是什么字符集。排查时拿一个只含\X2\00004E2D\(对应汉字「中」)的小文件做单测,能还原就说明分支逻辑正确。
4.5 空字符串与$未定义混淆
现象:下游把$和''都当空值存库,导致「未定义」和「空字符串」两种语义丢失。原因:词法层没区分这两个概念,STEP 语法里$表示参数未提供,''表示一个确实是空的字符串。解决:token 分类时把$单独提出来,映射到 Optional 的空值;''保持为字符串类型。判空逻辑分开写,先判断是否$,再判断是否空字符串,避免二义性。
5. 验证解析器是否可信的最低成本做法:引用完整性对拍
解析器写完,别急着接业务。先做一个引用完整性检查:数据区里所有#引用都必须能在记录表中找到定义。
import re def check_references(records): """records: {id: (class_name, decoded_arg_str)}""" defined = set(records) refs = set() for _, arg in records.values(): refs.update(int(r) for r in re.findall(r'#(\d+)', arg)) return refs - defined这个检查实现短,但漏引用是 STEP 文件最常见的结构性问题,返回值非空一定说明文件或解析器有一方有问题。把它的输出和实体总数一起打印,再拿同一批文件去喂另一个独立实现,比如某个开源几何内核自带的 STEP 读取器。对拍三个指标:实体数量是否一致、类型名集合是否一致、缺失引用是否都为 0。三组都对得上,你的解析器才算可信。这个习惯我保留了很久:每接手一个新格式,先拿小文件跑自检,再交叉验证一次,能省掉集成阶段一整周的排障时间。希望帮到你。
本文还有配套的精品资源,点击获取