OpenCC全称Open Chinese Convert,是一个Github上面的开源项目,主要用于简繁体汉字的转换,支持语义级别的翻译。本文就来简单介绍一下该库的编译以及python、C++和JAVA分别如何调用DLL进行转换。并记录一些使用过程中踩过的坑。
1.编译DLL
我们首先编译得到opencc的dll动态库。
CMake Command line
当前工作目录生成VS工程文件
cmake -G "Visual Studio 14 2015" -D CMAKE_INSTALL_PREFIX="D:/Projects/Cnblogs/Alpha Panda/OpenCC" ../opencc-ver.1.0.5
编译工程文件
cmake --build ./ --config RelWithDebInfo --target install
使用命令行build工程文件。
CMake - Gui
下载最新版本CMake,配置工程代码generator,本文使用的Visual Studio 14 2015。
Configure操作过程中需要正确的设置安装路径,这个安装路径决定了dll会去哪个目录下去读取简繁转换的配置文件。
CMake中的变量CMAKE_INSTALL_PREFIX控制安装路径,其默认值
- UNIX:/usr/local
- Windows:c:/Program Files/${PROJECT_NAME}
这里设置为:D:/Projects/Cnblogs/Alpha Panda/OpenCC
接着经过generate生成VS工程文件。
Visual Studio
使用CMake command line或者cmake-gui得到VS工程文件。
打开VS工程,这里我们只编译工程libopencc得到dll文件。为了后续便于使用attach功能调试dll文件,最好将工程配置为RelWithDebInfo。
工程libopencc的属性配置寻找一个宏变量:PKGDATADIR(PKGDATADIR="D:/Projects/Cnblogs/Alpha Panda/OpenCC/share//opencc/")
这个宏变量是源代码根目录下面的CMakeLists.txt中设置的,感兴趣的话可以简单了解一下这个变量的设置过程:
CMAKE_INSTALL_PREFIX = D:/Projects/Cnblogs/Alpha Panda/OpenCC set (DIR_PREFIX ${CMAKE_INSTALL_PREFIX}) set (DIR_SHARE ${DIR_PREFIX}/share/) set (DIR_SHARE_OPENCC ${DIR_SHARE}/opencc/) -DPKGDATADIR="${DIR_SHARE_OPENCC}"
简繁转换的配置文件必须要放到这个目录下。
2.使用python
利用上面编译得到的libopencc的DLL文件,通过python调用来进行字体的转换:(下面的代码改编自 OpenCC 0.2)
# -*- coding:utf-8 -*-
import os import sys from ctypes.util import find_library from ctypes import CDLL, cast, c_char_p, c_size_t, c_void_p __all__ = ['CONFIGS', 'convert'] if sys.version_info[0] == 3: text_type = str else: text_type = unicode _libcfile = find_library('c') or 'libc.so.6' libc = CDLL(_libcfile, use_errno=True) _libopenccfile = os.getenv('LIBOPENCC') or find_library('opencc') if _libopenccfile: libopencc = CDLL(_libopenccfile, use_errno=True) else: #libopencc = CDLL('libopencc.so.1', use_errno=True) # _libopenccfile = find_library(r'G:\opencc\build\src\Release\opencc') # 貌似不能使用相对路径? cur_dir = os.getcwd() lib_path = os.path.join(cur_dir, 'T2S_translation_lib', 'opencc') lib_path = './share/opencc' libopencc = CDLL(lib_path, use_errno=True) libc.free.argtypes = [c_void_p] libopencc.opencc_open.restype = c_void_p libopencc.opencc_convert_utf8.argtypes = [c_void_p, c_char_p, c_size_t] libopencc.opencc_convert_utf8.restype = c_void_p libopencc.opencc_close.argtypes = [c_void_p]
libopencc.opencc_convert_utf8_free.argstypes = c_char_p CONFIGS = [ 'hk2s.json', 's2hk.json', 's2t.json', 's2tw.json', 's2twp.json', 't2s.json', 'tw2s.json', 'tw2sp.json', 't2tw.json', 't2hk.json', ] class OpenCC(object): def __init__(self, config='t2s.json'): self._od = libopencc.opencc_open(c_char_p(config.encode('utf-8'))) def convert(self, text): if isinstance(text, text_type): # use bytes text = text.encode('utf-8') retv_i = libopencc.opencc_convert_utf8(self._od, text, len(text)) if retv_i == -1: raise Exception('OpenCC Convert Error') retv_c = cast(retv_i, c_char_p) value = retv_c.value # 此处有问题? # libc.free(retv_c) libopencc.opencc_convert_utf8_free(retv_i)
return value def __del__(self): libopencc.opencc_close(self._od) def convert(text, config='t2s.json'): cc = OpenCC(config) return cc.convert(text)
上面的这段代码可以当做离线工具来进行文件的转换,并没有线上运行时被调用验证过,可能存在内存泄露,仅供参考。
关于python如何调用DLL文件,可以参考我的另一篇文章:Python使用Ctypes与C/C++ DLL文件通信过程介绍及实例分析
使用示例:
origin_text = u'(理发 vs 发财),(闹钟 vs 一见钟情),后来'.encode('utf-8') s2t_1 = convert(origin_text, 's2t.json') t2s_1 = convert(s2t_1, 't2s.json') print t2s_1.decode('utf-8') print s2t_1.decode('utf-8') print origin_text == t2s_1 ============================================ >>>(理发 vs 发财),(闹钟 vs 一见钟情),后来 >>>(理髮 vs 發財),(鬧鐘 vs 一見鍾情),後來 >>>True
3.使用C++
下面我们来使用C++来演示一下如何使用OpenCC进行繁简字体的转换。
由于opencc传入的翻译文本编发方式为utf-8。因此需要对待翻译文本进行编码转换。
string GBKToUTF8(const char* strGBK) { int len = MultiByteToWideChar(CP_ACP, 0, strGBK, -1, NULL, 0); wchar_t* wstr = new wchar_t[len + 1]; memset(wstr, 0, len + 1); MultiByteToWideChar(CP_ACP, 0, strGBK, -1, wstr, len); len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len + 1]; memset(str, 0, len + 1); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL); string strTemp = str; if (wstr) delete[] wstr; if (str) delete[] str; return strTemp; } string UTF8ToGBK(const char* strUTF8) { int len = MultiByteToWideChar(CP_UTF8, 0, strUTF8, -1, NULL, 0); wchar_t* wszGBK = new wchar_t[len + 1]; memset(wszGBK, 0, len * 2 + 2); MultiByteToWideChar(CP_UTF8, 0, strUTF8, -1, wszGBK, len); len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL); char* szGBK = new char[len + 1]; memset(szGBK, 0, len + 1); WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL); string strTemp(szGBK); if (wszGBK) delete[] wszGBK; if (szGBK) delete[] szGBK; return strTemp; }
这是在windows平台上两个非常有用的UTF8和GBK编码互转函数。
方便起见我们直接在opencc中添加一个新的工程,命名为Translation。
#include <cstdio>
#include <cstdlib>
#include <iostream> #include <string> #include <windows.h> #include <fstream> #include "../OpenCC-ver.1.0.5/src/opencc.h" //using namespace std; using std::cout; using std::endl; using std::string; #define OPENCC_API_EXPORT __declspec(dllimport) OPENCC_API_EXPORT char* opencc_convert_utf8(opencc_t opencc, const char* input, size_t length); OPENCC_API_EXPORT int opencc_close(opencc_t opencc); OPENCC_API_EXPORT opencc_t opencc_open(const char* configfilename);
OPENCC_API_EXPORT void opencc_convert_utf8_free(char* str); #pragma comment(lib, "../Build/src/RelWithDebInfo/opencc.lib")
string GBKToUTF8(const char* strGBK); string UTF8ToGBK(const char* strUTF8); int main() { char* trans_conf = "s2t.json"; char* trans_res = nullptr; string gbk_str, utf8_str, res; // read from file and write translation results to file std::ifstream infile; std::ofstream outfile; infile.open("infile.txt", std::ifstream::in); outfile.open("outfile.txt", std::ifstream::out); // open the config file opencc_t conf_file = opencc_open(trans_conf); while (infile.good()) { infile >> gbk_str; utf8_str = GBKToUTF8(gbk_str.c_str()); std::cout << gbk_str << "\n"; trans_res = opencc_convert_utf8(conf_file, utf8_str.c_str(), utf8_str.length()); cout << UTF8ToGBK(trans_res) << endl; outfile << trans_res << endl;
opencc_convert_utf8_free(trans_res); // delete[] trans_res; trans_res = nullptr; } infile.close(); outfile.close(); opencc_close(conf_file); conf_file = nullptr; system("pause"); return 0; }
上面的这段C++代码可以从infile.txt中读取简体中文,然后将翻译结果写入到outfile.txt文件中。
3.使用JAVA
这里给出一个使用JNA调用DLL的方案:
package com.tvjody; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.charset.StandardCharsets; import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; import com.sun.jna.Pointer; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.FileOutputStream; public class JNA_CALL { public interface openccDLL extends Library{ openccDLL Instance = (openccDLL) Native.load( (Platform.isWindows() ? "opencc" : "libc.so.6"), openccDLL.class); // void* opencc_open(const char* configfilename); Pointer opencc_open(String configfilename); // int opencc_close(void* opencc); int opencc_close(Pointer opencc); // void opencc_convert_utf8_free(char* str); void opencc_convert_utf8_free(String str); // char* opencc_convert_utf8(opencc_t opencc, const char* input, size_t length) String opencc_convert_utf8(Pointer opencc, String input, int length); } public static void writeToFile(String utf8_str) throws IOException { Writer out = new OutputStreamWriter(new FileOutputStream("out.txt"), StandardCharsets.UTF_8); out.write(utf8_str); out.close(); } public static String readFromFile() throws IOException { String res = ""; Reader in = new InputStreamReader(new FileInputStream("in.txt"), StandardCharsets.UTF_8); try(BufferedReader read_buf = new BufferedReader(in)){ String line; while((line = read_buf.readLine()) != null) { res += line; } read_buf.close(); } return res; } public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException { System.setProperty("jna.library.path", "D:\\Projects\\Open_Source\\OpwnCC\\Build(x64)\\src\\RelWithDebInfo"); Pointer conf_file = openccDLL.Instance.opencc_open("s2t.json"); try { String res_utf8 = readFromFile(); System.out.println("From: " + res_utf8); byte[] ptext = res_utf8.getBytes("UTF-8"); // String utf8_str = new String(res_utf8.getBytes("GBK"), "UTF-8"); String trans_res = openccDLL.Instance.opencc_convert_utf8(conf_file, res_utf8, ptext.length); System.out.println("To:" + trans_res); // String trans_gbk = new String(trans_res.getBytes("UTF-8"), "GBK"); writeToFile(trans_res); openccDLL.Instance.opencc_convert_utf8_free(trans_res); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } openccDLL.Instance.opencc_close(conf_file); } }
json配置文件的路径有DLL决定,除了上面手动设置dll文件的路径之外,还可以将dll文件放置到bin目录下。上面使用的是jna-5.2.0。
4.填坑指南
实际上使用时会遇到N多的问题,这里仅列出一些注意事项,其实下面的有些问题具有一些普遍性,较为有价值。
DLL读取配置文件路径
工程中读取json配置文件的路径是用宏变量定义,而Cmake的变量MAKE_INSTALL_PREFIX决定了工程中配置文件的宏变量,也决定了DLL被调用时读取配置文件的路径。路径中最好使用‘/’,而不是‘\’。
OCD文件的生成
进行简繁体文字转换的过程需要读取json和对应的ocd文件,ocd文件是由工程Dictionaries生成的,该工程又依赖与opencc_dict的opencc.exe程序。
实际使用时发现最新的1.0.5版本好像有一个错误,需要将上面的一个函数声明,改为下面的函数声明,否者会有一个链接错误。
void ConvertDictionary(const string inputFileName, const string outputFileName, const string formatFrom, const string formatTo); OPENCC_EXPORT void ConvertDictionary(const string inputFileName, const string outputFileName, const string formatFrom, const string formatTo);
此外,data目录下生成的所有ocd文件需要和json配置文件放到同一个目录下,32位和64位的ocd文件也不要混用。
32位or64位
在使用java调用dll的时候要特别的注意,如果是64位的JDK,一定要编译64位的dll和所有的ocd文件。否者下面的这个错误会一直缠着你:
从两方面简述一下如何正确的生成64位的opencc工程文件。
使用cmake-gui configure直接指定64位的编译器,选择Visual Studio 14 2015 Win64,而不是Visual Studio 14 2015。
如果当前的工程为32位的工程,可以在VS中通过configuration manager来手动配置为x64位。将32位工程手动改为64位工程可能会有许多的坑,比如:
下面列举出一些解决方案:
编码问题
由于opencc内部处理字符串均使用的是utf-8编码,因此需要进行编解码的处理才能正确的调用DLL中的接口。
广义上来说,所谓乱码问题就是解码方式和编码方式不同导致的。这是一个很大的话题,这里不深入讨论,有兴趣可以参考我另一篇博文python编码问题分析,应该能对你有所启发。
在win10上使用cmake生成VS工程。编译的时候会遇到一个有趣的问题就是中文环境下utf-8文件中的部分汉字标点,竟然会有乱码,如下:
1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(32): error C3688: invalid literal suffix '銆'; literal operator or literal operator template 'operator ""銆' not found 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(32): error C3688: invalid literal suffix '锛'; literal operator or literal operator template 'operator ""锛' not found 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(32): error C3688: invalid literal suffix '鈥'; literal operator or literal operator template 'operator ""鈥' not found 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(32): error C2001: newline in constant 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(33): error C3688: invalid literal suffix '鈥'; literal operator or literal operator template 'operator ""鈥' not found 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(33): error C3688: invalid literal suffix '锛'; literal operator or literal operator template 'operator ""锛' not found 1>D:\Projects\Open_Source\OpwnCC\OpenCC-ver.1.0.5\src\PhraseExtract.cpp(33): error C3688: invalid literal suffix '銆'; literal operator or literal operator template 'operator ""銆' not found
文本编码对应关系(Visual Studio 2015 VS Notepad++):
file->Advance Save Options:
Chinese Simplified (GB2312) - Codepage 936 <==> GBK Unicode (UTF-8 with signature) - Codepage 65001 <==> Encoding in UTF-8 BOM Unicode (UTF-8 without signature) - Codepage 65001 <==> Encoding in UTF-8
将上面文件的编码方式从Unicode (UTF-8 without signature) - Codepage 65001改为 Chinese Simplified (GB2312) - Codepage 936即可。
python的编码转换比较简单,C++的转换接口上面已经列出,至于java,建议将java文件和数据文件的编码方式均改为utf-8,使用String utf8_str = new String(gbk_str.getBytes("UTF-8"), "UTF-8")这种转码方式可能带来一些奇怪的问题。
DLL与EXE局部堆问题
有一点需要注意,要确保正确释放DLL中使用new在堆中分配的内存空间,这里必须要使用DLL中提供的释放堆空间的函数,而不要在主程序中直接使用delete或者delete[].
简单的解释就是EXE和DLL分别有各自的局部堆,new和delete分别用于分配和释放各自局部堆上的空间,使用EXE中的delete来释放DLL中new的局部堆内存可能会导致错误,这个和具体的编译器有关。
上面的C++代码在EXE中delete DLL分配的空间,是一种未定义行为。
DLL调试技巧
实际使用尤其是使用不同语言对opencc.dll进行调用的时候会碰到很多问题,这时最好的办法就是使用VS的Attach To Process对DLL进行断点跟进。
对于python调用DLL,可以先打开一个python shell或者IDLE环境并在其中调用一下DLL,之后在VS中attach到对应的python进程,不要直接attach到sublime等IDE程序,因为IDE中运行的python程序而不是IDE本身直接调用DLL文件。
对于java而言,同样不能使用vs直接attach到Eclipse等IDE上。这里有一个技巧,就是在调用到DLL接口前的java代码加上一个断点,然后会在VS进程列表中看到一个javaw.exe程序,attach到这个程序后,接着运行java程序就会进入DLL中的断点了。
小结
如果能够耐心的浏览一遍,相信会发现这是一篇采坑复盘。能够从头开始独立的一步一步解决掉遇到的每一个问题,相信一定会别有一番滋味。希望本篇博文能在需要的时候对你有所帮助。