折腾笔记[59]-使用标准库将xml转xlsx2007格式
摘要
仅使用go语言标准库, 将xml文件转为xlsx2007格式.
声明
本文人类为第一作者, 龙虾为通讯作者. 本文有AI生成内容.
简介
xml格式简介
[https://developer.mozilla.org/zh-CN/docs/Web/XML/Guides/XML_introduction]
XML(可扩展标记语言,eXtensible Markup Language)是一种用于存储和传输数据的标记语言。它与HTML类似,但没有预定义的标签,允许用户自定义标签来构建结构化的数据模型,广泛应用于数据交换和系统配置文件中。
XML(Extensible Markup Language)是一种类似于 HTML,但是没有使用预定义标记的语言。因此,可以根据自己的设计需求定义专属的标记。这是一种强大将数据存储在一个可以存储、搜索和共享的格式中的方法。最重要的是,因为 XML 的基本格式是标准化的,如果你在本地或互联网上跨系统或平台共享或传输 XML,由于标准化的 XML 语法,接收者仍然可以解析数据。
有许多基于 XML 的语言,包括 XHTML、MathML、SVG、RSS 和 RDF。你也可以创建自己的。
XML——声明并非是一种标签,其用于传输文档的元数据。
判定一个 XML 文档正确的标准是:
文档必须是一个格式良好的文档。
文档遵循 XML 所有的语法规则并且有效。
文档遵循特定语义的规则,这些规则通常规定在 XML 或 DTD 规范中(文档类型定义)。
<?xml version="1.0" encoding="UTF-8"?>
OOXML格式简介
[https://zh.wikipedia.org/zh-cn/Office_Open_XML]
[http://www.ecma-international.org/publications/standards/Ecma-376.htm]
[http://www.iso.org/iso/home/store/catalogue_tc/catalogue_detail.htm?csnumber=61750]
[https://www.ecma-international.org/wp-content/uploads/OpenXML_White_Paper_Chinese.pdf]
[https://juejin.cn/post/7637797087795511332]
[https://05t3.github.io/posts/mshtml/]
- 标准: ECMA-376, ISO/IEC 29500
OOXML(Office Open XML),是由微软开发、现已成为国际标准的一种办公文档格式。它基于XML语言,采用ZIP压缩技术存储文字、表格和演示文稿等内容。我们常见的 .docx、.xlsx 和 .pptx 等均属于此格式。
微软公司发表的Office Open XML使用许多非标准的规范,造成与其他办公室软件(例如LibreOffice)读取时发生不兼容或内容偏移的情形。
本质特征:OOXML 不是“新的独立压缩格式”,而是建立在 ZIP 容器和 OPC(Open Packaging Conventions,开放包装约定)之上的 XML 文档包。
xlsx2007格式简介
[https://blog.lindexi.com/post/Office-文档解析-文档格式和协议.html]
[https://learn.microsoft.com/en-us/previous-versions/office/office-12/ee361919(v=office.12)?redirectedfrom=MSDN]
.xlsx 是自 Microsoft Office Excel 2007 起引入的默认电子表格文件格式。它基于开放的 Office Open XML (OOXML) 标准,采用 ZIP 压缩的 XML 结构存储数据,全面取代了旧版的 .xls 二进制格式。
xlsx 文件本质上是一个 ZIP 压缩包,包含以下 Office Open XML 文件:
[Content_Types].xml # 内容类型定义
_rels/.rels # 包关系定义
xl/_rels/workbook.xml.rels # 工作簿关系定义
xl/workbook.xml # 工作簿(包含工作表列表)
xl/styles.xml # 样式定义(含表头蓝色背景)
xl/sharedStrings.xml # 共享字符串表(去重优化)
xl/worksheets/sheet1.xml # 工作表数据
在 OOXML 格式里面,如上文所说是基于 zip+xml 定义的,这里的 Zip 提供文件的支持,而 xml 提供内容的支持。不过 OOXML 使用的 zip 也是有规范的,这里使用 OPC (Open Package Convention) 中文名叫 开放打包协定 作为文件存储格式。当然,这并非说 OPC 使用特殊的 zip 格式,而是 OPC 规定了文件存放的存储格式,然后将这些文件使用 zip 打包为一个文件。因此 一个 OPC 文件(不管其文件后缀是什么)本质上就是一个 zip 文件,你可以用任何常见的解压软件进行解压,解压后你看到的那些文件的组织结构,就是以 OPC 定义的方式存储的
在 ECMA-376,Fourth Edition,Part 2 详细定义了 OPC 开放打包协定。在 OPC 里面有三个重要的概念,分别是 Part 和 Relationship 和 ContentTypes 这三个.
excelize库简介
[https://github.com/qax-os/excelize]
[https://xuri.me/excelize/zh-hans/]
[https://github.com/xuri/excelize]
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.24.0 或更高版本。
Excelize 的目标是创建并维护一个 Go 语言版本的 Excel 文档 API,以处理符合基于 Office Open XML(OOXML)标准的电子表格文档,借助 Excelize 您可以使用 Go 读取和写入 MS Excel 文件。
在一些情况下我们需要通过程序操作 Excel 文档,例如:打开读取已有 Excel 文档内容、创建新的 Excel 文档、基于已有文档(模版)生成新的 Excel 文档、向 Excel 文档中插入图片、图表和表格等元素,有时还需要跨平台实现这些操作。使用 Excelize 可以方便的满足上述需求。
工程
功能
一个使用 Go 标准库编写的命令行工具,支持将任意 XML 文件转换为 Excel 2007 格式 (.xlsx)。无需外部依赖,仅使用 Go 标准库实现。
- 仅使用 Go 标准库:
archive/zip、encoding/xml、bufio等,零外部依赖 - 支持任意 XML 结构:自动识别数据记录,无需预定义 XML 结构
- 交互式 CLI:引导式输入,支持文件拖拽路径
- 智能数据识别:
- 自动检测列表容器(多个同构子节点)
- 自动检测纯数据记录节点
- 自动将数值/百分比转为数值单元格
- 共享字符串去重优化,减小文件体积
- 完整的 xlsx2007 格式:包含样式、共享字符串表、工作表等完整 Office Open XML 结构
程序使用三层策略自动发现 XML 中的数据记录:
- 列表容器检测:查找包含多个同名子节点的节点(如
<books><book>...</book><book>...</book></books>) - 纯数据记录检测:查找所有子节点都是叶子节点的节点
- 根节点兜底:将根节点本身作为数据记录
自动识别以下格式的数值:
- 整数:
100、-50 - 小数:
10.99、30.5 - 百分比:
100.0000%、50.5%
代码
main.go
package main
import (
"archive/zip"
"bufio"
"encoding/xml"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// ==================== 通用XML节点解析 ====================
// XMLNode 表示一个通用XML节点
type XMLNode struct {
Name xml.Name
Attributes []xml.Attr
Content string
Children []*XMLNode
Parent *XMLNode
}
// UnmarshalXML 实现xml.Unmarshaler接口,用于通用解析
func (n *XMLNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
n.Name = start.Name
n.Attributes = start.Attr
n.Children = nil
n.Content = ""
for {
tok, err := d.Token()
if err != nil {
return err
}
switch t := tok.(type) {
case xml.StartElement:
child := &XMLNode{Parent: n}
if err := child.UnmarshalXML(d, t); err != nil {
return err
}
n.Children = append(n.Children, child)
case xml.CharData:
text := strings.TrimSpace(string(t))
if text != "" {
n.Content = text
}
case xml.EndElement:
return nil
}
}
}
// IsLeaf 判断是否为叶子节点(无子节点)
func (n *XMLNode) IsLeaf() bool {
return len(n.Children) == 0
}
// IsDataRecord 判断是否为数据记录节点(包含多个同类型的叶子子节点或同构子节点)
func (n *XMLNode) IsDataRecord() bool {
if len(n.Children) == 0 {
return false
}
// 如果大多数子节点都是叶子节点(至少2个),则为数据记录
leafCount := 0
for _, child := range n.Children {
if child.IsLeaf() {
leafCount++
}
}
// 至少要有2个叶子子节点才算数据记录
return leafCount >= 2
}
// IsPureDataRecord 判断是否为纯数据记录(所有子节点都是叶子)
func (n *XMLNode) IsPureDataRecord() bool {
if len(n.Children) == 0 {
return false
}
for _, child := range n.Children {
if !child.IsLeaf() {
return false
}
}
return len(n.Children) >= 2
}
// IsListContainer 判断是否为列表容器(包含多个同名的数据记录子节点)
func (n *XMLNode) IsListContainer() bool {
if len(n.Children) < 2 {
return false
}
firstName := n.Children[0].Name.Local
for _, child := range n.Children {
if child.Name.Local != firstName {
return false
}
}
return true
}
// GetAllLeafPaths 获取所有叶子节点的路径(从根到叶子的节点名链)
func (n *XMLNode) GetAllLeafPaths(prefix string) []string {
var paths []string
currentPath := n.Name.Local
if prefix != "" {
currentPath = prefix + "/" + n.Name.Local
}
if n.IsLeaf() {
return []string{currentPath}
}
for _, child := range n.Children {
paths = append(paths, child.GetAllLeafPaths(currentPath)...)
}
return paths
}
// GetLeafValue 根据路径获取叶子节点的值
func (n *XMLNode) GetLeafValue(path string) string {
parts := strings.Split(path, "/")
if len(parts) == 0 {
return ""
}
// 检查当前节点名是否匹配路径第一部分
if n.Name.Local != parts[0] {
return ""
}
if len(parts) == 1 {
return n.Content
}
// 递归查找子节点
for _, child := range n.Children {
val := child.GetLeafValue(strings.Join(parts[1:], "/"))
if val != "" || child.Name.Local == parts[1] {
return val
}
}
return ""
}
// GetAttributeString 获取属性字符串表示
func (n *XMLNode) GetAttributeString() string {
if len(n.Attributes) == 0 {
return ""
}
var parts []string
for _, attr := range n.Attributes {
parts = append(parts, fmt.Sprintf("%s=%s", attr.Name.Local, attr.Value))
}
return strings.Join(parts, "; ")
}
// ==================== XML文档解析 ====================
// XMLDocument 表示解析后的XML文档
type XMLDocument struct {
Root *XMLNode
Records []*XMLNode // 发现的数据记录节点列表
Headers []string // 表头(叶子节点路径)
HasRecords bool // 是否发现数据记录
}
// ParseXMLFile 解析XML文件为通用文档
func ParseXMLFile(filePath string) (*XMLDocument, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("打开XML文件失败: %w", err)
}
defer file.Close()
decoder := xml.NewDecoder(file)
var root *XMLNode
for {
tok, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("解析XML失败: %w", err)
}
switch t := tok.(type) {
case xml.StartElement:
root = &XMLNode{}
if err := root.UnmarshalXML(decoder, t); err != nil {
return nil, fmt.Errorf("解析XML节点失败: %w", err)
}
}
}
if root == nil {
return nil, fmt.Errorf("XML文件为空或格式错误")
}
doc := &XMLDocument{Root: root}
doc.discoverRecords()
doc.buildHeaders()
return doc, nil
}
// discoverRecords 自动发现数据记录节点
func (doc *XMLDocument) discoverRecords() {
doc.Records = nil
doc.HasRecords = false
// 策略1: 查找列表容器(包含多个同名子节点的节点)
var findListContainers func(node *XMLNode)
findListContainers = func(node *XMLNode) {
if node.IsListContainer() {
// 检查子节点是否为数据记录(有多个叶子子节点)
// 或者子节点是叶子节点但有属性(如 <feature id="1">value</feature>)
firstChild := node.Children[0]
if firstChild.IsPureDataRecord() || firstChild.IsDataRecord() {
doc.Records = append(doc.Records, node.Children...)
doc.HasRecords = true
return
}
// 如果子节点是叶子节点且有属性,也认为是数据记录
if firstChild.IsLeaf() && len(firstChild.Attributes) > 0 {
doc.Records = append(doc.Records, node.Children...)
doc.HasRecords = true
return
}
}
for _, child := range node.Children {
findListContainers(child)
}
}
findListContainers(doc.Root)
// 策略2: 如果没有找到列表容器,查找直接的数据记录节点
// 跳过列表容器(已在策略1中处理)和根节点(在策略3中处理)
if !doc.HasRecords {
var findDataRecords func(node *XMLNode)
findDataRecords = func(node *XMLNode) {
// 跳过列表容器和根节点
if node.IsListContainer() || node == doc.Root {
for _, child := range node.Children {
findDataRecords(child)
}
return
}
if node.IsPureDataRecord() {
doc.Records = append(doc.Records, node)
doc.HasRecords = true
return
}
for _, child := range node.Children {
findDataRecords(child)
}
}
findDataRecords(doc.Root)
}
// 策略3: 如果根节点本身就是数据记录(有足够数量的叶子子节点)
if !doc.HasRecords && doc.Root.IsDataRecord() {
doc.Records = append(doc.Records, doc.Root)
doc.HasRecords = true
}
}
// buildHeaders 构建表头
func (doc *XMLDocument) buildHeaders() {
doc.Headers = nil
if len(doc.Records) == 0 {
// 没有数据记录时,将所有叶子节点作为表头
paths := doc.Root.GetAllLeafPaths("")
// 去重并排序
seen := make(map[string]bool)
for _, p := range paths {
// 只保留最后一级作为表头名
parts := strings.Split(p, "/")
name := parts[len(parts)-1]
if !seen[name] {
seen[name] = true
doc.Headers = append(doc.Headers, name)
}
}
sort.Strings(doc.Headers)
return
}
// 有数据记录时,收集所有可能的字段名
seen := make(map[string]bool)
for _, record := range doc.Records {
for _, child := range record.Children {
name := child.Name.Local
if !seen[name] {
seen[name] = true
doc.Headers = append(doc.Headers, name)
}
}
}
sort.Strings(doc.Headers)
}
// GetRecordValue 获取记录中指定字段的值
func (doc *XMLDocument) GetRecordValue(record *XMLNode, header string) string {
for _, child := range record.Children {
if child.Name.Local == header {
if child.IsLeaf() {
return child.Content
}
// 非叶子节点,尝试获取其所有叶子内容拼接
return child.getAllLeafContent()
}
}
return ""
}
// getAllLeafContent 获取节点下所有叶子节点内容的拼接
func (n *XMLNode) getAllLeafContent() string {
if n.IsLeaf() {
return n.Content
}
var parts []string
for _, child := range n.Children {
if content := child.getAllLeafContent(); content != "" {
parts = append(parts, content)
}
}
return strings.Join(parts, "; ")
}
// GetSummaryInfo 获取汇总信息
func (doc *XMLDocument) GetSummaryInfo() map[string]interface{} {
info := make(map[string]interface{})
info["总记录数"] = len(doc.Records)
info["表头数"] = len(doc.Headers)
info["根节点"] = doc.Root.Name.Local
// 如果有属性,显示根节点属性
if len(doc.Root.Attributes) > 0 {
info["根属性"] = doc.Root.GetAttributeString()
}
return info
}
// ==================== xlsx2007生成 (OfficeOpenXML) ====================
// XlsxBuilder xlsx构建器
type XlsxBuilder struct {
sharedStrings []string
stringIndex map[string]int
}
// NewXlsxBuilder 创建xlsx构建器
func NewXlsxBuilder() *XlsxBuilder {
return &XlsxBuilder{
sharedStrings: make([]string, 0),
stringIndex: make(map[string]int),
}
}
// AddString 添加共享字符串,返回索引
func (xb *XlsxBuilder) AddString(s string) int {
if idx, ok := xb.stringIndex[s]; ok {
return idx
}
idx := len(xb.sharedStrings)
xb.sharedStrings = append(xb.sharedStrings, s)
xb.stringIndex[s] = idx
return idx
}
// EscapeXml XML转义
func EscapeXml(text string) string {
if text == "" {
return ""
}
return strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
"\"", """,
"'", "'",
).Replace(text)
}
// GetContentTypesXml [Content_Types].xml
func GetContentTypesXml() string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
</Types>`
}
// GetRelsXml _rels/.rels
func GetRelsXml() string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>`
}
// GetWorkbookRelsXml xl/_rels/workbook.xml.rels
func GetWorkbookRelsXml() string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
</Relationships>`
}
// GetWorkbookXml xl/workbook.xml
func GetWorkbookXml(sheetName string) string {
escapedName := EscapeXml(sheetName)
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="%s" sheetId="1" r:id="rId1"/>
</sheets>
</workbook>`, escapedName)
}
// GetStylesXml xl/styles.xml
func GetStylesXml() string {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="2">
<font><sz val="11"/><name val="Calibri"/><family val="2"/></font>
<font><sz val="11"/><name val="Calibri"/><family val="2"/><b/></font>
</fonts>
<fills count="3">
<fill><patternFill patternType="none"/></fill>
<fill><patternFill patternType="gray125"/></fill>
<fill><patternFill patternType="solid"><fgColor rgb="FF4472C4"/><bgColor rgb="FF4472C4"/></patternFill></fill>
</fills>
<borders count="1">
<border><left/><right/><top/><bottom/><diagonal/></border>
</borders>
<cellStyleXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
</cellStyleXfs>
<cellXfs count="3">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"/>
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
</cellXfs>
</styleSheet>`
}
// BuildSharedStringsXml 构建共享字符串XML
func (xb *XlsxBuilder) BuildSharedStringsXml() string {
var sb strings.Builder
sb.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n")
sb.WriteString(`<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="`)
sb.WriteString(strconv.Itoa(len(xb.sharedStrings)))
sb.WriteString(`" uniqueCount="`)
sb.WriteString(strconv.Itoa(len(xb.sharedStrings)))
sb.WriteString(`">` + "\n")
for _, s := range xb.sharedStrings {
sb.WriteString(" <si><t>")
sb.WriteString(EscapeXml(s))
sb.WriteString("</t></si>\n")
}
sb.WriteString("</sst>\n")
return sb.String()
}
// BuildSheetXml 构建工作表XML
func (xb *XlsxBuilder) BuildSheetXml(doc *XMLDocument) string {
var sb strings.Builder
sb.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n")
sb.WriteString(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">` + "\n")
sb.WriteString(" <sheetData>\n")
rowNum := 1
headers := doc.Headers
// 表头行
sb.WriteString(xb.getRowStart(rowNum, 22))
cols := generateColumnNames(len(headers))
for i, h := range headers {
sb.WriteString(xb.getSharedStringCell(cols[i]+strconv.Itoa(rowNum), h, 1))
}
sb.WriteString(xb.getRowEnd())
rowNum++
// 数据行
if doc.HasRecords {
for _, record := range doc.Records {
sb.WriteString(xb.getRowStart(rowNum, 18))
for i, h := range headers {
val := doc.GetRecordValue(record, h)
cellRef := cols[i] + strconv.Itoa(rowNum)
if isNumeric(val) {
sb.WriteString(xb.getNumberCell(cellRef, parseNumeric(val)))
} else {
sb.WriteString(xb.getSharedStringCell(cellRef, val, 0))
}
}
sb.WriteString(xb.getRowEnd())
rowNum++
}
} else {
// 没有数据记录时,将根节点作为单行数据
sb.WriteString(xb.getRowStart(rowNum, 18))
for i, h := range headers {
val := doc.Root.GetLeafValue(h)
// 尝试从任意路径匹配
if val == "" {
val = findValueByName(doc.Root, h)
}
cellRef := cols[i] + strconv.Itoa(rowNum)
if isNumeric(val) {
sb.WriteString(xb.getNumberCell(cellRef, parseNumeric(val)))
} else {
sb.WriteString(xb.getSharedStringCell(cellRef, val, 0))
}
}
sb.WriteString(xb.getRowEnd())
rowNum++
}
sb.WriteString(" </sheetData>\n")
sb.WriteString("</worksheet>\n")
return sb.String()
}
// findValueByName 根据名称在节点树中查找值
func findValueByName(node *XMLNode, name string) string {
if node.Name.Local == name && node.IsLeaf() {
return node.Content
}
for _, child := range node.Children {
if val := findValueByName(child, name); val != "" {
return val
}
}
return ""
}
// isNumeric 判断字符串是否为数值
func isNumeric(s string) bool {
if s == "" {
return false
}
// 去除百分号
s = strings.TrimSuffix(s, "%")
// 尝试解析为float
_, err := strconv.ParseFloat(s, 64)
return err == nil
}
// parseNumeric 解析数值字符串
func parseNumeric(s string) float64 {
s = strings.TrimSuffix(s, "%")
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return val
}
// generateColumnNames 生成列名 A, B, C, ..., Z, AA, AB, ...
func generateColumnNames(count int) []string {
cols := make([]string, count)
for i := 0; i < count; i++ {
cols[i] = toColumnName(i)
}
return cols
}
// toColumnName 将数字转换为Excel列名
func toColumnName(n int) string {
name := ""
for n >= 0 {
name = string(rune('A'+n%26)) + name
n = n/26 - 1
if n < 0 {
break
}
}
return name
}
func (xb *XlsxBuilder) getRowStart(rowNum, height int) string {
return fmt.Sprintf(" <row r=\"%d\" ht=\"%d\">\n", rowNum, height)
}
func (xb *XlsxBuilder) getRowEnd() string {
return " </row>\n"
}
func (xb *XlsxBuilder) getSharedStringCell(cellRef, value string, styleId int) string {
index := xb.AddString(value)
return fmt.Sprintf(" <c r=\"%s\" t=\"s\" s=\"%d\"><v>%d</v></c>\n", cellRef, styleId, index)
}
func (xb *XlsxBuilder) getNumberCell(cellRef string, value float64) string {
return fmt.Sprintf(" <c r=\"%s\"><v>%g</v></c>\n", cellRef, value)
}
// PrepareSharedStrings 预准备所有共享字符串
func (xb *XlsxBuilder) PrepareSharedStrings(doc *XMLDocument) {
// 表头字符串
for _, h := range doc.Headers {
xb.AddString(h)
}
// 数据值
if doc.HasRecords {
for _, record := range doc.Records {
for _, h := range doc.Headers {
val := doc.GetRecordValue(record, h)
xb.AddString(val)
}
}
} else {
for _, h := range doc.Headers {
val := doc.Root.GetLeafValue(h)
if val == "" {
val = findValueByName(doc.Root, h)
}
xb.AddString(val)
}
}
}
// BuildXlsxFile 构建xlsx文件
func BuildXlsxFile(outputPath string, doc *XMLDocument) error {
xb := NewXlsxBuilder()
xb.PrepareSharedStrings(doc)
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("创建输出文件失败: %w", err)
}
defer file.Close()
zw := zip.NewWriter(file)
defer zw.Close()
sheetName := doc.Root.Name.Local
if len(sheetName) > 31 {
sheetName = sheetName[:31]
}
// [Content_Types].xml
if err := writeZipEntry(zw, "[Content_Types].xml", GetContentTypesXml()); err != nil {
return err
}
// _rels/.rels
if err := writeZipEntry(zw, "_rels/.rels", GetRelsXml()); err != nil {
return err
}
// xl/_rels/workbook.xml.rels
if err := writeZipEntry(zw, "xl/_rels/workbook.xml.rels", GetWorkbookRelsXml()); err != nil {
return err
}
// xl/workbook.xml
if err := writeZipEntry(zw, "xl/workbook.xml", GetWorkbookXml(sheetName)); err != nil {
return err
}
// xl/styles.xml
if err := writeZipEntry(zw, "xl/styles.xml", GetStylesXml()); err != nil {
return err
}
// xl/sharedStrings.xml
if err := writeZipEntry(zw, "xl/sharedStrings.xml", xb.BuildSharedStringsXml()); err != nil {
return err
}
// xl/worksheets/sheet1.xml
if err := writeZipEntry(zw, "xl/worksheets/sheet1.xml", xb.BuildSheetXml(doc)); err != nil {
return err
}
return zw.Close()
}
func writeZipEntry(zw *zip.Writer, name, content string) error {
w, err := zw.Create(name)
if err != nil {
return fmt.Errorf("创建zip条目 %s 失败: %w", name, err)
}
_, err = w.Write([]byte(content))
if err != nil {
return fmt.Errorf("写入zip条目 %s 失败: %w", name, err)
}
return nil
}
// ==================== 交互式CLI ====================
func main() {
fmt.Println("========================================")
fmt.Println(" 通用 XML 转 xlsx2007 转换工具")
fmt.Println("========================================")
fmt.Println()
reader := bufio.NewReader(os.Stdin)
// 交互式询问XML文件路径
var xmlPath string
for {
fmt.Print("请输入XML文件路径 (或拖拽文件到此处): ")
input, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "读取输入失败: %v\n", err)
os.Exit(1)
}
// 去除首尾空白和引号
xmlPath = strings.TrimSpace(input)
xmlPath = strings.Trim(xmlPath, `"'`)
if xmlPath == "" {
fmt.Println("路径不能为空,请重新输入。")
continue
}
// 检查文件是否存在
if _, err := os.Stat(xmlPath); os.IsNotExist(err) {
fmt.Printf("文件不存在: %s\n", xmlPath)
continue
}
break
}
fmt.Printf("\n正在解析XML文件: %s\n", xmlPath)
// 解析XML
doc, err := ParseXMLFile(xmlPath)
if err != nil {
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
os.Exit(1)
}
fmt.Printf("解析成功!\n")
fmt.Printf(" 根节点: %s\n", doc.Root.Name.Local)
// 显示汇总信息
summary := doc.GetSummaryInfo()
for k, v := range summary {
fmt.Printf(" %s: %v\n", k, v)
}
fmt.Printf(" 表头: %v\n", doc.Headers)
fmt.Printf(" 发现数据记录: %v\n", doc.HasRecords)
if doc.HasRecords {
fmt.Printf(" 记录数量: %d\n", len(doc.Records))
}
fmt.Println()
// 询问输出路径
defaultOutput := generateDefaultOutputPath(xmlPath)
fmt.Printf("请输入输出xlsx文件路径 (直接回车使用默认: %s): ", defaultOutput)
outputInput, _ := reader.ReadString('\n')
outputPath := strings.TrimSpace(outputInput)
outputPath = strings.Trim(outputPath, `"'`)
if outputPath == "" {
outputPath = defaultOutput
}
// 确保输出目录存在
dir := filepath.Dir(outputPath)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "创建输出目录失败: %v\n", err)
os.Exit(1)
}
}
fmt.Printf("\n正在生成xlsx文件...\n")
// 生成xlsx
if err := BuildXlsxFile(outputPath, doc); err != nil {
fmt.Fprintf(os.Stderr, "生成xlsx失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("\n✅ 转换成功!\n")
fmt.Printf(" 输出文件: %s\n", outputPath)
fmt.Println()
fmt.Println("按回车键退出...")
reader.ReadString('\n')
}
// generateDefaultOutputPath 生成默认输出路径
func generateDefaultOutputPath(xmlPath string) string {
dir := filepath.Dir(xmlPath)
base := filepath.Base(xmlPath)
ext := filepath.Ext(base)
name := strings.TrimSuffix(base, ext)
timestamp := time.Now().Format("20060102150405")
return filepath.Join(dir, fmt.Sprintf("%s_%s.xlsx", name, timestamp))
}
XmlToXlsx.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace exp325_xml_to_xlsx2007;
// ==================== 通用XML节点解析 ====================
/// <summary>
/// 表示一个通用XML节点
/// </summary>
public class XMLNode
{
public string Name { get; set; } = "";
public Dictionary<string, string> Attributes { get; set; } = new();
public string Content { get; set; } = "";
public List<XMLNode> Children { get; set; } = new();
public XMLNode? Parent { get; set; }
/// <summary>
/// 判断是否为叶子节点(无子节点)
/// </summary>
public bool IsLeaf() => Children.Count == 0;
/// <summary>
/// 判断是否为数据记录节点(包含多个同类型的叶子子节点或同构子节点)
/// </summary>
public bool IsDataRecord()
{
if (Children.Count == 0) return false;
int leafCount = Children.Count(c => c.IsLeaf());
return leafCount >= 2;
}
/// <summary>
/// 判断是否为纯数据记录(所有子节点都是叶子)
/// </summary>
public bool IsPureDataRecord()
{
if (Children.Count == 0) return false;
foreach (var child in Children)
{
if (!child.IsLeaf()) return false;
}
return Children.Count >= 2;
}
/// <summary>
/// 判断是否为列表容器(包含多个同名的数据记录子节点)
/// </summary>
public bool IsListContainer()
{
if (Children.Count < 2) return false;
string firstName = Children[0].Name;
foreach (var child in Children)
{
if (child.Name != firstName) return false;
}
return true;
}
/// <summary>
/// 获取所有叶子节点的路径(从根到叶子的节点名链)
/// </summary>
public List<string> GetAllLeafPaths(string prefix)
{
var paths = new List<string>();
string currentPath = string.IsNullOrEmpty(prefix) ? Name : $"{prefix}/{Name}";
if (IsLeaf())
{
paths.Add(currentPath);
return paths;
}
foreach (var child in Children)
{
paths.AddRange(child.GetAllLeafPaths(currentPath));
}
return paths;
}
/// <summary>
/// 根据路径获取叶子节点的值
/// </summary>
public string GetLeafValue(string path)
{
var parts = path.Split('/');
if (parts.Length == 0) return "";
if (Name != parts[0]) return "";
if (parts.Length == 1) return Content;
string remainingPath = string.Join("/", parts.Skip(1));
foreach (var child in Children)
{
string val = child.GetLeafValue(remainingPath);
if (!string.IsNullOrEmpty(val) || child.Name == parts[1])
{
return val;
}
}
return "";
}
/// <summary>
/// 获取属性字符串表示
/// </summary>
public string GetAttributeString()
{
if (Attributes.Count == 0) return "";
return string.Join("; ", Attributes.Select(a => $"{a.Key}={a.Value}"));
}
/// <summary>
/// 获取节点下所有叶子节点内容的拼接
/// </summary>
public string GetAllLeafContent()
{
if (IsLeaf()) return Content;
var parts = new List<string>();
foreach (var child in Children)
{
string content = child.GetAllLeafContent();
if (!string.IsNullOrEmpty(content))
{
parts.Add(content);
}
}
return string.Join("; ", parts);
}
}
// ==================== XML文档解析 ====================
/// <summary>
/// 表示解析后的XML文档
/// </summary>
public class XMLDocument
{
public XMLNode Root { get; set; } = new XMLNode();
public List<XMLNode> Records { get; set; } = new();
public List<string> Headers { get; set; } = new();
public bool HasRecords { get; set; }
/// <summary>
/// 解析XML文件为通用文档
/// </summary>
public static XMLDocument ParseXMLFile(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"打开XML文件失败: {filePath}");
}
using var reader = XmlReader.Create(filePath);
XMLNode? root = null;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
root = ReadNode(reader);
}
}
if (root == null)
{
throw new InvalidOperationException("XML文件为空或格式错误");
}
var doc = new XMLDocument { Root = root };
doc.DiscoverRecords();
doc.BuildHeaders();
return doc;
}
/// <summary>
/// 递归读取XML节点
/// </summary>
private static XMLNode ReadNode(XmlReader reader)
{
var node = new XMLNode
{
Name = reader.Name
};
if (reader.HasAttributes)
{
while (reader.MoveToNextAttribute())
{
node.Attributes[reader.Name] = reader.Value;
}
reader.MoveToElement();
}
if (reader.IsEmptyElement)
{
return node;
}
string accumulatedText = "";
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
var child = ReadNode(reader);
child.Parent = node;
if (!string.IsNullOrWhiteSpace(accumulatedText))
{
node.Content = accumulatedText.Trim();
accumulatedText = "";
}
node.Children.Add(child);
break;
case XmlNodeType.Text:
case XmlNodeType.CDATA:
accumulatedText += reader.Value;
break;
case XmlNodeType.EndElement:
if (reader.Name == node.Name)
{
if (!string.IsNullOrWhiteSpace(accumulatedText))
{
node.Content = accumulatedText.Trim();
}
return node;
}
break;
}
}
return node;
}
/// <summary>
/// 自动发现数据记录节点
/// </summary>
private void DiscoverRecords()
{
Records.Clear();
HasRecords = false;
// 策略1: 查找列表容器(包含多个同名子节点的节点)
FindListContainers(Root);
// 策略2: 如果没有找到列表容器,查找直接的数据记录节点
if (!HasRecords)
{
FindDataRecords(Root);
}
// 策略3: 如果根节点本身就是数据记录(有足够数量的叶子子节点)
if (!HasRecords && Root.IsDataRecord())
{
Records.Add(Root);
HasRecords = true;
}
}
private void FindListContainers(XMLNode node)
{
if (node.IsListContainer())
{
var firstChild = node.Children[0];
if (firstChild.IsPureDataRecord() || firstChild.IsDataRecord())
{
Records.AddRange(node.Children);
HasRecords = true;
return;
}
if (firstChild.IsLeaf() && firstChild.Attributes.Count > 0)
{
Records.AddRange(node.Children);
HasRecords = true;
return;
}
}
foreach (var child in node.Children)
{
FindListContainers(child);
}
}
private void FindDataRecords(XMLNode node)
{
if (node.IsListContainer() || node == Root)
{
foreach (var child in node.Children)
{
FindDataRecords(child);
}
return;
}
if (node.IsPureDataRecord())
{
Records.Add(node);
HasRecords = true;
return;
}
foreach (var child in node.Children)
{
FindDataRecords(child);
}
}
/// <summary>
/// 构建表头
/// </summary>
private void BuildHeaders()
{
Headers.Clear();
if (Records.Count == 0)
{
// 没有数据记录时,将所有叶子节点作为表头
var paths = Root.GetAllLeafPaths("");
var seen = new HashSet<string>();
foreach (var p in paths)
{
var parts = p.Split('/');
string name = parts[^1];
if (seen.Add(name))
{
Headers.Add(name);
}
}
Headers.Sort();
return;
}
// 有数据记录时,收集所有可能的字段名
var headerSet = new HashSet<string>();
foreach (var record in Records)
{
foreach (var child in record.Children)
{
headerSet.Add(child.Name);
}
}
Headers = headerSet.OrderBy(h => h).ToList();
}
/// <summary>
/// 获取记录中指定字段的值
/// </summary>
public string GetRecordValue(XMLNode record, string header)
{
foreach (var child in record.Children)
{
if (child.Name == header)
{
if (child.IsLeaf())
{
return child.Content;
}
return child.GetAllLeafContent();
}
}
return "";
}
/// <summary>
/// 获取汇总信息
/// </summary>
public Dictionary<string, object> GetSummaryInfo()
{
var info = new Dictionary<string, object>
{
["总记录数"] = Records.Count,
["表头数"] = Headers.Count,
["根节点"] = Root.Name
};
if (Root.Attributes.Count > 0)
{
info["根属性"] = Root.GetAttributeString();
}
return info;
}
}
// ==================== xlsx2007生成 (OfficeOpenXML) ====================
/// <summary>
/// xlsx构建器
/// </summary>
public class XlsxBuilder
{
private readonly List<string> _sharedStrings = new();
private readonly Dictionary<string, int> _stringIndex = new();
/// <summary>
/// 添加共享字符串,返回索引
/// </summary>
public int AddString(string s)
{
if (_stringIndex.TryGetValue(s, out int idx))
{
return idx;
}
idx = _sharedStrings.Count;
_sharedStrings.Add(s);
_stringIndex[s] = idx;
return idx;
}
/// <summary>
/// XML转义
/// </summary>
public static string EscapeXml(string text)
{
if (string.IsNullOrEmpty(text)) return "";
return text
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
/// <summary>
/// [Content_Types].xml
/// </summary>
public static string GetContentTypesXml()
{
return @"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<Types xmlns=""http://schemas.openxmlformats.org/package/2006/content-types"">
<Default Extension=""rels"" ContentType=""application/vnd.openxmlformats-package.relationships+xml""/>
<Default Extension=""xml"" ContentType=""application/xml""/>
<Override PartName=""/xl/workbook.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml""/>
<Override PartName=""/xl/worksheets/sheet1.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml""/>
<Override PartName=""/xl/styles.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml""/>
<Override PartName=""/xl/sharedStrings.xml"" ContentType=""application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml""/>
</Types>";
}
/// <summary>
/// _rels/.rels
/// </summary>
public static string GetRelsXml()
{
return @"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<Relationships xmlns=""http://schemas.openxmlformats.org/package/2006/relationships"">
<Relationship Id=""rId1"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"" Target=""xl/workbook.xml""/>
</Relationships>";
}
/// <summary>
/// xl/_rels/workbook.xml.rels
/// </summary>
public static string GetWorkbookRelsXml()
{
return @"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<Relationships xmlns=""http://schemas.openxmlformats.org/package/2006/relationships"">
<Relationship Id=""rId1"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"" Target=""worksheets/sheet1.xml""/>
<Relationship Id=""rId2"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"" Target=""styles.xml""/>
<Relationship Id=""rId3"" Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"" Target=""sharedStrings.xml""/>
</Relationships>";
}
/// <summary>
/// xl/workbook.xml
/// </summary>
public static string GetWorkbookXml(string sheetName)
{
string escapedName = EscapeXml(sheetName);
return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" +
"<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\n" +
" <sheets>\n" +
$" <sheet name=\"{escapedName}\" sheetId=\"1\" r:id=\"rId1\"/>\n" +
" </sheets>\n" +
"</workbook>";
}
/// <summary>
/// xl/styles.xml
/// </summary>
public static string GetStylesXml()
{
return @"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<styleSheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">
<fonts count=""2"">
<font><sz val=""11""/><name val=""Calibri""/><family val=""2""/></font>
<font><sz val=""11""/><name val=""Calibri""/><family val=""2""/><b/></font>
</fonts>
<fills count=""3"">
<fill><patternFill patternType=""none""/></fill>
<fill><patternFill patternType=""gray125""/></fill>
<fill><patternFill patternType=""solid""><fgColor rgb=""FF4472C4""/><bgColor rgb=""FF4472C4""/></patternFill></fill>
</fills>
<borders count=""1"">
<border><left/><right/><top/><bottom/><diagonal/></border>
</borders>
<cellStyleXfs count=""1"">
<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0""/>
</cellStyleXfs>
<cellXfs count=""3"">
<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0""/>
<xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""0"" xfId=""0"" applyFont=""1"" applyFill=""1""/>
<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>
</cellXfs>
</styleSheet>";
}
/// <summary>
/// 构建共享字符串XML
/// </summary>
public string BuildSharedStringsXml()
{
var sb = new StringBuilder();
sb.AppendLine("""<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>""");
sb.AppendLine($"""<sst xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" count=""{_sharedStrings.Count}"" uniqueCount=""{_sharedStrings.Count}"">""");
foreach (var s in _sharedStrings)
{
sb.AppendLine($" <si><t>{EscapeXml(s)}</t></si>");
}
sb.AppendLine("</sst>");
return sb.ToString();
}
/// <summary>
/// 构建工作表XML
/// </summary>
public string BuildSheetXml(XMLDocument doc)
{
var sb = new StringBuilder();
sb.AppendLine("""<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>""");
sb.AppendLine("""<worksheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">""");
sb.AppendLine(" <sheetData>");
int rowNum = 1;
var headers = doc.Headers;
var cols = GenerateColumnNames(headers.Count);
// 表头行
sb.Append(GetRowStart(rowNum, 22));
for (int i = 0; i < headers.Count; i++)
{
sb.Append(GetSharedStringCell($"{cols[i]}{rowNum}", headers[i], 1));
}
sb.Append(GetRowEnd());
rowNum++;
// 数据行
if (doc.HasRecords)
{
foreach (var record in doc.Records)
{
sb.Append(GetRowStart(rowNum, 18));
for (int i = 0; i < headers.Count; i++)
{
string val = doc.GetRecordValue(record, headers[i]);
string cellRef = $"{cols[i]}{rowNum}";
if (IsNumeric(val))
{
sb.Append(GetNumberCell(cellRef, ParseNumeric(val)));
}
else
{
sb.Append(GetSharedStringCell(cellRef, val, 0));
}
}
sb.Append(GetRowEnd());
rowNum++;
}
}
else
{
// 没有数据记录时,将根节点作为单行数据
sb.Append(GetRowStart(rowNum, 18));
for (int i = 0; i < headers.Count; i++)
{
string val = doc.Root.GetLeafValue(headers[i]);
if (string.IsNullOrEmpty(val))
{
val = FindValueByName(doc.Root, headers[i]);
}
string cellRef = $"{cols[i]}{rowNum}";
if (IsNumeric(val))
{
sb.Append(GetNumberCell(cellRef, ParseNumeric(val)));
}
else
{
sb.Append(GetSharedStringCell(cellRef, val, 0));
}
}
sb.Append(GetRowEnd());
rowNum++;
}
sb.AppendLine(" </sheetData>");
sb.AppendLine("</worksheet>");
return sb.ToString();
}
/// <summary>
/// 根据名称在节点树中查找值
/// </summary>
public static string FindValueByName(XMLNode node, string name)
{
if (node.Name == name && node.IsLeaf())
{
return node.Content;
}
foreach (var child in node.Children)
{
string val = FindValueByName(child, name);
if (!string.IsNullOrEmpty(val))
{
return val;
}
}
return "";
}
/// <summary>
/// 判断字符串是否为数值
/// </summary>
public static bool IsNumeric(string s)
{
if (string.IsNullOrEmpty(s)) return false;
s = s.TrimEnd('%');
return double.TryParse(s, out _);
}
/// <summary>
/// 解析数值字符串
/// </summary>
public static double ParseNumeric(string s)
{
s = s.TrimEnd('%');
if (double.TryParse(s, out double val))
{
return val;
}
return 0;
}
/// <summary>
/// 生成列名 A, B, C, ..., Z, AA, AB, ...
/// </summary>
public static List<string> GenerateColumnNames(int count)
{
var cols = new List<string>();
for (int i = 0; i < count; i++)
{
cols.Add(ToColumnName(i));
}
return cols;
}
/// <summary>
/// 将数字转换为Excel列名
/// </summary>
public static string ToColumnName(int n)
{
string name = "";
while (n >= 0)
{
name = (char)('A' + n % 26) + name;
n = n / 26 - 1;
if (n < 0) break;
}
return name;
}
private string GetRowStart(int rowNum, int height)
{
return $" <row r=\"{rowNum}\" ht=\"{height}\">\n";
}
private string GetRowEnd()
{
return " </row>\n";
}
private string GetSharedStringCell(string cellRef, string value, int styleId)
{
int index = AddString(value);
return $" <c r=\"{cellRef}\" t=\"s\" s=\"{styleId}\"><v>{index}</v></c>\n";
}
private string GetNumberCell(string cellRef, double value)
{
return $" <c r=\"{cellRef}\"><v>{value}</v></c>\n";
}
/// <summary>
/// 预准备所有共享字符串
/// </summary>
public void PrepareSharedStrings(XMLDocument doc)
{
// 表头字符串
foreach (var h in doc.Headers)
{
AddString(h);
}
// 数据值
if (doc.HasRecords)
{
foreach (var record in doc.Records)
{
foreach (var h in doc.Headers)
{
string val = doc.GetRecordValue(record, h);
AddString(val);
}
}
}
else
{
foreach (var h in doc.Headers)
{
string val = doc.Root.GetLeafValue(h);
if (string.IsNullOrEmpty(val))
{
val = FindValueByName(doc.Root, h);
}
AddString(val);
}
}
}
/// <summary>
/// 构建xlsx文件
/// </summary>
public static void BuildXlsxFile(string outputPath, XMLDocument doc)
{
var xb = new XlsxBuilder();
xb.PrepareSharedStrings(doc);
string sheetName = doc.Root.Name;
if (sheetName.Length > 31)
{
sheetName = sheetName[..31];
}
using var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
using var archive = new ZipArchive(fs, ZipArchiveMode.Create);
// [Content_Types].xml
WriteZipEntry(archive, "[Content_Types].xml", GetContentTypesXml());
// _rels/.rels
WriteZipEntry(archive, "_rels/.rels", GetRelsXml());
// xl/_rels/workbook.xml.rels
WriteZipEntry(archive, "xl/_rels/workbook.xml.rels", GetWorkbookRelsXml());
// xl/workbook.xml
WriteZipEntry(archive, "xl/workbook.xml", GetWorkbookXml(sheetName));
// xl/styles.xml
WriteZipEntry(archive, "xl/styles.xml", GetStylesXml());
// xl/sharedStrings.xml
WriteZipEntry(archive, "xl/sharedStrings.xml", xb.BuildSharedStringsXml());
// xl/worksheets/sheet1.xml
WriteZipEntry(archive, "xl/worksheets/sheet1.xml", xb.BuildSheetXml(doc));
}
private static void WriteZipEntry(ZipArchive archive, string name, string content)
{
var entry = archive.CreateEntry(name, CompressionLevel.Optimal);
using var stream = entry.Open();
using var writer = new StreamWriter(stream, new UTF8Encoding(false));
writer.Write(content);
}
}
// ==================== 交互式CLI ====================
class Program
{
static void Main(string[] args)
{
Console.WriteLine("========================================");
Console.WriteLine(" 通用 XML 转 xlsx2007 转换工具");
Console.WriteLine("========================================");
Console.WriteLine();
// 交互式询问XML文件路径
string xmlPath = "";
while (true)
{
Console.Write("请输入XML文件路径 (或拖拽文件到此处): ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("路径不能为空,请重新输入。");
continue;
}
// 去除首尾空白和引号
xmlPath = input.Trim().Trim('"', '\'');
if (!File.Exists(xmlPath))
{
Console.WriteLine($"文件不存在: {xmlPath}");
continue;
}
break;
}
Console.WriteLine($"\n正在解析XML文件: {xmlPath}");
// 解析XML
XMLDocument doc;
try
{
doc = XMLDocument.ParseXMLFile(xmlPath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"错误: {ex.Message}");
Environment.Exit(1);
return;
}
Console.WriteLine("解析成功!");
Console.WriteLine($" 根节点: {doc.Root.Name}");
// 显示汇总信息
var summary = doc.GetSummaryInfo();
foreach (var kvp in summary)
{
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
}
Console.WriteLine($" 表头: [{string.Join(", ", doc.Headers)}]");
Console.WriteLine($" 发现数据记录: {doc.HasRecords}");
if (doc.HasRecords)
{
Console.WriteLine($" 记录数量: {doc.Records.Count}");
}
Console.WriteLine();
// 询问输出路径
string defaultOutput = GenerateDefaultOutputPath(xmlPath);
Console.Write($"请输入输出xlsx文件路径 (直接回车使用默认: {defaultOutput}): ");
string? outputInput = Console.ReadLine();
string outputPath = (outputInput ?? "").Trim().Trim('"', '\'');
if (string.IsNullOrEmpty(outputPath))
{
outputPath = defaultOutput;
}
// 确保输出目录存在
string dir = Path.GetDirectoryName(outputPath) ?? "";
if (!string.IsNullOrEmpty(dir) && dir != ".")
{
Directory.CreateDirectory(dir);
}
Console.WriteLine("\n正在生成xlsx文件...");
// 生成xlsx
try
{
XlsxBuilder.BuildXlsxFile(outputPath, doc);
}
catch (Exception ex)
{
Console.Error.WriteLine($"生成xlsx失败: {ex.Message}");
Environment.Exit(1);
return;
}
Console.WriteLine("\n✅ 转换成功!");
Console.WriteLine($" 输出文件: {outputPath}");
Console.WriteLine();
Console.WriteLine("按回车键退出...");
Console.ReadLine();
}
/// <summary>
/// 生成默认输出路径
/// </summary>
static string GenerateDefaultOutputPath(string xmlPath)
{
string dir = Path.GetDirectoryName(xmlPath) ?? "";
string baseName = Path.GetFileNameWithoutExtension(xmlPath);
string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
return Path.Combine(dir, $"{baseName}_{timestamp}.xlsx");
}
}
测试数据
test_simple.xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
<appName>MyApplication</appName>
<version>2.5.1</version>
<debug>true</debug>
<maxConnections>100</maxConnections>
<timeout>30.5</timeout>
</config>
test_books.xml
<?xml version="1.0" encoding="UTF-8"?>
<library>
<books>
<book>
<title>The Great Gatsby</title>
<author>F. Scott Fitzgerald</author>
<year>1925</year>
<price>10.99</price>
</book>
<book>
<title>1984</title>
<author>George Orwell</author>
<year>1949</year>
<price>8.99</price>
</book>
<book>
<title>To Kill a Mockingbird</title>
<author>Harper Lee</author>
<year>1960</year>
<price>12.50</price>
</book>
</books>
</library>
test_employees.xml
<?xml version="1.0" encoding="UTF-8"?>
<company>
<department name="Engineering">
<employee>
<id>E001</id>
<name>张三</name>
<position>高级工程师</position>
<salary>25000</salary>
</employee>
<employee>
<id>E002</id>
<name>李四</name>
<position>产品经理</position>
<salary>22000</salary>
</employee>
<employee>
<id>E003</id>
<name>王五</name>
<position>测试工程师</position>
<salary>18000</salary>
</employee>
</department>
</company>
输出
全部完成!以下是执行结果:
编译结果
────────
• 成功编译生成可执行文件 ./xml2xlsx
• 自动创建了 go.mod 模块文件
测试XML文件
───────────
根据 README 创建了 3 个测试文件:
┌────────────────────┬────────────────┬──────────────────────────────────────────────────┐
│ 文件 │ XML结构类型 │ 说明 │
├────────────────────┼────────────────┼──────────────────────────────────────────────────┤
│ test_books.xml │ 列表容器模式 │ 3本书的数据,含title/author/year/price │
│ test_employees.xml │ 嵌套结构模式 │ 3名员工数据,含id/name/position/salary(含中文) │
│ test_simple.xml │ 根节点单值模式 │ 配置项单值数据 │
└────────────────────┴────────────────┴──────────────────────────────────────────────────┘
转换结果
────────
3个XML均成功转换为 .xlsx 文件:
┌────────────────────────────────────┬───────┬────────┐
│ 输出文件 │ 大小 │ 记录数 │
├────────────────────────────────────┼───────┼────────┤
│ test_books_20260520093646.xlsx │ 2.8KB │ 3条 │
│ test_employees_20260520093648.xlsx │ 2.8KB │ 3条 │
│ test_simple_20260520093649.xlsx │ 2.7KB │ 1条 │
└────────────────────────────────────┴───────┴────────┘
程序正确识别了:
• 列表容器:<books> 下的多个 <book>,<department> 下的多个 <employee>
• 数值类型:price、year、salary、maxConnections、timeout 等自动转为数值单元格
• 共享字符串:文本内容去重存储,优化文件体积

仅使用go语言标准库, 将xml文件转为xlsx2007格式.
浙公网安备 33010602011771号