JSON的XSLT等效方法

518

是否有针对JSON的XSLT等效工具? 让我可以像XSLT将XML转换一样在JSON上进行转换。


9
XSLT是一种标准,许多语言和平台都有实际的实现,我的问题是针对类似的努力。 - luvieere
55
对于你的问题点赞。许多人似乎忽视或者纯粹不喜欢XSLT,但这可能只是对XML冗长性的反应。随着XML逐渐失宠,使用XSLT的机会也越来越少,这很遗憾!如果有一个JSON的等效XSLT将是很棒的。 - Nicolas Le Thierry d'Ennequin
13
@NicolasLeThierryd'Ennequin 同意。很多人讨厌XML,因此忽视了XSLT。XML工具生态系统也以Java开发人员为主,这使更多人望而却步。但在2000年代中期我非常熟悉XSLT,在XML生态系统之外没有直接等效的强大功能。我希望有一个JSON的等效物! - Zearin
1
@NicolasLeThierryd'Ennequin:请查看以下链接:https://jsfiddle.net/YSharpLanguage/kj9pk8oz/10或者https://jsfiddle.net/YSharpLanguage/ppfmmu15/10或者https://jsfiddle.net/YSharpLanguage/hvo24hmk/3 - YSharp
4
我强烈反对关闭这个答案。简单的改写就足够了:“如何使用声明式的方式重构JSON?” - Gregory Higley
显示剩余10条评论
24个回答

210

JSON的XSLT等效替代品 - 候选列表(工具和规范)

工具

1. XSLT

您可以使用XSLT for JSON,目的是fn:json-to-xml

本节介绍了使用XSLT处理JSON数据的功能。

2. jq

jq就像是针对JSON数据的sed - 您可以使用它来切片、过滤、映射和转换结构化数据,就像sed、awk、grep和其他工具让您处理文本一样轻松。 不同操作系统都有安装包。

3. jj

JJ是一个命令行实用程序,提供了一种快速简单的方式来检索或更新JSON文档中的值。它在内部使用GJSON和SJSON进行驱动。
4. fx 命令行JSON处理工具 - 无需学习新的语法 - 纯JavaScript - 格式化和高亮显示 - 独立二进制文件
5. CsvCruncher 命令行表格数据基于SQL的处理工具 - 无需学习新的语法 - 当JSON包含具有相似项的大型数组时适用 - 支持将多个文档处理为多个SQL表 - 使用Kotlin编写,运行在Java上 - 也可作为Java库在Maven Central仓库中使用
6. jl("JSON lambda")是一个用于查询和操作JSON的微型函数式语言。
7. JOLT是一个用Java编写的JSON到JSON转换库,其中转换的"规范"本身就是一个JSON文档。
8. gron使JSON可被grep!gron将JSON转换为离散的赋值,以便更容易地grep所需内容并查看其绝对路径。它简化了返回大量JSON但文档糟糕的API的探索。
9. json-e是一个用于处理JSON的表达式引擎。
JSON-e是一种在JSON对象中嵌入上下文的数据结构参数化系统。 其核心思想是将数据结构视为一个“模板”,并使用另一个数据结构作为上下文进行转换,以生成输出数据结构。
10. JSLT JSLT是一种完整的用于JSON的查询和转换语言。该语言的设计灵感来自jq、XPath和XQuery。
11. JSONata JSONata是一种轻量级的用于JSON数据的查询和转换语言。受XPath 3.1的“位置路径”语义的启发,它允许用简洁直观的符号表示复杂的查询。
12. JSONPath Plus 分析、转换和有选择地从JSON文档(和JavaScript对象)中提取数据。 jsonpath-plus在原始规范的基础上添加了一些额外的运算符,并明确了一些原始规范没有明确说明的行为。
13. json-transforms 最后提交日期:2017年12月1日
提供了一种递归、模式匹配的方法来转换JSON数据。转换被定义为一组与JSON对象结构匹配的规则。当匹配发生时,规则会发出转换后的数据,可选择递归转换子对象。
14. json 最后提交日期:2018年6月23日
json是一个用于处理JSON的快速CLI工具。它是一个单文件的node.js脚本,没有外部依赖(除了node.js本身)。
15. jsawk 最后提交日期:2015年3月4日
Jsawk就像awk一样,但是用于JSON。您可以使用JavaScript对从stdin读取的JSON对象数组进行过滤,生成一个结果数组,并将其打印到stdout。
16. yate 最后提交日期为2017年3月13日
测试可以用作文档 https://github.com/pasaran/yate/tree/master/tests 17. jsonpath-object-transform 最后提交日期为2017年1月18日
使用JSONPath从对象文字中提取数据,并根据模板生成新的对象。
18. Stapling 最后提交日期为2013年9月16日
Stapling是一个JavaScript库,可以为JSON对象提供XSLT格式化功能。 与使用JavaScript模板引擎和文本/HTML模板不同,Stapling让您有机会使用XSLT模板 - 通过Ajax异步加载,然后在客户端进行缓存 - 来解析您的JSON数据源。
19. mapneat MapNeat是一个在Kotlin中编写的JVM库,为转换JSON到JSON、XML到JSON、POJO到JSON提供了易于使用的DSL(领域特定语言),以声明性的方式进行转换。
规格:
- JSON Pointer JSON Pointer是一个字符串语法或地址,用于标识较大的JSON对象中的特定对象。它没有查询功能或转换函数。JSON Pointer的引用可以是任何JSON对象。
- JsonPath JSONPath表达式与XPath表达式在处理XML文档时的方式类似,它们都用于引用JSON结构。 JSPath对于JSON就像XPath对于XML一样。 JSONiq是受到XQuery启发的主要来源,XQuery已经被证明是一种成功且高效的半结构化数据查询语言。 JMESPATH是一种用于JSON的查询语言。JMESPath语言在一个完整的规范中用ABNF语法进行描述。

