【问题标题】:How can I access values at different levels of nested JObjects in a JArray to export them to a CSV file?如何访问 JArray 中不同级别的嵌套 JObject 的值以将它们导出到 CSV 文件?
【发布时间】:2021-03-17 15:48:16
【问题描述】:

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

编辑(2021 年 4 月 4 日):使用新用例更新了下面的 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 教程),所以我不必重写我已经完成的所有内容,但我理解是否归结为这一点。提前致谢!

【问题讨论】:

  • @dbc 感谢您告诉我,我继续进行了一些更改。我的工作不允许粘贴到这个站点,所以我超级简化了 JSON,仍然足以解决问题。布赖恩,我还没有看到那个,所以我会试一试,看看我能不能让它同时工作。谢谢你们!

标签: c# json json.net export-to-csv jira-rest-api


【解决方案1】:

有一个similar question 我最近answered 询问者想要从嵌套的JObject 中提取所有属性并将这些值变成CSV 列。在您的情况下,您想从对象及其子对象中挑选不同级别的属性。

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

借用另一个问题,我会做两个辅助方法。第一种方法将接受代表行项目的JObjects 列表以及将列名映射到将用于从每个项目中检索值的相应路径的Dictionary&lt;string, string&gt;。第二种方法将接受一个值列表并将其转换为 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. 在第一种方法中将SelectToken 更改为SelectTokens。这将允许它为单个路径提取多个值,而不仅仅是一个。
  2. 在第二种方法中,检查并处理IEnumerable&lt;object&gt; values 中的对象本身是IEnumerable&lt;Jtoken&gt; 的可能性。在这种情况下,将 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

【讨论】:

  • 这真是令人难以置信,这不仅正是我所寻找的,而且它会使更新以添加更多字段对我来说更容易。谢谢你。我确实对 rows.Add(columnPathMappings.Values.Select(path => item.SelectToken(path).ToCsv())); 有疑问当我在 VS2019 中使用此代码时收到一条错误消息“参数 1:无法从 'System.Collections.Generic.IEnumerable' 转换为 'string'”,因此不确定发生了什么,尽管看到它在您链接的演示。无论哪种方式,将其标记为答案。谢谢你所做的一切,真的。
  • 你的括号弄错了。确保完全按照我的方式复制整行。
  • 这很尴尬。奇迹般有效。你让我省了很多麻烦!
  • 很高兴能帮上忙!
  • 您好,目前运行良好,但我在这里遇到了问题。我试图修复它几个星期无济于事。你不会真的知道,因为在我的原始示例中没有完全提供 JSON(我刚刚发现了这些嵌套数组字段......)但是当字段时代码没有将任何信息拉入 Excel 工作表它的拉动里面有一个数组。它只是空白,是否可以执行 foreach (JObject... 然后如果有一个数组,它会进一步下降?我猜它无法访问数组内部的东西,因为我只是打电话JObject.
【解决方案2】:

您尝试访问的字段在 Json 对象中嵌套得更深。 请注意Json结构。 “summary”字段嵌套在“fields”对象中,因此要访问它,您需要先访问 fields 对象。

这意味着,您必须重新考虑使用列名列表的方法。

【讨论】:

    猜你喜欢
    • 2017-11-23
    • 1970-01-01
    • 2015-08-16
    • 1970-01-01
    • 1970-01-01
    • 2013-04-09
    • 2020-11-12
    • 2016-06-21
    • 1970-01-01
    相关资源
    最近更新 更多