爆速でGo!

GoGo!

Goで軽量なスクレイピングライブラリを作ってみた

リポジトリ

github.com

はじめに

GoでWebクローラーを開発する際、皆さんどうされてますか??
フルスクラッチで作るのは少し面倒だし、asciimoo/collyのようなフルスタックなのはいらない、、という時に丁度いいライブラリがなかったので作りました!
本当に最低限の機能のみ搭載しました。

機能

主な3つの機能を紹介します。

オーガニック検索

キーワードを入れて自然検索結果画面に表示されるページURLを返します。

import "github.com/ryonakao/netsurfer"
urls, err := netsurfer.OrganicSearch("キーワード", 1)

順位調査

指定キーワードで検索した時、指定ページがオーガニックで何位に掲載されるかを返します。

import (
    "net/url"
    "github.com/ryonakao/netsurfer"
)
u, _ := url.Parse("https://qiita.com/ryonakao")
rank, _ := netsurfer.GetRank(u, "ryonakao", 2)

HTML取得

指定ページの静的ページを返します。

import "github.com/ryonakao/netsurfer"
html, err := netsurfer.GetHTML("https://qiita.com/ryonakao")

実装一部紹介

SERPsのurlスライスを返す以下の関数を御覧ください。

gist.github.com

ユーザーにdepthを指定してもらうことで、一回のスクレイピングで全SERPsを取得しています。

ryonakao/netsurderPuerkitoBio/goqueryに依存しています。

まとめ

ライブラリ開発めちゃんこ楽しい♪
どんどん作っていこうと思います!!

GoのcliツールUIライブラリを試す ~ 今日のGithub Trending ~

今日のGithub Trending

カリスマエンジニアになりたい!!
そう願う大学二年生の私は新たな企画を始めます\(^o^)/
題して、 >>今日のGithub Trending<<
Github Trendingを眺めて、面白そうなリポジトリがあったらガンガン試していきます。
Github Trendingとは、盛り上がってるリポジトリを探すことが出来るというGithubが提供するサービスです。
第一回のリポジトリは、今日だけで200スター以上ついているmarcusolsson/tui-goを試していこうと思います!!

f:id:NakaWatch:20171104103359p:plain

チャットクライアント風なcliツールを作ってみる

完成図

f:id:NakaWatch:20171104103118p:plain

実装

基本的に以下の2つの構造体を使って作っていきます。
box.go

ボックスはウィジェットを水平または垂直に配置するためのレイアウトです。 もし 水平にすると、すべてのウィジェットは同じ高さになります。 縦にすると、すべて同じ幅になります。

// Box is a layout for placing widgets either horizontally or vertically. If
// horizontally, all widgets will have the same height. If vertically, they
// will all have the same width.
type Box struct {
    WidgetBase

    children []Widget

    border bool
    title  string

    alignment Alignment
}

entry.go

エントリは、1行のテキストエディタです。 これによってユーザはアプリケーションにテキストを供給することができます。例えば、ユーザおよびパスワード情報を入力することができます。

// Entry is a one-line text editor. It lets the user supply the application
// with text, e.g., to input user and password information.
type Entry struct {
    WidgetBase

    text string

    onTextChange func(*Entry)
    onSubmit     func(*Entry)
}

サイドバー作成

sidebar := tui.NewVBox(
    tui.NewLabel(" <CHANNELS>"),
    tui.NewLabel(" general"),
    tui.NewLabel(" random"),
    tui.NewLabel(""),
    tui.NewLabel(" <DIRECT MESSAGES>"),
    tui.NewLabel(" maria"),
    tui.NewLabel(" john"),
    tui.NewSpacer(),
)
sidebar.SetBorder(true)
  1. NewVBox関数で、縦方向のBoxを作成できます。
  2. 任意数のWidgetを与えることが出来ます。
  3. SetBorderメソッドで枠を表示するかを決めます。

送信済みメッセージを表示

type post struct {
    username string
    message  string
    time     string
}

// 送信済みPostのスライス
var posts = []post{
    {username: "ryo", message: "Hello!", time: "14:41"},
}

history := tui.NewVBox()
history.SetBorder(true)
history.Append(tui.NewSpacer())

for _, m := range posts {
    history.Append(tui.NewHBox(
        tui.NewLabel(m.time),
        tui.NewPadder(1, 0, tui.NewLabel(fmt.Sprintf("<%s>", m.username))),
        tui.NewLabel(m.message),
        tui.NewSpacer(),
    ))
}

NewVBox関数で縦方向のBoxを作成し、Appendメソッドでメッセージを追加していきます。
Appendメソッドの実装を覗いてみましょう。
box.go

// Append adds the given widget at the end of the Box.
func (b *Box) Append(w Widget) {
    b.children = append(b.children, w)
}

Appendメソッドは、Widgetインターフェイスの値をBoxのchildrenというスライスに追加していきます。

入力ボックス作成

input := tui.NewEntry()
input.SetFocused(true)
input.SetSizePolicy(tui.Expanding, tui.Maximum)

inputBox := tui.NewHBox(input)
inputBox.SetBorder(true)
inputBox.SetSizePolicy(tui.Expanding, tui.Maximum)

Entry構造体を作ってNewHBoxの引数に渡すことで、Entry構造体を子要素として持ったBoxを作ることが出来ます。
NewHBoxメソッドの実装を覗いてみましょう。
box.go

// NewHBox returns a new horizontally aligned Box.
func NewHBox(c ...Widget) *Box {
    return &Box{
        children:  c,
        alignment: Horizontal,
    }
}

NewHBox関数はWidgetインターフェイスを子要素に含めることが出来ます。
そのためBox構造体だけではなく、Widgetインターフェイスが実装されているEntry構造体も渡すことができます。

投稿する

import "flag"
var name = flag.String("u", "kazuki", "Set your name")
flag.Parse()

input.OnSubmit(func(e *tui.Entry) {
    history.Append(tui.NewHBox(
        tui.NewLabel(time.Now().Format("15:04")),
        tui.NewPadder(1, 0, tui.NewLabel(fmt.Sprintf("<%s>", *name))),
        tui.NewLabel(e.Text()),
        tui.NewSpacer(),
    ))
    input.SetText("")
})

Entry構造体のOnSubmitメソッドで、Enterを押した時の挙動を設定出来ます。
historyにメッセージを追加します。
flagパッケージを使用して、実行時にユーザー名を指定出来るようにします。

UI作成

chat := tui.NewVBox(history, inputBox)
chat.SetSizePolicy(tui.Expanding, tui.Expanding)

root := tui.NewHBox(sidebar, chat)

ui := tui.New(root)
ui.SetKeybinding("Esc", func() { ui.Quit() })
  1. Boxの、Widgetインターフェイスを子要素に出来る機能を利用して、今まで作成したBoxをrootにまとめていきます。
  2. tcellUI構造体を作成し、Esc押下時にuiを終了するよう、キーバインディングを設定します。

実行

if err := ui.Run(); err != nil {
    panic(err)
}

Runメソッドを覗いてみましょう。
ui_tcell.go

func (ui *tcellUI) Run() error {
    if err := ui.screen.Init(); err != nil {
        return err
    }

    if w := ui.kbFocus.chain.FocusDefault(); w != nil {
        w.SetFocused(true)
        ui.kbFocus.focusedWidget = w
    }

    ui.screen.SetStyle(tcell.StyleDefault)
    ui.screen.EnableMouse()
    ui.screen.Clear()

    go func() {
        for {
            switch ev := ui.screen.PollEvent().(type) {
            case *tcell.EventKey:
                ui.handleKeyEvent(ev)
            case *tcell.EventMouse:
                ui.handleMouseEvent(ev)
            case *tcell.EventResize:
                ui.handleResizeEvent(ev)
            }
        }
    }()

    for {
        select {
        case <-ui.quit:
            return nil
        case ev := <-ui.eventQueue:
            ui.handleEvent(ev)
        }
    }
}
  1. 別ゴルーチンを立ててPollEventメソッドを呼ぶことで、イベント発生を監視します。
  2. イベントを検知したら、eventQueueというチャネルにenqueueしていきます。
  3. eventQueueの状態をメインゴルーチンで監視し、受信を確認したら実行します。