2
感谢您非常详细和有用的帖子。 为了将单行JSON转换为可读格式,jq(您列表中的第二个)对我来说是最好的选择。再次感谢! - primehunter
1
我经常使用 json_pp 进行漂亮的打印。它适用于许多发行版。 - jschnasse
1
参见:规格下的JMESpath - dreftymac

79

11
好的,谢谢,这正是我在寻找的。遗憾的是这种技术不太流行,JSON 经常用作 REST 风格服务的返回格式,如果能有一个标准的实现转换的方式就好了。 - luvieere
仅提供链接的回答 - Jean-François Fabre
1
这个回答似乎不相关。据我所知,上面提到的链接说“一个公司可能已经实现了适当的东西”,最终什么也没有带来。对于所谓的产品XJR的额外搜索是徒劳的。 - John Fischer

74

试试JOLT。 它是用Java编写的JSON到JSON转换库。

它专门为了避免玩“JSON - > XML - > XSLT - > XML - > JSON”游戏而创建,并且对于任何复杂的转换,使用模板难以维护。


5
这是一个严肃的项目!太好了。在线演示和示例大大有助于提高学习效率:http://jolt-demo.appspot.com/ - kevinarpe

17

XSLT支持JSON,详见http://www.w3.org/TR/xslt-30/#json

XML使用尖括号作为分隔符标记,而JSON使用大括号、方括号等。也就是说,XML的令牌识别比较少,因此它更适合用于声明性转换;而对于速度原因类似于switch语句的更多比较,则假定有可确信的分支预测,这在脚本语言中具有用处。因此,对于不同的半结构化数据组合,您可能需要测试XSLT和JavaScript引擎在响应式页面中的性能表现。对于数据负载很小的情况下,可以使用JSON而无需将其序列化为XML来进行转换处理。W3的决策应该基于更好的分析。


1
您还可以使用JSON2XML作为一个独立的库,它提供了一个XSLT 3.0表示的JSON,无需XSLT 3.0处理器。 - Martynas Jusevičius

16

4
因为其功能较少,所以它更加简洁。 - Ihe Onwuka
我没有找到如何在Json树中递归搜索给定属性的方法。 - Daniel
@Daniel是.. | .attr_name?你正在寻找的内容吗?(来自https://stedolan.github.io/jq/manual/#RecursiveDescent: ..) - ankostis
1
可能没有 XSLT 那么强大,但非常有用,也不像 XSLT 那样复杂。 - flq

15

我最近发现了一个我喜欢的用于格式化JSON的工具:https://github.com/twigkit/tempo。这是非常易于使用的工具--在我看来,它比XSLT更容易使用--无需使用XPATH查询。


9
如果最终的转换结果是HTML,那么Tempo看起来很不错。但是如果您只想将一个隐含的结构重新排列成另一种结构,而最终结果仍然是JSON,那该怎么办呢?我仍然希望有类似XPath的工具,以便我可以用函数式的方式编写转换过程。 - Toddius Zho
1
Tempo确实非常有趣,谢谢。但是你可以将一个xml发送到浏览器和一个xslt(<? xsl-stylesheet>),你的浏览器将应用xslt到xml,显示一个定义的视图,而无需任何其他代码。这也适用于jsonT/tempo。 - Martin Meeser

13

12

最近我写了一个小型的库,它尝试尽可能地接近

5.1 处理模型 (XSLT REC) https://www.w3.org/TR/xslt#section-Processing-Model

以几行 JavaScript 代码实现。

以下是一些不完全复杂的使用示例...

1. 将 JSON 转换为某些标记:

Fiddle: https://jsfiddle.net/YSharpLanguage/kj9pk8oz/10

(灵感来自于 D.1 文档示例 (XSLT REC) https://www.w3.org/TR/xslt#section-Document-Example)

其中这个:

var D1document = {
    type: "document", title: [ "Document Title" ],
    "": [
      { type: "chapter", title: [ "Chapter Title" ],
        "": [
        { type: "section", title: [ "Section Title" ],
          "": [
            { type: "para", "": [ "This is a test." ] },
            { type: "note", "": [ "This is a note." ] }
        ] },
        { type: "section", title: [ "Another Section Title" ],
          "": [
            { type: "para", "": [ "This is ", { emph: "another" }, " test." ] },
            { type: "note", "": [ "This is another note." ] }
        ] }
      ] }
    ] };

var D1toHTML = { $: [
  [ [ function(node) { return node.type === "document"; } ],
    function(root) {
      return "<html>\r\n\
  <head>\r\n\
    <title>\r\n\
      {title}\r\n".of(root) + "\
    </title>\r\n\
  </head>\r\n\
  <body>\r\n\
{*}".of(root[""].through(this)) + "\
  </body>\r\n\
</html>";
    }
  ],
  [ [ function(node) { return node.type === "chapter"; } ],
    function(chapter) {
      return "    <h2>{title}</h2>\r\n".of(chapter) + "{*}".of(chapter[""].through(this));
    }
  ],
  [ [ function(node) { return node.type === "section"; } ],
    function(section) {
      return "    <h3>{title}</h3>\r\n".of(section) + "{*}".of(section[""].through(this));
    }
  ],
  [ [ function(node) { return node.type === "para"; } ],
    function(para) {
      return "    <p>{*}</p>\r\n".of(para[""].through(this));
    }
  ],
  [ [ function(node) { return node.type === "note"; } ],
    function(note) {
      return '    <p class="note"><b>NOTE: </b>{*}</p>\r\n'.of(note[""].through(this));
    }
  ],
  [ [ function(node) { return node.emph; } ],
    function(emph) {
      return "<em>{emph}</em>".of(emph);
    }
  ]
] };

console.log(D1document.through(D1toHTML));

...给出:

<html>
  <head>
    <title>
      Document Title
    </title>
  </head>
  <body>
    <h2>Chapter Title</h2>
    <h3>Section Title</h3>
    <p>This is a test.</p>
    <p class="note"><b>NOTE: </b>This is a note.</p>
    <h3>Another Section Title</h3>
    <p>This is <em>another</em> test.</p>
    <p class="note"><b>NOTE: </b>This is another note.</p>
  </body>
</html>

并且

2. JSON-to-JSON:

Fiddle: https://jsfiddle.net/YSharpLanguage/ppfmmu15/10

代表以下内容:

// (A "Company" is just an object with a "Team")
function Company(obj) {
  return obj.team && Team(obj.team);
}

// (A "Team" is just a non-empty array that contains at least one "Member")
function Team(obj) {
  return ({ }.toString.call(obj) === "[object Array]") &&
         obj.length &&
         obj.find(function(item) { return Member(item); });
}

// (A "Member" must have first and last names, and a gender)
function Member(obj) {
  return obj.first && obj.last && obj.sex;
}

function Dude(obj) {
  return Member(obj) && (obj.sex === "Male");
}

function Girl(obj) {
  return Member(obj) && (obj.sex === "Female");
}

var data = { team: [
  { first: "John", last: "Smith", sex: "Male" },
  { first: "Vaio", last: "Sony" },
  { first: "Anna", last: "Smith", sex: "Female" },
  { first: "Peter", last: "Olsen", sex: "Male" }
] };

var TO_SOMETHING_ELSE = { $: [

  [ [ Company ],
    function(company) {
      return { some_virtual_dom: {
        the_dudes: { ul: company.team.select(Dude).through(this) },
        the_grrls: { ul: company.team.select(Girl).through(this) }
      } }
    } ],

  [ [ Member ],
    function(member) {
      return { li: "{first} {last} ({sex})".of(member) };
    } ]

] };

console.log(JSON.stringify(data.through(TO_SOMETHING_ELSE), null, 4));

... 给出:

{
    "some_virtual_dom": {
        "the_dudes": {
            "ul": [
                {
                    "li": "John Smith (Male)"
                },
                {
                    "li": "Peter Olsen (Male)"
                }
            ]
        },
        "the_grrls": {
            "ul": [
                {
                    "li": "Anna Smith (Female)"
                }
            ]
        }
    }
}

3. XSLT与JavaScript比较:

一个JavaScript等效的...

XSLT 3.0 REC第14.4节示例:基于共同值对节点进行分组

(位于: http://jsfiddle.net/YSharpLanguage/8bqcd0ey/1)

参见https://www.w3.org/TR/xslt-30/#grouping-examples

其中...

var cities = [
  { name: "Milano",  country: "Italia",      pop: 5 },
  { name: "Paris",   country: "France",      pop: 7 },
  { name: "München", country: "Deutschland", pop: 4 },
  { name: "Lyon",    country: "France",      pop: 2 },
  { name: "Venezia", country: "Italia",      pop: 1 }
];

/*
  Cf.
  XSLT 3.0 REC Section 14.4
  Example: Grouping Nodes based on Common Values

  https://www.w3.org/TR/xslt-30/#grouping-examples
*/
var output = "<table>\r\n\
  <tr>\r\n\
    <th>Position</th>\r\n\
    <th>Country</th>\r\n\
    <th>City List</th>\r\n\
    <th>Population</th>\r\n\
  </tr>{*}\r\n\
</table>".of
  (
    cities.select().groupBy("country")(function(byCountry, index) {
      var country = byCountry[0],
          cities = byCountry[1].select().orderBy("name");
      return "\r\n\
  <tr>\r\n\
    <td>{position}</td>\r\n\
    <td>{country}</td>\r\n\
    <td>{cities}</td>\r\n\
    <td>{population}</td>\r\n\
  </tr>".
        of({ position: index + 1, country: country,
             cities: cities.map(function(city) { return city.name; }).join(", "),
             population: cities.reduce(function(sum, city) { return sum += city.pop; }, 0)
           });
    })
  );

...给出:

<table>
  <tr>
    <th>Position</th>
    <th>Country</th>
    <th>City List</th>
    <th>Population</th>
  </tr>
  <tr>
    <td>1</td>
    <td>Italia</td>
    <td>Milano, Venezia</td>
    <td>6</td>
  </tr>
  <tr>
    <td>2</td>
    <td>France</td>
    <td>Lyon, Paris</td>
    <td>9</td>
  </tr>
  <tr>
    <td>3</td>
    <td>Deutschland</td>
    <td>München</td>
    <td>4</td>
  </tr>
</table>

4. JSONiq与JavaScript的比较:

JavaScript相当于...

JSONiq用例部分1.1.2. JSON分组查询

(位于:https://jsfiddle.net/YSharpLanguage/hvo24hmk/3)

参见 http://jsoniq.org/docs/JSONiq-usecases/html-single/index.html#jsongrouping

其中...

/*
  1.1.2. Grouping Queries for JSON
  http://jsoniq.org/docs/JSONiq-usecases/html-single/index.html#jsongrouping
*/
var sales = [
  { "product" : "broiler", "store number" : 1, "quantity" : 20  },
  { "product" : "toaster", "store number" : 2, "quantity" : 100 },
  { "product" : "toaster", "store number" : 2, "quantity" : 50 },
  { "product" : "toaster", "store number" : 3, "quantity" : 50 },
  { "product" : "blender", "store number" : 3, "quantity" : 100 },
  { "product" : "blender", "store number" : 3, "quantity" : 150 },
  { "product" : "socks", "store number" : 1, "quantity" : 500 },
  { "product" : "socks", "store number" : 2, "quantity" : 10 },
  { "product" : "shirt", "store number" : 3, "quantity" : 10 }
];

var products = [
  { "name" : "broiler", "category" : "kitchen", "price" : 100, "cost" : 70 },
  { "name" : "toaster", "category" : "kitchen", "price" : 30, "cost" : 10 },
  { "name" : "blender", "category" : "kitchen", "price" : 50, "cost" : 25 },
  {  "name" : "socks", "category" : "clothes", "price" : 5, "cost" : 2 },
  { "name" : "shirt", "category" : "clothes", "price" : 10, "cost" : 3 }
];

var stores = [
  { "store number" : 1, "state" : "CA" },
  { "store number" : 2, "state" : "CA" },
  { "store number" : 3, "state" : "MA" },
  { "store number" : 4, "state" : "MA" }
];

var nestedGroupingAndAggregate = stores.select().orderBy("state").groupBy("state")
( function(byState) {
    var state = byState[0],
        stateStores = byState[1];
    byState = { };
    return (
      (
        byState[state] =
        products.select().orderBy("category").groupBy("category")
        ( function(byCategory) {
            var category = byCategory[0],
                categoryProducts = byCategory[1],
                categorySales = sales.filter(function(sale) {
                  return stateStores.find(function(store) { return sale["store number"] === store["store number"]; }) &&
                         categoryProducts.find(function(product) { return sale.product === product.name; });
                });
            byCategory = { };
            return (
              (
                byCategory[category] =
                categorySales.select().orderBy("product").groupBy("product")
                ( function(byProduct) {
                    var soldProduct = byProduct[0],
                        soldQuantities = byProduct[1];
                    byProduct = { };
                    return (
                      (
                        byProduct[soldProduct] =
                        soldQuantities.reduce(function(sum, sale) { return sum += sale.quantity; }, 0)
                      ),
                      byProduct
                    );
                } ) // byProduct()
              ),
              byCategory
            );
        } ) // byCategory()
      ),
      byState
    );
} ); // byState()

...给出:

[
  {
    "CA": [
      {
        "clothes": [
          {
            "socks": 510
          }
        ]
      },
      {
        "kitchen": [
          {
            "broiler": 20
          },
          {
            "toaster": 150
          }
        ]
      }
    ]
  },
  {
    "MA": [
      {
        "clothes": [
          {
            "shirt": 10
          }
        ]
      },
      {
        "kitchen": [
          {
            "blender": 250
          },
          {
            "toaster": 50
          }
        ]
      }
    ]
  }
]

如果使用JSONPath查询祖先轴时遇到限制,可以使用它来克服这些限制,如此SO问题(以及其他类似问题)所提出的问题。

例如,如何在已知品牌ID的情况下获取杂货商品的折扣。

{
 "prods": [
    {
        "info": {
              "rate": 85
                },
        "grocery": [
                 {
                  "brand": "C",
                  "brand_id": "984"
                 },
                 {
                  "brand": "D",
                  "brand_id": "254"
                 }
                 ],
         "discount": "15"
    },
    {
        "info": {
              "rate": 100
                },
        "grocery": [
                 {
                  "brand": "A",
                  "brand_id": "983"
                 },
                 {
                  "brand": "B",
                  "brand_id": "253"
                 }
                 ],
         "discount": "20"
     }
 ]
}

?

一个可能的解决方案是:
var products = {
     "prods": [
        {
            "info": {
                  "rate": 85
                    },
            "grocery": [
                     {
                      "brand": "C",
                      "brand_id": "984"
                     },
                     {
                      "brand": "D",
                      "brand_id": "254"
                     }
                     ],
             "discount": "15"
        },
        {
            "info": {
                  "rate": 100
                    },
            "grocery": [
                     {
                      "brand": "A",
                      "brand_id": "983"
                     },
                     {
                      "brand": "B",
                      "brand_id": "253"
                     }
                     ],
             "discount": "20"
         }
     ]
};

function GroceryItem(obj) {
  return (typeof obj.brand === "string") && (typeof obj.brand_id === "string");
}

    // last parameter set to "true", to grab all the "GroceryItem" instances
    // at any depth:
var itemsAndDiscounts = [ products ].nodeset(GroceryItem, true).
    map(
      function(node) {
        var item = node.value, // node.value: the current "GroceryItem" (aka "$.prods[*].grocery[*]")

            discount = node.parent. // node.parent: the array of "GroceryItem" (aka "$.prods[*].grocery")
                       parent. // node.parent.parent: the product (aka "$.prods[*]")
                       discount; // node.parent.parent.discount: the product discount

        // finally, project into an easy-to-filter form:
        return { id: item.brand_id, discount: discount };
      }
    ),
    discountOfItem983;

discountOfItem983 = itemsAndDiscounts.
  filter
  (
    function(mapped) {
      return mapped.id === "983";
    }
  )
  [0].discount;

console.log("Discount of #983: " + discountOfItem983);

...这会给出:

Discount of #983: 20

'HTH,


11

作为对一个老问题的又一新回答,我建议看看DefiantJS。它不是 JSON 的 XSLT 等效替代品,而是 JSON 的 XSLT。文档的 "模板" 部分包括以下示例:

<!-- Defiant template -->
<script type="defiant/xsl-template">
    <xsl:template name="books_template">
        <xsl:for-each select="//movie">
            <xsl:value-of select="title"/><br/>
        </xsl:for-each>
    </xsl:template>
</script>

<script type="text/javascript">

var data = {
        "movie": [
            {"title": "The Usual Suspects"},
            {"title": "Pulp Fiction"},
            {"title": "Independence Day"}
        ]
    },
    htm = Defiant.render('books_template', data);

console.log(htm);
// The Usual Suspects<br>
// Pulp Fiction<br>
// Independence Day<br>

需要浏览器 DOM 的话,会成为一个致命问题。 (在高负载的 ETL 服务器上,Puppeteer 真的不是一个好选择) - Shayne

11
说缺少工具意味着缺少需求,这只是在回避问题。同样的情况也适用于对Linux中X或Y的支持(为什么要费心开发质量驱动程序和/或游戏给这么小众的操作系统?为什么要关注大型游戏和硬件公司不开发的操作系统?)。可能需要使用XSLT和JSON的人最终会使用某种相对简单的解决方法:将JSON转换为XML。但这不是最优解决方案,不是吗?
当您拥有本机JSON格式并希望以“所见即所得”的方式在浏览器中编辑它时,XSLT将是解决问题的更适当方案。用传统的JavaScript编程来做这件事可能会变成一件麻烦的事情。
实际上,我已经实现了一种“史前时代”的XSLT方法,使用子字符串解析来解释javascript的一些基本命令,例如调用模板,处理子元素等。当然,使用JSON对象实现转换引擎比实现完整的XML解析器来解析XSLT要容易得多。问题在于,要使用XML模板来转换JSON对象,您需要解析模板的XML。
要使用XML(或HTML、文本或其他)来转换JSON对象,您需要仔细考虑语法以及需要使用哪些特殊字符来标识转换命令。否则,您最终将不得不为自己的自定义模板语言设计解析器。我曾经走过那条路,我可以告诉你那很麻烦。
更新(2010年11月12日):在我的解析器上工作了几周之后,我已经能够对其进行优化。模板预先解析,命令存储为JSON对象。变换规则也是JSON对象,而模板代码则是一种类似于shell代码的混合HTML和自制语法。我已经能够将复杂的JSON文档转换为HTML以制作文档编辑器。编辑器的代码大约有1K行(它是用于私人项目的,所以我不能分享它),JSON转换代码大约有990行(包括迭代命令、简单比较、调用模板、变量保存和计算)。我计划在MIT许可下发布它。如果您想参与,请给我发送电子邮件。

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