20200116のGoに関する記事は5件です。

【Golang】mockgen で生成した *_mock.go のせいで Build がコケる場合の対処法

対象読者

  • Go の mockgen で生成した *_mock.go ファイルのせいで Build がコケている人
  • 自分

忙しい人のための結論

Build failed 時に、怒られているコード内の該当箇所を消せばよい。

事例

REST API を golang で書いていた。
テストコードを書くべく、mockgen でモックを生成したところ、それらのファイルにいちゃもんをつけられて Build が通らなくなった。

# hoge/fuga
fuga          | hoge/fuga_mock.go:**:*: m.ctrl.T undefined (type *gomock.Contr
oller has no field or method T)

m.ctrl.T で怒られるのは gomock の code generation が悪いからではないかと推測される。下記リンクによると gomock の version が関係していそう。
参考:https://github.com/openshift/openshift-azure/issues/870

下記リンクによると、同様の問題(m.ctrl.T undefined)が発生したが、gomock を v1.2.0 にしたところ動いたという。原因は構造体のフィールド t の表記を T と変更したことのようだ。
https://github.com/golang/mock/issues/255

では、go get で落とした gomock, mockgen の version を変更するには?

Q. go get で落としたパッケージの version を変更する場合には?

A. $GOPATH 以下の src/ で、以下を実行し、使用するバージョン情報のブランチにチェックアウトする。

$ git tag -l #各バージョンのブランチが表示(tags/v1.2.0 など)
$ git checkout tags/v1.2.0 #v1.2.0 に切り替える場合

または glide などでパッケージ管理するといいらしい(Go version 1.5 以上)。
参考:https://qiita.com/usk81/items/8e192e68d6b18bec3b4a

だが、今回の問題に関してはより簡単な方法があった。。。

改めて結論

以下のようなメッセージが表示される箇所はコメントアウトしたところ、問題なく Build が成功した。下記の場合では、m.ctrl.T を含む行をコメントアウトした。

*****-*** | hoge/fuga_mock.go:37:8: m.ctrl.T undefined (type *gomock.Controller has no field or method T)

mockgen のバージョンを最新にすると通ったという声もある(詳細は未確認)。

余談:GOPATH が 2 つ以上指定されている場合

$GOPATH に指定した全ての箇所が有効なのは、 Go コンパイラが必要な外部のパッケージを探す場合のみ。逆に 1 つ目の GOPATH しか有効にならないのは、go get により外部のパッケージをダウンロード・インストールする場合らしい。
参考:https://qiita.com/chano2/items/ea76cc503e651f07bfb0

余談:mockgen で生成されたファイル一覧を表示

ついでに mockgen で生成したファイルの一覧を取得した。
日付表示でラフにスクリーニングし($7>15)、ファイル名に mock を含むことを要求($9 ~ /.*mock.*/)した。誰かもっとスマートな方法を教えてください。

$ ls -ulR $(find . -type f) | awk '{if($7>15 && $9 ~ /.*mock.*/){print $6" "$7" "$8" "$9}}’

余談の余談:GOPATH, GOROOT の表示

$ echo ${GOPATH}
$ echo ${GOROOT}

ではなく、

go env GOPATH
go env GOROOT

を用いた方がいい。デフォルト(環境変数として設定していない)の場合は echo では表示されないからだ。
参考:https://qiita.com/YumaInaura/items/3372186e2c11a45c65d8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで超簡易版Twitterを作って見ました(初心者向け)

はじめまして

最近Goの勉強を始めたばかりで何か開発してみようと思い、簡単なアプリを開発しました。
twitterもやってます@khm_itac

見た目

スクリーンショット 2020-01-16 11.57.40.png

出来る事

・フォームから投稿する事が出来ます
・投稿をデータベースに保存する事が出来ます
・保存されている投稿を表示してくれます
・投稿を消す事も出来ます

はい、それだけです。
めちゃくちゃ低レベルですが、UIからサーバーにデータを送りDBへ保存、DBのデータをビューに表示するというのは全てのアプリの基本的な部分だと思いますので、これが出来るとようやくエンジニアレベル1達成です!
僕と同じようにこれからGoの勉強を始める方の少しでも役に立てればと思い記事を投稿しました。
僕も初心者ですので間違ってる部分や至らない点があるかと思いますがご了承ください。

解説

Goの導入や基礎文法やVSCodeの使い方などは解説しません、僕より詳しい人がYouTube等で動画を出していますのでそちらで勉強した方がいいと思います(あとProgateとか)
Macでの開発ですのでwindowsの方はうまく動かない可能性もあります

まずmain.goを作ります
メインディレクトリの直下でいいと思います。

スクリーンショット 2020-01-16 12.59.20.png
この画面上でF5を押すとスタートさせる事が出来ます、その後http://localhost:8080 
にアクセスすると
スクリーンショット 2020-01-16 13.01.28.png
この画面になります
これでサーバーを建てられました。
Goでは起動させると func main() が動き出します。
なのでmain関数の中に書いてあるhttp.ListenAndServe(":8080", nil)が起動するという事ですね。
8080という数字はお好みで大丈夫です、他の数字でも動きます。
ちなみにimport "net/http"も書いてあげないと動きません。
Goでは使用したい機能(パッケージ)をimportしてあげる必要があります。
ではサーバーを起動する事が出来たのでHTMLを表示させたいと思います。

メインディレクトリの下にviewsディレクトリを作成し、その中にindex.htmlを作ります
そして適当に文字を書いてみます
スクリーンショット 2020-01-16 13.18.07.png
そしてこのHTMLファイルをmain.goから呼び出す記述を書いてみます
スクリーンショット 2020-01-16 13.32.38.png

この記述後に再起動してみて、http://localhost:8080 
をリロードしてみてください
スクリーンショット 2020-01-16 13.25.22.png
このようにHTMLの文字が画面に表示されると成功です

