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文件中的部分汉字标点,竟然会有乱码,如下:

OpenCC的编译与多语言使用-LMLPHPOpenCC的编译与多语言使用-LMLPHP
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
View Code

文本编码对应关系(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中的断点了。

小结

如果能够耐心的浏览一遍,相信会发现这是一篇采坑复盘。能够从头开始独立的一步一步解决掉遇到的每一个问题,相信一定会别有一番滋味。希望本篇博文能在需要的时候对你有所帮助。

03-28 05:15