HaLake Magazine

コワーキングスペースHaLakeの技術情報発信サイト!IoT,AR,VRなど最新技術情報をお届け!

Scratch × Python 組み合わせて学ぶプログラミング

前の記事でScratch3.0の拡張機能として新しいブロックを制作してPythonサーバとFirmataを挟んでArduinoを制御しました。 しかし、いろいろな機能を盛り込みすぎたせいで、きなり難しくなっているようなイメージでした。今回は新しく同じようなブロックを追加したうえで、そこからサーバを挟んでPythonのプログラムを実行できるようにしてみたいと思います。

f:id:mischief_cat:20210704130111p:plain

目次

  1. プログラム
    ・Python サーバ側
    ・Scratch サーバ側
  2. 追加したブロックの説明と実行例
    ・パスワード時判定
    ・1~nの総和
    ・謎のブロック
  3. まとめ



  • 注意:前回の記事通りにScratchの拡張機能を作成していることを前提に話しますので、合わせて読んで頂けると同じ操作で問題なく動作します。

magazine.halake.com

前回作成したブロックに加えて今回作成するブロックが既に入っているスクラッチを誰でも起動できるようにしてあります。自分で作るのと細かい説明は軽く読んで理解するのは後回しにして以下のURLから最初に試してみるのもいいかもしれません。前回の記事に載せたものと同じURLですがどうぞ。

m-shinoda.github.io

拡張機能の有効化は以前の記事で紹介しているので説明は省きます。

  • 注意:上記のURLでスクラッチを起動して動作確認する際は以下のPythonサーバ側プログラムを同じPC内で実行しておく必要があるので注意してください。

最初にまずは試したい!という方は以下に示す項と手順で進めることによってほんの少しだけ説明を省略して試すことが出来ます。

  1. 上記のこちらが用意したScratchをURLから起動
  2. python サーバ側pythonコードをコピペ&実行のみ*必要に応じてモジュールのインストール)
  3. 追加したブロックの説明と実行例を呼んで進める

以上で簡単に試すことが出来ます。

プログラム

Python サーバ側

前回の記事でモジュールをインストールしていなければ以下を実行してください。

pip install requests

まずはPythonのサーバプログラムです。

from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse as p
import requests

def trim(path):
    url_n_par = p.urlparse(path).query

    pre_text = path.replace('/?URL=', '')
    par_pre = p.urlparse(pre_text).query
    par = p.parse_qs(par_pre)
    params = {k: par[k][0] for k in par}
    
    URL_pre = path.replace('/?' + par_pre, '')
    URL_pre = p.urlparse(URL_pre).query
    URL_pre_d = p.parse_qs(URL_pre)
    URL = URL_pre_d['URL'][0]

    return params, URL

