連載
» 2003年07月23日 00時00分 UPDATE

いまから始めるJava(7):制御文を使いこなす

[平井玄,@IT]

 前回「プログラムの制御構造を理解しよう」までで、Javaではif、switchなどの制御文を使って制御構造を記述できることを学びました。簡単におさらいすると、制御構造とはプログラムを「順番に実行する」「条件が合致すれば実行する」「繰り返して実行する」という3つのパターンのことであり、制御文とは制御構造を実際にプログラムとして記述するときに使うif、switchなどの文のことでした。

制御文を使いこなす

 制御文まで学習すると、ソースコードを読んでおおよその流れや何をしようとしているプログラムなのかは理解できるようになります。本来ならここでクラスについての理解をさらに深めたいところです。しかし、今回はJavaでアルゴリズムを記述して、制御文の使い方にさらに慣れることにします。というのも、今後Javaのクラスの継承について説明するうえで、今回作成するHTML文書を扱うクラスを題材にしたいからです。

 制御文を使うと、プログラム言語でアルゴリズムを表現できるようになります。アルゴリズムは日本語で「解法」「算法」といい、何かの問題を論理的に解くときの「やり方」や「方法」のことです。プログラムを書くということは、コンピュータに何かの処理をさせるアルゴリズムをプログラム言語で記述することです。今回はHTMLを解析して、HTML文書に含まれるテキストだけを表示するプログラムを紹介することにします。

HTMLパーサに挑戦

 Javaの説明に入る前に、Webブラウザの技術について簡単に確認しておきます。Webブラウザは、Webページ本体を記述するHTMLを解釈し、画像などの要素をダウンロードして画面に表示しています。Webブラウザの機能のうち、HTMLを解釈する部分を「HTMLパーサ」、画面として描画する部分を「HTMLレンダラ」と呼びます。「パーサ」はparse(構文を解析する)+erのことで、HTMLに含まれるタグとテキストを分離し、タグを解析することでHTML文書全体をWebページとして解釈しています。

 本物のWebブラウザで使われているHTMLパーサは、文法エラーへの対処やスクリプトの解釈など、非常に複雑なものになっていて、とてもJava入門で扱いきれるものではなくなっています。しかし、HTMLを解析してタグとテキストを分離する、というHTMLパーサの最も根本的な機能をどのように実現するかを学ぶことは、今後Javaで本格的なプログラムを記述しようという方にとって、参考になる点がたくさんあるはずです。

「状態」で考える

 「HTMLを解析するプログラムを作れ」といわれても、いったいどこから手をつけていいのか途方に暮れてしまうかもしれません。まずは解析される側のHTMLを眺めてみましょう。

<html><head><title>タイトル</title></head><body><p>段落</p></body></html>

 HTML文書はテキストの一種であり、「<」「>」で囲まれたタグと、タグ以外のテキストという2つの部分からできています。HTMLの仕様により、テキスト中の「<」「>」はそれぞれ「<」「>」という文字列に置き換えられているはずです。実際のHTMLではスクリプトの中に「<」「>」が含まれることがありますが、今回はそういう細かい話は抜きにします。

 ここで第5回「メソッドとコンストラクタ」で説明したCPUの動作について思い出してください。CPUはメモリ内に格納されたコードを順番に読み込み、足し算モードになったり掛け算モードになったりして、計算を処理していくのでした。例えば「足し算」を意味するコードを読み込んだら、足し算モードになり、足す数と足される数という2つのデータをメモリから読み込みます。第5回では「モード」という言葉を使いましたが、一般には「状態」という言葉を使います。CPUは、コードを順に読み込んでいき、特定のコードが読み込まれたら内部の状態を変化させて処理を切り替える、ということを電源がオンになってからオフになるまでずっとやり続けている装置のことです。

 このCPUの動作原理は、アルゴリズムを考えるときにも有効です。もう一度上記のHTMLを見てみましょう。CPUがコードを読むようにHTML文書を先頭から1文字ずつ読み込んでいき、「<」や「>」を読み込んだら状態を切り替える、という方法を使えば、タグとテキストを分離するHTMLパーサを実現できそうです。実際の動作を追いかけてみます。

 HTML文書の1文字目は「<」です。「<」はHTMLでタグの開始を意味する文字です。そこでHTMLパーサの状態を「タグ読み込み中」にします。2〜5文字目(h、t、m、l)はただの文字ですので状態は変化しません。6文字目は「>」です。現在のHTMLパーサの状態は「タグ読み込み中」ですので、「>」はタグの終了を意味します。そこで、HTMLパーサの状態を「テキスト読み込み中」に切り替えます。7文字目は「<」です。現在のHTMLパーサの状態は「テキスト読み込み中」ですので、「<」はタグの開始を意味します。HTMLパーサの状態はいったん「テキスト読み込み中」になりましたが、実際にはテキストは何もなかったことになります。同様に<head><title>も読み込んでいき、「<title>」の最後の「>」を読み終えたとしましょう。この時点でHTMLパーサの状態は「テキスト読み込み中」です。20文字目の「タ」を読み込んだ時点で、HTMLパーサは初めて本物のテキストに遭遇したことになります。その後21〜23文字目(イ、ト、ル)はただの文字ですので状態は変化せず、テキストとして読み取られます。24文字目の「<」を読み込むとHTMLパーサの状態は「タグ読み込み中」になります。

 以上のように、HTML文書を先頭から順番に読み込んでいき、「<」と「>」を読み込んだときに「タグ読み込み中」と「テキスト読み込み中」の2つの状態を切り替えれば、HTMLパーサを実現できそうです。

 ここまでで、これから作ろうとしているHTMLパーサのアルゴリズムの基本的な考え方が分かりました。では、このアルゴリズムをJavaで記述するにはどうしたらいいのでしょうか。

