無駄と文化

実用的ブログ

いまさらまとめるブックマークレットの作り方 〜 2016年版 〜

これまでプライベート・業務の両面で個人的にブックマークレットを作っては利用してきました。
我ながらだいぶ小慣れてきた感じがあります。

このブログでもブックマークレット関連のネタは書いてきましたが、ブックマークレットそのものの作り方に関してまとめていなかったので、あらためて書いてみます。


そもそもブックマークレットって何やー

シンプルに言うと「ブックマークに登録しておいて、自分の好きなタイミングで実行できるJavaScriptのコード」です。


f:id:todays_mitsui:20160229031614j:plain

実例として、
WordPress は文章を引用して素早く記事を作成するための Press This というブックマークレットが標準で付いてきます。


ブックマークレットでできること

長らく草案だった HTML5 がついに勧告され、近年では ECMAScript 2015(ES2015, ES6) がモダンブラウザに着々と実装されてきています。
この状況を見ても、これから先、ブラウザと JavaScript だけで出来る事は増えていくでしょう。

ブックマークレットは、あらかじめブックマークに登録しておいたJavaScriptを実行するだけというシンプルなものですが、応用次第でこれから使い方の幅は広がっていくと思います。


実用例を具体的に挙げてみると、

  • ajax で外部サーバーと通信
  • canvas で画像を動的に生成
  • File API でファイルを動的に生成
  • (iPhoneなどで) 加速度センサーやコンパスからの情報を表示

と、HTML5 の機能を積極的に利用することでかなりいろいろなことが出来そうです。


ブックマークレットの作り方

さて、ここから本編です。
やりたいことに合わせて、いくつかのテンプレートを紹介しようと思います。


基本編

まずは基本、いちばんシンプルなパターンから。

実行したい処理をコードに落とさなければ始まりませんので、

void((function(undefined) {
  /* ここに処理の本体を記述 */
})());

こんな感じに。

コードが書けたら、ブックマークレットとして動作させるために /packer/ などで1行に圧縮して、

f:id:todays_mitsui:20160229022244p:plain


圧縮したら、先頭に javascript: を付け足して、ブックマークに登録すればオッケィです。

f:id:todays_mitsui:20160229023217p:plain


テンプレートについて補足しておきますね。

  1. 元々のページのグローバルを汚染しないように処理全体を無名関数の即時実行でラップしています。

  2. さらに実行結果を void() 演算子に渡しています。
    実行された JavaScript が何らかの値を返してしまうと、それに応じてページ遷移してしまいます。それを抑制するのが目的です。
    void() 演算子に渡された式がどんな値を返そうとも、 void() 演算子自体は確実に undefined を返してくれます。

  3. グローバルの undefined が何らかの値で上書きされている場合を考えて、念のために仮引数を使って undefinedundefined で上書きすることもやっています。


/* ここに処理の本体を記述 */ となっている部分には思い思いの処理を記述してもらえればと思います。
どんなに長くなっても(モダンブラウザであれば)大丈夫です。ブラウザが提供している全ての標準API を使うことができます。

ただし、
特定のフレームワークやライブラリ(例えば jQuery とか)を前提としたコードを書くべきではありません。
ブックマークレットを使おうとしたページに偶然にも jQuery オブジェクトが読み込まれていればそれが利用できますが、全てのページに jQuery オブジェクトが存在していることを期待してはいけません。


jQuery などの外部ライブラリを使いたい場合には次の節で解説するやり方を使ってください。

なお、コードを1行に圧縮する工程と、javascript:を付け足してブックマークに登録する工程は全ての場合で共通なので、これ以降は省略します。


jQuery などのライブラリを使用する場合

近年は脱 jQuery の風潮もありますが、小規模なコードで手っ取り早く目的を達成するなら、なんやかんやで jQuery 便利ですよね。

そんなわけで、外部からjQueryを読み込んで使用したい場合は以下のようにしてください。

void((function(f){
  // <script> タグを生成
  var script = document.createElement('script');

  // cdnjs(https://cdnjs.com) から jQuery 2.2.1 を読み込む
  // プロトコルは省略してドメイン相対パスで
  script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';

  // jQuery の読み込みが完了してから本体の処理が実行されるように
  // onload イベントを設定
  script.onload = function(){
    // 既存サイトに存在するかもしれない $ を汚染しないように
    // jQuery.noConflict(true) を実行しつつ $ を取得
    var $ = jQuery.noConflict(true);

    // $ を渡しつつ本体の処理を実行
    f($);
  };

  // <body> 直下に生成した <script> を差し込む
  document.body.appendChild(script);
})(function($, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);

  /* ここに処理の本体を記述 */
}));

無名関数を二重にして使っているので、無名関数に慣れていない人は戸惑うかも知れません。

前半で cdnjs から jQuery を読み込んで jQuery オブジェクトを取得しています。
それを後半の無名関数に渡すことで、無名関数の中では $ を自由に使えるという仕組みです。


もちろん jQuery じゃなくて Lodash(Underscore.js) が使いたいんだよ、という場合もほぼ同じ記述で実現できます。

void((function(f){
  var script = document.createElement('script');

  // cdnjs(https://cdnjs.com) から Lodash 4.5.1 を読み込む
  // プロトコルは省略してドメイン相対パスで
  script.src = '//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.5.1/lodash.min.js';

  script.onload = function(){
    // 既存サイトに存在するかもしれない _ を汚染しないように
    // _.noConflict() を実行しつつ _ を取得
    var _ = _.noConflict();

    // _ を渡しつつ本体の処理を実行
    f(_);
  };

  document.body.appendChild(script);
})(function(_, undefined){
  // バージョン確認
  ;;; console.log('Lodash: ', _.VERSION);

  /* ここに処理の本体を記述 */
}));

このように。


jQuery などのライブラリを再利用する場合

CDN から読みこめばあらゆるライブラリを自由に使うことが出来るわけですが、せっかちな人であれば読み込みによる僅かなタイムラグが気になるかも知れません。

実際、jQuery であれば結構な割合のサイトで利用されてますからね。
なので、すでに jQuery オブジェクトが存在しているページでは CDN からの読み込みを省略するようにしてみましょう。

void((function(f){
  // jQueryの存在チェックとバージョンチェック
  if(window.jQuery && jQuery().jquery > '1.8') {
    // jQueryが存在していればそれをそのまま使う
    f(jQuery);
  }else{
    // 存在しない場合は cdnjs(https://cdnjs.com) から読み込み

    var script = document.createElement('script');

    script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';
    script.onload = function(){
      var $ = jQuery.noConflict(true);
      f($);
    };

    document.body.appendChild(script);
  }
})(function($, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);

  /* ここに処理の本体を記述 */
}));

はい、
jQuery オブジェクトの存在チェックをして条件分岐させています。
未だに v1.6 とかの古い jQuery を使っているサイトもまぁまぁあるのでバージョンチェックもしておいたほうがいいでしょう。


複数のライブラリを使用する場合

ここまでは、何か一つのライブラリを読み込んで使用したい場合について解説してきました。

では、複数のライブラリを使って処理したくなったらどうでしょう。
読み込むスクリプトが一つなら、onload イベントの中で本体処理を呼べばいいんですが、複数となると厄介ですね。

...仕方ない、Promise を使いましょう。


jQuery と Lodash と Moment.js を同時に使う例がこちら、

void((function(f){
  // jQuery 読み込み用の Promise オブジェクト
  var jQueryPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(jQuery.noConflict(true)); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // Lodash 読み込み用の Promise オブジェクト
  var lodashPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.5.1/lodash.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(_.noConflict()); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // Moment.js 読み込み用の Promise オブジェクト
  var momentPromise = new Promise(function(resolve, reject) {
    var script = document.createElement('script');
    script.src = '//cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js';

    // 読み込み成功時
    script.onload = function() { resolve(moment); };

    // 読み込み失敗時
    script.onerror = function() { reject(); };

    document.body.appendChild(script);
  });

  // 3つのスクリプトが全て正常に読み込まれた後の処理を設定
  Promise.all([jQueryPromise, lodashPromise, momentPromise])
    .then(function(libs) {
      // Function.prototype.apply() を使って配列を展開しつつ本体処理に渡す
      f.apply(window, libs);
    })
    .catch(function(err) {
      // 読み込みに失敗したときは何かエラー処理を
      console.error(err);
    });
})(function($, _, moment, undefined){
  // バージョン確認
  ;;; console.log('jQuery: ', $().jquery);
  ;;; console.log('Lodash: ', _.VERSION);
  ;;; console.log('Moment.js: ', moment.version);

  /* ここに処理の本体を記述 */
}));

