京橋のバイオインフォマティシャンの日常

まずは、データ分析、コマンドラインのメモとして

【R・ビッグデータ解析の処方箋①】readLines、connection オブジェクトを使って、テキストファイルの1行ずつ読み込みを実行してみた件〜

現状、数十GB・数百GBといった、大きなファイルを扱う際には、R/メモリ上で全データを読み込むことはややリスキーである。

ファイル全体を読み込まず、ファイル内の1行ずつで処理を実施する工夫が必要となる。*1

Rで、1行ずつの処理を実行するには、readLines関数を用いる。

readLines関数は、connection オブジェクトから一部またはすべてのテキスト行を読み込む関数である。

また、connection オブジェクトとは、つまり「一般化されたファイル」(圧縮ファイル、URL、パイプなど)を作成、オープン、クローズするための関数オブジェクトを指す。このR Documentationの説明を読んでてても、いまいち分からないのだが、要するに、R上に全ファイルのロードせずに、DB的にファイルと接続して、データのやり取りをするというイメージだろう。

テストファイルを用意したので、readLinesの実行例を解説する。

練習ファイルのダウンロード

#gistからファイル・ダウンロード
utils::download.file(url="https://gist.githubusercontent.com/kumeS/398fbeb50c71cf828190aced63b0d1b0/raw/0fb095ae07a4e331ef23e6fc9350a5d7d276b732/file.txt",
                     destfile="file.txt")

#ファイル表示
system("cat file.txt")
#AAA
#BBB
#CCC
#DDD
#EEE
#FFF
#GGG
#HHH
#III
#JJJ
#KKK
#LLL
#MMM
#NNN
#OOO
#PPP
#QQQ
#RRR

readLinesのダメな実行例

これは、間違った使い方であるが、 ファイル名を指定して、readLinesを実行してもダメ・・・である。

以下の実行だと、同じ出力が繰り返されるだけである。

#ファイルパス
txt_file <- "./file.txt"

readLines(con=txt_file, n = 1)
#[1] "AAA"
readLines(con=txt_file, n = 1)
#[1] "AAA"

##引数
#con: connection オブジェクト
#n: 読み込む (最大の) 行数

readLinesの実行コード例

まず、ファイルパスから、connection オブジェクトを作成する。

con_file <- file(description = "./file.txt", open = "r")

str(con_file)
# 'file' int 3
# - attr(*, "conn_id")=<externalptr> 

con_file
#A connection with                        
#description "./file.txt"
#class       "file"      
#mode        "r"         
#text        "text"      
#opened      "opened"    
#can read    "yes"       
#can write   "no"  

connection オブジェクトをreadLinesで読み込んでいくと、実行回数ごとに出力が変わる。

#readLinesを実行するごとに、違う行が表示される
readLines(con_file, n = 1)
#[1] "AAA"
readLines(con_file, n = 1)
#[1] "BBB"
readLines(con_file, n = 1)
#[1] "CCC"
readLines(con_file, n = 1)
#[1] "DDD"
readLines(con_file, n = 1)
#[1] "EEE"

次に、forループで実行する例を示す。

#for実行の場合
con_file <- file(description = "./file.txt", open = "r")
for(n in 1:18){
a <- readLines(con_file, n = 1)
print(a)
}

#[1] "AAA"
#[1] "BBB"
#[1] "CCC"
#[1] "DDD"
#[1] "EEE"
#[1] "FFF"
#[1] "GGG"
#[1] "HHH"
#[1] "III"
#[1] "JJJ"
#[1] "KKK"
#[1] "LLL"
#[1] "MMM"
#[1] "NNN"
#[1] "OOO"
#[1] "PPP"
#[1] "QQQ"
#[1] "RRR"

さらに、stackoverflow内で、dvdさんが示していた関数の事例を下記に示す。

whileを使って、if ( length(line) == 0 )の条件を入れることで、 ファイルの全行数が分からなくても、実行の開始・終了ができる。

processFile = function(filepath) {
  con = file(filepath, "r")
  while ( TRUE ) {
    line = readLines(con, n = 1)
    if ( length(line) == 0 ) {
      break
    }
    print(line)
  }
  close(con)
}

processFile(filepath="./file.txt")
#[1] "AAA"
#[1] "BBB"
#[1] "CCC"
#[1] "DDD"
#[1] "EEE"
#[1] "FFF"
#[1] "GGG"
#[1] "HHH"
#[1] "III"
#[1] "JJJ"
#[1] "KKK"
#[1] "LLL"
#[1] "MMM"
#[1] "NNN"
#[1] "OOO"
#[1] "PPP"
#[1] "QQQ"
#[1] "RRR"

まとめ

connection オブジェクトの扱い方とか、ノーマークだったけど、Rにも便利なオブジェクトがあるもんだ。

補足

実際の事例では、readLines関数でGBオーダーのテキストファイルを読み込んでいく場合に、n = 1では、インタプリンタ言語の性格上、非常に読み込み効率が悪く、n = 10000あるいはn = 100000くらいに設定して読み込むのが良い。

私の経験上、readLines関数での10万行の読み込みは、1-2秒で完了する。

#readLinesの実行例
readLines(con_file, n = 100000)

参考資料

stackoverflow.com

*1:sedコマンドで、1行ずつ行を抽出できるが、sedコマンドでも一度ファイル全体を読み込んでから処理を実行しているらしく、大きなファイルにsed実行は不向きである。