介绍和宗旨
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(?:int|float|string|boolean|Null|list|table):)|' # 数据类型声明
r'([{}=;;])|' # 符号(支持全角分号;)
r'(\b(?:True|False|Null|-?\d+\.?\d*[eE]?[+-]?\d*|-?\d+)\b)' # 基础值
)
tokens = []
for match in pattern.finditer(s):
token = next(g for g in match.groups() if g is not None)
# 统一将全角分号转为半角
if token == ';':
tokens.append(';')
else:
tokens.append(token.strip())
return tokens
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}")