:::

整合PostgreSQL資料庫的R中文文本探勘 / Chinese Text Mining with R and PostgreSQL

image

R的文本探勘(text mining)大多是基於純文字檔案進行,而我將文本探勘處理的資料輸入、輸出儲存整合到PostgreSQL資料庫,讓R的文本探勘能夠更容易跟其他系統整合。這篇文本探勘中進行了HTML內文擷取、新詞加入與斷詞處理、符號過濾、英數字過濾、停用字過濾、最小詞彙長度與頻率過濾等處理步驟。以下介紹系統架構跟R Script的設定,並以我的網頁為資料來源示範如何進行文本探勘。


基本的系統架構 / Basic System Architecture

大多數的文本探勘教學,如「用R進行中文 text Mining」,都是使用檔案系統儲存純文字檔案,再利用DirSource()來讀取資料夾底下的純文字檔案,以此進行文本探勘。但檔案系統並不是管理資料的理想做法,而且這樣子的模式也很難跟其他的系統整合。因此我參考大家用R進行文本探勘的做法,將整個架構與PostgreSQL資料庫整合。

image

上圖是這個系統的主要架構。「使用者」是指能夠操作R跟管理資料庫的人,「R環境」可以參考我架設的RStudio Server,「資料庫」則是使用PostgreSQL資料庫。整個系統的運作流程大致上如下:

  1. 使用者在資料庫中新增要分析的文本。
  2. 使用者設定R Script。
  3. 使用者執行R Script。
  4. R Script根據設定的內容進行以下動作:
    1. 從資料庫載入要分析的文本資料、斷詞用的新詞、停用字
    2. 進行文本探勘處理
    3. 將處理結果儲存到資料庫
  5. 使用者到資料庫查看處理結果
整合式的系統架構 / Integrated System Architecture

為了方便說明,本文尚未跟其他系統進行整合,乍看之下並沒有其他系統的角色在裡面。如果要其他系統整合的話,系統架構圖中的使用者就是換成其他角色:

image

上圖是以PHP為例的架構。PHP可以用exec()指令來執行R Script。詳細做法可以參考Integrating PHP and R


環境配置 / Environment Setup

在這個架構中,需要特別說明的是R環境跟資料庫的配置。

R環境配置 / R Setup

2016-11-07_131115

因為本文的架構是由使用者來驅動整個流程,因此R環境就需要有一個讓使用者方便運作的介面。在此使用的是RStudio Server,架設方式請看我另一篇「開箱即用的R運作環境!RStudio Server OpenVZ虛擬機器分享」的說明。

當然,你也可以使用自己的R環境。但是因為文本探勘會需要tm、tmcn、Rwordseg、XML、以及連結資料庫的RPostgreSQL等套件,請務必確認自己使用的R環境是否已經安裝了這些套件。在R安裝套件可能會遭遇很多困難,有必要的話請參考我另外一篇「R套件怎麼裝不起來?Ubuntu中舊版R安裝套件的方法」的說明。

PostgreSQL資料庫配置 / PostgreSQL Database Setup

2016-11-07_160431 - Copy

本系統所搭配的PostgreSQL資料庫名為「text_mining」。資料庫中至少需要4種不同的資料表。建立資料庫的SQL檔案可以由此下載:

建立資料庫「text_mining」之後,可以使用psql工具匯入上面的SQL檔案。做法可以參考「PostgreSQL的備份與復原」這篇的說明。


資料庫架構說明 / Schema Description

「text_mining」資料庫裡面有4張表,以下說明定義:

資料表「doc」 / Table: doc

儲存需要探勘的文本。

  • doc_id: 主鍵,也代表文件的代號
  • fulltext: 要探勘的文本,純文字格式。可以儲存HTML網頁或XML格式。
資料表「new_word」 / Table: new_word

自訂斷詞使用的新詞。

  • new_word_id: 主鍵
  • term: 新詞
資料表「stop_word」 / Table: stop_word

自訂停用字,這些字會被過濾掉,不會出現在探勘結果中。

  • stop_word_id: 主鍵
  • term: 停用字
資料表「term_freq」 / Table: term_freq

探勘結果。

  • term_freq_id: 主鍵
  • doc_id: 文件代號,文本的外鍵,對應到表格「doc」的主鍵
  • term: 探勘出來的詞彙
  • speech: 詞彙對應的詞性
  • freq: 在這份文件中的頻率
視表「view_term_freq_sum」 / View: view_term_freq_sum

除了四張表格之外,我還額外整理了一張視表(view),方便輸出到文字雲之中。

  • word: 詞彙
  • freq: 出現頻率

 

正式使用的時候,我們可以擴充資料表「doc」,讓它有更多其他可以用來過濾的欄位。例如我們可以加上「title」、「author」等欄位,這樣就可以更容易比較不同類型的文本探勘結果。

跟其他系統實際整合的時候,我們可以用其他資料表來取代資料表「doc」。舉例來說,我可能想要探勘的文本是儲存在討論區資料表中,那麼我只要在下面R Script設定中調整查詢文本的SQL語法即可。

資料庫的管理 / How to Manage Database?

2016-11-07_165637

使用者要管理PostgreSQL資料庫的方法很多,最基本的就是使用PostgreSQL內建的pgAdmin。Windows底下安裝PostgreSQL的時候就會一併安裝pgAdmin。pgAdmin建立資料庫與資料表、查詢資料很容易,但是卻不容易修改資料,所以我還會搭配phpPgAdmin使用。

2016-11-07_165936

phpPgAdmin是類似phpMyAdmin的網頁版PostgreSQL資料庫管理工具。架設起來之後,只要有瀏覽器就能夠直接管理資料庫,而且也比較不會像是pgAdmin管理資料庫的時候常常遭遇PostgreSQL權限設定的問題。phpPgAdmin在Linux裡面可以輕易用指令來安裝。以Ubuntu為例,安裝phpPgAdmin的apt-get指令如下:

sudo apt-get install phppgadmin -y

安裝之後就可以用網址加上phppgadmin來開啟管理介面:

當然,在這之前要先架設PostgreSQL資料庫的伺服器才行。

 

關於PostgreSQL跟關聯式資料庫的內容相當複雜,許多科系會以「關聯式資料庫管理」為課名開設一整個學期的課程,表示這門學問實在不是三言兩語就能夠講得清楚。如果想要學習PostgreSQL的話,可以參考PostgreSQL 8.0.0 中文文件來學習。


文本探勘R腳本的設定與執行 / Text Mining R Script

接下來是關鍵的R Script內容。基於程式寫作習慣,我偏好將設定跟執行部分拆開成兩個部分。我們主要要修改的內容在設定部分,執行部分都直接操作即可。這些腳本都可以直接輸入到R Console中執行。

設定部分的R腳本 / Configuration R Script

文本探勘有很多細節可供調整,像是是否要使用停用字、新詞,是否要從HTML文本當中抽取指定範圍的文本(通常是只找<body></body>之間的內容),是否要清除標點符號、數字、或是英文,要找出來的詞頻最短長度以及最少頻率是多少。許多參數的設定都在這個R Script中調整。參數的說明請看# 開頭的R註解。

雖然設定大部分都用預設值即可,但是資料庫連線設定請務必修改

文本探勘設定部分的R腳本可以在此下載:

# 文本查詢,至少要查詢doc_id跟fulltext兩個欄位
sql.content <- "SELECT doc_id, fulltext FROM doc"

# 新詞查詢
sql.newwords <- "SELECT term FROM new_word"

# 停用字查詢
sql.stopwords <- "SELECT term FROM stop_word"

# 過濾器設定
library("XML") # 先載入XML處理工具
filter.xpath.enable <- TRUE # 是否使用xpath過濾,可以取出XML/HTML指定的範圍
filter.xpath <- "/html/body" # xpath
filter.xpath.type <- xmlValue # xpath類型

filter.removePunctuation <- TRUE # 是否清除標點符號
filter.removeNumbers <- TRUE # 是否清除數字
filter.removeEnglish <- FALSE # 是否清除英文

filter.speech.enable <- TRUE # 是否使用詞性過濾
filter.speech <- c("n"); # 詞性設定,n表示名詞

filter.term.min.length <- 2 # 最小詞彙長度
filter.term.min.freq <- 3 # 詞彙最少頻率

# 資料庫設定
db.host <- "192.168.56.152" # 資料庫主機位置
db.port <- 5432 # 資料庫連接埠
db.user <- "postgres" # 資料庫登入帳號
db.password <- "password" # 資料庫登入密碼
db.name <- "text_mining" # 資料庫名稱

# 輸出結果詞頻資料表設定
db.term_freq.table_name <- "term_freq" # 詞頻表格
db.term_freq.field_name.doc_id <- "doc_id" # 文件編號
db.term_freq.field_name.term <- "term" # 探勘出來的詞彙
db.term_freq.field_name.speech <- "speech" # 詞性
db.term_freq.field_name.freq <- "freq" # 頻率
執行部分的R腳本 / Excuting R Script

以下R腳本通常不需要修改,列出來僅供學習R之用。文本探勘執行部分的R腳本可以在此下載:

# 引用函式
library("RPostgreSQL") # PostgreSQL資料庫連線需要的套件
library("tm") # 文本探勘工具
library("tmcn") # 文本探勘中文包
library("Rwordseg") # 中文斷詞工具
library("XML") # XML處理工具

# 資料庫連接
drv <- dbDriver("PostgreSQL")
con <- dbConnect(drv, dbname = db.name,
                 host = db.host, port = db.port,
                 user = db.user, password = db.password)

# 資料庫查詢
db.content <- dbGetQuery(con, sql.content)

# 如果有文本資料的話
if (length(colnames(db.content)) > 0) {

# 查詢其他資料
db.newwords <-dbGetQuery(con, sql.newwords)
db.stopwords <- dbGetQuery(con, sql.stopwords)

# 新詞加入斷詞器
if (length(colnames(db.newwords)) > 0) {
    insertWords(db.newwords[,1])
}

# 儲存詞性
df.speech <- list()

# HTML的body抽取處理
if (filter.xpath.enable == TRUE) {
    tryCatch({
        html <- htmlParse(db.content[,2], encoding="utf8")
        db.content[,2] <- xpathSApply(html, filter.xpath, filter.xpath.type)
    })
}

# 要輸入的文本
final_df <- as.data.frame(t(db.content[,2]))
colnames(final_df)<-db.content[,1]

# 建立文本物件
R_corpus <- Corpus(VectorSource(final_df), list(language = NA))

# 清除標點符號
if (filter.removePunctuation == TRUE) {
    R_corpus <- tm_map(R_corpus, removePunctuation)
}

# 清除數字
if (filter.removeNumbers == TRUE) {
    R_corpus <- tm_map(R_corpus, removeNumbers)
}

# 清除大小寫英文與數字
if (filter.removeEnglish == TRUE) {
    R_corpus <- tm_map(R_corpus, function(word) {
        gsub("[A-Za-z]", "", word)
    })
}

# 斷詞
R_corpus <- tm_map(R_corpus, segmentCN, nature = TRUE)

# 詞性過濾前的調整
if (filter.removeEnglish == FALSE) {
    # 如果不過濾英文,那就關閉詞性過濾的設定
    filter.speech.enable <- FALSE
}

# 詞性過濾
R_corpus <- tm_map(R_corpus, function(word) {
   
    if (filter.speech.enable == TRUE) {
        filter.words <- word[(match(names(word), filter.speech, nomatch = -1) > 0)]
        df.speech[filter.words] <<- names(filter.words)
        filter.words
    } else {
        df.speech[word] <<- names(word)
        word
    }
})

# 停用字設定
if (length(colnames(db.stopwords)) > 0) {
    my.stopwords <- c(stopwordsCN(), stopwords("english"), db.stopwords[,1])
} else {
    my.stopwords <- c(stopwordsCN(), stopwords("english"))
}

# 製作詞彙陣列
R_corpus <- Corpus(VectorSource(R_corpus))
tdm <- FALSE
tdm <-  TermDocumentMatrix(R_corpus, control = list(wordLengths = c(filter.term.min.length, Inf), stopwords = my.stopwords))

if (is.object(tdm) == TRUE) {

    m1 <- as.matrix(tdm) # 詞彙分佈儲存在m1裡面

    # 儲存結果之前先清除該文本的詞頻資料
    where_sql <- ""
    for (doc_id in colnames(m1)) {
        if (where_sql != "") {
            where_sql <- paste0(where_sql, ' OR')
        }
        where_sql <- paste0(where_sql,  " ",db.term_freq.field_name.doc_id," = '",doc_id, "'")
    }
    if (where_sql != "") {
        sql.term_freq.delete.docs = paste0("DELETE FROM ",db.term_freq.table_name," WHERE ", where_sql)
        dbSendQuery(con, sql.term_freq.delete.docs)
    }

    # 儲存詞頻結果,寫入資料庫
    for (doc_id in colnames(m1)) {
        for (term in rownames(m1)) {
            freq = m1[term,doc_id]
            if (freq > 0 && freq > filter.term.min.freq) {
                sql.term_freq.insert = paste0("INSERT INTO term_freq (",db.term_freq.field_name.doc_id,",",db.term_freq.field_name.term,",",db.term_freq.field_name.speech,",",db.term_freq.field_name.freq,") VALUES ('", doc_id, "', '",term,"', '",df.speech[term],"', ",freq,")")
                dbSendQuery(con, sql.term_freq.insert)
            }
        }
    }
} # if (is.object(tdm) == TRUE) {

} # if (length(colnames(db.content)) > 0) {

