介绍和宗旨
DCML(Data container markup language,数据容器标记语言),正如其名字,是一种就像JSON、YAML一样的数据交换格式,用于存储和传输结构化数据。
相比其它的数据交换格式,DCML有以下几种特点:
不仅强类型,还需要显式声明,避免出现类型混淆。
通过括号来区分层级,通过分号来区分对象,可以放心压缩至一行。
整个文件只有一种组成:对象,最多只有格式的区分。
语法格式
对象只有两种语法格式。
键值对格式
数据类型: “键” = 值;
键必须使用引号(双引号和单引号),例如:
string: “name” = “andy”;
如果是需要表示层级的对象,要使用括号作为值,括号内写包含的对象,详情可看后文:
table: “andy” = { int: “age” = 16; string: “like” = “cat”; };
元素格式
数据类型: 元素;
通常元素格式只能在list的数据类型使用(list会在后文提到),例如:
int: 2; int: 4; int: 6;
主对象
一个DCML文件必须包含一个,且只有一个叫做main的table对象,其中包含所有对象:
table: “main” = { string: “text” = “Hello world”; };
直接写内容是不允许通过编译的:
string: “text” = “不要这么写!“;
注释
DCML支持注释,注释不会被编译,注释使用**/作为开头,/**作为结束,可以跨行:
table: “main” = { string: “password” = ”********”; /警告 妥善保管你的密码 泄露概不负责/ };
对象的数据类型
对象有多种数据类型,与编程语言一一对应:
int
可以记录整数,支持正数和负数,负数在数字前加-号:
int: “high” = 100; int: “low” = -50;
float
用于记录小数,支持正负和指数:
float: “f” = 3.1415926; float: “-f” = -0.23; float: “e” = 1e06; float: “-e” = -2e-2;
string
字符串,值需要用双引号或单引号包围,可以跨行:
string: “name” = “andy”; string: “write” = “你只需要像我这样换行 就学会了 一首诗”;
boolean
布尔值,不多说,首字母大写:
boolean: “#T” = True; boolean: “#F” = False;
Null
空值,但是可以在任何数据类型的对象使用**(除了table和list)**:
int: “age” = Null; string: “name” = Null;
list
列表,元素格式对象的集合,需要扩展层级,里面也只能包含元素格式对象:
list: “user” = { string: “andy”; int: 16; boolean: True; };
table
表,键值对格式对象的集合,需要扩展层级,里面也只能包含键值对格式对象:
table: “andy” = { int: “age” = 16; string: “like” = “cat”; };
示例
以下示例用到了DCML的全部特性:
table: “main” = { /此文件存储部分用户的数据/ table: “Andy” = { string: “name” = “Andy”; int: “age” = 16; float: “balance” = 17.54; boolean: “vip” = True; list: “friend” = { string: “Ben”; string: “Lisa”; }; }; table: “Bob” = { string: “name” = “Bob”; int: “age” = Null; float: “balance” = 5e03; boolean: “vip” = False; list: “friend” = {}; }; };
编译器
这是一个简易的DCML编译器,可以将DCML字符串转换到Python字典,AI写的,但是不支持很多功能,而且有很多Bug,玩玩可以:
import re from typing import Any, Dict, List
class TokenStream: def init(self, tokens: List[str]): self.tokens = tokens self.pos = 0
def next(self) -> str:
if self.pos >= len(self.tokens):
raise ValueError(f"错误:意外的文件结尾(位置:{self.pos})")
self.pos += 1
return self.tokens[self.pos - 1]
def peek(self) -> str:
return self.tokens[self.pos] if self.pos < len(self.tokens) else ''
def tokenize(s: str) -> List[str]: # 支持全角分号和括号(用户可能输入的符号问题) pattern = re.compile( r’(”(?:\”|.)?”|’(?:\’|.)?’)|’ # 带引号的字符串(支持转义) r’(\b(?
def convert_value(data_type: str, value_token: str) -> Any: # 优先处理Null值(DCML规范:任何非容器类型都可以为Null) if value_token == ‘Null’: return None
# 处理其他类型
if data_type == 'int':
return int(value_token)
if data_type == 'float':
return float(value_token)
if data_type == 'string':
# 去除首尾引号并处理转义(支持\"和\')
return value_token[1:-1].replace('\\"', '"').replace("\\'", "'")
if data_type == 'boolean':
return value_token == 'True'
raise ValueError(f"错误:未知数据类型 '{data_type}'")
def parse_list_content(stream: TokenStream) -> List[Any]: lst = [] while stream.peek() != ’}’: type_token = stream.next() # 如”string:” data_type = type_token[:-1].strip() # 提取类型(去掉冒号) element_token = stream.next() # 元素值(带引号的字符串或基础值)
# 列表元素必须是带引号的字符串或基础值(根据DCML规范)
if data_type == 'string' and not (element_token.startswith(('"', "'")) and element_token.endswith(('"', "'"))):
raise ValueError(f"错误:列表字符串元素必须用引号包裹(当前值:{element_token})")
value = convert_value(data_type, element_token)
# 检查分号结尾
if stream.next() != ';':
raise ValueError(f"错误:列表元素缺少分号结尾(位置:{stream.pos})")
lst.append(value)
return lst
def parse_table_content(stream: TokenStream) -> Dict[str, Any]: table = {} while stream.peek() != ’}’: type_token = stream.next() # 如”table:” data_type = type_token[:-1].strip() # 提取类型(去掉冒号) key_token = stream.next() # 键(带引号的字符串)
# 检查键格式
if not (key_token.startswith(('"', "'")) and key_token.endswith(('"', "'"))):
raise ValueError(f"错误:键必须用引号包裹(当前键:{key_token})")
key = key_token[1:-1] # 去除引号
# 检查等号
if stream.next() != '=':
raise ValueError(f"错误:键值对 '{key}' 缺少等号(位置:{stream.pos})")
# 处理容器类型(list/table)
if data_type in ['list', 'table']:
# 检查左大括号
if stream.next() != '{':
raise ValueError(f"错误:{data_type}类型 '{key}' 缺少左大括号(位置:{stream.pos})")
# 递归解析内容
value = parse_list_content(stream) if data_type == 'list' else parse_table_content(stream)
# 检查右大括号
if stream.next() != '}':
raise ValueError(f"错误:{data_type}类型 '{key}' 缺少右大括号(位置:{stream.pos})")
else:
# 处理基础类型
value_token = stream.next()
value = convert_value(data_type, value_token)
# 检查分号结尾
if stream.next() != ';':
raise ValueError(f"错误:键值对 '{key}' 缺少分号结尾(位置:{stream.pos})")
table[key] = value
return table
def parse_main(stream: TokenStream) -> Dict[str, Any]: # 解析主对象(必须是table: “main” = { … }) if stream.next() != ‘table:’: raise ValueError(“错误:主对象必须是table类型(位置:0)”) if stream.next() != ‘“main”’: raise ValueError(“错误:主对象键必须是’main’(位置:1)”) if stream.next() != ’=’: raise ValueError(“错误:主对象缺少等号(位置:2)”) if stream.next() != ’{’: raise ValueError(“错误:主对象缺少左大括号(位置:3)”)
main_content = parse_table_content(stream)
if stream.next() != '}':
raise ValueError("错误:主对象缺少右大括号(位置:末尾)")
return {'main': main_content}
def dcml_to_dict(dcml_str: str) -> Dict[str, Any]: # 清理注释(支持跨行注释) cleaned = re.sub(r’/*.*?*/’, ”, dcml_str, flags=re.DOTALL) # 处理多余空白(保留字符串内的换行) cleaned = re.sub(r’(?<![”’])\s+(?![”’])’, ’ ’, cleaned) # 生成token流 tokens = tokenize(cleaned) stream = TokenStream(tokens) # 解析主对象 return parse_main(stream)
if name == “main”: # 修正测试用例(使用半角符号) test_dcml = """ table: “main” = { /此文件存储部分用户的数据/ table: “Andy” = { string: “name” = “Andy”; int: “age” = 16; float: “balance” = 17.54; boolean: “vip” = True; list: “friend” = { string: “Ben”; string: “Lisa”; }; }; list: “Bob” = { string: “name” = “Bob”; int: “age” = Null; /* 这里现在可以正确解析 / float: “balance” = 5e03; / 已修正为半角分号 */ boolean: “vip” = False; list: “friend” = {}; }; }; """ try: result = dcml_to_dict(test_dcml) import json print(“解析成功!结果:”) print(json.dumps(result, indent=2, ensure_ascii=False)) except Exception as e: print(f”解析失败:{e}”)