ElixirでWebスクレイピング
Webページの取得にHTTPoison, HTMLのパースにFlokiを利用します。
HTTPoison
GitHub - edgurgel/httpoison: Yet Another HTTP client for Elixir powered by hackney
Floki
実行環境を整える
プロジェクトの作成
$ mix new scraper_sample
関連モジュールの取得
mix.exsに依存関係を記述し取得します。
# mix.exs defmodule NetseaCrawler.Mixfile do use Mix.Project # .... def application do [applications: [:logger, :floki, :httpoison]] end defp deps do [ {:floki, "~> 0.10.0"}, {:httpoison, "~> 0.9.0"}, ] end end
$ mix deps.get Running dependency resolution Dependency resolution completed certifi: 0.4.0 floki: 0.10.0 hackney: 1.6.1 ...
iexの起動
以下のコマンドでiexを起動するとプロジェクトの環境が読み込まれるので、依存モジュールを参照することができます。
$ iex -S mix
スクレイピングしてみる
試しにelixirのページのコミットメッセージを取得してみます。
HTTPoisonでWebページを取得する
iex(1)> ret = HTTPoison.get! "https://github.com/elixir-lang/elixir" iex(2)> %HTTPoison.Response{body: body} = ret
これで変数bodyにHTMLが入ります。
Flokiでパース
ブラウザの「要素の調査」とかでHTMLの構造を把握します。
コミットメッセージは table.files > td.message > span で取れそうなのでFloki.findをパイプライン演算子でつなげていきます。(XPathで書けなそうなのでネストが深いと結構面倒)
iex(3)> Floki.find(body, "table.files") |> ...(3)> Floki.find("td.message") |> ...(3)> Floki.find("span") |> ...(3)> Enum.map(fn(span) -> Floki.text(span) end) ["Make batch style more consistent (#4944)", "Refactor and optimize aggregations over lists", "Fix a typo in the iex man page (#4655)", "Start v1.4.0-dev", "Move .gitattributes files to repo root", "Include docs zip in the publish process", "Use 18.2 instead of 18.2.1", "Integer.digits/2 & Integer.undigits changes (#4868)", "Small fixes in comments and documentation (#4744)", "Add Elixir Forum to list of help channels (#5121)", "Copyright requires just the starting year", "Properly check for earlier Erlang versions, closes#5090", "Add missing NOTICE file", "Add link to security mailing list", "Start v1.4.0-dev", "Start v1.4.0-dev", "Update rebar to rebar 2.1.0-pre. This (among other things) fixes a cr…", "Use proper case for proper nouns and acronyms", "Rebar3 dependency support"]
エラー処理とか全くしてない状態なので、今度はその辺突っ込んでみようかと。
- 作者: Dave Thomas,笹田耕一,鳥井雪
- 出版社/メーカー: オーム社
- 発売日: 2016/08/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
YQLを利用してRSSを取得する
はてブのホットエントリーをJSON形式で取得してReduxを利用したクライアントで表示してみます。
完成品
(左に寄っちゃってますが...)
YQL (Yahoo! Query Console)
CORSによるクロスドメイン制限があるため、Ajaxで直接RSSを取得することはできません。
PHP等のサーバーサイドでRSSを取得してクライアントに表示する必要があります。
今回はサーバーの実装をしないので、YQLを利用します。
こんな感じでSQLのようにクエリを実行すると結果が返ってきます。
最下部にあるTHE REST QUERYをcurlで叩いてみると、RSSがJSON形式で取得できます。
$ curl -X GET "https://query.yahooapis.com/v1/public/yql?q=SELECT%20*%20FROM%20rss%20WHERE%20url%20%3D%20'http%3A%2F%2Fb.hatena.ne.jp%2Fhotentry.rss'&format=json&diagnostics=true&callback=" {"query":{"count":30,"created":"2016-08-02T01:23:13Z","lang":"en-US","diagnostics":{"publiclyCallable":"true","url":{"execution-start-time":"1","execution-stop-time":"452","execution-time":"451","content":"http://b.hatena.ne.jp/hotentry.rss"},"user-time":"454","service-time":"451","build-version":"0.2.39"},"results":{"item":[{"about":"http://www.okinawatimes.co.jp/articles/-/55298","title":"スク水揚げ 奥武島で今年も大漁 | 沖縄タイムス+プラス ニュース","link":"http://www.okinawatimes.co.jp/articles/-/55298","description":"【南城】南城市の奥武島でスク漁が始まり、島の海人が ...
Fetch API
RSSを取得するだけなのでsuperagent等のモジュールではなくFetch APIを利用してみます。
redux-actions、redux-promiseと組み合わせる
fetch()はPromiseを返すのでredux-promiseにそのまま渡します。
then()でreturnした結果がreducerのnextのaction.payloadに渡されます。
// api/bookmarks.js import querystring from 'querystring' import { getRssUrl } from '../constants/ApiCategory' const YQL_URL = "https://query.yahooapis.com/v1/public/yql" const defaultParams = { format: 'json', diagnotistics: true } export default { fetchBookmarks: (category) => { const sql = `SELECT * FROM rss WHERE url = '${getRssUrl(category)}'` const data = Object.assign({}, defaultParams, { q: sql }) const url = `${YQL_URL}?${querystring.stringify(data)}` // Promiseを返す return fetch(url) .then((res) => { return res.json() }) .catch((err) => { console.log(err) }) } }
// actions/api.js import { createAction } from 'redux-actions' import * as types from '../constants/ApiActions' import api from '../api/bookmarks' export const fetchBookmarks = createAction(types.FETCH_BOOKMARKS, api.fetchBookmarks)
// reducers/bookmarks.js const bookmarks = handleActions({ [types.FETCH_BOOKMARKS]: { next: (state, action) => { let items = action.payload.query.results.item return items.map(b => bookmark(b, action)) }, throw: (state, action) => { return [] } } }, [])
今回は1つのstateを全タブで共有しているためクリック毎にrequestが飛んでしまいますが、
カテゴリ(タブ)ごとにstate持たせて管理するとrequest回数も減らせてサクサク動きますかね。
How to build Rails5 API + Redux ToDo Application その4
前回はクライアントをRedux化(API叩かない版)しました。
これにAPIを叩かせて完成とします。
CORS
実装前に、クロスドメインの問題があるので、Railsのほうでごにょごにょします。
# config/application.rb module Rails5ReactReduxTodo class Application < Rails::Application # ... config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', :headers => :any, :methods => [:get, :post, :patch, :delete] end end end end
SuperAgent
AJAXしたいだけなのでjQueryではなくHTTPリクエストに特化したライブラリを利用します。
$ npm install --save superagent
まず、Railsサーバーを起動してREPLからAPIを叩いてみます。
サーバーを起動
$ rails s
REPLから実行
$ node
get: ToDO一覧を取得
> request = require('superagent') > request.get('http://localhost:3000/todos').end(function(err, res) { console.log(res.body) }) > [ { id: 1, title: '単2電池を買いに行く', completed: false, order: 1, created_at: '2016-07-29T00:20:23.797Z', updated_at: '2016-07-29T00:20:23.797Z' } ]
post: ToDOを登録
> request.post('http://localhost:3000/todos').send({ todo: { title: 'ストレッチ', order: 1 }}).end(function(err, res) { console.log(res.body) }) > { id: 2, title: 'ストレッチ', completed: null, order: 1, created_at: '2016-07-29T00:23:17.983Z', updated_at: '2016-07-29T00:23:17.983Z' }
patch: ToDOを完了状態に更新
> request.patch('http://localhost:3000/todos/2').send({ todo: { completed: true }}).end(function(err, res) { console.log(res.body) }) > { id: 2, completed: true, title: 'ストレッチ', order: 1, created_at: '2016-07-29T00:23:17.983Z', updated_at: '2016-07-29T00:25:39.456Z' }
delete: ToDOを削除
> request.del('http://localhost:3000/todos/2').end(function(err, res) { console.log(res.body) }) > {}
こんな感じでWeb APIにアクセスできます。
redux-promise
redux-promiseはcreateActionにPromiseを渡すとよしなにしてくれるMiddlewareです。
redux-actionsと併せて使います。
まずはPromiseを返すWeb APIを作成します。
以下はToDO一覧を取得するAPIの場合です。
// api/todos.js export default { fetchTodos: () => { return new Promise((resolve, reject) => { request .get(url) .end((err, res) => { if (!res || res.status === 404) { reject() } else { resolve(res.body) } }) }) }, /* ... */ }
Action作成時にこの関数を渡します。
// actions/api.js export const fetchTodos = createAction(ApiAction.FETCH_TODOS, api.fetchTodos)
Actionのハンドリングをします。
nextのaction.payloadにはresolveの引数であるbody(todos)が格納されています。
// reducers/todos.js const todos = handleActions({ [ApiAction.FETCH_TODOS]: { next: (state, action) => action.payload, throw: (state, aciton) => [] }, /* ... */ }, [])
このActionをcomponentDidMount時に呼び出します。
// App.js import React, { Component } from 'react' import { connect } from 'react-redux' import * as actionCreators from './actions/api' export default class App extends Component { componentDidMount() { this.handleFetchTodos() } handleFetchTodos() { this.props.fetchTodos() } /* ... */ } App = connect(null, actionCreators)(App) export default App
これを実行しredux-loggerを利用してActionのログを見てみます。
自前でログを仕込んでみたところrequest, responseの間に1回目のFETCH_TODOSが発行されています。
(おそらくこの部分はredux-promiseが処理してくれている)
で、resolveのタイミングで2回目のFETCH_TODOSが発行されhandleActionsのnextがコールされます。
これで非同期でAPIを叩いて動くToDOアプリケーションの完成です。
つぎは何作ろっかなー
How to build Rails5 API + Redux ToDo Application その3
前回はReactでToDOクライアントを実装しました。
で、これをRedux化してみます。
いきなりAPI叩くとややっこしいので、まずはクライアント単独で動作するものを作成してみます。
ソースコードはこちら。
create-react-app使ってるので、clientの下でnpm installしてから
$ npm start
ってやるとブラウザが起動して実行開始されます。
モジュールのインストール
$ npm install --save redux react-redux redux-actions
ディレクトリの再構成
%RAILS_ROOT%にclientフォルダを作成しそこにプロジェクトを配置します。
また、Reduxの作法にしたがったディレクトリ構成にします。
src ├── App.js ├── actions │ └── index.js ├── components │ └── Todo.js ├── constants │ └── ActionTypes.js ├── containers │ ├── TodoForm.js │ └── TodoList.js ├── index.js ├── reducers │ ├── form.js │ ├── index.js │ └── todos.js └── store └── index.js
Actionの洗い出し
今回のToDOで発生するActionをリストアップします。
- 優先順位変更
- タイトル入力
- 登録処理
- 削除処理
- 完了⇔未完了の切り替え
これを元にActionを生成します。
// constants/ActionTypes.js export const CHANGE_ORDER = 'CHANGE_ORDER' export const CHANGE_TITLE = 'CHANGE_TITLE' export const ADD_TODO = 'ADD_TODO' export const DELETE_TODO = 'DELETE_TODO' export const TOGGLE_TODO = 'TOGGLE_TODO'
// actions/index.js import { createAction } from 'redux-actions' import * as types from '../constants/ActionTypes' export const changeOrder = createAction(types.CHANGE_ORDER) export const changeTitle = createAction(types.CHANGE_TITLE) export const addTodo = createAction(types.ADD_TODO) export const deleteTodo = createAction(types.DELETE_TODO) export const toggleTodo = createAction(types.TOGGLE_TODO)
Stateの洗い出し
各コンテナ・コンポーネントがもつStateをリストアップします。
TodoForm
- 入力中のタイトル
- 入力中の優先順位
- 登録ボタンの有効/無効状態
TodoList
- 登録されたToDO一覧
Todo
- タイトル
- 優先順位
- 完了/未完了状態
で、これを元にReducerを作成します。
// reducers/form.js const initialState = { title: '', order: 1, completed: false, disabled: true } const form = handleActions({ /* ... */ }, initialState) export default form
// reducers/todos.js const initialState = { title: '', order: 1, completed: false } const todo = handleActions({ /* ... */ }, initialState) const todos = handleActions({ /* ... */ }, []) export default todos
これらをcombineReducersでくっつけてstoreとします。
と書いてるとだんだんややこしくなってくるので、図にしてみました。
ユーザーからの操作でReducerに対してActionが発行され、Stateが更新されます。
更新されたStateで再描画される、ってなわけです。
ひとまずRedux方式になったので、次回はこれにAPIを叩かせたいと思います。
- メディア:
- この商品を含むブログを見る
How to build Rails5 API + Redux ToDo Application その2
前回はRails5でToDOリストのREST APIを実装しました。
今回はクライアント側の実装をしてみます。
イメージ
言語・ツール等
- ES6 (ES2015)
- React
- gulp
- webpack
. ├── components │ ├── todo-box.js │ ├── todo-form.js │ ├── todo-list.js │ └── todo.js └── main.js
コンポーネントの構成
各パーツをコンポーネント化していきます。
実装してみる
まずTodoから。StateとしてCompleted(完了/未完了)状態を持っています。
完了時にバックカラーが変更され削除時には列ごと消えます。
// components/todo.js import React, { Component } from 'react' export default class Todo extends Component { constructor(props) { super(props) this.state = { completed: props.completed } } rawMarkup() { var rawMarkup = marked(this.props.children.toString(), { sanitize : true }) return { __html: rawMarkup } } render() { return ( <tr className={this.state.completed ? "success": ""}> <td> <input type="checkbox" onChange={(e) => this.props.onTodoCompleted(this, e.target.checked)} checked={this.state.completed ? "checked": ""} /> </td> <td>{this.props.order}</td> <td><span dangerouslySetInnerHTML={this.rawMarkup()} /></td> <td> <input className="btn btn-danger" type="button" onClick={() => this.props.onTodoDelete(this.props.id)} value="削除" /> </td> </tr> ) } }
Todoを管理するTodoListを作成します。
// components/todo-list.js import React, { Component } from 'react' import Todo from './todo' export default class TodoList extends Component { constructor(props) { super(props) } render() { let todoNodes = this.props.data.map(todo => { return ( <Todo url={this.props.url} completed={todo.completed} order={todo.order} id={todo.id} key={todo.id} onTodoDelete={this.props.onTodoDelete} onTodoCompleted={this.props.onTodoCompleted} > {todo.title} </Todo> ) }) return ( <div className="row"> <table className="todoList table table-striped"> <thead> <tr> <th width="10%">完了</th> <th width="20%">優先順位</th> <th>やること</th> <th width="20%"></th> </tr> </thead> <tbody> {todoNodes} </tbody> </table> </div> ) } }
次にTodoFormです。優先順位とTodoを入力して登録する普通のフォームです。
// components/todo-form.js import React, { Component } from 'react' export default class TodoForm extends Component { constructor(props) { super(props) this.state = { order: 1, title: '' } } handleOrderChange(e) { this.setState({ order: e.target.value }) } handleTitleChange(e) { this.setState({ title: e.target.value }) } handleSubmit(e) { e.preventDefault() let order = this.state.order let title = this.state.title.trim() if (!order || !title) { return } this.props.onTodoSubmit({ order: order, title: title, completed: false }) this.setState({ order: 1, title: '' }) } render() { return ( <div className="row"> <form className="todoForm form-horizontal" onSubmit={this.handleSubmit.bind(this)}> <div className="form-group col-md-2"> <input id="todo-order" className="form-control" type="number" value={this.state.order} onChange={this.handleOrderChange.bind(this)} /> </div> <div className="form-group col-md-8"> <input id="todo-title" className="form-control" type="text" value={this.state.title} onChange={this.handleTitleChange.bind(this)} /> </div> <div className="col-md-2"> <input className="btn btn-primary" type="submit" value="登録" /> </div> </form> </div> ) } }
んでこれらを管理するTodoBoxを作成します。
GET, POST, PATCH, DELETE処理はすべてここに実装しています。
これらを下位のコンポーネントに引き渡して状態を更新していく流れです。
import React, { Component } from 'react' import TodoList from './todo-list' import TodoForm from './todo-form' export default class TodoBox extends Component { constructor(props) { super(props) this.state = { data: [] } } loadTodosFromServer() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, success: (data) => { this.setState({ data: data }) }, error: (xhr, status, err) => { console.error(this.props.url, status, err.toString()) } }) } handleTodoSubmit(todo) { todo.id = Date.now() let todos = this.state.data let newTodos = todos.concat([todo]) this.setState({ data: newTodos }) $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: { todo: todo }, success: (data) => { this.loadTodosFromServer() }, error: (xhr, status, err) => { this.setState({ data: todos }) console.error(this.props.url, status, err.toString()) } }) } handleTodoCompleted(todo, completed) { $.ajax({ url: this.props.url + '/' + todo.props.id, dataType: 'json', method: 'PATCH', data: { todo: { completed: completed } }, success: (data) => { todo.setState({ completed: data.completed }) }, error: (xhr, status, err) => { console.error(this.props.url, status, err.toString()) } }) } handleTodoDelete(id) { $.ajax({ url: this.props.url + '/' + id, dataType: 'json', method: 'DELETE', success: (data) => { this.loadTodosFromServer() }, error: (xhr, status, err) => { console.error(this.props.url, status, err.toString()) } }) } componentDidMount() { this.loadTodosFromServer() // setInterval(this.loadTodosFromServer.bind(this), this.props.pollInterval) } render() { return ( <div className="todoBox"> <h1>Todoリスト</h1> <TodoForm onTodoSubmit={this.handleTodoSubmit.bind(this)} /> <TodoList data={this.state.data} url="/todos" onTodoCompleted={this.handleTodoCompleted.bind(this)} onTodoDelete={this.handleTodoDelete.bind(this)} /> </div> ) } }
エントリポイントでこれらのコンポーネントを読み込みます。
import React from 'react' import { render } from 'react-dom' import TodoBox from './components/todo-box' let content = document.getElementById('todos') render( <TodoBox url="/todos" pollInterval={2000} />, content )
webpackしたものをHTMLに組み込んで完了です。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Rail5 + React Todo</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"> </head> <body> <div id="todos" class="container"></div> <script type="text/javascript" src="dist/bundle.js"></script> </body> </html>
ReactはあくまでUIに特化したライブラリなので、正直Ajax周りの処理をどこに書いてどうしたらいいかってのが全然わかりませんでした。
これをRedux化するとどうなるんでしょうかね。続きは次回。
入門 React ―コンポーネントベースのWebフロントエンド開発
- 作者: Frankie Bagnardi,Jonathan Beebe,Richard Feldman,Tom Hallett,Simon HØjberg,Karl Mikkelsen,宮崎空
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/04/03
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Hello, Mix!
Mixとは
Elixirのプロジェクト管理ツールです。
プロジェクトの作成
$ mix new hello_world * creating README.md * creating .gitignore * creating mix.exs * creating config * creating config/config.exs * creating lib * creating lib/hello_world.ex * creating test * creating test/test_helper.exs * creating test/hello_world_test.exs Your Mix project was created successfully. You can use "mix" to compile it, test it, and more: cd hello_world mix test Run "mix help" for more commands.
- README.md
- プロジェクトの説明
- .gitignore
- gitの管理下に置かないファイルの定義
- mix.exs
- プロジェクトの設定
- config/*
- アプリケーションの設定
- lib/*
- ライブラリ
- test/*
メインモジュールの作成
プロジェクトが作成されたので、プロジェクトのディレクリに移動してビルドしてみます。
$ cd hello_world $ mix escript.build Compiled lib/hello_world.ex Generated hello_world app Consolidated List.Chars Consolidated Collectable Consolidated String.Chars Consolidated Enumerable Consolidated IEx.Info Consolidated Inspect ** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1
「プロジェクト設定の:escriptオプションにmain関数を実装している:main_moduleを指定しろ」 とのことなので、そのようにします。
# ./mix.exs defmodule HelloWorld.Mixfile do use Mix.Project def project do [app: :hello_world, version: "0.0.1", elixir: "~> 1.2-rc", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps] end # ... 以下略 end
# ./lib/hello_world.exs defmodule HelloWorld do def main(args) do IO.puts "Hello, World!!" end end
ビルド&実行
メインモジュールを追記後、再度ビルドします。
$ cd hello_world
$ mix escript.build
Compiled lib/hello_world.ex
Generated hello_world app
Consolidated List.Chars
Consolidated Collectable
Consolidated String.Chars
Consolidated Enumerable
Consolidated IEx.Info
Consolidated Inspect
Generated escript hello_world with MIX_ENV=dev
ビルドに成功すると直下にバイナリが作成されるので実行してみます。
$ ./hello_world Hello, world!
Programming Elixir: Functional, Concurrent, Pragmatic, Fun
- 作者: Dave Thomas
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2014/10/19
- メディア: ペーパーバック
- この商品を含むブログを見る
Ansible入門
Ansibleとは
Chef, Puppetと同様の構成管理ツール。
インストール
$ git clone git://github.com/ansible/ansible.git --recursive $ cd ./ansible $ source ./hacking/env-setup $ ansible Usage: ansible <host-pattern> [options] Options: -a MODULE_ARGS, --args=MODULE_ARGS module arguments --ask-become-pass ask for privilege escalation password ...
リモートホストの管理
※事前にVagrant等でSSHログインできるホストを用意しておく
Inventoryの作成
$ vim ~/ansible_hosts [webservers] 192.168.33.100 ansible_user=vagrant ansible_ssh_pass=vagrant
疎通確認
$ ansible all -m ping -i ~/ansible_hosts 192.168.33.100 | SUCCESS => { "changed": false, "ping": "pong" }
しゃべらせてみる
$ ansible all -a "/bin/echo hello" -i ~/ansible_hosts 192.168.33.100 | SUCCESS | rc=0 >> hello
リモートホストの構成
ex.) Apache
Playbook
# httpd.yml --- - hosts: webservers vars: http_port: 80 max_clients: 200 become: true tasks: - name: ensure apache is at the latest version yum: name: httpd state: latest - name: ensure apache is running (and enable it at boot) service: name: httpd state: started enabled: yes handlers: - name: restart apache service: name: httpd state: restarted
実行
$ ansible-playbook -i ~/ansible_hosts httpd.yml PLAY *************************************************************************** TASK [setup] ******************************************************************* ok: [192.168.33.100] TASK [ensure apache is at the latest version] ********************************** changed: [192.168.33.100] TASK [ensure apache is running (and enable it at boot)] ************************ changed: [192.168.33.100] PLAY RECAP ********************************************************************* 192.168.33.100 : ok=3 changed=2 unreachable=0 failed=0
- 作者: 若山史郎
- 発売日: 2014/07/30
- メディア: Kindle版
- この商品を含むブログ (2件) を見る