为什么不建议使用XML::Simple?

60
根据XML::Simple文档的说明:

不建议在新的代码中使用此模块。还有其他提供更直接和一致接口的模块可用。特别是强烈推荐使用XML::LibXML。

使用此模块的主要问题是选项太多以及这些选项相互作用时的任意性 - 通常具有令人意外的结果。

请问有人能为我澄清其中关键原因吗?

1
也许听到 https://metacpan.org/pod/XML::Fast 的优缺点会很有趣。 - mpapec
你是否正在创建一篇知识库文章,以便在寻找消灭XML::Simple的过程中可以链接到它? :D - simbabque
7
XML::Simple不在Perl核心中,从未存在过。事实上,Perl核心中也没有任何XML解析模块。 - stu42j
15
作为XML::Simple的作者,我不鼓励使用它,因为有更好的解决方案,而且这些解决方案实际上更容易使用。我个人使用并推荐XML::LibXML,并已编写了一份教程来帮助人们入门 - XML::LibXML by example - Grant McLean
1
刚回到这里并阅读了评论。如果您希望将某些内容包含在核心中,您可以随时在p5p邮件列表中提出建议。如果您有充分的论据,他们可能会采纳。 - simbabque
显示剩余5条评论
3个回答

55

实际问题在于XML::Simple主要尝试将XML表示为Perl数据结构。

正如您从perldata中所知道的那样,两个主要的可用数据结构是哈希表(hash)数组(array)

  • 数组是有序标量。
  • 哈希表是无序键值对。

然而XML实际上不做这两件事。它具有以下特点:

  • 元素名称非唯一(这意味着哈希表不适用)。
  • ......但在文件中是“有序”的。
  • 可能有属性(可以插入到哈希表中)。
  • 可能有内容(但可能没有,但可以是一元标记)。
  • 可能有子元素(任何深度)。

这些内容不能直接映射到可用的Perl数据结构——在简单层面上,嵌套的哈希表可能是适合的——但它无法处理具有重复名称的元素。并且您无法轻松区分属性和子节点。

因此,XML::Simple尝试根据XML内容猜测,并从各种选项设置中获取“提示”,然后在尝试输出内容时,(尝试)应用相同的逆向过程。

因此,对于除了最简单的XML之外的任何内容,它都变得笨拙,最坏的情况下会丢失数据。

考虑:

<xml>
   <parent>
       <child att="some_att">content</child>
   </parent>
   <another_node>
       <another_child some_att="a value" />
       <another_child different_att="different_value">more content</another_child>
   </another_node>
</xml>

这个经过XML::Simple解析后会得到:

$VAR1 = {
          'parent' => {
                      'child' => {
                                 'att' => 'some_att',
                                 'content' => 'content'
                               }
                    },
          'another_node' => {
                            'another_child' => [
                                               {
                                                 'some_att' => 'a value'
                                               },
                                               {
                                                 'different_att' => 'different_value',
                                                 'content' => 'more content'
                                               }
                                             ]
                          }
        };
注意 - 现在您在parent下只有匿名哈希,但在another_node下有一个匿名哈希数组。
因此,要访问child的内容:
my $child = $xml -> {parent} -> {child} -> {content};

请注意,您有一个“子”节点,并且该节点下方有一个“内容”节点,这并不是因为它是...内容。

但是要访问第一个another_child元素下方的内容:

 my $another_child = $xml -> {another_node} -> {another_child} -> [0] -> {content};

请注意 - 由于存在多个<another_node>元素,XML 已被解析为数组,而单个元素时没有这种情况。(如果你在其中加入一个名为content的元素,你将得到另一种结果)。你可以通过使用ForceArray来更改此行为,但这可能会导致多层嵌套的哈希和数组,虽然它处理子元素时至少是一致的。编辑:请注意,经过讨论,这是一个不好的默认设置,而不是 XML::Simple 的缺陷。

你应该进行以下设置:

ForceArray => 1, KeyAttr => [], ForceContent => 1
如果您将此应用于上述的XML,您将得到以下结果:
$VAR1 = {
          'another_node' => [
                            {
                              'another_child' => [
                                                 {
                                                   'some_att' => 'a value'
                                                 },
                                                 {
                                                   'different_att' => 'different_value',
                                                   'content' => 'more content'
                                                 }
                                               ]
                            }
                          ],
          'parent' => [
                      {
                        'child' => [
                                   {
                                     'att' => 'some_att',
                                     'content' => 'content'
                                   }
                                 ]
                      }
                    ]
        };

