Goで株価をスクレイピングしてcsvファイルに出力するまで

**1. 株価などのデータを提供しているサイトからまずはドキュメントをGetで取得してきて**
**2.そこから必要なデータを取り出して**
**3. 取り出したデータをcsvファイルに吐き出す**

ってところまでを、Goで書いた記事です。
自分の整理用の記事ですが、誰かの参考になれば嬉しいです。

Goは普段書かないので、もしもっといい書き方とかあればコメントいただけると助かります。:rolling_eyes:

## 準備
- ライブラリのインポート
    - [goquery](https://github.com/PuerkitoBio/goquery)をインポート （GoでjQuery的にスクレイピングやクローリングができるライブラリです）

- スクレイピングするサイトの確認
    - Goに限らず当然ですが、スクレイピングをしてよいサイトかどうかは、事前にしっかり確認（各サイトのrobots.txtを見ることでその確認ができます）

調べた結果、ありがたいことにこちらのサイトで株価のデータを提供してくださっているので、こちらを今回は利用させていただいています。
[:moneybag:株式投資メモ 株価DB:moneybag:](https://kabuoji3.com/)

**今回の目的は、上記のサイトにスクレイピングを行って、その日の出来高上位100位でかつ終値が2000円以下の銘柄の1年分の株データを取得して、csvファイルの形にすることです。**

## Getリクエスト
まずは、NewDocumentで対象のURLからドキュメントを取得するところ。
`goquery.NewDocument()`の中に対象URLを書きます。
今回は、URLの中に日付が含まれるので、timeパッケージを使用して現在の日付を指定するようにしております。

```go
day := time.Now()
doc, err := goquery.NewDocument(fmt.Sprintf("https://kabuoji3.com/ranking/?date=%s&type=3&market=1", day.Format(layout)))
	if err != nil {
		log.Fatalln(err)
	}
 log.Println(doc)
```

これで出来高ランキングの上位100位の銘柄のデータを取得できました。:smile:

## 取得したデータの操作
次に、取得したデータすべてのうち、条件を満たす株の銘柄コードの配列を生成するように操作を加えます。

対象ページのHTMLの構成を調べると、テーブルになっており、trタグの中のthタグにテーブルのヘッドがあり、trタグの中のtdタグにデータが入っている構成でした。

以下の流れで銘柄を絞り込みます。

1. 銘柄コードと株価（終値）をマップに
2. 株価（終値）が2000円以下だけの銘柄コードを配列に

```go

tr := doc.Find("tr")
	var codeName string
	var value float64
	codeMap := make(map[string]float64)

	// 1. 銘柄コードと株価（終値）をmapに
	tr.Each(func(index int, s *goquery.Selection) {
		s.Find("td").Each(func(index int, s *goquery.Selection) {
			switch index {
			case 1:
				codeName = s.Text()[:4]
			case 3:
				value, _ = strconv.ParseFloat(s.Text(), 32)
			}
		})
		codeMap[codeName] = value
	})

	// 2. 株価（終値）が2000円以下だけの銘柄コードを配列に
	var data []string
	for key, value := range codeMap {
		if value < limitValue {
			data = append(data, key)
		}
	}
```

これで、その日の出来高上位100位でかつ終値が2000円以下の銘柄コードの配列を生成できました。:relaxed:

## 各銘柄の1年分の株データを取得
取得したい銘柄の株価データは、`"http://・・・・/銘柄コード/年”`
という形式になっていたので、こんな感じ銘柄コードと年を指定して取得します。

```go
doc, err := goquery.NewDocument(fmt.Sprintf("http://kabuoji3.com/stock/%s/%d/", stockCode, year))
	if err != nil {
		log.Fatalln(err)
	}
```
あとはこれを銘柄コードの配列の長さだけfor文で繰り返して取得すればよいのですが、スクレイピングさせていただいているサイトへの負荷を配慮してスリープを忘れずに入れます。:sleeping:

```go
time.Sleep(time.Second * 1)
```
これで1秒に1回リクエストが送られようになります。

## csvファイルに出力
最後に取得した各銘柄のデータを以下のようなテーブルをもつ
csvファイルに書き出すよう処理を追加します。

|date|open|high|low|close|volume|closing_adustment|
|:---|---:|---:|---:|---:|---:|---:|
|2020-01-01|xxx|xxx|xxx|xxx|xxx|xxx|
|2020-01-02|xxx|xxx|xxx|xxx|xxx|xxx|
|2020-01-03|xxx|xxx|xxx|xxx|xxx|xxx|
|：|xxx|xxx|xxx|xxx|xxx|xxx|

スクレイピングして取得したcsvファイルを分析などで使用することを考えたとき、ファイルに日本語文字を入れたくなかったので、ヘッダーとなる部分を意図的に英語にしています。
open:始値, high:高値, low:安値, close:終値, volume:出来高, closing_adustment:終値調整
（英訳これでいいはず）

```go
// O_WRONLY:書き込みモード開く, O_CREATE:無かったらファイルを作成
file, err := os.OpenFile(fmt.Sprintf("./crawlData/%s.csv", stockCode), os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
	log.Println("Error:", err)
}
defer file.Close()
err = file.Truncate(0) // ファイルを空っぽにする(2回目以降用)

writer := csv.NewWriter(file)
var data []string
doc.Find("tr").Each(func(index int, s *goquery.Selection) {
	if index == 0 {
		data = []string{"date", "open", "high", "low", "close", "volume", "closing_adustment"}
		writer.Write(data)
	} else {
		s.Find("td").Each(func(index int, s *goquery.Selection) {
			if index == 0 {
				t, _ := time.Parse(layout, s.Text())
				data = append(data, t.Format(layout))
			} else {
				data = append(data, s.Text())
			}
		})
		writer.Write(data)
	}
	data = nil
})
writer.Flush()
}
```

ちなみに、もし日本語入力に対応したければ、writerのところを以下のようにすれば文字化けせずにファイル出力できます。

```go
// ShiftJISのエンコーダーを噛ませたWriterを作成する
writer := transform.NewWriter(sjisFile, japanese.ShiftJIS.NewEncoder())
```
## 全体のサンプルコード
最後に全体のコードを貼っておきます。

```go:sample.go
package main

import (
	"encoding/csv"
	"fmt"
	"log"
	"os"
	"strconv"
	"time"

	"github.com/PuerkitoBio/goquery"
)

const limitValue = 2000
const layout = "2006-01-02"

func main() {
	saveStockDataFile(getStockCodeList(limitValue))
}

func getStockCodeList(limitValue float64) []string {
	day := time.Now()
	doc, err := goquery.NewDocument(fmt.Sprintf("https://kabuoji3.com/ranking/?date=%s&type=3&market=1", day.Format(layout)))
	if err != nil {
		log.Fatalln(err)
	}

	tr := doc.Find("tr")
	var codeName string
	var value float64
	codeMap := make(map[string]float64)

	// 1. 銘柄コードと株価（終値）をmapに
	tr.Each(func(index int, s *goquery.Selection) {
		s.Find("td").Each(func(index int, s *goquery.Selection) {
			switch index {
			case 1:
				codeName = s.Text()[:4]
			case 3:
				value, _ = strconv.ParseFloat(s.Text(), 32)
			}
		})
		if codeName != "" {
			codeMap[codeName] = value
		}
	})

	// 2. 株価（終値）が2000円以下だけの銘柄コードを配列に
	var data []string
	for key, value := range codeMap {
		if value < limitValue {
			data = append(data, key)
		}
	}
	return data
}

func saveStockDataFile(codeList []string) {
	for _, stockCode := range codeList {
		doc := crawlStockData(stockCode, time.Now().Year())
		fmt.Println(doc.Find("title").Text())

		// O_WRONLY:書き込みモード開く, O_CREATE:無かったらファイルを作成
		file, err := os.OpenFile(fmt.Sprintf("./crawlData/%s.csv", stockCode), os.O_WRONLY|os.O_CREATE, 0600)
		if err != nil {
			log.Println("Error:", err)
		}
		defer file.Close()
		err = file.Truncate(0) // ファイルを空っぽにする(2回目以降用)

		writer := csv.NewWriter(file)
		var data []string
		doc.Find("tr").Each(func(index int, s *goquery.Selection) {
			if index == 0 {
				data = []string{"date", "open", "high", "low", "close", "volume", "closing_adustment"}
				writer.Write(data)
			} else {
				s.Find("td").Each(func(index int, s *goquery.Selection) {
					if index == 0 {
						t, _ := time.Parse(layout, s.Text())
						data = append(data, t.Format(layout))
					} else {
						data = append(data, s.Text())
					}
				})
				writer.Write(data)
			}
			data = nil
		})
		writer.Flush()
		time.Sleep(time.Second * 1)
	}
}

func crawlStockData(stockCode string, year int) *goquery.Document {
	doc, err := goquery.NewDocument(fmt.Sprintf("http://kabuoji3.com/stock/%s/%d/", stockCode, year))
	if err != nil {
		log.Fatalln(err)
	}
	return doc
}

```
やりたかったことは完了です。:sparkles:

goqueryには他にも色々便利なメソッドがあるので、Goでスクレイピングをサクッと書きたい場合はとても便利そうでした！

UTをこれから書く予定なので、記事は随時更新するかと思います。
最後まで読んでいただき、ありがとうございました。:bow::dog2: