大数据项目开发过程中,ETL(Extract-Transform-Load)必不可少。即使目前 JSON 非常流行,开发人员也必定有机会面对远古系统的挑战,而 XML 格式的数据源作为经典存在浑身上下散发着浓浓 old money 的味道。
因为有 Newtonsoft.Json 这样优秀的 JSON 框架存在,开发人员可以很容易的对 JSON 格式的字符串反序列化。但是 XML 格式的数据就没有这么方便了:虽然 .NET 中内置了对 XML 序列化和反序列化的支持,但遇到需要对接外部数据时就不是很方便了。
从 XML 中提取目标数据最高效,也最麻烦的方式是直接使用 XmlReader :
<employee xmlns="urn:empl-hire">
<ID>12365</ID>
<hire-date>2003-01-08</hire-date>
<title>Accountant</title>
</employee>
使用以下代码对上述 hireDate.xml 文件读取:
using (XmlReader reader = XmlReader.Create("hireDate.xml")) {
// Move to the hire-date element.
reader.MoveToContent();
reader.ReadToDescendant("hire-date");
// Return the hire-date as a DateTime object.
DateTime hireDate = reader.ReadElementContentAsDateTime();
Console.WriteLine("Six Month Review Date: {0}", hireDate.AddMonths(6));
}
输出:
Six Month Review Date: 7/8/2003 12:00:00 AM
在 .NET Framework 3.5 发布后的时间里,开发人员可以使用 XDocument 来生成和解析 XML 文档,这要比 XmlReader 方便得多:
string str =
@"<?xml version=""1.0""?>
<!-- comment at the root level -->
<Root>
<Child>Content</Child>
</Root>";
XDocument doc = XDocument.Parse(str);
Console.WriteLine(doc.XPathSelectElement("//Child"));
输出:
<Child>Content</Child>
但硬编码的 XPath 并不方便调试,而且需要时刻关注空引用的问题。在 XML 格式复杂、项目工程比较大时使用起来也不方便。
在计算机科学中,可扩展样式表转换语言(英语:Extensible Stylesheet Language Transformations,缩写XSLT)是一种样式转换标记语言,可以将XML数据档转换为另外的XML或其它格式,如HTML网页,纯文字。XSLT最末的T字母表示英语中的“转换”(transformation)。
简单来说,开发人员可以借助 XSLT 技术编写一个 XML 文件,并使用该文件将一种 XML 格式转换为另一种 XML 。即:在对接复杂格式 XML 数据源时,开发人员可以编写一个后缀为 .xsl 的文件,并使用该文件将数据源格式转换为自己需要的格式(比如可以适配 XML 反序列化的格式)。
从一个简单的 XML 文件开始:
<?xml version="1.0" encoding="ISO-8859-1"?>
<catalog>
<cd>
<title>Empire Burlesque</title>
<artist>Bob Dylan</artist>
<country>USA</country>
<company>Columbia</company>
<price>10.90</price>
<year>1985</year>
</cd>
.
.
.
</catalog>
如果直接在浏览器打开这个文件:
假设我们只关心所有的 title 信息,可以使用下面的 cdcatalog.xsl 文件,该文件可以将 cdcatalog.xml 转为 XmlSerializer 所需要的格式:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:for-each select="catalog/cd">
<string>
<xsl:value-of select="title"/>
</string>
</xsl:for-each>
</ArrayOfString>
</xsl:template>
</xsl:stylesheet>
为了可以在浏览器中直接观察到转换效果,可以选择把 XSL 样式表链接到 XML 文档:向 XML 文档(”cdcatalog.xml”)添加 XSL 样式表引用即可。
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="cdcatalog.xsl"?>
<catalog>
<cd>
<title>Empire Burlesque</title>
<artist>Bob Dylan</artist>
<country>USA</country>
<company>Columbia</company>
<price>10.90</price>
<year>1985</year>
</cd>
.
.
.
</catalog>
刷新浏览器,打开开发者工具:
也可以在: https://www.coderbusy.com/demos/2021/1531/cdcatalog.xml 查看在线示例。
从上面的操作可以看出,调试 XLS 文件的成本是很低的,开发者可以很容易对 XLS 文件进行更改,并在短时间之内得到运行结果。
在 C# 中,可以使用 XslCompiledTransform 进行 XSL 转换。以下代码展示这个转换过程:
XslCompiledTransform xsl = new XslCompiledTransform();
xsl.Load("cdcatalog.xsl");
var sb = new StringBuilder();
using (var sw = new StringWriter(sb))
{
using (var xw = new XmlTextWriter(sw) { Formatting = Formatting.Indented })
{
xsl.Transform("cdcatalog.xml", xw);
}
}
var xml = sb.ToString();
Console.WriteLine(xml);
以上代码会产生如下输出:
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<string>Empire Burlesque</string>
<string>Hide your heart</string>
<string>Greatest Hits</string>
<string>Still got the blues</string>
<string>Eros</string>
.
.
.
</ArrayOfString>
转换 XML 不是目的,能直接拿到数据对象才是。以上的代码完成了格式转换,接着需要对转换好的 XML 字符串反序列化:
var xmlSerializer = new XmlSerializer(typeof(List<string>));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
{
var list = (List<string>) xmlSerializer.Deserialize(ms);
foreach (var item in list)
{
Console.WriteLine(item);
}
}
以上代码借助 XmlSerializer 实现了反序列化功能,这会产生以下输出:
Empire Burlesque
Hide your heart
Greatest Hits
Still got the blues
Eros
...
本文所述的转换和反序列化技术已经在真实的生产环境中得到验证,千万级的数据处理也毫不费力。
本文包含的演示的代码和数据可以在 Gitee 上找到:
https://gitee.com/coderbusy/demo/tree/master/hello-xslt/HelloXslt
一篇文章我们介绍了一个html/xml解析器——htmlparser,这篇文章我们介绍另外一个解析模块htmlparser2,后者是对前者的重构,同时对前者的API做了部分兼容。
安装
const { Parser } = require('htmlparser2');
const parser = new Parser(handler, options);
parser.parseComplete('html/xml内容');
写法
const { Parser } = require('htmlparser2');
const parser = new Parser(handler, options);
parser.parseComplete('html/xml内容');
htmlparser2提供了一个解析器——Parser,初始化它至少需要一个handler,options是可选的。
handler是一个对象,在这个对象上可以设置很多的钩子函数,Parser解析时会在每个阶段运行对应的钩子函数。
以下是可以设置的所有的钩子函数,
htmlparser模块是通过正则表达式来解析html内容的,而htmlparser2则不同,它会按顺序读取html的每个字符,并且推测后面字符是标签名、属性还是其他的类型,所以htmlparser2在解析完每一个标签后都会运行相应的钩子函数。
先来看一下例子,
图1
图1中设置了所有的钩子函数以便来说明每个钩子函数的作用,运行一下,
图2
对照图1和图2就能看出来每个钩子函数的运行时机,这其中有以下几个钩子函数需要注意一下。
除了自定义handler以外,htmlparser2还提供了几个handler,比如DomHandler,用法如下:
图3
运行一下,我们看看结果,
图4
如果4所示,DomHandler处理的结果是以数组的形式输出的,在每个单元数据中还可以拿到上一个、下一个以及父节点的数据。
htmlparser2还可以通过操作流Stream解析内容,写法如下:
图5
这篇文章和上一篇是姊妹篇,都是介绍解析html/xml内容的模块,通过对比,我们发现htmlparser2模块功能更强大一些,也更灵活一些,同时也兼容htmlparser模块的一些接口。虽然两者功能类似,但是这给了我们更多的选择性。
喜欢我的文章就关注我吧,有问题可以发表评论,我们一起学习,共同成长!
面是我们如何在 JavaScript 中轻松地将 JSON 转换为 CSV:
function jsonToCsv(items) {
const header = Object.keys(items[0]); const headerString = header.join(','); // handle null or undefined values here
const replacer = (key, value) => value ?? ''; const rowItems = items.map((row) =>
header
.map((fieldName) => JSON.stringify(row[fieldName], replacer))
.join(',')
); // join header and body, and break into separate lines
const csv = [headerString, ...rowItems].join('\r\n'); return csv;
}const obj = [
{ color: 'red', maxSpeed: 120, age: 2 },
{ color: 'blue', maxSpeed: 100, age: 3 },
{ color: 'green', maxSpeed: 130, age: 2 },
];const csv = jsonToCsv(obj);console.log(csv);
这将是 CSV 输出:
color,maxSpeed,age
"red",120,2
"blue",100,3
"green",130,2
了解步骤
我们创建了一个可重用的 jsonToCsv() 函数来让我们将多个 JSON 字符串转换为 CSV。 它需要一个包含对象的数组。 每个对象将在 CSV 输出中占据一行。
我们在此函数中所做的第一件事是获取将用于 CSV 标头的所有键。 我们希望数组中的所有对象都具有相同的键,因此我们使用 Object.keys() 方法将键从第一个对象项中提取到数组中。
const obj = [
{ color: 'red', maxSpeed: 120, age: 2 },
{ color: 'blue', maxSpeed: 100, age: 3 },
{ color: 'green', maxSpeed: 130, age: 2 },
];// { color: 'red', maxSpeed: 120, age: 2 }
console.log(obj[0]);// [ 'color', 'maxSpeed', 'age' ]
console.log(Object.keys(obj[0]));
获取键后,我们调用数组的 join() 方法将所有元素连接成 CSV 标头字符串。
const header = ['color', 'maxSpeed', 'age'];const headerString = arr.join(',');console.log(headerString); // color,maxSpeed,age
接下来,我们创建一个函数,该函数将作为回调传递给 JSON.stringify() 函数的 replacer 参数。 此函数将处理 JSON 数组中对象的未定义或空属性值。
const obj = { prop1: 'Earth', prop2: undefined };// replace undefined property values with empty string ('')
const replacer = (key, value) => value ?? '';const str = JSON.stringify(obj, replacer);// {"prop1":"Earth","prop2":""}
console.log(str);
然后我们使用 Array map() 方法从每个对象中获取属性值。 map() 接受一个回调函数,该函数在每个数组元素上调用以返回一个转换。
此回调使用标头数组来获取每个对象的所有键。 再次调用 map(),它会遍历每个键,获取对象中该键的对应值,并使用 JSON.stringify() 将其转换为字符串。
这个对 map() 的内部调用最终会产生一个数组,该数组包含数组中当前对象的所有字符串化属性值。
const header = ['color', 'maxSpeed', 'age'];const row = { color: 'red', maxSpeed: 120, age: 2 };const replacer = (key, value) => value ?? '';const rowItem = header.map((fieldName) =>
JSON.stringify(row[fieldName], replacer)
);// array of stringified property values
console.log(rowItem); // [ '"red"', '120', '2' ]
将对象转换为属性值数组后,使用 join() 将数组转换为 CSV 行。
['"red"', '120', '2'].join(',') // -> "red",120,2
因此,这种转换发生在 JSON 数组中的每个对象上,以生成 CSV 行列表,存储在我们原始示例中的 rowItems 变量中。
为了生成最终的 CSV 输出,我们使用扩展语法 (...) 将 headerString 和 rowItems 组合到一个数组中。
const headerString = ['color', 'maxSpeed', 'age'];const rowItems = ['"red",120,2', '"blue",100,3', '"green",130,2'];[headerString, ...rowItems];
/*
Output ->
[
[ 'color', 'maxSpeed', 'age' ],
'"red",120,2',
'"blue",100,3',
'"green",130,2'
]
*/
然后我们在这个数组上调用 join() 并使用 '\r\n' 字符串作为分隔符,以创建一个带有 CSV 标题的字符串,并且每个 CSV 行位于单独的行中。
const headerString = ['color', 'maxSpeed', 'age'];const rowItems = ['"red",120,2', '"blue",100,3', '"green",130,2'];console.log([headerString, ...rowItems].join('\r\n'));
/*
color,maxSpeed,age
"red",120,2
"blue",100,3
"green",130,2
*/
关注七爪网,获取更多APP/小程序/网站源码资源!
*请认真填写需求信息,我们会在24小时内与您取得联系。