引入編碼信息的一些實踐——亂碼探源(3)

前面說到,文本文件中沒有編碼信息,致使了各類混亂,那麼,最關鍵的就是要指定好所用的編碼信息。具體地講,有如下一些途徑。css

變相引入

什麼是變相引入呢?其實本質與前面提到的一些「文件頭」信息是相似的。html

xmljava

咱們來看看xml文件的例子,你一般能在最開始看到這樣的一行:python

<?xml version="1.0" encoding="UTF-8"?>

那麼這裏面,encoding指明的就是所用編碼的信息了。正則表達式

但是,等等!!爲了獲得這一編碼信息,我得先讀取這一文件;可要正確讀取文件,我又要先知道編碼信息!apache

這成了一個雞生蛋,蛋生雞,又或者說是先有雞仍是先有蛋的問題了。api

怎麼破呢?考慮這一行信息全部字符都是ASCII中的字符,那麼咱們能夠先使用最基礎的ASCII去讀取它開頭的一些信息,獲取到這一編碼信息後,再次用這一編碼去讀取文件便可。ruby

ASCII可謂是這樣一個始祖鳥或者始祖蛋同樣的存在。jsp

能夠動動手作些實驗,先創建一個xml文件,好比就叫foo.xmlmaven

image

內容以下:

<?xml version="1.0" encoding="UTF-8"?>
<foo>向我開炮</foo>

而後初步測試讀取編碼信息

package org.jcc.core.encode;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.junit.Test;

public class EncodingDetectTest {
    
    @Test
    public void testEncodingDetect() throws Exception {
        File foo = FileUtils.toFile(getClass().getResource("/foo.xml"));
        // 以ASCII方式讀取文件
        String content = FileUtils.readFileToString(foo, StandardCharsets.US_ASCII);
        
        // 匹配到首行,並用group方式抓取編碼的值
        Pattern headerPattern = Pattern.compile("<\\?xml[\\s\\S]*encoding=\"([^\"]*)\"\\?>");
        Matcher headerMatcher = headerPattern.matcher(content);
        
        assertThat(headerMatcher.find()).isTrue();
        assertThat(headerMatcher.group(1)).isEqualTo("UTF-8");
        
        // 匹配foo節點中的內容「向我開炮」
        Pattern fooPattern = Pattern.compile("<foo>([\\s\\S]*)</foo>");
        Matcher fooMatcher = fooPattern.matcher(content);
        
        assertThat(fooMatcher.find()).isTrue();
        // 四個UTF-8字符,每一個三字節,共12字節.
        // 因爲最高位都爲1,都不是有效的ASCII字節,最終被替換成了12個�(見亂碼探源2中的介紹)
        assertThat(fooMatcher.group(1)).isEqualTo("������������");
    }
}

注:僅爲演示用,就寫得比較粗糙了。好比直接就把所有內容讀取上來了,精細一點應該是讀取一行或者讀取到所需信息就好了。正則表達式也還能夠寫得更嚴謹些。

以後就能夠進一步測試了:

    @Test
    public void testRereadUsingDetectedEncoding() throws Exception {
        File foo = FileUtils.toFile(getClass().getResource("/foo.xml"));
        
        // 獲取xml中的編碼信息
        String encoding = getXmlEncoding(foo);
        
        // 使用檢測到的編碼再次讀取文件
        String content = FileUtils.readFileToString(foo, encoding);
        
        // 此次,內容正確了。
        assertThat(getTextInFooNode(content)).isEqualTo("向我開炮");
    }
    
    private String getXmlEncoding(File foo) throws Exception {
        String content = FileUtils.readFileToString(foo, StandardCharsets.US_ASCII);
        Pattern headerPattern = Pattern.compile("<\\?xml[\\s\\S]*encoding=\"([^\"]*)\"\\?>");
        Matcher headerMatcher = headerPattern.matcher(content);
        headerMatcher.find();
        return headerMatcher.group(1);
    }

    private String getTextInFooNode(String content) {
        Pattern fooPattern = Pattern.compile("<foo>([\\s\\S]*)</foo>");
        Matcher fooMatcher = fooPattern.matcher(content);
        fooMatcher.find();
        return fooMatcher.group(1);
    }