通过这样做,您将获得一致性,因为您不再需要单节点元素与多节点处理方式不同。

但是,您仍然:

  • 需要经过5个参考深度来获取一个值。

例如:

print $xml -> {parent} -> [0] -> {child} -> [0] -> {content};

你仍然有contentchild散列元素被视为属性处理,由于散列是无序的,所以你无法重建输入。因此,基本上你必须先解析它,然后通过Dumper运行它来确定需要查找的位置。

但是,使用xpath查询,您可以轻松获取该节点:

findnodes("/xml/parent/child"); 

XML::Simple缺少的功能,而在XML::Twig中(我认为XML::LibXML也有,但我不太熟悉):

  • xpath支持。 xpath 是表示到节点路径的一种 XML 方式。因此,您可以使用 get_xpath('//child') 在上述内容中“查找”节点。甚至可以在 xpath 中使用属性,例如 get_xpath('//another_child[@different_att]'),这将选择所需的节点。(您还可以迭代匹配项)。
  • cutpaste 以移动元素。
  • parsefile_inplace 允许您对 XML 进行原地编辑修改。
  • pretty_print 选项,用于格式化 XML
  • twig_handlerspurge - 允许您处理非常大的 XML,而无需将其全部加载到内存中。
  • simplify,如果您确实需要使其向后兼容 XML::Simple
  • 代码通常比尝试跟踪对哈希和数组的引用链要简单得多,这是因为结构上的根本差异永远无法一致。

它也广泛可用-可以从 CPAN 轻松下载,并作为可安装软件包分发在许多操作系统中。(遗憾的是它不是默认安装。)

参见:XML::Twig 快速参考

供比较:

my $xml = XMLin( \*DATA, ForceArray => 1, KeyAttr => [], ForceContent => 1 );

print Dumper $xml;
print $xml ->{parent}->[0]->{child}->[0]->{content};

对比。

my $twig = XML::Twig->parse( \*DATA );
print $twig ->get_xpath( '/xml/parent/child', 0 )->text;
print $twig ->root->first_child('parent')->first_child_text('child');

5
遗憾的是,它不是默认安装的。如果你所说的“默认安装”是指核心模块,那么我同意你的说法。但是,如果你的意思是与Perl发行版捆绑在一起,自从至少2014年5月以来,Strawberry Perl就已经包含了预安装的XML模块(如XML::LibXML、 XML::Parser、XML::Twig等)。 - Matt Jacob
7
总的来说,问题很大程度上在于ForceArray应该默认为1(而且这不能被更改,否则会破坏大多数现有的用法)。如果XML::Simple能够满足您的需求,那就没有理由不使用它。 - ysth
我同意,但是将“满足我的需求”的范围狭窄地限定为“如果我无法安装其他模块”,并且如果正则表达式的技巧行不通的话。因为说实话,我认为它与正则表达式非常相似,出于同样的原因。只要你对输入的XML有非常受控制的范围,它就能工作。但是它可能会在某一天毫无明显原因地崩溃。它确实解决了一个问题,并且它是一个核心模块。但是当存在更好的选择时,它是一个糟糕的解决方案。 - Sobrique
5
@Sobrique:我开始编辑你的解决方案,但当我到达最后一段和列表时,我不得不放弃。你的陈述目的是解释为什么XML::Simple是一个糟糕的选择,但你最终写了一封给XML::Twig的赞信。如果你想超越解释XML::Simple的问题,那么你需要考虑远比XML::TwigXML::LibXML更多的东西,而且我不认为这是进行这种扩展分析的地方。 - Borodin
2
因为我不喜欢提供“不要做X”而没有提供一个合适的替代方案,所以我试图提供一些转换的积极理由。理想情况下,这些原因会对商业案例有所帮助。我是XML::Twig的粉丝。我认为如果他们从核心中“简单地”删除XML::simple,它将是一个很好的替代品。最重要的是,“简化”可以让您保留向后兼容性。我知道这有点偏离主题,但还有很多其他很好的选择。 - Sobrique

33

XML::Simple是可用的最复杂的XML解析器

XML::Simple的主要问题在于其生成的结构非常难以正确地导航。即使对于遵循相同规范的元素,$ele->{ele_name}也可以返回以下任何一种:

[ { att => 'val', ..., content => [ 'content', 'content' ] }, ... ]
[ { att => 'val', ..., content => 'content' }, ... ]
[ { att => 'val', ..., }, ... ]
[ 'content', ... ]
{ 'id' => { att => 'val', ..., content => [ 'content', 'content' ] }, ... }
{ 'id' => { att => 'val', ..., content => 'content' }, ... }
{ 'id' => { att => 'val', ... }, ... }
{ 'id' => { content => [ 'content', 'content' ] }, ... }
{ 'id' => { content => 'content' }, ... }
{ att => 'val', ..., content => [ 'content', 'content' ] }
{ att => 'val', ..., content => 'content' }
{ att => 'val', ..., }
'content'

这意味着你需要执行各种检查来确定你实际得到了什么。但是这种复杂性鼓励开发者做出非常糟糕的假设。这导致各种问题悄悄地进入生产环境,当遇到边缘情况时,导致代码无法正常运行。

使树更加规则的选项不足

您可以使用以下选项创建一个更规则的树:

ForceArray => 1, KeyAttr => [], ForceContent => 1

但即使有这些选项,仍然需要许多检查才能从树中提取信息。例如,从文档中获取/root/eles/ele节点是一项常见操作,应该很容易执行,但使用XML::Simple时需要执行以下操作:

# Requires: ForceArray => 1, KeyAttr => [], ForceContent => 1, KeepRoot => 0
# Assumes the format doesn't allow for more than one /root/eles.
# The format wouldn't be supported if it allowed /root to have an attr named eles.
# The format wouldn't be supported if it allowed /root/eles to have an attr named ele.
my @eles;
if ($doc->{eles} && $doc->{eles}[0]{ele}) {
    @eles = @{ $doc->{eles}[0]{ele} };
}

在另一个解析器中,可以使用以下方式:
my @eles = $doc->findnodes('/root/eles/ele');

XML::Simple存在许多限制,并且缺乏常见功能

  • 它完全无法生成XML。即使使用ForceArray => 1, ForceContent => 1, KeyAttr => [], KeepRoot => 1,也有太多无法控制的细节。

  • 它不保留具有不同名称的子项的相对顺序。

  • 它的命名空间和命名空间前缀支持有限(使用XML::SAX后端)或没有(使用XML::Parser后端)。

  • 一些后端(例如XML::Parser)无法处理基于ASCII之外的编码(例如UTF-16le)。

  • 元素不能同时具有与其名称相同的子元素和属性。

  • 它无法创建带有注释的XML文档。

尽管忽略了先前提到的主要问题,XML::Simple仍然可能在这些限制下可用。但是为什么要费力地检查XML::Simple是否可以处理您的文档格式并冒险以后必须切换到另一个解析器呢?您可以从一开始就为所有文档使用更好的解析器。

一些其他解析器不仅不会受到这些限制的限制,而且还提供了许多其他有用的功能。以下是一些它们可能具有但XML::Simple不具备的功能:

  • 速度。XML::Simple非常慢,特别是如果您使用的是XML::Parser之外的后端。我说的是比其他解析器慢几个数量级。

  • XPath选择器或类似内容。

  • 支持极大的文档。

  • 支持漂亮的打印。

XML::Simple是否有用?

唯一适用于XML::Simple的格式是没有元素是可选的格式。我已经使用了无数种XML格式,并且从未遇到过这样的格式。

这种脆弱性和复杂性足以理由让我们远离XML::Simple,但还有其他原因。

替代方案

我使用XML::LibXML。它是一个非常快速、功能齐全的解析器。如果我需要处理无法适合内存的文档,我将使用XML::LibXML::Reader(及其copyCurrentNode(1))或XML::Twig(使用twig_roots)。


