Linux C++ 中文處理
背景
C++ 對於中文的處理是很蛋疼的事情,然而,不幸的我們接到命令,要在 Linux 下支援對文案進行文案超長截斷處理。這樣的話應該怎麼做呢?
UTF-8 介紹
首先,我們可以假定我們接受到的字串是 UTF-8 編碼的。如果在本地的話可以通過本地環境配置來保證。命令列下執行 locale
命令,LC_CTYPE 應該是 UTF-8 的。vim 開啟檔案敲下 :set
命令,應該有一行是 fileencoding=utf-8
。這樣我們就有了工作的基礎。
UTF-8 是對 Unicode 字符集的實現,它是一種變長編碼,對於一個Unicode 的字元編碼成 1 至 4 個位元組。我們可以認為,在 UTF-8 中,英文是 1 個位元組,中文是 3 個位元組。
UTF-8 的詳細介紹可以看:
Unicode 和 UTF-8 有何區別? — 知乎
UTF-8 — 維基百科
設計思路
既然知道 UTF-8 的中英文字元位元組長度,那我們可能想用這樣一個方案:遍歷字串,判斷當前位元組屬於中文還是英文,如果英文則對長度加一併從下一個位元組繼續處理,如果是中文則對長度加一併跳到後面第三個位元組繼續處理。達到我們需要的文案長度時,break 跳出迴圈,返回當前遍歷得到的子字串。
但是這樣的實現會感覺很 hack,有點暴力,程式容易寫出問題。而且我們前面的假設畢竟是一般情況下(雖然概率很低),如果出現一個四位元組的字元那程式會錯得一塌糊塗。
如果有一種編碼或資料型別,每個中英文字元都佔據相同長度,那我們的處理就會簡單多了。這時候我們想到了 C++ 的 wstring 型別,wstring 的 size() 函式返回的就是包含的中英文字元個數。wstring 與 string 一樣都是基於 basic_string 類模板,不同的是 string 使用 char 為基本型別,而 wstring 是 wchat_t。wchar_t 可以支援 Unicode 字元的儲存,在 Win 下是兩個位元組, Linux 的實現則是四個位元組,可以直接用 sizeof(wchar_t)
到這裡我們已經有了基本的思路:實現 string 和 wstring 的互相轉換,並用 wstring 來判斷字元個數,在超長時進行截斷。
string 與 wstring 的轉換
轉換版本一
如果你的 g++ 版本夠高(5.0以上),那麼可以採用下面的寫法,這是最好的:
#include <codecvt>
#include <string>
std::wstring s2ws(const std::string& str)
{
using convert_typeX = std::codecvt_utf8<wchar_t> ;
std::wstring_convert<convert_typeX, wchar_t> converterX;
return converterX.from_bytes(str);
}
std::string ws2s(const std::wstring& wstr)
{
using convert_typeX = std::codecvt_utf8<wchar_t>;
std::wstring_convert<convert_typeX, wchar_t> converterX;
return converterX.to_bytes(wstr);
}
std::wstring_convert 是 C++11 標準庫提供的對 string 和 wstring 的轉換,對 Unicode 進行了語言和庫級別的支援。但這一特性在 gcc/g++ 5.0 以上才被支援。
參考資料:
How to convert wstring into string? — stackoverflow
std::wstring_convert — cppreference
std::wstring_convert — cplusplus
轉換版本二
如果你的 g++ 版本是支援部分 c++11 特性,那麼第二個版本可以用 unique_ptr 來管理記憶體,這樣可以避免直接操作指標的尷尬,程式更加安全。
#include <cstdlib>
#include <memory>
#include <string>
std::wstring s2ws(const std::string& str) {
if (str.empty()) {
return L"";
}
unsigned len = str.size() + 1;
setlocale(LC_CTYPE, "en_US.UTF-8");
std::unique_ptr<wchar_t[]> p(new wchar_t[len]);
mbstowcs(p.get(), str.c_str(), len);
std::wstring w_str(p.get());
return w_str;
}
std::string ws2s(const std::wstring& w_str) {
if (w_str.empty()) {
return "";
}
unsigned len = w_str.size() * 4 + 1;
setlocale(LC_CTYPE, "en_US.UTF-8");
std::unique_ptr<char[]> p(new char[len]);
wcstombs(p.get(), w_str.c_str(), len);
std::string str(p.get());
return str;
}
new 陣列的長度要考慮到,因為 wchar_t 為 4 個位元組,對於 s2ws, wstring 的長度肯定小於等於 string 的長度,而對 ws2s, string 的長度也肯定小於等於 wstring 4 倍的長度。+1 是預留給字串的結束符 ‘\0’。
setlocale 函式用於執行時的語言環境,可以在命令列用 locale 檢視當前系統的語言環境設定,LC_CTYPE 指語言符號及其分類 。網上很多版本使用 setlocale(LC_CTYPE, "");
, 這裡第二個引數用空字串,會使用系統當前預設的 locale 設定。但是這樣有個問題,也許你寫出來的程式在本機執行正確,但到伺服器上就錯了,因為伺服器的 locale 不一定是 utf8,所以這裡要強制設定為 en_US.UTF-8。
mbstowcs 和 wcstombs 是兩個 C 語言中對多位元組字串和寬字元字串的互相轉換函式,依賴於當前 locale 中所指定的字元編碼。
轉換版本三
如果 g++ 連 unique_ptr 都不支援,那就只能使用下面的 new/delete 了。
#include <cstdlib>
#include <string>
std::wstring s2ws(const std::string& str) {
if (str.empty()) {
return L"";
}
unsigned len = str.size() + 1;
setlocale(LC_CTYPE, "en_US.UTF-8");
wchar_t *p = new wchar_t[len];
mbstowcs(p, str.c_str(), len);
std::wstring w_str(p);
delete[] p;
return w_str;
}
std::string ws2s(const std::wstring& w_str) {
if (w_str.empty()) {
return "";
}
unsigned len = w_str.size() * 4 + 1;
setlocale(LC_CTYPE, "en_US.UTF-8");
char *p = new char[len];
wcstombs(p, w_str.c_str(), len);
std::string str(p);
delete[] p;
return str;
}
最終實現
實現了 string 和 wstring 的轉換後,接下來的處理就很簡單了。實現處理函式 FormatText,然後加入 main 函式測試,完整程式碼如下:
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <string>
static const int kTextSize = 10;
std::wstring s2ws(const std::string& str) {
if (str.empty()) {
return L"";
}
unsigned len = str.size() + 1;
setlocale(LC_CTYPE, "");
wchar_t *p = new wchar_t[len];
mbstowcs(p, str.c_str(), len);
std::wstring w_str(p);
delete[] p;
return w_str;
}
std::string ws2s(const std::wstring& w_str) {
if (w_str.empty()) {
return "";
}
unsigned len = w_str.size() * 4 + 1;
setlocale(LC_CTYPE, "");
char *p = new char[len];
wcstombs(p, w_str.c_str(), len);
std::string str(p);
delete[] p;
return str;
}
bool FormatText(std::string* txt) {
if (NULL == txt) {
return false;
}
std::cout << "before:" << *txt << std::endl;
std::wstring w_txt = s2ws(*txt);
std::cout << "wstring size:" << w_txt.size() << std::endl;
std::cout << "string size:" << (*txt).size() << std::endl;
if (w_txt.size() > kTextSize) {
w_txt = w_txt.substr(0, kTextSize);
*txt = ws2s(w_txt);
*txt += "...";
}
std::cout << "after:" << *txt << std::endl;
return true;
}
int main() {
assert(L"" == s2ws(""));
std::string txt = "龍之谷app好玩等你";
assert(24 == txt.size());
std::wstring w_txt = s2ws(txt);
assert(10 == w_txt.size());
assert("" == ws2s(L""));
w_txt = L"龍之谷app好玩等你";
assert(10 == w_txt.size());
txt = ws2s(w_txt);
assert(24 == txt.size());
txt = "龍之谷app公測開啟";
std::string format_txt = txt;
FormatText(&format_txt);
assert(txt == format_txt);
txt = "龍之谷app公測火爆開啟";
FormatText(&txt);
format_txt = "龍之谷app公測火爆...";
assert(format_txt == txt);
return 0;
}