技術的な話題   

いい感じに Javascript がゴリゴリ書かれていて、認証が必要なサイトを定期的にキャプチャを取る、という謎タスクが生まれ、ちょうど最近話題の Headless Chrome でも試してみるかな~~と思ったら puppeteer なるものがあるそうなので、これを使ってみる。

GoogleChrome/puppeteer: Headless Chrome Node API

GoogleChrome 配下にいるので Chrome 公式のパッケージだ。

// puppeteer 導入
$ npm init
$ npm install puppeteer

ドキュメントにもあるサンプルソースを入れてみる。

// index.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

https://example.com へとアクセスし、そのページをキャプチャして ./example.png に保存するソースだ。 await で簡単わかりやすい書き方になるのが素晴らしい。これを動かしてみる。

$ node index.js
(node:30285) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to launch chrome!
/var/www/html/crawl/bitbucket_billing/node_modules/puppeteer/.local-chromium/linux-497674/chrome-linux/chrome: error while loading shared libraries: libXss.so.1: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

(node:30285) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

かなしみ…。どうにも libXss.so.1 が開けずにエラーっぽい。パッケージを探してインストールし、再度動かしてみる。

$ yum provides */libXss.so.1
...

libXScrnSaver-1.2.2-6.1.el7.i686 : X.Org X11 libXss runtime library
Repo        : base
Matched from:
Filename    : /usr/lib/libXss.so.1

libXScrnSaver-1.2.2-6.1.el7.x86_64 : X.Org X11 libXss runtime library
Repo        : base
Matched from:
Filename    : /usr/lib64/libXss.so.1


$ sudo yum install libXScrnSaver
...

$ node index.js
(node:1345) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to launch chrome!
/var/www/html/crawl/bitbucket_billing/node_modules/puppeteer/.local-chromium/linux-497674/chrome-linux/chrome: error while loading shared libraries: libgtk-3.so.0: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

(node:1345) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

GTK+3 が要求される。都合によって CentOS6 なのだが GTK+3 は入れるのがめっちゃ困難だった記憶。とりあえず GTK+3 というか Chrome を入れる方法を調べたら必要なライブラリも揃うだろう、と判断して調べる。

Google Chrome 60 Released - Install on RHEL/CentOS 7/6 and Fedora 26-20

Yes, they’ve discontinued support for RHEL 6.X version as of Google Chrome and on other side, latest Firefox and Opera browsers run successfully on the same platforms.

CentOS6 では Chrome は動かない、ダメ。Richard Lloyd 氏がスクリプトを作っているそうだが、そのサイトでも、いつまでも古いものを使うな、って書いてあった。

Site has been shut down

You have two choices really (ignoring Chromium, which no-one seems to be keeping up-to-date for RHEL/CentOS 6):