XML::TreePP 对我来说似乎没有 XML::Simple 那样的神奇猜测功能。但是你可以告诉它如何精确地行为。与 XML::LibXML 及其系列相比,它也更加简单易用。对于创建 XML,我会使用 XML::TreePP;对于解析外部 XML 内容,如果你有大型 XML 并且速度是一个问题,可能会使用 XML::LibXML。 - nicomen
1
@nicomen,假设您使用了$tpp->set( force_array => [ '*' ] );,则需要至少my @eles; if ($doc->{root} && $doc->{root}[0]{eles} && $doc->{root}[0]{eles}[0]{ele}) { @eles = @{ $doc->{root}[0]{eles}[0]{ele} } }来获取/root/eles/ele节点,这是假设没有多个eles节点的情况。这与最佳配置的XML::Simple没有区别。(如果没有force_array => [ '*' ],情况会更糟。) - ikegami
1
@nicomen,你说你会在处理大型文档时使用XML::TreePP而不是XML::LibXML。为什么????这对我来说听起来很荒谬,但我可能漏掉了什么。我没有对XML::TreePP进行基准测试,但我怀疑它无论是在大型文档还是其他方面都无法与XML::LibXML相比。大型文档的问题在于内存,而不是速度。XML::LibXML提供了一个用于大型文档的选项(pull解析器),而XML::TreePP则没有。话虽如此,XML::Twig在这方面要好得多。 - ikegami
我可能表达不够清晰,我的意思是XML::LibXML非常适合处理大型文档和重负载。对于简单的编写和阅读,我更喜欢使用XML::TreePP,但是需要设置一些合理的默认值。 - nicomen
@choroba,也许吧,但可能不是。XML::Twig的twig_handlers返回匹配元素的子树,而XML::LibXML::Reader几乎只是一个标记生成器。XML::LibXML::Reader更接近于XML::Parser(XML::Twig使用的底层解析器)而不是XML::Twig。 - ikegami
显示剩余3条评论

4

我不同意文档的说法

我持异议,认为XML::Simple就是简单易用的。在我看来,它一直都很容易且令人愉快地使用。只要输入数据没有发生变化,你就可以放心使用。那些抱怨使用XML::Simple的人也会抱怨使用JSON::Syck来序列化Moose。这些文档是错误的,因为它们考虑的是正确性而非效率。只要你关心以下内容,你就可以使用它:

  • 不丢失数据
  • 按照提供的格式构建而非抽象模式

如果你正在创建一个抽象解析器,其定义不是由应用程序而是由规格说明书确定的,我会使用其他工具。有一次我曾在一家公司工作,我们必须接受300种不同的XML模式,没有一个模式是规范的。使用XML::Simple轻松完成了这项工作。其他选项将要求我们雇佣某个人来完成这项工作。每个人都认为XML是以一种严格的、全面的、规范的格式发送的东西,这样,如果你编写了一个解析器,你就可以做到了。如果是这样的话,就不要使用XML::Simple。在JSON出现之前,XML只是从一种语言到另一种语言的“转储和行走”格式。人们实际上使用诸如XML::Dumper之类的工具。没有人真正知道输出了什么。在处理这种情况时,XML::Simple非常好用!理智的人仍然会将其转换为JSON而不需要规范来完成同样的事情。这就是现实中的运作方式。

想读取数据并不担心格式?想遍历Perl结构而不是XML可能性?那就使用XML::Simple

此外...

同样地,对于大多数应用程序,JSON::Syck足以转储并行走。但是如果你要向很多人发送数据,我强烈建议制定一个规范导出。但是,你知道吗,有时候你会接到某个你不想和他说话却要求提供数据的人的电话。然后,你就可以通过JSON::Syck的巫术让他们去烦恼。如果他们想要XML?再收取500美元的费用,启动老式的XML::Dumper

总结

虽然它不是完美的,但XML::Simple非常高效。在这个领域节省下来的每一小时都可以潜在地用在更有用的领域上。这是一个现实考虑。

其他答案

看起来XPath有一些优点。每个答案都表明喜欢使用XPath而不是Perl。这很好。如果您更喜欢使用一个标准化的XML领域特定语言来访问您的XML,那就用吧!

Perl没有提供一个简单的机制来访问深度嵌套的可选结构。

var $xml = [ { foo => 1 } ];  ## Always w/ ForceArray.

var $xml = { foo => 1 };

在这两个上下文中获取foo的值可能会很棘手。 XML::Simple知道这一点,这就是为什么你可以强制使用前者..但是,即使使用ForceArray,如果该元素不存在,则会抛出错误。

var $xml = { bar => [ { foo => 1 } ] };

现在,如果bar是可选的,你只能通过$xml->{bar}[0]{foo}@{$xml->{bar}}[0]来访问它,否则会出错。不管怎样,这只是Perl语言的特性。我认为这与XML::Simple无关。同时,我承认XML::Simple不适合用于按规范构建XML数据。如果给我数据,我可以使用XML::Simple来访问它。


评论不适合进行长时间的讨论;此对话已被移至聊天室 - George Stocker
让我们在聊天中继续这个讨论 - Evan Carroll
我已经删除了针对另一个用户的不必要的元评论。那并不真正需要成为答案的一部分,如果你想讨论这个问题,可以去聊天室里面谈论。 - Brad Larson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接