運維日記003-那些曾經令人頭痛的亂碼
“回車”與”換行“
在Linux上用vi編輯好的文件,傳給別人的Windows電腦上後,打開發現所有的文字都變成了一行;反之,Windows系統上編輯好的文本文件,拿到Linux上也會出現問題。什麼原因呢?
在計算機還沒有出現之前,有一種叫做電傳打字機(Teletype Model 33)的玩意,每秒鐘可以打10個字符。但是它有一個問題,就是打完一行換行的時候,要用去0.2秒,正好可以打兩個字符。要是在這0.2秒裏面,又有新的字符傳過來,那麼這個字符將丟失。
於是,研製人員想了個辦法解決這個問題,就是在每行後面加兩個表示結束的字符。一個叫做“回車”(,在C語言中用“\r”表示,對應的ASCII碼爲0x0D),告訴打字機把打印頭定位在左邊界;另一個叫做“換行”(,在C語言中用”\n”表示,對應的ASCII碼爲0x0A),告訴打字機把紙向下移一行。這就是“換行”和“回車”的來歷,從它們的英語名字上也可以看出一二。
後來,計算機發明瞭,這兩個概念也就被般到了計算機上。那時,存儲器很貴,一些科學家認爲在每行結尾加兩個字符太浪費了,加一個就可以。於是,就出現了分歧。UNIX一族決定只用進紙一個字符來表示行尾。來自蘋果陣營的人則把回車作爲換行的標準。MS-DOS(和微軟的Windows)仍然決定沿用古老的回車換行傳統。所以,Unix/Linux下的一些傳統軟件以0x0A作爲換行標誌,而Windows下的軟件以0x0A0D作爲換行標誌。這樣造成的直接後果是,Unix/Mac系統下的文件在Windows裏打開的話,所有文字會變成一行;而Windows裏的文件在Unix/Mac下打開的話,在每行的結尾可能會多出一個^M符號。
舉個例子,寫一小段c++代碼:
$ vim a.cpp
代碼內容:
#include <iostream>
using namespace std;
int main()
{
cout << "hahaha" << "\r\n" << "xixi" << "\r\n";
}
運行該程序,用vi打開輸出的內容:
$ g++ a.cpp
$ ./a.out > a.txt
$ vi a.txt
可以看到兩個”^M“的符號。而在Windows系統下用記事本打開則完全正常。用hexdump查看a.txt文件,可以看到兩處0D0A標誌。
$ hexdump a.txt
0000000 6168 6168 6168 0a0d 6978 6978 0a0d
000000e
處理這類亂碼和串行問題可以使用unix2dos和dos2unix兩個小工具。在RHEL6/CentOS6下我們用如下命令安裝:
# yum install unix2dos
# yum install dos2unix
這兩個工具使用的方法也很簡單:
$ dos2unix -n a.txt b.txt
上面這條命令把Dos/Windows下的文本文件a.txt轉換爲Unix/Linux下的文本文件b.txt。可以用hexdump打開b.txt觀察一下,可以發現0A0D的標誌都換成了0A標誌。unix2dos的用法與之類似。
字符編碼與iconv編碼轉換
字符編碼的基本概念
ASCII編碼,就是英文顯示文字所需要的256個字符(比如,英文字母、數字、標點符號等等)
ANSI編碼,像中文,肯定不能只用256個字符就代表所有漢字。因此對ASCII碼錶進行了擴展,使用兩個(或多個)字節,代表一個漢字。類似的,不同的國家和地區制定了不同的標準,這些使用 2 個字節來代表一個字符的各種延伸編碼方式,稱爲 ANSI 編碼。也就是說,ANSI是一種對ASCII碼錶進行擴展的泛稱,不同語言操作系統,其代表的編碼方式不一樣。比如中文操作系統,ANSI編碼就代指GB2312;日文操作系統ANSI編碼就代指JIS。
Unicode編碼,Unicode是一個超大的集合,也是一個統一的標準,可以容納世界上的所有語言符號。每個符號的編碼都不一樣,比如,U+0639表示阿拉伯字母Ain,U+0041表示英語的大寫字母A,“漢”這個字的Unicode編碼是U+6C49。
代碼頁(codepage),Unicode是一個世界統一的標準,也就是說,如果一個文本是用Unicode方式編碼的,那麼它可以同時顯示中文、日文、阿拉伯文等等,並且是在任何系統上都可以正常顯示的。但是由於ANSI編碼之間互不兼容,因此就需要有一個標識來表明不同的ANSI編碼到Unicode之間的映射關係(也就是不同編碼之間的映射關係),這個就是代碼頁。比如簡體中文的代碼頁是CP_936(中文系統默認的代碼頁),這個也就是windows API中MultiByteToWideChar第一個參數所代表的含義。如果不標明代碼頁,系統是不知道如何進行編碼轉換的。
SBCS(單字節字符集),MBCS(多字節字符集),DBCS(寬字節字符集),分別對應上面提到的ASCII編碼、ANSI編碼、Unicode編碼。
中文常見編碼:GB2312(CP_20936)->GBK(CP_936)->GB18030(CP_54936),三種編碼方式向下兼容,也就是說GB18030包含GB2312的所有字符。GB18030在2000年取代GBK成爲正式國家標準。
UCS(Unicode Character Set):UCS-2規定了2個字節代表一個文字,還有UCS-4規定了4個字節代表一個文字。我們工作中幾乎總是在和UCS-2打交道。
UTF(UCS Transformation Format):UCS只是規定的如何編碼,但是沒有規定如何傳輸、保存這個編碼。UTF則規定了由幾個字節保存這個編碼。UTF-7,UTF-8,UTF-16都是比較常見的編碼方式。UTF-8編碼與Unicode編碼並不相同,但是它們之間可以通過計算進行轉換,而不像ANSI和Unicode之間必須通過一個映射表來人爲規定其對應關係。UTF-16完全對應於UCS-2,並可通過計算代表一部分UCS-4文字。還有UTF-32則是完全對應於UCS-4,不過很不常見就是了。
UTF-8是與ASCII碼兼容的,英文字母1個字節,漢字通常是3個字節
;UTF-16的所有字符都是用2個字節進行保存,其編碼與Unicode是等價的。UTF-16又分爲UTF-16LE(little endian)和UTF-16BE(big endian),比如一個字母’a’,如果按utf-8來存,就是0x61;如果按utf-16le來存就是0x61 0x00(低有效位在前);如果按utf-16be來存就是0x00 0x61(高有效位在前)。這個也就是我們用記事本另存文件的時候可以選擇的幾個編碼方式的含義。BOM(byte order mark),上面提到的utf-8 utf-16le utf16-be都是unicode編碼,但是系統依然無法正確解析一個文本文件,即便已經知道它是unicode編碼。所以就有了這樣的規定:在文本文件的最開頭插入幾個字節的標識,來說明編碼方式。utf-8的BOM是0xef 0xbb 0xbf,utf-16le的BOM是0xff 0xfe,utf16-be的BOM是0xfe 0xff。事實上BOM並不是必須的,它僅僅是幫助程序自動判斷編碼方式使用的,如果我們手動選擇編碼方式(像ANSI一樣),即便沒有BOM,也是可以正常顯示的。反過來說,程序讀文本文件的時候要先讀文本開始的三個字節判斷下編碼方式。
iconv文件編碼轉換
iconv工具可以方便的在不同的字符編碼之間進行轉換。其命令用法爲:
iconv [選項…] [文件…]
有如下選項可用:
輸入/輸出格式規範:
-f, –from-code=名稱 原始文本編碼
-t, –to-code=名稱 輸出編碼
信息:
-l, –list 列舉所有已知的字符集
輸出控制:
-c 從輸出中忽略無效的字符
-o, –output=FILE 輸出文件
例如,要將文件readme.txt由GB2312編碼轉換爲UTF-8編碼,使用如下命令:
$ iconv -f gb2312 -t utf8 ./readme.txt -o ./readme_utf8.txt
另外,系統還提供了一個iconv函數可供編程時調用,用於解決編碼轉換問題。具體使用方法可查看手冊:
$ man 3 iconv
注:以上內容有較多篇幅摘抄自互聯網,在此向作者表示感謝!