202号室の手記

数学やコンピュータの小ネタを中心に書きます.

不純な動機で画像収集プログラムを作った件

誰もが一度は通る道(に違いない)。
某まとめブログを対象に作ったが、同じ要領で他のサイトにも適用できます。
乱用による成果物(色んな意味で)は、自己責任で処理(意味深)してください。
コードの全文はこちら

1. XPathの取得

収集対象のサイトのURLを仮に、http://niceurlとします。
ブラウザはChromeを使いましたが、他のブラウザでも同様の機能はあると思います。
http://niceurlにアクセスし、対象の画像で右クリック、"Inspect"を選択すると、 Developper Toolが立ち上がり、Webコンテンツとソースコードの対応付けをしてくれます。
下図のように、対象の画像に対応するソースコードの箇所で右クリック>Copy>Copy XPathと選択すると、 クリップボードXPathが貼り付けられます。

f:id:paperlefthand:20170630015746p:plain

上の例では、'//*[@id="more"]/a[1]'となっていました。
これは、"任意の階層及びタグで、idが"more"である要素の直下にある、aタグ要素の一番目" を指す一意名です。
XPathについては、こちらを参照。

2. 収集対象域の特定

'//*[@id="more"]/a[1]'を元にa[2],a[3],…と辿っていくと、 素敵画像のパスを特定できるわけです。
というわけで、コードの前半は次のようになります。

import os
import lxml.html
import urllib.request
import requests

url = " http://niceUrl"
actress = "niceName"

try:
    os.mkdir(actress) # (1)
except:
    pass

html = urllib.request.urlopen(url).read()  
root = lxml.html.fromstring(html)             # (2)
imgs = root.xpath('//*[@id="more"]/a')   # (3)
img_urls = [x.get("href") for x in imgs if ".jpg" in x.get("href")] # (4)
  1. 指定した人物名でディレクトリを作っています。
    顔認識なんかやりたいときのためにこうしています。
  2. urlopenで開いたWebページをいったんread()でテキスト化し、 改めてlxmlモジュールで読み直しています。
    こうすることで、取得したWebページをXPathの文法でパースすることができます。
  3. XPath'//*[@id="more"]/a'で、
    “任意の階層及びタグで、idが"more"である要素の直下にある、aタグ要素全て"を指定します。
  4. 取得したaタグ要素は、
    <a href="http://niceurl/nicegirl.jpg" target="_blank">...</a>
    などとなっているため、ここから.get()メソッドで、href属性の値すなわち画像ファイルのURLを取得します。 また、URLが画像ファイルになっていないものをリスト内包表記の条件式で排除しています。
    結果として、img_urlsには、お目当の画像全てのURLが格納されていることになります。

3. いざ、

コードの後半部です。

for (c,iurl) in enumerate(img_urls):   
    r = requests.get(iurl)
    if r.status_code == 200:
        with open(actress + "/" + actress + "{}.jpg".format(c), 'wb') as file:
            file.write(r.content)

requestsオブジェクトのget()メソッドで、URLにアクセスし、成功(200)した場合処理を進めます。
画像ファイル名は、"ディレクトリ名と同じ名前+番号.jpg"としました。
この名前でファイルをopenし、requestsオブジェクトのcontentを書き込みます。

4. さあ、お楽しみの時間だ

以上により、カレントディレクトリに作成された"niceName"ディレクトリ以下に、
番号でラベリングされた素敵画像が集まっています。
うまくいかない場合は、XPathが上記の例と異なっている可能性があるので、
もう一度確認してみてください。
楽しい画像収集ライフを!

Dockerコンテナをホスト側のcronで実行する

やりたいこと

  • とあるプログラムを実行するDockerコンテナを毎日定刻に起動したい。
  • 処理が完了したらコンテナは消去したい。

つまり、次のコマンドをcronで実行したい。

$ docker run -it --rm my_image my_command

やったこと

crontabにそのまま書けばいいじゃん!
と思ったが、そうは東京医科歯科大学

$ crontab -e
0 7 * * * docker run -it --rm my_image my_command

定刻になっても、うんともすんともしない。

まずは、crontabの実行環境で、dockerコマンドにパスが通ってんのかが気になった。

$ which docker
/usr/bin/docker
$ crontab -e 
* * * * * echo $PATH > /tmp/env.txt

結果は、

$ cat /tmp/env.txt
PATH=/usr/bin:/bin

.bash_profileに書いてあるようなユーザ定義の環境変数は受け継がれていないが、 少なくともdockerコマンドへのパスは通っているようだ。

デフォルトの/var/log/cronログファイルには、実行結果までは出力されない。
そこで、エラーチェックのために、次のように設定した。

* * * * * docker run -it --rm my_image my_command > /tmp/cron.log 2>&1

結果は、

$ cat /tmp/cron.log
the input device is not a TTY

となっていた。 実端末(TTY)からの入力でないことで怒られている。

原因はイカの通り。

cronで指定されたコマンドは実端末から実行される訳ではない。
実際、

$ crontab -e
* * * * * tty > /tmp/tty.txt

とすると、

$ cat /tmp/tty.txt
not a tty

となる。
docker runコマンドの-itオプションは、 実行時の端末をコンテナ内のプロセスに割り当てるものであるため、 割り当てるべき端末がない、と怒っていたのだ。

結論

crontabには、次のように書けば良いです。(毎朝7時に実行したい場合)

0 7 * * * docker run --rm my_image my_command

こうしてできたのが、↓のbotです。

twitter.com

以上です。

Flaskで作成されたWebアプリの動作確認ができるDocker Imageを作成した.

GitHub等で拾った, Flaskで作成されたWebアプリを, ローカルのDocker環境で試したい時に使える playgroundが欲しかったので作成しました.
Webアプリを公開するのは良いが, 人様の環境を汚すのが忍びないとき, などにも使えます.

1. ディレクトリ構造

- flask_env
  + Dockerfile
  - volume
    + main.py
    + requirements.txt

main.pyに, flaskを動かすプログラムを記述します.
なお, ポートは5000番で応答するようにします. 別の番号を利用したい場合は, DockerfileのEXPOSEの欄とdocker run の-pオプションの引数をそれぞれ変更します.
requirements.txtには, 必要なライブラリを, 次のように記載します.

Flask==0.12.1
requests==2.13.0
...

2. Dockerfile

FROM python:3.6

ARG project_dir=/volume/
ADD volume $project_dir

#RUN groupadd -r flask && useradd -r -g flask flask

WORKDIR $project_dir
RUN pip install -r requirements.txt

EXPOSE 5000
#USER flask

CMD ["python", "main.py"]
  • FROM… Pythonの公式イメージv3.6をベースイメージとします.
  • ARG… 環境変数project_dirを宣言しています.
  • ADD… ./volume以下を, コンテナ内部の/volumeとしてマウントしています.
  • RUN… flaskグループとflaskユーザを作成しています. 特に指定しない限り, Dockerfile内に記述されたコマンドは, コンテナ内部のrootユーザによって実行されます.
    今回は公開サーバ用途ではなく, ローカルで試しに動かすためのコンテナを作ろうとしているので, 実行ユーザについては気にしていませんが, アプリを公開する場合は, コメントアウトしてflaskユーザで実行するようにします.
  • WORKDIR… 作業ディレクトリを, 先ほど宣言したproject_dir=/volumeに変更します.
  • RUN… requirements.txtに記述されたライブラリをまとめてインストールします.
  • EXPOSE… 5000番ポートを解放します.
  • USER… 上記と同様に, 公開サーバ上で動かす場合は, 実行ユーザをflaskに変更します.
  • CMD… コンテナ起動時に実行するコマンドとして, python main.pyを指定します.

3. Docker Hubにリポジトリを作成

Docker Hubの自動ビルドサービスを利用して, 上記のイメージを管理するリポジトリを作成します.
Githubと連携することで, 上記のDockerfileから自動でイメージをビルドすることができます.

