LaravelのEditとDelete

こんにちはscouterでフロントエンジニアをしているhirokinishizawaです! laravel勉強ブログもVersion4.0.0になりました!

前回投稿した記事 techblog.scouter.co.jp

前回は釣りの記録を作成する際にデータベースにレコードを追加できるようにしました。なので今回は編集と削除を出来るようにしました!

はじめに

作成したものの動き

トップページの各記録に編集する削除するを追加しました

gyazo.com

あとレイアウトとして追加したのが

編集ページと

gyazo.com

削除する時の確認ページです

gyazo.com

やったこと

1.記録を編集できるようにした

2.Requestファイルを使って作成時と編集時のvalidateを一つにまとめた

3.記録を削除できるようにした

記録を編集

web.php

<?php

Route::get('/posts/{post}/edit', 'PostsController@edit');
Route::patch('/posts/{post}', 'PostsController@update');

PostsController.php

<?php

  public function edit(Post $post)
    {
        return view('posts.edit')->with('post', $post);
    }

 public function update(PostRequest $request, Post $post)
    {
        $this->validate($request, [
            'fish_name' => 'required',
            'year' => 'required|integer|between:1990,2018',
            'month' => 'required|integer|between:1,12',
            'day' => 'required|integer|between:1,31',
            'prefecture' => 'required',
            'place' => 'required'
        ]);
        $post->fish_name = $request->fish_name;
        $post->year = $request->year;
        $post->month = $request->month;
        $post->day = $request->day;
        $post->prefecture = $request->prefecture;
        $post->place = $request->place;
        $post->save();
        return redirect('/');
    }

前回の@postのときとほぼ同じです。違うのは引数でpostを受け取るので$post = new Post();このように新しくPostを作らなくていいところです。

edit.blade.php作成

コード自体は作成ページと編集ページのレイアウト見てもらうとわかると思いますがほぼかわっていません。変わったのはpostではくupdateするためにメソッドにpatchをを使用しました。

編集ページ

@section('content')
    <h1>記録編集ページ<a href="{{ url('/') }}" class="header-menu">戻る</a></h1>

    <form method="post" action="{{ url('/posts', $post->id) }}">
        {{ csrf_field() }}
        {{ method_field('patch') }}
        <div class="flex-box">
            <div class="column">
                魚の名前
            </div>
            <div>
                <input type="text" name="fish_name" placeholder="ブラックバス" value="{{old('fish_name', $post->fish_name)}}">
                @if($errors->has('fish_name'))
                    <span class="error">{{ $errors->first('fish_name') }}</span>
                @endif
            </div>
        </div>...

新規作成ページ

@section('content')
    <h1>記録を作成する<a href="{{ url('/') }}" class="header-menu">戻る</a></h1>

    <form method="post" action="{{ url('/posts') }}">
        {{ csrf_field() }}
        <div class="flex-box">
            <div class="column">
                魚の名前
            </div>
            <div>
                <input type="text" name="fish_name" placeholder="ブラックバス" value="{{old('fish_name')}}">
                @if($errors->has('fish_name'))
                    <span class="error">{{ $errors->first('fish_name') }}</span>
                @endif
            </div>
        </div>...

見比べると編集ページの方には{{ method_field('patch')}}がありますね。これをformタグの中に書くことによりhttpメソッドのpatchが使えるようになって{{ url('/posts') }}にidを渡すことにより上書きできるようになります。

(後で出てくるdeleteでも似たようなことをしています)

Requestファイル使用

前回postする際に書いたvalidateですがeditでも同じ条件でvalidateを掛けたいのですが下のように重複するのはあまり好ましくないのでRequestファイルに共通部分を書きました。

<?php

public function post(PostRequest $request)
    {
        $this->validate($request, [
            'fish_name' => 'required',
            'year' => 'required|integer|between:1990,2018',
            'month' => 'required|integer|between:1,12',
            'day' => 'required|integer|between:1,31',
            'prefecture' => 'required',
            'place' => 'required'
        ]);
        
        $post = new Post();
        $post->fish_name = $request->fish_name;
        $post->year = $request->year;
        $post->month = $request->month;
        $post->day = $request->day;
        $post->prefecture = $request->prefecture;
        $post->place = $request->place;
        $post->save();
        return redirect('/');
    }

    public function update(PostRequest $request, Post $post)
    {
        $this->validate($request, [
            'fish_name' => 'required',
            'year' => 'required|integer|between:1990,2018',
            'month' => 'required|integer|between:1,12',
            'day' => 'required|integer|between:1,31',
            'prefecture' => 'required',
            'place' => 'required'
        ]);
        
        $post->fish_name = $request->fish_name;
        $post->year = $request->year;
        $post->month = $request->month;
        $post->day = $request->day;
        $post->prefecture = $request->prefecture;
        $post->place = $request->place;
        $post->save();
        return redirect('/');
    }
php artisan make:request PostRequest

こちらのコマンドでApp\Http\Requestsの下にRequestファイルを作成することが出来ます。

