使用 C# 整合 OpenCC 執行中文繁簡轉換
7 | 8,217 |
前篇文章介紹了輕巧但威力強大的 OpenCC,使用 opencc.exe 可輕鬆完成繁簡轉換。
如果我們要在 .NET 裡寫一個函式招喚 OpenCC 將繁體字串轉成簡體字串該怎麼做?
呼叫外部 .exe 這等小事,自然難不倒 .NET 老鳥,生個 System.Diagnostics.Process,給對 exe 路徑,弄兩個隨機暫存檔放待翻文字與輸出結果,等待 opencc.exe 執行完畢,讀出結果刪掉暫存檔,搞定收工!
public static class OpenCCConverter
{
static string GetPath(string file) => $"X:\Tools\OpenCC\{file}";
static string GetTempFile() => $"X:\Temp\OpenCCFiles\{Guid.NewGuid()}";
static void CallOpenCC(string inputFile, string outputFile, string configFile)
{
var si = new ProcessStartInfo()
{
FileName = GetPath("opencc.exe"),
Arguments = $"-i {inputFile} -o {outputFile} -c {GetPath(configFile)}",
CreateNoWindow = true
};
var p = new Process()
{
StartInfo = si
};
p.Start();
p.WaitForExit();
}
/// <summary>
/// 將繁體轉為簡體
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToChsString(string text)
{
var inFile = GetTempFile();
File.WriteAllText(inFile, text);
var outFile = GetTempFile();
CallOpenCC(inFile, outFile, "tw2s.json");
var result = File.ReadAllText(outFile);
File.Delete(inFile);
File.Delete(outFile);
return result;
}
}
這個寫法醜歸醜但很管用,還十分簡單明瞭。只是啟動外部程序成本較高,加上要不斷建檔刪檔,就算只是翻譯一個字元也要動用兩個暫存檔,執行效能及資源使用效率並不好。
無意發現 OpenCC 將核心邏輯放在獨立程式庫 – opencc.dll,何不透過 Interoperability 由 C# 呼叫 C++ 函式直接執行轉換?於是,不知天高地厚的 C++ 麻瓜開啟了 Unmanged DLL 整合大冒險!
先用 Console Application 測試,為求部署方便,我將 OpenCC 納入專案,並設定編譯時輸出到 \bin\opencc 目錄:
開發心得如下:
- C# 要呼叫 C++ 寫的 DLL,起手式是用 DllImport 宣告外部函式對應到 C++ 函式,會遇到的挑戰主要是參數的型別傳換。
- 在 Github 討論串找到網友 C# DllImport 的程式片段,由於最後有成功,極富參考價值。我學到可先用 opencc_open() 指定轉換設定 json 檔建立 Instance,再呼叫 opencc_convert_utf8() 傳入 Instance Pointer 及待轉換字串,取得結果字串 IntPtr,再轉為 C# 字串。
- DllImport 設定不正確時,opencc_open() 時即會出錯,會傳回之類的訊息
Unable to load DLL 'opencc.dll': The specified module could not be found. (Exception from HRESULT: 0x8007007E)
我遇過兩種情況:1) DllImport 指定的 opencc.dll 路徑有誤 2) 執行主機缺少 Visual C++ Runtime。 - 官方下載的 OpenCC 1.0.1 Windows 版使用 Visual Studio 2012 編譯,需要「Visual Studio 2012 最新支援的 Visual C++ 可轉散發套件」,微軟支援網站有個 最新支援的 Visual C++ 下載 網頁已整理好所有 VC++ 版本的可轉散發套件,請自行依所需版本下載安裝。
若懷疑跟 C++ Runtime 套件沒裝有關,最簡單的驗證方法是手動執行 opencc.exe,若彈出缺少 msvcp***.dll 之類的錯誤訊息就是了。 - opencc_convert_utf8() 轉換失敗時不會出錯,會傳回 IntPtr.Zero,詳細錯誤訊息需另外呼叫 opencc_error() 取得。
- 我一度卡在一個關鍵點,待轉換字串與結果字串,形式為記憶體指標指向一段 UTF8 編碼格式的 byte[],與 string 之間需要特殊函式轉換,我在 Stackoverflow 找到可用範例。
瞎弄一陣,萬萬沒想到還真被 C++ 麻瓜試出來了,可執行程式範例如下:
【2024-05-19 更新】程式存在 MemoryLeak 問題,建議改用 CI-YU 版本,感謝 bill 分享。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Debug.WriteLine(
OpenCCHelper.ConvertToChs(
"預設記憶體大小與硬碟容量"));
Console.ReadLine();
}
}
public static class OpenCCHelper
{
[DllImport("opencc\\opencc.dll", EntryPoint = "opencc_open")]
static extern IntPtr opencc_open(string configFileName);
[DllImport("opencc\\opencc.dll", EntryPoint = "opencc_convert_utf8")]
static extern IntPtr opencc_convert_utf8(Int64 opencc, IntPtr input, long length);
static IntPtr OpenCCInstance = IntPtr.Zero;
static OpenCCHelper()
{
OpenCCInstance = opencc_open(".\\opencc\\tw2sp.json");
}
//https://stackoverflow.com/a/10773988/288936
//提醒:以下寫法存在 Memory Leak 問題,建議改用 CI-YU 版
public static IntPtr NativeUtf8FromString(string managedString)
{
int len = Encoding.UTF8.GetByteCount(managedString);
byte[] buffer = new byte[len + 1];
Encoding.UTF8.GetBytes(managedString, 0, managedString.Length, buffer, 0);
IntPtr nativeUtf8 = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, nativeUtf8, buffer.Length);
return nativeUtf8;
}
public static string StringFromNativeUtf8(IntPtr nativeUtf8)
{
int len = 0;
while (Marshal.ReadByte(nativeUtf8, len) != 0) ++len;
byte[] buffer = new byte[len];
Marshal.Copy(nativeUtf8, buffer, 0, buffer.Length);
Marshal.FreeHGlobal(nativeUtf8 );
return Encoding.UTF8.GetString(buffer);
}
public static string ConvertToChs(string text)
{
IntPtr inStr = NativeUtf8FromString(text);
IntPtr outStr = opencc_convert_utf8(OpenCCInstance.ToInt64(), inStr, -1);
Marshal.FreeHGlobal(inStr);
return StringFromNativeUtf8(outStr);
}
}
}
如下圖,我成功呼叫 opencc.dll 完成繁簡轉換。
核子試爆成功是第一步,要寫成共用元件還會再遇到一些問題,例如:x86/x64 必須使用不同 opencc.dll、部署到 ASP.NET 網站時 DllImport 路徑需動態指向網站資料夾、Thread-Safe 考量、Memory Leak 疑慮... C++ 麻瓜大冒險尚未結束,下集待續。
(聲明:程式為門外漢參考爬文及測試所得,如有 C/C++ 高人路過,請鞭小力一點並不吝指正)
Sample code of calling external OpenCC process or unmanaged OpenCC library in C#.
Comments
# by 小歆
可以把你编译好的opencc.dll文件拿来分享一下吗???
# by 我零0七
github 有一个winform版的https://github.com/maboroshinokiseki/opencc-gui-csharp
# by Radium
StringFromNativeUtf8 里是否需要call opencc_convert_utf8_free 把 nativeUtf8 所指向的内存释放掉? opencc_convert_utf8 函数的声明[1]说: You MUST call opencc_convert_utf8_free() to release allocated memory. [1] https://github.com/BYVoid/OpenCC/blob/master/src/opencc.h
# by Jeffrey
to Radium, 的確,應該要加上 Marshal.FreeHGlobal(nativeUtf8 ); 才對,謝謝你的提醒。
# by bill
好像只有C#有釋放記憶體,opencc的dll還是需要做到釋放才不會有問題?
# by bill
等待複核中,留言將在稍後顯示 / The comment is awaiting review.
# by Jeffrey
to bill, 謝謝研究與分享 ,已補充於文章。