pymmseg-cpp: rmmseg-cpp with Python interface

以前为了提高 RMMSeg 的性能,我写了 rmmseg-cpp ,根据 JavaEye 一段时间的使用情况来看,似乎挺稳定的,性能也很不错。其实 rmmseg-cpp 虽然名字里面有一个 “r” 字,但是算法核心完全是用 C++ 做的,最后才加了一层 Ruby 的接口包装。正巧最近我又两个用 Python 的项目都要用到中文分词,便决定给 rmmseg-cpp 加一个 Python 的接口。

正如我在 On the Rubinius FFI 一文中描述的一样,做到 Native 模块的接口有两种方法,一种是我在 rmmseg-cpp 中用的写 C 代码来粘合二者。另一种则是直接用语言提供的 FFI 接口,写 Python 代码来粘合。也可以算是各有各的好处吧,不过这里用 FFI 有一个很吸引我的地方就是粘合代码不用进行编译。如果用 C 来写粘合代码的话,需要用编译 Python 解释器相同的编译环境下加上 Python 的源代码(至少是头文件)一起编译,在 Linux 下倒是挺方便的,但是在 Windows 下可以说是相当麻烦,这似乎也是 rmmseg-cpp 一直不支持 Windows 平台的原因。但是如果用 FFI 来写粘合代码的话,就只需要单独编译原本的 Native 模块。编译一个标准的 C/C++ 模块和编译一个依赖于 Python/Ruby 源代码/头文件的模块是不一样的,这样的任务即使在 Windows 下也可以很方便地完成。

于是我就理所当然地选择了用 Python 的 FFI : ctypes 来做。首先为原来的 C++ 模块做一层简单的 C 浅包装,这是因为 C++ 的符号在二进制模块中的命名没有规范,函数名经过编译器 mingling 之后的结果虽然看起来都是像一堆乱码,但是不同的编译器会是不同的“乱码”,根本不通用,所以要在 shared library 中导出的话,还是得用 C 来做。几乎都是类似于这样的 trivial 的包装:


rmmseg::Algorithm *mmseg_algor_create(const char *text, int len)
{
    return new rmmseg::Algorithm(text, len);
}

然后将它编译为一个 shared library (Linux 下的 so 或者 Windows 下的 dll),在 Python 中通过 ctypes 加载进来。粘合代码虽然是 Python 代码,但是也并不简洁,因为 Python 并不会读取 C 的头文件,所以还要手工定义函数的 prototype ,类似于这样:


mmseg.mmseg_algor_create.argtypes = [c_char_p, c_int]
mmseg.mmseg_algor_create.restype  = c_void_p

这里我们并不关心创建出来的 rmmseg::Algorithm 对象是什么样子的,我们只是要将它作为参数再传回模块中而已,所以只要把它当作一个 void * (即 c_void_p)看待就可以了。

定义好函数原型之后,函数就可以直接调用了,这都是 ctypes 的功劳,最后,再把这些函数包装到一个更加 Python 的接口中:


class Algorithm(object):
    def __init__(self, text):
        """\
        Create an Algorithm instance to segment text.
        """
        self.algor = mmseg.mmseg_algor_create(text, len(text))
        self.destroied = False

def __iter__(self): """\ Iterate through all tokens. Note the iteration has side-effect: an Algorithm object can only be iterated once. """ while True: tk = self.next_token() if tk is None: raise StopIteration yield tk

# ... snip ...

就万事大吉了!然后就可以在 Python 代码中使用了:


from pymmseg import mmseg

mmseg.dict_load_defaults() text = # ... algor = mmseg.Algorithm(text) for tok in algor: print '%s [%d..%d]' % (tok.text, tok.start, tok.end)

pymmseg-cpp 的代码仍然放在 github 上。

Windows 下的预编译包虽然理论上来说是很容易做的,但是我测试的时候却发现 ctypes 会报一些莫名其妙的错误,比如说我调用函数的时候参数数目不对之类的。我将原本的 mmseg-cpp 的代码编译为 exe 测试的时候似乎没有问题,我想原因也许出在 ctypes 或者是和 Python 接口的地方吧。待我抽时间来好好调试一下看看能否找出原因。 :)

Comments (16)

ashchan August 14, 2008
Good news! 看到rmmseg-cpp终于解决掉Leopard下的引用问题了。下午试一下。辛苦了。
pluskid August 14, 2008
@ashchan,
那是别人打的一个 patch ,我昨天无意看到的,就加到仓库里去了。因为自己也没有 Mac ,无法测试。既然现在没有问题了,那么我回头发布一个新版本去。 :)
quark August 14, 2008

发现一些错别字,赶快改过来吧,看来是fcitx的功劳....

并不简[介], 要[讲]它 ...