<?php
    public function rules()
    {
        return [
            'fish_name' => 'required',
            'year' => 'required|integer|between:1990,2018',
            'month' => 'required|integer|between:1,12',
            'day' => 'required|integer|between:1,31',
            'prefecture' => 'required',
            'place' => 'required'
        ];
    }

    public function messages()
    {
        return [
            'fish_name.required' => '魚の名前を入れてください',
            'year.required' => '1990年から2018年の間で記入してください',
            'month.required' => '1年の中で1月から12月までしかありませんよ',
            'day.required' => '1ヶ月の中で1日から31日までしかありませんよ。',
            'prefecture.required' => '情報提供してください',
            'place.required' => '情報提供してください',
        ];
    }

rules()は名前の通りルールを書いていきます。

下のmessages()はそのルールを破った時にでてくる文章を作ることが出来ます!

前回は文字の変更のやり方が分からなかったのでとても分かりやすく便利だなと思ったRequestファイルでした!

記録を削除

web.php

<?php
Route::get('/posts/delete', 'PostsController@delete')->name('post.delete');
Route::delete('/posts/remove', 'PostsController@remove')->name('post.remove');

削除の方ではnameを使ってみました。nameについては後で説明します。

PostsController.php

<?php

    public function delete(Request $request)
    {
        $post = Post::find($request->id);
       return view('posts.delete')->with('post',$post);
    }

    public function remove(Request $request)
    {
        $post = Post::find($request->id);
        $post->delete();
        return redirect('/');
    }

delete.blade.php作成

コードはこのようになっています

@extends('layouts.app')

@section('content')
    <h1>本当に削除しますか?<a href="{{ url('/') }}" class="header-menu">戻る</a></h1>

    <div class="fishing-record">
        <div class="flex-box">
            <div class="column">魚種</div>
            <div class="text">{{ $post->fish_name }}</div>
        </div>
        <div class="flex-box">
            <div class="column">釣った日付</div>
            <div class="text">{{ $post->year }}年{{ $post->month }}月{{ $post->day }}日</div>
        </div>
        <div class="flex-box">
            <div class="column">釣った場所</div>
            <div class="text">{{ $post->prefecture }}: {{ $post->place }}</div>
        </div>
    </div>
    <form action='{{ route('post.remove') }}' method='post'>
        {{ csrf_field() }}
        {{ method_field('delete') }}
        <input type='hidden' name='id' value='{{ $post->id }}'><br>
        <input type='submit' value='削除'>
    </form>
@endsection
  • {{ route('post.remove') }} 先程のweb.phpで使用したnameはこのようにrouteとして使用することが出来ます

  • {{ method_field('delete') }} 編集機能のpatchと同じです。httpメソッドを使用できるようにしています。

躓いた点

今回editやdeleteをするのにid毎にそれぞれページ遷移させるのでidを渡さなければいけませんでした。以前作成ページを作った時は作成するボタン(遷移ボタン)を押したら作成ページに飛ぶだけでルーティングはRoute::get('/posts/create'だったので遷移ボタンのコードを書く時はhref="{{ url('/posts/create') }}"とこのような書き方をしました。ですが今回idを渡すのでルーティングはRoute::get('/posts/{post}/edit'。遷移先のコードはhref="{{ url('/posts/edit', $post->id) }}"とかいたのですが、遷移した先は/posts/edit/1になってしまいページがないよと言われました。なので別のページ遷移のやり方を調べhref="{{ action('PostsController@edit', $post) }}"という書き方で無事'/posts/1/edit'となりidごとのeditページやdeleteページに遷移することができました! href="{{ action('PostsController@edit', $post) }}"と同様にhref="{{route('post.edit', $post) }}"とnameを決めてrouteで書いても出来ました!というよりこっちの方が一般的みたいです!

終わりに

前回のデータベースを使って記録の更新に続きちょっとだけ応用したeditとdeleteですが今回調べるにあたりいろいろな人のブログを見たら大体一緒にやってることが多かったので次回は脱初心者に向けてなにかいいネタないか考えたいと思います(勉強するにあたって何をすればいいかわからないのでネタ提供求めますww)

来週もよろしくおねがいしますー!

Babel7 OptionalChainingでLaravelのバリデーションをハンドリングする

f:id:kotamat:20180829111209j:plain# Babel7 OptionalChainingでLaravelのバリデーションをハンドリングする

こんにちは、@kotamatです。

昨日Babel7がリリースされましたね🎉

https://babeljs.io/blog/2018/08/27/7.0.0

Babel6から3年かかってのリリースです。コントリビューターの方々お疲れ様でした。 パッケージ名からの破壊的変更が入っていますが、babelのupgrade用スクリプトが用意されているので、手軽にupgradeすることができます(執筆時点だと、beta版までしかあげられませんが‥)

Babel7のリリースに伴い、TS39の新しいシンタクスがサポートされ、その中でもStage1のOptional Chainingが、Laravelのエラーハンドリングにとても相性がいいので紹介させていただきます。

Laravelのバリデーションエラー

Laravel側でFormRequestを用いて下記のようなバリデーションルールを実装した場合、

class StoreRelationRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'address.prefecture'    =>['required', 'string'],
            'address.zipcode'    =>['required', 'integer'],
            'staff.*.id'    =>['required', 'integer'],
            'staff.*.name'  =>['required', 'string'], 
        ];
    }
}

staffの0, 1がバリデーションに引っかかった場合、下記のようなバリデーションメッセージが帰ってきます

