AOJの自動化ツールを作った
概要
Aizu Online Judge (AOJ) を快適に楽しむためのツールを作成し、公開した。
背景
私事だが4月からソフトウェアエンジニアとして某企業で働くことになったので、以前取り組んでいたオンラインジャッジをもう一度やろうかと考えていた。
しかし、以前オンラインジャッジをやっていた際に、単純作業を繰り返していたのが苦だった。 C++を使用していたので、プログラムが書けたら一度コンパイルし、入力データを標準入力から手入力(もしくはテキストファイルに書き出してリダイレクト)し、間違っていたらまたコンパイルからやり直し...といった具合だった。
この一連の作業が煩わしいんだよなぁと思いながら AOJ のサイトを眺めていた折、APIが公開されていることに気が付いた。
この API を使えば割といい感じに自動化できるのでは?と思い、ツールを作成した。 実装は Go で行なった。理由はまだ Go をちゃんと書いたことがなかったということと、クロスコンパイルすればどの OS でも動いてくれる(可能性が高い)からである。
aojtool
拙作 aojtool は先述した面倒な作業をコマンド一発で実行できる。 例えばこの問題のサンプルを実行したい場合、
$ aojtool run ITP1_1_c rectangle.cpp
のようにして実行できる。 提出したい時も
$ aojtool submit TP1_1_c rectangle.cpp
で提出でき、結果の確認も
$ aojtool status
で可能である。
実装
CLI フレームワークとして cobra を用いた。 サブコマンドやオプション等をいい感じに扱ってくれる大変便利なものである。
また API クライアントのアーキテクチャはこちらの記事を参考にした。 詳細は当該記事を参照されたし。
type Client struct { Endpoint *url.URL UserAgent string httpClient *http.Client cookieJar *cookiejar.Jar Auth *AuthService Submit *SubmitService Status *StatusService Test *TestService }
Client
からサービスオブジェクトを参照して各種処理を実行する。
こうすることで Client
のコードが肥大化することを防ぐ。
また、go の net/http/cookiejar
を用いることで cookie をいい感じに扱ってくれる。
この cookie をシリアライズして書き出したり読み出すことによってログインを保持している。
まとめ
AOJ を快適に楽しむためのツールを作った。
テストが全くなかったり、エラー処理(特に http リクエスト関連)がかなり雑なので、気が向いたら直していきたい。
よかったら go get
して使ってみてください。
追記
- 2019-02-18: 色々直した。テストケースを保存したりしたい。
PNGあぶり出し
Introduction
インターネットに溢れている画像ファイルには、適当なテキストを隠しておくことができる。 PNG画像のバイナリをいじりながら、画像にメッセージを埋め込んでみる。
PNGの仕様
PNG(Portable Network Graphics)は可逆な画像圧縮フォーマットである。 仕様の詳細はこちら。 PNGは8バイトのシグネチャといくつかのチャンクから構成されている。
チャンクにはいくつかの種類があるが、どのチャンクもチャンクサイズ、チャンクの種類、実際のデータ、チャンクのCRCを持っている。 チャンクの種類は画像の幅や高さを含むIHDRや、画像の終端を示すIENDといったものがある。 CRCはデータ破損などを検出するためのものだが、詳しい説明は割愛する。
チャンクを覗く
実際にPythonを使ってPNG画像からチャンクを見てみよう。
画像はいつものを使わせていただく。
まずチャンクを表すクラスを定義する。
Python3.7で追加されたData Classes
が便利なので、これを使う。
Data Classes
は__init__()
などを自動的に生成してくれるので大変使い勝手が良い。
変数の型も宣言すると補完が効いたりして気持ちがいいので、今回は積極的に使っていく。
from dataclasses import dataclass @dataclass class Chunk: length: int type: str data: bytes
次に、与えられたファイルのチャンクのジェネレータを返す関数を定義する。
f.read(size)
メソッドでsize
バイトを読み込むことができる。
また、PNGはビッグエンディアンであることにも注意する。
def read_chunks(fname: str): with open(fname, 'rb') as f: # シグネチャを出力 print(f'signature: {f.read(8)}') while True: length = int.from_bytes(f.read(4), 'big') type = f.read(4).decode() data = f.read(length) yield Chunk(length, type, data) # 画像の終端ならば終了 if type == 'IEND': break # CRCの4バイトをスキップ f.seek(4, 1) for chunk in read_chunks('Lenna_text.png'): print(f'chunk {chunk.type} ({chunk.length} bytes)')
これを実行すると、次のような出力が得られる。
signature: b'\x89PNG\r\n\x1a\n' chunk IHDR (13 bytes) chunk sRGB (1 bytes) chunk IDAT (473761 bytes) chunk IEND (0 bytes)
PNGのシグネチャは\x89PNG\r\n\x1a\n
であり、IHDRというチャンクには13バイトのデータがあることがわかる。
IHDRチャンクには画像の幅や高さ(4バイト)、ビット深度やカラータイプ(1バイト)などが含まれているので、これを出力してみよう。
1度に複数のバイト列を解釈するにはstruct
モジュールが便利だ。
import struct for chunk in read_chunks('Lenna_text.png'): print(f'chunk {chunk.type} ({chunk.length} bytes)') if chunk.type == 'IHDR': width, height, bit_depth, color_type = \ struct.unpack_from('>IIBB', chunk.data) print(f'width: {width}, height: {height}, bit depth: {bit_depth}, ' f'color type: {color_type}')
struct.unpack_from()
は与えられたフォーマットに沿ってアンパックしてくれる。
>
はビッグエンディアン、I
、B
はそれぞれunsigned int
(4バイト)、unsigned char
(1バイト)を表す。
これを実行すると次のように出力される。
signature: b'\x89PNG\r\n\x1a\n' chunk IHDR (13 bytes) width: 512, height: 512, bit depth: 8, color type: 2 chunk sRGB (1 bytes) chunk IDAT (473761 bytes) chunk IEND (0 bytes)
画像の高さと幅が512pxであることがわかる。 その他細かい仕様は各自調べてみて欲しい。
秘密のテキスト
次はPNG画像にテキスト情報を埋め込む。
PNGには補助チャンクとしてtEXtチャンクがあり、テキストを保持することができる。
tEXtチャンクはキーワードとテキストをnull文字で結合したものを格納できる。
まず、先ほどのChunk
クラスを継承したTextChunk
クラスを作る。
@dataclass class TextChunk(Chunk): def __init__(self, keyword: str, text: str): data_str = keyword + '\0' + text self.data = data_str.encode() self.length = len(self.data) self.type = 'tEXt'
このクラスの__init__()
は自動生成されるものではなく、新たに定義されたものが呼び出される。
次にChunk
をファイルに書き出す際にバイト列に変換する必要があるため、to_bytes()
メソッドを定義する。
from binascii import crc32 @dataclass class Chunk: # 省略 def to_bytes(self) -> bytes: # それぞれをバイト列に変換 length = self.length.to_bytes(4, 'big') type = self.type.encode() data = self.data crc = crc32(type + data).to_bytes(4, 'big') return length + type + data + crc
CRCはチャンクの種類とデータを用いて計算される。
今回は組込みのbinascii
モジュールを使って済ませてしまった。
最後に、これらのクラスを用いて、新しいPNGファイルを書き出す。
tEXtチャンクはIHDRチャンクとIENDチャンクの間にあればどこでもよい。
keyword = 'comment' text = 'nanrakano message' text_chunk = TextChunk(keyword, text) # 元画像のチャンクを取得 chunks = list(read_chunks('Lenna.png')) with open('Lenna_text.png', 'wb') as f: # シグネチャを出力 f.write(b'\x89PNG\r\n\x1a\n') # IHDRチャンクを出力 f.write(chunks[0].to_bytes()) # tEXtチャンクを出力 f.write(text_chunk.to_bytes()) # 残りのチャンクを出力 for chunk in chunks[1:]: f.write(chunk.to_bytes())
このようにして下の画像にテキスト(サンプルコードとは異なる)を埋め込んだので、ダウンロードしてメッセージを読んでもらえると嬉しい。
tEXtチャンクのテキストを読むために少しだけコードを追加する。
for chunk in read_chunks('Lenna.png'): print(f'chunk {chunk.type} ({chunk.length} bytes)') if chunk.type == 'IHDR': # 省略 elif chunk.type == 'tEXt': keyword, text = chunk.data.decode().split('\0') print(f'{keyword}: {text}')