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

南国のビーチパラソルの下で、Rプログラムを打ってる日常を求めて、、Daily Life of Bioinformatician in Kyobashi of Osaka

【Rでの文字列処理シリーズ(その3)】文字列/テキストの検出・検索: 完全一致、部分一致、拡張正規表現、曖昧一致判定

はじめに

文字列処理・テキスト処理とは、プログラミングを行うなかで、文字列・テキストに対する色々な操作のことを指します。それら処理をうまく使いこなすことで、文字列を自由に処理できるようになります。文字列処理の活用事例は、キーワード抽出、テキスト分類、テキストマイニングの前処理など、多岐に渡ります。 今回の「Rでの文字列処理」シリーズで扱う、文字列処理のライブラリ・関数群やプログラムコードは、R環境上で無料で提供されている、オープン・ソフトウェアを用います。

この記事では、R言語のプログラミングでのテキストマイニング、文字列の検出・検索、テキストの検出・検索について、いろいろと試してみました。 主に、base、stringrのパッケージを扱っています。

テキスト処理の関連記事

skume.net

skume.net

skume.net

skume.net

skume.net

skume.net

下準備について

まずは、関連パッケージとテキストデータを準備します。

########################
#シリーズ共通
########################
#必要なパッケージの読み込み
require(readr)
require(stringr)
require(stringdist)

#テストファイルのダウンロード
utils::download.file(url="https://raw.githubusercontent.com/kumeS/Blog/master/TXT_proc/test.txt",
                     destfile="test.txt")

【1】完全一致で、その文字列を含むかどうかのの判定

比較演算子 == !=

完全一致の基本は、比較演算子 ==!= を用いて、文字列の一致有無を判定する方法です。

#入力データフレーム
c <- data.frame(readr::read_tsv(file="./test.txt", col_names=F))
c

#完全一致で、文字列を含むかどうかの判定
#比較演算子を使う場合、その文字列(右辺)を含むかの判定
c == "abc"
#        X1
#[1,] FALSE
#[2,] FALSE
#[3,] FALSE
#[4,] FALSE
#[5,] FALSE
#[6,] FALSE

c == "abc ABC abc"
#        X1
#[1,]  TRUE
#[2,] FALSE
#[3,] FALSE
#[4,] FALSE
#[5,] FALSE
#[6,] FALSE

#その文字列(右辺)を含まないか?
c != "abc ABC abc"
#        X1
#[1,] FALSE
#[2,]  TRUE
#[3,]  TRUE
#[4,]  TRUE
#[5,]  TRUE
#[6,]  TRUE

base::match、stringr::str_match、演算子 %in%

次に、base::match、stringr::str_match、演算子 %in% を使って、完全一致の判定を行います。

#base::matchを使った場合
base::match(c$X1, table="abc")
#[1] NA NA NA NA NA NA

base::match(c$X1, table="abc ABC abc")
#[1]  1 NA NA NA NA NA

#stringr::str_matchを使った場合
stringr::str_match(c$X1, pattern="abc ABC abc")

# %in% を使った場合
c$X1 %in% "abc"
#[1] FALSE FALSE FALSE FALSE FALSE FALSE

c$X1 %in% "abc ABC abc"
#[1]  TRUE FALSE FALSE FALSE FALSE FALSE

【2】部分一致で、その文字列を含むかどうかの判定

charmatch関数、pmatch関数

まずは、charmatch関数、pmatch関数を使った、部分文字列の一致判定の実行例を示します。どうも文字列の先頭との部分一致を判定しているみたいです。

実行の挙動は、直観的に使いにくいかもしれません・・・

#基本的な実行例(1)
charmatch("m",   c("mean", "median", "mode"))
#[1] 0
# => 複数ヒットの場合、「0」を返す

base::charmatch("mo", table=c("mean", "median", "mode"))
#[1] 3
## => modeの「3」を返ってくる

base::charmatch("ode", table=c("mean", "median", "mode"))
#[1] NA
# => 先頭の文字が含まれないと、「NA」を返す

#基本的な実行例(2)
base::pmatch("m",   c("mean", "median", "mode"))
#[1] NA
base::pmatch("mo",   c("mean", "median", "mode"))
#[1] 3

#デフォルトは、1対1で対応づける
base::pmatch(c("", "ab", "ab"), c("abc", "ab"))
#[1] NA  2  1
#「duplicates.ok = T」で、1対多での対応づけをOKにする
base::pmatch(c("", "ab", "ab"), c("abc", "ab"), duplicates.ok = T)
#[1] NA  2  2

