使用状态机解析线

让我们使用状态机模式来解析具有特定模式的行,使用来自 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 块。

附件在这里你可以下载整个脚本。

任何建议都是受欢迎的。