*PollEventはgdamore/tcellという別パッケージのメソッドです。

コード全貌

長くなるのでGIstに記載しました。

https://gist.github.com/ryonakao/6247637a39f32ba0543f5b0421297357

まとめ

評価されてるライブラリを試すことで以下のメリットがあることに気づきました。

  1. コードを多読することになるため、コーディングスキルがUP(今回は、marcusolssonさんのinterface設計がとても勉強になりました。)
  2. OSS界隈での需要が分かる
  3. ワクワクする

また時間があったらやってみたいと思います!!

オレ流学習フレームワーク

はじめに

今記事では、大学生の私が独学でコンピュータサイエンスを学習する際に使用している独自の学習フレームワークを紹介していきます。
あくまで持論なので、すべての人に最適であるとは限りません。
これから紹介するのは、いかに楽して知識を得られるかを念頭に置いたフレームワークです。
ここで言う"楽"というのは、無駄な体力を使わないという意味です。
他にこんな学習の仕方してるよ〜というのがあれば、コメントして頂けると幸いです!

主な学習フレームワーク

発信ドリブン型学習

ブログ等で発信したり、人に教えることを前提としたインプットを行います。
インプットしながらどうアウトプットするかを同時に考えることで、高速な理解が可能になります。
なぜ高速な理解が可能になるのでしょうか?
理解とは、脳の棚に整理された形で分けられている状態だと私は考えています。
人に伝えるためには、内容を整理しないといけません。
インプット中に棚に振り分けていくことで、インプット終了後には既に"理解した"という状態になっているという考え方です。
これを習得すると、書籍を読んでいる時に文章が構造的に見えてくるようになります。

遅延評価型学習

これは有名な遅延評価勉強法のことで、その知識が必要になった時に初めて勉強する方法です。
遅延評価とは、元々はプログラミング手法の一つで、必要になるまで値の評価をしないという意味の言葉です。
"必要は発明の母"という言葉がある通り必要性を感じた時の吸収力はかなり高いため、必要度に比例して吸収力も上昇します。
「直近で必要ではないけど、今学習しておきたいことだってあるぞ!」
そう思う方も少なくないと思います。
実際私も、ネットワークの仕組みやミドルウェアの特性等、学生生活で必要になることがほとんどない学習をしています。
別に必要になるのを待つ必要は無いのです。
必要を作り出せばいいのです。
先日私は、ネットワークの仕組みやミドルウェアの特性を理解しないと勝てない大会に出場することを決め、必要を作り出しました。
その時のレポート↓
CyberAgentのアドテクチャレンジで正確なDMPを作る - 爆速でGo!

このように、遅延評価型学習では必要をコントロールしていくことがキーとなってくると考えています。

フレームワークをどう組み合わせるか

f:id:NakaWatch:20171028152630p:plain

爆発的な吸収力を生む遅延評価型でインプットするところから始めます。
しかし遅延評価型でバラバラに集めた情報はTipsにはなるのですが、知識にはならないと思います。
知識にするためには、ある程度網羅的なインプットが必要です。
そこで発信ドリブン型を使います。
網羅的なインプットというと、分厚い書籍を1ページ目から最後まで順に読むという印象がありますが、これは中々体力を使いますね💦
これでは冒頭で述べた"楽して"という言葉に反します。
ここで私は、独自の読書フレームワークであるgoto文型リーディングを使います。

goto文型リーディング

