戻る

Go言語スクレイピング解説

Oct 31, 2020

ネットで検索というのは情報を得る手段として最も基本的だが, スクレイピングによって他のwebサイトからある情報だけを自動的に抽出することができ, 強力なツールとなり得よう. goqueryでお手軽スクレイピング! が参考になるが, ここでは一つ一つの関数を詳しく解説する.

Goとgoqueryでスクレイピング

例えば東京都港区の天気をスクレイピングしてcsvにデータを保存したいとする. https://tenki.jp/forecast/3/16/4410/13103/ のソースを見てみると, 次のようなタグから10日間の天気の情報がわかる.



<section class="forecast-point-week-wrap">

  <h3 class="bottom-style date-set">港区の10日間天気</h3>
  <table class="forecast-point-week forecast-days-long">
    <tr>
      <th class="citydate">日付</th>      <td class="cityday">
        <p class="date-box">01月02日</p>
        <p class="youbi-box">(<span class="saturday">土</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月03日</p>
        <p class="youbi-box">(<span class="sunday">日</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月04日</p>
        <p class="youbi-box">(<span>月</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月05日</p>
        <p class="youbi-box">(<span>火</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月06日</p>
        <p class="youbi-box">(<span>水</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月07日</p>
        <p class="youbi-box">(<span>木</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月08日</p>
        <p class="youbi-box">(<span>金</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月09日</p>
        <p class="youbi-box">(<span class="saturday">土</span>)</p>
      </td>
            <td class="cityday">
        <p class="date-box">01月10日</p>
        <p class="youbi-box">(<span class="sunday">日</span>)</p>
      </td>
          </tr>

    <tr>
      <th class="ships-info">天気</th>      <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/01.png" alt="晴" title="晴" width="47" height="30"><p>晴</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/02.png" alt="晴時々曇" title="晴時々曇" width="47" height="30"><p>晴時々曇</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/12.png" alt="曇のち晴" title="曇のち晴" width="47" height="30"><p>曇のち晴</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/13.png" alt="曇のち雨" title="曇のち雨" width="47" height="30"><p>曇のち雨</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/03.png" alt="晴一時雨" title="晴一時雨" width="47" height="30"><p>晴一時雨</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/05.png" alt="晴のち曇" title="晴のち曇" width="47" height="30"><p>晴のち曇</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/02.png" alt="晴時々曇" title="晴時々曇" width="47" height="30"><p>晴時々曇</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/01.png" alt="晴" title="晴" width="47" height="30"><p>晴</p></td>
            <td class="weather-icon"><img src="https://static.tenki.jp/images/icon/forecast-days-weather/01.png" alt="晴" title="晴" width="47" height="30"><p>晴</p></td>
          </tr>

    <tr>
      <th class="ships-info">気温<br>(℃)</th>      <td><p class="high-temp">10</p><p class="low-temp">1</p></td>
            <td><p class="high-temp">8</p><p class="low-temp">1</p></td>
            <td><p class="high-temp">9</p><p class="low-temp">1</p></td>
            <td><p class="high-temp">8</p><p class="low-temp">2</p></td>
            <td><p class="high-temp">11</p><p class="low-temp">4</p></td>
            <td><p class="high-temp">9</p><p class="low-temp">1</p></td>
            <td><p class="high-temp">5</p><p class="low-temp">1</p></td>
            <td><p class="high-temp">4</p><p class="low-temp">-2</p></td>
            <td><p class="high-temp">6</p><p class="low-temp">-2</p></td>
          </tr>

    <tr>
      <th class="ships-info">降水<br>確率</th>      <td><p class="precip">0<span class="unit">%</span></p>      </td>
            <td><p class="precip">30<span class="unit">%</span></p>      </td>
            <td><p class="precip">40<span class="unit">%</span></p>      </td>
            <td><p class="precip">80<span class="unit">%</span></p>      </td>
            <td><p class="precip">80<span class="unit">%</span></p>      </td>
            <td><p class="precip">40<span class="unit">%</span></p>      </td>
            <td><p class="precip">40<span class="unit">%</span></p>      </td>
            <td><p class="precip">20<span class="unit">%</span></p>      </td>
            <td><p class="precip">20<span class="unit">%</span></p>      </td>
          </tr>

  </table><!-- /.forecast-point-week -->


</section>

タグを抽出するために, doc.Find()によってセレクションを取得する. セレクションとはあるタグで囲まれる部分全体(タグ自身も含める). セレクション.Find()によってさらに深いタグを取得していく. セレクション.Text()によってプレインテキストを取得する. タグの属性はまた別の関数で取得できる.


package main

import (
    "github.com/PuerkitoBio/goquery"
    "os"
		"time"
		"log"
		"encoding/csv"
)

type Result struct {
    days []string
		youbis []string
		weathers []string
    hightemps []string
		lowtemps []string
}

func GetPage(url string) Result {
	doc, _ := goquery.NewDocument(url)
	s := doc.Find("table.forecast-point-week").First()
	tbodies := s.Find("tbody")
	tbody := tbodies.First()
	var days []string
	var youbis []string
	var weathers []string
	var hightemps []string
	var lowtemps []string
	tbody.Find("td.cityday").Each(func(_ int, s *goquery.Selection) {
		cityday := s.Find("p.date-box").First().Text()
		youbi := s.Find("span").First().Text()
		days = append(days, cityday)
		youbis = append(youbis, youbi)
	})
	tbody.Find("td.weather-icon").Each(func(_ int, s *goquery.Selection) {
		weather := s.Find("p").First().Text()
		weathers = append(weathers, weather)
	})
	tbody.Find("p.high-temp").Each(func(_ int, s *goquery.Selection) {
		hightemp := s.Text()
		hightemps = append(hightemps, hightemp)
	})
	tbody.Find("p.low-temp").Each(func(_ int, s *goquery.Selection) {
		lowtemp := s.Text()
		lowtemps = append(lowtemps, lowtemp)
	})
	return Result{days,youbis,weathers, hightemps,lowtemps}
}


func main() {
		urls := []string{"https://tenki.jp/forecast/1/2/1400/1100/", "https://tenki.jp/forecast/2/7/3410/4100/", "https://tenki.jp/forecast/3/16/4410/13103/"}
		locations := []string{"sapporo", "sendai", "minatoku", }

		for index, url := range urls {
			result := GetPage(url)
			f ,err := os.Create(time.Now().Local().Format("2006010215") + locations[index])
			if err != nil {
				log.Fatal(err)
			}
			defer f.Close()
			w := csv.NewWriter(f)
			w.Write(result.days)
			w.Write(result.youbis)
			w.Write(result.weathers)
			w.Write(result.hightemps)
			w.Write(result.lowtemps)
			w.Flush()
		}
	
}


自動で定期的に取得する

スクレイピングは自動的処理なので, 当然これを実行するのも自動的に行いたい場合も多い. systemdのデーモンに次のように登録する.