hkのweblog

ひよっこエンジニアがにわとりになるまでの軌跡

canvasで画像ファイルを読み込む

プロ野球シーズンは家でコードを書く時間が少なくなりがちです。うーん。
ということで1ヶ月半ぶりに記事を書きます。


先日、html5で導入されたcanvasを使った実装をしたので、そのコードを残しておこうと思います。
公式のドキュメントはこちら↓
Canvas API - HTML: HyperText Markup Language | MDN

canvasを使うのは初めてで、ユーザーがローカルの画像ファイルをアップロードする機能を実装したのですが、なかなか楽しかったです。
この記事では選択した画像を表示し、その上に別の画像をかぶせてみます。
Vue.jsで画像をあげる部分だけをコンポーネント化したので、そのコンポーネントをまるっと載せてみます。

<template>
    <div>
        <label>写真:</label><br>
        <canvas id="canvas" height="0"></canvas><br>
        <label>
            <span class="btn btn-primary btn-sm">
                画像を選択
                <input type="file" name="photo" v-on:change="drawCanvas" style="display:none">
            </span>
        </label>
        <div id="result"></div>
    </div>
</template>

<script>
export default {
    methods: {
        drawCanvas(e) {
            let fileData = e.target.files[0]
            if (!fileData.type.match('image.*')) {
                alert('画像を選択してください')
                return
            }
            let canvas = document.getElementById('canvas');
            let canvasWidth = 400;
            let canvasHeight = 300;
            canvas.width = canvasWidth;
            canvas.height = canvasHeight;
            let ctx = canvas.getContext('2d');

            let reader = new FileReader();
            let that = this;
            reader.onload = function() {
                let uploadImgSrc = reader.result;
                // canvas上に画像を重ねて表示
                let img = new Image();
                img.src = uploadImgSrc;
                img.onload = function() {
                    ctx.drawImage(img, 0, 0, canvasWidth, this.height * (canvasWidth / this.width));
                    // imgをloadした後にframe.jpgをloadして乗せる
                    let frame = new Image();
                    frame.src = "img/frame.jpg";
                    frame.onload = function() {
                        ctx.drawImage(frame, 0, 200, 400, 100);
                        // canvasを画像に変換
                        let imgSrc = canvas.toDataURL();
                        that.$store.commit('setImage', imgSrc)
                    }
                }
            }
            reader.readAsDataURL(fileData);
        }
    }
}
</script>

画像が選択されるとdrawCanvasが呼ばれます。
最初に選択されたファイルが画像かどうかチェックしています。本当はサイズ等々もう少し丁寧にバリデーションをかけるべきでしょうね。

次にcanvasのサイズ等基本的な設定をしていきます。
canvas要素はデフォで中途半端に高さを取ってしまうようです。
<canvas id="canvas" height="0"></canvas>のように書いておくと余計な隙間をなくすことができます。
getContextのところで2dを指定していますが、3次元の画像もできるようです。canvasを直接書くのではなく、three.js等のライブラリを使ったほうが良いようです。いつかやってみたい。

次にFileReaderオブジェクトを生成し、読み込んだ画像をImageオブジェクトにあてていきます。
ctx.drawImage(img, 0, 0, canvasWidth, this.height * (canvasWidth / this.width)); 左上を基点とし、右に0、下に0の位置から、幅がcanvasWidth、高さをthis.height * (canvasWidth / this.width)の画像を描画しています。
高さ300幅400の領域に幅固定で高さは縦横比を崩さずに画像を読み込んでいるということになります。
同様に上からかぶせる画像frame.jpgを読み込んでいます。ここではユーザーが読み込んだ画像の下部にバナーのような感じでframe.jpgが乗ることになります。
このように入れ子にしてやることで必ずframe.jpgが後から読み込まれて上に乗るようになります。

最後にreader.readAsDataURL(fileData);で画像を表示しています。
読み込むとこんな感じになります↓ f:id:h2r4t:20180706230431p:plain
なお、その上の2行ではcanvasに表示したものを1枚の画像URLに変換しています。
自分の場合はこれを別コンポーネントに渡して表示したり保存したりしたかったので、Vuexを使って渡しています。

以上、雑な実装ですが誰か読んだ方の参考になれば。

Vue.jsでsetTimeoutする時の注意点

月2更新目標のこのブログ、だいぶ更新をサボってしまいました…
書けなかった理由は色々あるんですが、ライトなネタで再開します。

というわけで今日はVue.jsネタ。
プライベートで作っているサービスですが、

  1. 入力フォームで確認ボタンを押す
  2. 確認モーダルが表示される
  3. モーダル上の投稿ボタンを押す
  4. 成功にせよ失敗にせよモーダル内にメッセージが表示される
  5. 3秒後にモーダルを閉じる
    ということをしようと思いました。 この3~5のところをsetTimeoutを使って実装しようとしたのがこんな感じ

Modal.vue

<template>
    <transition name="modal">
        <div class="overlay" @click="$emit('close')">
            <div class="panel" @click.stop>
                <b>投稿してよろしいですか?</b>
                <p>お名前:{{form.name}}</p>
                <p>メールアドレス:{{form.mailAddress}}</p>
                <button v-on:click="postArticle">投稿する</button>
                <button @click="$emit('close')">閉じる</button>
                <p>{{message}}</p>
            </div>
        </div>
    </transition>
</template>

<script>
export default {
    data() {
        return {
            message: ''
        }
    },
    // 親コンポーネントから渡してるform
    props: ['form'],
    methods: {
        postArticle:function() {
            let self = this.form;
            let article = {
                name: self.name,
                mailAddress: self.mailAddress
            };
            this.$http.post('/api/article/', article).then(response => {
                this.message = "登録が完了しました";
                setTimeout(this.closeModal(), 3000);
            }).catch(error => {
                //サーバーからjsonでエラーメッセージを投げる
                this.message = error.response.data.error;
                setTimeout(this.closeModal(), 3000);
            });
        },
        closeModal:function() {
            //親コンポーネント側でモーダルを消す処理
            this.$parent.showModal = false;
        }
    }
}
</script>

これがだめでした。投稿するボタンを押すと3秒待たずすぐにcloseModal()が実行されてしまうのです。
で、解決策はこれだけ

//これを
setTimeout(this.closeModal(), 3000);
//こうする
setTimeout(this.closeModal, 3000);

はい。それだけでした。
ほんとドキュメント読めよって感じですね… developer.mozilla.org setTimeoutって有名だけど普段フロントエンド触らないマン的にはあんまり書く機会ないよね、という言い訳…
あれ?これVue関係なくね…?と今更思いました。

以上です。

rsyncについてのメモ

仕事をしているとサーバーに入ってrsyncを叩く機会がちょこちょこあります。
rsyncは便利ですが、失敗すると痛い目を見ることになるコマンドの一つなので使い方を軽くメモしておこうと思いました。

基本的な使い方

rsync -av /home/test/ dst

こう書くと、testの下にあるファイルが全てdstの下に同期されます。testの後にある「/」が鍵です。
「/」を入れるとtestの下にあるファイルが同期の対象となり、「/」を外すとtest自体が同期の対象になります。

なお、「-av」とオプションが付いていますが、「-a」はほぼ必須と言っていいオプションのようです。
ディレクトリを再帰的にコピーしたり、権限や所有者、変更時刻等をコピーするそうです。
次に「-v」ですが、こちらは転送したファイルのバイト数等々の詳細情報を表示してくれるオプションです。

注意したい使い方

上記のやり方で用が足りる場合も多いのですが、もう2つほど重要なオプションについて書いておきます。
1つ目は「--delete」です。こんなふうに使います。

rsync -av --delete /home/test/ dst

「--delete」が加わると、test配下で削除されたファイルをdstでも削除してくれます。
私は先日リリースの際にこのオプションを使ったのですが、一歩間違うと全部消えてしまうしまあ怖いわけです
(大半のデプロイでは内製したツールを使っていますが、たまたま触ったリポジトリが非常に更新頻度の低いリポジトリで置き去りにされていました)

そこで予行演習のようなオプションがあります。それが「-n」です。
これを付けてrsyncを実行すると、実際に転送は行わずに転送内容の表示だけを行ってくれます。dry-runというやつです。

rsync -avn --delete /home/test/ dst

これを事前に試しておけば、どのファイルを転送して、どのファイルが消えて、というのを確認できるのです。
これで安心して--deleteオプションを付けて実行できそうです。

ここまで書かなかったオプション(--excludeや--existingなど)もありますが、これもいずれ使う機会があるといいなと思います。

スレッドが競合した時のこと

今日は備忘録。

先日の業務中、「複数のプロセスが同時に一つのメソッドを通ったことが原因でデータがおかしなことになる」というバグを発見しました。
単純な例を出すとこんな感じでしょうか。

2018年1月25日0時30分時点で太郎さんのM銀行の口座の残高はちょうど100万円です。
太郎さんはA社とB社の2社から給料をもらっており、給料日はいずれも25日です。
1月25日1時00分ちょうどにA社から50万円の給与が振り込まれました。
と、全く同じタイミングでB社から70万円の給与が振り込まれました。
太郎さんの口座の残高は100万+50万+70万=220万円となるはずです。
ところが、実際の残高は150万円となっています。
おかしいですね…

無論、銀行のシステムがこんなポンコツ実装なはずはないですが、「スレッドセーフではない」というのはこういうことです。
次に実際に目撃したバグの状況を抽象的に書いてみます。

サイトの会員がログインし、トップ画面に入ってきました。
トップ画面では、会員に紐づく情報を取得するために2つのリクエストが飛びます。仮に/getと/gainの2つとします。
/getにアクセスするとGetクラスのAメソッドを通り、その中でCalledクラスのMメソッドを呼びます。
/gainにアクセスするとGainクラスのBメソッドを通り、その中でCalledクラスのMメソッドを呼びます。
このMメソッドは、会員に紐づく1つのレコードをDBから取得して返します。ただし、まだ会員に紐づくレコードが存在しない場合、新しくレコードを生成してからそのレコードを返します。
今回、会員に紐づくレコードが存在しない状態で2つのリクエストがほぼ同時に飛び、Mメソッドが1つしか存在してはいけないはずのレコードを2つ作ってしまっていました。
2つのプロセスがMメソッドを数十ミリ秒以内の絶妙なタイミングで通過した場合のみ発生する稀なバグでした。
幸い実害はなかったのですが、本来1件のレコードを返すはずのメソッドが2件のレコードを返してくるせいでフレームワークが大量にwarnを出していました。

過去の履歴を見ると、GetクラスとCalledクラスが同時に誕生し、その後数年を経てからGainクラスが実装された結果、こんなことが起こったようです。
1人の会員につき1つのレコードしか持たないのだから、DBで会員番号のカラムにunique制約を付けておけばいいのですが、そもそも同時にリクエストが飛ぶことは想定していなかったようです。まあ流石に仕方ない気もします。

最初はMメソッドにsynchronizedを付与して排他制御にすれば同時にレコードを更新することは防げるのでは?と思いましたが、リクエストが分かれているせいで別々にインスタンスを作ってクエリを実行してしまうので意味がありません。

そこで、DBに残ってしまった重複データを削除してから会員番号のカラムにunique制約を付けて、Mメソッドの中で例外をcatchし、そこでもう一度Mメソッドを通るようにしようと考えました。
が、そもそも同じ画面で1つの情報を取るために2つのリクエストを投げる実装そのものがよろしくないのです。投げるリクエストを1つに絞るために実装を改めるということで決着しました。

改めてここまでの文章を読み返してみるとあっさり対策できたような感じに見えますが、原因箇所を具体的に書いてくれないフレームワークのwarnログと大量のアクセスログを突合し、原因箇所を見つけるまでにはかなりの時間がかかりました。
おまけに「複数スレッドが1つのメソッドを同時に通った場合」と言っても実際テストしてみると20回に1回くらいしか起こらないもので、再現するのになかなか苦労しました。
そういうわけで、いつか同じようなことに出くわしたときに思い出せるよう、備忘録を書いたのでした。

BeautifulSoupで東証一部上場企業の株価をスクレイピングしてみた

東証一部上場企業の従業員数、売上高等のデータを収集し、これらの指標と株価にはどの程度の相関があるのか調べるために単回帰してみる、ということをやってみようと思っています。
この記事ではその工程の一部である「東証一部上場企業のデータをスクレイピングで集める」部分を書いてみます。
私は統計の専門家ではないし、日常業務ではPythonを全く使っていません。変なところがあったらすいません。

環境

  • macOS High Sierra 10.13.2
  • Python 3.5(Anacondaでインストール)
  • pyenv+virtualenv使用
  • BeautifuSoup(4.6.0) こんなもんです。

大雑把な手順

  1. 証券コードが格納されているcsvファイルを用意する
  2. csvから証券コードを取得する
  3. htmlを取得する
  4. 必要なデータを取り出す
  5. 使用しやすいようにデータを加工する

証券コードが格納されているcsvファイルを用意する

これは技術でも何でもないのですが、
私は日本取引所グループのページから拝借しました。
Excelの機能で業種や取扱市場を絞り込んで必要な証券コードを抽出、csvに貼り付けます。
何だかんだ言ってもExcelって便利ですね。

csvから証券コードを取得する

先ほど取得したcsvをcode.csvとし、同じディレクトリ内でcollect_data.pyを作成します。

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

import csv

# 証券コードを読み込む
with open('code.csv', 'r', encoding='sjis') as f:
    reader = csv.reader(f)
    code_list = [row[0] for row in reader]

こんな感じでcode_listというリストに証券コードを格納することができます。
with openで書いていくとcloseし忘れる…といったことがないので便利ですね。
Excelで作成したcsvファイルは文字コードがShift-JISになるみたいなので、encoding='sjis'を指定しています。

htmlを取得する

さて、スクレイピングを始めていきます。
今回は日経新聞のサイトからデータを拝借しました。(規約的にNGだったらごめんなさい…)

import urllib.request

for code in code_list:
    url = "https://www.nikkei.com/nkd/company/gaiyo/?scode=" + code
    html = urllib.request.urlopen(url)
    soup = BeautifulSoup(html, "html.parser")

先ほど取得したcode_listから各証券コードを取り出し、URLを作ります。
このURLからhtmlを取得、HTMLパーサーで読みます。

必要なデータを取り出す

import re
from bs4 import BeautifulSoup 
comma_pattern = r'([0-9,--]+)'

# ここからは先程のfor文の中に書いています
    stock_price = re.findall(comma_pattern, soup.find("dd", class_="now").text)[0] #株価
    employee = re.findall(comma_pattern, soup.find("td", text=re.compile("\s人")).text)[0] #従業員数

soup.find('HTMLタグ名', 条件)という形で条件に当てはまる最初のHTMLを取得できます。
findの部分をfind_allにすると条件に当てはまる全てのHTMLをリストの形で取得できます。
classやidで一つに絞れると非常にやりやすいのですが、必ずしもそうはなっていなかったりします。まあスクレイピングしやすいようにサイトを作る人なんていないですよね…
そんな時は上記の従業員数の例のように絞ることもできます。
さらに正規表現を使ってデータを取っています。
ちなみにcomma_patternにハイフンやら記号が含まれているのは、持株会社等で従業員数が書かれていない会社が散見されたからです。
他にも様々な抽出手段があるようですが、詳しくはBeautifulSoupのドキュメントをご参照ください。

使用しやすいようにデータを加工する

ここまででも必要なデータは揃いますが、この後回帰分析を行うことを考えて、数値データからカンマを取り除いておきます。

# ここからは先程のfor文の中に書いています
    stock_price = re.findall(comma_pattern, soup.find("dd", class_="now").text)[0].replace(",","") #株価
    employee = re.findall(comma_pattern, soup.find("td", text=re.compile("\s人")).text)[0].replace(",","") #従業員数

所感

