Python: BOMつきUTF-8のCSVファイルを読み込む

Windows の Excel で、CSV ファイルに UTF-8 で出力ができることを知った。調べてみると結構前からできるようになってたようだ。

Excel で作ったデータを CSV ファイルに出力して Python のスクリプトで処理する、っていうのを時々やるんだけど、今まではいったん Shift JIS で出力したのを秀丸エディタを使って UTF-8 に変換してから処理していた。スクリプトは Linux でも使うから入力データのエンコーディングは UTF-8 にしておきたいんだよね。

で、Excel が UTF-8 の CSV をはいてくれるなら面倒な変換の手間を省いてそのままスクリプトで処理できる……と思ってやってみたらエラーになった。BOM(バイトオーダーマーク)がついているのが原因らしい。UTF-8 に BOM がついてるのとついてないのがあるのは知ってたけど、気にしたことはなかった。今回初めて BOM つきの UTF-8 に出くわした。軽くググってみると Windows だけが BOM をつけるらしい。

さて、本題。

Python で BOM つきの UTF-8 を読み込むには、ファイルを開くときのエンコーディングに utf-8-sig を指定してやればいい。↓こんな感じ。

>>> import csv
>>> with open('data_with_bom.csv', encoding='utf-8-sig') as f:
...     for row in csv.reader(f):
...         if row[0]:
...             print(row[0])
...

utf-8-sig というエンコーディングは BOM なしの UTF-8 も扱える。なので UTF-8 であることがわかっていれば BOM を気にしなくていい。

>>> with open('data_without_bom.csv', encoding='utf-8-sig') as f:
...     for row in csv.reader(f):
...         if row[0]:
...             print(row[0])
...

というわけで、解決。

[余談]

コード中に if row[0]: とあるのは Excel のはいた CSV ファイルの後ろのほうにカンマだけの行がくっついてるからそれを避けるため。なんでそんな行がくっつくのかは謎。

っていうか Python って後置の if が使えないんだね。ちょっと使いにくいな。

自宅ネットワーク内のコンピュータのIPアドレス割当てを固定にした

昨日、今日と2日続けて停電が発生した。午後に降った雷雨のせいだ(たぶん)。停電は短時間(1分もない)だったけど断続的に発生して、おかげで普段つけっぱなしの PC の電源が落ちた。サーバにしている PC も、だ。

雨がおさまってきたころ、PC の電源を入れて立ち上げたけど、サーバ PC で動かしている web アプリにアクセスしても繋がらなくなっていた。どうも IP アドレスが変わってしまったようだ。停電でルータの電源も落ちたからだな(たぶん)。

たいていの家庭のネットワークも同じだと思うけど、ウチのネットワークもルータの DHCP 機能を利用して各 PC に自動で IP アドレスを割り当てている。今回、ルータの電源も落ちたことで、PC を立ち上げたときに割り当てられたアドレスが変わってしまったらしい。

で、リモートではつながらないのでローカルにログインして、割り当てられている IP アドレスを確認して、hosts ファイルを修正したら繋がるようになった。

さて、もしかすると明日も雷雨で停電するかもしれないし、それでまた IP アドレスが変わってしまうようなことになればまた今日と同じ作業をしなくちゃいけなくなる。そんなのは面倒なので、DHCP で割り当てられる IP アドレスを固定にすることにした。

ルータは web インターフェイスで設定できるので、ログインしたら DHCP の設定のページで固定割り当ての設定をする。もう少し詳しく書くと、PC の NIC の MAC アドレスと、割り当てたい IP アドレスの組を登録しておくってことだ。PC が何十台もあるわけじゃないから大した作業ではない。

ともあれ、これで電源が落ちても今日みたいな作業をしなくても済むはずだ。もっと早くやっときゃよかった。

Arctic Code Vault Contributor

今日、久しぶりに GitHub にアクセスしたら、自分のプロファイルの下のところに「Arctic Code Vault Contributor」なる文字があるに気がついた。

何かと思って調べてみると、GitHub のリポジトリのソースコードを北極圏の島にアーカイブして1000年後まで保管するとかいう計画に選ばれたらしい。