此次內容正常了,代表咱們的策略是可行的。

html,jsp

像Html文件也經常會這樣去引入一些編碼的信息,如在header裏常會包括如下元信息:

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

又或者是像這樣:

<meta charset="UTF-8">

智能一點的文本編輯器還會根據這一信息來做爲保存時的編碼。好比在Eclipse中,若是設定了用ISO-8859-1編碼,又同時錄入了中文,還會出現保存時的警告:

image

天然,像記事本這樣傻乎乎的編輯器就沒有這麼貼心了。這時,保持宣稱編碼與實際保存用的編碼一致就是源文件做者的責任了,不然可能不但沒有幫助還會誤導編輯器。

ruby,python

像ruby,python之類的語言有時會在文件頭加上以下聲明:

# -*- coding: utf-8 -*-

那麼,這也算是變相引入的編碼信息。天然,這須要相應的源碼編輯器及編譯(解釋)器的支持。

可是,像java這樣的語言彷佛沒有這樣的約定,那麼要怎樣才能儘量避免出錯呢?

外部指定

既然沒有編碼信息,又不打算用變相的方式指定,那麼靠譜的方式就是外部顯式指定了。

假如沒有編碼信息?

假如咱們用UTF-8編碼一個java源文件:

image

而後在cmd下用javac命令手動編譯並執行:

image

咱們發現亂碼了,沒有輸出「你好」,而是三個怪字。緣由實際上就是javac編譯器用了缺省的編碼,在Windows平臺,也就是GBK去讀取源文件。

「你好」兩個字按UTF-8一個字三個字節,總共6個字節,而按GBK去解碼,則兩字節一個字,最終成爲三個字。

注:也即生成的class文件就已是有缺陷的了。

明確引入編碼參數

糾正的方法也很簡單,就是在編譯時顯式指定所用的編碼:

javac -encoding utf-8 Foo.java

在加了「encoding」參數後再編譯,就能正確的讀取源文件從而生成正確的class文件,

注:若是你觀察一下新生成的class文件,會比原來的小3個字節。這與class文件中所使用的「modified UTF-8」編碼方式有關,可參考前面亂碼探源1中的介紹。

再次執行,就正常了:

image

在工程中指定

每次編譯時都要去指定這一編碼是件很繁瑣的事,一般是對一整個工程在一開始就設置一個明確的編碼。

好比對於一個Eclipse下的工程,咱們能夠在工程屬性裏指定一個編碼,好比用UTF-8:

image

這樣以後,新建的文本文件如各類源代碼文件都會使用這一編碼。而當要編譯時,也會使用這一編碼去讀取源文件。

固然,若是咱們從外部引入一些文件,編碼是不會自動轉換的。

好比引入一些css文件,話說天下css一大抄,你多是從某網站直接抓取來的,而不少網站因爲歷史等緣由可能仍是用GBK等編碼。

這時你須要手工轉換一下編碼,或者用一些批量轉換的工具(若是數量不少的話)

手工轉的話,好比能夠在記事本中先正確打開它,再拷貝到工程中的一個新建文件再保存。

注:編輯時,內容在內存中都是轉換成了統一的編碼(在Windows下,就是UTF-16),因此不一樣編碼的文件間互相拷貝也是OK的,只是保存時纔再次轉換成相應的編碼。

在構建文件中指定

也能夠在構建文件中指定源文件的編碼,好比java中用maven時能夠這樣指定:

<project>
	// ...
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	// ...
</project>

若是你用的是ant或者gradle之類的,也可自行查查文檔要如何設置。至於其它語言平臺的構建平臺,如grunt,make之類,讀者可自行去了解。

總而言之,越是明確地設置了編碼,才越能避免混亂的出現。

在下一篇,咱們再談下在內存中的編碼及相關的string,字節流及字符流的話題。

相關文章
相關標籤/搜索