解説

http.HandleFuncは第一引数でURLのパスを指定し、第二引数で動かしたい関数を指定します。
今回第一引数は "/" なのでルートパスがリクエストされた時にindexHandlerという関数を呼び出しているという事です。
次にindexHandlerの中身ですが、10行目でtemplate.ParseFilesにHTMLファイルを指定しその中身を tとerrという変数に代入しています。11行目で、もしtemplate.ParseFilesに渡したHTMLが存在しなければerr変数にエラーが代入されエラーを出してくれます。
この if err != nil { log.Fatalln(err) }という記述はGoではめちゃめちゃ出てきますので、とりあえずお決まりの文という認識でいいと思います。(エラーが出た時に内容を教えてくれるよーというものです)
で、HTMLが存在すればその t変数に対してExecuteメソッドを使用するとHTMLを表示する事が出来ます。
Executeに渡している引数ですが、第一引数にResponseWriterを渡しています(これもお決まり文という認識で大丈夫です)第二引数にmain.goからHTMLに渡すデータを指定してあげる事が出来ます。
今回は何も渡さないのでnilにしておきます。

とりあえずHTMLを完成させます、こんな感じにしました
スクリーンショット 2020-01-16 14.06.20.png
次にCSSファイルを作成します。
メインディレクトリの下にresourcesディレクトリを作り、さらにcssディレクトリを作ります。その中にview.cssを作ります。こんな感じです

header{
      height:50px;
      line-height: 50px;
      border:1px solid lightgray;
      text-align: center;
      color: rgb(142, 168, 184);
      background-color: rgb(249, 252, 252);
      }
    .tweet_view{
      float: left;
      height: 100vh;
      width: 50vw;
      background-color: rgb(235, 243, 243);
      padding-top: 20px;
    }
    .tweet_message{
      background-color: white;
      color: gray;
      font-size: 30px;
      margin: 0 20px 15px;
      padding: 10px;
      position: relative;
      border-radius: 15px;
    }
    button.tweet_delete_button{
      background-color: rgb(130, 168, 238);
      border-style: none;
      height: 20px;
      width: 20px;
      position: absolute;
      right: 10px; 
      bottom: 17px;
      color: rgb(255, 255, 255);
      line-height: 5px;
      padding-right: 18px;
      cursor: pointer;
    }
    button:focus {
        outline:0;
    }
    .tweet_form{
      height: 100vh;
      width: 100vw;
    }
    textarea{
      background-color: rgb(235, 243, 243);
      width: 450px;
      height: 250px;
      margin: 50px 0 0 120px;
    }
    input{
      width: 200px;
      height: 50px;
      background-color: rgb(130, 168, 238);
      color: white;
      position: absolute;
      left: 68vw;
      top: 400px
    }

ではリロードしてみます
スクリーンショット 2020-01-16 15.17.11.png

あれ、CSSが効いてませんね。
Goでは静的ファイルを読み出す時はmain関数内で宣言をしてあげないと行けないらしいです。
ではmain.goにcssを呼び出す記述を書きます

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8080", nil)
}

一番上にhttp.Handleという関数を追加しました。
正直自分はコードの細部は理解出来ていなんですが、とりあえずresourcesディレクトリを呼び出してくれる関数という事らしいです(なのでコピペで大丈夫です)
この記述後もう一度リロードしてみます(cm + sf + R でスーパーリロードした方がいいかも)
スクリーンショット 2020-01-16 15.29.34.png
今度はちゃんとCSSが効きました!

次はデータベースを導入します

SQLite3というDBを導入します。導入用記事はまた今度書きます(ググればすぐ出来ます)

ではDBインストール出来たのでmain.goにインポートします

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, nil)
}

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8080", nil)
}

上記のようにimportした後、var DbConnection *sql.DBでDBを定義してあげます。
これでDbConnectionという名前でDBにアクセス出来ます。
では実際にDBにデータベースを作ってみます。

今回はターミナルからSQLコマンドを打ちます。メインディレクトリでsqlite3 example.sqlと打ち込んでください。
スクリーンショット 2020-01-16 16.07.43.png
こんな風になると思います、次にCREATE TABLE tweets (id INTEGER PRIMARY KEY,tweet STRING);と打ち込みます。
これはtweetsという名前のテーブルを作成し、idとtweetというカラムを作成するというコマンドです。
tweetは文字型なのでstring、idはintegerにします。
idのprimary keyとは、何もしなければ自動的に数字を割り振ってくれるというものです。

これでtweetsテーブルは作成出来ました、一応確認してみます。ターミナルで.tabelと打ち込んでください。
スクリーンショット 2020-01-16 17.09.37.png

tweetsテーブルが作成されているのを確認出来ました。

ではフォームに入力した値を実際にDBに入れてみます。

index.htmlの14行目に注目してください。

<form action="/tweet/" method="POST" class="tweet_form">
 <textarea name="tweet" rows="20" cols="80"></textarea>
 <input type="submit" value="speak">
</form>

actionに"/tweet/"を指定してるので、ボタンを押すとhttp://localhost:8080/tweet/
というURLが走ります、なのでmain関数にそのパスが通った時に呼び出される関数を記述すればいいという訳です。

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, nil)
}
func getPostTweet(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    v := r.FormValue("tweet")
    cmd := `INSERT INTO tweets(tweet)VALUES(?)`
    DbConnection.Exec(cmd, v)
    http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/tweet/", getPostTweet)
    http.ListenAndServe(":8080", nil)
}