いざやってみるとデータを集めづらいサイトが多いです…
BeautifulSoupだけで完結せずに正規表現も使わざるを得ない場合も多そうです。
とはいえ、これだけのコード量でデータを収集できてしまうのは素晴らしいなと思います。
気が向いたら回帰分析についても書いてみようと思います。

コメントアウトに惑わされるな

結局今月も1回しか記事を書いていないことに気付き、実家で記事を書く大晦日です。
今日はほんと小ネタですが一応。

しばらく前、仕事で古めのページのjspを書き換えていた時、コメントアウトに翻弄されて1時間ほど作業が止まったことがありました。

ハマっていたのはこれです。

<html>
    <head>
        <title>test page</title>
        <script>
            <!--
                (function() {
                    console.log("非コメントアウト");
                })();
                //-->
            var nextYear = 2018;
            (function() {
                    console.log(nextYear + "年まであと少し!");
                })();
        </script>
    </head>
    <body>
        <p>test</p>
    </body>
</html>

ちょっとこの画面では分かりづらいのですが、普段私が業務で使っているEclipseでこのソースを開くと、scriptタグ内の<!--//-->に囲まれた部分が全て薄いブルーになります。
私は<!--//-->に囲まれた部分は実行されないだろうと思って読み飛ばしていたのです。
が、このhtmlを開くとコンソールはこうなります。

非コメントアウト
2018年まであと少し!

つまり一見コメントアウトされているように見える部分も実行されるのです。
これはjavascriptが無効にされているブラウザへの対応だそうで、<!--//-->に囲まれた部分が画面に表示されてしまうことを防ぐためのコメントアウトだそうです。

このことを知らなかった私は、約1200行の所々にscriptタグが挿入され、その一部がコメントアウトされていることを確認し、「これがコメントアウトされているのになんでこの挙動になるんだろう…?どこか他の箇所でjsを読み込んでいる?それともこれはサーバーサイドで実装している動き…?」などと1時間ほど悩みました。

今時javascriptを無効にしているユーザーはあまりいないように思いますが、古くから残っているソースコードにはこういう書き方が多く残っているのかもしれませんね。

今年はずいぶん初歩的なことばかり書いてきましたが、来年はもっと高度なブログも書いていきたいと思います。では良いお年を。

Laravel 5.5 + Vue 2.1でSPA的なものを作っている話のつづき

 少し間が空きましたが、前回の記事の続きです。
前回は複数のモデルのリレーションを設定して、jsonを返してやるところまで進めました。これですね↓

{
    sento_code: 1,
    sento_name: "テルマー湯",
    address: "東京都新宿区歌舞伎町1-1-2",
    tel: "03-5285-1726",
    sento_map: {
        sento_code: 1,
        lat: 35.694535,
        lng: 139.705160
    }
}

今回は返ってきたjsonを展開してみます。

Laravel側で最初に読み込むviewを設定する

 Single Page Applicationは文字通り最初のリクエスト時に1つのページを返しますが、以降はhtmlを返さず同じページを使いまわします。
APIにリクエストを投げて、返ってきたjsonを基にページを書き換えていくようなイメージになります。
今回の場合、最初のリクエスト時にベースとなるページを返したり、モデル周りなどサーバーサイドの処理を司るのがLaravel、以降ページを書き換えたりルーティングをしたりするのはVue側の役割になります。

というわけでまずはweb.phpに最初に読み込むページを設定します。

<?php

Route::get('/{any}', function () {
    return view('app');
})->where('any', '.*');

ざっくり言うと、「スラーの後に何が来ようとapp.blade.phpを返すよ」ということを書いています。

次に読み込まれるapp.blade.phpが以下です。

<!DOCTYPE html>
<html lang="{{config('app.locale')}}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X=UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <title>銭湯まっぷ</title>
        <link rel="stylesheet" href="{{mix('css/app.css')}}">
        <script>
            window.Laravel = {
                csrfToken: "{{csrf_token()}}"
            };
        </script>
    </head>
    <body>
        <div id="app">
            <navbar></navbar>
            <div class="container">
                <router-view></router-view>
            </div>
        </div>
    </body>
    <script src="{{mix('js/app.js')}}"></script>
</html>

