脚本专家 发表于 2026-9-9 15:00:01

Python CSV转XML脚本实现指南:编码处理与ElementTre

在数据对接和系统集成工作中,CSV转XML是脚本工程师经常面对的需求。CSV擅长表达扁平表格,XML则天然支持嵌套结构和属性,两者互补。实际项目中,上游数据常常以CSV交付,而财务、政务、制造业等老牌系统的接口却只认XML,因此需要一段可靠的Python转换脚本来自动完成格式桥接。

方案选型上,读取CSV优先使用内置csv模块的DictReader,而不是pandas。csv模块按行流式读取,内存占用平稳,几百万行数据处理时不会撑爆服务器;pandas默认将整个文件载入内存,在纯转换场景下过重。若CSV本身需要清洗或聚合,可以先用pandas处理完再导出临时CSV,最后仍用csv模块执行转换。生成XML时,不推荐手工拼接字符串,因为字段值中的&、<、>等特殊字符一旦漏转义,生成的XML就是非法的,下游解析器会直接报ParseError。推荐使用xml.etree.ElementTree或lxml树模型,库会自动完成转义。

读取CSV的细节最容易被忽视。Excel在简体中文环境另存的CSV默认是GBK编码,如果代码硬写encoding='utf-8',轻则乱码重则抛UnicodeDecodeError。稳妥做法是读取时用utf-8-sig,它兼容带BOM的UTF-8文件;更进一步可以写一个编码探测函数,依次尝试utf-8-sig、gbk、gb18030、latin1,以latin1作为最后兜底,保证脚本不会因为编码问题崩溃。无表头文件需要用reader逐行处理并自行维护字段映射;有表头时使用DictReader会得到键值对字典,但要注意表头和单元格中可能存在前后空格,建议统一strip()。空字段的合理策略是:值为空字符串或键不存在时,不生成对应的XML子元素,这样输出更干净,也能避免下游系统用空值做严格校验时报错。

下面是一份零第三方依赖的基础转换脚本,适用于CSV有表头、结构扁平的常规场景:


import csv
import xml.etree.ElementTree as ET

def csv_to_xml_flat(csv_path, xml_path, root_name='root', row_name='row'):
    root = ET.Element(root_name)
    with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f:
      reader = csv.DictReader(f)
      reader.fieldnames =
      for row in reader:
            item = ET.SubElement(root, row_name)
            for field in reader.fieldnames:
                value = row.get(field, '').strip()
                if value == '':
                  continue
                child = ET.SubElement(item, field)
                child.text = value
    tree = ET.ElementTree(root)
    try:
      ET.indent(tree, space=' ')
    except AttributeError:
      pass# Python 3.8及以下可改用自定义pretty_print函数
    tree.write(xml_path, encoding='utf-8', xml_declaration=True)


注意open时使用newline='',这是csv模块的官方推荐写法,防止CSV内部的换行符被操作系统二次转换。newline='\n'则用于输出文件,确保Linux和Windows下换行符一致。ET.indent()是Python 3.9才有的方法,老版本需要自己写递归函数,通过设置元素的text和tail属性插入换行和空格来美化输出。

当目标XML需要属性、命名空间或CDATA时,必须切换到lxml。lxml元素名使用{namespace}localname形式,例如:


from lxml import etree
import csv

NS = 'http://example.com/namespace/devices'
ET_NS = f'{{{NS}}}'

def csv_to_xml_advanced(csv_path, xml_path):
    root = etree.Element(f'{ET_NS}devices', nsmap={'dev': NS})
    with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f:
      reader = csv.DictReader(f)
      for row in reader:
            device = etree.SubElement(root, f'{ET_NS}device')
            device.set('id', row.get('设备编号', '').strip())
            etree.SubElement(device, f'{ET_NS}name').text = row.get('设备名称', '').strip()
            etree.SubElement(device, f'{ET_NS}room').text = row.get('所属机房', '').strip()
            status = etree.SubElement(device, f'{ET_NS}status')
            status.text = etree.CDATA(row.get('状态描述', '').strip())
    tree = etree.ElementTree(root)
    tree.write(xml_path, encoding='utf-8', xml_declaration=True, pretty_print=True)


etree.CDATA(value)会输出<!]>,特别适合字段中嵌入大段HTML或其他XML片段且希望原样保留的需求。如果CSV字段需要拆分成多层嵌套结构,比如订单下面挂客户节点、客户节点下再挂姓名,就不能遍历字段名自动建节点,而应维护一张“CSV字段名到节点路径”的映射表,代码按路径逐层创建父节点,只改映射表不循环主体逻辑。

对于几百万行的大文件,无论是ElementTree还是lxml,把整棵树构建在内存里都可能导致OOM。此时应绕开树模型,采用边读CSV边写XML片段的方式,手动完成转义后直接输出:


import csv

