ygo-agent

最近山ほどPythonのパッケージを作っている。

というのも、パッケージの配布方法を身に着けてからは過去のパッケージとは呼べないコードの山がただのゴミに思えてきて、そのゴミをリサイクルするためにまともなパッケージ化を進めている。 基本的に同じ作業なので、だた仕事が急に大量発生しただけである。

そのゴミの一つとして、ygo-agentを思い出して、現在掘り起こして作業中である。

強化学習によって遊戯王オフィシャルカードゲームのエージェントを作ろうというプロジェクトであったが、有限時間でニューラルネットワーク学習できる環境及びコードが用意できなくて、2021年5月からずっと放置していた。

では、なぜ今更掘り起こしたかというと改善案が浮かんだからである。 それを忘れないうちにここにメモでもしておこうかなと。

決定木

モチベーションが落ちた理由はTensorFlowでNNを実装しても実行環境が用意できないためであった。 ちなみに現在も実行環境は用意できていない。

そもそも「2階のテンソルで表現することの難しい遊戯王をビット列に変換するなどして無理やりテンソルの形にしていたのが無茶だったのでは?」と思い始めて、別のアルゴリズムでの強化学習を考えることにした。

一番最初に記憶から浮かんできたのが「決定木」である。 「決定木アルゴリズム」は簡単に説明すると、ツリー上の条件分岐を経て出力を決定するアルゴリズムで、その条件分岐の条件を最適化してしていく事でヒューリスティック解を得るという手法である。

f:id:nnt339:20211015073700p:plain
https://analysis-navi.com/?p=2007 より引用

よくよく考えてみると、我々もカードゲームをするときは決定木で判断していないか?

「現在の手札にあれとこれがあって、相手の手札に○○が確定していて、残りライフが××で~」みたいに一つ一つ条件を確認しながら、最終的にプレイするカードを決めているはずだ。

決定木は古典的なアルゴリズムだが、カードゲームをする人間の思考回路と似ていると所から遊戯王との親和性は高いと思われる。

決定木の最適化

問題はどのように決定木を作成し、最適化するかという点である。 ニューラルネットワークは使えない。

次に記憶から浮かんできたのは「遺伝的アルゴリズム」である。 「遺伝的アルゴリズム」は複数のインスタンスを作成し、「交配」と「淘汰」を何世代も繰り返すことで解を得るアルゴリズムである。

実はこのアルゴリズムは最適解へ収束するという保証はない。 我々生物の仕組みを模倣しているので、ある程度良い方向に改善されることが期待できるが、基本的に突然変異に頼った博打アルゴリズムである。 また、複数のインスタンスを作成してそれぞれの”性能”を評価するため計算コストも高い。

こんな欠陥だらけの古典的アルゴリズムであるが、それでも私が期待するのはそれがいわゆる”調整”のプロセスに似ているからである。 ”調整”とは即ち、何回かの試行を通して裏目や新たな選択肢を見つける作業である。

いや待て。書いていて思ったが、”調整”のトライアンドエラープロセスはニューラルネットワークのほうが似てないか? いやでもニューラルネットワークも遅いしハイパーパラメータの調整も上手くいかないし・・・

まあ、なんか良い感じのアルゴリズム考えておきます。

Pythonのパッケージ化

諸君の中にはPython初心者を抜け出して、まとまったパッケージを書けるようになったが配布の方法がワカラナイという者がいるかもしれない。

そこで私のやっているパッケージ配布の3分クッキングを紹介しよう。

ディレクトリの整理

プロジェクト名
├─.git
├─tests
├─パッケージ名
├─LICENSE
├─README.md
├─setup.cfg
└─setup.py

最小構成だとこんな感じ。

import パッケージ名で読み込むのでプロジェクト名は何でもいいです。

tests

テストコードを収納するディレクトリ。

テストコードは配布物に混ぜる必要がないから分けておく。

LICENSE, README.md

配布するつもりならどちらもしっかり用意しておきたい。

setup.cfg

pipなどのパッケージインストーラー(厳密にいえばsetup.py)が参照する設定ファイル。

コピペして名前とか変えればOK。

[metadata]
name = ygocore-python
version = attr: ygocore.__VERSION__
author = hinihatetsu
author_email = hinihatetsu@example.com
url = https://github.com/hinihatesu/ygocore-python
description = YGO core package for Python
long_description = file: README.md
license = MIT
license_file = LICENSE

[options]
python_requires = >= 3.8 # Python 3.8 以上
include_package_data = True
packages = find: # プロジェクトディレクトリから__init__.pyのあるディレクトリを自動でパッケージ化
test_suite = tests
install_requires = # pip install 時に一緒にインストールされるパッケージ
    library1
    library2