{
    "address.prefecture": ["The prefecture field is required."],
    "address.zipcode": ["The zipcode field is required."],
    "staff.0.id": ["The id field is required."],
    "staff.0.name": ["The name field is required."],
    "staff.1.id": ["The id field is required."],
    "staff.1.name": ["The name field is required."]
}

このままでは、フロント側では扱いづらいので、フロント側で扱いやすい形に整形します

reno-shelter/convert-laravel-validationを使うと、上記メッセージは下記のように修正されます

{
    address: {
      prefecture: ['The prefecture field is required.'],
      zipcode: ['The zipcode field is required.'],
    },
    staff: {
      0: {
        name: ['The name field is required.'],
        id: ['The id field is required.'],
      },
      1: {
        name: ['The name field is required.'],
        id: ['The id field is required.'],
      },
    },
  }

Optional Chainingでメッセージを取り扱う

上記オブジェクトでは、例えば、address.*系のエラーがない場合は、当然ながら参照できない形になってしまうため、メッセージ表示部分で存在可否のチェックを行わないとundefined indexのFatal Errorが発生してしまいます。

そこで便利なのがOptionalChainingです。

Optional chainingの仕組み

const obj = {
  foo: {
    bar: {
      baz: 42,
    },
  },
};

const baz = obj?.foo?.bar?.baz; // 42

const safe = obj?.qux?.baz; // undefined

// Optional chaining and normal chaining can be intermixed
obj?.foo.bar?.baz; // Only access `foo` if `obj` exists, and `baz` if
                   // `bar` exists

このように、存在しないパラメータがチェーンの途中にあるばあいは、そこでundefinedを返し、ランタイムでのエラーを防ぐ事ができます。

<template>
    <div>
        <input v-model="address.prefecture" />
        <error v-for="(msg, key) in error?.address?.prefecture || []" :key="key"  :msg="msg"/>
    </div>
</template>

上記のようにテンプレート内でerror?.address?.prefectureとすることによって、もしerror, error.address, error.address.prefectureがそれぞれ存在しなかったとしても問題なく実行されるため、無駄にif文で分岐して処理を複雑にする必要はなくなります。

インストール方法

babel7が入っている前提で話をすすめます。babel6以前からのアップデートはbabelupdateを参考にしてください。

まずはyarn (or npm)でプラグインをインストールし、

yarn add @babel/plugin-proposal-optional-chaining

.babelrcでpluginの設定をするだけです、

{
  "plugins": ["@babel/plugin-proposal-optional-chaining"]
}

Nuxt.jsであればnuxt.config.jsに下記のように記載します

export default {
  build: {
    babel: {
      plugins: ['@babel/plugin-proposal-optional-chaining']
    }
  }
}

まとめ

Babel7で新たに入ったOptionalChainingの紹介をさせていただきました。まだProposalの段階なので変更が入る可能性はありますが、より実装にフォーカスしたコーディングができるようになるので、是非導入してみてください!

Laravel:: Databaseを使用してデータの保管をしてみる

こんにちは最近laravelの勉強をしているSCOUTERでフロントエンドエンジニアをしているhirokinishizawaです!

前回はbladeを使って簡単なレイアウトを作成しました!

techblog.scouter.co.jp

今回はDatabaseを使ってデータの保管の練習をしてみたので最後まで見て頂けると嬉しいです!!

はじめに

今回何をどうゆう順序で作ったか箇条書き!

  1. テスト用のプロジェクト作成
  2. databaseを作成
  3. migration作成
  4. databaseにデータを送る
  5. databaseのデータを表示する

ざっと言うとこんな感じに作成していきました!

作成したもの

gyazo.com

これはまだデータベースに何も入っていない状態です!右上の作成するを押して作成画面に飛びます。

gyazo.com

これに記入していきアップロードするとデータが送られます。

gyazo.com

このように釣れた日付の古い順に表示するようにしました!そのやり方などは後ほど説明していきたいと思います!

新しいプロジェクトを作成した言い訳

前回のプロジェクトをそのまま使用してdatabaseを活用しようとしていたのですが、実際にやろうと思ったら頭の中でごちゃごちゃになってしまったので新しいプロジェクトを作成してアップロードして表示するまでの流れを理解するために新しいプロジェクトを作成しました!

データベース関連

データベース管理システムはMySQLを使用していて管理ソフトはSequel Proを使用しています。

やったこと

databaseの作成方法はSequel Proでクエリを開きcreate database DB_NAMEで作成できます!作成した後show databasesで作成出来ているか確認ができると思います!

databaseを作成したら次はmodelとmigrationを作成していきます!

php artisan make:model Post --migration

これを実行するとappディレクトリの直下にPost.phpというmodelと、database/migrationsの下にmigrationファイルが作成されます。

ではmigrationを書いていきましょうー

gyazo.com

このような感じに(魚の名前、釣った[年、月、日]、釣れた場所[都道府県、場所])を作成しました!

作成したら下のコマンドを実行することでmigrationがかかります

php artisan migrate
php artisan migrate:status

これを実行するといまmigrationがどのような常態かを見ることが出来ます!

次にdatabaseにデータを送ったり、取得してきて表示したりします!

まずはweb.phpとPostsControllerのなかを見ていこうかと思います!

web.php(ルーティング)

gyazo.com

今回はこちらの3つを使いました。使用用途をそれぞれ説明していきたいと思います!

  • '/'
    • これは釣りのデータを取得して表示する画面になります
  • '/posts/create'
    • これは送るためのデータを書き込む画面になります
  • '/posts'
    • これはdatabaseにpost送信して登録を行うurlになります。actionとしての内容はPostsController@postとなっているので次のControllerで説明します!

Controller

urlごとの役割をざっとweb.phpの方で書いたので、Controllerでどんな処理を行っているか説明をしていきたいと思います!

gyazo.com

index

  • orderBy('year', 'asc')
    • これは年が古い順にしてもらう処理です。続けて'month','day'を書くことで月日まで古い順に取得しています!下の写真が例です!
  • view('posts.index')->with('posts', $posts);
    • これは$postsという変数をindex.blade.phpでも使用できるようにしています。違う書き方だとview('posts.index', ['posts' => $posts]);でも同じように動きます!

gyazo.com

create

これはただcreate.blade.phpを表示しているだけですね笑

bladeは前回同様、表示のページと作成ページの共通部分をviews/layoutsのapp.blade.phpに書いてcontentに@yieldをセットしています!

gyazo.com

createのcontent部分はこのようになっています!

gyazo.com

formタグで囲いmethodをpost、actionを/postsにしました。

もしアップロードを押したときにエラーが発生した場合、直前に記入してあったものが消えないようにするためinputタグのvalue属性に{{ old('column名') }}を書いています。

error文に関してはController@postの説明でしたいと思います

post

gyazo.com

最初はこのようにvalidateを書かずにただデータベースに送ることだけを考えて作成。 return redirectは処理が終わった後にどこに遷移するかのurlです!

次にvalidateの設定をしていきます。validateの種類はこちらを確認してください!

gyazo.com

条件に当てはまらなかった時データベースには送られずエラーが発生し表示されます。

gyazo.com

躓いたポイント

maxやmin,betweenなどの数字の最大最小を活用したい時はその確認を実行する前に'integer'(整数値)をいれないと上手く最大最小値が確認できず送信できませんでした。

最後に

画像をアップロードしたりするにはどこかの保管場所が必要みたいなのでその辺の知識など増やして次は写真付きをチャレンジしてみようかと思います!

あと最近はtwitterで筋肉エンジニアの方々が活発に活動しているので、負けじと筋トレをしようかと思います!

hirokinishizawaでしたーーー!ありがとうございました!

データ収集の効率を圧倒的に高めるスクレイピングFW【Scrapy】

はじめまして、R&Dに所属している @shimada です。6月より業務委託として、データ分析・収集やマーケティング自動化などをやらせていただいています。

R&Dチームと何をやっているかについては、CTO id:kotamat のエントリーを参照していただければと思います。

R&Dと開発でチームを分けた理由とOKR

一部抜粋

SCOUTER社はSCOUTER、SARDINE共に人材紹介の事業を行っており、「転職者に寄り添う支援活動」を後押しするような機能開発を行っております。 この文脈において、すぐには結果に結びつかないかもしれないが、将来的にはやるべきな機能の検討から調査・開発までをR&Dチームは行っております。具体的には、上記リリースの通り、転職者属性の解析や求人のリコメンド、獲得をAIを用いて解決していくことを行っています。

上記エントリーにあるように、R&Dでは転職者・求人情報の解析やリコメンド・検索エンジンアルゴリズムの改良に取り組み始めているのですが、それらの機能を開発する前の段階として、SCOUTERが所有しているデータ以外にもWeb上から転職者や求人情報を解析するために必要な大量のデータを収集する必要が出てきました。またR&Dでは、データ分析・解析にはPHPではなく主にPythonを使用しています。このような背景から、今回はPython製の強固なスクレイピングフレームワークであるScrapyを採用するに至りました。

Scrapyって?

ScrapyはPython製のフルスタックなスクレイピングフレームワークです。スクレイピングに最低限必要な機能に加えて、スロットリングや非同期実行、リクエスト/レスポンスのフックなど、だいたいなんでもできちゃいます。

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

また、Djangoにインスパイアされた作りになっており、各コンポーネントの概念さえ把握してしまえば学習コストも高くはありません。

下の図がScrapyのアーキテクチャです。

f:id:tsmd44:20180816000246p:plain

Architecture overview — Scrapy 1.5.1 documentation

  • Engine:各コンポネーントを制御
  • Scheduler:キューイングとスケジューリング
  • Downloader:リクエストとレスポンスオブジェクトの生成
  • Spider:実際のデータ抽出処理を記述する部分、データをItemオブジェクトに詰めてPipelineに渡す
  • Pipeline:CSVやデータベースへの保存など、データの加工・出力を担当

一見複雑そうですが、SchedulerやDownloaderの部分はScrapyがやってくれるので、実際にコードを書くところはSpiderとPipelineぐらいです。

このエントリーでは、SARDINE人材紹介マガジンを例に、Scrapyの基本的な使い方について紹介していきたいと思います。

Scrapyのインストール

Python2.7にも対応していますが、スクレイピングでは特に文字コード問題に悩まされることになるので、3系を使いましょう。最新の3.7でも問題ありません。

$ pip3 install scrapy

今回は、tutorialという名前でプロジェクトを作成します。

$ scrapy startproject tutorial

下記のようなディレクトリが作成されたと思います。 コマンドもディレクトリ構成もDjangoそっくりですね。