def csv_to_xml_stream(csv_path, xml_path, root_name='root', row_name='row'):
    with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f, \
         open(xml_path, 'w', encoding='utf-8', newline='\n') as out:
      reader = csv.DictReader(f)
      out.write('<?xml version="1.0" encoding="utf-8"?>\n')
      out.write(f'<{root_name}>\n')
      for row in reader:
            out.write(f'<{row_name}>\n')
            for field, value in row.items():
                if value == '' or value is None:
                  continue
                safe_field = field.strip()
                safe_value = value.strip().replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
                out.write(f'    <{safe_field}>{safe_value}</{safe_field}>\n')
            out.write(f'</{row_name}>\n')
      out.write(f'</{root_name}>\n')


流式写法要求转义顺序必须正确:&要最先替换,否则其他实体中的&会被二次转义成&amp;。这种方案只要转义无误,可稳定处理千万行级别的数据。

实际运行中常遇到的问题也有固定排查路径。下游解析报“not well-formed”时,优先检查是否手工拼接字符串且遗漏了转义,打开XML搜索裸的&即可定位。浏览器提示“This XML file does not appear to have any style information”不是错误,只是XML没有关联XSLT样式表,浏览器默认按树状展示,程序解析不受影响。中文乱码的根源基本是XML声明中的编码与实际写入编码不一致,使用tree.write时指定encoding='utf-8'会自动配套声明;手工拼接时则必须确保打开文件的encoding参数与声明一致。字段名包含空格、点号或中文时,生成XML标签会不合法,需要提前将表头映射为合法的元素名,例如将空格替换为下划线,或改用属性方式输出:element.set('field_name', value)。CSV中数据库导出的换行符可能撑破XML结构,读取时用newline=''可以避免csv模块对外部换行多做处理,但字段值内含的换行符仍会进入XML,若下游不允许这种裸换行,应在生成前将value中的换行替换为空格或转义为&#10;。

CSV转XML本质上不是格式替代,而是搭桥。用Python脚本的目的在于可控性:字段映射、根节点命名、空值策略、日期格式、是否使用CDATA与缩进都由自己决定。建议日常转换以csv模块加ElementTree为主,复杂结构和大数据量场景按需升级到lxml,输出文件统一utf-8声明,输入文件兼容utf-8-sig与GBK,这样脚本放到任何一台有Python的机器上都能稳定运行。

热心网友5 发表于 2026-9-9 19:00:00

Re: Python CSV转XML脚本实现指南:编码处理与ElementTre

学习了,楼主这篇写得很实用。尤其是编码探测那段,真实开发里遇到GBK的CSV太常见了,硬写utf-8确实容易翻车。用DictReader按行流式处理也符合我的习惯,之前用pandas做过一次大文件转换,内存直接爆了,后来改成csv模块就稳多了。 有个小细节想补充:XML字段名如果是从CSV表头来的,最好校验一下是否符合XML元素命名规则,比如数字开头或者带空格的情况,ElementTree会直接抛错的。可以用正则替换或者做个映射表处理一下。 另外楼主提到老版本Python可以用自定义pretty_print,我一般用xml.dom.minidom的toprettyxml来做格式化,虽然有点慢但胜在通用,简单场景下够用。 CDATA那段对做数据对接的人来说是刚需,很多业务系统确实会往XML里塞HTML片段,不用CDATA的话转义起来会头疼。楼主的代码示例简洁清楚,直接可以参考。期待看到后面关于多层嵌套结构的部分,希望继续更新!

热心网友5 发表于 2026-9-9 19:00:00

Re: Python CSV转XML脚本实现指南:编码处理与ElementTre

楼主的方案很实用,尤其“编码探测 + utf-8-sig”这一招深有同感,实际对接Excel导出的CSV时,GBK简直是绕不开的坑。之前我图省事直接硬编码utf-8,结果线上日志刷了一屏UnicodeDecodeError,后来也是老老实实加了兜底逻辑。 另外你提到“空字段不生成子元素”这点我也很赞成。下游系统有的会把空标签当“显式空值”处理,导致可选字段变成必填校验,直接把整批数据拒了,确实不如直接省略干净。 对于老版本Python的格式化输出,之前我也写过递归设置tail和text的土办法,后来升级到3.9用ET.indent简直是解放双手。不过有个小提醒:如果生成的文件要给很老的下游解析器用,缩进和声明其实可有可无,重点是转义和结构别出错。 lxml那部分对命名空间和CDATA的处理讲得很到位,我现在碰到需要嵌入HTML片段的字段也喜欢用CDATA包一层,省得转义时头大。总之这帖能少让后来者踩不少坑,感谢分享。

热心网友2 发表于 2026-9-9 19:10:00

Re: Python CSV转XML脚本实现指南:编码处理与ElementTre

受益匪浅,尤其是指出csv模块流式读取和utf-8-sig编码这一点,实际项目里确实经常遇到Excel另存CSV导致乱码的问题。另外lxml那段关于命名空间和CDATA的写法也很实用,收藏了。
页: [1]
查看完整版本: Python CSV转XML脚本实现指南:编码处理与ElementTre