sakaki のすべての投稿

PQI Air Card向けにcurlをコンパイルする作業をDockerで自動化

随分と前の記事ですが、以前PQI Air Cardに向けてcurlをコンパイルする記事を書きました。

最近こういうことはDocker内で済ませると手間が省けてよいのではという知見を得たので、サクッとDockerに閉じ込めることにしました。

リポジトリはこちらです。

git clone https://github.com/Sakaki/PQICC.git
cd PQICC
docker-compose up

成功するとbin/curl以下にcurlのバイナリが作られていると思います。

ただし、このバイナリは以前の記事を参考に作成しただけなので、動作しない可能性が高いです。申し訳ありません・・・

実機で動かしてみて、ちゃんと動作するようにしたい・・・したい・・・

Dockerfile+aptでHash Mismatchエラー

Dockerでubuntuを元にしたイメージを作っているとき、以下のエラーに遭遇。

Dockerfileを使いaptでいろいろとインストールしている時でした。

Err:101 http://archive.ubuntu.com/ubuntu xenial/main amd64 fakeroot amd64 1.20.2-1ubuntu1
  Hash Sum mismatch

そこでaptを使う際に以下の警告が出ていたことを思い出します。

WARNING: apt does not have a stable CLI interface.

最初はちょっと意味が分かりませんでしたが、aptはCUIコマンドではあるもののCLI、つまりスクリプト等からの実行はサポートされていないみたいです。

Dockerfile内のコマンドをaptからapt-getに変更したところうまくいきました。

警告にはちゃんと従うべきですね・・・

Dockerでプログラムのビルドを自動化した話

Dockerが面倒なプログラムのビルドに便利だったという話です。

普段は使わないけど、たまにビルドしたいプログラムとかは環境設定が面倒ですよね。

それをDockerを用いて環境構築を行うことでとても快適にできたので備忘録として書いておきます。

具体的にはDockerfileで環境構築を行い、イメージの実行時にビルドします。

Dockerfileではビルドに必要な環境をなるべく整えておきます。例えば、

  • 必要なシステムライブラリのインストール
  • ツールチェーンやコンパイラのダウンロード・導入
  • 環境変数のエクスポート
  • ビルドするプログラムのコピー
  • コンパイラに必要な外部ライブラリのインストール

などがありますね。一度ビルドしてしまえば再利用が効くので、重い処理は全てここでやってしまいましょう。

環境変数のエクスポートが地味に便利で、ビルドオプションをあらかじめターゲットに絞って設定できます。

私はたまにarm向けのバイナリをコンパイルする時があるので、コンテナ内だけで有効な環境変数が設定されるのは重宝しています。

ここまで行えれば、あとはビルドするスクリプトをイメージ実行時に走らせれば良いだけです。

また、イメージにはビルドに必要な環境が整っているので、サンドボックス的な使い方もできます。

Dockerはサーバー向けみたいな勝手なイメージがありましたが、こういう作業にも向いているんですね。

今回は仕事で行った作業の備忘録だったので具体的な事は書けませんでしたが、いずれ具体的な利用法を書こうと思います。

Python+seleniumの描画時に要素配置を待機する

seleniumを使ってのスクレイピングは強力ですが、非同期で実行されるのでrequestsやcurlのような確実性が失われてしまいます。

フォーム入力などの操作を確実に行うためには、対象の要素が描画されるまで待機する必要があります。

例えば、ログインページでユーザー名を入力する際は、以下のようなコードで要素が入力可能になるまで待機するのが安全です。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

driver = webdriver.Firefox()
driver.get(login_page_url)
WebDriverWait(driver, 20).until(expected_conditions.element_to_be_clickable((By.NAME, "username")))

また、ログイン後に何か操作を行う場合は、ログイン後にリダイレクトされるページがきちんと描画されていることを確認した方が良さそうです。

確認には適当なIDを持つ要素を選び、それが描画されるのを待つのが良いと思います。

WebDriverWait(driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "some-element-id")))

これらに気を使わないと意外とうまくいきません。

Pythonを書いているのに非同期に気を使っていると、別言語を書いている気分になりますね(ブラウザを操作しているので当然ですが・・・)。

Python+seleniumでwebdriverとしてheadlessなFirefoxを使う方法

Python+seleniumでスクレイピングをする際webdriverとしてPhantomJSを使っていましたが、前々からPhantomJSは非推奨という警告が出ていて気になっていました。

また、PhantomJSの開発者の方も別のヘッドレスブラウザを使ってほしい意向を示しているようです。

替わりの選択肢(ヘッドレス機能があるもの)としてはFirefoxかChromeくらいしか思い浮かびませんが、今回はデスクトップ版のUbuntuをターゲットとしたプログラムを書いていたためデフォルトで入っているFirefoxを使う事にしました。

seleniumからFirefoxを扱う際は、geckodriverというプロキシを経由して操作を行うようです。

