WebEngine

だらだらと綴る技術系メモ

Docker for MacでPython3環境の構築

きっかけ

仕事ではPHPとか、javascriptとかばっかりいじっているので、プライベートでは仕事で使う以外の言語でなにかつくったりしよう(遊ぼう)と思い立ったのが2ヶ月前くらいでした。
機械学習とか流行ってたなあ。出来たら面白そうだなあ。日本ではそれほどだけど、世界的なシェアはかなりある言語だから、まあやって損はないかな」なんて安直な理由でPythonを勉強しはじめました。

あくまで趣味の範囲なので、Macに最初から入っている2系のPythonを使っていました。しかし、ネットに落ちている情報などを見ていると、3系のPythonで書かれたソースコードが多くなってきているように感じます。いちいち2系と3系の違いを意識しながらプログラミングするのは面倒です。

しかし、趣味の範囲と言えど、3系のPythonを直接Macに落とし込むのは2系のPythonとバッティングしそうでなんとなく怖い。

というわけでDockerで仮想環境を立てることにしました。

Docker for Mac

下のリンクからダウンロードできます。僕は安定版(Stable)の方を入れました。

ダウンロードが終了したら起動しましょう。
ターミナルからバージョンを確認しておきます。

$ docker --version
Docker version 17.06.1-ce, build 874a737
$ which docker
/usr/local/bin/docker

Docker Hubから公式イメージを入手

Docker Hubというサイトで、どのようなイメージが配布されているかチェックできます。イメージをpullしてくるだけなら登録は必要ないです。

Official(公式)のものがあったので、そのイメージを利用することにします。

上のリンクにも書いてあるのですが、docker pull python:<バージョン>のようにして、バージョン指定を行うことができます。今回は3.6を取得します。

sudo docker pull python:3.6

入れたらdocker imagesで確認しておきます。

REPOSITORY  TAG    ...
python      3.6    ...

ほかにもIMAGE_IDSIZEを見ることができるはずです。

コンテナ起動

docker run -itでコンテナを作成し、操作できます。ここでは後につづく/bin/bashでログインしています。

--name 名前でコンテナに名前をつけることもできます。ここではpytestのことを指します。

docker run -it --name pytest python:3.6 /bin/bash


入ることに成功したら、Linuxディストリビューションの情報を確認してみます。 /etc/ディストリビューション名-release/etc/issueのどちらかのファイルを見てましょう。

OSのバージョンを確認するにはuname -aコマンドを使います。

# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
NAME="Debian GNU/Linux"
VERSION_ID="8"
VERSION="8 (jessie)"
ID=debian
HOME_URL="http://www.debian.org/"
SUPPORT_URL="http://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

# cat /etc/issue
Debian GNU/Linux 8 \n \l

# uname -a
Linux 0823c3a40902 4.9.41-moby #1 SMP Fri Aug 18 01:58:38 UTC 2017 x86_64 GNU/Linux

pythonのバージョンを確認すると、ちゃんと3.6が入っています。

# python --version
Python 3.6.2

pythonを管理するのに便利なpipも入っていました。ありがたい。

# pip --version
pip 9.0.1 from /usr/local/lib/python3.6/site-packages (python 3.6)

Pythonを動かしてみる

僕の場合、エディタもなにも入っていなかったので、とりあえずVimを落としてきました。Emacs派の人はEmacsを、どちらもわからない人はnanoあたりをインストールしてきて使いましょう。

ちなみに、最初にapt-get updateは実行しないとapt-getが使えないようです。

sudoコマンドも入っていなかったのでrootユーザのままでエディタをインストールします。

# apt-get update
# apt-get install -y vim


rootユーザのままだと落ち着かないので、新しくユーザをつくって、そのユーザで操作することにします。

# useradd -m pytest

上記コマンドで新規ユーザを登録できます。オプション-m/homeにユーザを作成することを指示します。このオプションをつけないとどこにユーザがつくられるかわかりません。

ls /homepytestが追加されていることを確認したらsu pytestでpytestユーザになります。変わることができたかは、コマンドライン上の#$になったかならないかで判断できます。
su - pytestのようにコマンドを実行すると、一発でpytestのホームディレクトリまで移動してくれます。


/home/pytest/にまで移動し、そこでhello.pyを作成します。

#-*- coding: utf-8 -*-
print("hello, python3")

python hello.pyで実行し、文字列が出力されればオーケイです。

※2017.09.10追記
そのままの設定だと、rootユーザでないとファイル編集できないみたいです。(Permission denied)自分はこのファイルをつくったときtootでなかったような気がするのですが、実際新しくファイルを作成してみると「rootでないとダメ」とメッセージが表示されました。ファイル作成の際は、rootユーザの状態で行ってください。

※2017.09.14追記
スミマセン、上記の件ですが、普通にいろいろいじっていただけで勘違いでした。pytestユーザでもファイルを作成できることを確認しました。

終了するには

1回目のexitでルートユーザに戻り、2回目のexitでホスト環境へ戻ってきます。

コンテナの起動・停止

docker ps -aにより、停止しているコンテナも表示することができるのですが、上記までの流れをそのままやってみると、コンテナは自動的に停止しています。(STATUS項目にExitedと表示されていれば停止しています)

コンテナを起動するにはdocker start コンテナID 、もしくはdocker start コンテナ名を使います。

docker start pytest

もう一度docker ps -aで確かめると動いているのが確認できるはずです。(その場合ExitedがUpになっている) 稼働しているので、aオプションをつけなくても表示されます。

停止するには、docker stop コンテナID、またはdocker stop コンテナ名です。

docker stop pytest

もう一度ログインする

もう一度コンテナ内に入るためにはdocker execを使います。動作中のコンテナでコマンドを実行するコマンドです。裏を返せば停止してるコンテナは操作できないということです。

docker exec -it pytest /bin/bash

入ることができたら作成したhello.pyがあることを確認してみましょう。

ホストから編集・実行

お気づきの方もいるかもしれませんが、hello.pyがあるか確認するだけならば、docker execを使えばコンテナ内に潜る必要はありません。

docker exec -it pytest ls /home/pytest

pytestコンテナに「lsコマンドを実行せよ」と命令を出しています。ここではhello.pyを作成した/home/pytest下を見ています。

ということは、このようなこともできるわけです。

docker exec -it pytest vim /home/pytest/hello.py

軽く編集してみます。

#-*- coding: utf-8 -*-
print("hello, python3")
print("hello, docker!!")

execで実行。

docker exec -it pytest python /home/pytest/hello.py

追加した文字列がホストのターミナルに出力されるかと思います。

2017.09.10追記

上記のように

docker exec -it pytest python /home/pytest/hello.py

など、ホスト側でpythonを実行したとき、エラーではないけれど変な感じで出力されるときがあるかと思います。
そういう場合は、Ctrl + lなどで一度ターミナルをクリアしてから、もう一度プログラムを実行すると、上手く出力される 可能性があります。

また

docker exec -it pytest python

とすることでpythonの対話モードが呼び出せることも確認しました。ちょっとした確認ならば、こちらを使う方が良いでしょう。

参考


Milkcocoaを使って簡易チャットを制作

前提

Milkcocoaってなに?

Uhuru社から提供されているBaaS(Backend as a Service)です。

Baasというのは、「自分でサーバを用意して、データベースを入れて〜」というような一連の環境構築をする必要がなく、さらに、ある程度のサーバサイドの機能(データ管理、プッシュ通知など)を用意してくれているので、フロントエンドの開発だけに集中できるサービスです。

立ち位置としては、SaaSとPaaSの中間くらいです。

メールアドレスとパスワードの登録だけで始められるので手軽です。クレジットカードの登録なんか要りません。
また日本発のサービスなのでドキュメントが日本語。英語が苦手な僕にとってはありがたかったです。

簡易チャット作成

今回はjavascriptで適当にチャットアプリをつくってみます。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Milkcocoa</title>
    <script src="https://cdn.mlkcca.com/v0.6.0/milkcocoa.js"></script>
    <script src="main.js"></script>
</head>
<body>
    <textarea id="messageArea" style="width:300px;height:150px;background:#555;color:#fff;font-size:17px;"></textarea><br>
    <button onclick="pushData()" style="width:150px;height:30px;">データを追加</button>
    <button onclick="pullData()" style="width:150px;height:30px;">データを取得</button>
    <ul id="pullDataArea">
    </ul>
</body>
</html>

main.js

var milkcocoa = new MilkCocoa('app_id');
var chatDataStore = milkcocoa.dataStore("chat");
var textArea, ul;

/**
 * ロード時処理
 * 5件のデータを読み込む
 */
window.onload = function() {
    textArea = document.getElementById("messageArea");
    ul = document.getElementById('pullDataArea');
    getText();
}

/**
 * pushされたとき、新しいデータを引っ張ってくる(pushされる状態を監視)
 */
chatDataStore.on("push", function(data) {
    pullData();
});

/**
 * データ追加ボタンを押された時の処理
 */
function pushData() {
    var text = textArea.value;
    sendText(text);
}

/**
 * データをデータストアに追加し、テキストエリアは空にする
 * @param text データストアに追加するテキスト
 */
function sendText(text) {
    chatDataStore.push({message: text}, function(data) {});
    textArea.value = "";
}

/**
 * データ取得ボタンを押された時の処理
 * テキスト送信時の更新
 */
function pullData() {
    removePullData();
    getText();
}

/**
 * 新しい順に5つデータを取得
 */
function getText() {
    chatDataStore.stream().size(5).sort('desc').next(function(err, data) {
        addPullData(data);
    });
}

/**
 * 取得したデータを画面上に表示する
 * @param data 取得したデータ
 */
function addPullData(data) {
    for (var i = data.length - 1; i >= 0; i--) {
        var li = document.createElement('li');
        ul.appendChild(li);
        li.innerHTML = data[i].value.message;
    }
}

/**
 * 表示しているデータを画面上から削除
 */
function removePullData() {
    for (var i = ul.childNodes.length - 1; i >= 0; i--) {
        ul.removeChild(ul.childNodes[i]);
    }
}


MilkcocoaのWebサイトでログインして、ダッシュボードから新しいアプリを作成すると、アプリケーションのIDが生成されると思います。アプリケーション名はなんでも良いです。僕は「TutorialApplication」としました。

var milkcocoa = new MilkCocoa('app_id');

main.jsの1行目のapp_idの部分に、そのアプリケーションIDを置き換えてください。
ダッシュボードのアプリケーションの概要から確認することができます。
f:id:web-engine:20170820132211p:plain

IDは間違えてGitHubなんかに上げたりしないようにしましょう。

実行

ブラウザを2つ立ち上げてファイルを実行。

f:id:web-engine:20170820145037p:plain

ローカル上ですが、ちゃんとリアルタイムでチャットができていることがわかると思います。
デザインを修正して、テキストエリアに文字数制限など設ければ、それなりのものになるのではないでしょうか。

ちなみに、「データを取得」ボタンはこのアプリにおいてまったく必要ないですが、メモとして残しておくことにしています。

BaaSの可能性

このように、短いコードでリアルタイム通信を実現できたわけですが、もっとすごいこともできるらしいです。
MilkcocoaではIoTを見据えてサービスをやっているみたいで、スマホなどのデバイスとの親和性も強いです。公式サイトではRaspberry Piなどを使ったサンプル も紹介されています。

参考


内部結合と外部結合[SQL]

前提

  • 環境:MySQLを使います(mysql Ver 14.14 Distrib 5.7.18)


結合とは