まず, flask_envディレクトリをGithubまたはBitbucketリポジトリに登録します.

次に, Docker Hubのページ右上の"Create"を選択し, プルダウンから"Create Automated Build"を選択します. f:id:paperlefthand:20170601000330p:plain

GithubまたはBitbucketを選択する画面が出るので, 先ほど選択した方を選びます. f:id:paperlefthand:20170601000546p:plain

選択すると, 自分のリポジトリ一覧が表示されるので, “flask_env"リポジトリを選択します.f:id:paperlefthand:20170601001131p:plain

コメントを書きたければ書きます. なお, デフォルトはpublicリポジトリとなっていますが,1リポジトリのみなら無料でprivateリポジトリに設定できます. f:id:paperlefthand:20170601000847p:plain

4. 自動ビルド

以上で準備が整いました. ターミナルから次のようにコミットすると, 自動ビルドが始まります.

$ git add .
$ git commit -m "link to Docker Hub"
$ git push -u origin master

f:id:paperlefthand:20170601001919p:plain

数分待ってページ更新すると, 次のようにビルド完了の表示に切り替わります.

f:id:paperlefthand:20170601002101p:plain

以上で, Docker Hubにイメージが登録されました.
今後も, gitでpushするたびに, Docker Hubで自動ビルドが実行され, 常に最新の状態に保たれます.

5. コンテナの起動

./volume/main.pyに動かしたいflaskアプリケーションのプログラムを記述して, ターミナルで次のように入力します.

$ docker run -it -p 5000:5000 --name flask_container XXX/flask_env

XXXには, Docker Hubのアカウント名を入れます.

ブラウザからhttp://localhost:5000にアクセスすると, ローカルでflaskアプリケーションが動作していることが確認できます.

Nginxでリダイレクトに失敗していると思い込んでいた件

nginxでWebサーバを立てた際, 

80番ポートへのアクセスを443番へリダイレクトする設定をした.

が, ブラウザからアクセスしても, curlでリクエストしても, 

目的のページにはたどり着けなかった.

原因がわからなかったが, 問題は設定ではなくリクエストの仕方にあった.

1. 環境

2. nginxの設定

/etc/nginx/nginx.conf

server {
       listen 80;
       server_name www.hoge.com/;
       return 301 https://$host$request_uri;
       }

www.hoge.comの80番ポートへアクセスがあると,

同じくwww.hoge.comの443番ポートへリダイレクトされる.

サブディレクトリについても同様に, 

http://www.hoge.com/huga?a=1&b=2 は, 

https://www.hoge.com/huga?a=1&b=2 へリダイレクトされる.

このとき, $hostはホスト名 www.hoge.comに,

$request_uriはパス名 /huga?a=1&b=2に対応している.

 

一方, 443番ポートでは, 次の設定で, アクセスを待ち構えている.

/etc/nginx/conf.d/ssl.conf

server {
        listen 443 ssl;
        server_name www.hoge.com;
        root  /var/www/html/;
        ssl_certificate        /path/to/your/cert.pem;
        ssl_certificate_key    /path/to/your/privkey.pem;
        }

これにより, ホストOSの/var/www/htmlディレクトリ以下のコンテンツが読まれることになる.

 

3. 原因1 <Firefoxのリダイレクト拒否設定>

 ...はずであったが, Firefox(53.0.2)でhttp://www.hoge.com/にアクセスしても, 

"正常に接続できませんでした"と表示され, たどり着けない.

原因はブラウザのキャッシュで, 以前接続に失敗したときのキャッシュが残っていたせいであった.

履歴を消去して再度アクセスしたら, リダイレクトに成功した.

 

4. 原因2<curlのオプション>

コマンドラインから

$ curl -I http://www.hoge.com/ --insecure

としても, "HTTP/1.1 301 Moved Permanently"と表示され,

到達できなかった.

これも大変初歩的なのだが, curlはデフォルトでリダイレクトを許容していないのであった.

$ curl -I -L http://www.hoge.com/ --insecure

とすることで解決した.