goto文とはプログラミングの制御構造の一つで、指定された場所に処理をジャンプさせるための文です。
このフレームワークは、各章が独立しておらず順に読まないと意味が分からないタイプの書籍に有効です。
手順は以下の通りです。

  • 目次を見て、この書籍ではどんな内容に触れているかにしっかり目を通す。これは毎回読む前に行う

  • 一番興味ある部分から読む

  • 興味が無くなるまで以下を繰り返す

    • 意味が分からないワードや表現が出てくる

    • 目次が頭に入っていれば、その書籍のどこかで説明されていることが分かる

    • 該当箇所にジャンプする

書籍によりますが、これで大半は読むことが出来ます。
大半を読めばその書籍の内容に関してはかなり理解度があるため、残りの部分は小見出しに魅力がなくても興味が湧くことが多いです。
それでも興味がない部分は無理に読む必要ないと思います。
そっと本棚にしまいましょう。

モチベーション・マネジメント

上記を読んで正直面倒くさいと思った方もいると思います。
私も同感で、上記を完遂するためには一定のモチベーションが必要です。
上記を行うためにはモチベーション・マネジメントを常に行う必要があります。
私は、学習をしていく上でこれが一番重要だと思います。
夢や目標が遠すぎると挫折しがちですよね、、
私は、ちょっと頑張れば届く中期目標向かって進んでいる時が一番モチベーションを高く保つことが出来ます。
この中期目標の距離がどのくらい近いと良いのかは人によって違うと思います。
中期目標を最適な場所に置き続けることが大事だと思います。
これはスキルだと思っています。
環境が変化し続ける中で、常に適切な場所に中期目標を置けるスキルがある人の成長は止まらないと思います。

まとめ

最後まで読んで頂きありがとうございます。
冒頭でも申し上げましたが、あくまで自分の中でしっくりきているフレームワークなので、理解し難い部分があると思います。
学習の質をもっとあげていきたいと思っているので、他に実践されているものがあれば、教えて頂けますと幸いです!

CyberAgentのアドテクチャレンジで正確なDMPを作る

先日株式会社サイバーエージェントの2dayインターン、アドテクチャレンジに参加させて頂きました。
そしてなんと優勝させて頂きました!
私はインフラ設計の経験がなく、常に手探り状態でした💦
今記事では、優勝に至るまでの技術的苦悩をまとめていきます。

developers.cyberagent.co.jp

問題

概要

開発時間終了後、位置情報やIDFAなどのデータが3時間程度大量に送られてくる(2000QPS)。
1リクエストにつき100ms以内に返さないと無効になる。
送られてきたデータを元に問題を解答し、その精度を元に得点がつけられる。
*ちなみにGoogle Cloud Platform使い放題\(^o^)/

詳細

以下の2つの形式でそれぞれ何問か答える

【バッチ問題】

データ送信が完了した後に問題に答える
ex)○時から○時の間に〇〇から半径10km以内にいた人数は?

【リアルタイム問題】

データ送信中に、特定IDFA(端末ごとのID的なやつ)と時間がjsonで送られてくる。
そのユーザーがその時間にどこにいたか等を答える。
1000ms以内に返さないと無効。

アーキテクチャ

最初の案

f:id:NakaWatch:20171024183052p:plain

リアルタイム問題はレスポンスタイム制限があるため、直近で使うデータだけredisに置いておかないと💦
と思っていたのですが、1000msは余裕すぎました。
しかも直近で使うデータなんか分からないから却下。

二番目の案

f:id:NakaWatch:20171024184503p:plain

1000msは余裕と分かり、今回ネックなのは書き込みであることに気付きました。
正直2000QPSが想像つかなかったのですが、直接書き込むのではなくジョブキューを噛ませるのが安全だと考えました。
GCPのCloud Launcherなら、redisがプリインストールされているインスタンスを素早く立ち上げられるし、
クラスタリングも比較的簡単に出来そうだったので決まりか!?と一瞬笑顔になりました。
しかしredisはインメモリDBなので、ログを1つでも取りこぼしてはいけない今回は不適切でした。

三番目の案

