転職ドラフトでどの時間帯に指名が多いのか調べてみた

# 転職ドラフトに参加してみたけど
いつ指名がくるんだろう、とそわそわ...
使ったことある人に聞くと最終日にどっと来るらしい
本当に？
調べてみよう

# スクレイピングどうしよう
調べてみたけどNodeとかpythonとかGoがいいとか？
特にメリットはないんだろうけど最近ちょっと興味のあるGoでググりながらやってみる

# Goでスクレイピング
goqueryを使ってみる

```shell

go get github.com/PuerkitoBio/goquery
```

過去の開催回全部集めるのは一旦あきらめて
第10回に絞ってみる

ソース

```golang:draft.go

package main

import (
    "github.com/PuerkitoBio/goquery"
    "fmt"
    "regexp"
    "time"
    "strings"
    "strconv"
)


func main() {
    baseUrl := "https://job-draft.jp"
    userOfferInfo := []string{}
    offerTimes := []string{}

    // リンク集取得
    // fesLinks := getFestivalLinks()
    // fmt.Println(fesLinks)

    // 要素取得
    // fesLinksをforeachしてbaseUrlとくっつける
    fesInfo := getFesInfo(baseUrl + "/festivals/10")
    fmt.Println(fesInfo[0])
    fmt.Println("参加人数 : " + fesInfo[1])
    fmt.Println("総指名数 : " + fesInfo[3])

    // ユーザ一覧取得
    userLinks := getUserInfo(baseUrl, "/festivals/10/users")
    fmt.Println("取得したユーザ数 : " + strconv.Itoa(len(userLinks)))

    // 各ユーザのオファー情報まとめて取得
    fmt.Println("各ユーザのオファー情報取得")
    for i := 0; i < len(userLinks); i++ {
        userOfferInfo_tmp := getUserOfferLinks(baseUrl + userLinks[i], "10")
        userOfferInfo = append(userOfferInfo, userOfferInfo_tmp...)
        fmt.Println(strconv.Itoa(i) + " / " + strconv.Itoa(len(userLinks)) + "人")
    }

    // オファー情報から時間取得
    for i := 0; i < len(userOfferInfo); i++ {
        offerTime := getOfferTime(baseUrl + userOfferInfo[i])
        offerTimes = append(offerTimes, offerTime)
        fmt.Println(strconv.Itoa(i) + " / " + strconv.Itoa(len(userOfferInfo)) + "オファー")
    }

    fmt.Println("全オファー時間")
    fmt.Println(offerTimes)
}

/**
 * 過去の転職ドラフト開催回リンク集配列を取得
 */
func getFestivalLinks() ([]string) {
    time.Sleep(1 * time.Second)
    url := "https://job-draft.jp/festivals/"
    fesLinks := []string{}

    doc, err := goquery.NewDocument(url)
    if err != nil {
        panic(err)
    }

    // /festivals/[数値] のリンクを探す
    reg := regexp.MustCompile(`\/festivals\/[0-9]+\z`)
    doc.Find("a").Each(func(_ int, sel *goquery.Selection) {
        url, _ := sel.Attr("href")
        if reg.MatchString(url) {
            fesLinks = append(fesLinks, url)
        }
    })
    
    return fesLinks
}

// 開催回の情報取得
func getFesInfo(fesUrl string) ([]string) {
    time.Sleep(1 * time.Second)
    fesInfo := []string{}

    doc, err := goquery.NewDocument(fesUrl)
    if err != nil {
        panic(err)
    }

    // 開催期間
    doc.Find(".ibox-content").Each(func(_ int, sel *goquery.Selection) {
        text := sel.Find("h3").Text()
        if (strings.Contains(text, "開催日")) {
            fesInfo = append(fesInfo, text)
        }
    })

    // 参加人数/参加者数/総指名数/提示年収総額
    doc.Find(".col-sm-3.col-with-border__col").Each(func(_ int, sel *goquery.Selection) {
        text := sel.Find("span.emphasis-40").Text()
        fesInfo = append(fesInfo, text)
    })
    
    return fesInfo
}

// ユーザ集取得
func getUserInfo(baseUrl string, usersPath string) ([]string) {
    time.Sleep(1 * time.Second)
    userLinks := []string{}
    nextUrl := ""
    usersUrl := baseUrl + usersPath
    fmt.Println(usersUrl)

    doc, err := goquery.NewDocument(usersUrl)
    if err != nil {
        panic(err)
    }

    // ユーザリンクと次ページリンクあれば再帰的に探す
    reg := regexp.MustCompile(`\/users\/[0-9]+\z`)
    doc.Find("a").Each(func(_ int, sel *goquery.Selection) {
        // ユーザリンク探す
        url, _ := sel.Attr("href")
        if reg.MatchString(url) {
            userLinks = append(userLinks, url)
        }

        // 次ページリンク探す
        rel, _ := sel.Attr("rel")
        if rel == "next" {
            nextUrl = url
        }
    })

    // 再帰的に探す
    if nextUrl != "" {
        nextUserLinks := getUserInfo(baseUrl, nextUrl)
        userLinks = append(userLinks, nextUserLinks...)
    }
    
    return userLinks
}

// ユーザのオファー詳細へのリンク取得
func getUserOfferLinks(userUrl string, fesNum string) ([]string) {
    time.Sleep(1 * time.Second)
    offerLinks := []string{}

    doc, err := goquery.NewDocument(userUrl)
    if err != nil {
        panic(err)
    }

    // 各回オファー取得
    doc.Find("div.ibox").Each(func(_ int, sel *goquery.Selection) {
        text := sel.Text()
        // 指定回のオファーの中でオファーのリンク取得
        if (strings.Contains(text, "第" + fesNum + "回 指名")) {
            sel.Find("a").Each(func(_ int, sel2 *goquery.Selection) {
                url, _ := sel2.Attr("href")
                if strings.Contains(url, "biddings") {
                    offerLinks = append(offerLinks, url)
                }
            })
        }
    })

    return offerLinks
}

// オファーの時間を取得
func getOfferTime(offerUrl string) (string){
    time.Sleep(1 * time.Second)

    offerTime := ""
    doc, err := goquery.NewDocument(offerUrl)
    if err != nil {
        panic(err)
    }

    doc.Find("p > span.u-m-r-25").Each(func(_ int, sel *goquery.Selection) {
        offerTime = sel.Text();
    })

    return offerTime
}
```

