以前为了提高 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)
那是别人打的一个 patch ,我昨天无意看到的,就加到仓库里去了。因为自己也没有 Mac ,无法测试。既然现在没有问题了,那么我回头发布一个新版本去。 :)
发现一些错别字,赶快改过来吧,看来是fcitx的功劳....
并不简[介], 要[讲]它 ...
*^_^* 多谢!不过你这个评论应该是发在前一篇文章那里吧?
其实全用C写不就没有name mangling的问题了?
就是这篇文章啊?
这篇文章里哪里有“简介”或“简洁”这个词啊? ^_^
代码以前用 C++ 写好了的,何必再用 C 重写呢?
已经改过来了嘛... ~
寒……难道是有错乱了?怎么又变成这篇文章了? -.-bb
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]
memory.o: could not read symbols: Bad value
collect2: ld returned 1 exit status
全是乱码 比如鏄ㄥぉ [3..9]之类的
请看一下你的字符串是不是 utf-8 编码的。mmseg 只支持 utf-8 。 :)