[options.packages.find]
exclude = 
    tests # testsは配布しないので除外

他にもオプションがあるので慣れたら調べてみると良い。

setup.py

from setuptools import setup
setup()

これだけ。

あとはGithubに挙げておけば,

$ pip install git+https://github.com/yourname/yourprogect

でインストールできる。

PyPIはまた今度。

おまけ

そういえば夏休み中にR習得して、今年の目標Jupyterを達成したのだが、結局「データ分析?えーPythonで!」てなりそうな気がする。

Rは統計処理に向いた言語っていうけど、まともにデータ分析しようと思ったらtibbleとかggplotのライブラリに頼らないといけないし、若干VSCode+Jupyterでの使い勝手が悪いし(改行するごとに#を入力しなければならない)改善されてました!(2021/12/04)でPythonJuliaでいいかな~と。

アプリケーションレベルになるとJuliaは学術色が強いから選択肢から外れがち。結局残るのがPythonて感じ。 Juliaの多重ディスパッチはあるレベル以上のアプリケーションからは足かせになるイメージ。

日常の業務だとnode.jsのデフォルト非同期処理はあまりに面倒だし、Goも産業用プログラミング言語って感じ。 吐き出てきたエラーとか見てると、「ああ~これもGoで動いてんのか~」と普及率に結構驚く。

でもモダンなフロントエンド作るときはVue.js使うし、クロスプラットフォームのバイナリを生成したい時はGo使うから適材適所ってことね。

個人用のライトなアプリケーション作りたい私にはPythonがマッチしていたってだけ(動きはヘヴィーだけど)。

Portainer

複数環境でDockerを動かすことが多くなり、ログの監視に毎回、docker logs <containername> -fと打ち込むのが面倒になってきたので、GUI管理画面を探したところPortainerというサービスを見つけた。

www.portainer.io

KubernetesSwarmも管理できるが、まだそれらのツールには手を出していないため今回はDockerだけに絞って導入のメモをしたいと思う。

必要環境

  • Docker
  • Web Browser

インストール

Portainerはコンテナ管理サービスなだけあって、既に公式がコンテナイメージを用意してくれている。 したがって、Dockerさえ動けばLinuxでもWSLでもVMでもどこでもインストールできる。

コマンドラインから起動しても良いが、ボリュームのマウント等で長くなるのでdocker-compose.ymlを用意した。

version: "3.4"

services:
  portainer:
    image: portainer/portainer-ce
    container_name: portainer
    ports:
      - 9000:9000
    restart: always
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
      - type: volume
        source: portainer
        target: /data

volumes:
  portainer:
    name: portainer_data
$ docker-compose up -d

portrainerという名前のコンテナが起動して9000番でリッスンしている。 これはhttp用のポートなので、https接続したい時は9443番を使う。

Windowsの場合

Windowsの場合、「/var/run/docker.sockがない」と怒られるので、.envファイルを用意して次を記入する。

COMPOSE_CONVERT_WINDOWS_PATHS=1

ブラウザーhttp://localhost:9000を見に行けばOK。

リモートのDockerと繋ぐ

リモートのDocker(以降、リモートDocker)と繋ぐには、リモートDockerのデーモンをtcpでリッスンさせておく必要がある。serviceファイルを編集して対応する。

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target firewalld.service containerd.service
Wants=network-online.target
Requires=docker.socket containerd.service

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H tcp://127.0.0.1:2376 -H fd:// --containerd=/run/containerd/containerd.sock # ここ
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always

これで2376ポートでリッスンするのでPortainerのサイドバーのEnvironmentから追加する。 インターネットを介すときはTLSを施しておかないと危ない気がする。 私は面倒なのでsshのポートフォワーディングで対処してる。

$ ssh -l <username> <hostname> -fNL localhost:2376:<hostname>:2376

websockets(python) 型アノテーション

websocketsを使う

websocketsはWebSocketプロトコルPythonで実装する上で便利なライブラリである。

pypi.org

# インストール
$ pip install websockets

公式ドキュメントからそのまんまサンプルコードを

# クライアント
import asyncio
import websockets

async def hello():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        name = input("What's your name? ")

        await websocket.send(name)
        print(f">>> {name}")

        greeting = await websocket.recv()
        print(f"<<< {greeting}")

asyncio.run(hello())
# サーバ
import asyncio
import websockets

async def hello(websocket, path):
    name = await websocket.recv()
    print(f"<<< {name}")

    greeting = f"Hello {name}!"

    await websocket.send(greeting)
    print(f">>> {greeting}")

async def main():
    async with websockets.serve(hello, "localhost", 8765):
        await asyncio.Future()  # run forever

asyncio.run(main())

そしてお気に入りのエディターに入力して気付くのである。
「あれ?これ型ヒントどころか、インテリセンスも効かなくね?」

型ヒントを使う

原因は__init__.pyでなかなかの事をやっているから。

from __future__ import annotations

from .imports import lazy_import
from .version import version as __version__  # noqa


__all__ = [  # noqa
    "AbortHandshake",
    "basic_auth_protocol_factory",
    "BasicAuthWebSocketServerProtocol",
    "broadcast",
    "ClientConnection",
    "connect",
    "ConnectionClosed",
    "ConnectionClosedError",
    "ConnectionClosedOK",
    "Data",
    "DuplicateParameter",
    "ExtensionName",
    "ExtensionParameter",
    "InvalidHandshake",

# 以降省略 #

Issue で「カバレッジ100%でテストされているから型ヒントなくても大丈夫!」って言ってるからビビったけど、流石に実装してあった。

ライブラリ側で大丈夫でも利用者がミスったら意味ねえ。
Djangoもそれが理由で捨てた。
型ヒントの有無はマジでライブラリ選択の決定打になり得る

次のように変更。

# クライアント
import asyncio
import websockets.legacy.client as websockets # import websockets

async def hello():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        name = input("What's your name? ")

        await websocket.send(name)
        print(f">>> {name}")

        greeting = await websocket.recv()
        print(f"<<< {greeting}")

asyncio.run(hello())
import asyncio
import websockets.legacy.server as websockets # import websockets

async def hello(websocket, path):
    name = await websocket.recv()
    print(f"<<< {name}")

    greeting = f"Hello {name}!"

    await websocket.send(greeting)
    print(f">>> {greeting}")

async def main():
    async with websockets.serve(hello, "localhost", 8765):
        await asyncio.Future()  # run forever

asyncio.run(main())

以上、型ヒントのtipsでした。

サーバー on RaspberryPi を外部に公開

ラズパイを買ってスクレイピングやテスト環境として稼働させていたが、家庭内LANからしか利用できないのをもったいなく感じていた。 そこで何とかLAN外部からのアクセスを無料で実現する方法の調査結果および数日間の奮闘記を記す。 手っ取り早く実装したい方はリポジトリへどうぞ。

没案

固定グローバルIPの取得

なぜLAN外部からアクセスできないといえば、それは端末にグローバルIPが割り当てられていないからだ。 しかし、グローバルIPをトップの管轄から個人で取得するのは難しく、基本的にISP(Internet Service Provider)と特別なプランを契約して固定グローバルIPを割り当ててもらうことになる。

インターネット契約は私の管轄外であるため、この案はすぐに没になった。

DDNSに登録

固定グローバルIPが取得できなくても、ISPが自動的に割り当てたLANルータのWAN側IPをDDNSに登録して定期的に更新すれば、ドメインを使って外部からアクセスすることができる。 もちろんルーター側でNATやIPマスカレード設定してやる必要があるが、ルーターの設定権限があればできないこともない。 しかし私はルーターの設定権限も持っていなかったので没。LANに外部から穴を空けるため、セキュリティ的にも不安が残る案である。 そもそも集合住宅に住んでいるため、ルーターが二重に噛んでいる可能性が高く、LANルーターの設定をしたところで無駄に終わったかもしれない。

VPN

OpenVPNを活用すれば自力でVPNを構築できなくもない。 結局VPNサーバを設置するのに費用が掛かるので没。

WEBホスティングサービス

一応無料で借りられるサーバーは存在する。 もはやラズパイを活用することを諦めかけているが、やはりデータは自分の管理下に置いておきたいので没。

Remote.it

(おそらく)ポートフォワーディングを活用してネットワークを構築できるサービス。 端末のポートを開放しないためポートスキャンされる心配がない。 個人利用であれば5つの端末まで無料でネットワーク構築できる。 IoT関連で似たようなサービスは他にも見つかる。

導入は簡単なほうだが、それでも初めての利用ならばセットアップに数時間はかかる。 一度セットアップしてしまえば、デスクトップアプリ/CLIからLAN外端末に簡単にアクセスできる良サービス。

しかし、不特定多数へのWEB公開には向かないため没。

VPS

先ほどのRemote.itホストをVPS上で動かして、LANとVPSをRemote.itで繋げば間接的に公開できるのでは? 予算$3.0で格安のVPSを探しまくって1年契約のプランを見つけたが、やっぱり1セントも払いたくなくて没。

自作アプリケーション

先ほどのVPS+Remote.itの案から自分でアプリケーションを作ることを思いつく。 要はインターネットからアクセスできる環境を無料で探して、それをLAN内端末と繋げばよいのである。

クラウドサービス

クラウドサービスは山ほど存在するが、クレジットカード番号を入力したくなかったのでGCPAWS、Azureは没。 IBMクラウドは支払先を登録する必要はないが、永久無料のクラウドコンピューティング環境は存在しない。

良さそうなのが、無料枠で月500時間ほどWEBサービスを動かせるHeroku。 Dynoが実質コンテナなので、ローカルで動作しているDockerコンテナをCLIでデプロイできる(らしい)。

ということでHeroku Dynoでデプロイする(予定)。

アプリケーション

モデル

ルーターによって外部からの通信開始は遮断されているため端末を直接サーバーとしては使えないが、 今こうしてWEBページを閲覧しているようにクライアントから通信を開始すればルーターによって通信路が確保される。 そこで、

  1. DynoにRaspberryPiから接続することで通信路を確保
  2. Dynoに流れてきたユーザーのリクエストをその通信路を使ってRaspberryPiに転送
  3. RaspberryPiがローカルウェブサーバにリクエス
  4. RaspberryPiが受け取ったレスポンスをDynoに送信
  5. Dynoがユーザーに返信

通信路にHTTPを使用

実装しやすいHTTPが初期案。 サーバ側はFastAPI+Uvicornで非同期処理を行い、クライアント側はrequestsで実装する。

サーバAppはユーザーからリクエストがあるとパスやヘッダ、クッキーをクライアントAppに送信し、 クライアントAppはその情報を使ってローカルウェブサーバにHTTPリクエストする。

実装は上手くいったが、1ページの表示に30秒以上かかるため没。 HTTPは通信コストが高い。

FastAPIの宣伝

最近ウェブアプリケーション作るときはFastAPIがお気に入り。
採用の決定打は型ヒントと自動ドキュメントと非同期処理。
実体はStarlattePydanticを組み合わせたもの。
DjangoフルスタックだからRESTful APIには向かくて、Flaskは型ヒントがない(推測)からダメ。 同じ開発者が管理しているSQLModelもオススメ。

通信路にWebSocket

WebSocketは比較的新しいウェブプロトコルで、リアルタイムの画面更新などに使われる。 セッション開始時にのみHTTPを利用するため、定期的にHTTPでポーリングするAjaxなどと比べて通信コストが軽い。

FastAPIがWebSocketをサポートしていたため、ドキュメントを読みながら数時間で実装。 ページが数秒で表示されるようになり満足。

Cookieが上手く機能しない

FastAPIのレスポンスヘッダにSet-CookieをセットしてもブラウザにCookieが保存されない。(1つのレスポンスに複数Cookieを設定しても一つだけしかブラウザに保存されない?FastAPIに詳しい方がいたら教えてください!) ログイン機能などが機能せず無限ログイン地獄に嵌る。

Cookieについて調査しまくってメチャクチャ詳しくなった(気がする)。 が、問題は一向に解決しなまま時間だけが過ぎていった。

バイト列の直流し

間接的なリクエストは諦めて、Dynoに流れてきたバイト列をそのままローカルウェブサーバに転送することにした。 実装する上で参考にしたのがプロキシサーバである。

本来ならばより低レイヤーなソケットとかを扱うんだけど、Pythonはそこら辺のサポートが良くてasyncio.streamsモジュールで非同期TCPサーバを実装できる。

ホストAppとクライアントAppの通信は上手くいくんだけど、ブラウザに表示されない。 Googleしても理由がわからず。 数時間挙動を観察してると、どうやらブラウザがEoFを送信しないために永遠にストリームから読み込もうとしていたらしい。ページリロード等でブラウザから切断されるとホスト・クライアント間で通信が始まってローカルウェブサーバにアクセスできていたため正常に動作していると錯覚していた。

# 修正前のコールバック関数
def client_connected_callback(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
    data: bytes = await reader.read()  # ここで無限に待機していた
    do_something()  # websocketで転送する処理

# 修正後
def client_connected_callback(reader: asyncio.StreanReader, writer: asyncio.StreanWriter) -> None:
    buffer_size: int = 4096
    data: bytes = b''
    while True:
        packet: bytes = await reader.read(buffer_size)
        data += packet
        if len(packet) < buffer_size:
            break    
    do_something() # websocketで転送する処理

リポジトリ

github.com

まだ適当に書いただけなのでロギングとかエラーハンドリングとか不十分です。

おわりに

まあ何とかこれで動作確認(総所要時間丸二日)は上手くいったので、あとは

  1. 清書してライブラリとして仕上げて
  2. Dockerイメージを作成して
  3. Herokuにデプロイして
  4. WEBページを作る

だけかな。

古典的な攻撃法は勉強して、現在サイバー攻撃対策の基礎を勉強中だけど、セキュリティのプロから見たら危ない実装かもしれないね。 (over SSLプロトコル使うのは当たり前として)

まあどうせ個人的なブログとかのホスティングに使うだけだし、いつでもリセットできるラズパイだし、LAN内で感染するマルウェアを実行されない限り大丈夫でしょ(適当)。

このブログも近いうちに消えるかも?

パワポケ11ハタ人間1章攻略ルート

前田投手の綺麗なスライダーを見たら空振り三振を取りたくなったのでパワポケ11でペナントレースでもやろうと思ったら、手違いでデータが初期化されてしまった。

裏サクセスは表サクセスやグッピーとは違い、一度のクリアで最終的に高性能選手を量産できるようになるためハタ人間を1章からやり直すことにした。

その際の攻略ルートと反省点を記したい。

時間は±1時間くらいの目安。

結果

  • 仲間全員回収(茜も含む)
  • るりか、平山、堤、委員長、エリ、白瀬、青野、(リコ、夏奈)のアルバム達成。
  • 主人公を含む3人の第4スキル粘り達成(るりかのスキルレベル上げの調子が悪くて合計12で止まってしまった。)

改善の余地あり

残り9日

学校→マンション→学校→・・・を繰り返してるりか、平山を救出しつつ夏奈青野イベント1回目を消化。 一旦基地に戻って、るりか平山をパーティーに加える。
ここで平山がパーティに加わることによって、病院での戦闘に意味が生まれる。

9:00
マンションでワルザーを回収してからマップ左上の!を通りつつ病院へ。 ユイとるりか母を救出してモールのカギを回収しておく。 再び基地に戻ってユイをパーティーに追加。

12:00
埋立地を出入りしてリコ1回目。
運が良ければこの時点で白瀬が仲間になることも。

14:00
モールへ向かって石田を救出。 夏奈青野イベント2回目。

15:30
右上と左上の!を通って白瀬を狙いつつ病院で夏奈青野イベント3回目。

17:00
北公園でエリと村山を救出。

20:00
基地に戻って回復。

青野を加えて男2人以上で出発。

残り8日

夜が明けるまで、病院で堤、橋で委員長、埋め立て地でリコを狙う。

6:00
倉庫で小野を救出しつつ、夏奈イベント。

9:00
モールで夏奈を救出。

11:00
南公園で越後

12:00
学校で越後

残り時間
回収できなかった仲間を回収。

上手くいけば、るりか、平山、ユイ、石田、青野、エリ、村山、白瀬、堤、委員長、リコ、夏奈が仲間になっているのでパーティを整えて倉庫でLv10を目指す。

残り7日

港で大神を救出。 夜が明けてなければ学校で有田を救出。 夜が明けたら北公園で茜。

11:00
南公園で越後、茜。 日が暮れたらるりかをパーティに加えて南公園で幸せ草集め。

残り6日

スキルレベル合計7、Lv18~19ぐらいを目標に。 昼間に北公園で茜を救出。

残り5日

第3スキルを獲得して、アルバム用のイベント消化等。 食料集めのためにモール寄ったついでに爆弾教本、カレーなべを回収、椿を救出、ロボット撃破。

18:00
基地に戻って回復。

残り4日

0:00
一度基地に戻って鉄ゲタ、鋼のプロテクター、ゴキユニ、レイガン、ビームソードを合成。

カギに導かれるまま宇宙人基地へ。
その過程で大人たちも救出。

残り時間
徹底して1~6階を往復してスキルレベル合計9~10を目指す。 レベル差が6以上になったら1~9階を往復。
宇宙人基地はまだ撃破しない。
基地破壊が早いと10階まで潜らないとスキルが上がらなくなり、毎回10階まで潜ることによるやる気の減少が避けられない。

残り3日

スキルレベル合計11~12を目標に。
レベル32に達したら10階まで潜って基地撃破。

その後はモールで食料を回収してから帰宅&回復。

残り2日

肉壁を用意して主人公Lv39、残りLv36ぐらいまで。

残り1日

レベル40目指して頑張る。

結果(2回目)

結果

反省点

食料が余った

6つぐらい余ったので上手くマネジメントすれば5人目も粘れるのか?

ガンダー戦でリコ、夏奈を参戦し忘れた

時間切れでパーティー調整する暇がなかったので、肉壁にリコ夏奈を採用するなど工夫しておくべきだった。

村山イベで病院に寄る暇がない

残り6日あたりに肉壁として使って、病院の宇宙人撃破するついでに「病院のカギA」のイベント消化すべきだった。

Julia + Jupyter + VSCode + Windows10 でNotebook to PDF

以前導入したJuliaJupyter環境をリセットして、再度セットアップしようとしたら死ぬほど詰まったのでメモ。

はじめに

グラフ描画と言えばPython+matplotlibが文献も豊富で重宝するが、科学計算という点ではJuliaの方が優れている。 例えば、三角関数をプロットするだけを考えてもPythonではnumpyをインポートした上で、

import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
plt.plot(x, y)

と書かなければならないが、Juliaであれば、

using Plots
x = collect(0:0.01:2π)
y = sin.(x)
plot(x, y)

で書けるので、ちょっとしたグラフのレポートや計算のレポートを作成する際に最適である。

物理学科所属の私にとってunicodeをそのままコードに書けること程ありがたいものはない。

理系学生はJuliaの導入を検討すべきだと思うし、普及させたいと考えているのでその一助となれば。

環境

  • Windows10
  • Julia-1.6.1
  • Jupyter Notebook(IJuliaからminicondaを通してインストール)
  • VSCode-1.58.0-insider
  • MikTeX-21.6
  • Inkscape-1.1

Juliaのインストール

julialang.org (accessd on 2021/6/15)

環境変数PATHを通しておくと、どこからでも起動できるので便利。

Juliaのパッケージ

コンソールからJuliaREPLを起動する。

$ julia
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.1 (2021-04-23)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |
julia>

]キーを入力してパッケージモードへ切り替えると、(@1.6) pkg>になる。 使い方はGoogle検索しても良いし、helpで確認しても良い。

データ分析の基本的なパッケージを入れる。

(@1.6) pkg>add IJulia  # Jupyter用のカーネル
(@1.6) pkg>add Plots  # グラフ描画ライブラリ
(@1.6) pkg>add DataFrames # pandasのようなもの

それぞれの入門的なものは本題ではないのでここでは無し。 インターネット上のドキュメントは充実している。

Jupyter Notebookのインストール

既にインストールされている場合は必要ないかもしれないが、今回の本題であるNotebook to PDF という点では若干環境が異なるので注意。

パッケージモードからbackspaceキーで戻って、

julia> using IJulia
julia> notebook()

conda経由でインストールするか聞かれるのでyesと。 ~\.julia\condaPythonipython等の必要なライブラリがインストールされてJupyter Notebookが起動する。

因みに~\はホームディレクトリのことでWindowsだとC:\Users\username\

Jupyter NotebookVSCodeから使用するため今は閉じて良し。

VSCodeのインストールと拡張機能

VSCode InsidersのNotebookのGUIが好みだったんだけど、最近VSCodeの仕様と統一されたっぽい? setting.json

{
    "jupyter.experiments.optOutFrom": ["NativeNotebookEditor"]
}

となってただけでした。。。

code.visualstudio.com (accessd on 2021/6/15)

拡張機能JupyterJuliaだけで良い。

github.com

github.com (accessd on 2021/6/15)

Notebookの作成とグラフの描画

# julia-sin.ipynb #
using Plots
x = collect(0:0.01:2π)
y = sin.(x)
plot(x, y)

カーネルJuliaを選択してノートブックを実行するとグラフが描画される。

Notebook to PDF

さて、作成したノートブックをPDFのレポートとして提出したいわけだが、ここが難所なのである。

VSCodeJupyter拡張にはExport asというオプションがあるが、準備なしにはそれは上手くいかない。環境ごとに問題が発生するせいでインターネットにもピンポイントな回答は少ない。

今回はWindows10という環境に絞ってこの問題を解決したいと思う。

Jupyter Interpreterの選択

Jupyter拡張機能を起動する用のPythonインタープリターを選択する。Pythonを複数インストールしている場合は正しく選択されていない可能性があるので要確認。

VSCodeで「ctrl+shift+P」でコマンドパレットを開いて、「Jupyter: Select interpreter to start Jupyter Server」から先ほどcondaでインストールしたPython~\.julia\conda\3\python.exe)を指定する。

MiKTeXのインストール

PDFに変換する際nbconvertというPythonのライブラリを通して一旦texに変換するわけだが、その際に必要なパッケージを管理するのにWindowsではMiKTeXというソフトを使う。 miktex.org (accessd on 2021/6/15)

念のためC:\Program Files\MiKTeX\miktex\bin\x64\へPATHが通っている確認する。

Inkscapeのインストール

グラフをPDF出力するのにnbconvertInkscapeを必要とする。

inkscape.org (accessd on 2021/6/15)

さて、ここが詰まりポイントである。通常インストーラーでインストールするとC:\Program Files\へインストールされるのだが、Program Filesのスペースのせいでnbconvertsvg2pdf.pyが機能しない。InkscapeをCドライブ直下などに配置しなおすことで対処可能。このとき、C:\Inkscape\binにPATHを通すのを忘れないようにする。

PDFの日本語出力

レポートを書いていてすぐに気が付くのが、日本語が出力されないことである。日本の大学で教授も日本人(たとえいくら国際学会で活躍していたとしても)となると名前ぐらいは漢字で書くのが普通だと思うので厄介な問題となる。

結論から言うと、~\.julia\conda\3\share\jupyter\nbconvert\templates\latex\index.tex.j2を次のように書き換える。

((=- Default to the notebook output style -=))
((*- if not cell_style is defined -*))
    ((* set cell_style = 'style_jupyter.tex.j2' *))
((*- endif -*))

((=- Inherit from the specified cell style. -=))
((* extends cell_style *))


%===============================================================================
% Latex Article
%===============================================================================

((*- block docclass -*))
\documentclass[xelatex,ja=standard]{bxjsarticle}
((*- endblock docclass -*))

((*- block packages -*))
((( super() )))
% For greek in a code block
\usepackage[Greek]{ucharclasses}
\newfontfamily{\mygreek}{DejaVuSansMono} 
\setTransitionsForGreek{\mygreek}{}
((*- endblock packages -*))

もしファイルが見つからなければ、

$ cd ~\.julia\conda\3\Scripts
$ ./jupyter --path

で出力されたdataディレクトリを調べる。

Dockerでゴリ押し (2022/01/13)

現在のタスクが一段落したらイメージを作成してDockerHubに挙げたいと考えている。
イメージを作った。

github.com

hub.docker.com

最後に

どれも間接的にエラーとして出力はされてはいるんだけど、解決法は示してくれなくて相当手間取った。 例えば、「nbconvert is not installed」って出るけどpipで確認するとインストールされているから意味がわからないし、MiKTeXがインストールされていてパスも通っているのに「latex is not installed」って出るからイライラが募った。

特にInkscape周りはそもそもエラーが文字化けしてて何が問題なのかさっぱりだった。ディレクトリ名にスペース入れるのやめてもらえませんか?

Docker + Selenium + arm64

When I tried to use Selenium on RaspberryPi whose OS is Ubuntu, I couldn't do it because any image of SeleniumHQ/docker-selenium doesn't support arm64. I tried another way, which is downloading chromedriver from https://chromedriver.chromium.org/downloads, but it didn't work. I was completely exhausted. That's why I'm writing how to manage to do Selenium on the device .

Seleniarm

This repository is becoming mature. It could help you.

github.com (accessed on 2022/09/09)

Run a container

Start up a docker container.

docker run -it --rm python:3.9 /bin/bash

The following tasks should be done in the container.

Install chromium-browser and chromium-chromedriver

launchpad.net

Download .deb files of chromium-browser(depedencies also needed) and chromium-chromedriver from the above URL.

#! /bin/bash
# Update on 2021/12/30


# dependencies
wget https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files/chromium-codecs-ffmpeg_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb
wget https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files/chromium-codecs-ffmpeg-extra_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb

# chromium-browser
wget https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files/chromium-browser_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb

# chromium-chromedriver
wget https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files/chromium-chromedriver_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb

# Install all
apt-get update
apt-get install -y ./chromium-codecs-ffmpeg_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb 
apt-get install -y. /chromium-codecs-ffmpeg-extra_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb 
apt-get install -y ./chromium-browser_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb 
apt-get install -y ./chromium-chromedriver_96.0.4664.110-0ubuntu0.18.04.1_arm64.deb

# Install selenium
pip install -U pip
pip install selenium

Sample Script

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time

def main():
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    options.add_argument('--no-sandbox') # An error will occur without this line
    driver = webdriver.Chrome(options=options)
    try:
        driver.get('https://www.google.co.jp/')
        search = driver.find_element(By.NAME, 'q')
        search.send_keys('Python')
        search.send_keys(Keys.RETURN)

        time.sleep(3)
        driver.save_screenshot('search.png')
    finally:
        driver.close()
        driver.quit()

if __name__ == '__main__':
    main()

Result

I will fix the grabling someday...

Dockerfile

Here is an example of Dockerfile.

FROM python:3.9

# bullseye:  worked
# buster:    got an error "lsb_release: command not found"
# I don't know the reason


ARG VERSION=96.0.4664.110-0ubuntu0.18.04.1
ARG ARCH=arm64
ARG URLBASE=https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files
WORKDIR /opt/chromium
RUN wget ${URLBASE}/chromium-codecs-ffmpeg_${VERSION}_${ARCH}.deb \
 && wget ${URLBASE}/chromium-codecs-ffmpeg-extra_${VERSION}_${ARCH}.deb \
 && wget ${URLBASE}/chromium-browser_${VERSION}_${ARCH}.deb \
 && wget ${URLBASE}/chromium-chromedriver_${VERSION}_${ARCH}.deb \
 && apt-get update \
 && apt-get install -y ./chromium-codecs-ffmpeg_${VERSION}_${ARCH}.deb \
 && apt-get install -y ./chromium-codecs-ffmpeg-extra_${VERSION}_${ARCH}.deb \
 && apt-get install -y ./chromium-browser_${VERSION}_${ARCH}.deb \
 && apt-get install -y ./chromium-chromedriver_${VERSION}_${ARCH}.deb \
 && rm -rf /var/lib/apt/lists/*

# Install selenium
RUN pip install --upgrade pip \
 && pip install selenium

Build for amd64

docker build --build-arg ARCH=amd64 .

Docker Buildx

Create install_chromium.sh with the following content.

# Command line arguments
PLATFORM=$1

# Constants
VERSION="96.0.4664.110-0ubuntu0.18.04.1"
ARCH=`echo ${PLATFORM} | cut -d '/' -f 2` 
URLBASE="https://launchpad.net/~canonical-chromium-builds/+archive/ubuntu/stage/+files"

# Download
wget ${URLBASE}/chromium-codecs-ffmpeg_${VERSION}_${ARCH}.deb
wget ${URLBASE}/chromium-codecs-ffmpeg-extra_${VERSION}_${ARCH}.deb
wget ${URLBASE}/chromium-browser_${VERSION}_{ARCH}.deb
wget ${URLBASE}/chromium-chromedriver_${VERSION}_${ARCH}.deb

# Install
apt-get update
apt-get install -y ./chromium-codecs-ffmpeg_${VERSION}_${ARCH}.deb 
apt-get install -y ./chromium-codecs-ffmpeg-extra_${VERSION}_${ARCH}.deb 
apt-get install -y ./chromium-browser_${VERSION}_${ARCH}.deb 
apt-get install -y ./chromium-chromedriver_${VERSION}_${ARCH}.deb
rm -rf /var/lib/apt/lists/*

Dockerfile is as follows.

FROM python:3.9

ARG TARGETPLATFORM

WORKDIR /opt/chromium
COPY install_chromium.sh /opt/chromium
RUN bash -e install_chromium.sh ${TARGETPLATFORM}

# Install Selenium
RUN pip install --upgrade pip \
 && pip install selenium

Build it for multiplatform.

docker buildx build --platform linux/amd64,linux/arm64 --push -t yourname/yourtag .

https://docs.docker.com/build/buildx/docs.docker.com (accessed on 2022/07/27)

Selenium Server ※ NOT work

The official images of docker-selenium don't support linux/arm64 architecture. Then why don't you build your own image from its Dockerfile?
Here is the repository.

github.com

First, clone the repo to your arm64 device.
According to Building the images section of the README, you can build everything by running:

make build VERSION=local

However, you need some extra actions to build for arm64 before you run the line.

Comment out "Customize sources for apt-get" section

When I manually built selenium/base image to investigate the reason why my build failed, I found it is because apt-get update failed. Then I commented out Customize sources for apt-get section in ./Base/Dockerfile.

Replace "amd64" to "arm64"

Replace string amd64 to arm64. (I remember it was just one in ./Base/Dockerfile)

sed -i 's/amd64/arm64/g' ./Base/Dockerfile

Build

VERSION=local make <rule>
# e.g. VERSION=local make standalone_firefox 

(For now, I don't succeed in building standalone_edge and standalone_chrome.)

You don't get <rule> if you are not used to make command. Just google it!

Build on linux/amd64 device

Some people (most of Windows10 users, for example) may want to build images on your amd64 device. docker buildx build command is suitable for it. It's an experimental feature of Docker. Now buildx has been a standard feature of Docker(2021/11/12). Just type docker buildx build instead of docker build.

https://docs.docker.com/build/buildx/docs.docker.com

I'm sorry but I won't write the detail here, because it is too much information to do here. I can make another article about it.

Brief Instruction:

  1. Enable Docker experimental features.
  2. Create a new builder by running docker buildx create --use.
  3. Replace docker build to docker buildx build in Makefile.
  4. Run VERSION=local NAME=yourname BUILD_ARGS="--platform linux/arm64 --push" make <rule>.