# データをパース
goが最後に配列をどかんと吐いてくれるのでそれをテキストに写して
行単位のデータにする

```:offertimes.txt
2018.01.31 15:34
2018.01.20 13:35
2018.01.29 17:59
2018.01.31 18:54
2018.01.31 11:50
2018.01.20 15:01
2018.01.31 09:22
2018.01.31 11:56
2018.01.23 12:20
2018.01.31 14:54
2018.01.31 12:07
2018.01.31 22:57
2018.01.30 17:53
2018.01.29 15:17
2018.01.31 17:33
2018.01.31 22:53
2018.01.31 21:35
2018.01.31 11:32
.
.
.

```

あとは使い慣れてるphpで処理

```php:offerTimeParse.php
<?php
    $parseData = array();

    $file = file_get_contents("./offertimes.txt");
    $offerTimeList = explode(PHP_EOL, $file);

    foreach ($offerTimeList as $key => $offerTime) {
　　　　　// 年月日がドット区切りなのでスラッシュへ変換
        $offerTime = str_replace(".", "/", $offerTime);

        $timekey = date("Y-m-d H:00", strtotime($offerTime));
        $parseData[$timekey] = isset($parseData[$timekey]) ? $parseData[$timekey] + 1 : 1;
    }

    ksort($parseData);
    foreach ($parseData as $key => $value) {
        echo $key . "," . $value . "\n";
    }
```

```shell
php offerTimeParse.php > time.csv
```

# 集計したデータ
ってことでデータを見ると

| 日時           | オファー数 |
|:---------------|:----|
|2018-01-17 15:00|4|
|2018-01-17 16:00|4|
|2018-01-17 17:00|6|
|2018-01-18 09:00|3|
|2018-01-18 18:00|1|
|2018-01-18 21:00|1|
|2018-01-19 06:00|1|
|2018-01-19 13:00|2|
|2018-01-19 14:00|2|
|2018-01-19 15:00|1|
|2018-01-19 16:00|1|
|2018-01-19 17:00|3|
|2018-01-19 18:00|1|
|2018-01-20 13:00|2|
|2018-01-20 14:00|1|
|2018-01-20 15:00|1|
|2018-01-21 10:00|2|
|2018-01-22 15:00|1|
|2018-01-22 16:00|3|
|2018-01-22 17:00|1|
|2018-01-23 10:00|2|
|2018-01-23 11:00|5|
|2018-01-23 12:00|5|
|2018-01-23 13:00|2|
|2018-01-23 15:00|4|
|2018-01-23 16:00|4|
|2018-01-23 17:00|2|
|2018-01-23 18:00|3|
|2018-01-23 19:00|1|
|2018-01-23 20:00|1|
|2018-01-24 15:00|1|
|2018-01-24 17:00|2|
|2018-01-24 18:00|2|
|2018-01-25 11:00|2|
|2018-01-25 12:00|3|
|2018-01-25 13:00|1|
|2018-01-25 16:00|2|
|2018-01-25 17:00|1|
|2018-01-25 18:00|7|
|2018-01-26 11:00|1|
|2018-01-26 12:00|1|
|2018-01-26 14:00|2|
|2018-01-26 15:00|5|
|2018-01-26 17:00|20|
|2018-01-26 18:00|7|
|2018-01-26 20:00|7|
|2018-01-27 03:00|1|
|2018-01-27 19:00|1|
|2018-01-28 03:00|1|
|2018-01-28 04:00|1|
|2018-01-28 11:00|1|
|2018-01-28 12:00|1|
|2018-01-29 02:00|1|
|2018-01-29 04:00|1|
|2018-01-29 11:00|2|
|2018-01-29 12:00|7|
|2018-01-29 13:00|10|
|2018-01-29 14:00|17|
|2018-01-29 15:00|13|
|2018-01-29 16:00|10|
|2018-01-29 17:00|8|
|2018-01-29 18:00|8|
|2018-01-29 19:00|10|
|2018-01-29 20:00|4|
|2018-01-29 21:00|13|
|2018-01-29 22:00|3|
|2018-01-30 00:00|1|
|2018-01-30 01:00|3|
|2018-01-30 02:00|7|
|2018-01-30 03:00|7|
|2018-01-30 04:00|4|
|2018-01-30 10:00|9|
|2018-01-30 11:00|4|
|2018-01-30 12:00|10|
|2018-01-30 13:00|16|
|2018-01-30 14:00|10|
|2018-01-30 15:00|13|
|2018-01-30 16:00|31|
|2018-01-30 17:00|20|
|2018-01-30 18:00|38|
|2018-01-30 19:00|20|
|2018-01-30 20:00|18|
|2018-01-30 21:00|19|
|2018-01-30 22:00|5|
|2018-01-30 23:00|4|
|2018-01-31 00:00|4|
|2018-01-31 01:00|4|
|2018-01-31 09:00|25|
|2018-01-31 10:00|44|
|2018-01-31 11:00|44|
|2018-01-31 12:00|20|
|2018-01-31 13:00|27|
|2018-01-31 14:00|15|
|2018-01-31 15:00|33|
|2018-01-31 16:00|29|
|2018-01-31 17:00|23|
|2018-01-31 18:00|36|
|2018-01-31 19:00|44|
|2018-01-31 20:00|50|
|2018-01-31 21:00|39|
|2018-01-31 22:00|19|

# 結論
最終日の夜 > 最終日の朝 > 最終日の前日
といった感じですかね

開催期間が終わるまで諦めず待ちましょう

# 注意
スクレイピングのお作法には要注意です
ガンガンアクセスしないように