tutorial/
    scrapy.cfg
    tutorial/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py       
        spiders/
            __init__.py

Spider

サンプルとして人材紹介マガジンのタイトルとURLを取得するSpiderを作成してみます。SARDINE人材紹介マガジンはNuxt.jsで作成されたプロジェクトですが、サーバーサイドでレンダリングされているため通常のサイトと同じようにスクレイピング可能です。

最初に、tutorial/spiders/ の下に magazine.py というファイル名でSpiderを作成します。

import scrapy

class MagazineSpider(scrapy.Spider):
    name = 'magazine'

    def start_requests(self):
        url = 'https://sardine-system.com/media/'
        yield scrapy.Request(url, callback=self.parse)

    def parse(self, response):
        for selector in response.css('.main-wrapper .container a'):
            yield {
                'title': selector.css('.main-text::text').extract_first(),
                'url': response.urljoin(selector.css('::attr(href)').extract_first())
            }

上から順番に見ていくと、スクレイピング開始直後に start_requests が呼ばれ、リクエストを作成し yield すると、レスポンス取得後のコールバックとして parse メソッドが呼ばれます。

parse メソッドでは、https://sardine-system.com/media/からのレスポンスを引数として受け取り、.css() メソッドでHTMLをパースする処理を記述しています。

response.css('.main-wrapper .container a') では、下の画像の部分のHTMLを抽出しています。

f:id:tsmd44:20180816000644j:plain

Devツールではこんな感じ

f:id:tsmd44:20180816000703j:plain

.css() メソッドでリターンされるのは、 Selector オブジェクト(のリスト)です。Selector オブジェクトから、さらに .css() メソッドをチェーンして、最終的に .extract() でテキストとして抽出するのが基本的な流れです。

Selectorの詳しい使い方については公式ドキュメントを参照してください。

Selectors — Scrapy 1.5.1 documentation

HTMLからのデータ抽出は、XPathで記述する方法もありますが、CSSの方が簡単です。普段CSSを書くのと同じようにクラス名やタグを指定すればOKです。

プロジェクトルートで下記のコマンドを入力すると、out.csv にタイトルとURLの一覧が出力されていると思います。

$ scrapy crawl magazine -o out.csv

簡単ですね!

Scrapy Shell

Python系のライブラリの強みは、iPythonやJupyter notebookでコードをテスト出来るところだと思います。いちいち確認のためにテストやプログラムを実行する必要はありません。

ScrapyにもDjango Shell(Laravelで言うところのtinkerみたいなやつ)のようなシェルが組み込まれており、iPython環境で気軽にターゲットのXPathCSSのテストを行うことができます。

下記のようにコマンドを入力すると、シェルが立ち上がり、https://sardine-system.com/media/ からのレスポンスが response オブジェクトに自動で格納されます。

$ scrapy shell 'https://sardine-system.com/media/'

試しに、最新記事の作成日を取得してみましょう。

In [x]: pub_date = response.css('.main-wrapper .container a time::text').extract_first()
In [x]: pub_date
Out [x]: '2018/08/09'

また、新しいリクエストオブジェクトを作成し、fetch することでshell上で新しいページに遷移することができます。

In [x]: latest_page = response.css('.main-wrapper .container a::attr(href)').extract_first()
In [x]: r = scrapy.Request(response.urljoin(latest_page))
In [x]: fetch(r)
In [x]: response.url
Out [x]: 'https://sardine-system.com/media/posts/p180809'

取得したCookieも自動で送信してれるので、ログイン後に保護されたページをテストしたい場合でも問題ありません。

Pipeline

スクレイピングしたデータは、CSVJSONよりデータベースに保存したい場合の方が多いと思います。Scrapyでは、Pipelineを作成し settings.py に追加することで複数のデータソースに出力することが可能です。

例として、MySQL保存用のPipelineを作成してみたいと思います。 最初にテスト用のテーブルを作成してください。

create database scrapy;
create table scrapy.magazine (
  `guid` varchar(32) not null primary key,
  `title` text null,
  `url` text null,
  `created` datetime default CURRENT_TIMESTAMP not null,
  `updated` datetime default CURRENT_TIMESTAMP not null
);

pipelines.pyMySQLPipeline という名前の新しいPipelineを作成します。

from datetime import datetime
import hashlib
import logging
from twisted.enterprise import adbapi

class MySQLPipeline(object):
    def __init__(self, db_pool):
        self.db_pool = db_pool

    @classmethod
    def from_settings(cls, settings):
        db_args = {
            'host': settings['DB_HOST'],
            'db': settings['DB_NAME'],
            'user': settings['DB_USER'],
            'passwd': settings['DB_PASSWORD'],
            'charset': 'utf8',
            'use_unicode': True
        }
        db_pool = adbapi.ConnectionPool('MySQLdb', **db_args)
        return cls(db_pool)

    def process_item(self, item, spider):
        d = self.db_pool.runInteraction(self._upsert, item, spider)
        d.addErrback(self._handle_error, item, spider)
        d.addBoth(lambda _: item)
        return d

    def _upsert(self, conn, item, spider):
        guid = self.get_guid(item['url'])
        now = datetime.utcnow().replace(microsecond=0).isoformat(' ')

        conn.execute("""
            SELECT EXISTS(
                SELECT 1 FROM magazine WHERE guid = %s
            )
        """, (guid,))
        ret = conn.fetchone()[0]

        if ret:
            conn.execute("""
                UPDATE magazine
                SET title=%s, updated=%s
                WHERE guid=%s
            """, (item['title'], now, guid))
        else:
            conn.execute("""
                INSERT INTO magazine (
                    guid, title, url, created, updated
                ) VALUES (
                    %s, %s, %s, %s, %s
                )
            """, (guid, item['title'], item['url'], now, now))

        spider.log('Saved!')

    def _handle_error(self, failure, item, spider):
        spider.log(failure, logging.ERROR)

    def get_guid(self, url):
        return hashlib.md5(url.encode('utf-8')).hexdigest()

