复刻含类XML控制码的游戏对话系统的解析问题求助
我之前在复刻游戏对话系统时也碰到过一模一样的问题——想用类XML的控制码但又没法套根节点,下面这几个方案亲测好用,你可以根据自己的需求选:
方案1:正则表达式解析(适合简单标签场景)
如果你的控制码没有嵌套需求(比如不会出现<co red><b>text</b></co>这种情况),正则是最快的实现方式。核心思路是匹配所有的控制码标签和中间的普通文本,然后逐个处理。
比如针对你的<co>标签,可以用这样的正则模式:
import re pattern = re.compile(r'(<co ([^>]+)>(.*?)</co>|[^<]+)') text = "Hello <co FF0000FF>World!</co> This is <co 00FF00FF>green text</co> or just plain text." matches = pattern.findall(text) for match in matches: if match[0].startswith('<co'): # 处理颜色标签:match[1]是颜色值,match[2]是文本内容 color = match[1] content = match[2] print(f"渲染文本: {content},颜色: {color}") else: # 处理普通文本 print(f"渲染普通文本: {match[0]}")
优点:代码量极少,快速上手,适合标签逻辑简单的对话系统。
缺点:无法处理嵌套标签,标签如果有复杂属性(比如带空格的)需要调整正则,容错性稍差。
方案2:自定义状态机解析(支持嵌套标签)
如果需要支持标签嵌套,或者以后要扩展更多复杂控制码(比如<shake>抖动文本、<delay>延迟显示),状态机是更可靠的选择。核心是逐字符扫描文本,维护当前的解析状态(普通文本/标签开始/标签结束)。
举个简单的Python实现示例:
def parse_dialogue(text): tokens = [] current_text = [] i = 0 n = len(text) while i < n: if text[i] == '<': # 保存当前积累的普通文本 if current_text: tokens.append(('text', ''.join(current_text))) current_text = [] i += 1 # 解析标签名和属性 tag_parts = [] while i < n and text[i] != '>': tag_parts.append(text[i]) i += 1 tag_str = ''.join(tag_parts) if tag_str.startswith('/'): # 结束标签 tokens.append(('end_tag', tag_str[1:])) else: # 开始标签,拆分标签名和属性(比如co FF0000FF) tag_name, *attrs = tag_str.split() tokens.append(('start_tag', tag_name, attrs)) i += 1 # 跳过'>' else: # 积累普通文本 current_text.append(text[i]) i += 1 # 处理最后一段普通文本 if current_text: tokens.append(('text', ''.join(current_text))) return tokens # 测试 text = "Hello <co FF0000FF>World <b>bold</b></co>!" tokens = parse_dialogue(text) for token in tokens: print(token)
这个解析器会把文本转换成('text', 内容)、('start_tag', 标签名, 属性列表)、('end_tag', 标签名)这样的 token,之后你可以根据这些token来处理文本渲染逻辑。
优点:完全自定义,支持任意嵌套和复杂标签,扩展性极强。
缺点:需要自己实现扫描逻辑,代码量比正则多,但逻辑清晰,维护起来不难。
方案3:临时包装根节点用XML解析器处理
如果你还是想复用成熟的XML解析库,可以在解析前给文本套一个临时的根标签,比如<root>...</root>,然后用标准XML解析器解析,之后再遍历节点处理。
比如用Python的xml.etree.ElementTree:
import xml.etree.ElementTree as ET def parse_with_xml(text): # 临时包装根节点 wrapped_text = f"<root>{text}</root>" # 解析XML root = ET.fromstring(wrapped_text) # 遍历根节点的子节点和文本节点 def traverse(node): if node.text: yield ('text', node.text) for child in node: yield ('start_tag', child.tag, child.attrib) yield from traverse(child) yield ('end_tag', child.tag) if node.tail: yield ('text', node.tail) return list(traverse(root)) # 测试 text = "Hello <co color='FF0000FF'>World!</co> Plain text." tokens = parse_with_xml(text) for token in tokens: print(token)
注意这里要把你的控制码属性改成XML格式(比如<co color='FF0000FF'>而不是<co FF0000FF>),如果不想改的话,可以先预处理文本把属性转换成XML属性格式,比如把<co ([^>]+)>替换成<co value='$1'>。
优点:利用成熟的XML解析库,自动处理嵌套、属性解析,容错性好。
缺点:需要适配XML的语法规则(比如属性要加引号),如果原控制码有不符合XML的地方(比如未转义的&)需要提前处理。
最后选哪个方案?如果只是简单的颜色、字体大小这类无嵌套的标签,正则足够;如果要做复杂的对话效果(比如嵌套的样式、动画标签),状态机更灵活;如果想偷懒用现成库,就选临时包装根节点的方法。
内容的提问来源于stack exchange,提问作者user4148144

