5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Swift] txtファイルのn行目をなるべく速く取得する

Last updated at Posted at 2020-09-30

何MBもあるような巨大なtxtファイルのn行目だけを読み込みたい瞬間ってありますよね。
普通にやるなら、Stringとして読み込んで、string.components(separatedBy:"\n")[index]string.split(separator:"\n", omittingEmptySubsequences: false)[index]みたいな感じでなりそうですが、これだとファイル全体を分割しているので効率が悪そうです。
そこで実験してなるべく速くn行目を取得する方法を探してみました。

実験

用いたファイルは青空文庫のリスト「公開中 作家別作品一覧拡充版:全て(CSV形式、zip圧縮)」で、ダウンロードした時点で17000行ちょい、サイズにして15MBほどあります。計測は

  1. ファイルの読み込み
  2. 分割
  3. 1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000行目を同時に取得し、表示
    という処理を行わせています。

1.components

func testPerformanceGetNthLineComponents() throws{
    self.measure {

        let string:String
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }

            string = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            string = ""
        }

        let splited = string.components(separatedBy: "\n")
        let strings = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].map{splited[$0]}
        print(strings)
    }
}

わかりやすくて良い方法で、平均0.192秒とそこそこのスピードです。

[Time, seconds] 
average: 0.192, 
relative standard deviation: 5.157%, 
values: [0.221948, 0.188873, 0.188767, 0.188911, 0.189229, 0.188864, 0.188640, 0.188775, 0.188801, 0.189360]

2. enumerateLines

文字列を1行ずつ取得できるenumerateLinesを使えば、全体を分割せずとも必要な部分まで読んでstopすれば良いことになります。

func testPerformanceGetNthLineEnumerateLines() throws{
    self.measure {

        let string:String
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }

            string = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            string = ""
        }

        var indices = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].sorted().makeIterator()
        guard var target = indices.next() else{
            return
        }
        var count = 0
        var result:[String] = []
        string.enumerateLines(invoking: {line, stop in
            if target == count{
                result.append(line)
                if let _target = indices.next(){
                    target = _target
                }else{
                    stop = true
                }
            }
            count += 1

        })
        print(result)
    }
}

初回が異様に遅い(飛び値)ので平均ではcomponentsに負けていますが、初回を除いた平均は0.101001とcomponentsより高速です。
XCTestでPerformance測定をすると初回が異様に遅くなりがちなのはなぜなんでしょうか……。

[Time, seconds] 
average: 0.362, 
relative standard deviation: 216.306%, 
values: [2.711354, 0.101003, 0.100871, 0.100783, 0.101008, 0.101088, 0.100787, 0.101444, 0.101147, 0.100880]

3.Dataとして読み込んでwithUnsafeBytessplitする

Stringにする前にsplitしてn行目を取ってしまう、というやり方です。

func testPerformanceGetNthLineDataAndWithUnsafeBytes() throws{
    self.measure {

        let data:Data
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }
            let url = URL(fileURLWithPath: path)
            data = try Data(contentsOf: url)

        } catch let error {
            print("ファイルが存在しません: \(error)")
            data = Data()
        }

        let bytes = data.withUnsafeBytes {
            $0.split(separator: UInt8(ascii: "\n"), omittingEmptySubsequences: false)
        }
        let result = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].compactMap{String(bytes: bytes[$0], encoding: .utf8)}
        print(result)
    }
}

何をやっているのかわかりづらくなった分、速度がグンと高速化します。平均0.039と、enumuratedLinesの2.6倍、componentsの4.9倍高速です。

[Time, seconds] 
average: 0.039, 
relative standard deviation: 37.924%, 
values: [0.084047, 0.037760, 0.034003, 0.033994, 0.034018, 0.033920, 0.034091, 0.033933, 0.034028, 0.033961]

4.DatawithUnsafeBytesで1バイトずつ調べる

3の方法ではやはり全体をsplitしていますが、この方法なら必要な行まで読んだ時点でstopできるのでその分高速化するはずです。

func testPerformanceGetNthLineDataAndWithUnsafeBytesAndScaning() throws{
    self.measure {

        let data:Data
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }
            let url = URL(fileURLWithPath: path)
            data = try Data(contentsOf: url)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            data = Data()
        }

        var indicesIterator = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].sorted().makeIterator()
        guard var targetIndex = indicesIterator.next() else{
            return
        }
        let bytes:[[UInt8]] = data.withUnsafeBytes {
            var results:[[UInt8]] = []
            var result:[UInt8] = []
            var count = 0

            for byte in $0{
                let isNewLine = byte == UInt8(ascii: "\n")
                if count == targetIndex && !isNewLine{
                    result.append(byte)
                }
                
                if count > targetIndex{
                    results.append(result)
                    result = []
                    if let _targetIndex = indicesIterator.next(){
                        targetIndex = _targetIndex
                        if count == targetIndex{
                            result.append(byte)
                        }
                    }else{
                        break
                    }
                }
                
                if isNewLine{
                    count += 1
                }
            }

            if !result.isEmpty{
                results.append(string)
            }

            return results
        }
        
        print(bytes.map{String(bytes: $0, encoding: .utf8)})
    }
}

平均で0.026秒と、3の方法よりも高速になりました。最初のcomponentsと比較すると7.4倍ほど高速化できたことになります。思いついた限りではこれが一番速い方法でした。

[Time, seconds] 
average: 0.026, 
relative standard deviation: 45.925%, 
values: [0.061011, 0.029828, 0.023706, 0.020109, 0.020274, 0.025639, 0.020205, 0.020277, 0.020406, 0.020213]

10/2追記

if count == targetIndex && !isNewLine{
    result.append(byte)
}
↓↓↓↓↓
if count == targetIndex && !isNewLine{
    result.append(byte)
    continue   //count == targetIndexかつ!isNewLineなので、これ以降の行が呼ばれることはない。
}

とした方がパフォーマンスが上がるかな?と試したのですが、なんとcontinueがない方が速いという不思議な結果になりました。continueはなしでいきましょう。

[Time, seconds] 
average: 0.043, 
relative standard deviation: 31.214%, 
values: [0.082764, 0.040316, 0.038075, 0.038079, 0.037942, 0.038081, 0.038038, 0.037882, 0.038093, 0.038405]

結論

速度が必要な場合は4番の方法でいきましょう。もっと速く取得できる方法をご存知の方がいたら是非教えてください🙏🙏🙏

参考

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?