概要
私が開発を進めている終盤問題ジェネレーター(仮、以下ジェネレーターと略)の開発に関する記事です。
最初の記事はこちら。
今回はコード圧縮についてです。
例の如く、ITを本職とされている方々からすると気になる部分などあるかもしれませんが、素人の戯言でございますのでご容赦ください。
目的
他の言語とWeb開発系の言語(JavaScript, CSS, HTML)で大きく異なる点の一つは、ユーザーが(.exeファイルのようなバイナリではなく)ソースコードを受け取るところにあります。
このため、以下のような問題が生じます。
- 識別子名の命名法によって、ファイルサイズが大きく変化する。
- ユーザーに
私のヘタクソな書き方のコードがバレてしまう。
こういった理由から、「ファイルサイズを小さくしてトラフィックの負荷を軽減する」「コードを読みにくくする」といったことをする必要が出てきます。
実際、Web系のソースコードは圧縮や難読化といった処理をされてから公開されることが多いようです。
実際の処理Lv1
デバッグを容易にするために、識別子名は読んだだけでその機能が分かるようなものが望ましいです。
ただそうすると、どうしても長くなりがちです。
例えば以下の関数は、問題情報の入ったオブジェクトを2次元配列から取り出す関数です。
それぞれの機能を分かるように書こうとすると、(私の場合)どうしてもこうなってしまいます。
function LFFetchHistoryPosition(LVSelectedProblemIndex, LVSelectedMoveIndex) {
return LVHistory[LVSelectedProblemIndex][LVSelectedMoveIndex].concat();
//適当にコメントを書いておく
}
そこでClosure Compilerを使います。
Closure CompilerはGoogleが開発しているJavaScriptのコンパイラーです。
一般にコンパイラーというと、以下のような変換を実行するものをイメージされることが多いかと思います。
- C++ソースコード → 実行ファイル
- Javaソースコード → Jarファイル
ですがClosure Compilerは「JavaScriptソースコード → JavaScriptソースコード」の変換を行ってくれます。
この時、無駄に長い変数名を短縮してくれます。
また一部の処理についても機能そのままでコードサイズを最適化してくれるようです(便利!)。
以下にコマンドラインでの使用例を示します。
java -jar closure-compiler/closure-compiler-v20190325.jar --js non_compressed.js --js_output_file compressedLv1.js
このようにすると、以下のようなコードが生成されます。
コメントは自動で消してくれます。
function LFFetchHistoryPosition(a,b){return LVHistory[a][b].concat()}
この時注意したいのは、デフォルトでは基本的にローカル識別子(関数内でのみ有効な識別子)のみが処理される点です。
JavaScriptは別のファイル(HTML等)から読み込まれることが多いですので、その時に関数名の不一致などが起きないようにという意図でしょう。
実際の処理Lv2
私はさらに関数名やグローバル変数まで圧縮したかったのですが、残念ながら良さげなツールを見つけることはできませんでした。
そこで簡易なテキスト置換スクリプトを書き、それを実行することで関数名も圧縮しようと考えました。
# coding:utf-8
import sys
import os
import csv
if sys.version_info[0]==3:
import urllib.parse
elif sys.version_info[0]==2:
reload(sys)
import urllib
sys.setdefaultencoding('utf-8')
#argv 1=target file, 2=template file as csv.
#template item = template string, abbreviation string
templateFile = open(os.path.abspath(sys.argv[2]), mode='r')
templateLists = csv.reader(templateFile, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
destinationFile = open(os.path.abspath(sys.argv[1]), mode='rb')
destinationText = destinationFile.read()
destinationFile.close()
destinationText = destinationText.decode()
for row in templateLists:
# print(row[0] + " -> " + row[1])
destinationText = destinationText.replace(row[0],row[1])
destinationText = destinationText.encode()
destinationFile = open(os.path.abspath(sys.argv[1]), mode='wb')
destinationFile.write(destinationText)
destinationFile.close()
templateFile.close()
このスクリプトを実行すると、まずテンプレートファイル(CSV)を読み込みます。
ターゲットのファイルを対象として検索を行い、テンプレートの各行1列目の文字列を2列目の文字列で置換します。
このツールの注意点は以下の通りです。
- いろんなところに出てくる単語("history"など)がテンプレート1列目にあると誤置換が発生しますので、識別子の命名の段階でちょっとした工夫をします。
具体的には、簡単な接頭辞("LV"や"LF"など)をつけておくことで、識別子をユニークにします。
長いソースコード中で検索をかけた時に、"History"だとHistoryを含むたくさんの識別子がヒットしますが、"LVHistory"であれば1種類の識別子に絞ることができます。 - HTMLから呼び出す関数を置換した場合は、それを呼び出しているHTMLにも同じ置換処理を実行する必要があります。
後述するコマンドをHTMLにも適用すればOKです。
以下にコマンドラインでの使用例を示します。
python abbreviate.py target_file.js template.csv
...
LFFetchHistoryPosition,a0
LVHistory,a1
...
先ほどのLv1の圧縮済みファイルをこのスクリプトでさらに処理すると、以下のような結果が得られます。
function a0(a,b){return a1[a][b].concat()}
これでほぼほぼ圧縮は完了です。
実際の処理Lv3
Lv1とLv2の処理を行うのに、毎回コマンドを手打ちするのは少々骨が折れます。
ですので私はGNU Makeで処理しました。
依存ファイルにコンパイル前のファイルとテンプレートファイルの両方を指定することで、どちらかに変更があった場合にmakeすると一連の処理が自動で実行されます。
compressed.js : non_compressed.js template.csv
java -jar closure-compiler/closure-compiler-v20190325.jar --js non_compressed.js --js_output_file compressed.js
python abbreviate.py compressed.js template.csv
ここまで環境構築が完了したら、後はもりもりコードを書いてmakeするだけです!
自動で最小化されたコードが生成されます。
一例として、元々49402バイトだったファイルをこの方法で処理すると、14600バイトまで軽くなりました。
まとめ
Closure Compilerと簡単なpythonスクリプトの組み合わせにより、コードの効率的圧縮を実現しました。
この処理は、私が開発しているA4-games (スマホ向けサイト)のソース圧縮に使用しています。
長らく駄文にお付き合いいただきありがとうございました。