Javaでニュースサイトの記事を取得する

こんにちは。前回はランダムに単語が表示される単語学習アプリを作りましたが、今回も自分の外国語学習に役立てようという魂胆で、外国語ニュースサイトの記事を取得してくる機能を作ってみます。

この記事では[NHK World News](https://www3.nhk.or.jp/nhkworld/)のニュース記事を取得する方法を紹介していますが、少し修正すれば基本的にはどのようなサイトの文章も取得することができます。

前提条件

- IntelliJ IDEA： 2021.1.2 (Community Edition)
- JDK：16.0.1
- MacOS：Version 11.1

## 1.今回やることの整理

今回行うのは主に以下の処理です。

- 記事を取得したいニュースサイトのURLの入力を受け取る。
- http接続でニュースサイトのHTMLを取得する。
- 取得したHTMLをテキストファイルに書き出す。
- HTMLの余分な部分を取り除き、ニュース本文のみをコンソールに表示させる。

取得したHTMLを一度テキストファイルに書き出す必要はなく直接コンソールに表示させてしまっても良いのですが、今回は書き出しの練習も兼ねて以上の処理にしてみました。

直接HTMLを表示させる処理を今回はうまく実装できなかったため、もしスッキリした方法をご存知の方はアドバイスいただけますと幸いです。

http接続でHTMLを取得する方法については[こちらの記事](https://logicalerror.seesaa.net/article/128919218.html)を大いに参考にいたしました。


## 2.テキストファイルを準備する

それでは早速新規プロジェクトを作成し、メインクラスに以下の記述を追加しましょう。

```Main.java
public class Main {

    public static void main(String[] args) {

        // 以下使用するパスのホームディレクトリ部分を取得する
        final String homeDir = System.getenv("HOME");

        // 以下使用するファイルのパスを変数に格納する
        final String filePath = "/Desktop/sample.txt";        

        try {
            // HTMLを書き出すファイルのパスを指定する
            Path p = Paths.get(homeDir +  filePath);

            try {
                // 先ほどpで指定したパスにファイルを作る
                Files.createFile(p);
            } catch (IOException e) {
                // ファイルが存在していたら例外処理
                System.out.println(e);
            }

            // テキストで書き込む為の準備
            BufferedWriter bw = new BufferedWriter(
                    new OutputStreamWriter(
                            new FileOutputStream(homeDir +  filePath), "UTF8"
                    )
            );
    }
}
```

まずは取得してきたHTMLを書き出すテキストファイルの準備をします。ファイルを作りたい場所のパスを指定するためにHOMEディレクトリとそれ以下のパスを変数に格納します。FilesクラスのcreateFileメソッドに先ほどのパスを指定してファイルを作成します。既にファイルが存在していた場合は例外でスルーさせる処理をcatch文の中に記述しています。

catch文を閉じた後はBufferedWriterクラスをインスタンス化し、書き込むための準備をします。この部分はファイルの文字コードをUTF-８に設定している部分です。

## 3.取得したHTMLをテキストファイルに書き出す。

続いて前節のBufferedWriterの記述の後に以下を追加します。

```Main.java
    // テキストで書き込む為の準備
    BufferedWriter bw = new BufferedWriter(
            new OutputStreamWriter(
                    new FileOutputStream(homeDir +  filePath), "UTF8"
            )
    );

    // URL文字列の取得
    Scanner scanner = new Scanner(System.in);
    System.out.print("ニュース記事を取得したいサイトのURLを貼り付けてね！！！");

    // URL文字列入力の受け取り
    String str = scanner.nextLine();

    // URLを扱うためのURLインスタンスを作成する
    URL url = new URL(str);

    // http接続をするためのインスタンス
    HttpURLConnection http = (HttpURLConnection) url.openConnection();

    // http接続のリクエストを投げる
    http.setRequestMethod("GET");

    // http接続する
    http.connect();

    // UTF-8の文字コードで情報を取得するためのisrというインスタンスを作成
    InputStreamReader isr = new InputStreamReader(http.getInputStream(), "utf8");

    // 行単位で読み込む為の準備
    BufferedReader br = new BufferedReader(isr);
    String line_buffer;

    // BufferedReaderは、readLineがnullを返す（つまり読み込む行がなくなる）と終了する
    while (null != (line_buffer = br.readLine())) {

        // ファイルに情報を一行ずつ書き込む
        bw.write(line_buffer + "\n");
    }

    // http接続やHTML情報の読み込み、書き込みを終了する記述
    br.close();
    isr.close();
    http.disconnect();
    bw.close();

  // 不具合が生じたらエラーを出す
} catch (Exception e) {
    System.out.println(e.getMessage());
}
```

URLを貼り付けるプロンプトを表示させたのちにscanner.nextLine()でURLの入力を受け取ります。受け取ったURLはあくまで単なる文字列なので、URLとして扱うためURLクラスの記述を追加します。

その後http接続をするための記述を追加します。それぞれインスタンスの作成、リクエストを投げる、http接続する3つの部分に分かれています。ここの理解は[こちらの記事](https://techbooster.org/android/application/6812/)を参考にしました。

HttpURLConnectionというインスタンスを作成し、GETというリクエストメソッドを設定しています。今回はHTMLテキストをゲットしてくるためGETメソッドを設定しています。何らかのデータを送信する場合はPOSTを指定することもできます。http.connect()メソッドによってhttp接続を実行し、InputStreamReader、BufferedReaderクラスの処理につながっています。

InputStreamReaderは、バイトストリームで表現されているHTMLを文字コードに変換することのできるクラスです。コンピュータが理解できる形で書かれた文字をUTF-8の文字コードで行単位でHTMLを読み込む準備をします。

BufferedReaderという、行単位でテキストを読み込むクラスもインスタンス化します。line_bufferというString型変数を宣言していますが、これはBufferedReaderの処理の中で読み込んでいく各行のテキストを逐一格納します。

while文は、次に読み込む行がなくなったらループを抜ける、という条件を指定しています。while文の中のwriteメソッドで読み込んだテキストをテキストファイルに書き込んでいます。

最後にclose(), disconnect()の記述によって書き込みやhttp接続を終了しています。

## 4.テキストファイルを読み込み、ニュース本文を表示させる

それではいよいよ、サイトのHTMLを書き出したテキストファイルを読み込んでニュース本文を表示させるわけですが、どのようなHTMLを取得してくることになるのか確認してみましょう。

例として[こちらの記事](https://www3.nhk.or.jp/nhkworld/en/news/20210919_12/)を使います。リンクをクリックするとこのように記事が表示されます。

![スクリーンショット 2021-09-26 20.43.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/9e7cc1fe-3f51-22ed-04b3-60927c2d420c.png)

webサイトがどのようなHTMLで作られているのかを確認するには、右クリックメニューから「ページのソースを表示」をクリックします。すると以下のHTMLが表示されます。

![スクリーンショット 2021-09-26 20.44.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/d8993e6a-2e35-fc99-663c-04c86991d8a5.png)

こちらのHTMLから、どこに記事本文が記述されているのかを探します。ざっと見てみると以下の２箇所が見つかりました。

```
- 「"articleBody": 」 の後
- 「<div class="p-article__body">」 から 「</div>」 の間
```

![スクリーンショット 2021-09-26 20.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/c0e98ab2-84a4-4c53-87a1-768ae372361d.png)

![スクリーンショット 2021-09-26 20.46.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/08340111-892a-d25c-0318-998a988b0b27.png)

他の記事のソースも見てみると、どうやらこの２箇所に必ず記事本文が記述されているようです。これらの「"articleBody": 」やdivタグを目印にして記事本文を抜き出し、不要なpタグを削除したり改行してからコンソールに表示させるイメージです。

今回は後者のdivタグを目印にして記事本文を抜き出すことにしました。

前節の最後にclose()とcatch文を記述した部分に続けて以下を追記してください。

```Main.java
    // http接続やHTML情報の読み込み、書き込みを終了する記述
    br.close();
    isr.close();
    http.disconnect();
    bw.close();

    // 不具合が生じたらエラーを出す
} catch (Exception e) {
    System.out.println(e.getMessage());
}

try (BufferedReader br = new BufferedReader(new FileReader(homeDir +  filePath))) {

    // 読み込んだ各行を格納するための変数を用意
    String line;
    {
        // 読み込む行がなくなるまで実行
        while ((line = br.readLine()) != null) {

            // 本文部分に差し掛かる
            if (line.startsWith("              <div class=\"p-article__body\">")) {
                while ((line = br.readLine()) != null) {

                    // 本文部分が終わる
                    if (line.startsWith("              </div>")) {
                        break;
                    }

                    // 本文中のHTMLタグを正しい表記に変換する
                    line = line.replaceAll("<p>", "");
                    line = line.replaceAll("</p>", "");
                    line = line.replaceAll("&#039;", "\'");
                    line = line.replaceAll("&quot;", "\"");

                    // コンソールに出力する
                    System.out.println(line);

                }

            }
        }
    }
} catch (IOException e) {
    System.out.println("エラーが起きました。");
    e.printStackTrace();
}
```
テキストを一行ずつ読み込むreadlineメソッドを持つBufferedReaderクラスをインスタンス化します。String型で宣言した「line」という変数は、続くwhile文の中で、読み込んだ各行が一時的に格納されることになります。

while文のループ条件は先ほど出てきたものと同じく、読み込む行がなくなるまでという条件を指定しています。while文の中では二つのif文が入れ子になっています。これはline.startsWithメソッドによってdivタグの記事本文が始まる目印となる部分に差し掛かったら行の整形と表示を行い、divタグの終了部分まで読み込みを完了するというイメージで見ていただければと思います。if文の位置に気をつけないと、不要なdivタグまでコンソール出力されることになるので注意しましょう。

行の整形処理部分ではpタグを取り除いたり、「&#039」のようにHTMLエンティティという形で表示される記号を正しい表記に切り替えています。今回はシングル、ダブルクォーテーションのみ正規表現で置き換えていますが、さらに別の記号が使われている記事が存在する可能性もあるため、いろいろな記事を試しての調査の必要がありそうです。

整形したものを表示し、catch文でエラーを捕まえる記述を書いたら無事ソースの記述は完了です。実行してみましょう。

![スクリーンショット 2021-09-26 20.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/2eb69b33-3054-2266-f650-404f89f597b8.png)

まずは記事のURLを貼り付けろというプロンプトが出てくるので、指示に従ってURLを貼り付けエンターキーを押します。

![スクリーンショット 2021-09-26 20.51.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/5019dea7-37fc-7802-ebe7-c52cc4f9f28a.png)

すると無事取得された記事が綺麗な形でコンソールに出力されました。文ごとに改行されているため若干見づらい気はしますね。

試しに他の言語でも確認してみます。右から左に書く[ウルドゥー語の記事](https://www3.nhk.or.jp/nhkworld/ur/news/323241/)で確認してみましょう。

![スクリーンショット 2021-09-26 20.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/c0513aa4-24ca-c5e4-7fec-dc96b5eb72b0.png)

ピリオドの位置を確認すると、特に問題なく表示できているようですね。ちなみに先ほど英語の記事で実行した後に続けて実行したため、もうテキストファイルが存在していますよという「java.nio.file.FileAlreadyExistsException」の例外が発生しています。

参考として、作られるテキストファイルの中身も確認してみましょう。以下のようにHTMLが取得されていることが確認できます。

![スクリーンショット 2021-09-26 20.52.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1676583/a5b3815c-28a9-602c-d355-51b2d8cde487.png)

## 5.ソースコード全貌

インポート文についてはIDEで要求された時に適宜追加しました。package文の部分は各人の設定によって表記が変わります。

```Main.java
package com.company;

import java.net.*;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.*;
import java.util.*;

public class Main {

    public static void main(String[] args) {

        // 以下使用するパスのホームディレクトリ部分を取得する
        final String homeDir = System.getenv("HOME");

        // 以下使用するファイルのパスを変数に格納する
        final String filePath = "/Desktop/sample.txt";        

        try {
            // HTMLを書き出すファイルのパスを指定する
            Path p = Paths.get(homeDir +  filePath);

            try {
                // 先ほどpで指定したパスにファイルを作る
                Files.createFile(p);
            } catch (IOException e) {
                // ファイルが存在していたら例外処理
                System.out.println(e);
            }

            // テキストで書き込む為の準備
            BufferedWriter bw = new BufferedWriter(
                    new OutputStreamWriter(
                            new FileOutputStream(homeDir +  filePath), "UTF8"
                    )
            );

            // URL文字列の取得
            Scanner scanner = new Scanner(System.in);
            System.out.print("ニュース記事を取得したいサイトのURLを貼り付けてね！！！");

            // URL文字列入力の受け取り
            String str = scanner.nextLine();

            // URLを扱うためのURLインスタンスを作成する
            URL url = new URL(str);

            // http接続をするためのインスタンス
            HttpURLConnection http = (HttpURLConnection) url.openConnection();

            // http接続のリクエストを投げる
            http.setRequestMethod("GET");

            // http接続する
            http.connect();

            // UTF-8の文字コードで情報を取得するためのisrというインスタンスを作成
            InputStreamReader isr = new InputStreamReader(http.getInputStream(), "utf8");

            // 行単位で読み込む為の準備
            BufferedReader br = new BufferedReader(isr);
            String line_buffer;

            // BufferedReaderは、readLineがnullを返す（つまり読み込む行がなくなる）と終了する
            while (null != (line_buffer = br.readLine())) {

            // ファイルに情報を一行ずつ書き込む
                bw.write(line_buffer + "\n");
            }

            // http接続やHTML情報の読み込み、書き込みを終了する記述
            br.close();
            isr.close();
            http.disconnect();
            bw.close();

            // 不具合が生じたらエラーを出す
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        try (BufferedReader br = new BufferedReader(new FileReader(homeDir +  filePath))) {

            // 読み込んだ各行を格納するための変数を用意
            String line;
            {
                // 読み込む行がなくなるまで実行
                while ((line = br.readLine()) != null) {

                    // 本文部分に差し掛かる
                    if (line.startsWith("              <div class=\"p-article__body\">")) {
                        while ((line = br.readLine()) != null) {

                            // 本文部分が終わる
                            if (line.startsWith("              </div>")) {
                                break;
                            }

                            // 本文中のHTMLタグなどを正しい表記に変換する
                            line = line.replaceAll("<p>", "");
                            line = line.replaceAll("</p>", "");
                            line = line.replaceAll("&#039;", "\'");
                            line = line.replaceAll("&quot;", "\"");

                            // コンソールに出力する
                            System.out.println(line);


                        }

                    }
                }
            }
        } catch (IOException e) {
            System.out.println("エラーが起きました。");
            e.printStackTrace();
        }
    }
}
```

## 6.今後の課題と締めくくり

さて、ここまでニュース記事を綺麗にコンソール出力させる処理を書いてきましたが、今回はざっと以下のような課題が残ります。

- 一度HTMLをテキストファイルに書き出す必要はなさそう
- HTMLエンティティの変換方法はもっといいやり方があるかも
- 出力された記事本文が文ごとの開業となっており、一行が長くなってしまっている

まずは記事冒頭でも書いたように、HTML自体をわざわざテキストファイルに書き出す必要はなく、直接読み込み〜整形をしてしまえばよい！というところです。HTML自体よりも整形した記事本文をファイル出力した方が活用しやすそうな気もしますね。

HTMLエンティティの変換については今回ケースバイケースでひとつづつ置き換えていきましたが、一括で文字コードを変えるようなイメージで上手い変換の方法があるのではないかと予想しています。更なる調査が必要です。

最後の点はコンソール出力の際に、一行が何文字に達したら改行するなどの処理を入れることで簡単に実装できるのではないかと踏んでいます。

ということで以上、NHK World Newsの記事を取得してコンソールに出力する処理を書いてみました。現状では普通にサイトから記事を読んだほうが確実に楽、ということになってしまっていますので、今後の活用方法次第かなと考えています。例によって何かお気づきの点がございましたらどしどしご指摘ください！

参考にした記事：
[【Java入門】BufferedReaderでテキストをまとめて読み込む(readLine)](https://www.sejuku.net/blog/49981)
[PHPとJavaScriptでHTMLエンティティを扱う時のおさらい](https://qiita.com/ka215/items/ace36f55c3ad1297de81)
[Java : WEB上のテキストファイルを取得する](https://logicalerror.seesaa.net/article/128919218.html)
[ファイルを作成する(Files.createFile)](https://www.javadrive.jp/start/file/index4.html)
[Java で大きなテキストファイルを一行ずつ読み込む方法](https://www.delftstack.com/ja/howto/java/how-to-read-a-large-text-file-line-by-line-in-java/)
[【Java入門】exitでプログラムを終了させる(returnとの違いも解説)](https://www.sejuku.net/blog/52096)
[テキストファイルの入出力 - まとめてファイルに書き込む](https://www.javadrive.jp/start/stream/index6.html)
[HTTPURLCONNECTIONを使用してHTTP通信を行う](https://techbooster.org/android/application/6812/)
[クラスInputStreamReader](https://docs.oracle.com/javase/jp/8/docs/api/java/io/InputStreamReader.html)
[Javaで~(チルダ)は使えない！](https://qiita.com/cappyzawa/items/69a195326359bedf91d1)