使用狀態機解析線

讓我們使用狀態機模式來解析具有特定模式的行,使用來自 R 的 S4 類特徵。

問題啟動

我們需要使用分隔符(;)解析每行提供有關人員資訊的檔案,但提供的某些資訊是可選的,而不是提供空欄位,而是缺少。在每一行我們可以得到以下資訊:Name;[Address;]Phone。如果地址資訊是可選的,有時我們會有,有時也不會,例如:

GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553

第二行不提供地址資訊。因此,分隔符的數量可以是不同的,就像在這種情況下具有一個分隔符並且對於其他行的兩個分隔符。由於分隔符的數量可能不同,因此解決此問題的一種方法是基於其模式識別給定欄位的存在與否。在這種情況下,我們可以使用正規表示式來識別這種模式。例如:

  • 名稱^([A-Z]'?\\s+)* *[A-Z]+(\\s+[A-Z]{1,2}\\.?,? +)*[A-Z]+((-|\\s+)[A-Z]+)*$。例如:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
  • 地址^\\s[0-9]{1,4}(\\s+[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,2}|[A-Z\\s0-9]+)$。例如:11020 LE JEUNE ROAD87 SW 27TH。為簡單起見,我們這裡不包括郵政編碼,城市,州,但我可以包含在此欄位中或新增其他欄位。
  • 電話^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$。例如:305-123-4567, 305 123 4567, +1-786-123-4567

備註

  • 我正在考慮美國地址和手機最常見的模式,可以很容易地擴充套件到考慮更一般的情況。
  • 在 R 中,符號 \ 對於字元變數具有特殊含義,因此我們需要將其轉義。
  • 為了簡化定義正規表示式的過程,一個好的建議是使用以下網頁: regex101.com ,這樣你就可以使用給定的示例來玩它,直到獲得所有可能組合的預期結果。

該想法是基於先前定義的模式識別每個線欄位。狀態模式定義以下實體(類),它們協作以控制特定行為(狀態模式是行為模式):

GOF 狀態模式

讓我們根據問題的背景描述每個元素:

  • Context:儲存解析過程的上下文資訊,即當前狀態並處理整個 State Machine Process。對於每個狀態,執行一個動作(handle()),但是上下文根據狀態將其委託給為特定狀態定義的動作方法(來自 State 類的 handle())。它定義了客戶感興趣的介面。我們的 Context 類可以這樣定義:
    • 屬性:state
    • 方法:handle(),…
  • State:表示狀態機狀態的抽象類。它定義了一個用於封裝與上下文的特定狀態相關聯的行為的介面。它可以這樣定義:
    • 屬性:name, pattern
    • 方法:doAction()isState(使用 pattern 屬性驗證輸入引數是否屬於此狀態模式),…
  • Concrete States(state 子類):類 State 的每個子類,它實現與 Context 狀態相關的行為。我們的子類是:InitStateNameStateAddressStatePhoneState。這些類只是使用這些狀態的特定邏輯來實現泛型方法。不需要其他屬性。

注意: 如何命名執行動作的方法 handle()doAction()goNext() 是一個優先事項。方法名 doAction() 對於兩個類(Stateor Context)都可以是相同的,我們更喜歡在 Context 類中命名為 handle(),以避免在定義具有相同輸入引數但具有不同類的兩個泛型方法時出現混淆。

人類

使用 S4 語法,我們可以像這樣定義一個 Person 類:

setClass(Class = "Person",
    slots = c(name = "character", address = "character", phone = "character")
)

初始化類屬性是一個很好的建議。setClass 文件建議使用標記為 initialize 的通用方法,而不是使用棄用的屬性,例如:prototype, representation

setMethod("initialize", "Person",
  definition = function(.Object, name = NA_character_,
    address = NA_character_, phone = NA_character_) {
        .Object@name <- name
        .Object@address <- address
        .Object@phone <- phone
        .Object
    }
)

因為 initialize 方法已經是包 methods 的標準泛型方法,所以我們需要尊重原始的引數定義。我們可以驗證它在 R 提示符上輸入:

> initialize

它返回整個函式定義,你可以在頂部看到函式的定義如下:

function (.Object, ...) {...}

因此,當我們使用 setMethod 時,我們需要遵循 exaclty 相同的語法(.Object)。

另一個現有的通用方法是 show,它等同於 Java 的 toString() 方法,對於類域有一個特定的實現是個好主意:

setMethod("show", signature = "Person",
  definition = function(object) {
      info <- sprintf("%s@[name='%s', address='%s', phone='%s']", 
        class(object), object@name, object@address, object@phone)
      cat(info)
      invisible(NULL)
  }
)

注意 :我們使用與預設 toString() Java 實現中相同的約定。

假設我們想要將解析後的資訊(Person 物件列表)儲存到資料集中,然後我們應該能夠首先將物件列表轉換為 R 可以轉換的內容(例如將物件強制轉換為列表)。我們可以定義以下附加方法(有關此內容的詳細資訊,請參閱帖子

setGeneric(name = "as.list", signature = c('x'),
    def = function(x) standardGeneric("as.list"))

# Suggestion taken from here:
# http://stackoverflow.com/questions/30386009/how-to-extend-as-list-in-a-canonical-way-to-s4-objects
setMethod("as.list", signature = "Person",
    definition = function(x) {
        mapply(function(y) {
        #apply as.list if the slot is again an user-defined object
        #therefore, as.list gets applied recursively
        if (inherits(slot(x,y),"Person")) {
          as.list(slot(x,y))
        } else {
          #otherwise just return the slot
          slot(x,y)
        }
      },
        slotNames(class(x)),
        SIMPLIFY=FALSE)
    }
)

R 不為 OO 提供糖語法,因為該語言最初被設想為統計學家提供有價值的功能。因此,每個使用者方法需要兩個部分:1)定義部分(通過 setGeneric)和 2)實現部分(通過 setMethod)。就像上面的例子一樣。

