餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

Rでバイナリファイルを置換

なるべく避けた方が良い気がするのですが、Rでもバイナリのデータを扱えます。0から255の範囲を整数のpack形式と、0か1のunpack形式のraw型として処理されるのですが、C言語あたりから入った人だとややこしく感じるかも知れません*1

1. pack/unpack形式のraw型

やろうと思ったら画像ファイルのEXIFの情報を引っ張っていて、それを統計処理にかけたりできます。

  • よくあるコード番号をraw型にする場合は、例えばASCIIコード32をintToBits(32)とするとunpack形式のraw型になります。さらにpackBits(intToBits(32))とするとpack形式のraw型になります。
  • pack形式のraw型は文字列として取り扱うためにcharacter型にでき、例えばrawToChar(packBits(intToBits(32)))とすると空白が得られます。rawToCharの逆の操作になる関数もあって、charToRaw(" ")で文字列をraw型にでき、16進数の文字コードが表示されます。文字コード(数値文字参照)が連なった文字列をまとめてraw型に変える場合は、やや煩雑で、例えば`s <- "737472696e67"; s_raw <- as.raw(as.hexmode( sapply(seq(1, nchar(s), 2), function(i){ substr(s, i, i + 1) }) ));などとします*2
  • バイナリ形式のI/OはreadBin関数とwriteBin関数を用いますが、pack形式のraw型でデータを読み込み、また書き出すことになります。画像ファイルのEXIF情報などを読んだりするのに使えます。

2. バイナリ置換

RからはCを呼べるので、あまりRでバイナリ処理はしないだろうなと思っていたら、バイナリ置換をすることになりました*3

binreplace <- function(input, output, pattern, replacement, maximum=1, buffsize=1024*1024){

    istream <- file(input, "rb", blocking = FALSE)
    ostream <- file(output, "wb")

    if(length(pattern)<buffsize) buffsize <- length(pattern)*10
    buf <- readBin(istream, "raw", buffsize) 

    i <- 1
    counter <- as.integer(maximum) # maximum回だけ置換するが、負の数を入れれば無限回
    EOF <- FALSE
    while(!EOF){
        while(i <= length(buf) - length(pattern) + 1){
            # RでKnuth Morris Pratt法を書く元気がないのでバカ検索
            if(0!=counter && 
                all(buf[i:(i + length(pattern) - 1)] == pattern)){
                writeBin(replacement, ostream)
                i <- i + length(pattern)
                counter <- counter - 1
            } else {
                writeBin(buf[i], ostream)
                i <- i + 1
            }
        }
        abuf <- readBin(istream, "raw", buffsize)
        if(i<=length(buf)) buf <- c(buf[i:length(buf)], abuf)
        else buf <- abuf
        i <- 1
        if(0==length(abuf) && !isIncomplete(istream)) EOF = TRUE
    }

    writeBin(buf, ostream)

    close(ostream)
    close(istream)
}

binreplace("input.txt", "ouput.txt", charToRaw("old phrase"), charToRaw("new phrase"), 1)

わざわざblocking = FALSEにして、ファイル末尾に達する前に0バイト入力の可能性をつくって、isIncompleteでそうなっていないか確認しています。
なお、16進数の文字列に変えて正規表現で置換したらどうかと思って試してみたのですが、文字列からraw型に戻すのが手間なためか、逆に3倍ぐらい時間がかかるようになりファイルサイズの上限がつくものの処理時間を半分ぐらいに短縮できました*4

3. まとめ

明らかに遅いので使えるならばsedあたりを使いましょう。パッケージを追加しないとflockもできませんし。

*1:pack/unpackでアセンブラを思い出しますね。

*2: s_rawを表示させると16進数のベクトルに見えますが、rawToChar(s_raw)で文字列に変換されるのでraw型になっているのが分かります。

*3: localeがJapanese_Japan.932のRはUnicodeの絵文字(e.g. ✓ ♨ ♡ ⚑)を文字として認識できず、テキストファイルとして無理に読み込むと絵文字を消してしまいます。あるファイルの中のある単語を一つ置換したかったのですが、テキスト処理では不可能に・・・と思っていたんですが、2週間経って`Sys.setlocale("LC_ALL", "C")`して処理してから`Sys.setlocale("LC_ALL", "C")`で間に合うことに気づきました。警告は出ますが。なお、system関数でsedを呼んだら済むのですが、sedが入っていないWindows環境も考えたかったので手間暇かけています。

*4: コードを消すかと思っときに、文字列からraw型に戻すのにforしているところをsapplyに代えたら速くなるかなと思ったら、速くなりました。ただし処理対象のファイルサイズが指定するバッファサイズ以下という制限がつくので、今回は遅い方のコードのままにします。