結合とは、複数のテーブルを特定のキーで結びつけて処理することです。
DBを使ったシステムやアプリケーションの多くは、1つのテーブルだけで実装されていることは少なく、 何枚かのテーブルを使って成り立っていることが多いです。
ECサイトを例にすると、顧客情報のテーブルと商品(サービス)のテーブル、と少なくとも2つはいります。この2つのテーブルをまとめてしまうことがどれほど厄介で危険なことかは、なんとなく察しがつくはずです。


例で使うテーブル

従業員(employees)が、なに(items)をどれだけ売ったか(sales)というデータがあると想定します。

employees
employee_id name
1 青木
2 岩野
3 武内
4 中津
5 山口
sales
sale_id employee_id item_id quantity
1 2 1 8
2 3 4 12
3 1 4 6
4 4 4 10
5 4 2 2
6 3 3 5
7 17 1 5
items
item_id price
1 1200
2 1250
3 1370
4 980

実際に手を使って動かしてみたい方は次のCREATE文を参考に。
3枚のテーブルの最初のIDは面倒だったのでオートインクリメントを使っています。
itemsテーブルにname(商品名)が入ってないので違和感がある方は好きにデータを挿入して ください。(今回は特に意味を持たないので省きました)

CREATE TABLE employees (
  employee_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE sales (
  sale_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  employee_id INT NOT NULL,
  item_id INT NOT NULL,
  quantity INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE items (
  item_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  price INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


内部結語

employeesテーブルとsalesテーブルを内部結合させてみたいと思います。

内部結合を行うにはJOININNER JOINのいずれかを使います。

「結合とは」の項目で、結合っていうのは複数のテーブルを特定のキーで結びつけて処理すること と紹介しました。
この特定のキーをここではemployee_idにしましょう。このキーは両方の テーブルにありますね。結合をするときは2つのテーブル、どちらにも定義してあるキーを主軸にします。

キーを結びつけるにはON テーブル1.キー = テーブル2.キーを使うかUSING(キー)を 使います。今回はUSINGで統一します。

SELECT * FROM employees 
INNER JOIN sales USING(employee_id);

結果はこのようになります。

+-------------+--------+---------+---------+----------+
| employee_id | name   | sale_id | item_id | quantity |
+-------------+--------+---------+---------+----------+
|           2 | 岩野   |       1 |       1 |        8 |
|           3 | 武内   |       2 |       4 |       12 |
|           1 | 青木   |       3 |       4 |        6 |
|           4 | 中津   |       4 |       4 |       10 |
|           4 | 中津   |       5 |       2 |        2 |
|           3 | 武内   |       6 |       3 |        5 |
+-------------+--------+---------+---------+----------+

ここで重要なのが内部結合はキーが一致しているレコードのみを抽出することです。

上の結果では、結合したテーブルではemployeesテーブルのID5山口さんと、salesテーブルのID7の レコードが抜け落ちています。(どちらも最終のレコード)

山口さんは途中入社でまだ売上の情報がなく、salesテーブルのID7の情報は存在しない従業員ID17を指している ために表示されません。なにかしらの不手際で紛れ込んだと考えるのが妥当でしょう。
しかし、山口さんの情報も表示したい、というケースもあるはずです。そのような場合は外部結合を使いましょう。

外部結合

外部結合では、どちらのテーブルのレコードをすべて表示するかによってSQLを変える必要があります。

  • 左のテーブルの場合はLEFT JOINもしくはLEFT OUTER JOIN
  • 右のテーブルの場合はRIGHT JOINもしくはRIGHT OUTER JOIN

を使います。

山口さんの情報を抜き出したい場合、employeesテーブル寄りにするSQLを書けば良いです。

SELECT * FROM employees 
LEFT OUTER JOIN sales USING(employee_id);
SELECT * FROM sales 
RIGHT OUTER JOIN employees USING(employee_id);

2つのSQLの結果は同じになります。

+-------------+--------+---------+---------+----------+
| employee_id | name   | sale_id | item_id | quantity |
+-------------+--------+---------+---------+----------+
|           2 | 岩野   |       1 |       1 |        8 |
|           3 | 武内   |       2 |       4 |       12 |
|           1 | 青木   |       3 |       4 |        6 |
|           4 | 中津   |       4 |       4 |       10 |
|           4 | 中津   |       5 |       2 |        2 |
|           3 | 武内   |       6 |       3 |        5 |
|           5 | 山口   |    NULL |    NULL |     NULL |
+-------------+--------+---------+---------+----------+


せっかくなのでsalesテーブル寄りの情報も出力してみます。

SELECT * FROM sales 
LEFT OUTER JOIN employees USING(employee_id);
SELECT * FROM employees 
RIGHT OUTER JOIN sales USING(employee_id);


+-------------+---------+---------+----------+--------+
| employee_id | sale_id | item_id | quantity | name   |
+-------------+---------+---------+----------+--------+
|           1 |       3 |       4 |        6 | 青木   |
|           2 |       1 |       1 |        8 | 岩野   |
|           3 |       2 |       4 |       12 | 武内   |
|           3 |       6 |       3 |        5 | 武内   |
|           4 |       4 |       4 |       10 | 中津   |
|           4 |       5 |       2 |        2 | 中津   |
|          17 |       7 |       1 |        5 | NULL   |
+-------------+---------+---------+----------+--------+

きちんとsalesテーブルにあるID7のレコードが取れています。employeesテーブルにID17の情報がないので 名前はNULLになっていますね。

応用

  • Q. employeesテーブルに登録してある従業員の合計売上を表示せよ


最終的に、従業員名と売上の2つを表示させて、問題に答えたこととします。
その前に、3つの表を結合させて全体像を知っておきましょう。
JOINを3つつなげて書くことで3枚のテーブルの結合を実現できます。

SELECT * FROM employees 
LEFT JOIN sales USING (employee_id) 
LEFT JOIN items USING (item_id);
+---------+-------------+--------+---------+----------+-------+
| item_id | employee_id | name   | sale_id | quantity | price |
+---------+-------------+--------+---------+----------+-------+
|       1 |           2 | 岩野   |       1 |        8 |  1200 |
|       4 |           3 | 武内   |       2 |       12 |   980 |
|       4 |           1 | 青木   |       3 |        6 |   980 |
|       4 |           4 | 中津   |       4 |       10 |   980 |
|       2 |           4 | 中津   |       5 |        2 |  1250 |
|       3 |           3 | 武内   |       6 |        5 |  1370 |
|    NULL |           5 | 山口   |    NULL |     NULL |  NULL |
+---------+-------------+--------+---------+----------+-------+

上の3つのJOINを使ったSQLを上手くいじるとシンプルに問題が解けるはずです。
もしかしたら、JOINを使わずに解けてしまうかもしれませんが、今回は結合の話 なので、解答ではJOINを使います。もちろん読者の皆様は使わなくても良いです。

解答は下へスクロールしていくとあります。























※ヒント SUMとGROUP BYを使います




















解答

SELECT employees.name, SUM(sales.quantity * items.price) 
FROM employees 
LEFT JOIN sales USING (employee_id) 
LEFT JOIN items USING (item_id) 
GROUP BY employees.employee_id;
+--------+-----------------------------------+
| name   | SUM(sales.quantity * items.price) |
+--------+-----------------------------------+
| 青木   |                              5880 |
| 岩野   |                              9600 |
| 武内   |                             18610 |
| 中津   |                             12300 |
| 山口   |                              NULL |
+--------+-----------------------------------+


このSQLが唯一の正解ではなく、読者様がもっと良いSQLを書いている 可能性は大いにあります。
現に、解答のSQLは処理速度などまったく気にしていません。
自分で考えたロジックを大切にしましょう。


※ 注意:MySQLのバージョンが5.7以上だとONLY_FULL_GROUP_BYがデフォルトでオンになっているようで、sql_modeを上書きするか、 SELECTするものをすべてGROUP BYに書かなくてはならないようです。気をつけましょう。


参考