Scrapyでは twisted という非同期ライブラリを使用しているため、ここではDBへの保存もノンブロッキングにしています。twistedのadbapi というモジュールを使っています。

Twisted RDBMS support — Twisted 15.3.0 documentation

settings.py に作成したPipelineを追加して、もう一度Crawlしてみましょう。

ITEM_PIPELINES = {
    'tutorial.pipelines.MySQLPipeline': 100
}
$ scrapy crawl magazine -o out.csv

out.csvmagazine テーブルにデータが出力されていれば完成です!

SPAサイトをスクレイピング

最後にScrapyでSPAサイトをスクレイピングする方法を簡単に紹介したいと思います。

前提として、SPAサイトに限らずJavascriptで動的にデータを取得している場合などは、seleniumを使ってブラウザ経由の(Javascript実行後の)HTMLを解析することになります。しかし、seleniumは本来Webアプリケーションのテスト自動化のためのツールであり、スクレイピングをする上で使い勝手がいい訳ではありません。

動的なサイトのスクレイピングを行う場合、Scrapyと同じscrapinghub社によって開発されたSplashというヘッドレスブラウザがおすすめです。

Splash

Splashは、高速なレンダリング以外にも、複数ページの非同期取得・カスタムJavasriptの実行など、スクレイピングに便利な多くの機能を備えています。

Splash - A javascript rendering service

また、"Javascript rendering service" と記述されるように、Javascriptレンダリング後のHTMLを返すAPIインターフェースを備えた "サービス" であるため、単なるブラウザと表現するのは正しくないのかもしれません。

インストールは、公式から提供されているdockerfileでコンテナを作成する方法が簡単です。

$ docker pull scrapinghub/splash
$ docker run -p 8050:8050 scrapinghub/splash

これで render.html エンドポイントに下のようなリクエストを投げることで、Javascriptレンダリング後のHTMLを取得することができるようになりました。

$ curl 'http://localhost:8050/render.html?url=https://sardine-system.com/media&wait=1.0'

ScrapyからSplashのAPIを使ってスクレイピング

Scrapyから直接APIを叩いてもいいのですが、いくつか問題があるようなので、scrapy-splash というプラグインが推奨されています。

pipでインストール

$ pip3 install scrapy-splash

scrapy-splash settings.pyミドルウェアとして追加します。

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

SPLASH_URL = 'http://localhost:8050/'

HttpProxyMiddleware の優先度が750に設定されているため、scrapy_splashの優先度は750以下になるよう注意してください。

後はspiderをちょっと変更するだけで動的ページに対応です!

import scrapy
from scrapy_splash import SplashRequest

class MagazineSpider(scrapy.Spider):
    name = 'magazine'

    def start_requests(self):
        url = 'https://sardine-system.com/media/'
        yield SplashRequest(url,
                            callback=self.parse, 
                            endpoint='render.html',
                            args={'wait': 1.0})

    def parse(self, response):
        for selector in response.css('.main-wrapper .container a'):
            yield {
                'title': selector.css('.main-text::text').extract_first(),
                'url': response.urljoin(selector.css('::attr(href)').extract_first())
            }

終わりに

今回はScrapyの紹介的な内容で終わってしまいましたが、機会があればスクレイピングをする上で実際の業務でつまずいた点やインフラを交えた話もできればなと思います。

Scrapyは、Pythonを技術スタックとして置いていない企業でも採用する価値のあるフレームワークです。もし業務で(趣味でも)クローリング・スクレイピングを行う機会が出てきたら是非Scrapyを検討してみてください。

VueとBladeを使い簡単なレイアウトを作成!

こんにちはSCOUTERでフロントエンドエンジニアをしているhirokinishizawaです!

前回bladeを使いテンプレートを作ろうと言っていたのですが、弊社ではフロントでvueを使っているということもありせっかくなのでvueも使って自分が大好きな釣りの写真を年ごとにページを作成してみました!

はじめに

今回作ったものがこちらになります!

gyazo.com

画像をクリックするとその年に釣った魚の写真一覧が表示されますj!完全に自己満の写真ファイルみたいな感じになっています!笑

gyazo.com

弊社でlaravel + vueを使ってプロジェクトを作成しているので、vueを入れてから躓いたことは会社のプロジェクトを参考にしながら解決していきました!

Laravelとして使ったもの

今回Controllerファイルは使用していません。 Laravelの何を使っているのかと言いますと、ルーティングとbladeを使用しています!

環境

PHP Composer Laravel node npm Vue
7.0.9 1.6.5 5.5.0 9.1.0 5.5.1 2.7.1

仕様エディタ: PhpStorm

Vueの導入