静的ファイルの読み込み方とかcsrfがどうだとか細かく説明すべき箇所はたくさんあるのですが、今回は端折ります。
重要な役割を担うのは最後のほうで読み込んでいるapp.jsです。こいつが<div id="app">の中を書き換えます。

ルーティングの設定

import Vue from 'vue';
import VueRouter from 'vue-router';

window.Vue = require('vue');

require('./bootstrap');

Vue.use(VueRouter);

Vue.component('navbar', require('./components/Layouts/Navbar.vue'));

const router = new VueRouter({
    mode: 'history',
    routes: [
        {path: '/', component: require('./components/Index.vue')},
        {path: '/about', component: require('./components/About.vue')},
        {path: '/detail/:sento_code', component: require('./components/Detail.vue')},
    ]
});

const app = new Vue({
    router
}).$mount('#app');

私自身、SPAについて何も知らなかった頃は「SPAってURL固定でどうやってAPIリクエストするんだろう…」と思っていましたが、当然ルーティングはあります。
vue-routerというのが必要になるので予めpackage.jsonに書いてnpm installしておきましょう。
今回は'/'を叩くとIndex.vueを読み込み、'/about'を叩くとAbout.vueを読み込む…という構造にしています。
3つめだけ少し勝手が違いますが、'/detail/1'を叩くとsento_code=1というパラメータを渡しつつDetail.vueを読み込みます。 この他にコンポーネントを作っている部分があります。こんな書き方で<navbar></navbar>のところにNavbar.vueを読み込むことができます。
説明の順番がおかしい感じもしますが、最後の3行でVueインスタンスを生成しています。これで<div id="app">の中を書き換えることができます。

Vueのテンプレートの中でjsonを読み込む

さて、ようやく本題。Vueのテンプレートの中でjsonを読み込みます。
いくつかvueを読み込んでいますが、今回はこの中でDetail.vueについて書きます。
先程のjsonを読み込んで銭湯の情報の詳細ページを作るイメージです。
シンプルな構造のjsonのサンプルなら結構転がっているのですが、ちょっと階層が深くなると全然出てこなくて苦労しました…

<template>
    <div>
        <table>
            <tr>
                <th>施設名</th>
                <td>{{sento.sento_name}}</td>
            </tr>
            <tr>
                <th>住所</th>
                <td>{{sento.address}}</td>
            </tr>
            <tr>
                <th>電話番号</th>
                <td>{{sento.tel}}</td>
            </tr>
            <tr>
                <th>緯度</th>
                <td>{{sento.sento_map.lat}}</td>
            </tr>
            <tr>
                <th>経度</th>
                <td>{{sento.sento_map.lng}}</td>
            </tr>   
        </table>
    </div>
</template>

<script>
export default {
    created() {
        this.getSento()
    },
    data() {
        return {
            sento: {
                sento_name: '',
                address: '',
                tel: '',
                sento_map: {
                    lat: '',
                    lng: '',
                }
            }
        }
    },
    methods: {
        getSento() {
            this.$http.get('/api/sento/' + this.$route.params.sento_code)
            .then(res => {
                this.sento = res.data
            })
        }
    }
}
</script>

まず、<script>以下から見ていきます。
このへんは疎いのですが、export defaultはimportする際に特に指定がなければそのクラスや関数を呼ぶということのようです。
今回はgetSentoを呼んでいます。
下の方にgetSentoを書いています。'/api/sento/'のうしろにthis.$route.params.sento_codeとありますが、これが先ほどvue-routerで書いたパラメータです。
jQueryなんかと似ていますが、帰ってくるres.dataをsentoに渡してやって、これをテンプレートの中で展開していきます。
dataの部分では帰ってくるオブジェクトの型を書いています。主に苦労したのはここの構造だったりします。
cssを全く書いていないので表示はぐちゃぐちゃですが、表示されてほしいものはこれで表示されるはずです。
Vue.jsはまだまだ勉強中ですが、シンプルかつ便利な機能が豊富に準備されていて、おしゃれなアニメーションとかも作りやすそうです。
全体的に駆け足になりましたが、今日はこんなところで。