Javaを利用したスクレイピング実践②

前回の記事はこちら
（https://qiita.com/suisen/items/a856c06accdab922153c）

##Javaを利用したスクレイピング②
参考/サイト
Javaちょこっとリファレンス さま　（https://java-reference.com/java_string_tonumber.html ）
TECH PROJIN さま　（https://tech.pjin.jp/blog/2017/10/17/【java】CSV出力のサンプルコード/ ）
Samurai ヤマシタ さま　（https://www.sejuku.net/blog/20746 ）
Let's プログラミング さま ( https://www.javadrive.jp/start/stream/index6.html )

###前回のおさらい
①スクレイピングがしたいなって思った。
②jsoup.jarをダウンロード、設定してサンプルコードを作成した。
③実際にYahoo!やほかのサイトでタグ指定から文字を抜き取れることを確認した。

###今回したいこと
①netKeiba（http://www.netkeiba.com/ ）さまより競馬などの結果情報取得
②Excelなどで利用できるcsvファイルに書き出し

###本編
ということで、実際に組んでみました。
まだまだ未熟なので時間やら効率やらは度外視。
とりあえず取得することに意義があると思っている。
またスクレイピングは相手先に負荷をかける行為ともよく聞くのでやりすぎには注意。

まずはコードから(本来はクラス分けたりしないとなんだろうけど、サンプルなので一つにまとめてます）

```java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class Test6 {

	/**
	 * csv形式で書き出す準備⇒URLの指定⇒jsoupでWebスクレイピング⇒JavaBeans化してリスト
	 * ⇒リストをcsv形式で書き出す⇒catch文で例外処理⇒クローズの流れ
	 */
	public static void main(String[] args) {

		//実行時間計測用
		long start = System.currentTimeMillis();

		//csv書き出しの初期化
		BufferedWriter bw=null;


		//try-catch文が必要
		try {
			//書き出し先ファイルの名前を先頭列を記述
			bw=new BufferedWriter(new FileWriter("D:\\sakura\\ScrapingHtml\\scraping.csv", true));
			bw.write("馬名,日付,開催,天気,レース名,馬番,人気,着順,騎手,距離,状態,タイム");
			bw.newLine();

			//urlの末文を数字で入れ替える。
			for(int j = 2015100001; j<=2015100010; j++) {

				//別で用意したTestBeansに格納するListを生成
				List<TestBeans> list = new ArrayList<TestBeans>();

				//Document A = Jsoup.connect("url").get(); urlにスクレイピング対象
				Document doc = Jsoup.connect("http://db.netkeiba.com/horse/"+j).get();

				//Elements B = A.select("タグ"); この形でソースに含まれるタグで指定された範囲を書き出す。
				Elements elm = doc.select("tbody tr");
				Elements title = doc.select("title");

				//馬名を表示させる準備
				//文字数を大まかにとって、そこから空白があったらそこまでで切り取る。
				String tstr = title.text().substring(0, 10);
				int i = tstr.indexOf(" ");

				if(i==-1) {
					i=10;
				}
				String tstrs = tstr.substring(0, i);

				//以下で使用するために文字列を初期化しておく
				String str=null;
				//拡張for文でElementsをTestBeansに格納
				for(Element a : elm) {
					str = a.text();
					//除外とか中止とか出走取消は対象外にしたいので
					if(str.indexOf("除")!=-1 || str.indexOf("取")!=-1 ||str.indexOf("中")!=-1) {
						continue;
					}
					//欲するのはレース情報のみだが、上記のタグの指定だけだと絞り切れなかったので、文字数で判別した。
					if(str.length()>=70) {
						String hairetsu[] = str.split(" ");
						TestBeans bean = new TestBeans();
						bean.setDate(hairetsu[0]);
						bean.setPlace(hairetsu[1]);
						bean.setWeather(hairetsu[2]);
						bean.setRaceName(hairetsu[4]);
						bean.setHorseNo(Integer.parseInt(hairetsu[7]));
						bean.setFamous(Integer.parseInt(hairetsu[9]));
						bean.setScore(Integer.parseInt(hairetsu[10]));
						bean.setJockey(hairetsu[11]);
						bean.setCycle(hairetsu[13]);
						bean.setSituation(hairetsu[14]);
						bean.setTime(hairetsu[16]);

						//listに格納
						list.add(bean);
					}
				}

				//拡張for文でcsvファイルに書き出し。分かりやすいようにカンマ区切り
				for(TestBeans tb : list) {
					bw.write(tstrs);
					bw.write(",");
					bw.write(tb.getDate()+","+tb.getPlace()+","+tb.getWeather()+","+tb.getRaceName()+","+tb.getHorseNo()+","+tb.getFamous()+","+tb.getScore()+","+tb.getJockey()+","+tb.getCycle()+","+tb.getSituation()+","+tb.getTime());
					bw.newLine();
				}
			}
			//close処理。
			bw.close();
			System.out.println("完了");

		//例外処理
		}catch(IOException e) {
			e.printStackTrace();
		}catch(NumberFormatException e) {
			e.printStackTrace();

		//念のためfinally文で確実にcloseできるよう図る。必要かどうかは不明。
		}finally {
			try {
				if(bw!=null) {
					bw.close();
				}
			}catch(IOException e) {
				e.printStackTrace();
			}
		}

		//実行時間計測用
		long end = System.currentTimeMillis();
		System.out.println((end - start)  + "ms");
		System.out.println((end-start)/1000 + "秒");
	}
}
```
これの結果はきちんとcsvファイルで書き出される。
![2.PNG](https://qiita-image-store.s3.amazonaws.com/0/304664/541c6d8e-acb8-03f7-60ae-7b5a5f3f87c3.png)
![3.PNG](https://qiita-image-store.s3.amazonaws.com/0/304664/953f4655-e1a4-871e-f508-c05986cc916e.png)
時間かかりすぎっていうのは重々承知してます。



####補足説明
前回のコードから付け足したのはそんなに多くないです。

#####実行時間計測
まず実行速度を計測するためだけに

		//実行時間計測用
		long start = System.currentTimeMillis();



		//実行時間計測用
		long end = System.currentTimeMillis();
		System.out.println((end - start)  + "ms");
		System.out.println((end-start)/1000 + "秒");

を導入。
これは計測するだけなので今回の趣旨とは無関係です。


#####csv形式に書き出し
		//csv書き出しの初期化
		BufferedWriter bw=null;

			//close処理。
			bw.close();
			System.out.println("完了");

		//例外処理
		}catch(IOException e) {
			e.printStackTrace();
		}catch(NumberFormatException e) {
			e.printStackTrace();

		//念のためfinally文で確実にcloseできるよう図る。必要かどうかは不明。
		}finally {
			try {
				if(bw!=null) {
					bw.close();
				}
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
BufferedWriterクラスで外部に書き出しします。
最初に初期化のnullを設定しておくと便利です。
詳しくはまた後で書きますが、必ずクローズしないといけないので、大きいfor文が終わった直後にクローズ。
終わったことを明示するためにコンソールに「完了」と表示するようにもしました。

また例外処理が必要なので、IOExceptionでまとめてとってしまいます。
一緒に入れてるNumberFormatExceptionに関してはString型をInteger型に変換しているので、その時の例外です。
closeはしてますが、必ずできるか心配だったのでfinally文内にも記述。
ここに関しては必要があるのかも微妙だし、上手く書く方法があるようにも思われる。

#####urlの繰り返し処理
今回使用したnetkeibaさまは馬のページの場合urlの最後は数字で順番通りに入っているようだったので、for文で繰り返せばいいじゃんと。

```java
			//urlの末文を数字で入れ替える。
			for(int j = 2015100001; j<=2015100010; j++) {

				//別で用意したTestBeansに格納するListを生成
				List<TestBeans> list = new ArrayList<TestBeans>();

				//Document A = Jsoup.connect("url").get(); urlにスクレイピング対象
				Document doc = Jsoup.connect("http://db.netkeiba.com/horse/"+j).get();
```

#####主体
ここがメインの処理。
JavaBeansを利用して読み取ったものリストに格納してます。
今回使用したJavaBeansは以下の通り。

```java
public class TestBeans {

	private String date;
	private String place;
	private String weather;
	private int race;
	private String raceName;
	private int member;
	private int groupNo;
	private int horseNo;
	private float oz;
	private int famous;
	private int score;
	private String jockey;
	private int kinryo;
	private String cycle;
	private String situation;
	private String time;
	private int weight;
	public String getDate() {
		return date;
	}
	public void setDate(String date) {
		this.date = date;
	}
	public String getPlace() {
		return place;
	}
	public void setPlace(String place) {
		this.place = place;
	}
	public String getWeather() {
		return weather;
	}
	public void setWeather(String weather) {
		this.weather = weather;
	}
	public int getRace() {
		return race;
	}
	public void setRace(int race) {
		this.race = race;
	}
	public String getRaceName() {
		return raceName;
	}
	public void setRaceName(String raceName) {
		this.raceName = raceName;
	}
	public int getMember() {
		return member;
	}
	public void setMember(int member) {
		this.member = member;
	}
	public int getGroupNo() {
		return groupNo;
	}
	public void setGroupNo(int groupNo) {
		this.groupNo = groupNo;
	}
	public int getHorseNo() {
		return horseNo;
	}
	public void setHorseNo(int horseNo) {
		this.horseNo = horseNo;
	}
	public float getOz() {
		return oz;
	}
	public void setOz(float oz) {
		this.oz = oz;
	}
	public int getFamous() {
		return famous;
	}
	public void setFamous(int famous) {
		this.famous = famous;
	}
	public int getScore() {
		return score;
	}
	public void setScore(int score) {
		this.score = score;
	}
	public String getJockey() {
		return jockey;
	}
	public void setJockey(String jockey) {
		this.jockey = jockey;
	}
	public int getKinryo() {
		return kinryo;
	}
	public void setKinryo(int kinryo) {
		this.kinryo = kinryo;
	}
	public String getCycle() {
		return cycle;
	}
	public void setCycle(String cycle) {
		this.cycle = cycle;
	}
	public String getSituation() {
		return situation;
	}
	public void setSituation(String situation) {
		this.situation = situation;
	}
	public String getTime() {
		return time;
	}
	public void setTime(String time) {
		this.time = time;
	}
	public int getWeight() {
		return weight;
	}
	public void setWeight(int weight) {
		this.weight = weight;
	}



}
```
Eclipseは優秀ですよね。
privateで型と名称を設定すれば、あとは自動でゲッターとセッターを設定してくれるんですから。
ありがたい。

これを以下の処理でリストに格納する。

```java
				//以下で使用するために文字列を初期化しておく
				String str=null;
				//拡張for文でElementsをTestBeansに格納
				for(Element a : elm) {　　　㋐
					str = a.text();   ㋑
					//除外とか中止とか出走取消は対象外にしたいので
					if(str.indexOf("除")!=-1 || str.indexOf("取")!=-1 ||str.indexOf("中")!=-1) {
						continue;
					}　　　㋒
					//欲するのはレース情報のみだが、上記のタグの指定だけだと絞り切れなかったので、文字数で判別した。
					if(str.length()>=70) {
						String hairetsu[] = str.split(" ");　　㋓
						TestBeans bean = new TestBeans();
						bean.setDate(hairetsu[0]);
						bean.setPlace(hairetsu[1]);
						bean.setWeather(hairetsu[2]);
						bean.setRaceName(hairetsu[4]);
						bean.setHorseNo(Integer.parseInt(hairetsu[7]));
						bean.setFamous(Integer.parseInt(hairetsu[9]));
						bean.setScore(Integer.parseInt(hairetsu[10]));
						bean.setJockey(hairetsu[11]);
						bean.setCycle(hairetsu[13]);
						bean.setSituation(hairetsu[14]);
						bean.setTime(hairetsu[16]);　　㋔

						//listに格納
						list.add(bean);　　㋕
					}
				}

```
なんてことはないですね。
㋐　読み取った複数文からなるelm(Elements型)を拡張for文で短文からなるa(Element型）にする。
㋑　あらかじめ初期化しておいたstr(String型)に設定。
㋒　除外したい情報が含まれている場合は、continueしてfor文から抜け出す処理をする。
㋓　一文が空白で分けられているので、それらをString型配列として取得。
㋔　配列の中から必要なところをTestBeansのセッターを利用してTestBeansに格納。適宜String型⇒Integer型に変更。
㋕　TestBean型のリストにaddする。

もっとスマートに書くのは現段階の私の力量では無理でした。


細かいところはこのくらいでしょうか。
ともあれこれでcsv形式で出力ができました。
まだ10頭とかでしか試していないので、もう少し大きい数でどのくらいの時間かかるか見てみたいですね。

何かお気づきの点、変なところ、もっといい方法あればコメント等で教えてください。


###追記
saka1029さまよりありがたい助言コメントいただいてます。
たしかに文字数とかで判別するより楽ですよね～。
精進します。

あと、競争結果画面をスクレイピングしたほうが簡単だと気付いたけど、気づかないふりしてます。