Lチカ開発ブログ

https://l-chika.com/の開発ブログ

Goでgoqueryを利用してスクレイピング

Goでスクレイピング

やりたい事

  1. 検索結果へアクセス。
  2. 検索結果の一覧から、各詳細ページのリンク取得
  3. 詳細ページのコンテンツにアクセス
  4. 検索結果の次のページのリンクを取得。1へ戻る。

github.com

実装

パッケージをインストール

$ go get github.com/PuerkitoBio/goquery

goqueryで実装

$ vim scraping.go
package main

import (
    "github.com/PuerkitoBio/goquery"
    "log"
    "fmt"
    "time"
)

const (
    BASE_URL = "https://hoge"
)

func main() {
    url := nextUrl(BASE_URL + "/foo")
    contentsLinks(BASE_URL + url)
    
    for {
        url = nextUrl(BASE_URL + url)
        if url == "" {
            break
        }
        contentsLinks(BASE_URL + url)
        time.Sleep(3 * time.Second)
    }
}

func contentsLinks(url string) {
    doc, err := goquery.NewDocument(url)
    if err != nil {
        log.Fatal(err)
    }

    var links []string
    // コンテンツの一覧から各詳細ページのURLを検索
    doc.Find("#hoge p.foo a").Each(func(_ int, s *goquery.Selection) {
          url, _ = s.Attr("href")
          links = append(links, url)
    })
    
    // 各詳細ページをスクレイピング
    for _, link := range links {
        a, b := content(BASE_URL + link)
        fmt.Printf("%s, %s\n", a, b)
    }
}

func content(url string) (string, string) {
    doc, err := goquery.NewDocument(url)
    if err != nil {
        log.Fatal(err)
    }

    var name, link string
    doc.Find("#detail div.col-sm-9.col-xs-7.col-xxs-12 ").Each(func(_ int, s *goquery.Selection) {
          name = s.Find("h5.info_inner_title.pdB10.h4").Text()
          link = s.Find("dl.info_inner_contents dd.abel.blue.pdB15 a").Text()
    })
    return name, link
}

// ページネーションから「次へ」のリンクを取得
func nextUrl(current string) (url string) {
    fmt.Printf("========== url: %s\n", current)
    doc, err := goquery.NewDocument(current)
    if err != nil {
        log.Fatal(err)
    }

    var exist bool
    doc.Find("ul.pagination li.next a").Each(func(_ int, s *goquery.Selection) {
          url, exist = s.Attr("href")
    })
    if !exist {
        return ""
    }
    return
}

スターティングGo言語

スターティングGo言語

RailsのAPIモードでドキュメントを作成するならapipie-rails

目的

以前にdockerでraisのapiモードでアプリケーションを作成したので、その続きでapiのドキュメントツールを導入。 l-chika.hatenablog.com

やりたいこと

  • RailsAPIモードでAPI仕様を実装と結びつけて作成したい
  • controllerのparameterに仕様、バリデーション実装からドキュメント・仕様が自動生成される
  • なるべくRailsそのものの機能を利用してAPIを実装したい(grape 等のgemを利用しない。良いgemなのだが学習コストやRailsのルールからはみ出す実装を求められることもあるので)

検討したこ

grape-swagger-rails の利用を検討したが、apiモードでは最低限のGemしかインストールされないでのassets系のgemがないので、swaggerのweb画面を表示する事ができない。 追加でドキュメントのためだけにgemを追加するのも違和感があった。

apiモードでインストールされるGemfile

gem 'rails', '5.1.4'

gem 'mysql2', '>= 0.3.18', '< 0.5'
gem 'puma', '~> 3.7'

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

そこで、 - 学習コストが低く - 実装をそのままドキュメント化できる - assets系の設定が不要 な apipie-rails を採用。

github.com

実装手順

Gemfileの追加

maruku はドキュメントの記載に Markdown(Apipie::Markup::Markdown.new) を利用する場合に必要

gem 'apipie-rails'
gem 'maruku'

インストール

$ bundle install
$ rails g apipie:install
  • config/initializers/apipie.rb が生成
  • config/routes.rb が更新

config/initializers/apipie.rb の変更

Apipie.configure do |config|
  config.app_name = 'Hoge API'
  config.api_base_url = ''
  config.doc_base_url = '/doc'
  config.default_locale = 'ja'
  config.default_version = '1'
  config.languages = ['en', 'ja']
  config.markup = Apipie::Markup::Markdown.new
  config.api_controllers_matcher = Rails.root.join('app', 'controllers', '**', '*.rb')
  config.app_info['1'] = <<-EOS
    ## ほげAPI
    ---
    ## foo
    ### bar

    * a
    * b

        {
          "response": [
            {
              "a": "aa",
              "b": 111
            }
          ]
        }
  EOS
