Blog スタッフブログ

iOS Swift システム開発 ひとくちコードスニペット

[Swift]ローカルのCSVファイルを読み込むコードスニペット

Swift

こんにちは、株式会社MIXシステム開発担当のBloomです。

今回はSwiftでローカルのCSVファイルを読み込みたい時に利用するコードスニペットを掲載させていただきます。

それではさっそくCSVファイルを読み込むクラスを実装しましょう。

class CSVLoader {
    /// リソース内 CSV を [[String]] で返す
    static func load(name: String, in bundle: Bundle = .main) throws -> [[String]] {
        guard let url = bundle.url(forResource: name, withExtension: "csv") else {
            throw NSError(domain: "file not found", code: -1)
        }

        let data = try Data(contentsOf: url)

        // 1. 文字コード推定
        guard let encoding = data.detectStringEncoding() else {
            throw NSError(domain: "encoding detect failed", code: -2)
        }

        // 2. デコードおよび改行コード置換
        guard var text = String(data: data, encoding: encoding) else {
            throw NSError(domain: "decode failed", code: -3)
        }
        text.normalizeNewlinesInPlace() // CRLF/CRをLFへ置換

        // 3. CSVパース処理
        return parseCSV(text)
    }
}


private extension Data {
    // 簡単な文字コード推定処理
    func detectStringEncoding() -> String.Encoding? {
        // 1.BOMで判定
        if self.starts(with: [0xEF, 0xBB, 0xBF]) { // UTF-8 BOM
            return .utf8
        }
        if self.starts(with: [0xFF, 0xFE]) {       // UTF-16 LE BOM
            return .utf16LittleEndian
        }
        if self.starts(with: [0xFE, 0xFF]) {       // UTF-16 BE BOM
            return .utf16BigEndian
        }

        // 2.BOMなしutf8判定
        if String(data: self, encoding: .utf8) != nil {
            return .utf8
        }

        // 3.sjis判定
        if String(data: self, encoding: .shiftJIS) != nil {
            return .shiftJIS
        }
        
        return nil
    }
}

private extension String {
    mutating func normalizeNewlinesInPlace() {
        self = self.replacingOccurrences(of: "\r\n", with: "\n")
        self = self.replacingOccurrences(of: "\r", with: "\n")
    }
}

private func parseCSV(_ text: String, separator: Character = ",", quote: Character = "\"") -> [[String]] {
    var rows: [[String]] = []
    var row: [String] = []
    var field = ""
    var inQuotes = false
    var iterator = text.makeIterator()

    func endField() {
        row.append(field)
        field = ""
    }
    func endRow() {
        endField()
        rows.append(row)
        row.removeAll(keepingCapacity: true)
    }

    while let c = iterator.next() {
        if inQuotes {
            if c == quote {
                if let next = iterator.peek(), next == quote {
                    _ = iterator.next()
                    field.append(quote)
                } else {
                    inQuotes = false
                }
            } else {
                field.append(c)
            }
        } else {
            switch c {
            case quote:
                inQuotes = true
            case separator:
                endField()
            case "\n":
                endRow()
            default:
                field.append(c)
            }
        }
    }
    
    if !(field.isEmpty && row.isEmpty) {
        endRow()
    }
    return rows
}

private extension String.Iterator {
    mutating func peek() -> Character? {
        var copy = self
        return copy.next()
    }
}

呼び出し例

        do {
            let rows = try CSVLoader.load(name: "sample") // sample.csv
            print(rows)
        } catch {
            print("CSV 読み込み失敗: \(error)")
        }

ここではCSVファイルをある程度文字コードの自動判定、改行コードの自動判定を行いながら読み込んでいます。ユーザがインポートするCSVファイルであればある程度自動判定するのが良いかと思われますが、リソース内CSVファイルであればこれらは直接指定する方が良いため適宜書き換えて実装をしてください。

これだけでCSVファイルの読み込みができました。良かったですね。