如何访问 JArray 中嵌套 JObject 的不同级别的值以将它们导出到 CSV 文件?

How can I access values at different levels of nested JObjects in a JArray to export them to a CSV file?

我是 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 教程),这样我就不必重写我已经完成的所有内容,但我理解是否归结为这一点。提前致谢!

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

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

有一个 I recently where the asker wanted to extract all properties from a nested JObject 并将这些值变成 CSV 列。在您的情况下,您想从对象及其子项中挑选不同级别的属性。

有效解决这个问题的关键是使用 SelectToken() method to your advantage. This method accepts a JsonPath expression, which lets you query a JToken 找到它的后代之一。在最简单的形式中,这只是一个点分隔的字符串,例如 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,从中提取项目数组并调用助手来完成剩下的工作:

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<object> values中的对象本身是IEnumerable<Jtoken>的可能性。在这种情况下,将 JTokens 连接成一个换行符分隔的字符串,然后像以前一样处理该字符串。

修改后的代码如下所示:

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