文本探勘展示 / Text Mining Demo

配置完成之後,接下來我們就試著做一次文本探勘的流程。以下我以本部落格中的「開箱即用的R運作環境!RStudio Server OpenVZ虛擬機器分享」和「R的文字雲怎麼都是□亂碼?wordcloud套件需要中文字形」兩篇文章的原始碼作為文本探勘的資料來源,展示整個流程如何運作。

1.  取得文本 / Get Coupus

我們的目標是抓取以下兩個網頁的原始碼:

image

開啟網頁之後,按滑鼠右鍵,進入「檢視網頁原始碼」。

2016-11-07_172419

選取並複製全文,這樣我們就可以取得這個網頁的原始碼。

2. 插入到資料表doc / Insert into "doc" table

image

接下來我們到doc資料表中插入新資料,將網頁原始碼貼到「fulltext」欄位中。第一個doc_id欄位是由資料庫自動填入的主鍵,我們自己不需要輸入資料。

image

就這樣子,成功新增兩筆資料。

 

我將新增這兩筆資料的SQL語法另外存成以下檔案,你可以直接匯入SQL語法來新增資料:

3. R環境中進行文本探勘 / Text Mining with R

2016-11-07_174906

再來到R環境中,執行R Script。順利的話,就能不出現任何錯誤訊息地運作結束。

4. 查詢文本探勘結果 / Check Text Mining Result

image

探勘結果會儲存在資料表「term_freq」中,你可以檢查各文件所使用的詞彙以及出現的頻率。

2016-11-07_175310

如果要看全部詞彙的頻率,可以查看視表「view_term_freq_sum」。這裡面出現最多的是「布丁」,次數是70次,其次是英文字「name」跟「false」。

5. 調整文本探勘的設定 / Change Configuration

上面探勘的結果中,可以發現有大量英文字混雜其中。如果想要移除英文字,可以將R Script設定部分的filter.removeEnglish設為TRUE (注意,大小寫有區分):

filter.removeEnglish <- TRUE # 是否清除英文

另一方面,有些詞彙看起來意義不明,例如「name」或「什麼」。如果想要將這個詞彙排除在探勘結果之外,那麼我們可以把這些詞彙加入資料表「stop_word」之中:

2016-11-07_175833

這樣子這些詞彙就會在下次文本探勘的時候自動被過濾掉了。


結語 / Conclusion

有人可能會問說,怎麼這個文本探勘最後沒有畫文字雲(word cloud)

這是因為詞頻的計算就已經算是完成我的任務了,所以這篇就只做到儲存詞頻結果。光是做到這樣,我就能用SQL來查詢出現頻率最高的詞彙,也可以比較不同類型文本之間使用詞彙的差異。比起畫成文字雲,做成簡單的表格來比較還更為實用。

接下來可以用這個詞頻結果來做很多事情,除了可以畫文字雲之外,也可以根據用詞的不同來做文本分群。但是這些工作是可以分開介紹的,只要資料存在資料庫裡面,要做甚麼分享都不難。

不過,如果要做全文檢索的話,可以進一步改用PostgreSQL內建的全文檢索功能來處理,詳細可以看我之前所撰寫的「以PHP與PostgreSQL實作簡易中文全文檢索功能—概念說明篇」。

 

另外也可能會有人問到,為什麼一開始文本的蒐集要靠手動輸入,而不是自動上網抓取?感覺手動步驟太多,好像不太厲害。

這是因為我認為文本探勘的文本來源取得方式各異,比較常見的情況都應該是拆開來處理,不一定是要用R取得文本。舉例來說,我可能是用DSpace架設的數位典藏作為文本來源,也可能是分析PHP架設的留言板內容,我甚至可以用WinHTTrack這個圖形化工具就可以下載整個網站,實在是不用堅持一定要用R來做網路爬蟲。