class Server(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.send_header('Access-Control-Allow-Origin', '*')#これがないとリクエストに答えられない。js側エラー内容( No 'Access-Control-Allow-Origin' header is present on the requested resource.)
            self.end_headers()
            
            params, URL = trim(self.path)
            result = None#念のため一度Noneで初期化
            
            res = requests.get(URL)#URLにGETする

            func_text = '''{}'''.format(res.text)#三連引用符を使うことで複数行の文を実行可能にする
            print(func_text)#Gist更新が遅い時があるのでどんなコードを実行するか確認
            
            print()
            d = {}
            exec(func_text)#Gistのプログラムを実行、第3引数locals()でもdict型の変数でも大丈夫
            result = locals()['func'](params)
            # result = d['func'](paramater)#第3引数dict型変数の場合、()がとれて少しだけわかりやすい
            print(result)
            print()

            self.wfile.write(str(result).encode('utf-8'))#Scratchへの値渡し用

        except Exception as e:  
            print("An error occured")
            print("The information of error is as following")
            print(type(e))
            print(e.args)
            print(e)
            print()
            
def run(server_class=HTTPServer, handler_class=Server, server_name='', port=8000):

    server = server_class((server_name, port), handler_class)
    server.serve_forever()

print('start')
run(server_name='', port=8000)

実行する際は単純にコマンドプロンプトで保存したディレクトリまで移動して

python つけた名前.pyで大丈夫です。

例:python exec_gist.py

サーバの大部分は前の記事と大して変わっていません。

しかし、今回はこのプログラム自体は大きく書き換えることなく、別のところに置いてあるPythonのプログラムをブロックから呼び出せるようにしてあります。

それを実現するのが組み込み関数のexec関数です。

同じような関数としてeval関数がありますが、今回の用途には使いづらかったのでexec関数を使いました。

参考にさせて頂いた記事を以下に載せておきます。

qiita.com

このサーバプログラムから実行するプログラムはGitHubのサービスの一つであるGistに実行したいプログラムを載せておきます。

Gistの細かい説明は省きますが、プログラムを共有するためのものです。

使い方は簡単で、

gist.github.com

↑URLを開いて、コードとファイル名を書いたら右下緑色のボタンを押すと保存されます。この時create secret gist create public gistの2つを選ぶことが出来ます。

URLを知っている人以外から見られたくない場合は前者にしてください。

後者は誰でも見られるような設定になります。

f:id:mischief_cat:20210509115644p:plain

Git Hubのアカウントと連携していない場合はURLを忘れると探しようがないので気を付けてください。

面倒な方は後述するURLを使うことによって、こちらが用意したものを使うこともできます。その説明は適所で行います。

Gistのプログラムを2種類用意しました。

パスワード判定

gist.github.com

1~nの総和

gist.github.com

Gistでは関数を定義して、exec関数でその関数を読み込み、実行するような仕組みにしています。

Scratch サーバ側

次はScratchの拡張機能の編集です。

下記のファイルの内容をすべて消して、下のプログラムを全コピぺしてください。

scratch-vm\src\extensions\scratch3_halakeblocks\index.js

const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');
const nets = require('nets');

/**
 * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const blockIconURI = 'data:image/png;base64,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';

/**
 * Icon svg to be displayed in the category menu, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const menuIconURI = 'data:image/png;base64,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';


/**
 * Class for the new blocks in Scratch 3.0
 * @param {Runtime} runtime - the runtime instantiating this block package.
 * @constructor
 */
class Scratch3HaLakeBlocks {
    constructor (runtime) {
        /**
         * The runtime instantiating this block package.
         * @type {Runtime}
         */
        this.runtime = runtime;

        //this._onTargetCreated = this._onTargetCreated.bind(this);
        //this.runtime.on('targetWasCreated', this._onTargetCreated);
    }


    /**
     * @returns {object} metadata for this extension and its blocks.
     */
    getInfo () {
        return {
            id: 'halakeblocks',
            name: 'HaLake Blocks',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                {
                    opcode: 'fetchURL',
                    blockType: BlockType.COMMAND,
                    text: '[TEXT] にアクセスする',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "http://192.168.x.xx:8000/?ON=0"
                        }
                    }
                },
                {
                    opcode: 'reqURL',
                    blockType: BlockType.REPORTER,
                    text: '[URL] にアクセスして値を取ってくる',
                    arguments: {
                        URL: {
                            type: ArgumentType.STRING,
                            defaultValue: "http://192.168.x.xx:8000/?READ=0"
                        }
                    }
                },
                {
                    opcode: 'reqGistPython',
                    blockType: BlockType.REPORTER,
                    text: ' [URL]',
                    arguments: {
                        URL: {
                            type: ArgumentType.STRING,
                            defaultValue: "https://gist.github.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1/raw/?param=asdfghjk"
                        }
                    }
                },
                {
                    opcode: 'paramValue',
                    blockType: BlockType.REPORTER,
                    text: '  [onlyURL] /? [parNAME] = [parVAL] & [parPLUS]',
                    arguments: {
                        onlyURL: {
                            type: ArgumentType.STRING,
                            defaultValue: "https://gist.github.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1/raw"
                        },
                        parNAME: {
                            type: ArgumentType.STRING,
                            defaultValue: "param"
                        },
                        parVAL: {
                            type: ArgumentType.STRING,
                            defaultValue: "hogehoge"
                        },
                        parPLUS: {
                            type: ArgumentType.STRING,
                            defaultValue: " "
                        }
                    }
                },
                {
                    opcode: 'paramValuePlus',
                    blockType: BlockType.REPORTER,
                    text: '[parNAME] = [parVAL] & [parPLUS]',
                    arguments: {
                        parNAME: {
                            type: ArgumentType.STRING,
                            defaultValue: "param"
                        },
                        parVAL: {
                            type: ArgumentType.STRING,
                            defaultValue: "hogehoge"
                        },
                        parPLUS: {
                            type: ArgumentType.STRING,
                            defaultValue: " "
                        }
                    }
                },
                {
                    opcode: 'encode',
                    blockType: BlockType.REPORTER,
                    text: '[text]をエンコード',
                    arguments: {
                        text: {
                            type: ArgumentType.STRING,
                            defaultValue: "文字"
                        }
                    }
                }
            ],
            menus: {
            }
        };
    }

    /**
     * Fetch URL.
     * @param {object} args - the block arguments.
     * @property {number} TEXT - the text.
     */
    fetchURL (args) {
        const text = Cast.toString(args.TEXT);
        fetch(text,
        {
            method: 'GET',
            mode: 'no-cors'
        });
    }

    /**
     * Request URL
     * @property {number} URL
     * @return {number}
     */
    reqURL (args){
        const ajaxPromise = new Promise(resolve => {
            nets({
                url: Cast.toString(args.URL)
            }, function(err, res, body){
                resolve(body);
               return body;
            });
        });
        return ajaxPromise;
    }

    reqGistPython (args){
        const ajaxPromise = new Promise(resolve => {
            nets({
                url: "http://localhost:8000/?URL=" + Cast.toString(args.URL)//localhostを環境に応じて書き換えが必要
            }, function(err, res, body){
                resolve(body);
               return body;
            });
        });
        return ajaxPromise;
    }

    paramValue (args){
        compURL =  args.onlyURL + "/?" + args.parNAME + "=" + args.parVAL + "&" + args.parPLUS
        console.log(compURL)
        const ajaxPromise = new Promise(resolve => {
            nets({
                url: "http://localhost:8000/?URL=" + Cast.toString(compURL)//localhostを環境に応じて書き換えが必要
            }, function(err, res, body){
                resolve(body);
               return body;
            });
        });
        return ajaxPromise;
    }

    paramValuePlus (args){
        return Cast.toString(args.parNAME + "=" + args.parVAL + "&" + args.parPLUS)
    }

    encode (args){
        return encodeURIComponent(args.text)
    }
}

