Skip to content

Instantly share code, notes, and snippets.

@suzuken

suzuken/2.md Secret

Last active August 3, 2016 05:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save suzuken/b456e0f4679f86da572839d6d86f159e to your computer and use it in GitHub Desktop.
Save suzuken/b456e0f4679f86da572839d6d86f159e to your computer and use it in GitHub Desktop.
  • 課題提出方法は 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中に理解が深まりやすいでしょう。おすすめです。

課題2 net/http 入門

任意の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.Bodyio.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のコーポレートサイトです。" /> という構造なので、 namedescription であることを判別し、そのとなりにある 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 やテストにおけるモックの手法については講義内でも時間があれば紹介します。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment