查看: 297|回复: 0

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

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在数据对接和系统集成工作中,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有表头、结构扁平的常规场景:
  1. import csv
  2. import xml.etree.ElementTree as ET
  3. def csv_to_xml_flat(csv_path, xml_path, root_name='root', row_name='row'):
  4.     root = ET.Element(root_name)
  5.     with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f:
  6.         reader = csv.DictReader(f)
  7.         reader.fieldnames = [name.strip() if name else name for name in reader.fieldnames]
  8.         for row in reader:
  9.             item = ET.SubElement(root, row_name)
  10.             for field in reader.fieldnames:
  11.                 value = row.get(field, '').strip()
  12.                 if value == '':
  13.                     continue
  14.                 child = ET.SubElement(item, field)
  15.                 child.text = value
  16.     tree = ET.ElementTree(root)
  17.     try:
  18.         ET.indent(tree, space=' ')
  19.     except AttributeError:
  20.         pass  # Python 3.8及以下可改用自定义pretty_print函数
  21.     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形式,例如:
  1. from lxml import etree
  2. import csv
  3. NS = 'http://example.com/namespace/devices'
  4. ET_NS = f'{{{NS}}}'
  5. def csv_to_xml_advanced(csv_path, xml_path):
  6.     root = etree.Element(f'{ET_NS}devices', nsmap={'dev': NS})
  7.     with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f:
  8.         reader = csv.DictReader(f)
  9.         for row in reader:
  10.             device = etree.SubElement(root, f'{ET_NS}device')
  11.             device.set('id', row.get('设备编号', '').strip())
  12.             etree.SubElement(device, f'{ET_NS}name').text = row.get('设备名称', '').strip()
  13.             etree.SubElement(device, f'{ET_NS}room').text = row.get('所属机房', '').strip()
  14.             status = etree.SubElement(device, f'{ET_NS}status')
  15.             status.text = etree.CDATA(row.get('状态描述', '').strip())
  16.     tree = etree.ElementTree(root)
  17.     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片段的方式,手动完成转义后直接输出:
  1. import csv
  2. def csv_to_xml_stream(csv_path, xml_path, root_name='root', row_name='row'):
  3.     with open(csv_path, 'r', encoding='utf-8-sig', newline='') as f, \
  4.          open(xml_path, 'w', encoding='utf-8', newline='\n') as out:
  5.         reader = csv.DictReader(f)
  6.         out.write('<?xml version="1.0" encoding="utf-8"?>\n')
  7.         out.write(f'<{root_name}>\n')
  8.         for row in reader:
  9.             out.write(f'  <{row_name}>\n')
  10.             for field, value in row.items():
  11.                 if value == '' or value is None:
  12.                     continue
  13.                 safe_field = field.strip()
  14.                 safe_value = value.strip().replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
  15.                 out.write(f'    <{safe_field}>{safe_value}</{safe_field}>\n')
  16.             out.write(f'  </{row_name}>\n')
  17.         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的机器上都能稳定运行。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-9 17:25 , Processed in 0.023922 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部