備忘ログ

チラシの裏的備忘録&メモ

Rで漢数字からアラビア数字への変換-最終章その1-

前回総集編と言った気がしたが、あれは嘘だった。

相変わらずの{zipangu}ネタ。

漢数字からアラビア数字への変換について、くどくど書いてきたが、こんどこそ最終章のつもり(章って!?ってはなしもある)。意図的に小出しにしているわけじゃなく、思いついた改修を都度都度やっているから……

今回の改良は

  • 対応できる漢数字の形が増えた(ただし依存関係も増えた)。
  • 形態素解析MeCab)を利用した関数を作ってみた(この関数には依存関係にMeCab{RMeCab}あるのでちょっと使いにくさはある)。

zipanguのISSUEにあった漢数字(複数桁)の変換 · Issue #8もこれで解決できると思う。コード重たいけど。文字列じゃなくて数字で欲しかったらas.numericで数字に変換してやればいい。

改良版自作関数

依存関係は、{zipangu}と以前作成したアラビア数字から漢数字に変換する自作パッケージの{arabic2kansuji}

github.com

indenkun.hatenablog.com

最後2つ関数は形態素解析をしてその上で漢数字をアラビア数字に変換するになっているが、依存関係は上記に加えてさらにMeCab{RMeCab}があって、{RMeCab}が使える状態になっていることが必要。

library(magrittr)

kansuji2arabic_kai <- function(str, consecutive = c("convert", "non"), ...) {
  consecutive <- match.arg(consecutive)
  
  n <- stringr::str_split(str,
                          pattern = stringr::boundary("character")) %>%
    purrr::map(zipangu::kansuji2arabic) %>%
    purrr::reduce(c) %>% as.numeric()
  
  if(!any(n >= 10) && length(n) > 1){
    if(consecutive == "convert") return(zipangu::kansuji2arabic_all(str, ...))
    if(consecutive == "non") return(str)
  }
  
  if(length(n) > 2 && any(n >= 10000) && all(n != 10) && all(n !=100) && all(n != 1000)){
    for(i in 1:(length(n) - 1)){
      if(n[i] < 1000 && n[i + 1] < 10){
        n[i + 1] <- as.numeric(stringr::str_c(c(n[i], n[i + 1]), collapse = ""))
        n[i] <- NA
      }
    }
    n <- na.omit(n)
  }
  
  
  if(!any(n >= 10000)){
    if(length(n) == 1){
      res <- n
      return(res)
    }else{
      res <- NULL
      for(i in 1:length(n)){
        if(i == length(n) && n[i - 1] >= 10)
          res[i] <- n[i]
        else if(length(n[i - 1]) == 0 && n[i] >= 10)
          res[i] <- n[i]
        else if(n[i] <= 9 && n[i + 1] >= 10 )
          res[i] <- n[i] * n[i + 1]
        else if(n[i] >=10 && n[i - 1] >=10)
          res[i] <- n[i]
      }
    }
    res <- sum(na.omit(res))
    return(res)
  }else{
    if(length(n) == 1){
      res <- n
      return(res)
    }else{
      ans <- NULL
      l <- 1
      k <- 1
      basyo <- which(n >= 10000)
      keta <- sum(n >= 10000)
      ketasuu <- n[n >= 10000]
      if(max(which(n >= 10000)) <= max(which(!n >= 10000))){
        keta <- keta + 1 
        ketasuu <- c(ketasuu, 1)
        basyo <- c(basyo, (max(which(n >= 0)) + 1))
      }
      for (j in 1:keta) {
        m <- basyo[k] - 1
        nn <- n[l:m]
        res <- NULL
        for (i in 1:length(nn)) {
          if(length(nn) <= 1)
            res[i] <- nn[i]
          else if(i == length(nn) && nn[i - 1] >= 10)
            res[i] <- nn[i]
          else if(length(nn[i - 1]) == 0 && nn[i] >= 10)
            res[i] <- nn[i]
          else if(nn[i] <= 9 && nn[i + 1] >= 10 )
            res[i] <- nn[i] * nn[i + 1]
          else if(nn[i] >=10 && nn[i - 1] >=10)
            res[i] <- nn[i]
        }
        ans[k] <- sum(na.omit(res)) * ketasuu[k]
        l <- basyo[k] + 1
        k <- k + 1
      }
      ans <- sum(na.omit(ans))
      return(ans)
    }
  }
}