甚至我認為比起R來說,關聯式資料庫的管理跟資料處理應該是更多人熟悉的領域。所以R就做好你的文本探勘資料處理,資料的儲存就交給關聯式資料庫吧。

PostgreSQL不適合進行文本探勘 / Don’t Do Text Mining in PostgreSQL

最後來講這篇準備過程中的學到一個教訓:不要在資料庫中做文本探勘。

這篇R Script文本探勘處理手續中有幾個過濾器:

  • filter.xpath:從XML或從HTML中抽取部分文本的功能
  • filter.removePunctuation:清除標點符號
  • filter.removeNumbers:清除數字
  • filter.removeEnglish:清除英文

後三個功能可以簡單地用正規表達式搭配PostgreSQL的regexp_replace()功能來處理。但是第一個功能卻意外地很困難。PostgreSQL有XML功能,但這只能處理正規的XML文件,意外地其實不能處理大部分的HTTP網頁。雖然我們會以為HTML網頁也是使用標籤,可能可以跟HTML相容,但實際上兩者之間還是有不小的差距。

如果在PostgreSQL裡面將普通的HTML當做XML處理,可能遭遇到的問題包括:

  • 不能處理文件類型宣告:就是網頁原始碼第一行
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  • 不能處理<html>標籤中的xmlns名稱空間,有這項屬性就會出錯
  • 不能處理標籤屬性中使用單引號「'」
  • 不能處理HTML的註解格式 <!-- -->
  • 不能處理HTML特殊字元,例如空白「&nbsp;」

上述需要處理的情況實在是太多了,一開始我試著寫一個超級複雜的正規表達式,希望用來刪除上面這些可能造成網頁轉換XML失敗的危險因子,但是一來正規表達式相當複雜、需要考量的問題太多,套用在各種複雜的網頁上可能會有很多出乎意料之外的結果,二來最關鍵的一點就是,在PostgreSQL上跑正規表達式實在是太慢了。

因為研究正規表達式一直不是很順利,我就試著換到R環境之中,使用XML套件的htmlParse()功能來處理網頁,沒想到處理速度快得天差地遠,完全打消我在PostgreSQL上做文本探勘的念頭。

就這樣的,我最後還是選擇在R Script進行文本過濾。而且我把過濾器的設定額外拆開寫在設定裡面,這樣子就可以依照需求動態啟動或取消各項過濾了。

最後想想,各種工具各有所長,PostgreSQL還是乖乖儲存資料就好,如果要進行文本探勘的話,還是用像R這種專門的工具吧。(剛好跟前面的感想相反XD)

總共4 則留言 ( 我要發問 , 隱藏留言 顯示留言 )

  1. 您好:
    拜讀執行後,會產生錯誤訊息如下,煩請撥冗賜教,謝謝!
    Error in FUN(X[[i]], ...) : Please input character!
    Show Traceback 呈現:
    6.
    stop("Please input character!")
    5.
    FUN(X[[i]], ...)
    4.
    lapply(X, FUN, ...)
    3.
    mclapply(content(x), FUN, ...)
    2.
    tm_map.VCorpus(R_corpus, segmentCN, nature = TRUE)
    1.
    tm_map(R_corpus, segmentCN, nature = TRUE)

    回覆刪除
    回覆
    1. To St W,

      這是因為新版R的問題。新版的R連帶影響到新版的tm跟segmentCN套件,全部都不能用這篇文章的R Script。
      可以看我在另一篇最後的討論:
      http://blog.pulipuli.info/2016/11/r-draw-word-cloud-in-r.html#postcatar-draw-word-cloud-in-r.html0_anchor5

      這篇文章的R是用3.0.2版,請試著去找這一版的R來安裝,然後再努力克服套件安裝的問題。
      這裡下載Windows的R 3.0.2版,https://mirrors.tuna.tsinghua.edu.cn/CRAN/bin/windows/base/old/3.0.2/
      但是Windows的R會有中文亂碼的問題,無法完全克服,我建議改用Linux
      舊版套件安裝的問題請看這一篇:http://blog.pulipuli.info/2016/11/rubuntur-how-to-install-archived.html

      你也可以架設虛擬機器RStudio Server來做這件事情,我保證絕對可以正常運作
      http://blog.pulipuli.info/2016/11/rrstudio-server-openvz-standalone-r.html

      刪除
    2. 好的,
      我再試看看,
      感謝您熱心回覆!

      刪除
    3. To St W,

      不客氣,祝你能夠順利進行文本探勘!

      刪除