Laravelのプロジェクトを作成した際にLaravelMix(Laravel で CSS や JavaScript をビルドするための API を提供するもの)にvue.jsが組み込まれています。

なのでlaravelプロジェクトを作成した後にnpm installを実行することにより、vue.jsを導入することができます!

vue.jsを導入した後

vue.jsを導入した後に躓いた点があったので少し紹介

vueを使用する際に使ったファイル

  • 'resources/assets/js'にあるapp.jsファイル
  • 'resources/views'にあるblade.phpファイル
  • vueのcomponents内にある.vueファイル
  • web.php

こちらを使用しました。

ディレクトリ構造

resources
 ├── assets
 │   └── js
 │       ├── app.js
 │       └──components
 │           ├── ContentComponent.vue
 │           ├── HeaderComponent.vue
 │           └── imageComponents
 │               ├── Images_2016.vue
 │               ├── Images_2017.vue
 │               └── Images_2018.vue
 └── views
     ├── home
     │   └── content.blade.php
     ├── layouts
     │   └── app.blade.php
     └── memoryImage
         ├── 2016.blade.php
         ├── 2017.blade.php
         └── 2018.blade.php

app.js

vue.jsを導入した際にapp.jsというファイルが入るのですが、最初はこのようになっています。

gyazo.com

この設定のおかげで最初から

<example-component/>

というタグでExampleComponent.vueをbladeや.vueファイルで使用することが出来ます

上記のディレクトリ構造でapp.jsを書くと

gyazo.com

このような感じでvueコンポーネントをセットできます!

blade.php

※ 'layouts/app.blade.php' ->ベースとなるbladeファイル

親となるapp.blade.phpの中身はこのように記述しました

gyazo.com

headerは固定したかったのでベースファイルであるapp.blade.phpにそのままvueコンポーネントをを呼び出していますが、ベースのcontentの部分には@yield('content')と記述されています。

@yieldの使用方法は子コンポーネントの方でexdendsはファイルの場所(書き方は"ディレクトリ名.bladeファイル名")、sectionはそのファイル(今回で言う'app.blade.php')の@yieldと同じkey('content')を記述します。

gyazo.com

これでurlがcontent.blade.phpの設定のときにapp.blade.phpに読み込まれます。

ルーティングはweb.php

Route::get('/', function () {
//view('ディレクトリ名.bladeファイル名')
    return view('home.content');
});

と設定します。

なのでbladeでレイアウト作成する場合はControllerファイルは使用しないでできます。

これでtopページが作成完了です!

次に年ごとにページを作成していきます。やることは同じです。

ファイルを作成してapp.jsでvueコンポーネントを使用できる状態にしbladeで読み込み、app.blade.phpの@yield("content")で読み込めるようにしルーティングを作成すれば、レイアウトは完成です!

ルーティングはこんな感じになっています!

gyazo.com

2017年のページを開く場合は'/images/2017'となります。

この'memoryImage.2017'はviews/memoryImage/2017.blade.phpになっていて中身は

gyazo.com

と、content.blade.phpの中とほとんど同じです! 唯一違うところは呼び出しているvueコンポーネントが違います!

先程のルーティングの画像を見てもらうとわかると思いますが、

content.blade.phpを見るとき開発環境urlはlocalhost:8000

2017.blade.phpを見るときはlocalhost:8000/images/2017となります。

f:id:hiroki-nishizawa:20180813010533p:plain

f:id:hiroki-nishizawa:20180813024636p:plain

app.blade.phpでヘッダーと背景は固定しているので、@yield('content')の部分だけを作成してルーティングを設定してあげればいろんなページが作成できます!

躓いた点

上記で説明したようにやればレイアウトは作成できるはずだったのですが、なぜかbladeは呼ばれているのにvueコンポーネントが反映されていませんでした。 解決方法は、app.blade.php<body>内に

<script src="{{ asset('js/app.js') }}"></script>

を付けるて 一番最初のdivのidをappで指定してあげてれば解決できました。

gyazo.com

自分は会社のプロジェクトと比較をして間違え探しのような形で見つけたのですが理由を調べてみるとapp.jsのデフォルトで記述されている、

const app = new Vue({
    el: '#app'
});

でidを最初に指定されていて<script>タグで呼ばなければいけなかったみたいです。

今はapp.blade.phpにcontentを作成していく感じなのでapp.jsを呼び出すための<script>タグはapp.blade.phpのみの記述で大丈夫です!

最後に

今はただ画像を挿入して表示しているだけですが、ゆくゆくは年ごとにアップロード、ダウンロードしていけるような仕組みにしてきたいと考えています!

自分が書いているLaravelのブログは自分の成長記録みたいなものを書いているので最初から見ていただけたらとてもうれしく思います!

techblog.scouter.co.jp

NuxtMeetup#4開催しました

こんにちは! @kuwausk です。

昨日NuxtMeetup #4を開催しました!

nuxt-meetup.connpass.com

今回はLINEさんに会場をご提供頂きました!

f:id:yusuke_kuwa:20180807152057j:plain

発表

LINEとNuxt jun

LINEでのNuxtの事例をお話いただきました。 スポンサーセッションでしたが、登壇と同様の知見を共有していただき、とてもためになりました。

STUDIOのつくりかた @keimakai1993