module.exports = Scratch3HaLakeBlocks;

「追加した部分を書き加えてください」となると、各自の環境でミスが多発しそうなのですべて書き換えてください。

追加した内容を以下に示します。

{
    opcode: 'reqGistPython',
    blockType: BlockType.REPORTER,
    text: ' [URL]',
    arguments: {
        URL: {
            type: ArgumentType.STRING,
            defaultValue: "https://gist.github.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1/raw/?param=asdfghjk"
        }
    }
},
{
    opcode: 'paramValue',
    blockType: BlockType.REPORTER,
    text: '  [onlyURL] /? [parNAME] = [parVAL] & [parPLUS]',
    arguments: {
        onlyURL: {
            type: ArgumentType.STRING,
            defaultValue: "https://gist.github.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1/raw"
        },
        parNAME: {
            type: ArgumentType.STRING,
            defaultValue: "param"
        },
        parVAL: {
            type: ArgumentType.STRING,
            defaultValue: "hogehoge"
        },
        parPLUS: {
            type: ArgumentType.STRING,
            defaultValue: " "
        }
    }
},
{
    opcode: 'paramValuePlus',
    blockType: BlockType.REPORTER,
    text: '[parNAME] = [parVAL] & [parPLUS]',
    arguments: {
        parNAME: {
            type: ArgumentType.STRING,
            defaultValue: "param"
        },
        parVAL: {
            type: ArgumentType.STRING,
            defaultValue: "hogehoge"
        },
        parPLUS: {
            type: ArgumentType.STRING,
            defaultValue: " "
        }
    }
},
{
    opcode: 'encode',
    blockType: BlockType.REPORTER,
    text: '[text]をエンコード',
    arguments: {
        text: {
            type: ArgumentType.STRING,
            defaultValue: "文字"
        }
    }
}
reqGistPython (args){
    const ajaxPromise = new Promise(resolve => {
        nets({
            url: "http://localhost:8000/?URL=" + Cast.toString(args.URL)//localhostを環境に応じて書き換えが必要
        }, function(err, res, body){
            resolve(body);
            return body;
        });
    });
    return ajaxPromise;
}

