S-JIS[2009-01-31/2009-02-21] 変更履歴

HtHtmlLexer

ひしだま作の、JavaHTMLの解釈を行うクラスです。

htlexer.jar (144.0kB) [/2009-02-21] JDK1.6以降(htlexタスクも含む)
htlexer.src.zip (109.4kB) [/2009-02-21] ←ソースはEclipseで添付できる
Javadoc   [/2009-02-21]  

HtHtmlLexerの特徴


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はスクリプトの解釈をするものではないので、スクリプトの文法には無頓着)
 

HtHtmlLexer使用例

HTMLファイルを読み込んでコンソールに出力する例

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では、開始タグと終了タグをペアとして認識しない為)

タグ名を変更するAntタスク


HtHtmlParser

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を使う機能を追加。

自作ソフトへ戻る
メールの送信先:ひしだま