「GitHub Arctic Code Vault」というプロジェクトに一環で、北極圏にあるノルウェーのスヴァールバル諸島の永久凍土の地下(廃坑)のなかにマイクロフィルムに保存されたソースコード(2020年2月2日に取得したスナップショット)を保存するというものだ。今回は第1回目で、頻度は明らかにされていないものの数年に一度くらいの割合でスナップショットを保存するらしい。

それにしても、オレの書いたソースコードが1000年先まで残るのか。スゲー。

……と思いながらよく読むと、アクティブなパブリックリポジトリはすべて対象になったようだ。オレの書いたコードなんて自分でしか使わないようなのばっかなんだけど、そういうことね。いや、それにしてもすごいけど。

ところで、今回保存されたソースコードの総量は約21TBとのこと。圧縮してるのかもしれないけど、以外に少ないと感じた。まぁ、ソースコードってほとんどはテキストファイルだからそんなものなのかな。

Perl 7

驚きのニュースが飛び込んできた。Perl が約25年ぶりのメジャーバージョンアップだと。

どのくらい驚いたかって、Perl の記事がほとんどないこのブログにも書いちゃうくらい驚いた。

とりあえず記録として残しておく。

Ubuntu 20.04にnodenvをインストール

GitHub のリポジトリにあるドキュメントを見ながら。

GitHub – nodenv/nodenv

まずは GitHub から clone

takatoh@apostrophe:~$ git clone https://github.com/nodenv/nodenv.git ~/.nodenv

オプション扱いだけど dynamic bash extension とかいうのをビルド。nodenv がスピードアップするらしい。

takatoh@apostrophe:~$ cd ~/.nodenv && src/configure && make -C src

環境変数 PATH に ~/.nodenv を追加してから、セットアップ。

takatoh@apostrophe:~$ ~/.nodenv/bin/nodenv init
Load nodenv automatically by appending
the following to ~/.bashrc:

eval "$(nodenv init -)"

メッセージにあるとおり、.bashrc に記述を追加。終わったらシェルを起動し直す。

nodenv-doctor スクリプトで、ちゃんとインストールできてるか確認。

takatoh@apostrophe:~$ curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash

OK って出たから大丈夫なんだろう。

つぎに、これもオプションみたいだけど、node-build をインストール。

takatoh@apostrophe:~$ mkdir -p "$(nodenv root)"/plugins
takatoh@apostrophe:~$ git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build

これで完了。

nodenv install -l でインストール可能な Node.js のバージョン一覧が見られる。公式サイトによると 12.18.1 が最新の推奨版なので、それをインストールする。

takatoh@apostrophe:~$ nodenv install 12.18.1

nodenv global で利用するバージョンを指定。

takatoh@apostrophe:~$ nodenv global 12.18.1

これで最新推奨版の Node.js が使えるようになる。

takatoh@apostrophe:~$ node --version
v10.19.0

えっ??

takatoh@apostrophe:~$ nodenv versions
  system
* 12.18.1 (set by /home/takatoh/.nodenv/version)

ちゃんと 12.18.1 になってるじゃないか。どういうこと?

ちょっとびっくりしたけど、なんのことはない、シェルを立ち上げ直したらできた。

takatoh@apostrophe:~$ node -v
v12.18.1

おしまい。

Docker Compose

前回の記事では、Docker のネットワークを使って通信しながら、複数のコンテナを協働させた。

基本的には前回の記事のやり方でいいんだけど、サービスの数が増えたり、規模が大きくなってコンテナの数が増えたりすると、コンテナごとにコマンドを打って起動するのは面倒だ。コンテナによって指定するオプションも異なるので、それも覚えておく必要がある。

というわけで、そういう煩雑な部分を楽にしてくれるツールがある。それが Docker Compose だ。メインマシンである apostrophe をセットアップしたときに apt でインストールしておいた。

akatoh@apostrophe:docker$ docker-compose --version
docker-compose version 1.25.0, build unknown

題材と構成

前回記事と同じ、書籍管理 web アプリと HTTP サーバ(Nginx)という構成にする。詳しくは前回記事を参照。

ビルド済みのイメージを利用する

前回ビルドした Docker イメージが残っているので、まずはそれを使ってコンテナを起動する設定を書いてみる。

Docker Compose は docker-compose.yml という名前のファイルを読み込んで、その設定どおりにコンテナを起動してくれる。今回書いた docker-compose.yml ファイルは次の通り。

