readrパッケージがWindows上だと日本語のパスを読めない問題の現状

今のCRAN版のreadrには、WindowsでR 3.5.0以上だと日本語を含むパスのファイルを読み取れないというバグがあります。 これはGitHub上ではとっくに直したんですが、諸般の事情でリリースされていません。また、解決しそうにない問題もあります。

そのあたりの事情をちょっと説明します。細かいことはいいから解決方法を教えてくれ!という人は「どうすればいいのか」だけ読んでください。

背景

Windowsを日本語で使っているとデフォルトの文字コードShift_JIS(CP932)ですが、ファイル名・ディレクトリ名にはShift_JIS以外の文字を使うこともできます。 嘘だと思うなら、今すぐテキストエディタを開いて、適当なテキストを「🍣.csv」とかのファイル名で保存できることを確かめてみましょう。

こうしたファイルパスを適切に扱うために、R 3.5.0からはファイルパスを展開する関数path.expand()が常にUTF-8の文字を返すようになりました。

path.expand() on Windows now accepts paths specified as UTF-8-encoded character strings even if not representable in the current locale. (PR#17120)

具体的にはこういう感じです。

# R 3.4.4まで
Encoding(normalizePath("~/鬼"))
#> [1] "unknown"
# R 3.5.0から
Encoding(normalizePath("~/鬼"))
#> [1] "UTF-8"

とはいえ、これは大きな変更ではありません。Rの関数はだいたい文字コードをちゃんと見てくれるので大丈夫です。 実際、言われるまでこんな変更には気付かなかったという人がほとんどではないでしょうか。

問題は、C++の実装です。C++の関数に渡す前に適切な文字コードに変換していればいいんですが、英語圏だったりmacOSLinuxを使っていればその辺の実装が間違っていても動いてしまうので気付かれないままになりがちです。 readrパッケージも、文字コードに無頓着だったのがたまたま動いていて、それがR 3.5.0になったときに発覚した、という感じです。

readrパッケージ(とreadxlパッケージ)にあった問題

readrパッケージは、ファイルパスを扱うのに Boost.Interprocessのboost::interprocess::file_mapping()という関数を使っています。 この関数には、ファイルパスをUTF-8ではなくシステムのデフォルトの文字コードで渡す必要があります。しかし、今のCRAN版の実装は文字列をそのまま渡してしまうので、R 3.5.0の変更でUTF-8の文字列が渡され、それがShift_JISとして解釈されるので文字化けする、というわけです。

これの修正はPRを見てもらえばいいですが、Rcppには文字コードを操作する関数が用意されていないので、こういう場面ではCの実装に頼ることになります。具体的にはRf_translateChar()というCの関数を使います。

readxlパッケージにも、ちょっと直し方が違うんですが同じ問題がありました。以下のPRで直っています(個人的にはreadrと同じように直した方がいいのでは?と思っています)。

それでも直らない問題

さて、ここまで読んで、「ん、なんかおかしくない?」と思った方は勘がいい。 背景のところで「Shift_JIS外の文字も扱うためにUTF-8にする」と書きましたが、実際にboost::interprocess::file_mapping()が使うのはShift_JISです。 ということは当然、Shift_JIS外の文字を含むファイルパスは扱えません。🍣.csvはreadrでは読み込めないのです。

Boost.Interprocess側で対応しそうな動きはないので、これはちょっと解決方法がなさそうです。readrがBoost.Interprocessを捨てればあるいは、という感じですが、そこまで大きな変更を入れるモチベーションもないでしょう。 外国語や絵文字のファイル名にそれほど出会わないことを祈りましょう。

なぜリリースされないのか

まあそんな問題はありつつ、少なくとも日本語を扱えないというバグは直っています。 にも関わらず、なぜ新しいreadrがCRANにリリースされないのでしょうか?

それは、今年の1月12日にCRANにサブミットしたものの、Rのconnection関連のチェックに引っかかってしまってアクセプトされなかったからです。

それ以降、状況は変わっていなさそうです。むしろ今のCRAN版もずっとWarningが出続けているので、最悪このままCRANから削除されるというパターンさえあるかもしれません。

CRAN Package Check Results for Package readr

(ちなみにreadxlの方はそういう問題はなさそうなので、なぜリリースされないのかは謎...)

どうすればいいのか

(このへんは自己責任でお願いします)

GitHub版をインストールする

GitHub版は直っているので、install_github()するというのは手です。 開発中なのでバグがあるかもしれませんが、最近はほとんど開発されていないので、たぶんそこまで激しいバグには当たらないでしょう。

devtools::install_github("tidyverse/readr")

ファイル名・ディレクトリ名を英語に変える

可能ならこれがいいでしょう。めんどくさいですけど。

file.link()でsymlinkをつくる

あとは、めんどくささとしてはあまり変わらないですが、file.link()でsymlinkを貼るという方法もあります。 まあ、これもユーザ名が日本語だったりするとだめな気がしますけど...

library(readr)

write.csv(iris, "萼片长.csv", row.names = FALSE)

# 长がShift_JIS外の文字なので読めない
readr::read_csv("萼片长.csv")
#> Error in guess_header_(datasource, tokenizer, locale) : 
#>   Cannot read file C:/path/to/萼片<U+957F>.csv: ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。

# symlinkを張るといける
tmp <- tempfile(fileext = ".csv")
file.link("萼片长.csv", tmp)
#> [1] TRUE
readr::read_csv(tmp)
#> Parsed with column specification:
#> cols(
#>   Sepal.Length = col_double(),
#>   Sepal.Width = col_double(),
#>   Petal.Length = col_double(),
#>   Petal.Width = col_double(),
#>   Species = col_character()
#> )
#> # A tibble: 150 x 5
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#>           <dbl>       <dbl>        <dbl>       <dbl> <chr>  
#>  1          5.1         3.5          1.4         0.2 setosa 
#>  2          4.9         3            1.4         0.2 setosa 
#>  3          4.7         3.2          1.3         0.2 setosa 
#>  4          4.6         3.1          1.5         0.2 setosa 
#>  5          5           3.6          1.4         0.2 setosa 
#>  6          5.4         3.9          1.7         0.4 setosa 
#>  7          4.6         3.4          1.4         0.3 setosa 
#>  8          5           3.4          1.5         0.2 setosa 
#>  9          4.4         2.9          1.4         0.2 setosa 
#> 10          4.9         3.1          1.5         0.1 setosa 
#> # ... with 140 more rows

なので、こんな感じの関数を用意しておけば、日本語のファイル名でも読むことができるはずです。

my_read_csv <- function(file, ...) {
  tmp <- tempfile(fileext = ".csv")
  on.exit(unlink(tmp))
  file.link(file, tmp)
  readr::read_csv(tmp, ...)
}

まとめ

分かったところで救いはない話でした。つらい。