Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

Magma Calculator での計算をターミナル上で行う

Last updated at Posted at 2022-02-02

概要

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> => &lt;textarea&gt; になる
sed -i  -e '/<textarea name="output"/,/<\/textarea/ s/&/\&amp;/' \
        -e '/<textarea name="output"/,/<\/textarea/ s/>/\&gt;/' \
        -e '/<textarea name="output"/,/<\/textarea/ s/</\&lt;/' $resfile
# &lt;textarea&gt; => <textarea> に戻す
sed -i -E 's/&lt;(\/?textarea[^&]*)&gt;/<\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/&lt;/</g' -e 's/&gt;/>/g' -e 's/&amp;/\&/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 とすればこのファイルが実行される

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?