f:id:NakaWatch:20171024185718p:plain

様々なミドルウェアを検討しましたが、フルマネージドサービスで統一した方ががつなぎ込みも楽だと考えたので、Cloud Pub/Subを採用しました。
データストアにはBigtableを採用しました。GCPサービスの中では最も時系列データを扱うのに向いていたからです。(後ほど説明します)
さらにバッチ問題用にBigQueryを採用しました。集計が速く、且つ全員が慣れていて学習コストが低いSQLで操作できるためです。
データをBigtableからBigQueryにエクスポートしようと思っていたのですが、そのためのBigtableのリージョンがus-centralとeurope-westしかありませんでした。。。
そこでログをリアルタイムでエクスポートしてくれるStackdriverLogging採用。(後ほど審査時に突っ込まれます)
完成!・・・
しかし全員が口をそろえて言いました。
時間足りなくね(-.-;)

採用案

f:id:NakaWatch:20171024190746p:plain

とりあえずQPS2000でリクエストを流してみたところ、ジョブキュー噛ませなくても余裕で耐えられることが判明しました。
ということで、APIから直接Bigtableに書き込むアーキテクチャで決定しました。

言語

Go

理由

2日間という短い期間で高トラフィックを捌くには最適

  • 実行速度○
  • ビルド速度○
  • go fmtのお陰で規約がいらない
  • GCPサービスとの相性良さそう
  • みんなのGoモチベが高い (超大事)

Bigtableスキーマ設計

問題は時間指定が多かったため、時間順にソートしておきたいと考えていました。
リファレンスを読むとこんなことが書いてありました。

Cloud Bigtable ではデータが非構造化列として行に保存され、各行には行キーがあり、行キーは辞書順で並べ替えられています。

そこで行キーを’timestamp#IDFA'という形式にすることで、探索アルゴリズムの高速化を図りました。
ex)'2017-07-03 10:57:23 +0000 UTC#xxxxxxxxxxxxxx'

StackdriverLoggingとAPIのつなぎ込み

以下の数行コードで通常のstring型ログの書き込みが可能です。
Payloadというinterface{}型フィールドに文字列を渡します。

import "cloud.google.com/go/logging"
var logClient *logging.Client

logger := logClient.Logger("テストログ")
logger.Log(logging.Entry{Payload: "テストテキスト"})

