我设计的一个数据交换格式

介绍和宗旨

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文件必须包含一个,且只有一个叫做maintable对象,其中包含所有对象:

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

空值,但是可以在任何数据类型的对象使用(除了tablelist

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}")
注意:
除非另有声明,本文可以自由使用、转载和二次创作,但需注明作者,并以相同许可协议分享,且不得用于商业目的。详情请参阅:CC BY-NC-SA 4.0许可协议。

- 标题:我设计的一个数据交换格式
- 作者:Pinpe
- 链接:https://blog.pinpe.top/4801/
成为第一个评论的人吧!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
呼呼
上一篇