grep関数、grepl関数、stringr::str_detect関数

次に、grep関数、grepl関数、stringr::str_detect関数を使った事例を紹介する。こっちの方が使い勝手が良いと思われます。

c$X1
#[1] "abc ABC abc" "ABC abc ABC" "def DEF def"
#[4] "DEF def DEF" "abc.ABC.abc" "ABC.abc.ABC"

base::grep(pattern="abc", c$X1)
#[1] 1 2 5 6

base::grepl(pattern="abc", c$X1)
#[1]  TRUE  TRUE FALSE FALSE  TRUE  TRUE

c$X1[base::grepl(pattern="abc", c$X1)]
#[1] "abc ABC abc" "ABC abc ABC" "abc.ABC.abc"
#[4] "ABC.abc.ABC"

stringr::str_detect(c$X1, pattern="abc")
#[1]  TRUE  TRUE FALSE FALSE  TRUE  TRUE

#上記の逆論理値を返す
stringr::str_detect(c$X1, pattern="abc", negate = T)
#[1] FALSE FALSE  TRUE  TRUE FALSE FALSE

【3】文字列部分一致の曖昧判定

base::grepl関数

拡張正規表現を使った、文字列部分一致の曖昧判定について、いろいろな事例を扱ってみる。一通り覚えると、結構使えます。

まずは、base::grepl / stringr::str_detect + 拡張正規表現 を使います。

#「A〜C」の3文字からなる文字列を含むかどうか
base::grepl(pattern="[A-C][A-C][A-C]", c$X1)
#[1]  TRUE  TRUE FALSE FALSE  TRUE  TRUE

#「A〜C」、任意文字、「A〜C」の3文字からなる文字列を含むかどうか
base::grepl(pattern="[A-C].[A-C]", c$X1)
#[1]  TRUE  TRUE FALSE FALSE  TRUE  TRUE

#任意文字、任意文字、任意文字の3文字からなる文字列を含むかどうか
base::grepl(pattern="...", c$X1)
#[1] TRUE TRUE TRUE TRUE TRUE TRUE
(メモ) 正規表現の違い
任意文字 .
.(ピリオド)を文字指定 [.]

続いて、新たな文字列サンプルで試してみます。

#新たな文字列
test0 <- c("123", "12345", "Q1", "Q12", "QQ123")
test0
#[1] "123"   "12345" "Q1"    "Q12"   "QQ123"

#「0〜9」からなる文字列を3連続で含むかどうか
base::grepl(pattern="[0-9][0-9][0-9]", test0)
#[1]  TRUE  TRUE FALSE FALSE  TRUE

#先頭「Q」から始まって、数字2文字からなる文字列を含むかどうか
base::grepl(pattern="^Q[1-9][0-9]", test0)
#[1] FALSE FALSE FALSE  TRUE FALSE

#語尾が「0-9」「3-5」で終わる、数字2文字からなる文字列を含むかどうか
base::grepl(pattern="[0-9][3-5]$", test0)
#[1]  TRUE  TRUE FALSE FALSE  TRUE
(メモ) よく使う正規表現
先頭文字指定 先頭に ^ マーク
末尾文字指定 最後に $ マーク

次に、少し複雑な内容をやってみます。

TRUE / FALSEだと長ったらしいので、該当文字列のみを抽出しています。

#少し応用編
test1 <- c("あ", "ア", "M", "m", "1", " ", "  ", "㍍", "〒", "*", "+", "漢")

#平仮名 [あ-ん] を抽出する
test1[base::grepl(pattern="[あ-ん]", test1)]
#[1] "あ"

#片仮名 [ア-ン] を抽出する
test1[base::grepl(pattern="[ア-ン]", test1)]
#[1] "ア"

#大文字・小文字アルファベットを抽出する
test1[base::grepl(pattern="[a-z]|[A-Z]", test1)]
#[1] "M" "m"
#OR
test1[base::grepl(pattern="[[:alpha:]]", test1)]
#[1] "M" "m"

#小文字アルファベットを抽出する
test1[base::grepl(pattern="[a-z]", test1)]
#[1] "m"
#OR
test1[base::grepl(pattern="[[:lower:]]", test1)]
#[1] "m"