それぞれのスクリプトの読み込みを Promise オブジェクトにして実行。
Promise.all() で全てのスクリプトが読み込まれた後のコールバックとして本体の処理を呼び出しています。

確かに問題なく動くんですが、冗長で醜いコードですね。


複数のライブラリを使用する場合 (Bundle版)

Promise を使った例でなんとなくツラさを感じ取っていただけたかなと思います。

結論から言うと、複数のライブラリを使ってある程度以上の規模のコードを実行する場合には、必要なライブラリを全てまとめた JS ファイルを作っておいてそれを読み込ませる方が早いです。

ライブラリを npm などで管理して、Browserifywebpackrollup.js などで JS をパックしてから使うのが、開発環境的にもモダンなんじゃないでしょうか。


Browserify を使用して、先ほどと同じく jQuery, Lodash, Moment.js を使う処理を記述するとこんな感じになります。

var $ = require('jquery');
var _ = require('lodash');
var moment = require('moment');

// バージョン確認
;;; console.log('jQuery: ', $().jquery);
;;; console.log('Lodash: ', _.VERSION);
;;; console.log('Moment.js: ', moment.version);

/* ここに処理の本体を記述 */

めちゃくちゃシンプルになりましたね。 これを main.js とかの名前で保存して、

$ browserify main.js -o bundle.js

bundel して...

f:id:todays_mitsui:20160229024416p:plain

Web上の適当な場所に置いて、
※ 上記の例では Dropbox の Public ディレクトリに入れました。GitHub Page に置くのもいいかも知れませんね。

で、ブックマークレットの方は bundle.js を読み込むだけのものになります。

void((function(undefined) {
  var script = document.createElement('script');

  // 先ほど作った bundle.js を読み込み
  script.src = '//path.to/bundle.js';

  // <body> 直下に生成した <script> を差し込む
  document.body.appendChild(script);
})());


正直、開発の手順を考えると、このやり方がいちばん楽ですね。
bundle.js を Web 上に永続的に保持しておく必要はありますが...。


まとめ

というわけで、5つのパターンに分けてブックマークレット用のテンプレートをご紹介しました。

普段、JS はバリバリ書いてるけどブックマークレットは作ったことがないという方は、このテンプレートに自慢のコードをぶち込んでいい感じのブックマークレットを作ってみてください。


私からは以上です。

Laravel5 ベースのプロジェクトに React が爆速で導入できた話

f:id:todays_mitsui:20160121230347p:plain


ここ3ヵ月ほどプライベートで Laravel 5.2 を使ってみています。
この度、バックエンドを Laravel、フロントエンドを React で Web アプリを組んでみようかと思い立ち、初めて React を導入してみました。

『下準備が面倒かもな...』とか思ってたんですが、Laravel Elixir のおかげで爆速で React を導入することができました。


この記事は React 導入記事ではありますが、React 入門記事ではありません。React で "Hello, world!" を表示するところまでの内容です。


Laravel Elixir

Laravel には Laravel Elixir と呼ばれる gulp.js タスク群が標準で付属しています。
Laravel で構築しているプロジェクトならば、Sass や CoffeeScript のコンパイルのために別途セットアップが必要になることはありません。

... とはいえ、
さすがに Node.js や npm, gulp.js などが導入済みであることは前提なんですけどね。
それらの導入が済んでいない人は、以下のよく分かる記事を参照しつつ、npm i -g gulp くらいまでは済ませておいてください。

blog.mudatobunka.org


さて、Laravel Elixir で実行できるタスクは以下のとおり、

  • Less コンパイル
  • Sass コンパイル
  • css ファイルの結合
  • CoffeeScript コンパイル
  • Browserify で js ファイル生成
  • Babel コンパイル
  • js ファイルの結合

React を使うのに必要なのは Browserify と Babel くらいなので、必要なものは揃っていますね。
Laravel Elixir があれば事前準備はグッと簡単になります。


React のインストールから "Hello, world!" まで

では、ステップを追って React で "Hello, world!"を表示させるまでをやってみましょう。


1. 必要なパッケージのインストール

React の公式サイトを見ると npm でパッケージを揃える方法がオススメされているので、これをそのままやります。

必要なパッケージは、
react, react-dom, babelify, babel-preset-react, babel-preset-es2015, babel-preset-react
です。

