文字列を削除したい.R

要約

・対象は、各値の書式は「記号(説明)」かつマルチアンサーとして結合
・各値の(説明)だけを削除してマルチアンサー形式を維持
・stringr::str_replace_allを使い、正規表現で抽出して空白で置換

処理対象は書式固定のマルチアンサー

 目下の相手(処理対象のデータ)はこんな感じになっている。

No hoge            fuga    piyo
01 AA(AAはAのA)|BA(BAはBのA)   ……   ……
02 AB(ABはAのB)         ……   ……
03 BA(BAはBのA)         ……   ……

 つまり、Noとhogeとfuga以下略というラベルがつけられた行列が結合されたデータであり、Noがキーになっている。
 各Noに対して、その属性を記載したのがhogeだと思ってくれてよい。
 例えばDQの呪文でいえば、メラは火属性で、ヒャドは氷属性、メドローアは火と氷の2属性を持つ(=マルチアンサー)。
 そしてメラは火属性(A)の最下位呪文(A)なのでAAと記載されるが、とにかく火属性であればよいものを抽出するときは先頭のAだけで抽出して分析するというような使い方をしている。
 メドローアは厳密には違う?そうだよ。

 で、このデータ自体は(そこそこ)整然と作られているので、データフレームとして読み込むのは簡単だった。たまに欠損してるとかhogeの説明部分が同じ記号に対してなぜか2種類あるとかそんなイライラはさておき。

 ところが、hogeには記号AA・AB・BA…だけが入っていると思っていたら、記号がなんであるかを説明した部分が()でくくられてくっついて出てくることに、データを出力して初めて気づいた。
 説明部分は分析中には必要のないもので、むしろ値が無駄に長くなって邪魔くさいから削除してしまいたい。
 これが、各値がマルチアンサーじゃなかったら、最初の"("以降を削除するという単純な話で良くて、Excelで言えばleftとfindだけで可能だ。

Excelではどうやってたか

 賢いかどうかはともかく、マクロを使って、要素ごとに処理を行い、さらにループで繰り返して全体を処理していた。
 要素ごとの処理そのものは、マルチアンサーになった値=文字列を先頭からバラシていく感じだ。
 文字列の先頭から最初の"("までを切り取って適当な変数に格納し、そこから最初の")"+区切り文字を削除したら、残った文字列を同様に先頭から"("までを切り取って以下を繰り返す。
 最後には区切り文字がないので、ループを抜ける条件、特に再帰呼び出しを使うときはもう少し工夫するのだけど、それは割愛。

stringrで使える置換関数

 Rだってプログラミング言語だ。VBAマクロを翻訳すればできる。でもあんな入れ子が多いコードはメンテナンス性が悪いから残したくない。
 となると、Rで新規に書かないといけないのだが、見通しはあった。
 Rは行列をそのまま放り込めるから、ループが省略できる。あとはどんな関数を使うかだ。
 処理後の形としては、「AA(...)|BA(...)」が「AA|BB」となっていればいい。マルチアンサーは維持する。
 結論、()で囲まれた部分を特定して()ごと削除(=空白で置換)してやればよさそうだ。

 stringrパッケージには、置換に使える関数が2つあるらしい。

str_replace(df$hoge, "検索文字列", "")   #最初の1個だけ置換
str_replace_all(df$hoge, "検索文字列", "")  #複数あったら全部置換

 マルチアンサーなので今回はstr_replace_allを使う必要がありそうだ。あとは検索文字列に、()で囲まれた部分を特定できるような文字列を入れればいい。

検索文字列を正規表現で特定する

 俺だって*(ワイルドカード)くらいは知ってるんだ。"(*)"でやればほら完璧。

 まあ、そんなわけはない。
 色々調べた結果、気を付けるポイントは2か所のようだ。

・"("や")"はエスケープ文字(\)を使って書く必要がある
・任意文字列は".*?"を使う

 エスケープ文字はつまり、"("や")"を式の一部ではなく文字列として解釈させるための記号だ。ただ、エスケープ文字自体もエスケープしておく必要があるのか、"\\("及び"\\)"と書く必要があるとのこと。
 なんだそのめんどくさいエスケープと思ったが、見慣れてくるとちょっとかわいい。

 次に任意文字列、つまり説明部分。
 正しい正規表現としては、"*"(アスタリスク)は直前の文字の0回以上繰り返しであって、任意文字は"."(ドット)なので、".*"とすればいいはず。
 すなわち全体としては、"\\(.*\\)"とすれば、()内の文字列は()と共に削除できるはずなのだ。
 OKOK。顔文字っぽくてかわいいぞ。

 ところが色んな解説を読むと、".*"を使った場合には、区切り文字単位で値を区切れないようで、例えばマルチアンサーの先頭の値の"("と、最後の値の")"を組み合わせて処理してしまう(i.e. 2個目以降の値が失われる)ことがあるようだ。しかも関数依存性があるらしい。
 両方試した結果、少なくともstr_replace_allではNGパターンだったので、先人に従って任意文字列は".*?"を使い、全体としては検索文字列="\\(.*?\\)"と指定することで、目的の形式に変換することができた。
 目が2つになってかわいさもさらにアップである。

 なお、この記事を全体を上記の式で置換すると、()内の文字列は当然()とともに削除できる。
 のだが、かっこは全角と半角が混在しているので一つの式でやろうとするともう一工夫が必要になってくるのだけど、これは重要なことなので覚えて置きたい。
 全角半角を系統的に使い分けて書いてあるデータなんて少ないのだ。他人のデータも、自分のデータも信用してはいけない。

 あれ?そういえばVBAでも正規表現って使えなかったっけ……まあいいか。

最終形

x <- str_replace_all(df$hoge, "\\(.*?\\)","")

 df$hogeはn行1列の行列なので、出力もn行1列で出てくる。
 順番は当然保存されているので、そのまま元のデータフレームに追加しても行の対応はとれたままだ。
 最後はxを元のdfに結合する。こうすれば、前回のopenxlsxパッケージ使ってxlsx形式で出力可能だ(し、多分探せばcsv出力だってあるんだろう)。

 df <- data.frame(df, hogehoge = x) #xをhogehoge列として追加

以上。

この記事が気に入ったらサポートをしてみませんか?