StackdriverLoggingはBigQueryで爆速集計するために採用したため、構造化したログを渡すことが必須でした。
BigQueryにはJsonPayloadという型があるため、Payloadフィールドにjsonで渡すのが最適解なのでは?という仮説のもとリファレンスを漁りましたが、のってませんでした(;´Д`)
GCPソースコードを読んでみました

cloud.google.com/go/logging/logging.go 506~509行目

// Payload must be either a string or something that
// marshals via the encoding/json package to a JSON object
// (and not any other type of JSON value).
Payload interface{}

jsonオブジェクト渡せってことかな??
早速以下のように構造体をエンコードして[]byte型に変換!

import (
        "encoding/json"
        "cloud.google.com/go/logging"
)
type Info struct {
    Latitude  string `json:"latitude"`
    Longitude string `json:"longitude"`
    DeviceID  string `json:"device_id"`
    SysName   string `json:"sysname"`
    SysVer    string `json:"sysver"`
    Timestamp string `json:"timestamp"`
}
info := &Info{}
i, err := json.Marshal(info)
logger := logClient.Logger(logName)
logger.Log(logging.Entry{Payload: i})

エラー!!

深くまでソースを見てみると、
cloud.google.com/go/logging/logging.go 637行目

jb, err := json.Marshal(v)

Payloadフィールドの値を最終的にエンコードしてる!!
普通に構造体渡せば良かったのね!!!

import "cloud.google.com/go/logging"
type Info struct {
    Latitude  string `json:"latitude"`
    Longitude string `json:"longitude"`
    DeviceID  string `json:"device_id"`
    SysName   string `json:"sysname"`
    SysVer    string `json:"sysver"`
    Timestamp string `json:"timestamp"`
}
info := &Info{}
logger := logClient.Logger(logName)
logger.Log(logging.Entry{Payload: info})

無事StackdriverLoggigにjsonログが流れました。
BIgQueryには1カラムに複数のデータを入れることが出来るRECORD型というデータ型があります。
jsonログをStackdriverLoggingに流すことで自動的にRECORD型のカラムを生成してくれます。(設定は必要)

オペレーション

開発時間が終わり、いよいよデータ送信の時間になりました。
すごい速度でリクエストが流れ、興奮していたのも束の間。

http: Accept error: accept tcp [::]:8080: accept4: too many open files

開始30分後、テスト時には発生しなかったエラーが!!
TCPが開いたままになってるのかな??と思いプログラムコードを確認したところ、リクエストボディを閉じ忘れていました。

func collect(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
        // some codes...
}

すぐにデプロイ!
Goはシングルバイナリを上げればいい!素晴らしい!

http: Accept error: accept tcp [::]:8080: accept4: too many open files

なぜだ、、、
KeepAliveオフにしたら解決するかな、、
net/http/request.go 203~206行目

// For client requests, setting this field prevents re-use of
// TCP connections between requests to the same hosts, as if
// Transport.DisableKeepAlives were set.
Close bool

Closeという公開フィールドをfalseにすればDisableKeepAlivesにできるっぽい!
関数が用意されてない時点で非推奨だと思うけど、やるしかない!

func collect(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
        // some codes...
        r.Close = false
}
http: Accept error: accept tcp [::]:8080: accept4: too many open files

1分で死にました。
結局データ送信中は、人間が常にstatusを監視するという力技で乗り切りました笑
大会終了後に社員さんに聞いたところ、linuxのファイルディスクリプタ数(同時に開けるファイル数)の上限を上げれば解決したそうです。 何が起きていたかというと、

  • ファイルopenしてはcloseを繰り返す
  • リクエストが急増するとcloseがopenに追いつかず、too many open filesが発生

勉強になりました。
このような、知らないと解けない問題を解けるエンジニアになりたいです。

まとめ

とにかく死ぬほど楽しかったので、サーバーサイドに興味ある学生さんは絶対参加するべきだと思います!
技術的なことしか書きませんでしたが、チームビルディング等、多くのことを学びました。
また参加したいと思います!

*インフラ初体験の大学生2年生が書いてるので、間違いがあるかもしれません!
もし見つけたら指摘して頂けるとありがたいです。

優勝やったー!

f:id:NakaWatch:20171024221118j:plain

f:id:NakaWatch:20171024221317j:plain

2017年は「決める年」

今週のお題「2017年にやりたいこと」

 

タイトルに書いたように、2017年は「決める年」にします。

以下に記す中期的な目標のために具体的な3つの目標を定める。

 

中期的な目標:時間密度を濃くする(人生哲学は定まっていないが、どんな人生哲学でもおそらくこの能力は必要だろうという仮定のもと)
 
  • 人生哲学(人生で何をしたいかという最上級の目標。心理学的には究極的関心というらしい)を「決める」
人生哲学が決まっていないと成長が左右にブレてしまう。何か重要で複雑な決断をするためには柱となる人生哲学が必要であるだろう。
  • 全てにおいて「決める」スピードと精度を高める
自分には多くの短所があるが、その短所が場合によっては長所になる場合もある。しかしいかなる場合も短所になるであろう短所は「決める」能力の低さだ。この能力を高める(決定のための要因をすべて洗い出して今出せる最善の決断をするまでの時間を早める)ことが必要。
  • めりはりをつけるために逐一何時までになにをするかを「決める」
中期的な目標である「時間密度を濃くする」のためには、今の時間の使い方ではいけない。このままでは続くものも続かないやる時はやる。休む時は休む。
 
 
このように、「決める」を意識しながらこの一年を送ることをここで「決める」