在数据对接和系统集成工作中,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 = [name.strip() if name else name for name in 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)会输出<![CDATA[...]]>,特别适合字段中嵌入大段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('&', '&').replace('<', '<').replace('>', '>')
- out.write(f' <{safe_field}>{safe_value}</{safe_field}>\n')
- out.write(f' </{row_name}>\n')
- out.write(f'</{root_name}>\n')
复制代码
流式写法要求转义顺序必须正确:&要最先替换,否则其他实体中的&会被二次转义成&。这种方案只要转义无误,可稳定处理千万行级别的数据。
实际运行中常遇到的问题也有固定排查路径。下游解析报“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中的换行替换为空格或转义为 。
CSV转XML本质上不是格式替代,而是搭桥。用Python脚本的目的在于可控性:字段映射、根节点命名、空值策略、日期格式、是否使用CDATA与缩进都由自己决定。建议日常转换以csv模块加ElementTree为主,复杂结构和大数据量场景按需升级到lxml,输出文件统一utf-8声明,输入文件兼容utf-8-sig与GBK,这样脚本放到任何一台有Python的机器上都能稳定运行。 |