簡體   English   中英

Java 字節 stream 非英文字符

[英]Java byte stream non english characters

我讀了這段代碼 作為 xanadu.txt 內容使用“測試”。 該文件有 4 個字節大小。 如果我使用調試每次運行一個字節的out.write(c)並且在每次打開文件 outagain.txt(使用記事本)后我依次看到:t-->te-->tes-->test。 好的,但是如果我們將源文件 (xanadu.txt) 的內容更改為等同於測試 (τέστ) 的希臘語(或其他語言),那么該文件現在有 8 個字節大小(我認為因為 UTF 我們每個字符有 2 個字節)。 再次調試時,每次out.write(c)運行時,它都會出現沒有意義的象形文字。 當最后一個字節(第 8 個)打印出來時,原始的希臘詞 (τέστ) 突然出現。 為什么? 如果我們選擇控制台 stream (in.netbeans) 作為目標,則相同,但在這種情況下,如果調試,奇怪的字符將保留在末尾,但如果我們正常運行它則不會(.!!)。

如您所見,單個字符(Java 內部表示中的 16 位)變成字節流表示中的可變字節數,特別是 UTF-8。

(有些字符占用兩個char值;我將忽略那些,但答案仍然適用,只是更多)

如果您像在實驗中那樣輸出“按字節”,在某些情況下,您將得到 output 一個小數字符。 這是一個沒有意義的非法序列; 盡管如此,某些軟件(例如記事本)仍會嘗試理解它。 這甚至可能包括猜測編碼。 例如,我不知道是這種情況,但如果文件的前幾個字節無效 UTF-8——我們知道你的半個字符 output 無效 UTF-8——記事本可能會猜測一種完全不同的編碼,它將字節序列視為完全不同字符的有效表示。

tl; dr - 垃圾輸出,垃圾顯示。

現代計算機有一張巨大的表,里面有 40 億個字符。 每個字符都由一個 32 位數字標識。 你能想到的角色都在這里; 從基本的“測試”到“τέστ”再到雪人 (☃),再到表示即將出現從右到左拼寫的單詞的特殊不可見連字,再到一堆連字(例如 ff - 這是一個單一的表示 ff 連字的字符)到表情符號、彩色和所有:。

整個答案本質上是這些 32 位數字的序列。 但是您想如何將這些存儲在文件中? 這就是“編碼”的用武之地。有很多很多編碼,一個關鍵問題是(幾乎)沒有任何編碼是“可檢測的”。

就像這樣:

如果一個完全陌生的人走到你面前說“嘿”? 他們說什么語言。 可能是英語,但也許是荷蘭語。 其中還有“嘿”。 也可能是日本人,他們甚至都不問候你? 他們說“是”(或多或少)。 你怎么知道的?

答案是,要么來自外部環境(如果您在英國紐卡斯爾中部,可能是英語),要么因為他們明確告訴您,但一個是外部環境,另一個不常見。

文本文件也是如此

它們只包含編碼的文本,它們不指示它是什么編碼。 這意味着您需要在保存該 txt 內容時告訴編輯器或您的newBufferedReader java,或您的瀏覽器,您想要什么編碼。 然而,因為每次都必須這樣做很煩人,所以大多數系統都有一個默認選擇。 一些文本編輯器甚至試圖弄清楚它是什么編碼,但就像那個人說“嘿”,對你來說可能是英語或日語,有着截然不同的解釋。 這種對字符集編碼的半智能猜測也會發生同樣的情況。

這讓我們得到以下解釋:

  1. 您在編輯器中τέστ並點擊“保存”。 你的編輯在做什么? 它保存在 UTF-16 中嗎? UTF-8? UCS-4? ISO-8859-7? 對於所有這些編碼,都會生成完全不同的文件,因為它有 8 個字節。 這意味着它是 UTF-16 或 UTF-8。可能是 UTF-8。

  2. 然后,您將這些字節一個一個地復制過來,這是有問題的:在 UTF-8 中,一個字節可以是一個字符的一半。 (你說:UTF-8 將字符存儲為 2 個字節;那不是真的,UTF-8 存儲字符時每個字符都是 1、2、3 或 4 個字節;它的每個字節長度可變,- τέστ 中的每個字符存儲為 2 個字節,雖然) - 這意味着如果你已經復制了 3 個字節:你的文本編輯器猜測它可能是什么的能力受到嚴重阻礙,它可能會猜測 UTF-8 但隨后意識到它根本無效 UTF-8 (因為你最后得到的半個字符),所以它猜錯了。 並向您展示官方文章。