$ npm install --save react react-dom babelify babel-preset-react
$ npm install --save babel-preset-es2015 babel-preset-react --no-bin-links


babel-preset-es2015 と babel-preset-react について公式サイトでは全く言及されていないんですが、babel6 からはパッケージが細かく分割されるようになったから別途インストールが必要とのことです。

詳しくは以下を参照してください。

http://qiita.com/kamijin_fanta/items/e8e5fc750b563152bbcf


2. gulpfile.js を記述

続いて gulpfile.js に Browserify を使いたい旨を記述します。


gulpfile.js

var elixir = require('laravel-elixir');

elixir(function(mix) {
  mix.browserify('app.js');
});

なんと、これだけです。
簡単すぎかよ...。


3. React のコードを用意

React 公式の "Hello, world!" をそのまま使います。

コンパイル前の js は resources/assets/js/ に置くのがデフォルトですね。ファイル名は app.js で。


resources/assets/js/app.js

var React = require('react');
var ReactDOM = require('react-dom');

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);


4. Browserify コンパイルを実行

$ gulp

これだけでコンパイルが走ります。

開発中にファイル保存のたびにコンパイルを走らせたい場合は、

$ gulp watch

として watch しましょう。


これにて、resources/assets/js/app.js を元にして public/js/app.js にコンパイル済みのファイルが入ります。


5. HTML を用意

実際に表示させる HTML を用意します。
これも、React 公式のサンプルをそのままです。

あくまでも React の導入記事なので、Laravel 本体の機能は使いません。プレーンな HTML です。 public/react-example/ ディレクトリに置きましょう。


public/react-example/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Hello React!</title>
</head>
<body>
  <div id="example"></div>
  <script src="/js/app.js"></script>
</body>
</html>


6. 表示させて確認

全ての準備が整ったので実際に表示させてみます。
Laravel Artisan コマンドに簡易ローカルサーバーの機能がコレを使います。

プロジェクトルートディレクトリで、

$ php artisan serve

でサーバーが立ち上がります。


ブラウザで http://localhost:8000/react-example/ にアクセスしてみてください。

f:id:todays_mitsui:20160121230406p:plain

"Hello, world!" の文字が表示されたでしょうか?
デベロッパーツールなどで要素を覗いてみると、新たに <h1> が生成されて挿入されているのが見えますね。


まとめ

今回は "Hello, world!" までですが、思った以上に簡単に導入できたので拍子抜けしました。

特に npm と Browserify と gulp.js の連携が設定済みなのがめちゃくちゃ便利ですね。
必要なパッケージは npm install して、require() して、Browserify するだけで使えるので必要パッケージの管理から完全に開放されます。

個人的には、フロントのパッケージ管理を bower ではなく npm でやれるのも好印象ですね。


フロントエンドフレームワークの導入までも激簡単にしてくれる Laravel と Laravel Elixir すごい、という紹介でした。


私からは以上です。

Scrapy の start_urls をファイルから読み込む for v1.0.4

表題の通り、 Scrapy の start_urls を外部ファイルから読み込んで設定する方法を書き留めます。
Scrapyのバージョンは 1.0.4 を想定しています。


導入

まず、scrapy コマンドで生成されるプレーンなスパイダーを見てみましょう。

$ scrapy startproject scrapy_sample
$ cd scrapy_sample
$ scrapy genspider example exapmle.com

こんな感じで scrapy コマンドを叩くと、spiders ディレクトリの中に example.py が生成されます。


scrapy_sample/spiders/example.py

# -*- coding: utf-8 -*-
import scrapy


class ExampleSpider(scrapy.Spider):
    name = "example"
    allowed_domains = ["example.com"]
    start_urls = (
        "http://www.example.com",
    )

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

実際に動かしてみるために、あらかじめ perse() に少し書き足しています。
よくある例ですが、URL と <title> を抜き出して、辞書にして返すようにしました。


今回注目したいのは start_urls の部分ですね。
現状だとスクレイピング対象の URL はハードコーディングされています。
対象 URL をソースコードに直書きしつつ管理したいと思う人はあまり居ないのでしょう。

そんなわけで、この start_urls に指定する URL を外部ファイルから読み込んであげることにします。


start_urls 設定用ファイルの仕様

仕様といっても簡単なものです、プロジェクトディレクトリの直下に start_urls.txt というテキストファイルを作って、そこから URL を読み込むことにします。
URL は改行で区切ります。つまり、1行に1URLを書いていくスタイルです。


