概要
Magmaは有償のシステムです.
しかしWebサイト上に「Magma Calculator」という無料のWebサービスがあり,そこでMagma言語のプログラムを実行することができます.
でもブラウザでプログラムをコピペしたりsubmitボタンをいちいちポチるのはめんどい!!
そこでPOSTリクエストを利用したスクレイピングによってMagma Calculatorでの計算結果を抽出し,ターミナルの画面上に出力するスクリプトを作りました.
このスクリプトを使うとローカルのMagmaファイルをコピペせずに実行することができます!
GitHubにコードを上げてもよいですが,Qiitaの限定共有が都合が良いので,コードをここに貼り付けました.
実行環境
コードはBashとPython 3の両方を用意しています.
基本的な仕様は同じですが,若干違いがあります.
- Bashではファイル名を指定せずにパイプラインを利用することができますが,Pythonでは利用できません.
- BashではPOSTリクエストで得たWebページのデータが一時ファイルに保管されます.この一時ファイルをどこに作成するかを指定する必要があります.
- Bashはnkf,curl,xmllintがインストールされていなければなりません.また,実行環境はUbuntuを想定しています.他の環境でのテストはしておりません.
- Pythonはrequests,BeautifulSoup,html5libがpipコマンドでインストールされていなければなりません.
なお,Dockerを導入している環境ならば,次章のプログラムリストにあるDockerfileで環境構築を行えます.
テストもそこまで十分になされていないので,もしかしたらバグが見つかるかもしれません.そのときは何らかの手段でご連絡いただけると幸いです.
実行例
- ファイル
test.mg
がカレントディレクトリ上にあるとする - Bashスクリプトファイルの名前は
main.sh
とする - Pythonスクリプトファイルの名前は
main.py
とする
Bashでの実行例
bash main.sh test.mg
-> 通常の使い方.送信して実行結果を得て出力
bash main.sh -p test.mg
-> 送信せずに送信内容を出力する(URLエンコード済みのものが出力される)
bash main.sh -c test.mg
-> curlで行われる標準エラー出力をターミナル上に表示する
cat test.mg | bash main.sh
-> パイプラインを利用した方法
cat - | bash main.sh
-> 送信内容を標準入力で受け付ける(Ctrl-D
で入力終了)
Pythonでの実行例
python3 main.py test.mg
-> 通常の使い方
python3 main.py --help
-> ヘルプを出力
python3 main.py --print test.mg
-> 送信せずに送信内容を出力する
python3 main.py --silent test.mg
-> 実行ログの出力を行わない
python3 main.py test.mg --output out.txt --info info.txt
-> 出力先ファイルの指定
プログラムリスト
Bashスクリプト
#!/bin/bash
# POSTリクエストを利用して,
# Magma Calculator(http://magma.maths.usyd.edu.au/calc/)
# 上で任意のMagmaプログラムを実行させて結果を出力するスクリプト
# コマンド nkf, curl, xmllint が使用可能であることを想定
# 実行させるMagmaファイルの文字コードはUTF-8であることを想定
# 改行コードはLFでもCRLFでも多分問題ない
# ***** 必要に応じて変更する部分 *****
# 一時保存ファイル置き場
tmpdir="/magma/tmp"
# 一時保存用ファイル名(パスにはしないこと!)
resfile="res.txt"
errfile="err.txt"
# このスクリプトファイルを実行するコマンドを指定(usage用)
execommand="run"
# ***** 変更部分終わり *****
# エラー文を出力する
function cerr(){
echo "Error: $1" 1>&2
}
# ヘルプ文を出力する
function print_help(){
echo "usage: $execommand [OPTIONS] filename" 1>&2
echo "options:" 1>&2
echo " -h, --help, -?: print help" 1>&2
echo " -c, --curl, --dialog: print output of curl" 1>&2
echo " -p, --print, --whatif: not execute, print info" 1>&2
}
# コマンドの存在確認
hash nkf curl xmllint
if [[ $? -ne 0 ]] ; then
cerr "install the above command(s)"
exit 1
fi
# tmpdir が存在することを確認
if [[ ! -e "${tmpdir}/" ]] ; then
cerr "directory ${tmpdir} is not prepared"
exit 1
fi
# ***変数***
# ファイル名をパスに更新
resfile="${tmpdir}/${resfile}"
errfile="${tmpdir}/${errfile}"
# 値が1なら送信内容を出力して終了する(リクエストを送らない)
flag_print=0
filename=''
# ファイル名を検出したかを管理
flag_file=0
# curlでのエラー出力を表示するかを管理
flag_curl=0
# ユーザーエージェントの指定(これがないと401エラーを返される)
ua='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'
# リクエスト先のURL
url='http://magma.maths.usyd.edu.au/calc/'
# パイプラインでファイル内容を受け取ったとき
if [[ -p /dev/stdin ]] ; then
flag_file=1
filename='-'
# cat - でパイプラインの中身を出力できる
fi
# オプション設定
for OPT in "$@" ; do
case $OPT in
-p | --print | --whatif )
# 送信内容の出力のみ行う
flag_print=1
;;
-c | --curl | --dialog )
# curlでの情報出力を行う
flag_curl=1
;;
-h | -\? | --help )
print_help
exit 0
;;
-* )
cerr "illegal option $OPT"
exit 1
;;
--* )
cerr "illegal option $OPT"
exit 1
;;
* )
if [[ $flag_file -eq 1 ]] ; then
cerr "too many arguments"
exit 1
fi
flag_file=1
filename="$OPT"
;;
esac
done
# ファイル指定の有無の確認
if [[ $flag_file -eq 0 ]] ; then
print_help
exit 1
fi
# ファイルの存在確認
if [[ $filename != '-' && ! -e $filename ]] ; then
cerr "file $filename not found"
exit 1
fi
# 行ごとにURLエンコード
function url_encode(){
while read -r line ; do
echo "$line" |
nkf -W8MQ |
sed 's/=$//g' |
tr '=' '%' |
paste -s -d '\0' - |
sed -e 's/%7E/~/g' \
-e 's/%5F/_/g' \
-e 's/%2D/-/g' \
-e 's/%2E/./g'
done
}
# 行ごとにURLエンコードして結合
# magma calculator のサイトのSource->calculator.js->160行目(submitData関数)にURLエンコードの処理がなされているのでそれにならい実装
input=$( \
cat $filename | \
sed '/^\s*$/d' | \
url_encode | \
nkf -Lu | \
sed -e ':loop' -e 'N; $!b loop' -e 's/\n/%0D%0A/g' \
)
# 送信内容の出力のみと指定したらその内容を出力して終了
if [[ $flag_print -eq 1 ]] ; then
echo $input
exit 0
fi
# POSTリクエスト
if [[ $flag_curl -eq 1 ]] ; then
curl $url -A "'$ua'" -XPOST -d "input=$input" 1> $resfile
echo -e "\n\n *** result ***\n" 1>&2
else
echo "sending..." 1>&2
curl $url -A "'$ua'" -XPOST -d "input=$input" 1> $resfile 2> /dev/null
fi
# 必要な部分以外はすべて捨てる
sed -i -n '/<table class="calculator">/,/<\/table>/p' $resfile
# textareaタグの開きタグと閉じタグの両方に対して改行文字を挿入する
sed -i -E 's/(<textarea[^>]*>|<\/textarea>)/\n\1\n/g' $resfile
# textarea内の文字列はHTMLエスケープされていないのでエスケープ処理を施す
# 下の処理はtextareaの開きタグと閉じタグが同じ行にあるとうまくいかないので上の処理にて改行文字を挿入している
# <textarea> も含めてエスケープ処理: <textarea> => <textarea> になる
sed -i -e '/<textarea name="output"/,/<\/textarea/ s/&/\&/' \
-e '/<textarea name="output"/,/<\/textarea/ s/>/\>/' \
-e '/<textarea name="output"/,/<\/textarea/ s/</\</' $resfile
# <textarea> => <textarea> に戻す
sed -i -E 's/<(\/?textarea[^&]*)>/<\1>/g' $resfile
# 内容を取得
# エラーが発生したかどうかを判定する
xmllint --html --xpath '//*[@id="warnings"]//li/text()' $resfile 1> $errfile 2> /dev/null
if [[ $? -eq 0 ]] ; then
echo -e "\033[31m *** ERROR!!! ***\033[m"
fi
# 結果(エラーが出た場合はエラー文)を出力(エスケープも戻す)
xmllint --html --xpath '//*[@id="result"]/text()' $resfile |
sed -e 's/</</g' -e 's/>/>/g' -e 's/&/\&/g'
# メタデータを出力(エラー出力)
xmllint --html --xpath '//*[@id="summary"]//li/text()' $resfile 1>&2
# 末尾に空行を追加
echo "" 1>&2
Pythonスクリプト
import sys
import re
import argparse
import requests
from bs4 import BeautifulSoup
# 次の方法でインストールする:
# pip install requests beautifulsoup4 html5lib
parser = argparse.ArgumentParser(description = 'desc')
parser.add_argument('filename', type = str, help = '送信するプログラムファイル')
parser.add_argument('--print', '-p', action = 'store_true', help = '送信内容(URLエンコード済み)を出力して終了する')
parser.add_argument('--silent', '-s', action = 'store_true', help = 'ログを表示しない')
parser.add_argument('--output', '-o', type = str, help = '出力先のファイル名を指定')
parser.add_argument('--info', '-i', type = str, help = '実行時間・使用メモリなどの情報の出力先を指定')
args = parser.parse_args()
# ユーザーエージェントの指定(これがないと401エラーを返される)
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'
# リクエスト先のURL
url = 'http://magma.maths.usyd.edu.au/calc/'
try:
f = open(args.filename, 'r')
except Exception as e:
print(
"ファイルの読み込みに失敗しました:",
e,
sep = "\n",
file = sys.stderr
)
exit(1)
data = [s.strip() for s in f.readlines() if s.strip()]
f.close()
instr = "\n".join(data)
if args.print:
print(instr)
exit(0)
headers = {'User-Agent': ua}
param = {'input': instr}
if not args.silent:
print("sending...\n", file = sys.stderr)
# 自動でURLエンコードがなされる
try:
res = requests.post(url, headers = headers, params = param)
except Exception as e:
print(
"POST送信に失敗しました",
e,
sep = "\n",
file = sys.stderr
)
exit(1)
content = res.text
# こちらを利用するという手段もあるがテストはしていない
# soup = BeautifulSoup(content, 'html.parser')
soup = BeautifulSoup(content, 'html5lib')
warn = soup.select('#warnings li')
if len(warn) > 0:
print(' *** ERROR ***\n', file = sys.stderr)
def validate_path(path: str):
if re.search(r'[\\/:*?"<>|]', path) != None:
print(f"ファイルパス {path} は無効です", file = sys.stderr)
exit(1)
if args.output == None:
fout = sys.stdout
else:
path = args.output
validate_path(path)
fout = open(path, mode = "a", encoding = "utf-8")
result = soup.select('#result')[0]
print(result.string, file = fout)
fout.close()
if args.info == None:
finfo = sys.stderr
else:
path = args.info
validate_path(path)
finfo = open(path, mode = "a", encoding = "utf-8")
if not args.silent:
meta = soup.select('#summary li')
for m in meta:
print(m.string, file = finfo)
finfo.close()
環境構築用Dockerfile
- 環境構築のみ(スクリプトファイルのコピーは別ファイルで行う)
# 実行環境を構築(実行ファイルは別で用意する)
FROM ubuntu:latest
# 使用するディレクトリを追加
RUN mkdir /magma /magma/bin /magma/src /magma/tmp
# 作業ディレクトリを設定
WORKDIR /magma/src
# パスを通す
ENV PATH=/magma/bin:$PATH
# apt install で入力を要求させない
ARG DEBIAN_FRONTEND=noninteractive
# 必要なパッケージをインストールする
# 日本語入力環境,vim,python3,pipはおまけ
RUN apt update
RUN apt install python3 curl vim locales nkf libxml2-utils -y
RUN apt install pip -y
# 日本語入力を可能にする
RUN locale-gen ja_JP.UTF-8
# 起動時に設定されるエイリアスを定義
RUN echo "export LANG=ja_JP.UTF-8" >> ~/.bashrc
RUN echo "alias python='python3'" >> ~/.bashrc
# 使い方
# 0. カレントディレクトリにこのDockerfile-v1,Dockerfile-v2,main.shがあることを確認&dockerが使用可能であることも確認
# 1. "docker build -t magma:1.0 -f Dockerfile-v1 ." を実行
# 2. "docker build -t magma:latest -f Dockerfile-v2 ." を実行
# 3. "docker run -it --rm -v "$PWD":/magma/src magma:latest" を実行してコンテナ作成
# alias magma='docker run -it -rm -v "$PWD":/magma/src magma:latest' としてエイリアスを作成するのもよい
# そのコンテナの中では run コマンドで main.sh を実行できる
# Docker が使用可能な環境でなくても,bash, nkf, curl, xmllintが使用可能ならOK
# 環境構築はこのファイルを参考に行えばよい
- スクリプトファイルのコピー用
# 実行ファイルを設定
FROM magma:1.0
# bash の実行ファイルを指定
COPY ./main.sh /magma/bin/run
RUN chmod +x /magma/bin/run
# python の実行ファイルを指定
COPY ./main.py /magma/bin/main.py
RUN pip install requests beautifulsoup4 html5lib
RUN echo "alias py='python /magma/bin/main.py'" >> ~/.bashrc
# パスは通してあるので単に run とすればこのファイルが実行される