データ収集の効率を圧倒的に高めるスクレイピング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(月)本日開催します! ライブ配信もいたしますのでぜひご視聴ください!

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

こんにちは、 id:kotamat です。 先日このようなリリースを出させていただきました。

prtimes.jp

このリリースに対する顧客へ伝えたいことは、COOが記事にしてもらっているので、こちらを見てもらえればと思います。

reno-coo.hatenablog.jp

今回は組織の話をさせていただければと思います。 この機能の連携は、開発組織とは別軸の「R&D」という組織が行っています。 「R&D」という組織を発足したのにはある理由があり、発足背景がユニークなものだったので、背景とともに記事とさせていただきます。

R&Dチームのやっていること

そもそもR&Dとは「Research & Development」の略で、日本語に訳すと「研究開発」となります。 長期的な目線でプロダクトや技術を研究し、必要に応じて開発にブレイクダウンするというのが、R&Dの大きな役割となります。

SCOUTER社はSCOUTER、SARDINE共に人材紹介の事業を行っており、「転職者に寄り添う支援活動」を後押しするような機能開発を行っております。 この文脈において、すぐには結果に結びつかないかもしれないが、将来的にはやるべきな機能の検討から調査・開発までをR&Dチームは行っております。

具体的には、上記リリースの通り、転職者属性の解析や求人のリコメンド、獲得をAIを用いて解決していくことを行っています。

R&Dを作った背景

そもそもなぜこのような組織の分割を行ったのか。それは会社全体でOKRを取り入れたことが始まりでした。

参考記事 reno-coo.hatenablog.jp

導入背景は上記COOの記事を参考にしていただければと思います。 こちらを開発組織にも導入したわけなのですが、決定までは長く大変な道のりでした。

OKRの導入でわかった時間軸の違い

f:id:kotamats:20180726144004p:plain

OKRを設定するに当たり、チーム内での野心的な目標を設定することになるのですが、メンバー内での「わくわくすること」という軸において、サービスとしていずれ解決しなければならないことと、現状の顧客をどうやって満足させるかというところで意見が割れました。 サービスとしてはどちらも大事であり、当時設定していた全社のOKRにおいてはどちらも着手しておく必要があるという判断の元、チームを分けるという判断に落ち着きました。

OKRという手段を目的として施行していくのは違うかと思いますが、少なくともOKRを用いた、理想形からのブレイクダウンという思考軸によって、各々のメンバーの中にあるサービスへの印象や課題感が明確になり、議論する中で解像度が上がったことは間違いないです。

R&Dと開発の役割

そういった中でR&D発足時のR&Dと開発の役割は下記のようになりました

  • 開発
    • 目の前の顧客がリアルに感じている課題を解決する。そもそものフローとして欠けている機能や、ユーザヒアリングやデータを通じて明らかになった課題への着手。とりわけリリース期間は1〜2週間程度。いかにその価値を最短で届けるかが課題。
  • R&D
    • サービスとしてあるべき姿からの逆算から生まれた機能。顧客ベースでの開発ではあるが、顧客がまだニーズとして認識できていないが、あるとサービスへの依存度や大きな価値を享受できるもの。理想形のイメージや実現確度のバランス感覚、実装にたる技術力や選定が課題

スタートアップにおけるR&Dの時間軸

事業ドメインにおいて、各社方針は違うかと思いますが、ビジネス面での優位性から始まったスタートアップにおいては、どのように対象の顧客をグリップできるかという軸の中で、それをいかに最短に実現するかという問題を解いていく活動になるため、一般的な大企業におけるR&Dに比べると5年10年といった長期間を指すわけではないかと思います。 ビジネスメンバーもアプローチを変えて施策を打っていくということを考えると、半年 から1年程度。さらに、その中でもユーザへの認知をいかに最短にし仮説検証フェーズに回すかという軸で考えると、リリース頻度はそれほど長くとらない方針になるかと思います。

つまり、R&Dの組織が置くべき役割は、「長期的に着手していくこと」というよりも「メイン開発とは別の時間軸で考えなえればならない大きな価値への投資」という位置づけになっていくでしょう。

まとめ:R&Dは手段ではなく目的の実現のために生まれた

開発チームからR&Dチームが分離した背景と、その役割配分を述べさせていただきました。 ビジネスドメインによってR&Dを採用するのかどうかは違うかと思います。

R&Dを導入するべきかどうかという思考回路で検討するのではなく、「サービスとしてどこを目指すか」や「自分たちはなぜここにいるのか」のような大局観からの分解から、R&Dという手段を使うかどうかを判断するという思考で選択するのが大事なのではないかと思います。 そういう大局観を持って思考するという手段として「OKR」は有効でした。 相当の覚悟は必要ですが、必要なプロセスであったと思います。

業務委託メンバーから見た SCOUTER 社と開発チーム

はじめまして、 id:nunulk と申します。フリーランスウェブアプリケーションエンジニアで、今年の2月から SCOUTER 開発チームに業務委託で関わっております。

最近好きな飲み物はルートビアです。

開発者ブログに入社エントリー的な記事書いていいですよって言われてたものの、タスクを捌くので精一杯で放置してしまっていましたが、何度目かのリリースを終えてようやく落ち着いたので、遅ればせながら書いてみようと思います。

軽く自己紹介

プログラマーとしての経験年数もうすぐ 20 年近くになりますが、 ウェブとの出会いは C/C++ 使ってたキャリア3年目くらいで、 C++CGI 書いたり、Java ServletXML に嫌気が差したり、PerlPHP の進化を肌で感じたりして、いつの間にかベンチャーやスタートアップを転々とするようになっていました。

数年前、時流に乗って Ruby や Node.js の仕事もやってはみたものの、長年親しんだ PHP への愛着を捨てきれず、進むべき道について悶々としていた頃、Laravel に出会って最高の開発体験ができるようになったおかげで、迷いが吹っ切れ今に至ります。

ジョイン前の認知

SCOUTER 社と開発チームに対するジョイン前の認知としては、LaraVue 勉強会 の第1回で LT (こんなやつ)させていただいたり、たまたま、代表の中嶋さんが出られていたスクーの「シェアリングエコノミーと私たちの未来を考えよう」という授業(?)のライブ配信を観たりしていて、わりと先端な技術とビジネスモデルの会社で面白そうだなーという印象を持っていました。

特に、中嶋さんのルックスと語り口はロックミュージシャンのようで、元バンドマン(アマチュアですが)の私は、勝手に親近感を抱いておりました(実際の彼もかなりロック1)。

ジョインした経緯

前の現場の契約が切れるタイミングで次を探していたんですが、ふとこんなことをツイートしたら、

CTO の松本さんに拾われて、一緒にランチ行って技術の話やら業界の話やらをして、その何日か後に代表の中嶋さんと面談して決まりました。

相手が意思決定の速いスタートアップで、こちらが Qiita とかブログとか Twitter とかで色々発信していると、こういうのもわりとすんなり話が進むので、仕事探すのがどんどんラクになってる&確度が高くなってると思います。

ジョイン後に印象の変わったところ

閑話休題

事前に聞いてはいたものの、思ったよりもビジネスチームが強い組織です。

代表の中嶋さんと COO の山田さん(の書かれているブログ2がいつも大変面白いです)のおふたりがとてもクレバーで、月末に行われる「締め会」での、冷静で緻密な分析に基づいた各チームへのアドバイスや、今後の事業戦略に関するお話など、毎月タダで聴けるというのが本当にありがたいです。

一方で開発チームは、少数精鋭ではあるものの正直まだパワー不足で、ビジネスチームとのバランス、という点ではやや物足りない感じがあります。

ただ、メンバーのバランスは絶妙だな、というのは感じていて、ひたすら頭の切れる CTO id:kotamat 、唯一の(ほぼ)創業メンバーで、SCOUTER & Sardine を知り尽くした大黒柱 id:ryotakodaira 、常に柔和で前向き、みんながやりたがらない細かいタスク管理を巻き取ってやってくれてる母のような存在 kuwausk 、プログラミング実務未経験 から3ヶ月で100人の前で LT3 できるほど経験&成長したチームのホープ hirokinishizawa 、というプロパーメンバーが、それぞれの得意分野を活かしながらチームをつくっていて、わくわくしています。

経験だけは無駄にあるので、そんなチームの成長に少しでも寄与できればいいなと思っております。

個人的な感想としては、正社員と業務委託の区別がほとんどないのが、私にとっては魅力的です。

業務委託であっても、仕様に関する議論に加えてもらったり、こうやってブログの記事も書かせていただいたり、なんと開発合宿にまで参加させていただいたり、本当にありがたいこと尽くめです。