kansuji2arabic_num <- function(str, ...){
  purrr::map(str, kansuji2arabic_kai, ...) %>% unlist()
}

kansuji2arabic_kai2 <- function(str, consecutive = c("convert", "non"), widths = c("all", "halfwidth"), ...){
  consecutive <- match.arg(consecutive)
  widths <- match.arg(widths)

  if(widths == "all"){
    arabicn_half <- "1234567890"
    arabicn_full <- "1234567890"
    
    arabicn_half <- unlist(stringr::str_split(arabicn_half, ""))
    arabicn_full <- unlist(stringr::str_split(arabicn_full, ""))
    
    names(arabicn_half) <- arabicn_full
    
    str <- stringr::str_replace_all(str, arabicn_half)
  }
  str <- arabic2kansuji::arabic2kansuji(str)
  
  
  doc_num <- stringr::str_split(str, pattern = "[^零〇一二三四五六七八九十百千万億兆京]")[[1]]
  doc_num[doc_num == ""] <- NA
  str <- stringr::str_replace_all(str, pattern = "[零〇一二三四五六七八九十百千万億兆京]",  replacement = "〇〇")
  doc_str <- stringr::str_split(str, pattern = "[零〇一二三四五六七八九十百千万億兆京]")[[1]]
  doc_num <- kansuji2arabic_num(na.omit(doc_num), consecutive)
  
  j <- 1
  for(i in 1:length(doc_str)){
    if(!stringr::str_detect(doc_str[i], pattern = "") && i == 1){
      doc_str[i] <- doc_num[j]
      j <- j + 1
    }
    else if(consecutive == "non"){
      if((stringr::str_detect(doc_str[i - 1], pattern = "[^0123456789]") 
          && stringr::str_detect(doc_str[i - 1], pattern = "[^零〇一二三四五六七八九]"))
         && !stringr::str_detect(doc_str[i], pattern = "")){
        doc_str[i] <- doc_num[j]
        j <- j + 1
      }
    }
    else if(stringr::str_detect(doc_str[i - 1], pattern = "[^0123456789]")
            && !stringr::str_detect(doc_str[i], pattern = "")){
      doc_str[i] <- doc_num[j]
      j <- j + 1
    }
    if((length(doc_num) + 1)  ==  j) break
  }
  ans <- stringr::str_c(doc_str, collapse = "")
  return(ans)
}

kansuji2arabic_str <- function(str, ...){
  purrr::map(str, kansuji2arabic_kai2, ...) %>% unlist()
}

kansuji2arabic_str_mecab <- function(str, ...){
  purrr::map(str, kansuji2arabic_mecab, ...) %>% unlist()
}

