hkのweblog

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

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はまだまだ勉強中ですが、シンプルかつ便利な機能が豊富に準備されていて、おしゃれなアニメーションとかも作りやすそうです。
全体的に駆け足になりましたが、今日はこんなところで。

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

 月2回更新を目標としながら丸1ヶ月ブログの更新をサボってしまいました…
気を取り直して更新を再開していきます…

 最近、個人的にLaravelとVue.jsを組み合わせてSPA的なサイトを作っています。
LaravelもVue.jsも初めて扱うのでそこそこ苦労していますが、新しい技術を学ぶのはやっぱり楽しいものです。
以前、「Vagrant上にLaravelの開発環境を作る」という記事を書きましたが、これからしばらくはLaravelとVue.jsでサイトを作りながら気付いたことや学んだことを書いていこうと思います(たぶん他のことも書くけど)。

 今日は手始めにLaravelのmodelのリレーションあたりについて書いてみようと思います。

modelの生成

まずはmodelを作るところからスタートです。
DBにあらかじめ以下のような2つのtableが用意されているとします。
銭湯の基本情報と銭湯の位置情報の2つのテーブルです。1件のレコードに対して1件のレコードが対応する最もシンプルなhasOneのパターンです。

sento_info←銭湯の基本情報

カラム名 長さ データ例 備考
sento_code int 7 1 primaryKey
name varchar 50 テルマー湯
address varchar 100 東京都新宿区歌舞伎町1-1-2
tel varchar 15 03-5285-1726

sento_map←銭湯の位置情報

カラム名 長さ データ例 備考
sento_code int 7 1 sento_info.sento_codeのforeignKey
lat double 10,6 35.694535
lng double 10,6 139.705160

これもLaravelのmigrateで作ればOKですが、長くなるので今回は割愛します。 モデルは職人さんが作ってくれます。

php artisan make:model SentoInfo

同様にSentoMapも作ります。
なお、デフォルトではappのすぐ下にmodelが生成されるのですが、以下のようにパスを追加してやるとちゃんとフォルダの下に入れてくれます。

php artisan make:model Models/SentoMap

modelの修正

次に生成したモデルをDBと紐付ける必要があります。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class SentoInfo extends Model
{
    protected $table = 'sento_info';
    protected $primaryKey = 'sento_code';

}

鍵となるのは最後の2行です。
Laravelはデフォルトではテーブル名としてmodel名の複数形(この場合はsento_infos)、primaryKeyとしてidを指定してくれます。
今回のように別名を付けている場合は必ずこのように明示してやる必要があります。

SentoMap.phpの方も同じようにDBと紐付けておきます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class SentoMap extends Model
{
    protected $table = 'sento_map';
    protected $primaryKey = 'sento_code';
    
    }
}

リレーションを設定する

ようやく本題のリレーションの設定です。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class SentoInfo extends Model
{
    protected $table = 'sento_info';
    protected $primaryKey = 'sento_code';
    public function sentoMap() {
        return $this->hasOne('App\Models\SentoMap', 'sento_code');
    }
}

こんな感じ。hasOneの第1引数に紐付けるモデルのパス、第2引数にforeignKeyを設定します。
特にforeignKeyの方を忘れるとControllerから引っ張る時に正しいクエリを生成してくれなくてエラーになります。

Controllerから呼び出してみる

まずはapi.phpの方にルーティング情報を書いてみます。

<?php

use Illuminate\Http\Request;

/* 中略 */

Route::group(['middleware' =>'api'], function() {
    Route::get('sento/{sento_code?}', 'SentoController@detail');
}) ;

今回はルーティングが本題ではないので詳しく書きませんが、/api/sento/2 とかを叩くとsento_code=2でAPIが返ってきます。
このAPIをVue側で取得してテンプレートに埋め込んで表示するわけですが、そのあたりはまた次回にでも書こうと思います。

次にSentoController.phpです。コントローラーも職人さんが生成してくれます。

php artisan make:controller SentoController

で、生成されたSentoController.phpにdetailメソッドを書き加えます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\SentoInfo;

class SentoController extends Controller
{
  public function detail($sento_code) {
    $sento = SentoInfo::with('sentoMap')->find($sento_code);
    return $sento;
  }
}

with('リレーション名')はeager loadingと呼ばれるもので、SentoInfoをロードする段階でSentoMapも先に取得してしまう処理を指しています。
このやり方をしないとsento_mapの列の数だけクエリが発行されることになるため無駄な負荷がかかってしまうようです。
今回は1対1のhasOneなのでこだわる必要性は薄いかもしれませんが、スピード面やクエリの発行回数で課金されるような状況を考えると、このやり方をしておいた方が良さそうです。

find(primaryKey)でそのprimaryKeyを持ったレコード1件を取得できます。
さて、 /api/sento/1を叩いてみると…

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

こんな感じでjsonが返ってくれば成功です。
今日はとりあえずこんなところで。次回は返ってきたjsonをVue.jsの方で扱う記事でも書きたいと思います。 →次の記事はこちら

Cookieに複数の値を格納したい時に

1つのCookieに複数の値を入れたい、配列を入れたい、そんな時に使える便利な方法をメモしておきます。

こいつを使います↓
https://github.com/carhartl/jquery-cookie

基本的な使い方

例えば、連想配列Cookieに格納する場合を想定してみます。

//以下でCookieをjson化する
$.cookie.json = true;

//sampleHashという連想配列をsample_cookieというCookieにjson形式で保存する
let sampleHash = {fruit: 'apple', vegetable: 'tomato'};
$.cookie('sample_cookie', sampleHash);

//json形式で保存された'sample_cookie'をパースして連想配列に戻す
let parsedHash = $.cookie('sample_cookie');
console.log(parsedHash.fruit);
// apple

実にシンプルです。
1つのサイトで持てるCookieの数には上限があるので、使い所はあるのかなと思います。
その一方で1つのCookieの容量は4096byteが上限となっているので、いたずらにCookieを膨らませていくとオーバーしてしまうので注意が必要です。
短いですが今日はこんなところで。