如何在XML文件指定位置插入<s>标签并保留原有格式?
解决TEI XML跨多行文本添加标签的方案
核心思路
直接提取纯文本分词会丢失原XML结构(如<lb>标签),正确的做法是保留所有原节点的顺序与结构,通过占位符标记特殊节点后再做句子分割,最后将原节点重新映射到对应的句子标签中:
- 遍历目标文本容器(如
<ab>、<p>),收集文本节点与<lb>节点序列 - 用自定义占位符(如
[[LB:n]])替换<lb>节点,拼接成带标记的完整文本 - 对标记文本做句子分割,再将分割结果映射回原节点集合,包裹到
<s>标签内 - 全程保留原XML的空白、换行与节点属性
具体实现(基于lxml)
ElementTree对节点文本的处理不够灵活,推荐用lxml库,它能更好地保留原XML格式与节点结构。
步骤1:安装依赖
pip install lxml nltk
步骤2:完整代码
from lxml import etree import nltk nltk.download('punkt') from nltk.tokenize import sent_tokenize def add_sentence_tags(xml_path, output_path, text_container_xpath='//ab'): # 初始化XML解析器,严格保留空白与原格式 parser = etree.XMLParser(remove_blank_text=False, preserve_whitespace=True) tree = etree.parse(xml_path, parser) # 遍历所有目标文本容器(可根据TEI结构调整XPath) for container in tree.xpath(text_container_xpath): nodes = [] # 处理容器开头的文本节点(若存在) if container.text and container.text.strip(): nodes.append(container.text) # 收集容器内的所有子节点 nodes.extend(list(container)) current_text = [] current_nodes = [] processed_elements = [] for node in nodes: if isinstance(node, str): # 文本节点直接加入缓存 current_text.append(node) current_nodes.append(node) elif node.tag == 'lb': # 用占位符标记<lb>节点,记录其xml:id lb_id = node.get('xml:id', '') current_text.append(f'[[LB:{lb_id}]]') current_nodes.append(node) else: # 遇到非文本/非lb节点,先处理当前缓存的内容 if current_text: full_text = ''.join(current_text) sentences = sent_tokenize(full_text) ptr = 0 for sent in sentences: sent_nodes = [] remaining_sent = sent while ptr < len(current_nodes) and remaining_sent: n = current_nodes[ptr] if isinstance(n, str): # 匹配文本节点与句子片段 if remaining_sent.startswith(n[:len(remaining_sent)]): # 句子边界在当前文本节点内,截断文本 sent_nodes.append(n[:len(remaining_sent)]) remaining_text = n[len(remaining_sent):] if remaining_text: current_nodes[ptr] = remaining_text else: ptr += 1 remaining_sent = '' else: # 整个文本节点属于当前句子 sent_nodes.append(n) remaining_sent = remaining_sent[len(n):] ptr += 1 else: # 还原<lb>节点,移除占位符 sent_nodes.append(n) remaining_sent = remaining_sent.replace(f'[[LB:{n.get("xml:id", "")}]]', '') ptr += 1 # 创建<s>标签并添加节点 s_elem = etree.Element('s') for sn in sent_nodes: if isinstance(sn, str): if not s_elem.text: s_elem.text = sn else: # 处理文本节点的tail last_child = s_elem[-1] if len(s_elem) > 0 else None if last_child: last_child.tail = (last_child.tail or '') + sn else: s_elem.text += sn else: s_elem.append(sn) processed_elements.append(s_elem) # 重置缓存 current_text = [] current_nodes = [] # 将非文本/非lb节点直接加入结果 processed_elements.append(node) # 处理最后一批缓存内容 if current_text: full_text = ''.join(current_text) sentences = sent_tokenize(full_text) ptr = 0 for sent in sentences: sent_nodes = [] remaining_sent = sent while ptr < len(current_nodes) and remaining_sent: n = current_nodes[ptr] if isinstance(n, str): if remaining_sent.startswith(n[:len(remaining_sent)]): sent_nodes.append(n[:len(remaining_sent)]) remaining_text = n[len(remaining_sent):] if remaining_text: current_nodes[ptr] = remaining_text else: ptr += 1 remaining_sent = '' else: sent_nodes.append(n) remaining_sent = remaining_sent[len(n):] ptr += 1 else: sent_nodes.append(n) remaining_sent = remaining_sent.replace(f'[[LB:{n.get("xml:id", "")}]]', '') ptr += 1 s_elem = etree.Element('s') for sn in sent_nodes: if isinstance(sn, str): if not s_elem.text: s_elem.text = sn else: last_child = s_elem[-1] if len(s_elem) > 0 else None if last_child: last_child.tail = (last_child.tail or '') + sn else: s_elem.text += sn else: s_elem.append(sn) processed_elements.append(s_elem) # 清空原容器,替换为处理后的内容 container.clear() for elem in processed_elements: container.append(elem) # 保存处理后的XML,保留原编码与格式 tree.write(output_path, encoding='utf-8', pretty_print=True, xml_declaration=True) # 调用示例:根据你的TEI结构调整text_container_xpath add_sentence_tags('input_tei.xml', 'output_tei.xml', text_container_xpath='//div[@type="text"]/p')
关键注意事项
- 调整XPath:必须根据你的TEI文件结构修改
text_container_xpath,定位到实际包含文本的容器元素(如<p>、<ab>) - 分词工具适配:NLTK的
sent_tokenize适合通用文本,若处理古文本或专业领域TEI,可替换为spaCy的专业模型(如en_core_web_sm)或自定义分词规则 - 测试验证:先处理小片段XML,检查
<s>标签是否正确包裹跨多行句子,<lb>节点的位置与属性是否完全保留
内容的提问来源于stack exchange,提问作者aarbeikop
相关产品推荐
相关产品推荐