paramValue (args){
    compURL =  args.onlyURL + "/?" + args.parNAME + "=" + args.parVAL + "&" + args.parPLUS
    console.log(compURL)
    const ajaxPromise = new Promise(resolve => {
        nets({
            url: "http://localhost:8000/?URL=" + Cast.toString(compURL)//localhostを環境に応じて書き換えが必要
        }, function(err, res, body){
            resolve(body);
            return body;
        });
    });
    return ajaxPromise;
}

paramValuePlus (args){
    return Cast.toString(args.parNAME + "=" + args.parVAL + "&" + args.parPLUS)
}

encode (args){
    return encodeURIComponent(args.text)
}

以上の二か所になり追加したブロック分プログラムが増えています。

追加したブロックの説明と実行例

今回追加したブロックは以下の3つになります。

画像と対応して上から順に

  1. GistのPythonを起動するブロック
  2. 1のブロックにパラメータを追加するプロック
  3. URLエンコードするブロック

f:id:mischief_cat:20210509162142p:plain

f:id:mischief_cat:20210704095756p:plain ブロックの表示幅を変えられなくて一部見切れていますが、今回の追加分です。

  • 注意:拡張機能として追加されているブロックは全部で6種類ほどになっていますが上記のものはそこの下から3つ分にあたります。そのほかの3つは以前の記事で説明されているので説明は省きます。

入力欄の説明を表で示します。

f:id:mischief_cat:20210509165314p:plain

上記図内のGistのURLに入力する例を示すと、私のGistURLの1つが

https://gist.github.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1

なのでこれに/rawを付与するとそこに保存してあるソースコードだけを送り返してくれるので/rawまでを付与したものがブロックに入力するGistのURLになります。

https://gist.githubusercontent.com/M-Shinoda/73d1b4fb557d5efa76a1405b347809f1/raw

これです。

試しに、これら2つのURLを開いてみると違いが分かると思います。

  • 注意:上記の方法だと書き換えたプログラムがすぐに更新されない(3~4分程度)ので、すぐに更新したものを使いたい場合はその都度rawボタンを押して開かれるURLをコピペして使ってください。 f:id:mischief_cat:20210509162812p:plain その都度URLが変わることと、URLが長くなってしまうので使い分けることをお勧めします。

今回追加分ブロックの一番目の物で、最初から入力されていると思いますが赤枠で囲った部分にGistのURLを入力します。

f:id:mischief_cat:20210523170325p:plain

次にこれの右側の入力欄に任意のパラメータ(クエリストリング)を入力して使います。

パラメータはPythonサーバでGistのURLと分割され、Gistに記述されたPythonのプログラムで使う変数のために用意されています。

先ほど指定したGistのURLに載せているプログラムを見ながら説明します。

パスワード判定

gist.github.com

基本書き換えないPythonサーバ側にアクセスされたら実行する関数として指定されているfunc関数をGistに置くプログラムに必ず記述します。(言い回しが難しいですが理解せず何度か実行して体で覚えるといいかもしれません)

そのfunc関数は第一引数だけ指定できるようにしておきます。これはPythonサーバ側で実行する際に決まっているのでfunc関数を定義するときはdef func(params):がほぼ定型文となります。(仮引数名は変えても問題ありませんが。)

この第一引数にはPythonサーバプログラムでparams変数が指定され実行されます。(paramsという名前が2回出てきますがGist側では仮引数でサーバプログラムでは実引数として動作しています。当たり前のことですが...)

