本家「@IT」にはない内容をエンジニアライフで技術紹介するコラム。広く議論する場になることを目指します。

第043回_EncodingDecl3

»
前回のコラムで文字符号化方式の自動検知について仕様と処理を整理しました。ただし、字句解析器より前にエンコーディング宣言まで読むための仕様の整理がまだできていません。今回は、エンコーディング宣言までを読むための仕様の整理実装の紹介(前半)をします。

エンコーディング宣言までを読む

(エンコーディングファミリまでは決まっていますが)文字符号化方式が不明なままエンコーディング宣言を読む方法について考えます。まずは、文書実体について考えます。

文書実体
文書実体でEncName(エンコーディング名を示す識別子)までの構文は次のようになっています。
[1]document::=( prolog element Misc* ) − ( Char* RestrictedChar Char* )
[3]S::=(#x20 | #x9 | #xD | #xA)+
[22] prolog::=XMLDecl Misc* (doctypedecl Misc*)?
[23]XMLDecl::='<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>'
[24]VersionInfo::=S 'version' Eq ("'" VersionNum "'" | '"' VersionNum '"')
[25]Eq::=S? '=' S?
[26]VersionNum::='1.1'
[80]EncodingDecl::=S 'encoding' Eq ('"' EncName '"' | "'" EncName "'" )
[81]EncName::=[A−Za−z] ([A−Za−z0−9._] | '−')*
ですから、
'<?xml' (#x20 | #x9 | #xD | #xA)+ 'version' (#x20 | #x9 | #xD | #xA)*
'=' (#x20 | #x9 | #xD | #xA)* ("'" '1.1' "'" | '"' '1.1' '"')
(#x20 | #x9 | #xD | #xA)+ 'encoding' (#x20 | #x9 | #xD | #xA)*
'=' (#x20 | #x9 | #xD | #xA)*
('"'[A-Za-z] ([A-Za-z0-9._] | '–')* '"' | "'" [A-Za-z] ([A-Za-z0-9._] | '–')* "'" )
に展開できます。

ここで
'<?xml' , 'version' , '=' , "'1.1'" , '"1.1"' , 'encoding' , '=' , '"' , "'"
は固定バイトとして読み込んで解釈することができます。
一方で
 (#x20 | #x9 | #xD | #xA)+
 (#x20 | #x9 | #xD | #xA)*
 [A-Za-z] ([A-Za-z0-9._] | '-'*
は任意の文字数ですから、先頭のBOMを4バイト固定で読み込むように固定的に読み取ることはできません。

つまるところ、固定位置にEncNameが現れないので、何等かの方法で先頭から字句・構文を解析してEncNameを抽出しなければなりません。抽出方法はいくつか考えられますが--文字符号化方式が確定していない状況なので--バイナリ抽出で行える方法を採用するために、「encodingの後ろの"または'で囲まれた値を取り出す」方法を検討してみます。

encodingの後ろの"または'で囲まれた値を取り出す方法の検討
-リテラルの探索
リテラルの開始点の"か'を探した後、末尾まで探してリテラルとします。ただし、VersionInfoにもリテラルがあるので1つ目のリテラルは無視し、2つ目のリテラルを探索します。ここでEncodingDeclは省略可能なため、2つ目のリテラルはSDDeclかそれともその先の他の何かかもしれません。
# SDDeclも省略可能なため2つ目のリテラルはSDDeclでないかもしれません。

SDDeclでもないとすると面倒なので、2つ目のリテラルを探すまえに >が見つかった場合は発見できずとします。またSDDeclのリテラルを抽出した場合はyesかnoですからあまり解析せずに判定できると思います。
# EncNameとしてyes/noを指定することは仕様上可能ですが
# 意味ある文字符号化方式でないので無視して良いでしょう。
# またSDDeclのリテラルにyes/no以外の値を与えるとEncName
# として間違って取得しますが、その場合は
# 実際の構文解析でエラーになるでしょう

-エンコーディング決定 処理シーケンス
1.リテラルを取得する(1回目)
1-1.リテラルが取得できなかった場合
1-1-1.デフォルトエンコーディングに決定する # version="1.0"の場合はXMLDeclを省略できるのでこのパターンもありうる
1-2.リテラルが取得できた場合 #1回目はVersionInfoなので無視する
→処理2へ
2.リテラルを取得する(2回目)
2-1.リテラルが取得できなかった場合
2-1-1.ファミリ毎のデフォルトエンコーディングに決定する
2-2.リテラルが取得できた場合
2-2-1.ファミリ毎のリテラルを元にしたエンコーディングに決定する。処理3へ
3.リテラルを元にしたエンコーディングの決定
3-1.リテラルがSDDeclのリテラルの場合(yesまたはnoの場合)
3-1-1.デフォルトエンコーディングに決定する
3-2.リテラルがSDDeclのリテラルでない場合
3-2-1.エンコーディングファミリのいずれかと一致するか判定する
3-2-1-1.一致する場合:一致したものを文字符号化方式に設定する
3-2-1-2.一致しない場合: 処理NG

-リテラル取得 処理シーケンス
1.先頭を発見できた場合
1-1.リテラルの末尾を探索する
1-1-1.末尾を発見できた場合
1-1-1-1.処理OK
1-1-2.末尾を発見できなかった場合
1-1-2-1.処理NG
2.先頭を発見できなかった場合
2-1.処理NG

-実装のためのbyteコード
byteコードについてまとめておきます。

文字符号化方式:" :' :yes :no :> :EncName
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
UTF-8 :0x22 :0x27 :0x79 0x65 0x73 :0x6E 0x6F :0x3E :0x55 0x54 0x46 0x2D 0x38
UTF-16BE :0x00 0x22:0x00 0x27:0x00 0x79 0x00 0x65 0x00 0x73:0x00 0x6E 0x00 0x6F:0x00 0x3E:0x00 0x55 0x00 0x54 0x00 0x46 0x00 0x2D 0x00 0x31 0x00 0x36
UTF-16LE :0x22 0x00:0x27 0x00:0x79 0x00 0x65 0x00 0x73 0x00:0x6E 0x00 0x6F 0x00:0x3E 0x00:0x55 0x00 0x54 0x00 0x46 0x00 0x2D 0x00 0x31 0x00 0x36 0x00

文字符号化方式の自動検知機能の実装1:処理シーケンス

さて、これまでの議論を経て実装コードを紹介していきます。

文字符号化方式の自動検知器はEncodingDetectorとします。EncodingDetectorの呼び出し元となるLexerも修正します。

Lexerの修正

public class Lexer
{
    ・・・・
    
    //修正
    public void analyze(String path)
    {
        //■追加
        //文字符号化方式を判定する
        EncodingDetector detector = new EncodingDetector(path);
        AbstractEncoding encoding = detector.guess();
        
        if (encoding.isError())
        {
            m_hasError = true;
        }
        else
        {
            //入力ストリームを作成する
            BufferedReader br = encoding.getBR(path);
            
            //Documentトークナイザを作成する
            DocumentTokenizer dt = new DocumentTokenizer(path, br);

            //ファイルの内容をトークン化する
            dt.tokenize();

            //もしエラーがあるならメッセージを出力する
            dt.putMessageIfError();

            m_hasError = dt.hasError();

            //作成したトーク列を取り出す
            DocumentToken token = dt.getToken();

            //トークン管理に設定する
            m_tokenManager.setDocumentToken(token);
        }
    }
    
    ・・・・
}

EncodingDetectorの修正
先頭の4バイトを取得してエンコーディングファミリを決定する処理は内部クラスのSupportedEncoding#identifyEncodingFamilyで行います。また、エンコーディングファミリから文字符号化方式を決定する処理は、SupportedEncoding#identifyEncodingで行います。

public class EncodingDetector
{
    //内部クラス
    private static class SupportedEncoding
    {
        private SupportedEncodingFamilies m_supportedEncodingFamilies 
                            = SupportedEncodingFamilies.getInstance();

        private FileInputStream        m_input;
        private AbstractEncodingFamily m_family;
        
        public SupportedEncoding(FileInputStream input)
        {
            m_input = input;
            m_family = null;
        }
        
        public void identifyEncodingFamily()
        {
            byte[] buf = new byte[]{0,0,0,0};
            
            try
            {
                //4byte読み取る
                m_input.read(buf);
            }
            catch (IOException e)
            {
                //何もしない
            }
            
            //サポートする文字符号化方式のファミリに一致するものを探す
            //読み込めなかった場合は 0 0 0 0 を与える
            m_family = m_supportedEncodingFamilies.match(buf);
        }
        
        public AbstractEncoding identifyEncoding()
        {
            AbstractEncoding retval = null;
        
            //リテラル探索が必要な場合
            if (m_family.needLiteralSeaching())
            {
                //第1リテラルを取得する
                byte[] literal1 = m_family.getLiteral(m_input);
                
                //第1リテラルを取得できなかった場合
                if (literal1 == null)
                {
                    //エラーを設定する
                    ErrorEncoding error = new ErrorEncoding();
                    error.setNoVersionNumLiteral();
                    retval = error;
                }
                //第1リテラルを取得できた場合
                else
                {
                    //第2リテラルを取得する
                    byte[] literal2 = m_family.getLiteral(m_input);
                    
                    //第2リテラルを取得できなかった場合
                    if (literal2 == null)
                    {
                        //未指定なのでデフォルトの文字符号化方式を
                        //ファミリから取得する
                        retval = m_family.getDefaultEncoding();
                    }
                    //第2リテラルを取得できた場合
                    else
                    {
                        //取得したリテラルでファミリ内から
                        //該当する文字符号化方式を取得する
                        retval = m_family.getEncoding(literal2);
                    }
                }
            }
            //リテラル探索が不要な場合
            else
            {
                //デフォルトの文字符号化方式を設定する
                retval = m_family.getDefaultEncoding();
            }
            
            return retval;
        }
    }

    private final String m_path;
    

    public EncodingDetector(String path)
    {
        m_path = path;
    }

    public AbstractEncoding guess()
    {
        AbstractEncoding retval = null;
        
        try
        {
            FileInputStream input = new FileInputStream(m_path);

            SupportedEncoding se = new SupportedEncoding(input);
            
            //先頭の4バイトを使って文字符号化方式のファミリ
            //(同一の先頭バイト列を持つ異なる文字符号化方式)を分類する
            se.identifyEncodingFamily();
            
            //エンコーディング宣言を使ってファミリの中から1つを選択する
            //(ファミリが現在1つの場合もチェックする)
            retval = se.identifyEncoding();
        }
        catch (FileNotFoundException e)
        {
            //エラーを設定する
            ErrorEncoding error = new ErrorEncoding();
            error.setFileNotException();
            retval = error;
        }

        return retval;
    }

}

SupportedEncodingFamiliesの実装
サポートするエンコーディングファミリを統括するクラスとしてSuppotedEncodingFamiliesを用意します。実装は次のようにして、EncodingDetector.SupportedEncoding#identifyEncodingFamilyで呼び出します。

public class SupportedEncodingFamilies
{
    private static final SupportedEncodingFamilies
              m_instance = new SupportedEncodingFamilies();

    public static SupportedEncodingFamilies getInstance()
    {
        return m_instance;
    }
    
    private final UnknownEncodingFamily m_unkwnon
                  = new UnknownEncodingFamily();
    private final ArrayList<AbstractEncodingFamily>
              m_familyList = new ArrayList<AbstractEncodingFamily>();

    private SupportedEncodingFamilies()
    {
        m_familyList.add(new Utf_8WithBomFamily());
        m_familyList.add(new Utf_8WithoutBomFamily());
        m_familyList.add(new Utf_16BEWithBomFamily());
        m_familyList.add(new Utf_16LEWithBomFamily());
    }

    public AbstractEncodingFamily match(byte[] buf)
    {
        AbstractEncodingFamily retval = null;
        
        for(AbstractEncodingFamily family : m_familyList)
        {
            boolean b = family.startMatch(buf);
            
            if (b)
            {
                retval = family;
                break;
            }
            else
            {
                //何もしない
            }
        }
        
        //どれにもマッチしなかった場合
        if (retval == null)
        {
            retval = m_unkwnon; 
        }
        //いずれかにマッチした場合
        else
        {
            //何もしない
        }
        
        return retval;
    }
}
長くなってしまったので続きは次回とします。
Comment(0)

コメント

コメントを投稿する