end

ドキュメントの確認

http://0.0.0.0:3000/doc

画面キャプチャ

f:id:l-chika:20171007225204p:plain

関連本

Ruby on Rails 5 超入門

Ruby on Rails 5 超入門

Web API: The Good Parts

Web API: The Good Parts

「サッカーマティクス」を読んで

サッカーマティクス 数学が解明する強豪チーム「勝利の方程式」 (サッカー数学)を読んでの感想。 本書の目的は「数学観とサッカー観の両方を一変させる」こと。 「数学を現実世界に当てはめてみることは、その理論の細部を正確に理解するのと同じくらい重要なこと」とあるようにサッカーに対する数学的アプローチの考え方が大変面白く、思考整理の参考になった。

すべての選手が自分の役割を果たし、パスや動きを通じて連携を保つことで、チームは部分の総和以上の力を発揮できるようになるのだ。この「チーム全体が部分の総和を上回る」という概念こそが、サッカーを数学的なスポーツにする。

本書では、サッカーマティクスを用いて様々な問題に挑んでいる。出発点は常にサッカーだが、そこで足踏みすることはない。 どの章も、サッカーと数学の組み合わせが強力な類推につながることを示す物語で構成されている。

本書ではサッカーの詳しい知識も難しい数学知識などの前提は必要なく、興味深く物語に陶酔できた。

チャンピオンズリーグ決勝の終了間際に2ゴールが決まる確率は?なぜバルセロナの「ティキ・タカ」パスはあれほど効率的なのか?なぜリーグ戦の勝ち点は3なのか?メッシとロナウド、どちらの方が名選手か?なぜブックメーカー(賭け屋)はあれほど魅力的なオッズを提示できるのか?

上記のような切り口のテーマが数学的な根拠で明らかにされるのが大変面白い。

2章 バルセロナと粘菌の隠れた共通点

構造を理解する一つの方法は、俯瞰するというものだ。

フォーメーション・ネットワークの分析と、粘菌との共通点の考察は興味深かった。

7章 戦術マップが暴くチームの個性

イタリアとスペインの最大のちがいはパスのネットワークにある。 スペイン・チームは、1人の中心的なミッドフィルダーにパスを集める代わりに、4人の選手にある程度まんべんなくパスを出した。計算された中心性の値は、イタリアの19.7%に対し、スペインは14.6%。これは微々たる差だがトーマスの研究によると、中心性の値が小さいチームほど有利になる。 中心性の値が小さいチームほど有利になる。(中略) パスの分散が勝利をもたらしたのだ。

8章 部分の総和を上回る超チーム力

1+1は3にも4にもなる 「チームの規律と自己の規律、個人の責任と集団の責任」を教え込むことであり、「それができて初めて全体が部分の総和を超えるものになる」

サッカーマティクス 数学が解明する強豪チーム「勝利の方程式」

サッカーマティクス 数学が解明する強豪チーム「勝利の方程式」

  • 作者: デイヴィッド・サンプター,千葉敏生
  • 出版社/メーカー: 光文社
  • 発売日: 2017/06/16
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Docer & Mysql & Rails APIモード

ローカルにdockerを利用して、RailsAPIモード環境を構築。

作業ディレクトリ・Gemfileの作成

$ mkdir api-app && cd $_
$ bundle init
Writing new Gemfile to /{WORK_PATH}/api-app/Gemfile

Gemfile

$ vim Gemfile
source "https://rubygems.org"
gem 'rails', '5.1.4'

Docker

Dockerfile

FROM ruby:2.4.2
RUN apt-get update -qq && apt-get install -y build-essential libmysqlclient-dev
RUN mkdir /api-app
WORKDIR /api-app
ADD Gemfile /api-app/Gemfile
ADD Gemfile.lock /api-app/Gemfile.lock
RUN bundle install
ADD . /api-app

docker-compose.yml

$ vim docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/api-app
    ports:
      - "3000:3000"
    depends_on:
      - db

MYSQL_ROOT_PASSWORDerror: database is uninitialized and password option is not specified You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD が発生するため

Rails

rails new

$ docker-compose run web rails new . --api --force --database=mysql --skip-bundle

database.yml変更

$ vim config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password: password
  host: db

実行

$ docker-compose build
$ docker-compose up

db作成

別なターミナルで実行

$ docker-compose run web rake db:create

http://0.0.0.0:3000/ で確認。

参考

Docker

Docker

docs.docker.com

easyramble.com

qiita.com

ActiveRecordでIncorrect string value

ActiveRecordで絵文字をインサートする場合に、カラムが utf8mb4なのにMysql2::Error: Incorrect string value が発生。

障害

F, [2017-09-13T09:56:29.800518 #16145] FATAL -- : [ActiveRecord::StatementInvalid] Mysql2::Error: Incorrect string value: '\xF0\x9F\x8D\x9A' for column 'hoge' at row 1: INSERT INTO `talbe` (`column`) VALUES ('絵文字')

改善

config/database.yml の encoding を utf8utf8mb4 にする事で解消。

 default: &default
    adapter: mysql2
 -  encoding: utf8
 +  encoding: utf8mb4
    pool: 5

「人月の神話」を読んで

人月の神話【新装版】 を読んで。

有名過ぎる名著なので、気に入った一節を抜粋。

第2章 人月の神話

人月

コストは実際に人数と月数の積に比例する。が、進捗はそうではない。したがって、仕事の大きさを測る単位としての人月は、疑うべき危険な神話なのだ。人月は、人と月が互いに交換できるという意味だからである。  人と月が交換可能になるのは、多くの作業者の間でコミュニケーション(意思疎通)を図らなくても、仕事が分担できる場合だけである。これは小麦を刈り取るとか、綿を摘むとかいうことには当てはまるが、どうがんばってもシステムプログラム開発には当てはまらない。  仕事が連続して分担できない場合、人を増やすという対策はスケジューリング上、何の効果も生まない。女性がどれほどたくさん動員されたところで、子供1人が生まれてくるまで十月十日かかることに変わりないのと同じだ。多くのソフトウェア開発作業は、デバッグという順次的性質があるためにこうした特徴を持っている。  分担はできるがサブタスク間でのコミュニケーションが必要な仕事においては、コミュニケーションを図る労力を、こなすべき仕事量に追加しなければならない。したがって「人」を「月」と交換してもお粗末な結果しか得られないだろう。

人月の神話【新装版】

人月の神話【新装版】

GoでGoogle Custom Search APIを利用して画像収集

Google Custom Search を利用し、Goで画像を収集。 google-api-custom-search-example を参考に実装。

前提

構成

customsearch/
├── customsearch.go
└── search-key.json

ソース

(Google Custom Search APIを使って画像収集)http://qiita.com/onlyzs/items/c56fb76ce43e45c12339を参考に、検索エンジンID、サービスアカウントキーを発行。

search-key.json

{
  "type": "xxx",
  "project_id": "xxx",
  "private_key_id": "xxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\nxxxxx\n-----END PRIVATE KEY-----\n",
  "client_email": "xxx",
  "client_id": "xxx",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "xxx"
}

customsearch.go

package main

import (
  "fmt"
  "golang.org/x/oauth2"
  "golang.org/x/oauth2/google"
  customsearch "google.golang.org/api/customsearch/v1"
  "io/ioutil"
  "log"
)


func main() {
  data, err := ioutil.ReadFile("search-key.json")
  if err != nil {
    log.Fatal(err)
  }

  conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/cse")
  if err != nil {
    log.Fatal(err)
  }

  client := conf.Client(oauth2.NoContext)
  cseService, err := customsearch.New(client)
  search := cseService.Cse.List("ロゴ")

  // 検索エンジンIDを適宜設定
  search.Cx("xxxxx")
  // Custom Search Engineで「画像検索」をオンにする
  search.SearchType("image")

  search.Start(1)
  call, err := search.Do()
  if err != nil {
    log.Fatal(err)
  }

  for index, r := range call.Items {
    fmt.Println(index)
    fmt.Println(r.Link)
  }
}

実行

$ go run customsearch.go 
0
https://www.google.com/cloud/assets/press/Android_Logo.png
1
https://www.google.com/cloud/assets/press/Chrome_Logo.png
2
https://www.google.com/intl/ja_ALL/insidesearch/images/promos/playground-doodles.jpg
3
https://www.google.com/cloud/assets/press/Google_Cloud_Platform_Logo.png
4
https://www.google.com/cloud/assets/press/G_Suite_Logo.png
5
https://www.google.com/cloud/assets/press/Google_Cloud_Logo.png
6
https://www.google.com/intl/ja/earth/outreach/images/tutorials_screenoverlay1.jpg
7
http://www.google.com/logos/doodles/2016/new-years-day-2016-5637619880820736-hp2x.gif
8
https://www.google.com/maps/about/images/treks/gombe/partner1-logo.jpg
9
http://www.google.com/recaptcha/shared-media/logo2.gif

参考

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

カスタム検索

https://cse.google.com/create/new

APIのパラメータについて

https://developers.google.com/custom-search/json-api/v1/reference/cse/list

package customsearchのドキュメント

godoc.org

Google Go API

github.com

qiita.com