抽出による文字列分離(1).R
要約
・stringr::str_extract_all()+正規表現で、セパレータのコンマとそうでないコンマを区別しながら抽出
過去のチャレンジ
というわけで、マルチアンサーを文字列操作で各値に分けるときには、セパレータを基準に各値に分離(separate, split)するということをやっていた。マルチアンサーになっている値は、セパレータ区切りで複数の値を一つにまとめたものだからだ。
ところが、実データに適用しようとしたら問題が出てしまった。
特許分類(FI)の分離
[1] "G02F1/133,545,G09G3/36,H04N5/66,102@B"
特許には特許分類というものが付けられている。各特許に書かれている発明を特許庁が技術的に分類して分類コードをつけたものだ。いくつか種類はあるのだが、これはFIというものになる。
セパレータはコンマで、その数を数えると"4つ"ある。従って、5つのFIが4つのコンマを使って1つのマルチアンサー形式の値になっている……わけではなく、実はFIは3つしかない。
FIは分類コードなので書式が決まっているのだが、ちょっとややこしい。
上の"G02F1/133,545"で言えば、"G02F1/133"がFIの本体部分で、そこに",545"という補足部分がくっついている。また、"G09G3/36"は本体部分のみだけで成り立っているし、"H04N5/66,102@B"に至っては、本体部分である"H04N5/66"に",102"とさらに"@B"という2つの補足部分がくっついている。なお、",hoge"がなくて"@hoge"だけがくっついているFIも存在する。
なので、セパレータとして使われているコンマを"|"で置換してあげると、次のようになる。
[1] "G02F1/133,545|G09G3/36|H04N5/66,102@B"
ほら3つだった。ところが、これを機械的に分離しようと思うと少し骨が折れる。少なくとも、単純には分離できない。
なお、特許データベースであるJ-PlatPatでダウンロードできる形式はcsv形式だ。そうコンマ区切りだ。だからFIもコンマ区切りマルチアンサーだ。
なんでや。
分離の方針:抽出する
もう単純分離は無理だとあきらめて、別の方法を考える。
以前、かっこで囲まれた文字列を削除するために、stringr::str_replace_all()を使った。ここでは削除したい文字列を正規表現で抽出して、空白に置き換えるということをした。
幸い、FIという分類コードは特定の書式に従って書かれている。こういったものは、正規表現で抽出しやすい。
言い換えると、正規表現を用いて特定の書式を満たす文字列をFIとして抽出できれば、FIの一部としてのコンマと、セパレータとしてのコンマを区別しながら、マルチアンサーを構成する各FIを取り出すことができるということになる。
stringr::str_extract_all()
str_replace_all()では指定した文字列を置換したが、str_extract_all()では指定した文字列を抽出する。ここで正規表現を使って文字列を指定すれば、その書式に当てはまるものすべてが抽出されることになる。
また、str_extract_all()をベクタに適用すると、入力ベクタの各要素ごとで抽出された複数の文字列がベクタとして出力され、さらにこれらの出力ベクタはリスト形式でまとめられて出力される。
上で述べたように、FIは本体部分と補足部分(2種類)に分かれていて、本体部分は必ずあるけれども、補足部分はあったりなかったりする。
とりあえずは本体部分だけを抽出してみて、出力を確認する。
------
与えるFI(入力ベクタ)
------
> df$FI %>% head()
[1] "G06Q20/06"
[2] "E04F13/10@A,E04F13/08,102@A,E04F15/04@E"
[3] "A41D13/00,A41D3/00@Z"
[4] "G06Q50/06,G06Q30/02,322,G06Q20/38,310"
[5] "G06Q50/00"
[6] "B60L53/80,E03D9/08@B,B60K1/04@A,B62D63/02,B60S5/06,B60L50/60"
------
出力リスト(リスト化されたベクタ)
------
> df$FI %>% str_extract_all("[A-H]\\d{2}[A-Z]\\d{1,4}/\\d{2,5}") %>% head()
[[1]]
[1] "G06Q20/06"
[[2]]
[1] "E04F13/10" "E04F13/08" "E04F15/04"
[[3]]
[1] "A41D13/00" "A41D3/00"
[[4]]
[1] "G06Q50/06" "G06Q30/02" "G06Q20/38"
[[5]]
[1] "G06Q50/00"
[[6]]
[1] "B60L53/80" "E03D9/08" "B60K1/04" "B62D63/02" "B60S5/06" "B60L50/60"
本体部分の書式は、1桁目はA-Hのいずれか1文字で、次に2他桁に揃えられた数字が来て、4桁目はA-Zのいずれか1文字が入り、さらに1~4桁の数字が入った後に、"/"を挟んでさらに2~5桁の数字が来るようになっているのだけれども、正規表現を使って書くと上のようになる。
ちなみに、この数字部分の処理については実はデータベースごとに違っている。今回のように桁数非固定だったり、桁数固定で0や空白で埋めたり(例. G06Q0020/0006)、固定する桁数もFIとして存在しうる最大桁数だったり、今後の増加をみこして1桁余分に持っていたりと様々なので、そのデータベースごとで考えてやる必要があったりする。
それはともかく、出力結果の感じもなんとなくつかめたところで、次は補足部分だ。
補足部分はあったりなかったりなので、正規表現的に言えば、"(補足部分)?"になる。丸カッコでグループ化された部分が0または1回繰り返される、だ。
補足部分は2種類あるので同様に作ってやって、先ほどの正規表現に追記して実行してみる。
> df$FI %>%
str_extract_all("[A-H]\\d{2}[A-Z]\\d{1,4}/\\d{2,5}(,\\d{1,3})?(@[A-Z])?") %>%
head()
[[1]]
[1] "G06Q20/06"
[[2]]
[1] "E04F13/10@A" "E04F13/08,102@A" "E04F15/04@E"
[[3]]
[1] "A41D13/00" "A41D3/00@Z"
[[4]]
[1] "G06Q50/06" "G06Q30/02,322" "G06Q20/38,310"
[[5]]
[1] "G06Q50/00"
[[6]]
[1] "B60L53/80" "E03D9/08@B" "B60K1/04@A" "B62D63/02" "B60S5/06" "B60L50/60"
なんとかできてそうだ。念のため、str_remove_all()で同じことをして、抽出残しがないか見てみることにする。
> df$FI %>% str_remove_all("[A-H]\\d{2}[A-Z]\\d{1,4}/\\d{2,5}(,\\d{1,3})?(@[A-Z])?")
[1] "" ",,"
[3] "," ",,"
[5] "" ",,,,,"
#中略
[93] ",,,,," ",,,,H02P101:25,H02P101:45"
すると、セパレータとして使われていたコンマだけが残……あ、古い書式のものが残っていた。
面倒だから、予め新しい書式に合わせるように全体をstr_replace_all(":", "/")しておいてから、str_extract_all()する。これでOK。
> df$FI %>% str_replace_all(":","/") %>%
str_extract_all("[A-H]\\d{2}[A-Z]\\d{1,4}/\\d{2,5}(,\\d{1,3})?(@[A-Z])?") %>%
head()
[[1]]
[1] "G06Q20/06"
[[2]]
[1] "E04F13/10@A" "E04F13/08,102@A" "E04F15/04@E"
[[3]]
[1] "A41D13/00" "A41D3/00@Z"
[[4]]
[1] "G06Q50/06" "G06Q30/02,322" "G06Q20/38,310"
[[5]]
[1] "G06Q50/00"
[[6]]
[1] "B60L53/80" "E03D9/08@B" "B60K1/04@A" "B62D63/02" "B60S5/06" "B60L50/60"
最終的にはこのリストを解体して、複数のFIを別のセパレータで結合しなおしながら、元のベクタ要素との対応を取りつつデータフレームに戻してあげたい。
すると次は、リストの解体方法や結合方法とかを考えなければいけない。
今日はここまで。