色んな組織で働いていた身からすると、こうやって若くて勢いのある会社が、業界の慣習やいままで常識とされていたことを、自分たちの頭で考えて、打ち破っていく姿を間近で見られることは、非常に頼もしく喜ばしいことで、いま非常に満足度が高いです。

なので、直近で思ったこととしては、

ということに尽きますね、これは組織全体の話ですが、特に開発チームに言えることなんじゃないかと思っています。

これからの SCOUTER 開発チーム

どんどん新しいメンバーが加わっていて、活気が増しています。ただ、わりとフロントエンドは充実してきつつあるものの、バックエンドがまだ寂しいかんじがしています。

使ってるフレームワークが最近話題の4 Laravel なんで、人口はそこそこ増えてきてると思うんですけども、まだあまり応募がないみたいです。

下記の記事にもあるように、開発効率(「やりたいことを最小限の工数で実現できる」)を重視した技術選定や思想でやっているので、開発体験としては、ほとんどストレスを感じずできるはずですし、Laravel が好きでスタートアップのスピード感が好きってひとならすごくフィットすると思います。

techblog.scouter.co.jp

ご興味ある方はぜひご応募ください、一緒に爆速開発しましょう。

ご応募はこちらから!

bosyu.me


  1. SCOUTERのVALUE作りから浸透まで|Taro Nakajima|note

  2. 株式会社SCOUTERのCOOが人事を尽くして考えた

  3. 入社して3ヶ月でLTをして自分が思った抑えておく3つのこと - SCOUTER開発者ブログ

  4. もうだいぶ浸透してるとは思いますね、去年あたりから、勉強会などで使ったことある人って聞くと、7 - 8 割は手が挙がるので。

NuxtMeetup#3開催しました

こんにちは id:kotamat です。

昨日SCOUTER社にてNuxtMeetup #3を開催させていただきました!

f:id:kotamat:20180607150438p:plain

今回はじめてYoutubeライブをいれてみたのですが、承認が降りたのがイベント直前で十分に仕様を把握できずに使う形になってしまったので、だいぶ視聴環境として良くない形になってしまいました🙏

次回は反省を生かして改善しようかと思います。。

事前アンケート

今回はVue.jsとNuxt.jsを使っているかどうかを事前アンケートとして任意でとらせていただきました。

f:id:kotamat:20180607145908p:plain

ほとんど業務で使用されている方で、Vue自体の認知は確実に広まっている感じがしますね。

f:id:kotamat:20180607145927p:plain

Nuxt.jsに関して業務で使用されている方がほぼ半数とのことで、今年1.0がリリースされたフレームワークとは思えない、参加者の濃度の濃さになりました。

発表

@nuxtjs/auth with Django Rest Framework

www.slideshare.net

Nuxt.jsと認証周りをバックエンドをDjangoで構築するという内容でした。 認証周りはNuxt.js触り始めで悩むポイントなので、いろんな認証の仕方が今後も増えてくるのかなと期待しております!

Nuxt+Storybook+Dockerで作るモダンな開発環境

slides.com

Nuxt, Storybook, Dockerを使った環境に関してお話いただきました。 弊社でもそれぞれ使用していますが、やっぱりコマンド一発で構築できるのは楽ですね!

Nuxt.jsのSSR対応で失敗しないためのDockerデプロイ

speakerdeck.com

SSRモードだと静的ストレージに上げることはできないので、AWS FargateとDockerで固めましたという話でした。 SSRのサーバーサイドの方法はnow.shやNetlify等がある中で、Dockerを使うのはあんまり情報ないので面白く聞かせていただきました

Nuxtでオウンドメディア開発

speakerdeck.com

最後は弊社のエンジニアのRyotaKodairaからの発表でした。 オウンドメディアでWordpress + Nuxt.jsを使うメリットやExpressを使ったSSR +αサーバーサイドの構築について話してもらいました。

懇親会

懇親会ではSCOUTER社から全員分のTシャツをお配りしました!

ケータリングもまい泉カツサンド等を用意させていただきました。

皆さん自社のNuxtの運用の悩みや、相談をされていて、少人数だからこそできた深い話ができていたのかなと思います。

f:id:kotamat:20180607150227j:plain

まとめ

今後もNuxtMeetupを開催させていただきます! 何人か声かけていただきましたが、会場提供者募集中なので、もしご協力いただける場合は私に声かけていただけると嬉しいです!

最後に

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

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