【问题标题】:import a csv into a array of structs将 csv 导入到结构数组中
【发布时间】:2017-11-21 20:41:11
【问题描述】:

编辑:我需要将 CSV(具有字符串和整数列类型)作为结构数组导入。 不是作为结构的二维数组(答案很好地解释了)。

原始问题(解释不清):

我正在尝试创建一个包含字符串和整数值的二维数组(基于我导入的 CSV 的内容)。这些可以通过它们所在的“列”来识别。有没有办法做到这一点?我知道三种可能的解决方案:

  1. 将数组声明为[Any],但我不想这样做,因为我希望它被显式键入

  2. 通过使用.flatmap(如How to convert a String (numeric) in a Int array in Swift

    让字符串 = "123,456,789" 让 intArray = string.components(separatedBy: ",").flatMap { Int($0) } 但我不希望这样做,因为我的数组的“字符串”列中可能有整数。

  3. 将我的数组保留为[String],并将我的“Int”列作为字符串(这是我目前正在做的)。

下面是我正在使用的代码的简化版本。当我尝试将逗号分隔的字符串解析为具有不同值类型的数组时,它会出错。

非常感谢所有帮助...谢谢!

struct MyStruct {
    let Str1 : String
    let Str2 : String
    let Str3 : String
    let Str4 : String
    let Int1 : Int
}

    func csvStringToArray(stringCSV: String) -> [[MyStruct]] {
        
        // create an empty 2-D array to hold the CSV data.
        var dataArray: [[MyStruct]] = []
        
        // parse the CSV into rows.
        var csvRows: [String] = stringCSV.components(separatedBy: "\n") 
       
        print(csvRows)
        // ["a,b,c,d,99", "j,k,l,m,98", "w,x,y,z,97", etc]

        // append each row (1-D array) to dataArray (filling the 2-D array).
        for i in csvRows {
            let csvColumns: [MyStruct] = i.components(separatedBy: ",") // Cannot convert value of type '[String]' to specified type '[MyStruct]'
            dataArray.append(csvColumns)
            print(csvColumns) // this is the output if I set csvColumns: [String]; otherwise it errors out.
            // ["a", "b", "c", "d", "99"]
            // ["j", "k", "l", "m", "98"]
            // ["w", "x", "y", "z", "97"]
            // etc...
        }
        print(dataArray) // again only if csvColumns: [String]
        // ["a", "b", "c", "d", "99"], ["j", "k", "l", "m", "98"], ["w", "x", "y", "z", "97"], [etc... ]

        return dataArray
    }

【问题讨论】:

  • 为什么不使用 CSV 解析库?一方面,components(separatedBy: ",") 将通过检测所有转义的逗号(必须将其解析为字符串的一部分,而不是 CSV 结构的一部分)而失败
  • 为了简单起见,我试图避免使用第 3 方库。我知道我的数据将始终遵循相同的格式(我确定还有其他问题,但您提到的具体问题不会影响我)。即使在这种情况下使用一个会更好吗?
  • 我认为重新发明轮子而不是使用已经免费提供的东西并不简单。需求变化,数据格式变化,所以我不会浪费时间为如此有限的 CSV 子集编写解析器。
  • 点了。进一步思考,我喜欢components(separatedBy: ","),因为我可以很容易地看到它是如何工作的。以前没有使用过图书馆,这对我来说更像是一个黑匣子。我需要加快速度的许多事情之一。
  • 那么,这绝对是要解决的问题。黑匣子很好。您越频繁地生成功能良好的代码而无需关心其内部结构越好。少即是多。

标签: arrays swift struct


【解决方案1】:

正如您在评论中指出的那样,此行不起作用:

let csvColumns: [MyStruct] = i.components(separatedBy: ",")

components(separatedBy:) 方法总是返回一个字符串数组 ([String]),因此您不能将该值设置为类型为 MyStruct 数组 ([MyStruct]) 的变量。

看起来你的 MyStruct 类型应该代表一整行数据(4 个字符串和一个 int),所以我认为说一列是 MyStructs 的数组是不正确的。第一个原因是这些不代表列,而是行,并且似乎 1 MyStruct == 1 行值,并且数据数组只是这些 MyStruct 行值的数组([MyStruct])而不是您现在拥有的 MyStruct 实例数组 ([[MyStruct]]) 的数组。

也就是说,为了最接近您所拥有的,我建议将 MyStruct 替换为可以保存 String 或 Int 的 FieldValue 枚举。使用这种方法,您的代码将如下所示:

enum FieldValue {
    case string(String)
    case int(Int)
}

func csvStringToArray(stringCSV: String) -> [[FieldValue]] {

    // create an empty 2-D array to hold the CSV data.
    var dataArray: [[FieldValue]] = []

    // parse the CSV into rows.
    var csvRows: [String] = stringCSV.components(separatedBy: "\n") 

    print(csvRows)
    // ["a,b,c,d,99", "j,k,l,m,98", "w,x,y,z,97", etc]

    // append each row (1-D array) to dataArray (filling the 2-D array).
    for i in csvRows {
        let csvRowValues: [FieldValue] = i.components(separatedBy: ",").map{
            if let value = Int($0) { 
                return FieldValue.int(value)
            }
            return FieldValue.string(value)
        }
    dataArray.append(csvRowValues)
    print(csvRowValues)
    print(dataArray) 
    return dataArray
}

【讨论】:

  • 在阅读了您和 Santhosh 的帖子后,我现在完全理解了您对 [[MyStruct]] 与 [MyStruct] 的看法。谢谢你解释。使用这种方法,例如,如果一行中的第二个元素是一个字符串整数,如“2”,它也会被转换为 Int 类型吗?
  • @user45392 是的,每个元素都被解析为包含 Int 或 String 的 FieldValue。准备好使用它们时,您可以像这样打开它们:switch fieldValue { case .int(let int): /*do something with the Int value int*/ case .string(let string): /*do something with the String value string*/ }
【解决方案2】:

您的MyStruct 已经是一行的结构。 MyStruct 的每个元素都是一列。所以最终你的dataArrayfunc csvStringToArray(stringCSV: String)的返回值)应该是[MyStruct],而不是[[MyStruct]]。那么你的函数就变成了:

func csvStringToArray(stringCSV: String) -> [MyStruct] {

    // create an empty 2-D array to hold the CSV data.
    var dataArray: [MyStruct] = []

    // parse the CSV into rows.
    var csvRows: [String] = stringCSV.components(separatedBy: "\n") 

    print(csvRows)
    // ["a,b,c,d,99", "j,k,l,m,98", "w,x,y,z,97", etc]

    // append each row (1-D array) to dataArray (filling the 2-D array).
    for i in csvrows {
        let columns = i.components(separatedBy: ",")
        let csvColumns: MyStruct = MyStruct.init(Str1: columns[0], Str2: columns[1], Str3: columns[2], Str4: columns[3], Int1: 
        Int(columns[4])!)
        dataArray.append(csvColumns)
    }
    print(dataArray) 
    return dataArray
}

这显然是假设您的数据类型和结构始终有效(您的行肯定总是有 4 个字符串和一个整数,因此它们可以被索引或强制解包),否则您将不得不为此添加检查。

【讨论】:

  • 感谢您指出我只需要 MyStruct 周围的一组括号来返回函数的值,因为它已经设置为保存一维元素。这使错误消息对我来说消失了。如果我使用这种方法,我是否会失去在 dataArray 上使用下标的能力?
  • 别介意我的问题。我忽略了将其放入此结构的全部原因。 dataArray[0].Int1 将返回 99。非常感谢!
【解决方案3】:

如果您希望转换完全键入并避免Any,请尝试以下方法:

struct MyDataType {

  struct Row {

    enum DataPoint {
      case string(String)
      case integer(Int)

      init?(stringValue: String) {
        guard !stringValue.isEmpty else { return nil }

        if let integerValue = Int(stringValue) {
          self = .integer(integerValue)
        } else {
          self = .string(stringValue)
        }
      }
    }

    let dataPoints: [DataPoint]

    init?(csv: String) {
      let components = csv.components(separatedBy: ",")
      guard !components.isEmpty else { return nil }

      dataPoints = components.flatMap(DataPoint.init)
    }
  }

  let rows: [Row]

  init?(csv: String) {
    let csvRows = csv.components(separatedBy: .newlines)
    guard !csvRows.isEmpty else { return nil }

    rows = csvRows.flatMap(Row.init)
  }
}


// Example usage.

let csvString = """
123,134,a,65,4,bc
43,53,t,4,5,1
a,3,e,12,u,50
"""

//  Force unrwapping here, because we know `csvString` is valid csv.
//  You should not force unwrap generally.
let data = MyDataType(csv: csvString)!

if let firstRow = data.rows.first {

  print("First rows content:")

  for (idx, dataPoint) in firstRow.dataPoints.enumerated() {
    let descriptor: String

    switch dataPoint {
    case .string(let stringValue):
      descriptor = "String: \(stringValue)"
    case .integer(let integerValue):
      descriptor = "Integer: \(integerValue)"
    }

    print("[\(idx)] > \(descriptor)")
    /*
     Prints:


     First rows content:
     [0] > Integer: 123
     [1] > Integer: 134
     [2] > String: a
     [3] > Integer: 65
     [4] > Integer: 4
     [5] > String: bc
     */
  }
}

解释:

  • MyDataType 代表所有导入的 csv 数据。
  • Row 表示导入的 csv 数据中的一行。
  • DataPoint 可以容纳 StringInt。请注意,这与 Optional<T> 的工作方式非常相似。
  • 每个MyDataType 有多个Rows。
  • 每个Row 有多个DataPoints。
  • MyDataTypeRowDataPoint 声明可以解析它们各自部分的 csv 字符串的初始化程序。添加了一些保护措施来检查空字符串,但这取决于实现。

更新

考虑到 cmets 中的更多上下文,您可以通过以下方式扩展上面提供的通用解决方案并强制执行特定的 csv 格式:

extension MyDataType.Row {
  init?(enforcingCustomFormatOn csv: String) {
    let components = csv.components(separatedBy: ",")
    guard components.count == 5 else { return nil }

    dataPoints = components.enumerated().flatMap { idx, stringValue in
      switch idx {
        //  Enforcing .string in the first 4 columns
      case 0..<4: return .string(stringValue)
      default: return DataPoint(stringValue: stringValue)
      }
    }
  }
}

extension MyDataType {
  init?(enforcingCustomFormatOn csv: String) {
    let csvRows = csv.components(separatedBy: .newlines)
    guard !csvRows.isEmpty else { return nil }

    rows = csvRows.flatMap(Row.init(enforcingCustomFormatOn:))
  }
}

您现在可以简单地使用init(enforcingCustomFormatOn:),第 1-4 列将被视为字符串值。如果可能,仅将第 5 列转换为 .integer。

请注意,您需要将MyDataType.init(csv:) 中的rows = csvRows.flatMap(Row.init) 更改为rows = csvRows.flatMap(Row.init(csv:))。否则,.flatMap 将不知道选择哪个 init,因为两者具有相同的签名。

【讨论】:

  • 是的。改变了那个。谢谢:)
  • 谢谢你,卡布斯。即使我这次使用了 Santhosh 的方法,详细的解释也非常有帮助(我知道我的数据将始终遵循非常严格的格式)。您的方法将转换任何连续的整数(不仅仅是第 5 列),对吗?我需要将第 5 列中没有的任何内容保留为字符串,即使它看起来像一个整数。
猜你喜欢
  • 2021-06-25
  • 2013-12-11
  • 1970-01-01
  • 2013-12-16
  • 1970-01-01
  • 2014-09-19
  • 1970-01-01
  • 1970-01-01
  • 2018-05-16
相关资源
最近更新 更多