國家級

遵循 S4 語法,讓我們定義抽象的 State 類。

setClass(Class = "State", slots = c(name = "character", pattern = "character"))

setMethod("initialize", "State",
  definition = function(.Object, name = NA_character_, pattern = NA_character_) {
      .Object@name <- name
      .Object@pattern <- pattern
      .Object
  }
)

setMethod("show", signature = "State",
  definition = function(object) {
      info <- sprintf("%s@[name='%s', pattern='%s']", class(object), 
          object@name, object@pattern)
      cat(info)
      invisible(NULL)
  }
)

setGeneric(name = "isState", signature = c('obj', 'input'),
    def = function(obj, input) standardGeneric("isState"))

setGeneric(name = "doAction", signature = c('obj', 'input', 'context'),
    def = function(obj, input, context) standardGeneric("doAction"))

State 的每個子類都會關聯一個 namepattern,但也是一種識別給定輸入是否屬於這種狀態的方法(isState() 方法),並且還實現了該狀態的相應動作(doAction() 方法)。

為了理解這個過程,讓我們根據收到的輸入定義每個狀態的轉換矩陣:

輸入/當前狀態 init 名稱 地址 電話
名稱 名稱
地址 地址
電話 電話 電話
結束 結束

注意: 單元 [row, col]=[i,j] 表示當前狀態 j 的目標狀態,當它收到輸入 i 時。

這意味著在狀態名下它可以接收兩個輸入:地址或電話號碼。表示事務表的另一種方法是使用以下 UML 狀態機圖:

狀態機圖表示

讓我們將每個特定的狀態實現為類 State 的子狀態

州級子類

初始狀態

初始狀態將通過以下類實現:

setClass("InitState", contains = "State")

setMethod("initialize", "InitState",
  definition = function(.Object, name = "init", pattern = NA_character_) {
      .Object@name <- name
      .Object@pattern <- pattern
      .Object
  }
)

setMethod("show", signature = "InitState",
  definition = function(object) {
      callNextMethod()
  }
)