version: '3'
services:

  bruschetta-back:
    image: bruschetta:1
    container_name: bruschetta-back
    restart: always

  nginx-front:
    image: nginx-c:1
    container_name: nginx-front
    restart: always
    ports:
      - 8080:80

service: の下に連なっているのが、Docker Compose で起動するコンテナだ。各設定項目は見ればだいたいわかるだろうからここでは説明は省略。ただ、docker コマンドで起動するときとは次の点が違う。

  • Docker ネットワークは自動で作ってくれるので設定不要
  • コンテナ間通信は、コンテナ名ではなく service 名で行われる

特に2つ目の挙動は docker コマンドで起動したときと違うので注意が必要だ。最初、ここでハマった。

で、これを起動するには次のようにする。

takatoh@apostrophe:docker$ docker-compose up -d

起動中のコンテナの確認。

takatoh@apostrophe:docker$ docker-compose ps

そして停止。

takatoh@apostrophe:docker$ docker-compose stop

最後にコンテナの削除。

takatoh@apostrophe:docker$ docker-compose rm

Dockerイメージをビルドして利用する

さて、今度は Docker イメージをビルドするところからやってみる。そのためには、docker-compose.yml ファイルと同じディレクトリに、イメージ作成用のファイル一式を収めたディレクトリを置いておく。こんな感じ。

akatoh@apostrophe:docker$ ls -l
合計 12
drwxrwxr-x 3 takatoh takatoh 4096 5月 9 03:15 bruschetta
-rw-rw-r-- 1 takatoh takatoh 239 6月 14 13:26 docker-compose.yml
drwxrwxr-x 3 takatoh takatoh 4096 5月 14 06:19 nginx-c

そして、docker-compose.yml ファイルを次のように書き変える。

version: '3'
services:

  bruschetta-back:
    build: ./bruschetta
    image: bruschetta:1
    container_name: bruschetta-back
    restart: always

  nginx-front:
    build: ./nginx-c
    image: nginx-c:1
    container_name: nginx-front
    restart: always
    ports:
      - 8080:80

書き変えた、というか書き足したのは、build: の行だ。この行でイメージをビルドするための Dockerfile (とその他に必要なフィアル)を置いてあるディレクトリを指定している。

ビルドするには次のようにする。

takatoh@apostrophe:docker$ docker-compose build

イメージができたか、確認。

takatoh@apostrophe:docker$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
nginx-c             1                   c8b3966bbc31        54 seconds ago       127MB
bruschetta          1                   7d6607e796c2        About a minute ago   468MB
nginx               1.17.10             9beeba249f3e        4 weeks ago          127MB
ubuntu              20.04               1d622ef86b13        7 weeks ago          73.9MB

nginx-c:1 と bruschetta:1 がビルドされたイメージ。下の2つは元になったイメージ(Docker Hub から取ってきたもの)だ。

じゃあ、これを起動してみよう。

takatoh@apostrophe:docker$ docker-compose up -d

OK。ブラウザからアクセスしてみて、期待通りに動いているのを確認できた。

Dockerコンテナ間通信

Docker を使ったサービスでは、サービスを構成する各サーバプログラムをそれぞれ別のコンテナで立ち上げるのが一般的なようだ。例えばデータベース(MySQLなど)を利用する web アプリケーションであれば、つぎの3つのコンテナを立ち上げることになる:

  • データベースサーバ
  • web アプリケーションサーバ
  • HTTP サーバ

当然、これらを連携するにはコンテナ間をまたいだ通信をする必要がある。Docker にはコンテナ間通信を実現するネットワーク機能が備わっていて、各コンテナは同じ(Docker の)ネットワークに接続していれば、コンテナ名とポート番号を使って通信することができる。

今日はそのコンテナ間通信を試してみる。

題材と構成

先日作った書籍管理 web アプリと、HTTP サーバとして Nginx を前に立てる構成とする。上にはデータベースサーバが書いてあるけど、この書籍管理アプリはデータベースに Sqlite を使っているので、今回データベースサーバはなし。アプリケーションサーバと HTTP サーバ(Nginx)の2つだ。