#数値を抽出する
test1[base::grepl(pattern="[0-9]", test1)]
#[1] "1"
#OR
test1[base::grepl(pattern="[[:digit:]]", test1)]
#[1] "1"

#アルファベット OR 数値を抽出する
test1[base::grepl(pattern="[a-z]|[A-Z]|[0-9]", test1)]
#[1] "M" "m" "1"
#OR
test1[base::grepl(pattern="\\w", test1)]
#[1] "M" "m" "1"
test1[base::grepl(pattern="[[:alnum:]]", test1)]
#[1] "M" "m" "1"

#空白文字、タブをを抽出する: やや分かりづらい
test1[base::grepl(pattern=" |  ", test1)]
#[1] " "  "  "
#OR
test1[base::grepl(pattern="[[:blank:]]", test1)]
#[1] " "  "  "

#印字可能な文字列を抽出する
test1[base::grepl(pattern="[[:print:]]", test1)]
#[1] "あ" "ア" "M"  "m"  "1"  " "  "  " "㍍" "〒" "*"  "+"  "漢"

#グラフィカル文字列を抽出する
test1[base::grepl(pattern="[[:graph:]]", test1)]
#[1] "あ" "ア" "M"  "m"  "1"  "㍍" "〒" "*"  "+"  "漢"

#パンクチュエーション文字列を抽出する
test1[base::grepl(pattern="[[:punct:]]", test1)]
#[1] "㍍" "〒" "*"  "+" 

stringr::str_detect関数

次に、stringr::str_detect関数での実行例を示します。

#平仮名 [あ-ん] を抽出する
test1[stringr::str_detect(test1, pattern="\\p{Hiragana}")]
#[1] "あ"
#OR
test1[stringr::str_detect(test1, pattern="[あ-ん]")]
#[1] "あ"

#片仮名 [ア-ン] を抽出する
test1[stringr::str_detect(test1, pattern="\\p{Katakana}")]
#[1] "ア" "㍍"
#OR
test1[stringr::str_detect(test1, pattern="[ア-ン]")]
#[1] "ア"

#漢字 を抽出する
test1[stringr::str_detect(test1, pattern="\\p{Han}")]
#[1] "漢"

stringr::str_detect関数では、\\p{Katakana}で、が抽出できるのが素敵です。

まとめ

文字列の検出は、実際テキスト処理とかでよく使います。

個人的には、grepl関数でT/F結果で返すのが「テッパン」だと思います。

補足

データフレーム全体に対して、文字列の置換を行う場合のTips

data.frame関数、lapply関数、gsub関数を組み合わせて行う。

以下には、データフレームのDatオブジェクトの各列の%を``に置換する場合の実施例を示す。

Dat <- data.frame(lapply(Dat, 
                         function(x){gsub(pattern="%", replacement = "", x)}),
                  stringsAsFactors = FALSE)

TRUE/FALSEをカウントする

lengthをよく使ってたけど、sumでもT/Fのカウントができるみたい。

文字列サンプル
test0 <- c("123", "12345", "Q1", "Q12", "QQ123")

#(例)「0〜9」からなる文字列を3連続で含むかどうか
count <- base::grepl(pattern="[0-9][0-9][0-9]", test0)
count
#[1]  TRUE  TRUE FALSE FALSE  TRUE

#lengthで、TRUEのカウント
length(count[count])
#[1] 3

#lengthで、FALSEのカウント
length(count[!count])
#[1] 2

#sumで、TRUEのカウント
sum(count)
#[1] 3

#sumで、FALSEのカウント
sum(!count)
#[1] 2

【応用編】文字列の検出: character(0)、numeric(0)を検出する

character(0)numeric(0)の処理は結構悩ましいので、メモしておく。

#前準備
a <- character(0)
b <- numeric(0)

#character(0)検出の方法
identical(a, character(0))
#[1] TRUE

#失敗例
character(0) == 0
#logical(0)

#numeric(0)検出の方法
identical(b, numeric(0))
#[1] TRUE

#失敗例
numeric(0) == 0
#logical(0)

#character(0)あるいはnumeric(0)の条件で検出する
Hantei <- function(x){
  ch <- identical(x, character(0))
  nu <- identical(x, numeric(0))
  return(any(c(ch, nu)))
}

#判定結果
Hantei(a)
#[1] TRUE
Hantei(b)
#[1] TRUE
Hantei("Baka")
#[1] FALSE

参考資料

mikuhatsune.hatenadiary.com

www.karada-good.net

www.okadajp.org