制御文とStrings型のメソッド

 「HTML文書を先頭から読んでいく」という処理は、文字列のn番目の文字を読むという処理を1文字目から文字列の長さ分、繰り返す処理です。繰り返しの制御文にはfor、whileがありますが、この場合はfor文を使うのが適切でしょう。

   文字列を1文字ずつ順番に表示するプログラム
public class PrintEachChars {
  public static void main( String args[] ) {
    String source = "<html><head><title>タイトル</title></head><body><p>段落</p></body></html>";
    int pos;
    for ( pos = 0; pos < source.length(); pos++ )
     System.out.println(source.charAt(pos));
  }
}

 HTML文書は任意のファイル名やURLを指定できるようにしたいところですが、この連載ではまだファイル入出力について説明していません。今回はString型の変数にHTML文書を格納することにしました。

 String型はJavaが用意している基本的なクラスで、文字列を格納できるだけではなく、文字列を操作するための手段がクラスのメソッドという形で用意されています。上記の「source.length()」はString型の変数sourceに格納された文字列の長さを取得するものです。sourceの内容は変化しませんので、この例の場合は必ず67が返ってきます。前回説明したように、for文は「for (式1; 条件; 式2)」という形式の制御文です。上記の場合、for文が実行された直後に変数posに0が代入され、posが変数sourceに格納された文字列の長さよりも小さければ処理が実行されます。処理を繰り返す前に、posの値を1増やし、posが変数sourceに格納された文字列の長さよりも小さければ処理が実行されます。この動作を67回繰り返すことで、sourceに格納された文字を先頭から調べられるわけです。

 sourceに格納された文字を実際に取り出すには、charAt()というメソッドを使います。1文字目を調べるときはcharAt(0)、2文字目を調べるときはcharAt(1)というように、最初の文字を0文字目と考えることに注意してください。

 このプログラムを実行すると、以下のようになります。sourceに格納された文字列が先頭から1文字ずつ表示されます。

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>javac PrintEachChars.java

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>java PrintEachChars
<
h
t

(略)

m
l
>

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>

 文字列を先頭から順に読み込む方法が分かったので、次に「状態」を表す変数を導入してみましょう。今回は状態が2つしかないので、「タグ読み込み中」状態のときにtrueになるboolean型の変数processingTagを使います。

   HTMLを読み込んで、タグとテキストの状態を切り替えるプログラム
public class ProcessHTML {
  public static void main( String args[] ) {
    boolean processingTag = false;
    String source = "<html><head><title>タイトル</title></head><body><p>段落</p></body></html>";
    int pos;

    for ( pos = 0; pos < source.length(); pos++ ) {
      // タグを解析中のときの処理
      if (processingTag) {
        if ( source.charAt(pos) != '>' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '>' )
              break;
            }
          }
        }
      }
      // テキストを解析中のときの処理
      else {
        if ( source.charAt(pos) != '<' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '<' )
              break;
            }
          }
        }
      }
      processingTag = !processingTag;
    }
  }
}

 プログラムが一気に複雑になった印象を受けるかもしれませんが、やっていることは非常に単純です。まず、HTMLパーサの状態を管理する変数processingTagはfalseに初期化されます。つまり、このHTMLパーサは「テキスト読み込み中」状態でHTML文書を解析し始めることになります。初期状態が「テキスト読み込み中」ですので、1文字目が「<」であれば「タグ読み込み中」に切り替わり、「<」でなければテキストとして読み込みます。このアルゴリズムでは、「テキスト読み込み中」に「<」を読み込んだときにタグが開始した、と考えることにしています。従って、初期状態を「タグ読み込み中」にしてしまうと、1文字目が「<」だとしてもタグの開始を検出できないことになります。初期状態は「テキスト読み込み中」である必要があるのです。

 最初のfor文内の処理は読みにくいので、for文内の処理を骨格だけ抜き出して説明します。