コンテナ名は、アプリケーションサーバを bruschetta-back、HTTP サーバを nginx-c とする。この2つが Docker ネットワークで通信するわけだ。nginx-c は80番ポートをホスト側の8080ポートに接続する。ホスト側からは bruschetta-d というホスト名で nginx-c にアクセスできるようにしておく。

アプリケーションサーバ

先日作ったものなので詳細は省略。ただ、あいだに PC のリプレイスをはさんだので、イメージ名が bruschetta:1 に変わっている。内容は変更なし。

HTTPサーバ

全面に立てる HTTP サーバには、Docker Hub で公開されている Nginx こイメージをメースにして、アプリケーションサーバにつなぐための設定ファイルをコピーしたものを用意する。

Dockerfile はこう:

ROM nginx:1.17.10

COPY ./files/bruschetta-d /etc/nginx/conf.d/bruschetta-d.conf
RUN mkdir /var/log/nginx/bruschetta

CMD [ "nginx", "-g", "daemon off;" ]

この中でコピーしている Nginx の仮想ホストの設定ファイルはこうだ:

upstream uwsgi-bruschetta {
    server bruschetta-back:5000;
}

server {
    # port
    listen      80;

    # server name
    server_name bruschetta-d;

    # log files
    access_log /var/log/nginx/bruschetta/access.log combined;
    error_log  /var/log/nginx/bruschetta/error.log  warn;

    keepalive_timeout     60;
    proxy_connect_timeout 60;
    proxy_read_timeout    60;
    proxy_send_timeout    60;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://uwsgi-bruschetta;
    }

}

これで docker build した。

できたイメージの状況。

akatoh@apostrophe:docker$ docker image ls
REPOSITORY          TAG                IMAGE ID            CREATED              SIZE
nginx-c             1                  6f5be529ea6d        About a minute ago   127MB
bruschetta          1                  11afb91367ae        2 hours ago          451MB
nginx               1.17.10            9beeba249f3e        8 hours ago          127MB
ubuntu              20.04              1d622ef86b13        3 weeks ago          73.9MB

Docker ネットワークの作成

Docker ネットワークの作成は docker network create コマンドを利用する。

takatoh@apostrophe:docker$ docker network create nginx-network

これで nginx-network という名前の Docker ネットワークができた。つぎのようにすると様子が見える。

takatoh@apostrophe:docker$ docker network ls
NETWORK ID           NAME               DRIVER              SCOPE
d0c23099f6be         bridge             bridge              local
492e64e5d79d         host               host                local
7f38bac647a0         nginx-network      bridge              local
b42cbd843632         none               null                local

nginx-network 当名前が見える。ほかにもあるけど、これらはデフォルトで用意されているものらしい。詳細は調べてない。

コンテナの起動

2つのコンテナを起動する。その際、--netowork オプションで接続する Docker ネットワークの名前(今回は nginx-network)を指定する。また、HTTP サーバのコンテナの方でアプリケーションサーバの名前を bruschetta-back と指定しているので、これを間違えないようにする。

まずはアプリケーションサーバのコンテナから。

takatoh@apostrophe:docker$ docker run --name bruschetta-back --network nginx-network -d bruschetta:1

HTTP サーバ。ポートの指定も忘れずに。

akatoh@apostrophe:docker$ docker run --name nginx-front --network nginx-network -p 8080:80 -d nginx-c:1

ホスト側から確認

起動したコンテナは bruschetta-d というホスト名で待ち受けているので、/etc/hosts ファイルに 127.0.0.1 を指すように記述を追加する。それができたら準備は完了だ。http://bruschetta-d:8080/ にブラウザでアクセスしてみる。

結果、期待通りにアプリケーションにアクセス、使用できることが確認できた。

参考ページ

新しいPCにUbuntu 20.04 LTSをインストール

メインに使っている PC を買い替えた。

先日ダウンロードしておいた Ubuntu 20.04 LTS 日本語 Remix を DVD に焼いて(この作業は別の Windows マシンでやった)、プリインストールされていた Windows 10 は起動すらせずに上書きインストールした。ホスト名は替える前と同じ apostrophe。

OS のインストールが終わったら、とりあえずすぐに使いそうなソフトウェアだけインストールした。

  • rbenv と Ruby (2.7.1)
  • pyenv と Python (3.8.2)
  • Git
  • Sublime text
  • Dropbox
  • Tweaks
  • Docker