これはdict型になっており、1や2のブロックで入力したパラメータ名とその値が対としてすべて格納されているためfunc関数内で簡単に使えるようにしてあります。

上記のパスワード判定を行うGistのプログラム2,3行目を見るとパスワードの設定とブロックで入力された値との比較を行っているので、ブロックに以下のような(ほげ ほげ/&\|=?)入力をした後、ブロックを起動すると

f:id:mischief_cat:20210523174019p:plain

f:id:mischief_cat:20210509170633p:plain

Trueと帰ってきます。これはGist側でもブロック側でもパスワード(PASSWORD)ほげ ほげ/&\|=?という値が設定されているので同じ文字列であると判定されて帰ってきます。

これを最後の入力欄をふげ ふげ/&\|=?とパラメータの値を変えるとFalseが返ってきます。

f:id:mischief_cat:20210523174243p:plain

しっかりとGistのプログラムが動いていることがわかります。

今回追加分3つ目のURLエンコードブロックは日本語や記号を使ったときに誤作動しないようにするためのブロックになります。

URLエンコードブロックを使わずに直接入力することもできますが、誤作動の原因になるので注意しましょう。

追加分の1つ目のブロックにその機能も追加しておけばいいじゃん!と思おう人もいるかもしれませんが、こういったことが原因で誤動作に繋がるというのも学んでもらいたいと思っています。

1~nの総和

gist.github.com

f:id:mischief_cat:20210704104933p:plain

赤枠のURLと青枠を好きな数字に書き換えてください。

書き換えるURLは以下に貼っておきます。

https://gist.github.com/M-Shinoda/10ee62409f1500ffec887ec9cbdec1fd

パスワード判定の時はデフォルトでそのURLが入っていましたが、今回はGistに置いてある違うプログラムを使いたかったのでURLをかきかえました。自分で用意したプログラムを使いたい方はURLとそのパラメータを自由に書き換えてください。

今回のプログラムは1~好きな数字までの総和を出力してくれるものになっていますので、青枠の書き換えた数字まで一つづつ足した数がかえってきます。

画像では10を指定しているので10までの緑枠に総和=(1+2+3+4+5+6+7+8+9+10)=55と返ってきているのがわかります。

謎のブロック

真面目にすべて読んでいる方なら気づいていいるかもしれませんが、追加したブロックで使ってないのあるじゃん!と思っているかもしれません。

今回追加したブロックの2つ目について説明します。

f:id:mischief_cat:20210704111147p:plain

追加分1つ目のブロックの一番右側に穴が空いていて使っていなかったと思います。そのところに追加して入れることを想定して作りました。

今まで使っていた方法だとGistに置いてあるプログラムに1つしか値を渡してあげることしかしていなかったのですが、これを使うことによってある意味で無限に値を渡すことができます。つまりGist内でScratchから受け取った値を複数使えることになります。

  • 注意:図中青字で説明されているように、パラメータ名は必ず同じ名前にならないようにする必要があります。通常の変数と同じで、同じ名前にしてしまうと後に入力した値に代わってしまうので思ったような動作をしなくなってしまいます。

f:id:mischief_cat:20210704115306p:plain

こんな感じに無限に増やして実行すことが出来ます。見えにくいですがそれほど追加できます。

以下に実行したプログラムとGistのURLを載せます。

gist.github.com

Scratch貼り付け用: https://gist.github.com/M-Shinoda/e7e3d52dc48b59673a982e3e59798e4d/raw

Scratch側の出力で改行して出力したかったのですが、実用性がないのと試しのプログラムなので今回は無視しています。ですがしっかりと変数が渡されていことがわかります。

動作は同じですがパラメータ名ごとにその値を取り出していることが分かり易く確認できるプログラムです。単純ですがどうぞ。

Scratchのブロックで指定したパラメータ名と対になっていることがわかるはずです。

gist.github.com

Scratch貼り付け用: https://gist.github.com/M-Shinoda/dc70ed264b02520e6da4e42f2f940fee

まとめ

  • 複雑で説明が難しくなってしまった。
  • flaskを使った方が簡単にできたと後から気づいた