for ( pos = 0; pos < source.length(); pos++ ) {
  if (processingTag) {
    // タグを解析中のときの処理
  }
  else {
    // テキストを解析中のときの処理
  }
  processingTag = !processingTag;
}

 複雑に見えたかもしれませんが、こうしてみると非常に単純であることが分かります。要は、変数sourceに格納された文字列を先頭から1文字ずつ読み込んでいき、現在の状態を変数processingTagで調べて、タグを解析中のときの処理とテキストを解析中のときの処理に分岐させているのです。

 それぞれの処理の内容も、抜き出してみれば単純です。まず、タグを解析中のときの処理を見てみましょう。

if (processingTag) {
  if ( source.charAt(pos) != '>' ) {
    for ( pos++; pos < source.length(); pos++ ) {
      if ( source.charAt(pos) == '>' )
        break;
    }
  }
}

 現在の状態が「タグ読み込み中」であるとき、まずは現在の位置の文字が「>」かどうか調べています。processingTagがtrueになるのは「<」を検出したときですから、その次の文字が「>」というのは「<>」という文字の組み合わせのときです。「<>」は空のタグですので、タグとして読み取る必要があるかどうか調べていることになります。次に、タグの終了を示す「>」を探すために、もう一度for文を使って文字を検査しています。「>」を見つけるとbreak文によって繰り返し処理を中断します。

else {
  if ( source.charAt(pos) != '<' ) {
    for ( pos++; pos < source.length(); pos++ ) {
      if ( source.charAt(pos) == '<' )
        break;
    }
  }
}

 現在の状態が「テキスト読み込み中」であるときの処理は、「タグ読み込み中」の処理と大差ありません。違いは、「タグ読み込み中」の処理とは逆にタグの開始を示す「<」を検査していることだけです。

 プログラムの最後にある「processingTag = !processingTag;」は何をしているのでしょうか? この文は、「テキスト読み込み中」と「タグ読み込み中」のいずれかの状態の処理が終わった後で必ず実行されます。処理が終わった後というのは、タグの開始や終了を見つけたということです。「!」はJavaの演算子の一種で否定のことです。つまり「processingTag = !processingTag;」は、processingTagの値を反転(trueならfalseに、falseならtrueに)させることで、状態を管理する変数processingTagを、現在とは逆の値に変えているわけです。

 このプログラムには実際に何かを表示する機能はありませんので、実行しても何も起こりません。そこで以下のようにプログラムを変更すると、解析したHTMLのテキスト部分を表示するようになります。

   HTMLのテキスト部分のみを表示するプログラム
public class ExtractPlainText {
  public static void main( String args[] ) {
    boolean processingTag = false;
    String source = "<html><head><title>タイトル</title></head>
<body><p>段落</p></body></html>";
    int pos;
    int start = 0;

    for ( pos = 0; pos < source.length(); pos++ ) {
      // タグ
      if (processingTag) {
        if ( source.charAt(pos) != '>' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '>' ) {
              break;
            }
          }
        }
        start = pos + 1;
      }
      // テキスト
      else {
        if ( source.charAt(pos) != '<' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '<' ) {
              System.out.println(
                source.substring( start, pos ));
              break;
            }
          }
        }
      }
      processingTag = !processingTag;
    }
  }
}

 赤い太字の部分が変更点です。int型の変数startは、テキストの開始位置を格納するのに使っています。「タグ読み込み中」に「>」を見つけたら、テキストの開始位置は「>」の位置+1です。「テキスト読み込み中」に「<」を見つけたら、テキストの開始位置からその直前の位置までをHTML文書から取り出すことで、HTML内のテキスト部分を表示できるわけです。

 文字列を部分的に取り出すために、このプログラムではsubstringというメソッドを使っています。substringはString型クラスのメソッドで、「substring(開始位置, 終了位置)」のように使います。元の文字列の開始位置から、終了位置で指定した値の直前までを部分文字列として取得できます。「終了位置で指定した値の直前まで」というのが分かりにくいですが、substring(0, 1)としたときに取得できるのは先頭から1文字と考えれば納得がいくでしょう。

 このプログラムを実行すると以下のようになります。HTML文書からテキストを取り出すことができました。

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>java ExtractPlainText
タイトル
段落

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>

 今回紹介したHTMLパーサはコメントやスクリプトに対応していない極めて初歩的なものです。実用レベルのHTMLパーサを作るには、文法エラーへの対処など、もっと複雑な処理が必要です。しかし、一見どのようにプログラムを作ったらよいのか分からない処理も、「状態」などの概念を使うことでアルゴリズムとして設計できることは分かったと思います。

 次回は、今回作ったHTMLパーサをHTML文書を扱うクラスのメソッドとして独立させ、クラスやメソッドについての理解をもう一段深められるような説明をします。


Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。