這里要吸取的教訓是雙重的:

  1. 當你想處理字符時,使用charReaderWriterString和其他面向字符的東西。

  2. 當你想處理字節時,使用bytebyte[]InputStreamOutputStream和其他面向字節的東西。

  3. 永遠不要犯這兩者很容易互換的錯誤,因為它們不是。 每當您從一個“世界”到另一個“世界”的 go 時,您必須指定字符集編碼,因為如果沒有,java 會選擇您不想要的“平台默認值”(因為現在您的軟件依賴於外部因素並且不能被測試。哎呀)。

  4. 盡你所能默認為 UTF-8。

tl;博士

閱讀: 每個軟件開發人員絕對、絕對必須了解 Unicode 和字符集的絕對最低限度(沒有借口!)

不要按八位字節(字節)解析文本文件。 使用專門用於處理文本的類。 例如,使用Files及其readAllLines方法。

細節

請注意該教程頁面底部的警告,這不是處理文本文件的正確方法:

CopyBytes 看起來像一個普通的程序,但它實際上代表了一種您應該避免的低級 I/O。 由於 xanadu.txt 包含字符數據,因此最好的方法是使用字符流,這將在下一節中討論。

文本文件可能會也可能不會使用單個八位字節來表示單個字符,例如US-ASCII文件。 您的示例代碼假定每個字符一個八位字節,它適用於test作為內容,但不適用於τέστ作為內容。

作為程序員,您必須從數據文件的發布者那里知道在編寫代表原始文本的數據時使用了何種編碼。 一般寫文字最好使用UTF-8編碼。

用兩行寫一個文本文件:

測試 τέστ

...並使用編碼為UTF-8文本編輯器保存。

將該文件作為String對象的集合讀取。

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

運行時:

line = test
line = τέστ

UTF-16 與 UTF-8

你說:

我認為因為 UTF 我們每個字符有 2 個字節)

沒有“UTF”之類的東西。

  • UTF-16編碼對每個字符使用一對或多對八位字節。
  • UTF-8編碼每個字符使用 1、2、3 或 4 個八位字節。

可以使用 UTF-16 或 UTF-8 編碼將諸如τέστ之類的文本內容寫入文件。請注意, UTF-16“被認為是有害的” ,如今通常首選 UTF-8。 請注意,UTF-8 是 US-ASCII 的超集,因此任何 US-ASCII 文件也是 UTF-8 文件。

字符作為代碼點

如果您想對文本中的每個字符進行示例,請將它們視為代碼點編號。

切勿在 Java 中使用char類型。該類型甚至無法表示Unicode中定義的一半字符,現在已過時。

我們可以通過添加這兩行代碼來查詢上面示例文件中的每個字符。

IntStream codePoints = line.codePoints();
codePoints.forEach( System.out :: println );

像這樣:

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
        IntStream codePoints = line.codePoints();
        codePoints.forEach( System.out :: println );
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

運行時:

line = test
116
101
115
116
line = τέστ
964
941
963
964

如果您還不熟悉流,請將IntStream轉換為集合,例如Integer對象的List

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
        List < Integer > codePoints = line.codePoints().boxed().collect( Collectors.toList() );
        for ( Integer codePoint : codePoints )
        {
            System.out.println( "codePoint = " + codePoint );
        }
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

運行時:

line = test
codePoint = 116
codePoint = 101
codePoint = 115
codePoint = 116
line = τέστ
codePoint = 964
codePoint = 941
codePoint = 963
codePoint = 964

給定代碼點編號,我們可以確定預期的字符

字符串 s = Character.toString( 941 ); // 字符。

請注意,某些文本字符可能表示為多個代碼點,例如帶有變音符號的字母。 (文本處理不是一件簡單的事情。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM