- 課題提出方法は https://github.com/VG-Tech-Dojo/treasure2016-pre/blob/master/README.md を見てください。
answers/your-github-user-name/go/q2
以下にコードを配置してください。- コードは
gofmt
で整形して提出してください。 https://golang.org/cmd/gofmt/ - 課題2のみできていればいいですが、余裕があったら発展課題もやってください。発展課題をやっておくとTreasure中に理解が深まりやすいでしょう。おすすめです。
任意のURLからタイトルとmeta:descriptionを抽出するアプリケーションを書きましょう。以下の手続き Get
を実装してください。
type Page struct {
Title string
Description string
}
func Get(url string) (*Page, error)
Get
は任意のURLをうけとり、ページ型の参照を返すこととします。 以下の仕事をするものとします。
- 受け取ったURLについてHTTPリクエストを送信します。
- HTTPレスポンスに含まれるHTMLからタイトルとmeta:descriptionを抽出します。
- 抽出したタイトルとmeta:descriptionを
Page
型にいれて返します。 - エラーが有る場合はエラーを返します。
例えば http://voyagegroup.com について Get
を実行してみましょう。
package main
type Page struct {
Title string
Description string
}
func Get(url string) (*Page, error) {
// ...
}
func main() {
// Getを利用しているとします
p, err := Get("http://voyagegroup.com")
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("%#v", p)
}
すると以下の結果が返ってくるようになります。
&main.Page{Title:"株式会社VOYAGE GROUP | 人を軸にした事業開発会社", Description:"株式会社VOYAGE GROUPのコーポレートサイトです。"}
例えばGETメソッドをつかってリクエストをおくるには以下のようにします。
package main
import (
"net/http"
)
func main() {
resp, err := http.Get(url)
if err != nil {
// 実際にはちゃんとエラー処理をしましょう
panic(err)
}
defer resp.Body.Close()
// ...
}
resp.Body
は io.ReadCloser
として扱うことができます。つまり、 io.Reader
を受け取る任意のパーサを扱うことができます。
DOMツリーのトラバースには https://godoc.org/golang.org/x/net/html を使うといいでしょう。DOMツリーのトラバースには2つの方法があります。
html.NewTokenizer(io.Reader)
を使うhtml.Parse(io.Reader)
を使う
今回は html.Parse
の例を簡単に紹介します。
例えば以下のようにHTMLをParseしてみます。ここでは strings.NewReader
をつかって io.Reader
に対応した変数を文字列から生成しています。テストの際にもこのようにすると便利です。
html.Parse
に渡すことでまずDOMツリーを構成します。タイトルのみを印字するには以下のようにするとよいです。
// sample.go
package main
import (
"fmt"
"strings"
"golang.org/x/net/html"
)
func main() {
// strings.NewReaderをつかって io.Reader を生成しています。
h := strings.NewReader(`
<html>
<head>
<title>test</title>
</head>
</html>
`)
doc, err := html.Parse(h)
if err != nil {
// 実際にはちゃんとエラー処理しましょう
panic(err)
}
var f func(*html.Node)
// fはDOMツリーを再帰的にトラバースするための手続きです。
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" {
fmt.Printf("%s", n.FirstChild.Data)
}
// 再帰的にノードをおっていくために、次のノードを探し、
// ノードが存在すれば再びこのfを実行する、ということをしています。
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
}
これを実行すると以下のように出力されます。
-> % go run sample.go
test
上記の手続きに meta:description
を取得できるようにしていくと、解答に近づくでしょう。ただし少々面倒かもしれません。
meta
タグであり、かつ、name="description"
のタグのcontent
の内容を取得する必要があります。meta
タグは<meta name="description" content="株式会社VOYAGE GROUPのコーポレートサイトです。" />
という構造なので、name
がdescription
であることを判別し、そのとなりにあるcontent
の内容を取得しなければなりません。
例えば以下のようなヘルパとなる手続きを用意するのもよいでしょう。
func isDescription(attrs []html.Attribute) bool {
for _, attr := range attrs {
if /* KeyがnameでValがdescriptionか? */ {
return true
}
}
return false
}
すると以下のようにすっきりかけます。
if n.Type == html.ElementNode && n.Data == "meta" {
if isDescription(n.Attr) {
for _, attr := range n.Attr {
// contentの中身を取得
}
}
}
ここではHTTPサーバを実装します。以下のように動作するサーバを書いてください。
$ curl -D - "http://localhost:8080?url=http%3A%2F%2Fvoyagegroup.com"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 02 Aug 2016 05:08:13 GMT
Content-Length: 156
{"title":"株式会社VOYAGE GROUP | 人を軸にした事業開発会社","description":"株式会社VOYAGE GROUPのコーポレートサイトです。"}
もし http://hoge
などの存在しないURLを指定した場合には 500
を返すようにします。例えば以下のように返します。
$ curl -D - "http://localhost:8080?url=http%3A%2F%2Fhoge"
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 02 Aug 2016 05:09:01 GMT
Content-Length: 53
request failed
net/http
を読んでHTTPサーバの書き方を確かめましょう。エコーサーバ(単純な応答を返すだけのHTTPサーバ)を書くには以下のようにします。
// server.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// `/` にリクエストが来た場合の振る舞いを書いています。
// ここでは fmt.Fprintf で w にレスポンスを書き込んでいます。
// http.ResponseWriter は io.Writer を満たしているので、
// fmt.Fprint から書き込むことができます。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "s-tanno is god")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
これを実行し、動作を確認してみましょう。
$ go run server.go
---
$ curl -D - "http://localhost:8080"
HTTP/1.1 200 OK
Date: Tue, 02 Aug 2016 05:13:04 GMT
Content-Length: 14
Content-Type: text/plain; charset=utf-8
s-tanno is god
s-tanno is god
が表示されました。
次にGETパラメータを受け取ってみましょう。GETパラメータというのはURLに含まれる ?
以降の部分です。以下の場合だと name
に対する値が hoge
となります。
http://localhost:8080?name=hoge
GETパラメータを受け取るには以下のようにします。
// server.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
if name == "" {
http.Error(w, "name not specified", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%s is god", name)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
go run server.go
でプロセスを立ち上げてから、確かめてみましょう。( go run
しなおさないとコードの変更は反映されません。)
$ curl -D - "http://localhost:8080"
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 02 Aug 2016 05:18:32 GMT
Content-Length: 19
name not specified
$ curl -D - "http://localhost:8080?name=s-tanno"
HTTP/1.1 200 OK
Date: Tue, 02 Aug 2016 05:17:57 GMT
Content-Length: 14
Content-Type: text/plain; charset=utf-8
s-tanno is god
name
パラメータを受け取り、レスポンスに反映することができました。
あとは結果をJSONに変換して返せばよいです。GoでJSONを扱うには encoding/json
を使うのが便利です。
ここで http://localhost:8080?title=hoge
とした場合、それに伴う Page
型のオブジェクトをつくり、JSONを返すサーバを書くようにしてみましょう。以下が実装例です。
package main
import (
"encoding/json"
"log"
"net/http"
)
// PageはPageの構成を示します。
// `json:"title"` とすることによって、各フィールドがJSONのどの
// キーにmapされるかを明示することができます。
type Page struct {
Title string `json:"title"`
Description string `json:"description"`
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
title := r.FormValue("title")
if title == "" {
http.Error(w, "title not specified", http.StatusBadRequest)
return
}
page := &Page{Title: title}
w.Header().Set("Content-Type", "application/json")
// json.NewEncoder はJSON用のエンコーダを初期化しています。
// ここで初期化したエンコーダを使って `enc.Encode` をすることで、
// 初期化時に指定した io.Writer に出力を書き込むことができます。
// w はここでは http.ResponseWriter なので、HTTPの出力、
// つまりHTTPレスポンスとしてJSONを返すことができます。便利ですね。
enc := json.NewEncoder(w)
if err := enc.Encode(page); err != nil {
http.Error(w, "encoding failed", http.StatusInternalServerError)
return
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
これを使うと以下のように返すことができます。ちゃんとタイトルが埋め込まれたことがわかりますね。
-> % curl -D - "http://localhost:8080?title=kuke"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 02 Aug 2016 05:22:36 GMT
Content-Length: 34
{"title":"kuke","description":""}
あとは前に実装した Get
をつかって取得した Page
オブジェクトを使えば題意を満たせるでしょう。
- Tour of Goにはなかった課題、かつTreasureに関わるものということで
net/http
に関わるものを出すことにしてみました。 net/http/httptest
やテストにおけるモックの手法については講義内でも時間があれば紹介します。