start_urls.txt

http://www.cnn.co.jp/
https://www.yahoo.com/
http://www.reuters.com/

ひとまず、当たり障りのない URL を並べておきます。


コンストラクタを記述

スパイダーの初期設定はコンストラクタの中でするのがふさわしいでしょう。ExampleSpider クラスの __init__() を記述します。
親クラスである scrapy.Spider クラスの __init__() を呼び出すのも忘れずに、まずはお決まりのコードを書きます。


scrapy_sample/spiders/example.py

# -*- coding: utf-8 -*-
import scrapy


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = (
        "http://www.example.com",
    )

    def __init__(self, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)
        # Do something.

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

このように、

さて、Do something. の所にテキストファイル読み込みの処理を書いていきましょう。


scrapy_sample/spiders/example.py

# -*- coding: utf-8 -*-
import scrapy


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = []

    def __init__(self, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)

        f = open("start_urls.txt")
        urls = f.readlines()
        f.close()

        # 各要素の末尾についた改行文字 "\n" を削除
        # 空行("\n" だけの行)も削除
        self.start_urls = [url.rstrip("\n") for url in urls if url != "\n"]

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

1行1URLにしたので、readlines() を呼ぶだけで URL の配列を取得できます。
self.start_urls に URL の配列を上書きしてあげればOKです。

ただし、

  • readlines() で取得した配列は各要素の末尾に改行文字 "\n" を含んでいる
  • もしかしたら空行があるかも知れない

という2点を考慮して, リスト内包表記で 末尾の改行文字 と 空行 を取り除く処理をはさんでいます。


ファイル名を settings.py で設定

さて、無事に外部ファイルから start_urls を読み込むことに成功しましたが、今度は "start_urls.txt" というファイル名をハードコーディングすることになってしまいました。

せっかくなんで、読み込むファイル名は settings.py に記述したいですよね。


settings.py に独自の項目を追加して使う方法は以前の記事で書いた方法をそのまま使います。

まずは settings.py の末尾に項目を追加しましょう。

scrapy_sample/settings.py

# -*- coding: utf-8 -*-

# Scrapy settings for scrapy_sample project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'scrapy_sample'

# ... 中略 ...

START_URLS = "start_urls.txt"

settings.py に記述した設定値は crawler.settings というオブジェクトに格納されます。
この crawler.settingsExampleSpider クラスの中で読めるようにしましょう。


from_crawler() メソッドを使うとクローラーから値を引き次ぐことができます。


scrapy_sample/spiders/example.py

# -*- coding: utf-8 -*-
import scrapy


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = []

    def __init__(self, settings, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)

        # 読み込むファイルは settings.py の START_URLS で設定する
        f = open(settings.get("START_URLS"))
        urls = f.readlines()
        f.close()

        # 各要素の末尾についた改行文字 "\n" を削除
        # 空行("\n" だけの行)も削除
        self.start_urls = [url.rstrip("\n") for url in urls if url != "\n"]

    @classmethod
    def from_crawler(cls, crawler):
        # settings.py に記述された全ての設定項目を settings として格納
        return cls(settings = crawler.settings)

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

from_crawler() の中で cls()crawler.settings を渡してあげます。

それを受け取る __init__() の側では、第2引数に settings を追加しています。


これにて __init__() の中で settings にアクセスできるようになりました。
ファイル読み込み部分も f = open(settings.get("START_URLS")) と書き換えています。


実行

$ scrapy crawl example -o result.json

とすれば、結果が result.json に JSON 形式で保存されます。


result.json

[{"url": "http://www.cnn.co.jp/", "title": "CNN.co.jp"},
{"url": "https://www.yahoo.com/", "title": "Yahoo"},
{"url": "http://www.reuters.com/", "title": "Business & Financial News, Breaking US & International News | Reuters.com"}]

となっているので、上手く動いてくれているようです。


まとめ

Scrapy はシンプルで綺麗なアーキテクチャを持って設計されているフレームワークですが、公式ドキュメントを読むと「使用目的に合わせて好きなように組み替えて使ってくれぃ」というメッセージも感じます。

Scrapy の各クラスとメソッド群があれば、少ない記述で ある程度カスタマイズすることは可能です。
いい感じですね。


私からは以上です。