kansuji2arabic_mecab <- function(str, consecutive = c("convert", "non"), widths = c("all", "halfwidth"), ...){
  if(length(str) > 1) stop("only one strings can convert to kansuji.")
  consecutive <- match.arg(consecutive)
  widths <- match.arg(widths)
  
  if(widths == "all"){
    arabicn_half <- "1234567890"
    arabicn_full <- "1234567890"
    
    arabicn_half <- unlist(stringr::str_split(arabicn_half, ""))
    arabicn_full <- unlist(stringr::str_split(arabicn_full, ""))
    
    names(arabicn_half) <- arabicn_full
    str <- stringr::str_replace_all(str, arabicn_half)
    }
  
  tmpf <- tempfile()
  write(str, tmpf)
  invisible(capture.output(keitaiso_res <- RMeCab::RMeCabText(tmpf)))
  str_kansuji <- NULL
  str_nonkansuji <- NULL
  ans <- NULL
  
  if(length(keitaiso_res) == 1){
    if(stringr::str_detect(keitaiso_res[[1]][1], pattern = "[零〇一二三四五六七八九十百千万億兆京]")) return(kansuji2arabic_num(str))
    else return(str)
  }

  for(i in 1:length(keitaiso_res)){
    if(keitaiso_res[[i]][3] == "数" &&
       stringr::str_detect(keitaiso_res[[i]][1], pattern = "[零〇一二三四五六七八九]") &&
       stringr::str_length(keitaiso_res[[i]][1] > 1)) keitaiso_res[[i]][1] <- kansuji2arabic_num(keitaiso_res[[i]][1], ...)
    
    if(keitaiso_res[[i]][3] == "数" &&
       stringr::str_detect(keitaiso_res[[i]][1], pattern = "[0123456789]")) keitaiso_res[[i]][1] <- arabic2kansuji::arabic2kansuji(keitaiso_res[[i]][1])
    
    if(keitaiso_res[[i]][3] == "数" && 
       stringr::str_detect(keitaiso_res[[i]][1], pattern = "[^0123456789]") &&
       stringr::str_detect(keitaiso_res[[i]][1], pattern = "[^0123456789]")){
      str_kansuji[i] <- keitaiso_res[[i]][1]
      str_nonkansuji[i] <- ""
    }else{
      str_nonkansuji[i] <- keitaiso_res[[i]][1]
      str_kansuji[i] <- "ダ"
    }
  }
  
  str_kansuji <- stringr::str_c(str_kansuji, collapse = "")
  str_kansuji <- stringr::str_split(str_kansuji, pattern = "[^零〇一二三四五六七八九十百千万億兆京]")[[1]]
  str_kansuji[str_kansuji == ""] <- NA
  str_kansuji <- kansuji2arabic_num(na.omit(str_kansuji), consecutive, ...)
  
  j <- 1
  
  for(i in 1:length(str_nonkansuji)){
    if(!stringr::str_detect(str_nonkansuji[i], pattern = "") && i == 1){
      str_nonkansuji[i] <- str_kansuji[j]
      j <- j + 1
    }
    else if(consecutive == "non"){
      if((stringr::str_detect(str_nonkansuji[i - 1], pattern = "[^0123456789]")
          && stringr::str_detect(str_nonkansuji[i - 1], pattern = "[^零〇一二三四五六七八九]"))
         && !stringr::str_detect(str_nonkansuji[i], pattern = "")){
        str_nonkansuji[i] <- str_kansuji[j]
        j <- j + 1
      }
    }
    else if(stringr::str_detect(str_nonkansuji[i - 1], pattern = "[^0123456789]")
            && !stringr::str_detect(str_nonkansuji[i], pattern = "")){
      str_nonkansuji[i] <- str_kansuji[j]
      j <- j + 1
    }
    if((length(str_kansuji) + 1)  ==  j) break
  }
  ans <- stringr::str_c(na.omit(str_nonkansuji), collapse = "")
  return(ans)
  
}

使ってみる

例文「加藤一二三さんは七十七銀行銀行コード0125)に1234567890123円預けており、追加で5円預けた。」で複数の漢数字表記パターンがあるのですべてを試す。

具体的には

  • 加藤一二三さんは七十七銀行銀行コード〇一二五)に一兆二千三百四十五億六千七百八十九万百二十三円預けており、追加で五円預けた。」これを標準型と勝手に定義する。
  • 加藤一二三さんは七十七銀行銀行コード〇一二五)に一兆二三四五億六七八九万一二三円預けており、追加で五円預けた。」これを新聞等の縦書きで見られる形として新聞型と勝手に定義する。
  • 加藤一二三さんは七十七銀行銀行コード〇一二五)に1兆2345億6789万123円預けており、追加で5円預けた。」これを販売価格などでたまに見られる万などのみ漢数字でそれ意外がアラビア数字になっている形として、勝手に販売価格型とする。
  • 加藤一二三さんは七十七銀行銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。」これは銀行コードがなぜか全角アラビア数字になっているパターンで、半角アラビア数字と全角アラビア数字になっている形として勝手に混在型とする。
  • もう一つ、混在型として「加藤一二三さんは七十七銀行銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。」と金額の途中に(おそらく誤って)全角数字が混在しているパターンもあり、こちらは誤植混在型とする。

なお、実在する加藤一二三さんとは一切関係ない。七十七銀行も特に意図があるわけじゃなく、固有名詞として一二三が漢数字の0~9の並びの例で、七十七銀行が漢数字として十などが含まれる例として取り上げたかったからという以上でも以下でもない。

kansuji2arabic_str

基本的に形態素解析を伴わない漢数字からアラビア数字への変換はこの関数のみですべて事足りるようにしている。

まずは標準型から。

すべての漢数字を変換するパターン。

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二千三百四十五億六千七百八十九万百二十三円預けており、追加で五円預けた。")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

consecutiveで連続する0~9の漢数字(例:一二三)は固有名詞としてそのまま変換しないようにもできる。

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二千三百四十五億六千七百八十九万百二十三円預けており、追加で五円預けた。", consecutive = "non")
## [1] "加藤一二三さんは77銀行(銀行コード〇一二五)に1234567890123円預けており、追加で5円預けた。"

銀行コードも未変換になってしまうが、機械的判断なのでこれ以上は無理。これ以上やりたかったら形態素解析して品詞調べるしかない(後述)。あと、一文字だけの漢数字は避けがたい(例:金田一)。これを避けはじめるとそもそも変換って話になる。

次に新聞型

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二三四五億六七八九万一二三円預けており、追加で五円預けた。")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

同様に一二三を変換しないようにもできる。

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二三四五億六七八九万一二三円預けており、追加で五円預けた。", consecutive = "non")
## [1] "加藤一二三さんは77銀行(銀行コード〇一二五)に1234567890123円預けており、追加で5円預けた。"

次に販売価格型

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード〇一二五)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

混在型

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

処理の関係で全角数字は半角数字に処理されてしまう、これは次の誤植混在型を処理するため。これが嫌なら、全角数字を保持するように引数widthshalfwidthとする。

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。", widths = "halfwidth")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

誤植混在型

kansuji2arabic_str("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤123さんは77銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

となり、基本的にすべてのパターンで意図する変換ができる、と思っている。

kansuji2arabic_str_mecab

テキストを形態素解析するためにMeCabをRMeCabで呼び出して、品詞細分類を参照し、漢数字が数なら変換する。RMeCabで品詞細分類を参照すために一時ファイルを作っているので結構へんてこな処理している。

あと結果はMeCabで使っている辞書に依存するのでこの通りにはならないかもしれない。

すべての漢数字を変換するパターン。

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二千三百四十五億六千七百八十九万百二十三円預けており、追加で五円預けた。")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

一二三と七十七銀行が品詞細分類で固有名詞になるので変換されない。漢数字を含むものでも固有名詞として形態素解析でうまく処理されていれば変換されない。ただし、デフォルトの辞書では「加藤一二三九段」は「一二三九」が一文字ずつ分かたれ、数として処理されてしまうのでうまく行かないこともある。形態素解析といえども万能ではない。あとは辞書次第というところ。

consecutiveで連続する0~9の漢数字(例:〇一二五)は固有名詞としてそのまま変換しないようにもできる。

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二千三百四十五億六千七百八十九万百二十三円預けており、追加で五円預けた。", consecutive = "non")
## [1] "加藤一二三さんは七十七銀行(銀行コード〇一二五)に1234567890123円預けており、追加で5円預けた。"

次に新聞型

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード〇一二五)に一兆二三四五億六七八九万一二三円預けており、追加で五円預けた。")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

次に販売価格型

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード〇一二五)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

混在型

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

処理の関係で全角数字は半角数字に処理されてしまう、これは次の誤植混在型を処理するため。これが嫌なら、全角数字を保持するように引数widthshalfwidthとする。

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。", widths = "halfwidth")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

誤植混在型

kansuji2arabic_str_mecab("加藤一二三さんは七十七銀行(銀行コード0125)に1兆2345億6789万123円預けており、追加で5円預けた。")
## [1] "加藤一二三さんは七十七銀行(銀行コード0125)に1234567890123円預けており、追加で5円預けた。"

で一通り変換できる。

今後の課題

コードが必要な要素を一つずつクリアするために足したり引いたりしていてごちゃごちゃしていてきれいじゃないし、速度的にも微妙なのできれいにしたい。最終章その2くらいでの課題。

本件とは直接関係ないが、このコードで以前作った{arabic2kansuji}を使っていたらバグを見つけて、修正できたものは修正できたが修正できなかったものについてはちょっと考えなければと思っている。具体的には1億2345万などのアラビア数字と漢数字混じりのものがうまく変換できない。追記:←これは直した。しかし1億2345万を意図した12345万は一万二千三百四十五万になってしまう。

暫定的な解決策としては一回、今回のコードでアラビア数字漢数字混じりのものをアラビア数字に統一して、それを漢数字に変換するのが簡単そう。

雑感

RMeCabの挙動がよくわからなかったので、ソースコードを見たら魔法少女まどかマギカキュウべぇがいて、わけがわからないよとなった。

kansuji2arabic_str_mecabを使えばどこからか拾ってきた漢数字混じりだったり半角全角数字混じりの整形されていない住所録を辞書で判別できる範囲で正しく住所の丁目以降の数字を漢数字から半角数字に変換できるようになる、と思う。