STUDIOのサービスの説明と、どういう構成で実装されているのかというところを発表していただきました。 NuxtのSSRKubernetesで構築したりFirebaseで構築したりと、とてもモダンな環境を構築されているという印象でした! リアルタイム処理は普通に構築すると面倒なので、そういったところを外部のサービスに逃がすというのは興味深かったです。

で、NuxtのSSRっていつ使うの? @kotamats

弊社CTOの松本が登壇いたしました。 NuxtのSSRはコストが高いという話とどういう使いみちがあるかを発表していました。 技術選定するときは、アプリケーションのことだけではなく、インフラも全て含めて総合的に検討する必要があると思いました。

Nuxtのプロダクション事例 @AkiraTameto

様々なプロダクトをNuxtを使って実装された話をしていただきました。 いろいろなアプリケーションで異なる技術的チャレンジをされている話は、聞いていてとてもおもしろかったです。

Nuxtを使うと初心者と上級者の実装差がない @aintek4

f:id:yusuke_kuwa:20180807151356j:plain

Nuxtを実際に使うことに対する、技術的差異のお話をしていただきました。 Nuxtは週末でマスターするという名言がでました。 久しぶりにシリコンバレー見ようかと思います。

リクルートライフスタイルにおけるNuxt.jsの導入事例 @YuG1224

リクルートライフスタイルさんでのNuxtのお話をしていただきました。 既存アプリケーションの乗り換えは非常に難しいところがあるため、こういう事例をお話しいただき大変勉強になりました。

懇親会

LT後に、ピザと飲み物を囲んで懇親会を行いました。

f:id:yusuke_kuwa:20180807151530j:plain

f:id:yusuke_kuwa:20180807151329j:plain

会場提供並びに懇親会をスポンサーしてくださったLINEの皆さん、ありがとうございました!

次回

次回のNuxtMeetupはCyberAgentさんにて開催いたします。 日付は10/18(木)を予定しています。

最後に

SCOUTER社では一緒に頑張ってくれる方を募集しております。 デザイン、エンジニアの皆さん興味のある方はご応募お願いします!

また、一日就業体験も募集しておりますので、お気軽にご応募ください!

Laravel ルーティングとコントローラで躓いた点

SCOUTERでフロントエンドエンジニアをしているhirokinishizawaです。

今回弊社で使用しているLaravelをやり始めることにしたのでアウトプットをするためにインプットをするという自分のための記事を書いていこうかと思います!ちなみにサーバーサイドはprogateでPHPをやったぐらいです!

はじめに

勉強する際PHPフレームワーク Laravel入門という本を使用しているのですが、その内容に沿って今回はLaravelインストールまでと、ルーティングとコントローラを使用して躓いた点・解決策を書いていこうかと思います。

環境

PHP Composer Laravel
7.0.9 1.6.5 5.5.0

仕様エディタ: PhpStorm

Composer

LaravelはComposerを活用してインストールとパッケージの管理をしています。

インストールをしていない方はこちらからインストールしてください。

またコマンドでインストールしたい場合、インストールしたいディレクトリに移動して

curl -sS https://getcomposer.org/installer | php

でインストールしできます。

Laravelをインストール

インストールする方法はLaravelのインストーラーかcomposerのcreate-projectでインストールする2つの方法があります。 今回はcomposerを使いました!

composer create-project --prefer-dist laravel/laravel <プロジェクト名>

これでプロジェクトが作成できるかと思います。次に

php artisan serve

を実行してhttp://localhost:8000にアクセスし f:id:hiroki-nishizawa:20180802043134p:plain このような真っ白な画面が表示されれば、環境構築完了です!

ちなみに後ろに --port=8005のように後ろにつければいくつかのローカルサーバーをたてれます この場合はhttp://localhost:8005にアクセスします。

使っていて躓いた点

Laravel入門を読むのが2回めということもあり、前回読んだときよりも理解できていると思い自信満々で書いたのにつまずいたことを書いていこうかと思います。

ルーティングを作成して404 not foundになった

何度コードを見直してもページがないと言われ、エラーコードもないので完全お手上げ状態だったのですが上司に質問をした結果routeがcacheされてただけでした。 自分でもいつやったのかわからないですが、php artisan route:cacheしていたらしく、php artisan route:clearで解決できました。

Controllerファイルが使えない。

controllerファイルはターミナルからコマンドで作成できます

php artisan make:controller <コントローラ名>

こちらのコマンドを先程作成したリポジトリに移動して実行することでapp/Http/Controllersにcontrollerファイルが作成されます。

最初コマンドがあることを知らず見よう見まねでファイルを作成したのですが、namespaceをApp/Http/~なのに app/Http/~と書いたりclassを間違えたりして動かないことがありました;;

コマンドを使って作成すればこのようにnamespaceやファイル名のclassが書いてあるのでタイプ間違えもなく安心です!

gyazo.com

最後に

次回

今回はルーティングとコントローラでレイアウト作成をするという目標だったためhtmlをコントローラに書いたのですが、laravelにはbladeというテンプレートがあり<?php echo ~;?>みたいな記述をする必要がなかっりレイアウト作成において便利なので次回はvueとbladeを組み合わせてなにか作成してみようかと思います!

イベント開催

LINEさんに会場をお借りしてnuxtmeetup#4 8/6(月)本日開催します! ライブ配信もいたしますのでぜひご視聴ください!