pluskid August 15, 2008
@quark,
*^_^* 多谢!不过你这个评论应该是发在前一篇文章那里吧?
Mike August 15, 2008
...抢偶生意。。。。
其实全用C写不就没有name mangling的问题了?
quark August 15, 2008
@pluskid,
就是这篇文章啊?
pluskid August 15, 2008
@quark,
这篇文章里哪里有“简介”或“简洁”这个词啊? ^_^
pluskid August 15, 2008
@Mike,
代码以前用 C++ 写好了的,何必再用 C 重写呢?
quark August 15, 2008
@pluskid,
已经改过来了嘛... ~
pluskid August 15, 2008
@quark,
寒……难道是有错乱了?怎么又变成这篇文章了? -.-bb
youngking June 07, 2009

pluskid你好,有时分词会出现段错误,用catchsegv跟踪结果如下:

Backtrace: /lib/libSegFault.so[0xb8080100] [0xb8085400] /xapian/pymmseg/mmseg-cpp/mmseg.so(mmseg_next_token+0x21)[0xb7aac3c1] /usr/lib/python2.5/site-packages/_ctypes.so(ffi_call_SYSV+0x17)[0xb7853467] /usr/lib/python2.5/site-packages/_ctypes.so(ffi_call+0x86)[0xb78532e6] /usr/lib/python2.5/site-packages/_ctypes.so(_CallProc+0x2e4)[0xb784e5e4] /usr/lib/python2.5/site-packages/_ctypes.so[0xb784902c] /usr/bin/python2.5(PyObject_Call+0x27)[0x805d897] /usr/bin/python2.5(PyEval_EvalFrameEx+0x371d)[0x80ccd4d] /usr/bin/python2.5(PyEval_EvalFrameEx+0x604d)[0x80cf67d] /usr/bin/python2.5[0x81151d6] /usr/bin/python2.5(PyEval_EvalFrameEx+0x919)[0x80c9f49] /usr/bin/python2.5(PyEval_EvalFrameEx+0x604d)[0x80cf67d] /usr/bin/python2.5(PyEval_EvalFrameEx+0x604d)[0x80cf67d] /usr/bin/python2.5(PyEval_EvalCodeEx+0x685)[0x80cfea5] /usr/bin/python2.5(PyEval_EvalFrameEx+0x51a4)[0x80ce7d4] /usr/bin/python2.5(PyEval_EvalCodeEx+0x685)[0x80cfea5] /usr/bin/python2.5(PyEval_EvalFrameEx+0x51a4)[0x80ce7d4] /usr/bin/python2.5(PyEval_EvalCodeEx+0x685)[0x80cfea5] /usr/bin/python2.5(PyEval_EvalFrameEx+0x51a4)[0x80ce7d4] /usr/bin/python2.5(PyEval_EvalCodeEx+0x685)[0x80cfea5] /usr/bin/python2.5(PyEval_EvalFrameEx+0x51a4)[0x80ce7d4] /usr/bin/python2.5(PyEval_EvalCodeEx+0x685)[0x80cfea5] /usr/bin/python2.5(PyEval_EvalCode+0x57)[0x80d00b7] /usr/bin/python2.5(PyRun_FileExFlags+0xef)[0x80edaef] /usr/bin/python2.5(PyRun_SimpleFileExFlags+0x1a2)[0x80eddc2] /usr/bin/python2.5(Py_Main+0xc57)[0x8059617] /usr/bin/python2.5(main+0x22)[0x8058992] /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7ee3775] /usr/bin/python2.5[0x80588d1]

pluskid June 16, 2009
咦,很奇怪呀,请问你能提供可以重现这样错误的数据吗?
耿志峰 July 20, 2009
我也想添加中文分词,但是我的机器是linux_64的,请问是否可以使用?我make时候的提示错误:/usr/bin/ld: memory.o: relocation R_X86_64_32 against `__gxx_personality_v0' can not be used when making a shared object; recompile with -fPIC
memory.o: could not read symbols: Bad value
collect2: ld returned 1 exit status
pluskid July 20, 2009
64 位的 Linux 应该是可以的,我记得我好像曾经在一个 Ubuntu AMD64 上测试过,是可以用的。不知道你用的是什么平台?看那个错误信息好像是编译的时候没有加上 -fPIC 选项,但是我在 build.py 里确实加了这个选项了啊。你看它在编译的时候有显示编译命令是正常的吗?
supernova November 21, 2009
我按照上面的mmseg的调用 为什么显示出来有问题
全是乱码 比如鏄ㄥぉ [3..9]之类的
pluskid November 21, 2009
@supernova,
请看一下你的字符串是不是 utf-8 编码的。mmseg 只支持 utf-8 。 :)