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 https://pypi.org/project/OpenCC/#description )
- # -*- 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 文件. 否者下面的这个错误会一直缠着你:
java.lang.UnsatisfiedLinkError: %1 不是有效的 Win32 应用程序
从两方面简述一下如何正确的生成 64 位的 opencc 工程文件.
使用 cmake-gui configure 直接指定 64 位的编译器, 选择 Visual Studio 14 2015 Win64, 而不是 Visual Studio 14 2015.
如果当前的工程为 32 位的工程, 可以在 VS 中通过 configuration manager 来手动配置为 x64 位. 将 32 位工程手动改为 64 位工程可能会有许多的坑, 比如:
fatal error LNK1112: module machine type 'x64' conflicts with target machine type 'X86'
下面列举出一些解决方案:
- Check your properties options in your linker settings at: Properties> Configuration Properties> Linker> Advanced> Target Machine. Select MachineX64 if you are targeting a 64 bit build, or MachineX86 if you are making a 32 bit build.
- Select Build> Configuration Manager from the main menu in visual studio. Make sure your project has the correct platform specified. It is possible for the IDE to be set to build x64 but an individual project in the solution can be set to target win32. So yeah, visual studio leaves a lot of rope to hang yourself, but that's life.
Check your library files that they really are of the type of platform are targeting. This can be used by using dumpbin.exe which is in your visual studio VC\bin directory. use the -headers option to dump all your functions. Look for the machine entry for each function. it should include x64 if it's a 64 bit build.
In visual studio, select Tools> Options from the main menu. select Projects and Solutions> VC++ Directories. Select x64 from the Platform dropdown. Make sure that the first entry is: $(VCInstallDir)\bin\x86_amd64 followed by $(VCInstallDir)\bin.
Check in Visual Studio:Project Properties -> Configuration Properties -> Linker -> Command line."Additional Options" should NOT contain /machine:X86.I have such key, generated by CMake output: CMake generated x86 project, then I added x64 platform via Configuration Manager in Visual Studio 2010 - everything was create fine for new platform except linker command line, specified /machine:X86 separately.
编码问题
由于 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
- 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 中的断点了.
小结
如果能够耐心的浏览一遍, 相信会发现这是一篇采坑复盘. 能够从头开始独立的一步一步解决掉遇到的每一个问题, 相信一定会别有一番滋味. 希望本篇博文能在需要的时候对你有所帮助.
来源: https://www.cnblogs.com/yssjun/p/10447017.html