1. Use Mozilla Firefox ESR via "yum install firefox" that is shipped with RHEL/CentOS 6. Unfortunately, this can be up to a year out of date w.r.t. features compared to the more "normal" Mozilla Firefox that you can download from Mozilla (but that "normal" Mozilla Firefox is now also using GTK+3 and doesn't work on RHEL/CentOS 6 either).

2. Move to RHEL/CentOS 7, perhaps first in a VM before upgrading on bare metal once the VM is to your satisfaction. The latest Google Chrome installs and runs out-of-the-box on RHEL/CentOS 7 (see below).

まあ、一旦使ってみるなら docker でもいいか。

$ docker run -it -d -v $(pwd):/var/www/puppeteer_test centos:latest
Unable to find image 'centos:latest' locally
latest: Pulling from centos
6c5159923047: Pull complete
acec82331181: Pull complete
1c1b67b33c28: Pull complete
Digest: sha256:57c91a43765de2147fa666a128477bc6101d6fda660feaaa5fbb1a8b110c934f
Status: Downloaded newer image for centos:latest
2be8ec65e6e4db3d852d563b82cc4c90ca90f62b4aaa6304d6e00f06d6a4a9e7

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
2be8ec65e6e4        centos:latest       "/bin/bash"         16 seconds ago      Up 16 seconds                           romantic_sinoussi

$ docker exec -it 2be /bin/bash

# cd /var/www/puppeteer_test/
# ls
index.js  node_modules  package-lock.json  package.json

# node -v
bash: node: command not found

// nodeがいないので入れる
# curl -sL https://rpm.nodesource.com/setup_8.x | bash -
# yum install nodejs

# node index.js
(node:114) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to launch chrome!
/var/www/puppeteer_test/node_modules/puppeteer/.local-chromium/linux-497674/chrome-linux/chrome: error while loading shared libraries: libpangocairo-1.0.so.0: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

(node:114) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

// とりあえず yum provides で探しながら一個ずつ入れていく
// をまとめたものがこちら
# yum groupinstall "Development Tools"
# yum install pango libXScrnSaver libXcomposite libXcursor libXi libXtst cups-libs libXrandr GConf GConf2 alsa-lib atk gtk3

// 起動してみる
# node index.js
(node:11985) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to launch chrome!
[0920/101508.987383:ERROR:zygote_host_impl_linux.cc(88)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

(node:11985) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

root で --no-sandbox なしは認められないらしいのでソースを変える。

// index.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({args: ['--no-sandbox']});
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

再び実行。

# node index.js

# ls
example.png  index.js  node_modules  package-lock.json  package.json

キャプチャ取れたので、画像を確認。

勝ち。

というわけで puppeteer 動いた。CentOS6 で CentOS7 が動いてて~ってちょっとアレなので、バージョン上げてまた試……、じゃない、本来の目的だった認証通してキャプチャできるかをやらなくちゃ。続く。

   技術的な話題   

Rx 云々を見かけるたびに Web でそんな出番ないような…?非同期やすとりーむや配列的なのをよろしくするやつ?と、もはや知ることを放棄していて。今頃もったいないなーと思ったので、改めてちょっと調べるの巻。

Rx について

ReactiveX - Intro

オブザーバーパターンを拡張したもので、シーケンスなデータやイベントをサポートするため、スレッドやノンブロックIOとかを抽象化して組み立てることができる。オブザーバーパターン = 監視する人と通知する人がいるよー、というイメージでとりあえずはよい。

Promise について

JavaScript Promiseの本

非同期処理を上手くパターン化して書くための、統一的なインターフェース。ここではオブザーバーという話はない。 コードを見ても、非同期を取りまわすためのいい感じのインターフェースである、というイメージ(個人の感想です)

Rx とPromise はどう使い分けるとよさそう?

いくつかサイトを眺めてみて。

JavaScriptの非同期処理には何を使うべきか - Qiita
State, Promise & Reactive プログラミング
What are the differences between observables and promises in JavaScript? - Stack Overflow

で、 Stackoverflow の回答が結構それっぽいのかなあと。

Simply put: A promise resolves to a single value asynchronously, an observable resolves to (or emits) multiple values asynchronously (over time).

ざっくり訳すと。

Observable (= Rx として) は多数の非同期を継続的に処理したい
Promise は単発の非同期処理をしたい

なので。

Rx は配列操作とか、画面操作とか、とかと相性がよい
Promise は単発になりがちのもの、外部呼出し(Ajaxとか)と相性がよい

このボタンおしたら Ajax して、待機状態にして、結果が来たらここを書き換えて、イベント設定して、… なんてものは Rx と Promise を組み合わせるといいのかもしれない。が、昨今のフロントエンドにおいては Flux なんかでライブラリの中でよろしくやっていることが多いので、あえて開発する人が関与していくことは少ない…?

どちらかというとアプリ系で Rx が盛り上がってる気がしていて、 Web フロントではそうでもないような…。どっちも知識薄くてわからんのだけど、言語が違っても画面とロジック、裏でデータがほいほい、みたいなことってどちらも一緒のことだと思うんだけど、その差はなんなんなん?

   技術的な話題   

開発用だったり検証用だったりで docker をぼちぼち使っているのですが、ゲストのポートをホストのポートへバインドするときに、IPアドレスを指定できます。

$ docker run -it -d -p 127.0.0.1:10080:80 myapp

どこで使うんだよって話ですが、セキュリティを考えるときには使うんじゃないっすかね、たぶん。

IPアドレスの指定がないと 0.0.0.0 で待ち受けるので、接続をどこからでも受け付けます。そのため、例えばホスト側で nginx 等を使って何かしらの認証が必要なリバースプロキシをしていたとしても、指定ポートでコンテナに直接アクセスができてしまいます。

nginx で不特定多数から見られないよう Basic 認証を設定していたのですが、見れるのかな?ってポート指定してアクセスしたら無事に見れてしまって、うふふってなってました。

$ docker run -it -d -p 10080:80 nginx
485e21a84ea180bc69db198bcdb7e417fb61d2747642059b8bd4435873aa1503

$ netstat -nao | grep 10080
tcp        0      0 :::10080                    :::*                        LISTEN      off (0.00/0/0)

$ docker rm -f 485
485

$ docker run -it -d -p 127.0.0.1:10080:80 nginx
94ea5a9d74578fc69777cf0dc3b7dc918ce2e4ec74fef52441521a8d163680d8

$ netstat -nao | grep 10080
tcp        0      0 127.0.0.1:10080             0.0.0.0:*                   LISTEN      off (0.00/0/0)

ドキュメントはここ
https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p-expose

   生きる   

プロポーズはおととしの 12 月にしていて、入籍は昨年 4 月にしていて、お金や時間の都合で時間差、今年の 5 月くらいに結婚式を挙げてました。

あ、結婚式の日に結婚式なう~~~みたいなツイートしそこねた…。

当然ながら人生初の結婚式だったわけですが、いわゆるホテルウェディングとか、神社でーとか、そういう形にはしませんでした。

株式会社 CRAZY がやっている、コンセプトや内容、演出などなど、1 からウェディングのプランニングしていって、最高の1日をみんなで作っていこう、というものを利用しました。クレイジーって最近テレビ出たりメディア出たりで結構盛り上がっているそうですよ☆

株式会社 CRAZY (株式会社クレイジー) | CRAZY,Inc.

コンセプトウェディングのプロデュースチーム | CRAZY WEDDING (クレイジーウェディング)

そんなクレイジーウェディングで迎えた、ぼくらの最高の1日の様子をちょっとだけご紹介。

余談ですけど Theta S + 自撮り棒 をもっていったので、ハットなのも相まってステッキ!?って驚かれました。集合写真とか、歩き回るときに動画とったりして面白い画になりましたとさ。おすすめ。

クレイジーウェディングでよかったこと

・これやるならウン万ですよみたいな営業がほぼされなかった
・現実的に出来ることは何でもできる(お金しだい)
・コンセプトをちゃんとすり合わせて決めるので本人たちの納得感、やった感が非常に強い
・家族メッセージしっかり伝えられた

自分たちがどういうことをしたいのかを考えたり、自分たちがどういう人生を歩んできたのかを話したり、そもそもなんで結婚式するんだっけを考えたり…。そうこうして、プロデューサーの方からワクワクするようなコンセプトが提案されました。
一部抜粋ではありますが、ぼくらの場合はこんなコンセプトです。


オモシロ屋
いつでもどこでも オモシロイこと 創ります 材料はこの世界にあるものすべて

新しいものを ゼロから創り出すことを 楽しんで生きてきた 僕と
誰よりユニークを愛し 好奇心を元に 手を抜かず生きてきた 私

ふたりでいたら最強です

こうしたい!という想像力と 遊びゴコロがあれば どんなものだって創り出せる
飽くなき探求心と 人を笑顔にしたい想い それがオモシロ屋の原動力


最初にこのコンセプトを見聞きしたときにちょっと感動しちゃって涙が出かかるほどに。で、このコンセプトを、結婚式当日直前の会場装飾と初対面ってときにも読まれて、会場ながめてこれまだ涙が。エモい言葉に弱いんですかね、ぼく。

コンセプトイメージがレトロなモダンな和風チックな感じで、それに合わせて会場の装飾だったり、席札だったり、ぼくらの衣装も見ていました。だから、その日、その会場はすべてが「オモシロ屋」です。っていうくらい作りこむ文字通りのクレイジーな集団で、こっちはめっちゃ驚くし感動するしでした。

クレイジーウェディングちょっと微妙だったところ

・プロデューサーさんめちゃんこいそがしそう

ほぼすべて良くて、はーーーーやってよかったなーーーー、みたいな空気感に包まれていたのですが、プロデューサーさんが掛け持ちしたりで、結果忙しそう(に見える)ので、連絡するのに気が引けてしまったり、とかとか。
話を聞くところによると、それで提供したいことや考えたいことがうまくできていなかったところもあったそうなので、体制ややり方を変えていっているそうですよ!これからやるひとはきっと大丈夫だと思います!

個人的な反省

・炎上プロジェクトだった(直前でもうれつにがんばらないといけなかった)
・人呼びたりなかった。。 Party !!にしたかったけど会場キャパとかコンテンツとか。。

二人で作っていくので、細かいスケジュールややること整理は自分たちで管理していかないといけないです。もちろんプロデューサーさんがお手伝いはしてくれますが、すでに書いてある"忙しそう"が相まって、煽りがされないこともしばしばありました。
一応 Trello を使ってやることと日程の管理はしていたのですが、直前 1-2 週間くらいは動画つくったりなんだりーってやっていました。
あ!ぼくらの場合は、ある程度自分たちでも作りたいっていう意思があったので、動画とか。プロにお任せするのがお手軽らくちんではあるのですが、自分たちでやったほうがやりたいようにできるんじゃないかなーと。

ちなみに動画作成は Vegas Pro 14 です。2,3 年くらいまえに 5000 円?だったかなあ、のセールをしてたところで買って、それからずっと使ってます。動画作成する仕事ではないのですが、ちょこちょこと動画つくってよ~~なんてお願いがあったりするので、そういうところで。

個人的にはめっちゃ人呼んでパーリナイにして高まる曲をもりもりと流したかったのですが、予算都合だったり、妻との調整の結果、ってところでですね。それでももう少し呼びたい人たちもいましたし、呼べなかったのマジゴメンの気持ち。会場キャパが 80 くらいだったんだよ~~~


というところで。

結婚式も披露宴も、こちらは非常に楽しくやっていましたし、ゲストの盛り上がりも良くて、後で話を聞いてもよかったねー、だったのも嬉しいポイントでした。自由度高くお金と頑張り次第でなんでもいけるのでクレイジーウェディングおすすめです!

あと干し芋を置いておきますね、お祝いの気持ちが湧いてきたらお願いします。
http://amzn.asia/j4BudTt

   技術的な話題   

NGINX Unit なるものがリリースされたらしい。

・NGINX 公式サイトの情報
NGINX Unit

・NGINX Unit 公式サイト
NGINX Unit

・見かけた記事
NGINXからアプリケーションサーバ「NGINX Unit」がオープンソースで登場。PHP、Go、Pythonに対応。Java、Node.jsにも対応予定 - Publickey

ドキュメントに書かれているインストール方法や設定を見てたらパッと出来そうだったので思いたってやったったの巻。

Read more »

   技術的な話題   

最近は Python をちょこまかといじることが増えてきたように感じています。というところで、クラスタリングのことを調べていたのですが、なんとなく k-means しておけばいいっしょ、くらいだったのですが、もっと奥深い色々が出てきて、クラスタリングわからないマンになりつつあります。そんなわけで、階層型クラスタリングを調べて、やってみたのでまとめておきます。

Read more »

   技術的な話題   

タイトルそのまま。

// たぶん mysql 5.7 がプルされる
// が、わからんので DockerHub を要確認
$ docker pull mysql:latest

// mysqlコンテナの起動
//   オプションについて
//     -e    : 環境変数
//     --name: コンテナ名
//     -d    : バックグラウンド
//     -p    : ポートバインディング(ホスト:ゲスト)
//   環境変数について
//     MYSQL_DATABASE     : 作るデータべース
//     MYSQL_USER         : 作るユーザ
//     MYSQL_PASSWORD     : 作るユーザのパスワード
//     MYSQL_ROOT_PASSWORD: ルートパスワード
$ docker run --name my_mysql \
  -e MYSQL_DATABASE=homestead \
  -e MYSQL_USER=homestead \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_ROOT_PASSWORD=secret \
  -d \
  -p 13306:3306 \
  mysql

// 接続確認
//   -h : ホスト名
//   -P : ポート番号
//   -u : ユーザ名
//   -p : パスワードを入力するプロンプトを出す
$ mysql -h 127.0.0.1 -P 13306 -u root -p

記事書くときに Docker Hub を確認したら 5.7.19 が latest で指定されているっぽい。というか 8.0 系も指定できるのか、検証なんかにも便利そうだ。

library/mysql - Docker Hub

でもって docker で MySQL が立ち上がると、こうさ、ほら、ユニットテスト回すときなんかに、こうしてクリーンな DB を提供できてよさそうだなーって。
イメージとしてはこんな感じ。

{
    ...
    "scripts": {
        "test": "phpunit --verbose",
        "docker-test": [
            "composer docker-db-remove"
            "composer docker-db-create",
            "composer test",
            "composer docker-db-remove"
        ],
        "docker-db-create": "docker run --name test_mysql -e MYSQL_DATABASE=homestead -e MYSQL_USER=homestead -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=secret -d -p 13306:3306 mysql",
        "docker-db-remove": "docker rm --force test_mysql",
        "docker-db-connect": [
          "#  Copy this command!",
          "#  mysql --host=127.0.0.1 --port=13306 --user=homestead --password=secret homestead"
        ]
    },
    ...
}

composer docker-test とやったらコンテナが立ち上がり 127.0.0.1:13306 で待ち受けているので、そこを指定するように phpunit.xml だとか .env だとかで DB 接続用の値を設定すればよい。テストの後はコンテナがポイされるので、テスト用のDBはいつでもクリーンに。テストでコケても大丈夫なように、最初にコンテナをポイしたほうがいいのかな。

composer スクリプトで I/O を全部引き渡す方法がわからなかったので docker-db-connect タスクが非常にあやしい感じだけど、これくらいでもまあまあ運用していけるんじゃないかなーー

   技術的な話題   

artisan コマンドでエラーが出た場合、そのまま実行していると例外クラスとメッセージが出る程度で、どこでエラーになっとんねん!がわかりません。

実は自分で作った artisan コマンド含めてすべての artisan コマンドは verbose なオプションをサポートしています。そのためオプションを指定するだけで、例外を詳しく見ることができます。

$ php artisan migrate

  [Illuminate\Database\QueryException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists (SQL: alter table `my_tables` drop `created_at`, drop `updated_at`)

  [Doctrine\DBAL\Driver\PDOException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists
  
  [PDOException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists 


$ php artisan migrate -v
                                                                                       
  [Illuminate\Database\QueryException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists (SQL: alter table `my_tables` drop `created_at`, drop `updated_at`)

Exception trace:
 () at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:647
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35


  [Doctrine\DBAL\Driver\PDOException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists

Exception trace:
 () at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:107
 Doctrine\DBAL\Driver\PDOStatement->execute() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:449
 Illuminate\Database\Connection->Illuminate\Database\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:640
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35


  [PDOException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists

Exception trace:
 () at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:105
 PDOStatement->execute() at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:105
 Doctrine\DBAL\Driver\PDOStatement->execute() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:449
 Illuminate\Database\Connection->Illuminate\Database\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:640
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35

ちなみにサポートするオプションと、表示レベルは以下のようになっています。


// /vendor/laravel/framework/src/Illuminate/Console/Command.php
// ※抜粋

use Symfony\Component\Console\Output\OutputInterface;

protected $verbosity = OutputInterface::VERBOSITY_NORMAL;

protected $verbosityMap = [
    'v' => OutputInterface::VERBOSITY_VERBOSE,
    'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
    'vvv' => OutputInterface::VERBOSITY_DEBUG,
    'quiet' => OutputInterface::VERBOSITY_QUIET,
    'normal' => OutputInterface::VERBOSITY_NORMAL,
];
オプション レベル 生数値
v OutputInterface::VERBOSITY_VERBOSE 64
vv OutputInterface::VERBOSITY_VERY_VERBOSE 128
vvv OutputInterface::VERBOSITY_DEBUG 256
quiet OutputInterface::VERBOSITY_QUIET 16
normal OutputInterface::VERBOSITY_NORMAL 32

まあ、これってソースのとこにもあるんですけど、ベースになってる Symfony2 のものなんですけどね。

Verbosity Levels (current)

Symfony2 で定義されている値を使って artisan コマンドも同じように verbose な指定をサポートしているんですねー。もし自分で作るコマンドも verbose な指定をサポートするなら、指定されている定数を目安に出力すると良さそう。

   技術的な話題   

Google Analytics の API を使ってアシストコンバージョンをとりたいが、通常のセッション数などを取得できる Core Reporting API にはアシストコンバージョンという指標は無く取得することができない。 Multi-Channel Funnels Reporting API を使うことでその指標は取得できるようになる。がトラップがあり、やりたいことはできなかったので、タイトルが過去形。

なお MCF API のドキュメントにあるサンプルでは Java で書かれているが、言語別のライブラリが持っているインターフェースはおよそ同じなので、読み換えればよい。

Multi-Channel Funnels Reporting API - デベロッパー ガイド  |  アナリティクス Multi-Channel Funnels Reporting API  |  Google Developers

PHP ならこんな具合にして MCF をコールできる。

// セッション準備
session_start();

// API利用のための認証
//==================================================
$client = new \Google_Client();

// クライアントキー、シークレットの設定
// Developer Console で登録すると発行される
$client->setClientId('...');
$client->setClientSecret('...');

// オフラインアクセスを付与するとリフレッシュトークンを使って無期限にアクセスできる
$client->setAccessType('offline');

// 認証画面を都度表示する
$client->setApprovalPrompt('force');

// リダイレクトURLの設定
// Developer Console で設定した値と合わせないとエラーになる
$client->setRedirectUri('.....');

// アクセス権限の設定
$client->addScope([\Google_Service_Analytics::ANALYTICS_READONLY]);


// 認証が通ってくるとcodeがついてくるのでコレを利用して認証を通す
if (!empty($_GET['code'])) {
    unset($_SESSION['refresh_token']);
    $client->fetchAccessTokenWithAuthCode($_GET['code']);
    $_SESSION['refresh_token'] = $client->getRefreshToken();

} else {

    // リフレッシュトークンがなければ認証URLに飛ばす
    // とりあえず試すならセッションから読み込むと良い。
    if (empty($_SESSION['refresh_token'])) {
        header('Location: ' . $client->createAuthUrl());
        exit;
    }

    // リフレッシュトークンで認証する
    $client->refreshToken($_SESSION['refresh_token']);
}


// Google Analytics とのやり取りするオブジェクト準備
//==================================================
$analytics = new \Google_Service_Analytics($client);


// MCF API を実行する
//==================================================
$gaRequestResult = $analytics->data_mcf->get(
    // プロファイルID
    'ga:......',

    // 開始日
    '30daysAgo',
    // 終了日
    'yesterday',

    // 取得するメトリクス
    'mcf:assistedConversions',

    // 追加のオプション
    [
        'dimensions' => 'mcf:source',
        'filters' => 'mcf:source=@.',
    ]
);


// 結果確認
//==================================================
var_dump($gaRequestResult);

この例だと、流入元別のアシストコンバージョンを30日前から昨日まで範囲で取得する。また流入元の値に "." が含まれていることを条件とする。 流入元とは言うが (direct) のような値も入ってきて厄介。そこで「"." が含まれていること」にすればドメインが付いているものだけ抜けるという仕組み。

Google Developer Console で、このあたりから API 登録をすると動かせるようになる。

https://console.cloud.google.com/apis/dashboard

しかし、実はこの MCF API にはトラップがある。それはディメンションの設定にある。

Google Analytics の画面上からはデバイス別のアシストコンバージョンを確認することができるが API からはこれを確認するすべはない。ドキュメントを見ても MCF API のディメンションにはデバイスの話はない。

ディメンションと指標のリファレンス  |  アナリティクス Multi-Channel Funnels Reporting API  |  Google Developers

また MCF API を呼ぶときにディメンションに Core Reporting API の値を設定してもエラーになってしまう。逆に Core Reporting API を呼び出すときに MCF API のもの(アシストコンバージョンなど)を設定してもエラーになる。

ちなみに Core Reporting API で使える指標はここ。

Dimensions & Metrics Explorer  |  アナリティクス Reporting API v4  |  Google Developers

まあ。アシストコンバージョンそんな重要じゃねーからよくね、って話なのかもしれない。

   技術的な話題   

以前からごみばこいんでは、コンソールにようこそメッセージを出していました。

Chrome Devtools のドキュメントを見ると console.log の詳細が書かれています。

診断とコンソールへのログ出力  |  Web  |  Google Developers

ドキュメントには CSS プロパティが利用できるとあるのですが、なぜかこれまでは色とサイズが変えられる程度だと思っていました。試すと text-shadow が出来たり border が出来たりと、様々なプロパティが効くようです。

var properties1 = [
    'font-size: 8em;',
    'color: white;',
    'padding: 5px;',
    'background: linear-gradient(red, orange, yellow, green, aqua, blue, violet);',
    'text-shadow: white 0 0 15px;'
];

var properties2 = [
    'font-size: 8em;',
    'color: #222;',
    'text-shadow: white 0 0 3px;',
    'margin-left: calc(-15em - 6px);',
];
console.log(
    '%cうぇるかむとぅーごみばこいん!%cうぇるかむとぅーごみばこいん!',
    properties1.join(''),
    properties2.join('')
);

いくつかプロパティを試してみたのですが display だったり float だったり position 、 top や left あたりは無効化されているようです。。

で。実は Inspector を使うと、ここも HTML/JS/CSS で構成されていることがわかります。
Inspector は URL バーに chrome://inspect/ と入力するとアクセス出来ます。その中でも Other の項目を見ると Developer Tools について Inspect することができます。

DevTools の DevTools ができるなんてちょっと不思議な光景ですねw

こうやって console.log した結果を眺めてみると %c で指定したものは span タグの style 属性に入っています。
これでは :beforeなどのセレクタが使えなかったり、階層構造が持てないのでアニメーションしたりなどなども出来ないです。先にあった使えないプロパティのところもあるので、凝ったことをするのはできなさそうですねー。残念。

   技術的な話題   

MySQL 使ってて utf8_unicode_ci や utf8_general_ci とか mb4 …? とプチ事故が起きたので解消するべく調べた。

照合順序とは

「utf8_general_ci」といった文字コードの指定してるっぽいやつは、文字列カラムにおけるコレーション(=照合順序)と呼ばれる。ここで指定された値によって、MySQL が文字列を解釈したり比較を行うようになっている。

表記は 文字コード_文字セット名_比較方法(※特殊なものとして *_bin もある)

  • 文字コード
    • utf8mb4 とか utf8 とか cp932 とか
  • 言文字セット名
    • japanese とか german とか
    • Unicodeに限り、japanese はないので general か unicode か bin を選ぶ。
      • (言語によって文字の特殊性があるらしく、その言語別の定義があるが日本語はない)
  • 比較方法
    • 3種類あある
    • ci
      • 大文字小文字を比較しない
    • cs
      • 大文字小文字を比較する
    • bin
      • バイナリとして判断する
    • よく使う utf8... では ci と bin のみになる…?(何でー?)

どんな照合順序が使えるかを確認する

show colattion というクエリを実行することで確認できる。

show collation;
Collation Charset Id Default Compiled Sortlen
big5_chinese_ci big5 1 Yes Yes 1
big5_bin big5 84 Yes 1
dec8_swedish_ci 8-Dec 3 Yes Yes 1
dec8_bin 8-Dec 69 Yes 1
cp850_general_ci cp850 4 Yes Yes 1
cp850_bin cp850 80 Yes 1
hp8_english_ci hp8 6 Yes Yes 1
hp8_bin hp8 72 Yes 1
koi8r_general_ci koi8r 7 Yes Yes 1
koi8r_bin koi8r 74 Yes 1
latin1_german1_ci latin1 5 Yes 1
latin1_swedish_ci latin1 8 Yes Yes 1
latin1_danish_ci latin1 15 Yes 1
...

※ like など SQL を書くときに使うような演算子を利用して絞り込みができたり、 ORDER BY 並び順を設定できる。

show collation where Collation like 'utf8%';
Collation Charset Id Default Compiled Sortlen
utf8_general_ci utf8 33 Yes Yes 1
utf8_bin utf8 83 Yes 1
utf8_unicode_ci utf8 192 Yes 8
utf8_icelandic_ci utf8 193 Yes 8

MySQL サーバーのバージョンによって使える照合順序が違うので、環境によって結果が変わることがある。(古いと utf8mb4 がないとかそういう) 5.6, 5.7 あたりならこの記事の話は問題ないはず。詳しい一覧はここで確認ができる。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.14 MySQL でサポートされる文字セットと照合順序

*_unicode_ci と *_general_ci の違い

照合順序の違いによってどんなことが起こるか、実際にクエリを発行しつつ確認する。

# バージョン確認
select version();
# 5.7.17


# アルファベットの大文字小文字、半角全角
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 1, 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 0, 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 0, 0, 0
 
 
# ひらがな, 半角カタカナ + 半濁音
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'は' = 'パ';
# 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'は' = 'パ';
# 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'は' = 'パ';
# 0

この結果をまとめると。

半角全角の比較 大文字小文字の比較 ひらがなカタカナの比較
unicode_ci できない できない できない
general_ci できる できない できる
bin できる できる できる

というわけで general_ci か bin を使うようにするとこのあたりの比較がちゃんと出来てよさそう。

utf8 と utf8mb4 の違い

MySQL の UTF-8 は 3 バイトの幅しかなく、絵文字などを表現するためには 4 バイトが必要。きっと歴史的な背景から utf8 と utf8mb4 が分裂している。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.10.7 utf8mb4 文字セット (4 バイトの UTF-8 Unicode エンコーディング)

先程と同じように文字コードを指定して比較していく。

SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_bin';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 0

SET NAMES 'utf8' COLLATE 'utf8_unicode_520_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_520_ci';
select 'あ' = '🍣', '🍣' = '🍺';
# 0, 0

むむ、結構厄介。絵文字が入力される可能性があり、ちゃんと識別したい場合は utfmb4_bin を使うのがよさそう。

参考リンク

utf8_unicode_ci に対する日本の開発者の見解 - かみぽわーる
MySQL と寿司ビール問題 - かみぽわーる

   生きる   

某所に溜めとったネタを整形して加筆して投稿しとるんや、せやからナウで取り組んでいることとちょーっと違うことが多かったり、日によってまったく違うコンテンツになるんや、ウケるやろ、ガハハw
似非関西弁だ!!!捕らえろ!!!!

書き溜めしておいたネタは雑に書かれているものも多く、あとはそっち見ろ、みたいな。さすがにこれではブログにするのもあれなので、会社のお昼休みや帰る直前、そして通勤時間を使って、文章を整えたりコードを追記したりしている。

 
いままで都心部に住んでいたので電車で 15 分程度ゆられていたのだが、横浜の方に引っ越してからというものジャスト 1 時間くらいゆられるようになった。さすがに電車の中が暇になってしまう。そこでせっかくだからと思って、書き溜めしておいたものから順番に整形してブログに出せるようにしている。

コード書いて試したりーなどは PC からじゃないとちょっとつらいところもあるので、あとで書くように空白にしておき、周りの文章を整えキレイにしたり、言葉足らずなところを修正していっている。

そうこうして整った文書は Trello でカードに登録している。これを PC で開いてコードを追記したり画像添付を作ったりしている。出来上がったものはブログにアップし Trello のカードはアーカイブ。

Trello

平日はそんな様子で、土日もちょっと時間があくねー、みたいなタイミングで Trello を開いてちまちまブログ記事を作っている。

こんな流れで、ここ 1 ヶ月くらいはちまちまブログを書いている。夏休みが間にあってそこはまったくやらなかったから間が空いているけど 10 記事を超える投稿をした。今までの自分からしたらあり得ない量だ。

 
ブログを更新していると、デザインだったりなにやらが見にくいなあーなどとと思う箇所もあって、ちょっとずつブログの見栄えや機能も調整している。

こちらも同様に、次にこういう機能やろうかなーとか、こういう調整しようかなー、みたいなものは Trello でカード化している。行き帰りの電車でどうやるか調べたり段取りを考えたりし、あとで PC からサーバに入って実際に操作していく。こんな様子でやっている。

 
なんでこんなこと突然はじめたかというと、知り合いから「 1 日 1 つ強くなる」という話を聞いてから。これはあの有名なプロゲーマー・ウメハラの著書のタイトルにもなっている。

1日ひとつだけ、強くなる。 世界一プロ・ゲーマーの勝ち続ける64の流儀 | 梅原 大吾 |本 | 通販 | Amazon

実際にぼくが本を読んだわけではないが、これを読んだ知り合いはそれに影響を受けて、毎日知らなかった単語を 1 つだけ調べてメモしているそう。 たとえ 1 日で 1 単語調べたとしても 1 ヶ月も続ければ 30 単語 くらいは抑えられる。半年もやれば 180 単語。分野を絞ってやれば、だいぶ詳しくなる量だろう。

これのポイントはその人曰く「やりすぎないこと」だって。 1 日でやりすぎちゃうと燃え尽きて、次の日のやる気がなくなってしまうから、って本にも書いてあるらしい。

これまでは業務だったりプライベートだったりで何かと調べたりするわけだけど、だいたい頭の中に入れるて、 Twitter にシェアして、「ほー」とか言って、目の前の問題を解決したらおしまい。
それがすごいもったいないなーって思うようになって。問題を解決したのだから、後でなのか今なのかわからないけど同じ問題に当たる人を助けることができるかもしれない。せっかく勉強にしたことなのだから世の中に出していけばいい。もはや情報過多の世界なのだから、こんなちっぽけな記事が大多数の人の目に触れるなんてこともそうそうないだろう、多少汚くてもいいと思うよ。何を躊躇しているんだか。「 1 日 1 つ強くなる」でブログをかくなら「 1 日のうち多少なりともブログ記事に手をいれる 」という目標でいいじゃんか。別にその教えてくれた人に負けたくないとか張り合うとかそういう話ではなくて、「 1 日 1 つ強くなる」ためにブログを書いていこう、って思いましたとさ。

 
よく考えれば "Write Code Every Day." と同じだよね。 1 日で何かしらコードを書く、 Github へ上げて草を生やす。

毎日コードを書けば新しい発見があるだろうし、新しいことを学べるだろうし、エンジニア…あるいはプログラマー的「 1 日 1 つ強くなる」はまさにそれなんじゃないかな。

 
と。実はもう 1 つ理由があって。ある種のラインでもある 3 年が経過したわけで、そろそろ自分のことを一回考え直してみるかなーと。人生のステージも変わったわけだし。なので棚卸しじゃないけど、自分のもっていたものをいろいろと出してますよー、というかんじですね。

 
そんなこんななごみばこさんの最近のブログ事情でした。

   技術的な話題   

artisan コマンドを作ったとき、何かと様子を出力するために Log ファサードを使ってログファイルに出している。

 

ちなみに。そのとき Monolog のハンドラを使って、レベル別にファイルを分けたりなどしている。こんな具合にするとログレベルごとにファイルが分かれる、めっちゃ便利。

// bootstrap/app.php
$app->configureMonologUsing(function ($monolog) {
    $logConfigure = config('log');
    foreach ($logConfigure['file'] as $logLevel => $logFile) {
        $path = storage_path('logs/' . $logFile . '.log');
        $handler = new \Monolog\Handler\RotatingFileHandler(
            $path,
            $logConfigure['rotate'],
            $logLevel,
            false,
            $logConfigure['permission']
        );
        $handler->setFormatter(new Monolog\Formatter\JsonFormatter());
        $monolog->pushHandler($handler);
    }
});


// config/log
return [
    'permission' => 0777,
    'rotate' => 30,
    'file' => [
        \Monolog\Logger::DEBUG     => 'debug',
        \Monolog\Logger::INFO      => 'debug',
        \Monolog\Logger::NOTICE    => 'debug',
        \Monolog\Logger::WARNING   => 'warning',
        \Monolog\Logger::ERROR     => 'error',
        \Monolog\Logger::CRITICAL  => 'error',
        \Monolog\Logger::ALERT     => 'error',
        \Monolog\Logger::EMERGENCY => 'error',
    ],
];

自分で用意する artisan コマンド、ようはバッチでは様子の出力などをファイルに出すために Log::info(); とかして、いろいろな情報をログに出ていく。この ID のデータをやるよー、とか、おわったよー、とか、色々。

しかし開発中はそれって厄介で、パッとコマンド実行してログ見て。いや、横で tail -f とかしておけば良いのだろうけど、それはそれでちょっと面倒だなあ。というか artisan コマンドって verbose なオプションをサポートしているので、それをうまく使ってログに出すと同時に標準出力にも出せないだろうか。

と思って出来たのがコレ。

namespace App\Console;

use Illuminate\Console\Command;
use Monolog\Handler\StreamHandler;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

trait LogWithStdout
{
    /**
     * execute メソッドに割り込み Monolog の設定をする
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        // verboseなオプションが付いたらログを画面に出す
        if ($output->getVerbosity() >= $this->verbosityMap['v']) {
            $monolog = \Log::getMonolog();
            $handler = new StreamHandler("php://stdout", 'info');
            $monolog->pushHandler($handler);
        }

        // 元の処理の呼び出し
        return parent::execute($input, $output);
    }
}
namespace App\Console\Commands;

use App\Console\LogWithStdout;
use Illuminate\Console\Command;

class FetchTrafficSummary extends Command
{
    use LogWithStdout;

    public function handle()
    {
        Log::info('start!');

        ...

        Log::info('complete!');
    }
}

本当はサービスプロバイダにしてイベントにしてあげたほうがキレイなのだが、 Laravel の実装上、それは出来ないっぽい…。うぬ。。

具体的にはコマンドの発火時は取れるのだが verbose オプションがあるかどうかが取れない。 OutputInterface がイベントに入ってこないのだ。ソースも追いかけてみたがイベント引数に入ってくるのは Application インスタンスだけ。 OutputInterface を持ったものは Application から Command を呼ぶときにしか作られないらしい。

 
 
とまあ、作ってみたものの Artisan コマンドでも $this->info などして標準出力に出せるので、そちらをオーバーラップしたほうがキレイなんじゃないかなーと思った。こういうイメージ。

public function info($string, $verbosity = null)
{
    parent::info($string, $verbosity);
    \Log::info($string);
}

どちらかといえばこちらのほうが、メソッド別にログファサードのメソッドを呼べるのでパッと見でわかりやすそうだ。

   技術的な話題   

TF-IDF は文章における、単語の重みづけを行うもの。
処理した文章中のうち、ある単語はどの程度重要なものか、出現度合いから重み付けを計算する。
プログラム、機械からは文章の分析っていってもよくわからないので、そのよくわからない特徴をわかるようにするための、数値化する方法の1つ。

前提として TF-IDF には複数の文章を入力する必要がある。

TF-IDF = TF * IDF

TF = 1つの文章において、ある単語の出現回数 / 文章内の単語数
→ 文章1つずつの単語の出現頻度がわかる。

IDF = log( 文章数 / ある単語が出現する文章数 ) + 1
→ 横断的に使われる単語は低い値になる。

簡単な文章を入れつつ実際の数字を計算してみる。

 
step1:文章の入力

文章
リンゴとレモンとレモン
リンゴとミカン

 
step2:単語に分割(ここでは形態素解析し、名詞だけに絞ったとする)

文章 分割した結果
リンゴとレモンとレモン リンゴ レモン レモン
リンゴとミカン リンゴ ミカン

 
step3:TF値、IDF値を計算する

文章 TF:リンゴ TF:レモン TF:ミカン
リンゴとレモンとレモン 1 / 3 = 0.33 2 / 3 = 0.67 0 / 3 = 0
リンゴとミカン 1 / 2 = 0.5 0 / 2 = 0 1 / 2 = 0.5
単語 IDF
リンゴ log(2 / 2) + 1 = 1
レモン log(2 / 1) + 1 = 1.3
ミカン log(2 / 1) + 1 = 1.3

 
step4:TF-IDFを計算する

文章 TF-IDF:リンゴ TF-IDF:レモン TF-IDF:ミカン
リンゴとレモンとレモン 0.33 * 1 = 0.33 0.67 * 1.3 = 0.87 0 * 1.3 = 0
リンゴとミカン 0.5 * 1 = 0.5 0 * 1.3 = 0 0.5 * 1.3 = 0.65

 

この結果からわかることとして。

  • 「リンゴ」は横断的に出現するので値が低くなる。
  • 「レモン」は文章1だけに多く使われるので値に特徴が強く出る。
  • 「ミカン」は文章2だけに使われているが、リンゴと同程度しか使われていないので、特徴が大きく出るわけではない。
  • この2つの文章を切り分けるには レモン の様子を見ればよいらしい。ということで、確かに単語の重要度がわかるよ、っていうのはあっているらしい。

ただ、全ての文章・全ての単語を利用するという計算ロジックの都合、文章を逐次増やして差分計算していくー、というのは出来なさそうなので、そういう用途はむりそうだなー。
例えばこの計算済み TF-IDF の結果に加えて「バナナとレモン」を追加したときに困っちゃうね、という話。この量なら良いけど、もっと文章量が増えてもっと単語量が増えると、毎回計算し直すコストがかかる。

   技術的な話題   

composer require コマンドでライブラリを入れようとすると「ライブラリをいれたいけど入れられないよ!」なんて怒られることがある。いや、 composer install でも起こることあるわ。

起きたこと

※既に他のライブラリを入っている状態

$ composer require --dev "phpdocumentor/phpdocumentor:^2.8"
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
./composer.json has been updated

Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.
 
  Problem 1
    - phpdocumentor/phpdocumentor v2.8.0 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.1 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.2 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.3 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.4 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.5 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - Conclusion: don't install herrera-io/phar-update 1.0.3
    - Installation request for phpdocumentor/phpdocumentor ^2.8 -> satisfiable by phpdocumentor/phpdocumentor[v2.8.0, v2.8.1, v2.8.2, v2.8.3, v2.8.4, v2.8.5].
 
 
Installation failed, reverting ./composer.json to its original content.

原因

Composer は依存ライブラリの解決を行う際、それぞれのライブラリの composer.json に記載されているバージョンを解釈し、お互いに問題のない、なるべく最新のものを選択しようとします。

このとき「あるライブラリではAの1.0.* を使います。別のあるライブラリではAの2.0.*を使います」といった記述がされていると、エラーになってしまうようです。

あるいは composer.lock としてバージョンが固定されてしまうときにも同様の問題が起こります。依存を再解決しようとするときに上記のようにぶつかってしまうことがあるようです。

対応策

順番に確認していくと良さそう。

  1. そもそも composer.json で書かれているバージョンとエラーになっているバージョンとが噛み合わない
    • → 使っているライブラリ、または入れようとしているライブラリのバージョンを見直す
  2. そんなことはない、依存していたんだ、というくらいのものでエラーが出てる
    • → composer.lock を消して composer install

どちらにしても、依存ライブラリのバージョンが変わるはずなので、テストの実行や動作確認を行ったほうがよいです。