S-JIS[2009-01-31/2009-02-21] 変更履歴
htlexer.jar | (144.0kB) | [/2009-02-21] | JDK1.6以降(htlexタスクも含む) |
htlexer.src.zip | (109.4kB) | [/2009-02-21] | ←ソースはEclipseで添付できる |
Javadoc | [/2009-02-21] |
|
HtHtmlLexerは、HTMLファイル(HTMLの文字列)を読み込んで解析を行います。
例えば以下のようなHTMLは、
<head> <title>タイトル</title> </head>
普通は字句解析は、「<
:タグ開き」「head
:文字列」「>
:タグ閉じ」「<
」「title
」「>」「タイトル
:文字列」「</
」「title
」「>
」「</
」「head
」「>
」といった単位(トークン)に分解します。
構文解析すると、head要素
の中にtitle要素
があり、title要素
の中には「タイトル
」というテキストがある…という様に、HTMLの要素の木構造が出来ます。
HtHtmlLexerでは、「<head>
:タグ」「<title>
:タグ」「タイトル
:テキスト」「</title>
:タグ」「</head>
:タグ」という単位が順番に並んでいるリストが得られます。
すなわち、字句解析よりは上位の解析を行っていますが、構文解析までは至っていません。それに該当する用語を知らないので、HtHtmlLexerは「Lexer(字句解析)」を名乗っています。
また、それに合わせて、「<head>
」「<title>
」「タイトル
:テキスト」「</title>
」「</head>
」のことをトークンと呼んでいます。(本来なら「要素」と言うべき?)
HtHtmlLexerでは、タグやタグの属性も解釈対象ですが、厳密に文法に沿った解釈はしません。
自分が「こう解釈したい」と思っている文法になっています(爆)
また、構文解析を行うのではないので、タグの並び順については無頓着です。入れ子になっていないタグや、開始タグの無い終了タグも別に問題ありません。
<strong>これは<em>どう解釈すれば</strong>よい?</em> ←入れ子になっていない
</br> ←開始タグの無い終了タグ
</a href="zzz"> ←終了タグに属性が入っている
→解析仕様
HtHtmlLexerで解釈した結果をそのままHTMLファイル(文字列)として出力すると、入力と全く同じ内容が得られます。
改行の数やタブ(インデント)の変更もありませんし、もちろん“本来あるはずのタグ”(例えば<p>に対する</p>)を補填したりしません。
たとえ解釈を誤っていたとしても、出力は全く同じになるはずです。
世の中にHTMLパーサー(構文解析)はいっぱいある訳ですが、自分で作ろうと思った理由がこれ、すなわち「全く同じ内容が得られること」です。
HTMLパーサーはタグを補填してくれたりして高機能なんですが、たいていはHTMLファイルに出力すると元の形とだいぶ変わってしまうので、それが嫌だったのです。
ちなみに「HtHtmlLexer」の先頭の「Ht」は「ひしだまがてきとーに作った」という意味です(爆)
HtHtmlLexerは概ねHTMLの文法に従おうとしていますが、そうでない部分もあります。
例 | 一般?解釈 | HtHtmlLexerによる解釈 | 更新日 |
---|---|---|---|
</tag> |
終了タグ | 開始タグと終了タグは区別しない。 左記の例は、“タグ開きが「 </ 」である”タグとなる。 |
|
<tag ////> |
IE6だと「< :タグ開き」「//// :属性名」「> :タグ閉じ」と解釈される模様。また、XMLだと不正な文法としてエラー? |
「< :タグ開き」「/// :属性名」「/> :タグ閉じ」と解釈する。 |
|
<input type="checkbox" checked> |
「checked 」は、属性値が省略されているのではなく、属性名が省略されている(らしい)。 |
属性名が「checked 」で、属性値が省略されているものとして扱う。 |
|
<!--本来は---ダメ--> |
コメントは「--」の後に「>」以外が来るとダメらしい。 | 「-->」が来るまでどんな文字列もコメントとして扱う。 | 2009-02-08 |
<script> 〜</script> |
スクリプト | HtHtmlLexerは基本的にタグの種類や開始・終了は気にしないが、scriptタグだけは認識する。 すなわち、script要素が終了するまでの間は、コメント「<!-- -->」とCDATAセクション以外は全てテキストとして扱う。 その間のダブルクォーテーションやシングルクォーテーションは文字列を囲むための引用符として認識する。 (つまりJavaScriptにおける文字列指定。その中に「</script>」があってもスクリプトの終了とは認識されない) (HtHtmlLexerはスクリプトの解釈をするものではないので、スクリプトの文法には無頓着) |
import java.io.*; import jp.hishidama.html.lexer.rule.HtLexer; import jp.hishidama.html.lexer.token.ListToken; import jp.hishidama.html.lexer.token.Token;
public class Sample1 { public static void printHtmlFile(File f) throws IOException { ListToken list = readHtmlFile(f); for (Token t : list) { // トークンの一覧を順次処理 String s = t.getText(); System.out.print(s); } // System.out.print(list.getText()); でも同じ } public static ListToken readHtmlFile(File f) throws IOException { Reader reader = new InputStreamReader(new FileInputStream(f), "MS932"); HtLexer lexer = new HtLexer(); // まずインスタンス生成 lexer.setTarget(reader); // 読み込むHTMLファイルをセット ListToken list; try { list = lexer.parse(); // 解釈実行 } finally { lexer.close(); // Readerをクローズ } return list; } }
HtLexer#parse()で返ってきた直後の結果(トークン)には、行番号が入っていません。[2009-02-03]
Token#calcLine()を呼び出すと、各トークンに行番号が振られます。
calcLine()で計算した後では、getLine()で、そのトークンの始まる行番号が得られます。
public static void printLine(ListToken list) { list.calcLine(1); // 行番号を1から振る。 // 一番最初のタグの行番号 Token first = list.get(0); System.out.println(first.getLine()); // 一番最後のタグの行番号 Token last = list.getLast(); System.out.println(last.getLine()); }
import jp.hishidama.html.lexer.token.Tag;
public static void changeTagName(ListToken list) { for (Token t : list) { if (t instanceof Tag) { Tag tag = (Tag) t; String name = tag.getName(); // タグ名取得 if ("b".equalsIgnoreCase(name)) { tag.setName("strong"); // タグ名設定 } } } }
bタグをstrongタグに変える。
HtHtmlLexerでは開始タグ・終了タグの区別はしないので、「<b>」でも「</b>」でも「<b
attr="zzz">」でも、「b」という名のタグは全て「strong」に変える。
(逆に、“fontタグのうち、<font size="-1">〜</font>のペアだけを変えたい”というような事はすぐには出来ない。HtHtmlLexerでは、開始タグと終了タグをペアとして認識しない為)
HtHtmlLexerで取得したトークン一覧を構文解析してタグ(要素)のツリーを作成します。[2009-02-14]
ただ、構文解析は難しいので、高度・汎用的なことは全く行っていません。不正なタグの補正なんて無理(爆)
import java.io.*; import jp.hishidama.html.lexer.rule.HtLexer; import jp.hishidama.html.lexer.token.ListToken; import jp.hishidama.html.parser.elem.HtElementUtil; import jp.hishidama.html.parser.elem.HtListElement; import jp.hishidama.html.parser.rule.HtParser; import jp.hishidama.html.parser.rule.HtParserManager;
public static void printParseHtml(File f) throws IOException { ListToken tlist = readHtmlFile(f); // HtHtmlLexerによるファイル読み込み tlist.calcLine(1); HtParser parser = new HtParserManager().getDefaultParser(); HtListElement elist = parser.parse(tlist); HtElementUtil.dumpTree(elist, -1); }
HtElementUtil#dumpTree()は、デバッグ出力用を行うメソッドです^^;
以下のような出力になります。
MarkDeclare[1]true: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> [2]true <head> [4]true <meta> [5]true (/meta) [5]true 〜 <title> [8]true TextToken[8]true: ひしだまのホームページ(Hishidama's HomePage) </title> [8]true <link> [9]true (/link) [9]true <style> [10]true Comment[11]true: <!-- span.chg { } --> </style> [14]true </head> [15]true <body> [17]true 〜 <p> [23]true <img> [23]true (/img) [23]true </p> [23]true 〜 </body> [61]true </html> [62]true
角括弧[ ]内はファイル内の行番号です。
その直後のtrueは、解析によって確定していることを意味します。
falseの場合、解析が確定しなかった事を意味します。マッチするタグが見つからなかった場合や、内包しているタグが未確定の場合にfalseになります。
(↑この辺りが「デバッグ用」。とりあえず自分のページで試したら、falseが出たのは開始タグと終了タグがマッチしていないケースばかりでした)
<tag>や</tag>はファイル内にあったタグで、(tag)や(/tag)は暗黙の開始・終了を意味します。要するに<tag>〜(/tag)の場合、開始タグしか無いことを表しています。
更新日 | 変更内容 |
---|---|
2009-01-31 | とりあえず公開。 |
2009-02-01 | htlexタスク系を色々ちょっと修正。 |
2009-02-06 | タグとテキストの前にスキップ文字(スペース・タブ・改行)のみの解釈を行うよう修正。 タグとテキストの間にスキップ文字があった場合、今まではテキストの一部だったが、SkipTokenが新たに入るようになった。 |
2009-02-08 | トークンのclone()メソッドを実装。 HTMLエスケープの実体参照にnbsp・copy・regを追加。 htlexタスクでバックアップディレクトリーの指定を追加。 |
2009-02-14 | HtHtmlParserをとりあえず公開。 |
2009-02-21 | htlexタスクにHtHtmlParserを使う機能を追加。 |