CSVファイルの読み書きにはいくつか方法があると思いますが、ここのサンプルは正規表現を使ってしています。
他のサンプルを見ていると、列データを特に"や'で囲んでないサンプルが多いようです。
コメントアウトしてないコードでは、"で囲まれたCSVデータに対応しています。
コメントアウトしているところに、'で囲まれたCSVデータと何にも囲まれていないCSVデータに対応できるようにしています。
皆さんの助力になれば幸いです。
ViewController.swift
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { //ファイルより一行を取得する正規表現 let CONST_REG_LINE: String = "^.*$" //項目データを取得する正規表現 //2段構えとする let CONST_REG_COLUMN_USE_DOUBLE_QUOTATION: String = "(?<=(^|,))\\s*\"((\"\")|[^\"])*\"\\s*(?=(,|$))" let CONST_REG_NET_COLUMN_USE_DOUBLE_QUOTATION: String = "(?<=\")((\"\")|[^\"])*(?=\")" let CONST_REG_COLUMN_USE_SINGLE_QUOTATION: String = "(?<=(^|,))\\s*'(('')|[^'])*'\\s*(?=(,|$))" let CONST_REG_NET_COLUMN_USE_SINGLE_QUOTATION: String = "(?<=')(('')|[^'])*(?=')" let CONST_REG_NET_COLUMN_NO_USE_QUOTATION: String = "(?<=(^|,))[^,]*(?=(,|$))" //読み込んだCSVのデータ var result: [[String]] = [] //CSVファイルにカラム名を含むかどうか var hasTitle: Bool = true @IBOutlet weak var tv: NSTableView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.tv.delegate = self self.tv.dataSource = self } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBAction func readCSV(_ sender: Any) { //列番号の変数 var index: Int = 0 //列名を処理したかどうかのフラグ var blDidTitle: Bool = false let op = NSOpenPanel() op.canChooseFiles = true op.canChooseDirectories = false op.allowsMultipleSelection = false op.allowedFileTypes = ["", "csv", "txt"] if op.runModal() == NSApplication.ModalResponse.OK { let u = op.url if u != nil { do { //ファイルデータを取得 let s = try String(contentsOf: u!, encoding: String.Encoding.utf8) //データのクリア self.result = [] let match_item: [String] = getMatchStrings(targetString: s, pattern: CONST_REG_LINE) for item in match_item { let column = getColumnData(item) if blDidTitle == false { blDidTitle = true if self.hasTitle { //タイトル行を絡むヘッダーに for r in column { let col: NSTableColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: index.description)) col.title = r col.isEditable = true index += 1 tv.addTableColumn(col) } } else { //ダミーのカラムヘッダーを //タイトル行を絡むヘッダーに result.append(column) for _ in result[0] { let col: NSTableColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: index.description)) col.title = "Column" + index.description col.isEditable = true index += 1 tv.addTableColumn(col) } } } else { result.append(column) } } } catch { return } } } else { //何もしない } self.tv.reloadData() } @IBAction func writeCSV(_ sender: Any) { var saveData: String = "" if self.hasTitle { for i in 0 ..< self.tv.tableColumns.count { if i == 0 { saveData = saveData + "\"" + tv.tableColumns[i].title + "\"" } else { saveData = saveData + ",\"" + tv.tableColumns[i].title + "\"" } } saveData = saveData + "\n" } for line in self.result { for i in 0 ..< line.count { //"で列データが囲まれている場合 if i == 0 { saveData = saveData + "\"" + line[i] + "\"" } else { saveData = saveData + ",\"" + line[i] + "\"" } //'で列データが囲まれている場合 //if i == 0 { // saveData = saveData + "'" + line[i] + "'" //} else { // saveData = saveData + ",'" + line[i] + "'" //} //列データを囲んでいない場合 //if i == 0 { // saveData = saveData + line[i] //} else { // saveData = saveData + "," + line[i] //} } saveData = saveData + "\n" } //ファイルに保存 let sp = NSSavePanel() sp.title = "保存" sp.prompt = "保存" if sp.runModal() == NSApplication.ModalResponse.OK { let path = sp.url! do { try saveData.write(to: path, atomically: false, encoding: String.Encoding.utf8) } catch { print("error: saveCSV") print("\(error.localizedDescription)") } } } func numberOfRows(in tableView: NSTableView) -> Int { return result.count } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { let i: Int = Int(tableColumn!.identifier.rawValue)! return result[row][i] } func getMatchStrings(targetString: String, pattern: String) -> [String] { var matchStrings: [String] = [] do { let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive,.anchorsMatchLines]) let targetStringRange = NSRange(location: 0, length: (targetString as NSString).length) let matches = regex.matches(in: targetString, options: [], range: targetStringRange) print(matches.count) for match in matches { let range = match.range(at: 0) let result = (targetString as NSString).substring(with: range) //print(match) print(result) matchStrings.append(result) } return matchStrings } catch { print("error: getMatchStrings") } return [] } func getColumnData(_ data: String) -> [String] { //'でカラムが囲まれている時 //return getColumnDataByQuotation(data, regColumn: self.CONST_REG_COLUMN_USE_SINGLE_QUOTATION, regNetColumn: self.CONST_REG_NET_COLUMN_USE_SINGLE_QUOTATION) //"でカラムが囲まれているとき return getColumnDataByQuotation(data, regColumn: self.CONST_REG_COLUMN_USE_DOUBLE_QUOTATION, regNetColumn: self.CONST_REG_NET_COLUMN_USE_DOUBLE_QUOTATION) //特にカラムが囲まれていない時 //return getColumnDataByComma(data) } func getColumnDataByQuotation(_ data: String, regColumn: String, regNetColumn: String) -> [String] { var columnData: [String] = [] do { //let regex = try NSRegularExpression(pattern: self.CONST_REG_COLUMN_USE_DOUBLE_QUOTATION, options: [.caseInsensitive, .anchorsMatchLines]) let regex = try NSRegularExpression(pattern: regColumn, options: [.caseInsensitive, .anchorsMatchLines]) let targetStringRange = NSRange(location: 0, length: (data as NSString).length) let matches = regex.matches(in: data, options: [], range: targetStringRange) print(matches.count) for match in matches { //let regex2 = try NSRegularExpression(pattern: self.CONST_REG_NET_COLUMN_USE_DOUBLE_QUOTATION, options: [.caseInsensitive]) let regex2 = try NSRegularExpression(pattern: regNetColumn, options: [.caseInsensitive]) //let targetStringRange2 = NSRange(location: 0, length: (data as NSString).length) //一度マッチした項目の前後を含んだデータを取得するためのRange let range2 = match.range(at: 0) //ここで正味のほかも含む文字列を取得する let result2 = (data as NSString).substring(with: range2) let targetStringRange2 = NSRange(location: 0, length: (result2 as NSString).length) let matches2 = regex2.matches(in: result2, options: [], range: targetStringRange2) let match2 = matches2[0] let range = match2.range(at: 0) let result = (result2 as NSString).substring(with: range) print(result) columnData.append(result) } } catch { print(error.localizedDescription) print("error: getColumnDataByQuotation") } return columnData } func getColumnDataByComma(_ data: String) -> [String] { var columnData: [String] = [] do { let regex = try NSRegularExpression(pattern: self.CONST_REG_NET_COLUMN_NO_USE_QUOTATION, options: [.caseInsensitive, .anchorsMatchLines]) let targetStringRange = NSRange(location: 0, length: (data as NSString).length) let matches = regex.matches(in: data, options: [], range: targetStringRange) print(matches.count) for match in matches { let range = match.range(at: 0) let result = (data as NSString).substring(with: range) print(result) columnData.append(result) } } catch { print(error.localizedDescription) print("error: getColumnDataByComma") } return columnData } }