Python はデフォルトで入っていた(コマンドとしては python3)けど、今後を考えて pyenv を使った。Tweaks というのは、参考にした web ページにあった設定ツール。デスクトップまわりの設定ができる。

Docker についてはインストール方法を書いておこう。最初は先日 16.04 にインストールしたのを参考に作業をしたんだけどダメだった。えぇっ?と思いながらググってみると、apt コマンドでインストールできるようになっていた。

takatoh@apostrophe:~$ sudo apt install -y docker.io
takatoh@apostrophe:~$ sudo systemctl start docker
takatoh@apostrophe:~$ sudo systemctl enable docker

ついでに docker-compose もインストール(今後使う予定なので)。

takatoh@apostrophe:~$ sudo apt install -y docker-compose

あとはいくつか設定をカスタマイズして、データをバックアップサーバからコピーしてきて、ひとまずは完了。のこりは必要に応じてやっていこう。

参考にしたページ:

Dockerfileを書いてDockerイメージを作る練習→解決編

先日、Dockerfile を書いてイメージをビルドしたものの、コンテナが期待通りに動いてくれない、という記事を書いた。今日はその解決編。

結論を先に書くと、問題は Dockerfile じゃなくて Python のアプリケーションサーバである uWSGI の設定ファイルにあるつぎの行だった。

daemonize = file:/usr/bruschetta/bruschetta.log

この行をつぎのように書き換えたところ、期待通りに動いてくれて web アプリにアクセス、使用できるようになった。

logger = file:/usr/bruschetta/bruschetta.log

これは、なんとか原因を見つけようとして試行錯誤しながら「docker uwsgi」で検索に引っかかったいくつかのページを見ている時に気づいた。それらのページに載っている uWSGI の設定ファイルには daemonize = ... という記述がない。この記述は uWSGI にデーモンモードで動作するように指示する記述だ。そして、Dockerfile の CMD で指定したコマンドはコンテナの中でプロセス ID 1 で動作する、というのをどこかで読んだ。

つまり、こういうことだ。

  1. コンテナが起動すると、CMD で指定されている uWSGI がプロセス ID 1 で実行される。
  2. uWSGI はデーモンモードで動作するように設定ファイルで指定されているので、デーモンをフォークして自分自身は死ぬ。
  3. プロセス ID 1 のプロセスが死ぬと、コンテナも終了する。

実際に試してみよう。これが書きなおした uWSGI の設定ファイル。

[uwsgi]
http = :5000
chdir = /usr/bruschetta
wsgi-file = /usr/bruschetta/manage.py
callable = app
master = true
pidfile = /usr/bruschetta/bruschetta.pid
logger = file:/usr/bruschetta/bruschetta.log

上述したように最後の daemonize の行を logger に書き換えてある。Dockerfile も載せておく。

FROM ubuntu:20.04

RUN apt update && apt install -y python3 python3-pip git
RUN pip3 install uwsgi
RUN git clone https://github.com/takatoh/Bruschetta.git /usr/bruschetta
RUN cd /usr/bruschetta && pip3 install -r requirements.txt
COPY files/bruschetta.ini /usr/bruschetta/
RUN cd /usr/bruschetta && python3 manage.py init_db

CMD [ "/usr/local/bin/uwsgi", "/usr/bruschetta/bruschetta.ini" ]

こちらは、書き方は先日とちょっと変わっているが、実質的に変更無し。

これでビルドしたイメージからコンテナを起動する。

takatoh@apostrophe $ docker run -it -d -p 8080:5000 --name bruschetta-5 bruschetta:5

ブラウザで localhost:8080 アクセスすると、期待どおり web アプリが動作しているのを確認できた。

ちなみに、起動中のコンテナの中に入ってプロセスを確認してみると:

takatoh@apostrophe $ docker exec -it bruschetta-5 bash
root@423460e65b10:/# ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/0    Ss+    0:01 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
    8 pts/0    S+     0:00 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
    9 pts/0    S+     0:00 /usr/local/bin/uwsgi /usr/bruschetta/bruschetta.ini
   10 pts/1    Ss     0:00 bash
   21 pts/1    R+     0:00 ps ax

やっぱり、uWSGI がプロセス ID 1 で動いている。

教訓:コンテナで起動するプログラムはデーモンモードにするな