Java Network 〜 Socket通信で受付を行う〜

Socket通信処理の受付

Socket通信処理の実装を行いました。Githubに下のようなコードをコミットしてあります。

今回の内容について

  • まずはコードを見る
  • 処理の概要について
  • 通信処理について
  • 受信処理について
  • 感想

以上のような内容を記載します。

まずはコードを見る

下のように実装しました。
メインメソッドは一番下にあります。色々と試していたら長いコードになってしまいました。

/**
 * 低レベルSocketサーバー。
 * サーバーとクライアントの関係を理解するためのサンプルコード。
 * 単体アプリとする
 * @author 作成者の名前
 */
public class NaturalBornServlet extends Thread {
    private static final boolean DEBUG = true;
    /** 受け付けるおポート番号 */
    public static final int PORT_NO = 8081;
    /** 自分自身のインスタンス */
    private static NaturalBornServlet main;

    /** リクエスト */
    private BufferedReader request;
    /** レスポンス */
    private PrintWriter response;

    /** サーバーソケット */
    private ServerSocket server;
    /** 改行コード */
    private final char SEP = (char) 10;

    /**
     * 外部からの起動を禁止する。プライベートコンストラクタ。
     */
    private NaturalBornServlet() {
        try {
            server = new ServerSocket(PORT_NO);
        } catch (IOException e) {
            System.out.println("*** サーバーソケットの起動に失敗しました。 ***");
            e.printStackTrace();
            System.exit(-1);
        }
    }

    /**
     * デストラクタ。
     * サーバーソケットのクローズ、メモリの開放を行う。
     */
    public void finalize() {
        try {
            server.close();
        } catch (IOException e) {
            System.out.println("*** サーバーソケットの終了に失敗しました。 ***");
            e.printStackTrace();
        } finally {
            server = null;
            main = null;
            System.exit(-1);
        }
    }

    /**
     * 起動した、サーバーソケットで待機を行う。
     */
    public void run()  {
        try {
            System.out.println("*** サーバーソケット起動 ***");
            Socket sock = server.accept();
            // リクエスト
            request = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            // レスポンス
            response = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
            // リクエストの読み込み
            while(true) {
                String inputTxt = readRequest(request);

                if (inputTxt.contains("HTTP/1.1")) {
                    System.out.println("HTTP: " + inputTxt);
                    inputTxt = getHttpResponse(inputTxt);
                } else if ("".equals(inputTxt) ) {
                    System.out.println("***> 空リクエスト: " + inputTxt);
                    inputTxt = "<html><body>Hello World!</body></html>";
                } else {
                    System.out.println("***> else: " + inputTxt);
                    if (isBye(inputTxt)) {
                        break;
                    }
                }
                if (DEBUG) System.out.println("*** サーバーソケット: リクエスト受信 " + inputTxt + "***");

                // レスポンスを返す
                response.write(inputTxt + SEP);
                response.flush();
                System.out.println("*** サーバーソケット: レスポンス送信 " + inputTxt + "***");
                inputTxt = null;
//              request.close();
//              response.close();
//              sock.close();
//              request = null;
//              response = null;
//              break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private boolean isBye(String inputTxt) {
        if (inputTxt != null && inputTxt.startsWith("bye")) {
            System.out.println("*** サーバーを終了します。 ***");
            return true;
        }
        return false;
    }

    private String getHttpResponse(String req) {
        StringBuilder response = new StringBuilder();

        response.append("200 OK" + SEP);
        response.append("Access-Control-Allow-Origin: *" + SEP);
        response.append("Connection: Keep-Alive" + SEP);
        response.append("Content-Encoding: gzip" + SEP);
        response.append("Content-Type: text/html; charset=utf-8" + SEP);
        response.append("Date: Mon, 18 Jul 2016 16:06:00 GMT" + SEP);
        response.append("Etag: \"c561c68d0ba92bbeb8b0f612a9199f722e3a621a\"" + SEP);
        response.append("Keep-Alive: timeout=5, max=997" + SEP);
        response.append("Last-Modified: Mon, 18 Jul 2016 02:36:04 GMT" + SEP);
        response.append("Server: NaturalBorn" + SEP);
        response.append("Set-Cookie: mykey=myvalue; expires=Mon, 17-Jul-2017 16:06:00 GMT; Max-Age=31449600; Path=/; secure" + SEP);
        response.append("Transfer-Encoding: chunked" + SEP);
        response.append("Vary: Cookie, Accept-Encoding" + SEP);
        response.append("X-Backend-Server: developer2.webapp.scl3.mozilla.com" + SEP);
        response.append("X-Cache-Info: not cacheable; meta data too large" + SEP);
        response.append("X-kuma-revision: 1085259" + SEP);
        response.append("x-frame-options: DENY" + SEP);

        return response.toString();
    }

    /**
     * 受信した、メッセージを読み込む。
     *
     * @param in Socketより取得した入力ストリーム
     * @return
     * @throws IOException
     */
    private String readRequest(BufferedReader in) throws IOException {
        if (DEBUG) System.out.println("*** サーバーソケット: readRequest() ***");
        int read = 0;
        StringBuilder inputTxt = new StringBuilder();
//      System.out.println("**** read() : " + in.read());
//      System.out.println("**** ready() : " + in.ready());
//      System.out.println("**** readLine() : " + in.readLine());

        // CRとCRLFの場合で入力が終了している時がある
        while((read = in.read()) > 0) {
            // 空の場合
            if (read == 10 || read == 13) {
                System.out.println("*** read break ***");
                break;
            }
            char ch = (char) read;
            inputTxt.append(ch);
        }
        if (DEBUG) System.out.println("*** サーバーソケット: 完了:readRequest(): " + inputTxt + " ***");
        return inputTxt.toString();
    }
    /**
     * メインメソッド、ここから始まる。
     * @param args
     */
    public static void main(String[] args) {
        main = new NaturalBornServlet();

        try {
            main.start();
        } catch (Exception e) {
            System.out.println("*** 例外が発生しました。 " + e.getMessage() + "***");
            e.printStackTrace();
        }

    }
}

処理の概要と受信処理

上記のコードは大まかに下のような処理を行っています。

  1. メインメソッドでメイン処理を実行します。このクラスはThreadクラスを継承しているので、マルチスレッド処理を行うことができます。

  2. メインメソッドでstart()`が呼び出されているので、`run()メソッドが起動します。

  3. run()`メソッドでは、ServerSocketで`accept()を呼び出し、リクエストの受付を行います。
    つまりは、リクエストを受け付けるまで処理を行わず待っている状態になります。

  4. リクエストをつけ付けたら次のコードに処理が進みます。Socketクラスを受け取り、さらに入力ストリームと出力ストリームを取得します。入力は受け付けたリクエストで、出力は返却するレスポンスになります。具体的には下のようなコードです。

    // リクエスト
    request = new BufferedReader(new InputStreamReader(sock.getInputStream()));
    // レスポンス
    response = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
  5. リクエストを読み込み、受信した内容に応じてレスポンスを返します。

    // リクエストの読み込み
    while(true) {
    String inputTxt = readRequest(request);
    
    if (inputTxt.contains("HTTP/1.1")) {
        System.out.println("HTTP: " + inputTxt);
        inputTxt = getHttpResponse(inputTxt);
    } else if ("".equals(inputTxt) ) {
        System.out.println("***> 空リクエスト: " + inputTxt);
        inputTxt = "<html><body>Hello World!</body></html>";
    } else {
        System.out.println("***> else: " + inputTxt);
        if (isBye(inputTxt)) {
            break;
        }
    }
    if (DEBUG) System.out.println("*** サーバーソケット: リクエスト受信 " + inputTxt + "***");
    
    // レスポンスを返す
    response.write(inputTxt + SEP);
    response.flush();
    System.out.println("*** サーバーソケット: レスポンス送信 " + inputTxt + "***");
    inputTxt = null;
    }
  6. リクエストの読み込みを行っているのは下のコードです。

    /**
    * 受信した、メッセージを読み込む。
    *
    * @param in Socketより取得した入力ストリーム
    * @return
    * @throws IOException
    */
    private String readRequest(BufferedReader in) throws IOException {
    if (DEBUG) System.out.println("*** サーバーソケット: readRequest() ***");
    int read = 0;
    StringBuilder inputTxt = new StringBuilder();
    System.out.println("**** read() : " + in.read());
    System.out.println("**** ready() : " + in.ready());
    System.out.println("**** readLine() : " + in.readLine());
    
    // CRとCRLFの場合で入力が終了している時がある
    while((read = in.read()) > 0) {
        // 空の場合
        if (read == 10 || read == 13) {
            System.out.println("*** read break ***");
            break;
        }
        char ch = (char) read;
        inputTxt.append(ch);
    }
    if (DEBUG) System.out.println("*** サーバーソケット: 完了:readRequest(): " + inputTxt + " ***");
    return inputTxt.toString();
    }

    ポイントになるのは、文字列を読み込をするときにに読み込む文字がないことを確認するために「改行コード」の数値型の値を使用しているところです。改行コードのchar型の値は整数型(int)で表現すると「13」もしくは「10」になります。CRLFCRの存在があるため2つになります。

通信処理について

通信処理=レスポンスの送信処理部分に関しては出力ストリームを使用して、送信します。

// レスポンスを返す
response.write(inputTxt + SEP);
response.flush();
System.out.println("*** サーバーソケット: レスポンス送信 " + inputTxt + "***");

「inputTxt」はString型の変数で、リクエストの値を取得しています。
つまりはリクエストで受けた文字列をそのまま返却しているというわけです。

感想

この実装は、Socket通信の最も簡単な処理になります。いわゆる「Socket通信のハローワールド」を実装したようなものでなので、通信処理の土台になります、逆に言うとこれを基本にどのような処理も実装することが可能ということです。

それなりに大変なことがありますが。。。

まとめると、通信の基本というところです。これを理解すれば、いろいろな通信処理の実装を行うことも可能になります。
なぜかというと、通信の基本を学んだからです。

でわでわ。。。

関連ページ

Java Basic

  1. Java Basic Level 1 Hello Java
  2. Java Basic Level2 Arithmetic Calculate
  3. Java Basic Level3 About String class
  4. Java Basic Level 4Boolean
  5. Java Basic Level 5If Statement
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 Traning of If statement
  8. Java Basic Level8 How to use for statement
  9. Java Basic Level 8.5 Array
  10. Java Basic Level 9Training of for statement
  11. Java Basic Level 10 While statement 
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2
  14. サンプル実装〜コンソールゲーム〜
  15. Java Basic インターフェース・抽象クラスの作り方
  16. Java Basic クラスとは〜Step2_1
  17. Java Basic JUnit 〜テストスイートの作り方〜

Java ウェブサーバー作り〜Soket通信アプリ作成〜

イントロダクション

世の中では、「ウェブサーバー」というものが「普通」に存在しています。というか多くの人が「常識のように」言葉として使用します。

しかし、その中身はどうなっているのでしょうか?ITエンジニアではない人ならば、知る必要ないと思うかもしれません。ITエンジニアであれば、知っていて当然みたいな雰囲気が感じられますが、実際はどうでしょうか?

何がいいたいかというと、「ウェブサーバー作って見ませんか?」ということです。当然、自分ウェブサーバーを作ったらウェブサーバーを理解しているのも必然です。

早い話が、作ってみれば理解できるということです。

ウェブサーバーを作る

ここで使用する言語はJavaです。自分がJava屋なのもありますが、ウェブ系のアプリといえば、だいたいJavaで実装していることが多いです。(業務アプリに関して。。。)

そして、今回作成したサーバーは、いまだに不完全です。SOcけt同士の通信ならば問題なくできるのですが、ブラウザからアクセスを受付た場合は、うまくいきません。セキュリティ的にアクセスが拒否られているようです。
Socketでの通院は、下のクラスを起動することでやり取りができます。

Client App

public class ClientMain {
    /** 自身のインスタンス */
    private static ClientMain main;
    /** クライアントソケット */
    private Socket sock = null;
    /** 送信用のクラス */
    private PrintWriter send;
    /** 受信用のクラス */
    private BufferedReader response;

    /** 改行コード */
    private static final char SEP = (char) 10;

    public ClientMain() {
        try {
            sock = new Socket("localhost", NaturalBornServlet.PORT_NO);
        } catch (IOException e) {
            System.out.println("*** ソケットで例外が発生しました。 ***");
        }
    }

    /**
     * デストラクタ
     */
    @Override
    public void finalize() {
        try {
            sock.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            sock = null;
        }
    }

    /**
     * 標準入力からサーバーソケットへデータを送信する。
     *
     * @return 終了時のメッセージ
     */
    public String execute() {
        try (Scanner scan = new Scanner(System.in)) {
            while(true) {
                System.out.println("*** クライアントソケット: 入力 ***");
                String input = scan.nextLine();

                // リクエストの送信
                sendRequest(input);

                if ("bye".equals(input)) {
                    System.out.println("*** クライアントアプリを終了します。***");
                    break;
                }
                // レスポンスを受信する
                System.out.println("クライアント: " + getResponse());
            }
        }

        return "クライアントを終了しました。";
    }

    private void sendRequest(String in) {
        try {
            //System.out.println("*** クライアントソケット: リクエスト送信" + in + " ***");
            send = new PrintWriter(sock.getOutputStream());

            send.write(in + SEP);
            send.flush();
//          send.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * サーバーからのレスポンスを受ける。
     * @throws IOException
     */
    private String getResponse() {
        StringBuilder build = new StringBuilder();
        try {
            // System.out.println("*** クライアントソケット: レスポンス受信 ***");
            if (response == null) {
                response = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            }
            while (true) {
                int read = response.read();
                if (read == 13 || read == 10) {
                    break;
                }
                char ch = (char) read;
                build.append(ch);
            }
            //response.close();
        } catch (IOException e) {
            e.printStackTrace();
            build.append(" Errors... ");
        }
        // System.out.println("*** クライアントソケット: 完了:レスポンス受信 ***");
        return build.toString();
    }

    public static void main(String[] args) {
        ClientMain main = new ClientMain();
        String mes = main.execute();
        System.out.println(mes);
    }
 }

java.net.ServerSocket

Javaでのサーバー作成で最もベーシックな低レベルAPIがこのクラスです。巷で出回っているフレームワークも結局はこのテクノロジーを使用しています。

ServerSocketとは

このクラスはサーバー・ソケットを実装します。サーバー・ソケットは、ネットワーク経由で要求が送られてくるのを待ちます。これは、その要求に基づいていくつかの操作を実行します。その後、場合によっては要求元に結果を返します。
サーバー・ソケットの実際の処理は、SocketImplクラスのインスタンスによって実行されます。アプリケーションは、ソケット実装を作成するソケット・ファクトリを変更することで、ローカル・ファイアウォールに適したソケットを作成するようにアプリケーション自体を構成することができます。

このように解説がJavaDocドキュメントにはありますが、実装の方は下のようになります。

ServerSocketの実装

サーバーソケットの作成(インスタンス化)

実際に下のようにコーディングしました。これは、サーバーソケットのインスタンスを作成する処理です。

try {
    server = new ServerSocket(portNo);
    if (server.isBound() == false) {
        server.bind(new InetSocketAddress("localhost", portNo));
        server.setSoTimeout(3000);
    }
} catch (IOException e) {
    this.finalize();
    e.printStackTrace();
}

処理としては、以下の通りです。

  1. サーバーソケットをポート番号"portNo"で作成
  2. サーバーソケットがバインドされていない場合は、新たにバインドし直し
  3. サーバーソケットのタイムアウト時間を3秒に設定

サーバーソケットの送受信処理

インスタンスを作成したら、クライアントとの送受信を行います。手順は以下の通りです。

  1. サーバーを起動する
  2. クライアントアプリでアクセスする

なので、先にサーバーを作成します。そして、一番下にメインメソッドを作っています。

/**
 * コンストラクタで作成されたサーバーソケットで
 * 受付処理を開始する。
 */
public void startServer() {
    try {
        System.out.println("*** SocketServer start " + server.isBound() + "***");
        Socket recieve = server.accept();
        System.out.println("*** Server Get Request start***");
        // 受信データの読み込み
        StringBuilder responseTxt = new StringBuilder(" Response");
        // 受信状態
        System.out.println("クライアント: " + recieve.getRemoteSocketAddress());
        System.out.println("接続: " + recieve.isConnected());
        System.out.println("入力ストリームが閉じている: " + recieve.isInputShutdown());

        // 受信したリクエスト
        InputStream in = recieve.getInputStream();
        // 返却するレスポンス
        OutputStream writer = recieve.getOutputStream();
        System.out.print("Server recieve is ...");
        int c = 0;
        // CRとCRLFの場合で入力が終了している時がある
        while((c = in.read()) != -1) {
            char ch = (char)c;
            responseTxt.append(ch);
            // 空の場合
            if (c == 10 || c == 13) {
                break;
            }
        }
        System.out.println(responseTxt.toString());
        System.out.println("*** Server Send Response start***");
        // レスポンス送信
        writer.write((responseTxt.toString() + System.getProperty("file.separator")).getBytes());
        writer.flush();
        in.close();
        writer.close();
        System.out.println("*** SocketServer end ***");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 開いたストリームを閉じる
        try {
            closeServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * メインメソッド
 * @param args プログラム引数
 */
public static void main(String[] args) {
    SocketServerBasic serverSocket = new SocketServerBasic(8080);
    serverSocket.startServer();
}

次に、クライアントアプリの作成を行います。
クライアントの処理は、以下の通りです。

  1. クライアントソケットの作成(ローカルホスト、ポート番号指定)
  2. 受診するデータを準備(InputStream)
  3. 送信するデータを準備(OutputStream)
  4. リクエストの送信
// リクエスト送信
writer.write("ping...\n".getBytes());
writer.flush();
  1. サーバーのレスポンスを受信
            int c = 0;
            while((c = reader.read()) != -1) {
                System.out.print((char)c);
            }

ここで、注意点があります。受信時の処理で、BufferedReader#readLine()では、データの区切りが取得できないケースがあります。
そのような時は、下のように、改行文字をデータの区切りとして取得すれば、処理がループを抜けない状態を回避することができます。

// レスポンス
response = new PrintWriter(socket.getOutputStream());
int ch = -1;
StringBuilder build = new StringBuilder();
while((ch = request.read()) != -1) {
    build.append((char) ch);
    if (ch == 10 || ch == 13) {
        break;
    }
}
/**
 * コンストラクタ
 * @param portNo ポート番号
 */
public static void main(String[] args) {
    System.out.println("*** SocketClient start ***");
    try (Socket sock = new Socket(HOST, PORT)) {
        // 受信したレスポンス
        InputStream reader = sock.getInputStream();
        // 送信するリクエスト
        OutputStream writer = sock.getOutputStream();
        System.out.println("接続済み: " + sock.isConnected());
        System.out.println("接続サーバー: " + sock.getRemoteSocketAddress());
        // リクエスト送信
        writer.write("ping...\n".getBytes());
        writer.flush();

        System.out.println("*** Testing start " + sock.getInetAddress() + " ***");
        // レスポンス受信
        int c = 0;
        while((c = reader.read()) != -1) {
            System.out.print((char)c);
        }
        System.out.println("*** Testing end***");

        // 終了処理
        reader.close();
        writer.close();
        sock.close();
        System.out.println("*** SocketClient end ***");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

これで、一応は動きます。「一応」というのは下のソースでは、while文が終わらないという事象に見舞われました。以下の部分です。

while((c = reader.read()) != -1) {
    System.out.print((char)c);
}

他のサイトを調べてみると、うまく動いているようですが、自分のところでは動きませんでした。

そして、悩んだ結果次のように修正しました。

// CRとCRLFの場合で入力が終了している時がある
while((read = in.read()) > 0) {
    // 空の場合
    if (read == 10 || read == 13) {
        System.out.println("*** read break ***");
        break;
    }
    char ch = (char) read;
    inputTxt.append(ch);
}

受信したデータの終わりがわからない状態になってしまうのでループ処理が終わらなかったというわけです。

これを解消するために、終わりを示す「改行」文字をINT型で判別しています。intの10, 13はchar型の改行コードの値に相当します。つまり、(10)という値は、LF改行のことを指します。

なので、改行コードと取得した値が同じ場合は処理を抜けるというわけです。

if (read == 10 || read == 13) {
System.out.println("*** read break ***");
break;
}

そして、最終的にはこちらのようなコードになりました。全体はGithubにあります。

try {
    System.out.println("*** サーバーソケット起動 ***");
    Socket sock = server.accept();
    // リクエスト
    request = new BufferedReader(new InputStreamReader(sock.getInputStream()));
    // レスポンス
    response = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
    // リクエストの読み込み
    while(true) {
        String inputTxt = readRequest(request);

        if (inputTxt.contains("HTTP/1.1")) {
            System.out.println("HTTP: " + inputTxt);
            inputTxt = getHttpResponse(inputTxt);
        } else if ("".equals(inputTxt) ) {
            System.out.println("***> 空リクエスト: " + inputTxt);
            inputTxt = "<html><body>Hello World!</body></html>";
        } else {
            System.out.println("***> else: " + inputTxt);
        if (isBye(inputTxt)) {
            break;
        }
    }
    if (DEBUG) System.out.println("*** サーバーソケット: リクエスト受信 " + inputTxt + "***");

    // レスポンスを返す
    response.write(inputTxt + SEP);
    response.flush();
    System.out.println("*** サーバーソケット: レスポンス送信 " + inputTxt + "***");
    inputTxt = null;
// デバック用コード
//  request.close();
//  response.close();
//  sock.close();
//  request = null;
//  response = null;
//  break;
    }
} catch (IOException e) {
    e.printStackTrace();
}

実行結果は、後日アップロードします。。。

でわでわ。。。

関連記事

Java Network 〜Javaでホームページにアクセスする仕組み〜

今回は、JavaでWebページにアクセスする処理を作るときの内容を記載します。

サンプルコードはGithubにアップしてあります。

1.ウェブにアクセス

プログラムでウェブにアクセスする部分に関して記載します。コードの説明になりますが。。。

System.out.println("*** execute ***");
String url = "https://ja.wikipedia.org/wiki/%E6%AD%A6%E5%99%A8";
Document doc = null;
try {
    doc = Jsoup.connect(url).get();
} catch (IOException e) {
    e.printStackTrace();
}

変数の「url」にアクセスします。
処理の内容としては以下の通りです。

  1. アクセスするURLを指定します。つまり変数に代入します。
  2. そして、取得したHTMLをDocumentクラスに変換して取得します。doc = Jsoup.connect(url).get();
  3. 取得するときに例外(IOException)が出る可能性がある→メソッドの定義にthrows IOExceptionがある、のでtry chatchで囲む

2.HTMLの取得

これも同様に、コードから見ていきます。

Elements eles = doc.getElementById("toc").children();
for (int i = 0; i < eles.size(); i++) {
    Element item = eles.get(i);
    System.out.print("wholeText(): " + item.wholeText());
    System.out.println(" / text(): " + item.text());
    printIndex(item, i);
}

Jsoupのフレームワークを使うと言う部分になります。
ドキュメント(Document)クラスを中心にして取得します。

doc.getElementById("toc").children()

このコードでid="toc"と記述されたHTMLを取得します。
実際には[Wikiの「武器」ページ](https://ja.wikipedia.org/wiki/%E6%AD%A6%E5%99%A8)から目次の部分を取得します。

具体例参照するぺーじのHTML
![](http://zenryokuservice.com/wp/wp-content/uploads/2020/03/スクリーンショット-2020-03-23-20.49.38-300x192.png)

<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none"><div class="toctitle" lang="ja" dir="ltr"><h2 id="mw-toc-heading">目次</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>

目次のタグ部分です。はじめのタグ(一番外側のタグです)を取得しています。

よくJSで以下のような実装を行いますが、それと同じです。

var tag = document.getElementById("タグのID");

取得したタグからタグの中にある子供のタグを取得します。

## タグの取得
例えば、下のようなHTMLから。。。

<div class="toctitle" lang="ja" dir="ltr"><h2 id="mw-toc-heading">目次</h2><span class="toctogglespan">
    <label class="toctogglelabel" for="toctogglecheckbox"></label></span>
</div>

以下のようなコードを実行したとします。
```java
Document doc = Jsoup.connect(url).get();
// HTMLボディの取得
Element body = doc.body();
Elements eles = body.getElementById("toctitle");
eles.text();
```
と実装すると「目次」が取得できます。(間違ってたらごめんなさい。。。)大切なのは、子供のタグを取得(Elements)してElements#child()子供のタグを取得すると言うわけです。

## 結論から言うと
実装して動かしてみるのが一番ですが、サンプルコードとして。。。
```java
Element body = doc.body();
Elements eles = body.getElementsByTag("h2");
System.out.println("size: " + eles.size());
Elements eles = doc.getElementById("toc").children();
for (int i = 0; i < eles.size(); i++) {
Element item = eles.get(i);
System.out.print("wholeText(): " + item.wholeText());
System.out.println(" / text(): " + item.text());
printIndex(item, i);
}
```

## 関連ページ

Java Basic

  1. Java Basic Level 1 Hello Java
  2. Java Basic Level2 Arithmetic Calculate
  3. Java Basic Level3 About String class
  4. Java Basic Level 4Boolean
  5. Java Basic Level 5If Statement
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 Traning of If statement
  8. Java Basic Level8 How to use for statement
  9. Java Basic Level 8.5 Array
  10. Java Basic Level 9Training of for statement
  11. Java Basic Level 10 While statement 
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2
  14. サンプル実装〜コンソールゲーム〜
  15. Java Basic インターフェース・抽象クラスの作り方
  16. Java Basic クラスとは〜Step2_1
  17. Java Basic JUnit 〜テストスイートの作り方〜


Java HTML解析 〜Wikipedia 武器 ページの解析3〜

前回は、目次の表示を行いました。

しかし、このままでは、インデックスがちゃんと表示されないので、インデックスをちゃんと表示できるように一工夫する必要があります。

インデックスがうまくいかない

前回のコードでは、「id=toc」のタグを取得してから中にあるタグを単純に表示するだけです。というか表示用のメソッドの記述がありませんでした。

それは、インデックスをちゃんと表示するためのメソッドを用意したためです。
対象になる部分は以下のような処理です。

// System.out.println("*** Get Content ***");
Elements eles = doc.getElementById("toc").children();
for (int i = 0; i < eles.size(); i++) {
    Element item = eles.get(i);
    printIndex(item, i);
}

「printIndex(item, i);」の部分です。
このメソッドを下のように変更すると単純にタグの中身を表示できます。
System.out.println(item.text());

インデックスの処理

結論から言うと、インデックス部分かどうかの判定をして、インデックスであれば。。。と言う処理を行ってやれば良いと言うところですが、ちょっと面倒でした。

作成した処理は以下のようなものになります。
Gitにアップしてあります

private void printIndex(Element row, int num) {
//      System.out.println("index" + num + ": " + row.text());
    if (row.childrenSize() != 0) {
        printChild(row.children());
    }
}

private void printChild(Elements eles) {
    for (Element ele: eles) {
        if (ele.childrenSize() != 0) {
            printChild(ele.children());
        } else {
            printElement(ele);
        }
    }
}
private void printElement(Element ele) {
    if (ele.text().contains(".")) {
        print(ele,"-   ", ": ");
    } else if (ele.text().matches("[0-9]{1,2}")) {
        print(ele,"", ": ");
    } else {
        if (ele.text().equals("")) {
            return;
        }
        println(ele, "", "");
    }
}
private void print(Element ele, String prefix, String safix) {
    if (safix != "") {
        System.out.print(ele.text() + safix);
    } else {
        System.out.print(prefix + ele.text());
    }
}
private void println(Element ele, String prefix, String safix) {
    if (safix != "") {
        System.out.println(ele.text() + safix);
    } else {
        System.out.println(prefix + ele.text());
    }
}

メソッド呼び出しの順番は以下のようになっています。

  1. printIndex(): タグの子供を取得
  2. printChild(): タグの子供がいる場合は再起処理、そうでない場合は文字を取得して表示
  3. printElement(): ⑴を参照
  4. print(改行なし) or println(改行あり)
⑴ 以下のような条件分岐を行う
A. 文字列に「.」を含む場合
B. 文字列が「0-9」の数字の場合
C. 上記以外の場合

それぞれに対し、下のような処理を行います。※改行しない場合

       if (safix != "") {
            System.out.print(ele.text() + safix);
        } else {
            System.out.print(prefix + ele.text());
        }

表示する文字の手前(prefix)とあと(safix)に値があればそれを表示すると言う処理を行っています。

このプログラムで、取得した値は以下のように表示されます。

*** execute ***
*** Get Content ***
目次
1: 武器のエネルギー
1.1: 運動エネルギー
1.2: 人力
1.3: 火・燃焼
1.4: 生物(兵器)
1.5: 電気
1.6: 光線
1.7: 毒・化学物質
1.8: その他
2: 武器の構造
2.1: 柄
2.2: 柄頭
2.3: 刃
2.4: 刀身・剣身
2.5: 鎖物・縄
2.6: 鉤
2.7: 暗器
2.8: 機械
3: 歴史
3.1: 石器・自然物
3.2: 金属製武器の登場
3.3: 銃の登場
3.4: 近代
3.5: 第二次大戦後
4: 日本の武器の歴史
4.1: 古代
4.2: 中世
4.3: 近世
4.4: 近代
4.5: 現代
4.6: 現在
5: 玩具としての武器
6: スポーツに用いられる武器
7: 象徴・祭器としての武器
8: 架空の武器
9: 武器の分類
10: 主な武器の種類
10.1: 刀剣類
10.2: 鈍器類
10.3: 竿状武器(ポールウェポン)
10.4: 投擲武器
10.5: 射出武器
10.5.1: 暗器・格闘武器
10.6: その他の武器
11: 脚注
12: 参考文献
13: 関連項目
14: 外部リンク

でわでわ。。。

関連ページ

Java Basic

  1. Java Basic Level 1 Hello Java
  2. Java Basic Level2 Arithmetic Calculate
  3. Java Basic Level3 About String class
  4. Java Basic Level 4Boolean
  5. Java Basic Level 5If Statement
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 Traning of If statement
  8. Java Basic Level8 How to use for statement
  9. Java Basic Level 8.5 Array
  10. Java Basic Level 9Training of for statement
  11. Java Basic Level 10 While statement 
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2
  14. サンプル実装〜コンソールゲーム〜
  15. Java Basic インターフェース・抽象クラスの作り方
  16. Java Basic クラスとは〜Step2_1
  17. Java Basic JUnit 〜テストスイートの作り方〜


Java HTML解析 〜Wikipedia 武器 ページの解析2〜

前回は、とりあえずでWikiのページ(武器)を取得する処理を実装しました。
これは、以下のような処理を行っています。

  1. 単純にURLで対象のHTMLのH2タグを取得
  2. 文字列でコンソールに表示

しかし、これでは意味がないので取得したいものを取得できるようにWikiページの構成を考えていきます。

Wikiページの中身

とりあえずは目次を取得することを考えます。
Wikiページの「目次」に関して、タグの属性に注目してみます。目次部分のHTMLをはじめの部分を抜き出してみました。

<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">
<input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none"><div class="toctitle" lang="ja" dir="ltr"><h2 id="mw-toc-heading">目次</h2><span class="toctogglespan">
<label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
<ul>
<li class="toclevel-1 tocsection-1"><a href="#武器のエネルギー"><span class="tocnumber">1</span> <span class="toctext">武器のエネルギー</span></a>
<ul>
<li class="toclevel-2 tocsection-2"><a href="#運動エネルギー"><span class="tocnumber">1.1</span> 

これの構成を見てみると大元(一番上)のタグのidとclassに注目してみると目次部分のHTMLにはID=「toc」とあります。
そしてその「toc」内に全ての内容が記載されているのでこれを取得すれば、目次部分が取得できます。

そして、以下のような構成で内容が記載されています。

<div id="toc" class="toc" /> => 全体のタグ
    「目次」
    toclevel-1 => トピックレベル1の項目
    toclevel-? => トピックレベル?の項目
       tocsection-? => 各セクション1〜?までの項目

なので、Javaのコードでは以下のような処理を行います。

  1. 「id=”toc"」のタグを取得
  2. それぞれの値をコンソールに出力

Jsoupを使用するとここら辺が簡単にできます。

Javaでの処理

public void execute() {
    // System.out.println("*** execute ***");
    String url = "https://ja.wikipedia.org/wiki/%E6%AD%A6%E5%99%A8";
    Document doc = null;
    try {
        // Wikiページへアクセス、HTMLを取得する
        doc = Jsoup.connect(url).get();
    } catch (IOException e) {
        e.printStackTrace();
    }
    // System.out.println("*** Get Content ***");
    Elements eles = doc.getElementById("toc").children();
    for (int i = 0; i < eles.size(); i++) {
        Element item = eles.get(i);
        printIndex(item, i);
    }
}

これは自分が作成したものですが、以下のような処理を行っています。

  1. 「id="toc"」のタグを取得
  2. タグの子供(タグ内にあるタグ)
    <div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none"><div class="toctitle" lang="ja" dir="ltr"><h2 id="mw-toc-heading">目次</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
    <ul>
    <li class="toclevel-1 tocsection-1"><a href="#武器のエネルギー"><span class="tocnumber">1</span> <span class="toctext">武器のエネルギー</span></a>
    <ul>
    <li class="toclevel-2 tocsection-2"><a href="#運動エネルギー"><span class="tocnumber">1.1</span> <span class="toctext">運動エネルギー</span></a></li>

    上の「武器のエネルギー」などが含まれるタグを取得

  3. 取得したタグの文字列部分を取得

こんな感じの処理を行っています。

問題点

このままでは、取得したいタグの番号をつけている部分「1.1、2 ...」という数字があるので、混乱してしまいます。
本当であれば、これらは各項目のインデックスであり、項目の順番を示すものですので、これをちゃんと表示してやる必要があります。イメージとしては下のような感じです。

目次
1: 武器のエネルギー
1.1: 運動エネルギー
1.2: 人力
1.3: 火・燃焼
1.4: 生物(兵器)

これらの表示を行うために、頭をひねる必要があります。
これは次回にしようと思います。

でわでわ。。。

関連ページ

Java Basic

  1. Java Basic Level 1 Hello Java
  2. Java Basic Level2 Arithmetic Calculate
  3. Java Basic Level3 About String class
  4. Java Basic Level 4Boolean
  5. Java Basic Level 5If Statement
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 Traning of If statement
  8. Java Basic Level8 How to use for statement
  9. Java Basic Level 8.5 Array
  10. Java Basic Level 9Training of for statement
  11. Java Basic Level 10 While statement 
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2
  14. サンプル実装〜コンソールゲーム〜
  15. Java Basic インターフェース・抽象クラスの作り方
  16. Java Basic クラスとは〜Step2_1
  17. Java Basic JUnit 〜テストスイートの作り方〜