【#55 エンジニア転職学習】Goでスクレイピングプロジェクト（完成）

# はじめに
富山県に住んでいるChikaといいます。
毎日投稿を目標に、バックエンドエンジニア転職に向けた学習内容をアウトプットします。

間隔が空いてしまいましたが、
スクレイピングのプロジェクトが一旦完成しました。

バックエンドエンジニアになるまでの学習内容は以前投稿した以下の記事を基にしています。


https://qiita.com/Chika110/items/ef54dddd565a0193ef44

# 本日の学習内容

* **Goで求人情報のスクレイピング　←Topics!!**


# Goで求人情報のスクレイピング
前回から取得したURL一覧の各求人情報を取得してくる機能、
その情報をMySQLからCSVファイルにエクスポートする機能を追加しました。

各ファイルのおおまかな役割は以下になります。
```
job_offer_scraping/
├── main.go           --> DBコネクタ起動、スクレイピング、CSV出力実行
├── csv
│   └── csv.go        --> MySQLのmynavi_jobsテーブルからCSVを作成
├── db
│   ├── dbmodels.go   --> DBテーブル用、CSV用のstruct定義
│   └── base.go       --> MySQLのコネクタ作成、スクレイピング時のテーブルリセット、マイグレーション
└── scrape
    └── scrape.go     --> URL一覧、各求人情報のスクレイピング
```

最終的なコードです。エラー処理等できていない箇所が多々ありますがご容赦ください。
```main.go
package main

import (
	"job_offer_scraping/csv"
	"job_offer_scraping/db"
	"job_offer_scraping/scrape"
)

func main() {
	dbconn := db.DbConnect()
	scrape.SaveURLs(dbconn)
	scrape.SaveJobs(dbconn)
	csv.PutCSV(dbconn)
}
```
```dbmodels.go
package db

import (
	"gorm.io/gorm"
)

type Mynavi_url struct {
	gorm.Model
	URL string
}

type Mynavi_job struct {
	gorm.Model
	URL          string
	Title        string
	Company_name string
	Features     string
}

type CSV_job struct {
	ID           uint64
	URL          string
	Title        string
	Company_name string
	Features     string
}}
```
```base.go
package db

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func DbConnect() *gorm.DB {
	var dsn = "USER:PASS/job_offer_scraping?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		panic("failed connecting DB")
	}
	db.Migrator().DropTable(&Mynavi_url{})
	db.AutoMigrate(&Mynavi_url{})

	db.Migrator().DropTable(&Mynavi_job{})
	db.AutoMigrate(&Mynavi_job{})

	return db
}
```
```scrape.go
package scrape

import (
	"job_offer_scraping/db"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/PuerkitoBio/goquery"
	"gorm.io/gorm"
)

var urllist []db.Mynavi_url   //mysqlのINSERT用
var urls []string
var joblist []db.Mynavi_job   //mysqlのINSERT用
var host = "https://tenshoku.mynavi.jp"

func SaveURLs(dbconn *gorm.DB) {
    //取得したいページ数分指定
	for i := 1; i < 2; i++ {
		indexurl := host + "/list/pg"
		indexurl += strconv.Itoa(i)
		indexurl += "/"

		res, err := http.Get(indexurl)
		if err != nil {
			log.Fatal(err)
		}
		defer res.Body.Close()
		if res.StatusCode != 200 {
			log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
		}

		doc, err := goquery.NewDocumentFromReader(res.Body)
		if err != nil {
			log.Fatal(err)
		}

		doc.Find("div.cassetteRecruit").Each(func(i int, s *goquery.Selection) {
			href, _ := s.Find("p.cassetteRecruit__copy > a").Attr("href")
			href = string([]rune(href))
			url := strings.SplitAfterN(href, "/", 3)
			trimurl := strings.Join(url[0:2], "")

			urls = append(urls, trimurl)

			mynaviurl := db.Mynavi_url{URL: trimurl}
			urllist = append(urllist, mynaviurl)
		})
		time.Sleep(30 * time.Second)
	}
	dbconn.Create(&urllist)
}

func SaveJobs(dbconn *gorm.DB) {
    //urls[:2]は動作確認用に最小にしているだけなので実際は指定なしでOK
	for _, v := range urls[:2] {
		pageurl := host + v
		mynavijob := db.Mynavi_job{}

		res, err := http.Get(pageurl)
		if err != nil {
			log.Fatal(err)
		}
		defer res.Body.Close()
		if res.StatusCode != 200 {
			log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
		}

		doc, err := goquery.NewDocumentFromReader(res.Body)
		if err != nil {
			log.Fatal(err)
		}

		var ftlist []string
		doc.Find("body").Each(func(i int, s *goquery.Selection) {
			title := s.Find(".occName").Text()
			companyname := s.Find(".companyName").Text()
			s.Find("div.cassetteOfferRecapitulate__content--2colLarge.cassetteOfferRecapitulate__content--2colLargeJobinfo > ul > li").Each(func(i int, s *goquery.Selection) {
				ft := s.Find("span").Text()
				ftlist = append(ftlist, ft)
			})
			joinFtlist := strings.Join(ftlist, ",")

			mynavijob = db.Mynavi_job{URL: pageurl, Title: title, Company_name: companyname, Features: joinFtlist}
		})

		joblist = append(joblist, mynavijob)
		time.Sleep(30 * time.Second)
	}
	dbconn.Create(&joblist)
}
```
```csv.go
package csv

import (
	"encoding/csv"
	"job_offer_scraping/db"
	"os"
	"strconv"

	_ "github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
)

func PutCSV(dbconn *gorm.DB) {
	file, err := os.Create("mynavi_jobs.csv")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	cw := csv.NewWriter(file)
	defer cw.Flush()

	header := []string{"ID", "URL", "Title", "Company_name", "Features"}
	cw.Write(header)

	rows, err := dbconn.Raw("select `id`, `url`, `title`, `company_name`, `features` from mynavi_jobs").Rows()
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	for rows.Next() {
		j := db.CSV_job{}
		err := rows.Scan(&j.ID, &j.URL, &j.Title, &j.Company_name, &j.Features)
		if err != nil {
			panic(err)
		}
		strID := strconv.FormatUint(j.ID, 10)
		col := []string{strID, j.URL, j.Title, j.Company_name, j.Features}
		cw.Write(col)
	}
}
```

#### 使用している教材はこちら↓

https://www.youtube.com/watch?v=tYbVl6N8oxI&list=PLv6F1QYhG31S-sDkutIQjCnKjFTvjuUus&index=1&t=2049s

# おわりに
最後までお読みいただきありがとうございました。
アドバイス・応援コメント等いただけますと幸いです。