首页 > 解决方案 > 如何访问 JArray 中不同级别的嵌套 JObject 的值以将它们导出到 CSV 文件?

问题描述

我是 C#、REST API 和 JSON 的新手,所以请多多包涵。我已经搜索了数百页,并试图弄清楚这一切是如何运作的。对于上下文,我使用类似搜索http://mylink.com/rest/api/2/search?jql=project%3DBULK并获取问题列表从 JIRA REST API 中提取问题。我的程序引入的 JSON 结构如下(这已从实际 JSON 中简化):

编辑(04/04/2021):用新的用例更新了下面的 JSON。

[
   {
     "key": "FAKE-5402",
     "fields": {
        "summary": "I need access to blah",
        "customfield_18302": [{
            "self": "fake.url.com",
            "value": "I need this"
        },  {
            "self": "fake.url2.com",
            "value": "I need this also"
        }]
     }
   },
   {
     "key": "FAKE-5450",
     "fields": {
        "summary": "Example number 2",
        "customfield_18302": [{
            "self": "fake.url3.com",
            "value": "I need this"
        },  {
            "self": "fake.url4.com",
            "value": "I need this also"
        }]
     }
   }
]

我目前正在研究一种获取此 JSON 数据的方法,找到用户正在搜索的键并将其以 CSV 格式打印出来,以便稍后写入文件。这是我的formatAsCSV()功能:

public void formatAsCSV()
{
    try
    {
        var dynObj = JsonConvert.DeserializeObject<dynamic>(this.jsonData);
        String issues = dynObj["issues"].ToString();

        JArray issuesArray = JArray.Parse(issues);

        List<String> columnNames = new List<String>()
        {"key", "summary"};

        String headerRow = "";
        foreach (String columnName in columnNames)
        {
            headerRow += columnName + ", ";
        }
        headerRow = headerRow.TrimEnd(' ');
        headerRow = headerRow.TrimEnd(',');
        headerRow += "\n";

        String dataRows = "";
        foreach (var record in issuesArray)
        {
            String thisRecord = "";
            foreach (String columnName in columnNames)
            {
                thisRecord += record[columnNames] + ", ";
            }

            thisRecord = thisRecord.TrimEnd(' ');
            thisRecord = thisRecord.TrimEnd(',');
            thisRecord += "\n";
            dataRows += thisRecord;
        }

        this.csvData = headerRow + dataRows;

        Console.WriteLine("\ncsvData: ");
        Console.WriteLine(this.csvData);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error in formatAsCSV: " + ex);
        this.errorsOccurred = true;
    }
}

我在上面的代码中遇到的问题(我已经尝试了几天来解决这个问题......)是它将所有问题作为单独的对象引入。因此,当代码循环通过时,我可以获得“key”、“expand”、“id”、“self”,因为它们没有嵌套。当我需要诸如“摘要”和“问题类型”->“名称”之类的内容时,相同的代码不会抓取嵌套在“字段”下的内容。我不知道从哪里开始获取这些信息。

这是我从这样的东西中得到的输出(注意第一个逗号后缺少摘要):

csvData:
key, summary
BULK-62, 
BULK-47,

如果有人有任何想法,请告诉我,因为我已经没有东西可以尝试了。这里的大多数问题都以 JObject 开头,但我不断收到错误,“问题”是我拉的主要内容,它是一个数组,而不是一个对象,所以不知道如何处理这个问题。我知道它可以以某种方式工作,因为 JIRA 允许 CSV 通过手动过程提取。我希望有一种方法可以保持我拥有的过程(它来自遵循 YT 教程),所以我不必重写我已经完成的所有内容,但我理解它是否归结为这一点。提前致谢!

标签: c#jsonjson.netexport-to-csvjira-rest-api

解决方案


我最近回答了一个类似的问题,提问者想从嵌套中提取所有属性并将这些值变成 CSV 列。在您的情况下,您希望从对象及其子项中选择不同级别的属性。JObject

有效解决此问题的关键是利用该SelectToken()方法对您有利。此方法接受JsonPath表达式,它允许您查询 aJToken以查找其后代之一。在最简单的形式中,这只是一个以点分隔的字符串,例如fields.summary.

借用另一个问题,我会做两个辅助方法。第一种方法将接受JObjects表示行项目的列表以及Dictionary<string, string>将列名映射到将用于从每个项目中检索值的相应路径。第二种方法将接受一个值列表并将其转换为 CSV 行。

public static class JsonHelper
{
    public static string ToCsv(this IEnumerable<JObject> items, Dictionary<string, string> columnPathMappings)
    {
        if (items == null || columnPathMappings == null) 
            throw new ArgumentNullException();

        var rows = new List<string>();
        rows.Add(columnPathMappings.Keys.ToCsv());
        foreach (JObject item in items)
        {
            rows.Add(columnPathMappings.Values.Select(path => item.SelectToken(path)).ToCsv());
        }
        return string.Join(Environment.NewLine, rows);
    }

    public static string ToCsv(this IEnumerable<object> values)
    {
        const string quote = "\"";
        const string doubleQuote = "\"\"";
        return string.Join(",", values.Select(v => 
            v != null ? string.Concat(quote, v.ToString().Replace(quote, doubleQuote), quote) : string.Empty
        ));
    }
}

有了这些方法,创建 CSV 字符串所需要做的就是设置映射,然后解析 JSON,从中提取 items 数组并调用帮助程序来完成剩下的工作:

var columnPathMappings = new Dictionary<string, string>
{
    { "key", "key" },
    { "summary", "fields.summary" }
};

string csv = JArray.Parse(json).Cast<JObject>().ToCsv(columnPathMappings);

这是一个工作演示:https ://dotnetfiddle.net/zzBpbT


如果您的 JSON 有一个内部数组,那么这意味着单行中的特定列可能有多个值。鉴于 CSV 是一个扁平结构,在插入 CSV 之前,需要将多个值连接在一起形成一个以换行符分隔的字符串。您可以像这样修改上面的代码:

  1. 在第一种方法中更改SelectTokenSelectTokens. 这将允许它为单个路径提取多个值,而不仅仅是一个。
  2. 在第二种方法中,检查并处理对象 inIEnumerable<object> values本身是一个IEnumerable<Jtoken>. 在这种情况下,将 JToken 连接到一个以换行符分隔的字符串中,然后像以前一样处理该字符串。

修改后的代码如下所示:

public static class JsonHelper
{
    public static string ToCsv(this IEnumerable<JObject> items, Dictionary<string, string> columnPathMappings)
    {
        if (items == null || columnPathMappings == null) 
            throw new ArgumentNullException();

        var rows = new List<string>();
        rows.Add(columnPathMappings.Keys.ToCsv());
        foreach (JObject item in items)
        {
            rows.Add(columnPathMappings.Values.Select(path => item.SelectTokens(path)).ToCsv());
        }
        return string.Join(Environment.NewLine, rows);
    }

    public static string ToCsv(this IEnumerable<object> values)
    {
        const string quote = "\"";
        const string doubleQuote = "\"\"";
        return string.Join(",", values.Select(v => 
        {
            if (v != null)
            {
                if (v is IEnumerable<JToken> e)
                {
                    v = string.Join(Environment.NewLine, e.Select(t => t.ToString()));  
                }
                return string.Concat(quote, v.ToString().Replace(quote, doubleQuote), quote);
            }
            return string.Empty;
        }));
    }
}

最后一步是您需要指定一个路径,该路径将获取数组中的子值。为此,您可以*在数组索引的路径中使用通配符。所以你的映射看起来像这样:

var columnPathMappings = new Dictionary<string, string>
{
    { "key", "key" },
    { "summary", "fields.summary" },
    { "customfield_18302", "fields.customfield_18302[*].value" },
};

在这里工作演示:https ://dotnetfiddle.net/IfB8sw


推荐阅读