实现简单FSM的Pythonic方式是什么?

14

昨天我需要解析一个非常简单的二进制数据文件 - 规则是,查找连续两个字节都是0xAA的位置,接着下一个字节将是长度字节,然后跳过9个字节并输出从那里给出的数据量。重复执行直到文件末尾。

我的解决方案确实有效,并且很快就能完成(尽管我本质上是一名C程序员,但我仍然认为用Python编写比用C编写更快)- 但是,它显然不够Pythonic,并且读起来像一个C程序(而且还不是一个很好的C程序!)

有没有更好/更符合Pythonic的方法?在Python中使用这种简单的自动机是否仍然是正确的选择?

我提供的解决方案:

#! /usr/bin/python

import sys

f = open(sys.argv[1], "rb")

state = 0

if f:
    for byte in f.read():
        a = ord(byte)       
        if state == 0:
            if a == 0xAA:
                state = 1
        elif state == 1:
            if a  == 0xAA:
                state = 2
            else: 
                state = 0
        elif state == 2:
            count = a;
            skip = 9
            state = 3
        elif state == 3:
            skip = skip -1
            if skip == 0:
                state = 4
        elif state == 4:
             print "%02x" %a
             count = count -1 
             if count == 0:
                 state = 0
                 print "\r\n"

1
在我看来很好。那就是我写代码的方式。 - Ignacio Vazquez-Abrams
11
好的,首先你需要一个啤酒火山,然后最好再配上海盗装...等等,我们这里是在谈论哪个FSM呢? - Conspicuous Compiler
4
我一定很累了,我不停地把FSM误读成“飞行意大利面怪物”。 - Donal Fellows
7个回答

8

哇!谢谢——我开始阅读Beazley的参考资料,这将花费我大部分晚上的时间,但它已经很值得了。 - Pierce
这太神奇了。我来自一个非常(好吧,完全就是)命令式编程背景,第一次读到它的时候完全震惊了我。 - Hank Gay
有趣 - 谢谢!对于我的特定情况可能过于复杂了,但仍然非常酷。 - Vicky

7
你可以给状态常量命名,而不是使用 0、1、2 等数字,以提高可读性。
你可以使用字典将 (current_state, input) -> (next_state) 进行映射,但这并不能让你在转换过程中进行任何额外的处理。除非你还包括一些“转换函数”来执行额外的处理。
或者你可以采用非有限状态机 (FSM) 的方法。我认为只要 0xAA 0xAA 只出现在表示“开始”的时候(不出现在数据中),这种方法就可以奏效。
with open(sys.argv[1], 'rb') as f:
    contents = f.read()
    for chunk in contents.split('\xaa\xaa')[1:]:
        length = ord(chunk[0])
        data = chunk[10:10+length]
        print data

如果数据中确实出现了这个字符,您可以使用string.find('\xaa\xaa', start)来扫描字符串,将start参数设置为上一个数据块结束的位置。重复此操作,直到返回-1。

太棒了 - 谢谢!我知道肯定有类似的方法,但是一直想不明白。 - Vicky
哦 - 0xAA 0xAA 在有效数据中没有出现。它可能会出现在有效数据块之间的垃圾中,如果是这样,那么你的解决方案和我的解决方案(以及我所知道的任何其他解决方案)都无法处理它。 - Vicky
我认为这种方法比大多数过度面向对象的方式更符合Pythonic风格。https://github.com/kashifrazzaqui/themstates - keios

3
我有些担心告诉别人什么是“Pythonic”,但还是试试吧。首先,要记住在 Python 中函数只是对象。可以使用字典定义转换,其中以 (输入, 当前状态) 为键,(下一个状态,操作) 为值。操作只是执行从当前状态到下一个状态所需的任何操作的函数。
这里有一个看起来不错的例子可以参考:http://code.activestate.com/recipes/146262-finite-state-machine-fsm。我没有用过它,但从快速阅读中似乎它涵盖了所有内容。
类似的问题在几个月前曾被问到/回答过: Python state-machine design。你也可能会发现查看那些响应很有用。

1
你可以使用正则表达式。类似这样的代码将会找到第一个数据块,然后只需从上一次匹配之后开始下一次搜索。
find_header = re.compile('\xaa\xaa(.).{9}', re.DOTALL)
m = find_header.search(input_text)
if m:
    length = chr(find_header.group(1))
    data = input_text[m.end():m.end() + length]

1

我认为你的解决方案看起来不错,除了你应该用 count -= 1 替换 count = count - 1

这是那种时候,花哨的代码展示者会想出将状态映射到可调用对象的字典,再加上一个小型驱动函数,但这并不更好,只是更花哨,并使用了更晦涩的语言特性。


1
我建议查看David Mertz的《Python文本处理》第4章。他在Python中实现了一个非常优雅的状态机类。

1

我认为最Pythonic的方法是像FogleBird建议的那样,将(当前状态,输入)映射到一个函数,该函数将处理处理和转换。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接