ますmain関数内にhttp.HandleFunc("/tweet/", getPostTweet)という関数を記述します。
これは先ほど説明しましたね、これで"/tweet/"というパスが通ればgetPostTweetという関数が呼び出されます。
ではgetPostTweet関数の中身な解説をします。
DbConnection, _ := sql.Open("sqlite3", "./example.sql")でデータベースを開いて読み込みます。
defer DbConnection.Close()で開いたら閉じましょう。
これもお決まり文という認識で大丈夫だと思います、openしたらdeferでcloseです
ビューから送信したデータはr変数(http.Request)として送られてくるので
v := r.FormValue("tweet")としてv変数に代入してあげます。
このFormValueの中の"tweet"とは

 <textarea name="tweet" rows="20" cols="80"></textarea>

このtextareaのname属性の事ですね。
textareaに記述した投稿がtweetという名前で受け取れますよという意味ですね。

今度はv変数に投稿内容が渡せているのでこれをDBに書きこみたいです。なのでデータを書き込むSQL文を記述してあげます。
cmd := INSERT INTO tweets(tweet)VALUES(?)
それがこれですね。 
tweetsテーブルのtweetカラムに値を入れてね、というSQL文です。

DbConnection.Exec(cmd, v)で実際にデータを書きこむ事が出来ます。
Execの第一引数にSQL文を、そして第二引数に書き込む値を渡している訳です。
そして最後のhttp.Redirect(w, r, "/", http.StatusFound)ですが、この関数処理が終わった後に"/"パス(つまり元のページ)に戻ってきてねという関数です。
この処理を書かないとtweet.htmlの画面に飛んでしまうのですが、そんなもの作成していないので真っ白の世界に行っちゃいます。
では実際に投稿フォームから投稿してみたいと思います。
スクリーンショット 2020-01-16 17.10.51.png
今回は Go と投稿してみます、ボタンを押すと...
スクリーンショット 2020-01-16 17.12.04.png
またこのページがリロードされましたね。
では本当にDBへ保存されているのか確認してみます。
ターミナルでselect * from tweets;と打ち込んでください。これでtweetsテーブルの中身を確認出来ます。
スクリーンショット 2020-01-16 17.13.54.png
1|Go と保存されているのが確認出来ました。
左のidはprimary keyを設定しているので勝手に数字が振られてます。

次はこのデータを画面に表示してみます

まずはstructを作成します。

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

type Tweets struct {
    Id    int
    Tweet string
}

適当にこの辺りに作成します。
structとは他の言語でいうクラスのようなものです(間違ってたらごめんなさい)
DBから引っ張ってきたデータを一旦入れておく入れ物みたいなイメージですかね!
DBのデータをHTMLに渡したい時は一旦このstructに入れてからじゃないと渡せません。

次にindexHandleを編集します

func indexHandler(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    cmd := `SELECT * FROM tweets`
    rows, err := DbConnection.Query(cmd)
    if err != nil {
        log.Fatalln(err)
    }
    defer rows.Close()
    var body []Tweets
    for rows.Next() {
        var b Tweets
        err := rows.Scan(&b.Id, &b.Tweet)
        if err != nil {
            log.Fatalln(err)
        }
        body = append(body, b)
    }
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, body)
}

先ほどと同じくDBをOpenしてdeferでCloseしまして、tweetsテーブルを探すSQL文を記述してcmd変数に入れてやってるだけですね。
投稿の時と違うのは、DbConnection.ExecではなくDbConnection.Queryになっている事です。Queryメソッドでデータを引っ張り出せるんですね。
今回はrows変数にそのデータを代入してあげます。ちなみこれもdeferでcloseしてあげる必要があります。

その後スライスのTweets型のbody変数を定義してます。
rows.Next()でなにをしてるかと言いますと、DBから引っ張って来たrows変数を一つづつ b変数に代入して行って、それをスライスのbodyへ追加してあげてる訳です。(これもお決まり文として覚えればいいです)
これでtweetsテーブルのデータをTweets structへ変換する事が出来ました。
ではこれをHTMLヘ渡します。Executeの第二引数でデータを渡せるんでしたね。

次にHTMLも編集します

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>Gopher Tweets</title>
  <link rel="stylesheet" type="text/css"  href="../resources/css/view.css">
</head>
<body>
  <main>
    <header>Gopher Tweets</header>
    <div class="tweet_view">
      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}
    </div>
    <form action="/tweet/" method="POST" class="tweet_form">
      <textarea name="tweet" rows="20" cols="80"></textarea>
      <input type="submit" value="speak">
    </form>
  </main>
</body>
</html>

{{}}←これでデータを受け取る事が出来ます。詳しくはこの人の記事で

ではまた再起動して画面を見てみましょう
スクリーンショット 2020-01-16 17.50.09.png
先ほど投稿した Go という投稿が表示されてますね、今度は Hello と投稿してみます。
スクリーンショット 2020-01-16 17.51.55.png
ちゃんと投稿する事が出来ました。

次は投稿を削除してみます

今投稿の横に青いボタンがありますが、今は押しても何も変化しません。このボタンを押すと投稿が削除できるようにしてみましょう。

      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}

これが投稿表示部分のhtmlですが、このボタンをおすと 
/tweet_delete/ パスが走り
tweet_deleteというnameで 
{{.Id}} という値が送られる訳ですね。

なので先ほどと同様にこのパスのHandlefuncをmain関数に記述します。


func deleteTweet(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    cmd := "DELETE FROM Tweets WHERE id = ?"
    i := r.FormValue("tweet_delete")
    var I int
    I, _ = strconv.Atoi(i)
    DbConnection.Exec(cmd, I)
    http.Redirect(w, r, "/", http.StatusFound)
}
func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/tweet_delete/", deleteTweet)
    http.HandleFunc("/tweet/", getPostTweet)
    http.ListenAndServe(":8080", nil)
}

はい、先ほどの投稿の時とほとんど同じですね、削除するSQL文と削除したいIDを渡して実行しているだけです。
ただ送られて来たIdはstring型で送られてくるのでInt型に変換する為にstrconv.Atoi(i)を使用しているだけです。
ではまた再起動してから、一番上のGoという投稿のボタンをクリックしてみます。
スクリーンショット 2020-01-16 18.04.24.png
ちゃんと消えました!

このままでは寂しいのでマスコットキャラのゴーファくんを表示してあげて終了です。
resourcesディレクトリ内にimagesディレクトリを作成し、その中に画像を入れてCSSにてbackground-imageで指定するだけですね。

スクリーンショット 2020-01-16 18.11.48.png
以上です。
この記事が僕と同じような初学者の方の為になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで超簡易版Twitterを作ってみました(初心者向け)

はじめまして

最近Goの勉強を始めたばかりで何か開発してみようと思い、簡単なアプリを開発しました。
twitterもやってます@khm_itac

見た目

スクリーンショット 2020-01-16 11.57.40.png

出来る事

・フォームから投稿する事が出来ます
・投稿をデータベースに保存する事が出来ます
・保存されている投稿を表示してくれます
・投稿を消す事も出来ます

はい、それだけです。
めちゃくちゃ低レベルですが、UIからサーバーにデータを送りDBへ保存、DBのデータをビューに表示するというのは全てのアプリの基本的な部分だと思いますので、これが出来るとようやくエンジニアレベル1達成です!
僕と同じようにこれからGoの勉強を始める方の少しでも役に立てればと思い記事を投稿しました。
僕も初心者ですので間違ってる部分や至らない点があるかと思いますがご了承ください。

解説

Goの導入や基礎文法やVSCodeの使い方などは解説しません、僕より詳しい人がYouTube等で動画を出していますのでそちらで勉強した方がいいと思います(あとProgateとか)
Macでの開発ですのでwindowsの方はうまく動かない可能性もあります

まずmain.goを作ります
メインディレクトリの直下でいいと思います。

スクリーンショット 2020-01-16 12.59.20.png
この画面上でF5を押すとスタートさせる事が出来ます、その後http://localhost:8080 
にアクセスすると
スクリーンショット 2020-01-16 13.01.28.png
この画面になります
これでサーバーを建てられました。
Goでは起動させると func main() が動き出します。
なのでmain関数の中に書いてあるhttp.ListenAndServe(":8080", nil)が起動するという事ですね。
8080という数字はお好みで大丈夫です、他の数字でも動きます。
ちなみにimport "net/http"も書いてあげないと動きません。
Goでは使用したい機能(パッケージ)をimportしてあげる必要があります。
ではサーバーを起動する事が出来たのでHTMLを表示させたいと思います。

メインディレクトリの下にviewsディレクトリを作成し、その中にindex.htmlを作ります
そして適当に文字を書いてみます
スクリーンショット 2020-01-16 13.18.07.png
そしてこのHTMLファイルをmain.goから呼び出す記述を書いてみます
スクリーンショット 2020-01-16 13.32.38.png

この記述後に再起動してみて、http://localhost:8080 
をリロードしてみてください
スクリーンショット 2020-01-16 13.25.22.png
このようにHTMLの文字が画面に表示されると成功です

解説

http.HandleFuncは第一引数でURLのパスを指定し、第二引数で動かしたい関数を指定します。
今回第一引数は "/" なのでルートパスがリクエストされた時にindexHandlerという関数を呼び出しているという事です。
次にindexHandlerの中身ですが、10行目でtemplate.ParseFilesにHTMLファイルを指定しその中身を tとerrという変数に代入しています。11行目で、もしtemplate.ParseFilesに渡したHTMLが存在しなければerr変数にエラーが代入されエラーを出してくれます。
この if err != nil { log.Fatalln(err) }という記述はGoではめちゃめちゃ出てきますので、とりあえずお決まりの文という認識でいいと思います。(エラーが出た時に内容を教えてくれるよーというものです)
で、HTMLが存在すればその t変数に対してExecuteメソッドを使用するとHTMLを表示する事が出来ます。
Executeに渡している引数ですが、第一引数にResponseWriterを渡しています(これもお決まり文という認識で大丈夫です)第二引数にmain.goからHTMLに渡すデータを指定してあげる事が出来ます。
今回は何も渡さないのでnilにしておきます。

とりあえずHTMLを完成させます、こんな感じにしました
スクリーンショット 2020-01-16 14.06.20.png
次にCSSファイルを作成します。
メインディレクトリの下にresourcesディレクトリを作り、さらにcssディレクトリを作ります。その中にview.cssを作ります。こんな感じです

header{
      height:50px;
      line-height: 50px;
      border:1px solid lightgray;
      text-align: center;
      color: rgb(142, 168, 184);
      background-color: rgb(249, 252, 252);
      }
    .tweet_view{
      float: left;
      height: 100vh;
      width: 50vw;
      background-color: rgb(235, 243, 243);
      padding-top: 20px;
    }
    .tweet_message{
      background-color: white;
      color: gray;
      font-size: 30px;
      margin: 0 20px 15px;
      padding: 10px;
      position: relative;
      border-radius: 15px;
    }
    button.tweet_delete_button{
      background-color: rgb(130, 168, 238);
      border-style: none;
      height: 20px;
      width: 20px;
      position: absolute;
      right: 10px; 
      bottom: 17px;
      color: rgb(255, 255, 255);
      line-height: 5px;
      padding-right: 18px;
      cursor: pointer;
    }
    button:focus {
        outline:0;
    }
    .tweet_form{
      height: 100vh;
      width: 100vw;
    }
    textarea{
      background-color: rgb(235, 243, 243);
      width: 450px;
      height: 250px;
      margin: 50px 0 0 120px;
    }
    input{
      width: 200px;
      height: 50px;
      background-color: rgb(130, 168, 238);
      color: white;
      position: absolute;
      left: 68vw;
      top: 400px
    }

ではリロードしてみます
スクリーンショット 2020-01-16 15.17.11.png

あれ、CSSが効いてませんね。
Goでは静的ファイルを読み出す時はmain関数内で宣言をしてあげないと行けないらしいです。
ではmain.goにcssを呼び出す記述を書きます

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8080", nil)
}

一番上にhttp.Handleという関数を追加しました。
正直自分はコードの細部は理解出来ていなんですが、とりあえずresourcesディレクトリを呼び出してくれる関数という事らしいです(なのでコピペで大丈夫です)
この記述後もう一度リロードしてみます(cm + sf + R でスーパーリロードした方がいいかも)
スクリーンショット 2020-01-16 15.29.34.png
今度はちゃんとCSSが効きました!

次はデータベースを導入します

SQLite3というDBを導入します。導入用記事はまた今度書きます(ググればすぐ出来ます)

ではDBインストール出来たのでmain.goにインポートします

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, nil)
}

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8080", nil)
}

上記のようにimportした後、var DbConnection *sql.DBでDBを定義してあげます。
これでDbConnectionという名前でDBにアクセス出来ます。
では実際にDBにデータベースを作ってみます。

今回はターミナルからSQLコマンドを打ちます。メインディレクトリでsqlite3 example.sqlと打ち込んでください。
スクリーンショット 2020-01-16 16.07.43.png
こんな風になると思います、次にCREATE TABLE tweets (id INTEGER PRIMARY KEY,tweet STRING);と打ち込みます。
これはtweetsという名前のテーブルを作成し、idとtweetというカラムを作成するというコマンドです。
tweetは文字型なのでstring、idはintegerにします。
idのprimary keyとは、何もしなければ自動的に数字を割り振ってくれるというものです。

これでtweetsテーブルは作成出来ました、一応確認してみます。ターミナルで.tabelと打ち込んでください。
スクリーンショット 2020-01-16 17.09.37.png

tweetsテーブルが作成されているのを確認出来ました。

ではフォームに入力した値を実際にDBに入れてみます。

index.htmlの14行目に注目してください。

<form action="/tweet/" method="POST" class="tweet_form">
 <textarea name="tweet" rows="20" cols="80"></textarea>
 <input type="submit" value="speak">
</form>

actionに"/tweet/"を指定してるので、ボタンを押すとhttp://localhost:8080/tweet/
というURLが走ります、なのでmain関数にそのパスが通った時に呼び出される関数を記述すればいいという訳です。

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

var DbConnection *sql.DB

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, nil)
}
func getPostTweet(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    v := r.FormValue("tweet")
    cmd := `INSERT INTO tweets(tweet)VALUES(?)`
    DbConnection.Exec(cmd, v)
    http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/tweet/", getPostTweet)
    http.ListenAndServe(":8080", nil)
}

ますmain関数内にhttp.HandleFunc("/tweet/", getPostTweet)という関数を記述します。
これは先ほど説明しましたね、これで"/tweet/"というパスが通ればgetPostTweetという関数が呼び出されます。
ではgetPostTweet関数の中身な解説をします。
DbConnection, _ := sql.Open("sqlite3", "./example.sql")でデータベースを開いて読み込みます。
defer DbConnection.Close()で開いたら閉じましょう。
これもお決まり文という認識で大丈夫だと思います、openしたらdeferでcloseです
ビューから送信したデータはr変数(http.Request)として送られてくるので
v := r.FormValue("tweet")としてv変数に代入してあげます。
このFormValueの中の"tweet"とは

 <textarea name="tweet" rows="20" cols="80"></textarea>

このtextareaのname属性の事ですね。
textareaに記述した投稿がtweetという名前で受け取れますよという意味ですね。

今度はv変数に投稿内容が渡せているのでこれをDBに書きこみたいです。なのでデータを書き込むSQL文を記述してあげます。
cmd := INSERT INTO tweets(tweet)VALUES(?)
それがこれですね。 
tweetsテーブルのtweetカラムに値を入れてね、というSQL文です。

DbConnection.Exec(cmd, v)で実際にデータを書きこむ事が出来ます。
Execの第一引数にSQL文を、そして第二引数に書き込む値を渡している訳です。
そして最後のhttp.Redirect(w, r, "/", http.StatusFound)ですが、この関数処理が終わった後に"/"パス(つまり元のページ)に戻ってきてねという関数です。
この処理を書かないとtweet.htmlの画面に飛んでしまうのですが、そんなもの作成していないので真っ白の世界に行っちゃいます。
では実際に投稿フォームから投稿してみたいと思います。
スクリーンショット 2020-01-16 17.10.51.png
今回は Go と投稿してみます、ボタンを押すと...
スクリーンショット 2020-01-16 17.12.04.png
またこのページがリロードされましたね。
では本当にDBへ保存されているのか確認してみます。
ターミナルでselect * from tweets;と打ち込んでください。これでtweetsテーブルの中身を確認出来ます。
スクリーンショット 2020-01-16 17.13.54.png
1|Go と保存されているのが確認出来ました。
左のidはprimary keyを設定しているので勝手に数字が振られてます。

次はこのデータを画面に表示してみます

まずはstructを作成します。

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "text/template"
)

type Tweets struct {
    Id    int
    Tweet string
}

適当にこの辺りに作成します。
structとは他の言語でいうクラスのようなものです(間違ってたらごめんなさい)
DBから引っ張ってきたデータを一旦入れておく入れ物みたいなイメージですかね!
DBのデータをHTMLに渡したい時は一旦このstructに入れてからじゃないと渡せません。

次にindexHandleを編集します

func indexHandler(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    cmd := `SELECT * FROM tweets`
    rows, err := DbConnection.Query(cmd)
    if err != nil {
        log.Fatalln(err)
    }
    defer rows.Close()
    var body []Tweets
    for rows.Next() {
        var b Tweets
        err := rows.Scan(&b.Id, &b.Tweet)
        if err != nil {
            log.Fatalln(err)
        }
        body = append(body, b)
    }
    t, err := template.ParseFiles("views/index.html")
    if err != nil {
        log.Fatalln(err)
    }
    t.Execute(w, body)
}

先ほどと同じくDBをOpenしてdeferでCloseしまして、tweetsテーブルを探すSQL文を記述してcmd変数に入れてやってるだけですね。
投稿の時と違うのは、DbConnection.ExecではなくDbConnection.Queryになっている事です。Queryメソッドでデータを引っ張り出せるんですね。
今回はrows変数にそのデータを代入してあげます。ちなみこれもdeferでcloseしてあげる必要があります。

その後スライスのTweets型のbody変数を定義してます。
rows.Next()でなにをしてるかと言いますと、DBから引っ張って来たrows変数を一つづつ b変数に代入して行って、それをスライスのbodyへ追加してあげてる訳です。(これもお決まり文として覚えればいいです)
これでtweetsテーブルのデータをTweets structへ変換する事が出来ました。
ではこれをHTMLヘ渡します。Executeの第二引数でデータを渡せるんでしたね。

次にHTMLも編集します

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>Gopher Tweets</title>
  <link rel="stylesheet" type="text/css"  href="../resources/css/view.css">
</head>
<body>
  <main>
    <header>Gopher Tweets</header>
    <div class="tweet_view">
      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}
    </div>
    <form action="/tweet/" method="POST" class="tweet_form">
      <textarea name="tweet" rows="20" cols="80"></textarea>
      <input type="submit" value="speak">
    </form>
  </main>
</body>
</html>

{{}}←これでデータを受け取る事が出来ます。詳しくはこの人の記事で

ではまた再起動して画面を見てみましょう
スクリーンショット 2020-01-16 17.50.09.png
先ほど投稿した Go という投稿が表示されてますね、今度は Hello と投稿してみます。
スクリーンショット 2020-01-16 17.51.55.png
ちゃんと投稿する事が出来ました。

次は投稿を削除してみます

今投稿の横に青いボタンがありますが、今は押しても何も変化しません。このボタンを押すと投稿が削除できるようにしてみましょう。

      {{range .}}
        <form action="/tweet_delete/" method="DELETE">
          <p class="tweet_message">{{.Tweet}}
            <button type="submit" value={{.Id}} name="tweet_delete" class="tweet_delete_button">-</button>
          </p>
        </form>
      {{end}}

これが投稿表示部分のhtmlですが、このボタンをおすと 
/tweet_delete/ パスが走り
tweet_deleteというnameで 
{{.Id}} という値が送られる訳ですね。

なので先ほどと同様にこのパスのHandlefuncをmain関数に記述します。


func deleteTweet(w http.ResponseWriter, r *http.Request) {
    DbConnection, _ := sql.Open("sqlite3", "./example.sql")
    defer DbConnection.Close()
    cmd := "DELETE FROM Tweets WHERE id = ?"
    i := r.FormValue("tweet_delete")
    var I int
    I, _ = strconv.Atoi(i)
    DbConnection.Exec(cmd, I)
    http.Redirect(w, r, "/", http.StatusFound)
}
func main() {
    http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/tweet_delete/", deleteTweet)
    http.HandleFunc("/tweet/", getPostTweet)
    http.ListenAndServe(":8080", nil)
}

はい、先ほどの投稿の時とほとんど同じですね、削除するSQL文と削除したいIDを渡して実行しているだけです。
ただ送られて来たIdはstring型で送られてくるのでInt型に変換する為にstrconv.Atoi(i)を使用しているだけです。
ではまた再起動してから、一番上のGoという投稿のボタンをクリックしてみます。
スクリーンショット 2020-01-16 18.04.24.png
ちゃんと消えました!

このままでは寂しいのでマスコットキャラのゴーファくんを表示してあげて終了です。
resourcesディレクトリ内にimagesディレクトリを作成し、その中に画像を入れてCSSにてbackground-imageで指定するだけですね。

スクリーンショット 2020-01-16 18.11.48.png
以上です。
この記事が僕と同じような初学者の方の為になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPCメモ

自身のメモとしてgRPCをまとめてみました。

gRPCとは

protocol buffersをMessage interchange format(メッセージのI/Oに使うための形式)として使えるリモートプロシージャコールシステムです。

protocol buffersとは

IDL:Interface Definition Language (インタフェース定義言語)を用いたファイルフォーマットです。

gRPCで何ができるのか

クライアントが別のマシンにあるメソッドをまるでローカルにあるかのように使えるようになります。
下の図のように様々なプログラミング言語に対して実装できる。

image.png

雑に言うと

今までjsonなどを介してAPIを叩いていたようなところを、もっと確固たる定義をもった上でより高パフォーマンスでやりとりができる…という感じでしょうか。

protocol bufferの定義の仕方

やりとりするmessageの構成は、以下のようにname-valueでfieldを定義します。

person.proto
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

gRPC Serverの定義はrpcメソッドを使って行います。

greeter.proto
// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

protoファイルののコンパイル

上のgreeter.protoをGo言語ようにコンパイルする場合は以下のようなコマンドを叩きます。
protoc --go_out=plugins=grpc:./ ./greeter.proto

このコンパイルでClient側のメソッドもServer側のメソッドも作成されます。

もし、何かのディレクトリの中にあるすべての.protoをコンパイルしたい場合は、以下のような感じでワイルドカードを使うこともできます。
protoc --go_out=plugins=grpc:./ ./*.proto

もし、どこかのディレクトリに入れたいという場合は、grpc:後のところを書き換えます。
以下の場合はprotoディレクトリの中にコンパイルされたデータが書き出されます。
protoc --go_out=plugins=grpc:proto ./*.proto

もし、どこかのディレクトリからどこかのディレクトリに書き出したい場合は、以下のような書き方になります。
protoc -I protobuf --go_out=plugins=grpc:proto protobuf/*.proto
-I が無いとprotoディレクトリ配下にprotbufディレクトリが作成され、そのなかにコンパイルされたデータが書き出されます。
-I はimportの基点となる場所をしてしてやるオプションと考えてもらえば良いかもしれません。

--go_out=plugins=grpc:と書いているのですが、protocol buffer自体は別にgRPCのためだけのものではないので、このようにgRPCのための書き出しですよーというオプションを書いておく必要があります。

Ruby用にコンパイルしたい場合は --go_outを書き換えます。(複数書けば複数の言語のファイルを同時に書き出せます。)
protoc -I protobuf --ruby_out=plugins=grpc:proto protobuf/*.proto

gRPCサーバーのリクエストとレスポンスの種類

種類は4つあります。

Unary RPC

一番シンプルな方法で、単一のメッセージのリクエストに対して、単一のメッセージをレスポンスで返す方法です。

greeter.proto
rpc SayHello(HelloRequest) returns (HelloResponse) {
}

Server streaming RPC

単一のメッセージのリクエストに対して、複数のメッセージをレスポンスで返す方法です。
例えばですが、何かのデータの一覧が欲しいなどと言うときに、この方法を使えたりします。

greeter.proto
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {
}

Client streaming RPC

複数のメッセージのリクエストを受けた上で、単一のメッセージのレスポンスを返します。
greeter.proto
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

Bidirectional streaming RPC

これはリクエストもレスポンスも複数のメッセージをレスポンスを扱う方法ですが、
すべてのメッセージを待って、すべてのメッセージを書き出して返すことも、一つメッセージを受け取る毎に、メッセージを書き出していき、最後にまとめてメッセージを返すことも、サーバー側の実装次第で返ることができます。

greeter.proto
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {
}

サーバー側の実装

主な処理は以下のような感じです。

main.go
package main
import (
    pb "github.com/{リポジトリ名}/{デフォルトならproto}"
    "flag"
    "net"
    "log"
    "sync"
    "fmt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/testdata"
)

var (
    tls        = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
    certFile   = flag.String("cert_file", "", "The TLS cert file")
    keyFile    = flag.String("key_file", "", "The TLS key file")
    jsonDBFile = flag.String("json_db_file", "", "A json file containing a list of features")
    port       = flag.Int("port", 10000, "The server port")
)

func main() {
    flag.Parse() // 引数を入れて実行したい時用にflagを使ってます。

    // hostとportを設定します。
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // 必要であればcredentialの設定をします。
    var opts []grpc.ServerOption
    if *tls {
        if *certFile == "" {
            *certFile = testdata.Path("server1.pem")
        }
        if *keyFile == "" {
            *keyFile = testdata.Path("server1.key")
        }
        creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
        if err != nil {
            log.Fatalf("Failed to generate credentials %v", err)
        }
        opts = []grpc.ServerOption{grpc.Creds(creds)}
    }

    // gRPCのサーバーを初期化
    grpcServer := grpc.NewServer(opts...)
    // protocでコンパイルされたコードからサーバーを登録するメソッドを実行
    pb.RegisterGreeterServer(grpcServer, newServer())
    // サーバーを起動
    grpcServer.Serve(lis)
}

上の記述で書いていないのですがnewServer()という関数が重要なのですが、Register*Serverの第二引数にはprotoのServer内で定義したメソッドを持ったinterfaceを入れる形になっています。

その為に以下のような形の定義もmainに入れておきます。(例えば、Server streaming RPCだけ定義している場合)

main.go
type greeterServer struct {}
// nilかerrを返せばstreamingが終了します。
func(s *greeterServer) LotsOfReplies(req *pb.LotsOfRepliesRequest, stream pb.Greeter_LotsOfRepliesServer) error {
    hs := []pb.HelloReply{
        pb.HelloReply{ Name: "ohayo" },
        pb.HelloReply{ Name: "konnichiwa" },
        pb.HelloReply{ Name: "konbanwa" },
    }
    for _, h := range hs {
        if err := stream.Send(&h); err != nil {
            return err
        }
    }
    return nil
}

func newServer() *greeterServer {
    s := &greeterServer{}
    return s
}

その他

repeated

protoの定義の中でmessageの中に配列をいれたい場合はRepeatedが使えます。

greeter.proto
message HelloResponse {
  repeated string name = 1;
}

こうするとnameの配列を返す事ができます。
複数のmessageを返したい場合はstreamでmessageの中で処理できるならrepeatedというところでしょうか。

oneof

messageに入るのがstringの可能性もあるし、int32の可能性もある…なんて場合に使えます。

greeter.proto
message HelloResponse {
    oneof name {
        string text = 1;
        int32 id = 2;
    }
}

リファレンス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フロントエンジニア(仮)がGO言語で簡単なスクレイピング処理を実装するまで

はじめまして。kenalog210と申します。
ある事情によりGo言語でのスクレイピング処理方法を色々調べてたんですが、無事お流れとなりました。
もったいないのでQiitaに上げさせていただきます。
今回は下記のサイトをサンプルとして使用させて頂きました。
https://wiki.hackerspaces.org/

使用したフレームワーク


Colly
http://go-colly.org/
https://github.com/gocolly/colly

スターが1万近くついてるスゴイやつです。
リポジトリにサンプルがいっぱいあるので、基本的なケースなら自分の応用したいパターンに合わせて
サンプルをちょっといじくり回せば使えそうです。
この記事もサンプルをちょっと応用しただけのものです。

やりたかったこと


・マッピング情報を渡したらうまいことインスタンス化してくれるようなものを組みたい
・インスタンス化出来たら、JSON文字列化してコンソールで出力
・構造体とのマッピング用スクリプトはめんどいので組みたくない

構造体を定義

まずは型定義を用意してやります。下記のようにフィールド定義と紐付けたい要素を簡単に定義できます。
ちなみに、Go言語は先頭文字が小文字か大文字かでPrivateかPublicかを定義するようなので
フィールド名の先頭を大文字にしてやらないとちゃんとパースしてくれません。(1敗)

/*
    パース処理対象の構造体
    型名指定の後に、JQueryと同じ要領でフィールドと紐付けたい要素や属性をセレクタで指定
*/
type hackerspacesWiki struct {
    // 注釈
    Note string `selector:"pre"`
    // 紹介文
    Description []string `selector:"p"`
    // 右側のサムネ画像一覧
    ThumbnailImageUrls []string `selector:".thumbimage" attr:"src"`
}

クローラー、パース対象の構造体のインスタンスを生成

下記のようにインスタンスを生成しました。クローリング

    // クローラーインスタンスを生成
    c := colly.NewCollector(
        // クローリングを許可するドメインを設定
        colly.AllowedDomains("wiki.hackerspaces.org"),
    )
    // パース対象のインスタンスを生成
    parseTargetStruct := &hackerspacesWiki{}

クローラーの各イベント処理を定義

今回はOnHTMLイベントとOnRequestイベントのみ使用しました。

    /*
       セレクタで指定した要素が存在した場合の処理
       第1引数にJQueryと同じ要領でセレクタを指定可能

       めんどくさいのでルート要素を指定して一括パースをかける
    */
    c.OnHTML(".mw-parser-output", func(e *colly.HTMLElement) {
        /*
            OnHTMLで指定したセレクタに適合した要素が引数で入ってくるため
            Unmarshalメソッドを使用してパース対象の型のインスタンスを渡してやれば
            構造体に指定したセレクタに従って自動的にマッピングしてくれる
        */
        e.Unmarshal(parseTargetStruct)
    })

    // リクエスト実行時(Visitメソッド実行時)に実行される処理
    c.OnRequest(func(r *colly.Request) {
        fmt.Println("Visiting", r.URL.String())
    })

スクレイピング処理開始

Visitメソッドを実行すると OnRequest → OnHTML の順で処理が流れます。

    // スクレイピング処理を開始
    c.Visit("https://wiki.hackerspaces.org/")

JSON文字列化 → コンソール出力

    // マッピング後のインスタンスをJSON文字列化
    bs, err := json.Marshal(parseTargetStruct)
    if err != nil {
        // 申し訳程度のエラー処理
        fmt.Println("JSON Parse Error")
        os.Exit(1)
    }

    // コンソールに出力
    fmt.Println(string(bs))

実行結果

こんな形でコンソールに出力されました。
対象要素が複数存在する場合は、構造体の方で配列として定義してやらないとうまく取得してくれないです。

Visiting https://wiki.hackerspaces.org/

{
    "Note": "Note: We are currently updating the wiki. If you notice something you want to tell us, join #hackerspaces on Freenode IRC network.",
    "Description": [
        "Hackerspaces are community-operated physical places, where people can meet and work on their projects.",
        "This website is for Anyone and Everyone who wants to share their hackerspace stories and questions with the global hackerspaces community.",
        "Have some free time? Help with the Hackerspace Census 2019 and find out if your local hackerspaces listed here are still active!"
    ],
    "ThumbnailImageUrls": [
        "/images/e/ec/Cbase07.jpg",
        "/images/0/0f/NYCR2.JPG",
        "/images/7/73/Hackerspace_charlotte.jpg"
    ]
}

ソース全文

package main

import (
    "encoding/json"
    "fmt"
    "os"

    "github.com/gocolly/colly/v2"
)

/*
    パース処理対象の構造体
    型名指定の後に、JQueryと同じ要領でフィールドと紐付けたい要素や属性をセレクタで指定
*/
type hackerspacesWiki struct {
    // 注釈
    Note string `selector:"pre"`
    // 紹介文
    Description []string `selector:"p"`
    // 右側のサムネ画像一覧
    ThumbnailImageUrls []string `selector:".thumbimage" attr:"src"`
}

func main() {
    // クローラーインスタンスを生成
    c := colly.NewCollector(
        // クローリングを許可するドメインを設定
        colly.AllowedDomains("wiki.hackerspaces.org"),
    )
    // パース対象のインスタンスを生成
    parseTargetStruct := &hackerspacesWiki{}

    /*
       セレクタで指定した要素が存在した場合の処理
       第1引数にJQueryと同じ要領でセレクタを指定可能

       めんどくさいのでルート要素を指定して一括パースをかける
    */
    c.OnHTML(".mw-parser-output", func(e *colly.HTMLElement) {
        /*
            OnHTMLで指定したセレクタに適合した要素が引数で入ってくるため
            Unmarshalメソッドを使用してパース対象の型のインスタンスを渡してやれば
            構造体に指定したセレクタに従って自動的にマッピングしてくれる
        */
        e.Unmarshal(parseTargetStruct)
    })

    // リクエスト実行時(Visitメソッド実行時)に実行される処理
    c.OnRequest(func(r *colly.Request) {
        fmt.Println("Visiting", r.URL.String())
    })

    // スクレイピング処理をスタート
    c.Visit("https://wiki.hackerspaces.org/")

    // マッピング後のインスタンスをJSON文字列化
    bs, err := json.Marshal(parseTargetStruct)
    if err != nil {
        // 申し訳程度のエラー処理
        fmt.Println("JSON Parse Error")
        os.Exit(1)
    }

    // コンソールに出力
    fmt.Println(string(bs))
}

感想

Collyが使いやすくてスゴイ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む