在 R 中表示一個類是其他類的子類,它使用屬性 contains 並指示父類的類名。

因為子類只是實現了泛型方法,而沒有新增額外的屬性,所以 show 方法只需從上層呼叫等效方法(通過方法:callNextMethod()

初始狀態沒有關聯的模式,它只是表示程序的開始,然後我們用 NA 值初始化類。

現在讓我們來實現 State 類的泛型方法:

setMethod(f = "isState", signature = "InitState",
  definition = function(obj, input) {
      nameState <- new("NameState")
      result <- isState(nameState, input)
      return(result)
  }
)

對於這個特定的狀態(沒有 pattern),它只是初始化期望第一個欄位的解析過程的想法將是一個 name,否則它將是一個錯誤。

setMethod(f = "doAction", signature = "InitState",
    definition = function(obj, input, context) {
        nameState <- new("NameState")
        if (isState(nameState, input)) {
            person <- context@person
            person@name <- trimws(input)
            context@person <- person
            context@state <- nameState
        } else {
            msg <- sprintf("The input argument: '%s' cannot be identified", input)
            stop(msg)
        }
        return(context)
    }
)

doAction 方法提供轉換並使用提取的資訊更新上下文。在這裡,我們通過 @-operator 訪問上下文資訊。相反,我們可以定義 get/set 方法來封裝這個過程(因為它在 OO 最佳實踐中強制要求:封裝),但是這將為每個 get-set 增加四個方法而不為此示例新增值。

在所有 doAction 實現中,如果未正確識別輸入引數,則新增安全措施是一個很好的建議。

姓名國家

以下是此類定義的定義:

setClass ("NameState", contains = "State")

setMethod("initialize","NameState",
  definition=function(.Object, name="name",
        pattern = "^([A-Z]'?\\s+)* *[A-Z]+(\\s+[A-Z]{1,2}\\.?,? +)*[A-Z]+((-|\\s+)[A-Z]+)*$") {
        .Object@pattern <- pattern
        .Object@name <- name
        .Object       
  }
)

setMethod("show", signature = "NameState",
  definition = function(object) {
      callNextMethod()
  }
)

我們使用函式 grepl 來驗證輸入屬於給定模式。

setMethod(f="isState", signature="NameState",
  definition=function(obj, input) {
      result <- grepl(obj@pattern, input, perl=TRUE)
      return(result)
  }
)

現在我們定義要為給定狀態執行的操作:

setMethod(f = "doAction", signature = "NameState",
  definition=function(obj, input, context) {
      addressState <- new("AddressState")
      phoneState <- new("PhoneState")
      person <- context@person
      if (isState(addressState, input)) {
          person@address <- trimws(input)
          context@person <- person
          context@state <- addressState
      } else if (isState(phoneState, input)) {
          person@phone <- trimws(input)
          context@person <- person
          context@state <- phoneState
      } else {
          msg <- sprintf("The input argument: '%s' cannot be identified", input)
          stop(msg)
      }
      return(context)
  }
)

這裡我們考慮可能的轉換:一個用於地址狀態,另一個用於電話狀態。在所有情況下,我們更新上下文資訊:

  • person 資訊:addressphone 帶有輸入引數。
  • 該過程的 state

識別狀態的方法是呼叫方法:isState() 用於特定狀態。我們建立一個預設的特定狀態(addressState, phoneState),然後要求進行特定的驗證。

其他子類(每個狀態一個)實現的邏輯非常相似。

地址狀態

setClass("AddressState", contains = "State")

setMethod("initialize", "AddressState",
  definition = function(.Object, name="address",
    pattern = "^\\s[0-9]{1,4}(\\s+[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,2}|[A-Z\\s0-9]+)$") {
        .Object@pattern <- pattern
        .Object@name <- name
        .Object
    }
)

setMethod("show", signature = "AddressState",
  definition = function(object) {
      callNextMethod()
  }
)

setMethod(f="isState", signature="AddressState",
    definition=function(obj, input) {
        result <- grepl(obj@pattern, input, perl=TRUE)
        return(result)
    }
)
    
setMethod(f = "doAction", "AddressState",
    definition=function(obj, input, context) {
        phoneState <- new("PhoneState")
        if (isState(phoneState, input)) {
            person <- context@person
            person@phone <- trimws(input)
            context@person <- person
            context@state <- phoneState
        } else {
            msg <- sprintf("The input argument: '%s' cannot be identified", input)
            stop(msg)
        }
        return(context)
    }
)

電話州

setClass("PhoneState", contains = "State")

setMethod("initialize", "PhoneState",
  definition = function(.Object, name = "phone",
    pattern = "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$") {
        .Object@pattern <- pattern
        .Object@name <- name
        .Object
    }
)

setMethod("show", signature = "PhoneState",
  definition = function(object) {
      callNextMethod()
  }
)

setMethod(f = "isState", signature = "PhoneState",
    definition = function(obj, input) {
        result <- grepl(obj@pattern, input, perl = TRUE)
        return(result)
    }
)

這裡我們將人員資訊新增到 contextpersons 列表中。

setMethod(f = "doAction", "PhoneState",
    definition = function(obj, input, context) {
        context <- addPerson(context, context@person)
        context@state <- new("InitState")
        return(context)
    }   
)

上下文類

現在讓我們解釋一下 Context 類的實現。我們可以考慮以下屬性來定義它:

setClass(Class = "Context",
     slots = c(state = "State", persons = "list", person = "Person")
)

哪裡

  • state:流程的當前狀態
  • person:當前人,它代表我們已從當前行解析的資訊。
  • persons:已處理的已解析人員列表。

注意 :可選地,我們可以新增 name 以通過名稱標識上下文,以防我們使用多種解析器型別。

setMethod(f="initialize", signature="Context",
  definition = function(.Object) {
        .Object@state <- new("InitState")
        .Object@persons <- list()
        .Object@person <- new("Person")
        return(.Object)
    }
)

setMethod("show", signature = "Context",
  definition = function(object) {
      cat("An object of class ", class(object), "\n", sep = "")
      info <- sprintf("[state='%s', persons='%s', person='%s']", object@state,
          toString(object@persons), object@person)
      cat(info)
      invisible(NULL)
  }
)

setGeneric(name = "handle", signature = c('obj', 'input', 'context'),
    def = function(obj, input, context) standardGeneric("handle"))

setGeneric(name = "addPerson", signature = c('obj', 'person'),
    def = function(obj, person) standardGeneric("addPerson"))

setGeneric(name = "parseLine", signature = c('obj', 's'),
    def = function(obj, s) standardGeneric("parseLine"))

setGeneric(name = "parseLines", signature = c('obj', 's'),
    def = function(obj, s) standardGeneric("parseLines"))

setGeneric(name = "as.df", signature = c('obj'),
    def = function(obj) standardGeneric("as.df"))

使用這些通用方法,我們可以控制解析過程的整個行為:

  • handle():將呼叫當前 state 的特定 doAction() 方法。
  • addPerson:一旦我們到達最終狀態,我們需要將 person 新增到我們已解析的 persons 列表中。
  • parseLine():解析一條線
  • parseLines():解析多行(一行陣列)
  • as.df():將 persons 列表中的資訊提取到資料框物件中。

讓我們繼續使用相應的實現:

handle() 方法,從 context 的當前 state 代表 doAction() 方法:

setMethod(f = "handle", signature = "Context",
    definition = function(obj, input) {
        obj <- doAction(obj@state, input, obj)
        return(obj)
    }
)

setMethod(f = "addPerson", signature = "Context",
  definition = function(obj, person) {
      obj@persons <- c(obj@persons, person)
      return(obj)
  }
)

首先,我們使用分隔符將原始行拆分為陣列,以通過 R 函式 strsplit() 識別每個元素,然後針對給定狀態迭代每個元素作為輸入值。handle() 方法再次返回 context,其中包含更新的資訊(statepersonpersons 屬性)。

setMethod(f = "parseLine", signature = "Context",
  definition = function(obj, s) {
      elements <- strsplit(s, ";")[[1]]
      # Adding an empty field for considering the end state.
      elements <- c(elements, "")
      n <- length(elements)
      input <- NULL
      for (i in (1:n)) {
        input <- elements[i]  
        obj <- handle(obj, input)
      }
      return(obj@person)
  }
)

因為 Becuase R 複製了輸入引數,我們需要返回上下文(obj):

setMethod(f = "parseLines", signature = "Context",
  definition = function(obj, s) {
      n <- length(s)
      listOfPersons <- list()
      for (i in (1:n)) {
          ipersons <- parseLine(obj, s[i])
          listOfPersons[[i]] <- ipersons
      }
      obj@persons <- listOfPersons
      return(obj)
  }
)

屬性 persons 是 S4 Person 類的例項列表。這個東西不能被強制轉換為任何標準型別,因為 R 不知道如何處理使用者定義類的例項。解決方案是使用之前定義的 as.list 方法將 Person 轉換為列表。然後我們可以通過 lapply() 函式將此函式應用於列表 persons 的每個元素。然後在下一次呼叫 lappy() 函式時,現在應用 data.frame 函式將 persons.list 的每個元素轉換為資料幀。最後,呼叫 rbind() 函式來新增轉換為生成的資料幀的新行的每個元素(有關此內容的更多詳細資訊,請參閱此帖子

# Sugestion taken from this post:
# http://stackoverflow.com/questions/4227223/r-list-to-data-frame
setMethod(f = "as.df", signature = "Context",
  definition = function(obj) {
    persons <- obj@persons
    persons.list <- lapply(persons, as.list)
    persons.ds <- do.call(rbind, lapply(persons.list, data.frame, stringsAsFactors = FALSE))
    return(persons.ds)
  }
)

把所有東西放在一起

最後,讓我們來測試整個解決方案。定義要解析缺少地址資訊的第二行的位置的行。

s <- c(
    "GREGORY BROWN; 25 NE 25TH; +1-786-987-6543",
    "DAVID SMITH;786-123-4567",
     "ALAN PEREZ; 25 SE 50TH; +1-786-987-5553"
)

現在我們初始化 context,並解析這些行:

context <- new("Context")
context <- parseLines(context, s)

最後獲取相應的資料集並列印出來:

df <- as.df(context)
> df
           name    address           phone
1 GREGORY BROWN 25 NE 25TH +1-786-987-6543
2   DAVID SMITH       <NA>    786-123-4567
3    ALAN PEREZ 25 SE 50TH +1-786-987-5553

我們現在測試 show 方法:

> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']

而對於某些子州:

>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']

最後,測試 as.list() 方法:

> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"

$address
[1] "25 NE 25TH"

$phone
[1] "+1-786-987-6543"

> 

結論

此示例顯示如何使用 R 中的一種可用機制來實現狀態模式,以使用 OO 範例。然而,R OO 解決方案不是使用者友好的,與其他 OOP 語言有很大不同。你需要改變思維方式,因為語法完全不同,它更多地提醒了函數語言程式設計正規化。例如,代替:在 Java / C#中使用 object.setID("A1"),對於 R,你必須以這種方式呼叫方法:setID(object, "A1")。因此,你始終必須將物件包含為輸入引數,以提供函式的上下文。同樣地,沒有特殊的 this 類屬性和用於訪問給定類的方法或屬性的 . 表示法。這是更多錯誤提示,因為通過屬性值(PersonisState 等)來引用類或方法。

如上所述,S4 類解決方案比傳統的 Java / C#語言需要更多的程式碼行來完成簡單的任務。無論如何,狀態模式對於這類問題是一個很好的通用解決方案。它簡化了將邏輯委託給特定狀態的過程。我們在每個 State 子類實現中都有較小的 if-else 塊來實現在每個狀態下執行的操作,而不是用於控制所有情況的大塊 if-else 塊。

附件在這裡你可以下載整個指令碼。

任何建議都是受歡迎的。