最新版は、ここからダウンロードできますので、Pythonからそのパスを指定します。

geckodriverディレクトリ内にWindows(geckodriver.exe)とLinux(geckodriver)のバイナリを配置しています。

import os
from selenium import webdriver
from selenium.webdriver.firefox.options import Options

if os.name == 'nt':
    geckodriver_path = "geckodriver/geckodriver.exe"
else:
    geckodriver_path = "geckodriver/geckodriver"
options = Options()
options.set_headless(Options.headless)
driver = webdriver.Firefox(executable_path=geckodriver_path, options=options)

これでFirefoxがインストールされていれば動作するはずです(Windows機しかなかったのでこちらでしかテストしていませんが・・・)。

後でLinuxでの動作確認も行おうと思います。

ubuntuで再起動が要求される際に更新されるパッケージの内容を調査

サーバーのメンテでカーネルを更新したのですが、再起動後も「再起動が必要です」という表示が出ていました。

実行中のカーネルのバージョンも最新だったので、なぜ再起動が必要かを調査してみました。

まず、再起動が必要な場合 /var/run/reboot-required 内にその旨が記述されています。

$ cat /var/run/reboot-required
*** システムの再起動が必要です ***

また、 /var/run/reboot-required.pkgs を参照すると再起動が必要なパッケージの理由を閲覧できます。

$ cat /var/run/reboot-required.pkgs
linux-base

linux-baseというとカーネル関連のアップデートですが、その詳細がよく分かりませんね・・・

どうやら /usr/share/update-notifier/notify-reboot-required というスクリプトが実行されることでこの文章が記録されるらしいです。

スクリプト中に次の記述があります。

if [ "$0" = "/etc/kernel/postinst.d/update-notifier" ]; then
    DPKG_MAINTSCRIPT_PACKAGE=linux-base
fi

驚いたことにlinux-baseという文章は決め打ちで記述されているようです(だったらreboot-required.pkgsは必要ないのでは・・・)。

また、update-notifierはOSのアップデートやクラッシュレポートなどを監視・報告するプログラムのようです。

具体的に再起動で適用されるパッケージを表示したかったのですが、少し調べた限りでは方法が分かりませんでした。

とりあえず再起動を行って様子を見ようと思います。

Ubuntu 16.04のDocker更新時にCould not resolve hostエラー

Ubuntu ServerのDocker更新時に以下のエラーが出たので対応。

$ sudo apt update
エラー:1 https://download.docker.com/linux/ubuntu xenial InRelease
  Could not resolve host: download.docker.com

調べるとIPv6で通信時にエラーが出ているようです。

本来ならIPv6で更新できるのが理想と思いますが、IPv4で更新するように設定することで暫定的に対処しました。

参考サイト

$ echo 'Acquire::ForceIPv4 "true";' | sudo tee /etc/apt/apt.conf.d/99force-ipv4

私の環境ではこれでアップデートできるようになりました。

考えてみるとIPv6の設定がいろいろと疎かになっている気がするので、ちゃんと設定していればエラーが起きずに更新できるのかな・・・頭の痛い問題です・・・

【Python】seleniumのcookieをrequestsで使う

seleniumは重いのでログイン処理など動的な値の生成が必要な時だけ使いたい。

ログインした後はrequestsとかでサクッと処理したい、そんな時にはcookieの受け渡しが必要になります。

受け渡すのは意外と簡単で、例えばニコニコ動画にseleniumでログインしてrequestsでマイリストを取得したい時は、

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

import os
import requests
from selenium import webdriver


def get_mylist(login_id, password):
    # ログインを行う
    login_page_url = "https://account.nicovideo.jp/login"
    if os.name == "nt":
        phantomjs_path = "node_modules/phantomjs/lib/phantom/bin/phantomjs.exe"
    else:
        phantomjs_path = "node_modules/phantomjs/bin/phantomjs"
    try:
        driver = webdriver.PhantomJS(phantomjs_path)
    except FileNotFoundError:
        print("PhantomJSの実行バイナリが見つかりませんでした。\n"
              "npm install phantomjs \n"
              "を実行してインストールしてください。")
        return
    driver.get(login_page_url)
    login_id_form = driver.find_element_by_id("input__mailtel")
    password_form = driver.find_element_by_id("input__password")
    submit_button = driver.find_element_by_id("login__submit")
    login_id_form.send_keys(login_id)
    password_form.send_keys(password)
    submit_button.submit()
    # cookieの受け渡し
    session = requests.session()
    for cookie in driver.get_cookies():
        session.cookies.set(cookie["name"], cookie["value"])
    # requestsで取得
    mylist_url = "http://www.nicovideo.jp/api/deflist/list"
    response = session.get(mylist_url)
    print(response.text)


if __name__ == "__main__":
    get_mylist("your_id", "your_password")

のように実現できます。

なお、コードはほとんど前回の記事の流用なので、PhantomJSなどのセットアップなどについてはそちらを参照してください。

また、クッキーを受け渡す処理は

    session = requests.session()
    for cookie in driver.get_cookies():
        session.cookies.set(cookie['name'], cookie['value'])

のみで、PhantomJSからdict型のcookieを取得し、requestsのcookiejarにセットしているだけです。

ニコニコ動画にpython+selenium+PhantomJSでログイン

以前作成したニコニコ用のpythonスクリプトが動かなくなっていたので原因を調査。

どうやらログインの形式が変わったようで、古い方のログインページが削除されてしまったようです。

また、新しいログインページはJavascriptで動的にログイン情報を生成するため、今まで使っていたrequestsではうまくいきませんでした。

そこで、Javascriptなどの動的な要素を再現できるselenium+PhantomJSを用いてニコニコ動画にログインしてみたいと思います。

準備

pipでseleniumをインストールして、npmでPhantomJSを入れます。

npmは実行したいスクリプト直下のディレクトリ内で実行してください。

pip install selenium
npm install phantomjs

この作業により、node_modulesディレクトリ内にphantomjsが入ります。

スクリプト

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

import os
from selenium import webdriver


def login(login_id, password):
    login_page_url = "https://account.nicovideo.jp/login"
    if os.name == "nt":
        phantomjs_path = "node_modules/phantomjs/lib/phantom/bin/phantomjs.exe"
    else:
        phantomjs_path = "node_modules/phantomjs/bin/phantomjs"
    try:
        driver = webdriver.PhantomJS(phantomjs_path)
    except FileNotFoundError:
        print("PhantomJSの実行バイナリが見つかりませんでした。\n"
              "npm install phantomjs \n"
              "を実行してインストールしてください。")
        return
    driver.get(login_page_url)
    login_id_form = driver.find_element_by_id("input__mailtel")
    password_form = driver.find_element_by_id("input__password")
    submit_button = driver.find_element_by_id("login__submit")
    login_id_form.send_keys(login_id)
    password_form.send_keys(password)
    submit_button.submit()
    mylist_url = "http://www.nicovideo.jp/api/deflist/list"
    driver.get(mylist_url)
    print(driver.page_source)


if __name__ == "__main__":
    login("your_id", "your_password")

ブラウザの機能をそのまま実行してくれるので、ソースからhidden属性を探したりする必要も無くとっても簡単ですね。

ただ、少し重いようなので大量の処理には不向きかもしれません。

requestsのようなモジュールとうまく使い分けができると便利です。

WindowsでPyCharmの高DPIスケーリング問題に対処する

TL;DR

IDEのHelp > Find Action > Switch IDE Boot JDK にてIDE起動に用いるJDKをJetBrainsが提供する[bundled]の物からOracleが提供する物に変更する。

本文

普段からPyCharmにお世話になっているのですが、少し前からPyCharmを起動するたびにこの警告が出ていました。

PyCharm(というかJavaの問題らしいですが)でWindowsのUIスケール(設定>システム>ディスプレイ>拡大縮小とレイアウト から設定できます)に対応できていないらしく、画面の実際の大きさに対応して適切なフォントサイズで描画できないようです。

もともとの原因はJava8でのUIスケールのサポートが限定的らしく、最新のJava9にしないと完全サポートされない事らしいですが、残念ながらPyCharmはJava9では動かないようです(公式サイトより)。

実際、現在ラップトップディスプレイで作業を行っていますが、家の32インチディスプレイの設定が引き継がれており文字がとても小さいです・・・

これに対処するために、公式サイトではいくつかの設定案を提案しています。

  1. フォントサイズを変更してみる
  2. 互換性設定からウィンドウ全体を拡大するよう変更する

1はディスプレイを変更した際にいちいちフォントを設定しなおすのが面倒で、2は単なる画像拡大なので文字がぼやけて大変見づらいです。

そこで、JetBrainsが自社のIDE用にOpenJDKベースのカスタム版JDKを提供しているような話を思い出したので、そちらに変更することで問題が修正されていないか期待しました。

IDEを起動する際のJDKの変更は、Help > Find Action > Switch IDE Boot JDK から行えます。

ここでは、[bundled]と書かれているものがバンドルされているもので、もう片方がOracle公式の物のようです。

なんと、設定を見てみると既にJetBrainsのカスタム版JDKで起動するように設定されていました・・・

ならば物は試しということでOracle版のJDKを選択して再起動すると、驚いたことに警告が出ません。

設定からスケーリングを変更してみましたが、設定に合わせてフォントサイズが見事にスケールしてくれます。

スケーリング問題は公式JDKには発生しておらず、OpenJDKのみの問題だったということでしょうか・・・

よくわかりませんが、無事に解決できてよかったです。

フォントはかなり昔に作ったRicty for Powerlineを使っているのですが、そろそろ変えたいですね・・・