プログラミングなどに関する、ひらう子のブログです

HTMLSeaをちょっとだけ実践で利用してみて

開発中のHTMLSeaですが、現在ちょっとだけ実践で活用してみています。

現在私は、Twitter連携Webサービスを個人開発中でして、そのデザインの段階において、HTMLSeaを使用しています。まず、Flaskと、開発中のHTMLSeaコンパイラモジュールを使って、HTMLSeaコードの書かれたファイルをHTMLに変換して表示するアプリを作り、それをローカルで動かしながらHTMLSeaコードを書き進めていくという感じです。そして、出来たHTMLコードに手を加えて、Webサービスの方にコピペするのです。

このように使ってみたところ、コピペの手間を差し置いても、生のHTMLなどを書くのに比べてスムーズに進められる感じでして、ああやはり開発してよかったなあと思っております。まだまだバグが多いのですが、早く完成度を上げて、オープンなPythonモジュールとして公開したいなあという気持ちでいっぱいです。

実際に使ってみると、最初に作ったテストでは想定していないバグがポロポロと出て来ます。また、「ここは言語仕様としてこうした方が良いのではないか」といったアイデアも、使いながら膨らんでいくのを感じます。そこまで高等な言語でなくとも、やはり言語を作るのは大変ですね。

まあ、今は少しずつHTMLSeaを活用しつつ、色々なWebアプリを作りながらじっくりと開発を進めていきたいと思います。

SSHを通したGit利用というのを初めて知ったという話

私は、ITエンジニアとしての業務経験はありますが、ちゃんとした開発業務の経験はありません。そんな訳で、結構そういった知識はまばらだったりします。

最近知ったのですが、GitはSSHを通して利用出来るのですね…。恥ずかしながらHTTP(S)を通したリモート機能と、あとはローカルの機能しか知りませんでした。用あってGitのリファレンスを眺めていて、初めてSSHを通してのリモート機能を知りました。

GitはGithubやHerokuでしか使ったことがなかったもので、Gitをリモート利用するには、基本的にはWebサーバを立てるしかないと思っていました。なので、割と面倒だろうなあと思っていました。

SSHを通してそのままGitを使えるということは、それこそSSHで接続可能なリモート端末にディレクトリを作ってリポジトリにしてしまえば、そのまま外から接続できますね。このブログでは何かとHTMLSeaの事ばかり書いていますが、並行してWebアプリの個人開発もやっていたりするので、Githubを介さない方が良いであろうリポジトリも結構あります。なので、とてもありがたい機能です。


まあそんなこんなで、VPS内にリポジトリを作って、Gitで簡単にWebアプリがデプロイが出来るように準備しました。ローカル端末はWindowsなので、Puttyを通してGitを使うという、まあどうやらネットで見る限りは一般的らしい方法です。

という訳で、色々な技術は「何となくかじっていて使える」という状態で済まさず、ちゃんとドキュメントを読んだ方が後々効率が良いという教訓を得ました。ははは…。

HTMLSeaを使った簡単なCMSでも作ろうかなと

現在私は、HTML代替言語であるHTMLSeaを開発中でして、製作中のPython製HTMLSeaコンパイラも、段々ちゃんと動くようになってきました。まだ例外処理などの作りこみがほとんどされていなかったりはするものの、勝手が分かった上で手元で動かすなら、そこそこ安定してHTMLSeaコードからHTMLコードを生成出来ます。

そろそろ、自分用に使って使い勝手を確かめつつ、機能を増やしていこうかと思うようになりました。そんな訳で、私が管理する「かざりのり」及び「文萌開花Webサイト」を、HTMLSeaコンパイラを使ったCMSで管理するようにしていこうかと思っています。


この二つのサイトは、基本的に静的サイトとして作られています。最初はHTMLで手打ちして作っていたのですが、前バージョンのHTMLSeaコンパイラを作った際に、手元PCにおいてmakefileを使ってHTMLSeaファイルからHTMLファイルを生成し、それをアップロードする方式に変えました。

前バージョンのHTMLSeaはかなりソースもぐちゃぐちゃで、Pythonモジュールとしての利用もまともに出来ない状態だったので、makefileを使ってコマンドラインを通して使っているのです。


さて、今回新しく作り直しているHTMLSeaコンパイラはモジュールとして使えるので、例えばflaskを利用したアプリ等からも利用できます。それなら、localhostで起動したflaskのサーバで表示を確認しつつ、HTMLSeaコードファイル群をテキストエディタで編集して、そのあと静的ファイルを出力するというやり方もできます。更新にあたっては、出力した静的ファイルをSFTP等でサーバにアップするというのが良さそうです。

本来ならGitなどでそのままアップロードしたりサーバ側でCMSを走らせるというのが楽なのでしょうが、まああんまり高機能なレンタルサーバを借りている訳ではないので…。とはいえレンタルサーバを借りてWebサイトを作っている人は未だに結構多いですし、毎日更新するサイトでもないので、とりあえずそれで良いかなと思っています。(ちなみに、PythonからFTP等を直接叩くというのは、FTPの仕様の所為もあって意外と面倒くさそうなので、今のところはやめとこうかなという感じです)

ふわりふわりとGithubを使う練習をしています

開発中の、Python製HTMLSeaのコンパイラですが、ある程度の段階でモジュールとしてソースごと公開しようと思っています。HTMLSeaは用途的にはWebアプリケーションのテンプレートエンジンとして使用するケースが想定されますし、何らかの方法でバイナリ化したコンパイラだけを公開してもあまり意味はないと思いますので。

今の時代、OSSの公開にあたってはGithubを利用するのがデファクトスタンダードと言えるでしょう。私はGitの利用経験こそあるものの、業務でがっつり利用したことはなく、チームでのGitリポジトリの利用も未経験なので、正直あまり使い勝手が分かっているとは胸を張って言えません。(業務での開発経験が少ないとこういうのがつらい)

そんな訳で、ひとまずGithubでプライベートリポジトリを作って、こちらにHTMLSeaのプロジェクトを置くことにしました。Githubに慣れていく為です。


…という感じなのですが、とりたてて何かしら書くこともないですね。会員登録して、リポジトリを作るだけです。詳しいことは検索して調べれば分かります。

ちなみにPythonのパッケージインストーラであるpipはGitリポジトリをそのまま利用できますので、自作のモジュールもGithubなどに置いておけば、どこでもpipインストールして使えます。(勿論インストールする環境にGithubへのログイン情報を入れるか、リポジトリをパブリックにする必要はあります。)

$ pip install git+≪リポジトリのURL≫

リポジトリ内にモジュールのフォルダを置き、リポジトリのトップにsetup.pyを置いて適切なスクリプトを書き込めば、それで、pipから利用出来るリポジトリが出来上がります。やはり、どこでも使えるというのはすごく便利な感じがします。まだそういう使い方をする段階にはなっていませんが…。

そんな訳で、とりあえず予定ではGithubで完全に公開出来るまで、少しずつGitとGithubの仕様に慣れていこうと思います!

PythonのコードをPythonで生成して実行してみる

Pythonでも特に有名なHTML用テンプレートエンジンと言えば、Jinja2だと思います。そのJinja2ですが、どうやらJinja2の処理系は、受け取ったテンプレートをPythonコードに変換して、コンパイルして実行しているということです。そして、その処理方法がJinja2の高速さの大きな要因だとか…。

テンプレートエンジンの処理をふつうに素直に実装するなら、「テンプレートをパースして各種オブジェクトの集合体を生成し、レンダリングの際はオブジェクトのメソッドを呼び出して、各オブジェクトのメソッドが呼び合いつつ結果を出力する」という方式が普通でしょう。
恐らく、Jinja2のようにPythonコードを生成してコンパイルしたものを呼び出してレンダリングする方式なら、テンプレート読み込み時は時間が掛かるにしても、レンダリングはかなり高速に出来そうな気がします。

そんな訳で、物は試しにそのような処理を書いてみて、処理速度を簡単に計測してみました。ちゃんとやるならJinja2のソースを読み解くべきところですが、まあ軽い実験なのでさくっとそれらしいものを…。


書いたのが次のコードです。

import io

class Node:
    def __init__(self,tagName):
        self.tagName = tagName
        self.children = []
    def appendChild(self,node):
        self.children.append(node)

    def getCodeFuncName(self):
        return "f{}".format(id(self))
    def writeFuncCode(self,codeIO):
        codeIO.write("def {}():\n".format(self.getCodeFuncName()))
        codeIO.write("  output.write('<{}>')\n".format(self.tagName))
        for child in self.children:
            codeIO.write("  {}()\n".format(child.getCodeFuncName()))
        codeIO.write("  output.write('</{}>')\n".format(self.tagName))
        
        for child in self.children:
            child.writeFuncCode(codeIO)
    def getWholeCode(self):
        codeIO = io.StringIO()
        self.writeFuncCode(codeIO)
        codeIO.write("{}()\n".format(self.getCodeFuncName()))
        return codeIO.getvalue()
    def getCompiledCode(self):
        return compile(self.getWholeCode(),"<string>","exec")

    def writeString(self,output):
        output.write('<{}>'.format(self.tagName))
        for child in self.children:
            child.writeString(output)
        output.write('</{}>'.format(self.tagName))

import time
class Timer:
    # 生成された時間を記録して、そこからの経過時間を取得できるクラス
    def __init__(self):
        self.start = time.time()
    def elapsed(self):
        return time.time()-self.start

def createNodeTree():
    root = Node("root")
    head = Node("head")
    root.appendChild(head)
    middle = Node("middle")
    root.appendChild(middle)
    foot = Node("foot")
    root.appendChild(foot)
    return root

def main():

    # まず木構造を作って、
    # Pythonコードにした文字列を取得
    # コンパイルしたバイトコードも取得
    root = createNodeTree()
    sCode = root.getWholeCode()
    bCode = root.getCompiledCode()

    # Pythonコードを表示
    print("---------code---------")
    print(sCode)
    print("---------code---------")
    print()
    
    # 計測時の繰り返し回数を定数にする
    COUNT = 100000

    output = None

    # まずはNodeクラスのメソッドで、結果をoutput書き出すプロセス
    # COUNT分、繰り返して、経過時間を表示
    t = Timer()
    for i in range(COUNT):
        output = io.StringIO()
        root.writeString(output)
    print("did node method.   time:",t.elapsed())
    # 最後の繰り返しでの結果を標準出力
    print(output.getvalue())

    # 次は、コンパイルされたバイトコードを実行し、結果を書き出すプロセス
    # COUNT分、繰り返して、経過時間を表示
    t = Timer()
    for i in range(COUNT):
        output = io.StringIO()
        exec(bCode,{"output":output})
    print("did compiled code. time:",t.elapsed())
    # 最後の繰り返しでの結果を標準出力
    print(output.getvalue())

    # 最後に、Pythonに変換した文字列をexecで実行し、結果を書き出すプロセス
    # COUNT分、繰り返して、経過時間を表示
    t = Timer()
    for i in range(COUNT):
        output = io.StringIO()
        exec(sCode,{"output":output})
    print("did string code.   time:",t.elapsed())
    # 最後の繰り返しでの結果を標準出力
    print(output.getvalue())

main()

メインの処理はmain()関数に書いて呼び出しています。

Nodeクラス

Nodeクラスは、木構造を作るノードです。子オブジェクトのリストを持っていて、writeString()メソッドを呼び出すと、引数で指定した出力ストリームに、HTMLライクな入れ子構造を書き込みます。
writeString()メソッドとは別に、getWholeCode()メソッドと、getCompiledCode()メソッドがあります。getWholeCode()は、writeString()と同等の処理を行うコード文字列を、指定した出力ストリームに書き込むメソッドです。getCompiledCode()メソッドは、writeString()メソッドによって出力されたコードをコンパイルして、バイナリコードを返すメソッドです。

compile()とexec()、あとeval()

さて、このコンパイル作業ですが、PythonにはPythonコードをやってくれる関数が組み込みで存在します。それが、compile()です。この関数でコンパイルされたバイナリコードを実際に実行できるのは、同じく組み込み関数のexec()とeval()です。
exec()もeval()も、基本的には文字列をコードとして実行する関数ですが、compile()によって生成されたバイトコードを実行することも出来ます。

exec()とeval()の違いは、eval()は式として文字列を実行して結果を返す関数で、exec()は一つのPythonスクリプトのコードを実行出来る関数です。今回はexec()の方を使います。

exec()関数は、第二引数に辞書オブジェクトを渡せば、その辞書オブジェクトをグローバル変数のスコープとして利用します。例えば、{"a":1,"b":2}という辞書を渡せば、グローバル変数にa(値は1)とb(値は2)とが存在する状態でコードを実行します。今回のコードの中では、outputというグローバル変数として、結果を出力する為のストリームをコードに渡す形にしています。

コードの根幹、main関数

main関数は、まずNodeインスタンス木構造を組み上げ、そのあと結果出力用のコード文字列と、コンパイル済みのバイトコードを生成させます。コード文字列はprintで表示します。
そして、「Nodeインスタンス群に直接結果を出力させる方式」、「生成させたバイトコードをexecで実行する方式」、「生成させたコード文字列をexecで実行する方式」の3パターンで、100000回分の時間を計測します。ちなみに、100000回のそれぞれの繰り返しの最後、出力された文字列を表示しています。


結果は以下のようになります。

---------code---------
def f5164112():
  output.write('<root>')
  f5164208()
  f9014480()
  f9078576()
  output.write('</root>')
def f5164208():
  output.write('<head>')
  output.write('</head>')
def f9014480():
  output.write('<middle>')
  output.write('</middle>')
def f9078576():
  output.write('<foot>')
  output.write('</foot>')
f5164112()

---------code---------

did node method.   time: 0.6110348701477051
<root><head></head><middle></middle><foot></foot></root>
did compiled code. time: 0.36802101135253906
<root><head></head><middle></middle><foot></foot></root>
did string code.   time: 9.005515098571777
<root><head></head><middle></middle><foot></foot></root>

先に述べたとおり、Nodeによって生成されたPythonコードが先に表示されています。execで実行されるのがこれですね。

100000回の繰り返しに掛かった時間は、以下の通りです。

  • Nodeインスタンス群による直接出力方式では0.611秒
  • 生成したコードをcompileしたものをexecする方式が0.368秒
  • コード文字列をそのままexecに渡す方式は、9.006秒

流石に、文字列をexec実行する方式のものは遅いですが、バイトコードをexec実行する方式のものは、インスタンス群に直接出力させる方式より中々速いです。やはり、インスタンス群の処理に比べて、生成したコードで行う処理はかなり単純なので、これだけの差が出るのでしょう。


割と適当に書いた実験でも、結構な速度差が確認できました。これなら、jinja2がPythonコードへの変換方式を使うのも、納得ですね。

コードを生成して実行すると聞くとかなり大変そうなイメージがありますが、実際に書いてみるとそれほど大変でもなさそうです。
私が現在制作しているHTMLSeaもHTMLを生成する為の言語ですし、特にテンプレートエンジンのように変数を使う場合スコープの管理を自前のPythonスクリプトで行うのはかなり効率が悪いので、コード生成方式を使うのはかなり有効な感じがします。高速化にあたっては、是非採用してみたいと思いました!

はてなブログ内でCodeMirrorのシンタックスハイライトを使う

前回までCodeMirrorで独自言語を扱うにあたっての使用方法を書いていましたが、実際にこのブログで、コードにハイライトを行ってみようと思います。


はてなブログでは、Wordpressなどと違って、テンプレートのHTMLを自由に変更することは出来ないようです。ですので、CodeMirrorの【codemirror.js】や【codemirror.css】へのリンクタグは、『デザイン設定』からヘッダ下パーツのHTMLを編集して埋め込むことにします。
そして、自分で定義した言語モードのスクリプトも、minifyしてヘッダに仕込みましょう。<script>タグを使って直接書き込むのが良いと思います。

次に、同じようにフッタのHTMLを編集して、CodeMirror.colorizeを起動しましょう。

<script>setTimeout(function(){CodeMirror.colorize(document.querySelectorAll('pre[data-lang="htmlsea"]'));},20);</script>

起動の為のコードはsetTimeoutで動作を20ミリ秒遅らせています。これは必ず必要という事はないのですが、一応フッタよりも下でコードが表示される場合(基本的にはありえないと思いますが)にも動作するようにという意味合いと、それとはてなブログが載せる広告のスクリプトの動作を待って動作させる為です。後者に関してはおまじない程度の意味合いですが…。

さて、デザイン設定に仕込みを入れたら、あとははてな記法でその言語ののコードを書きましょう。
コードを書く際には他の言語と同様、モード名がhtmlseaなら、「>|htmlsea|」「||<」で囲めばそれでOKです。

そうして表示されるのが、以下のコードです。モダンブラウザでJavascriptが有効になっていれば、ハイライトされて表示されると思います。
はてなブログでコード記法を行うと、preタグで囲まれて描写されます。はてなブログにとって未知の言語であれば、指定した言語名をdata-langに入れて表示することになっているようです。丁度、Codemirror.colorize()にとって扱いやすい形式になっているのです。

* $html > head > title |○○のページ
* $html > body > #wrapper
こんにちは!

↑ひとまずさくっとHTMLSeaを定義したスクリプトを使っています。完璧ではないものの、ハイライトなしで表示されるよりはかなり読みやすい表示になっているはずです。(ヘッダに仕込んだスクリプトだけに、この記事を書いた時点よりも後のバージョンのスクリプトでハイライトが行われている可能性があります)

ちなみに、フッタに仕込んだスクリプトでは、colorize関数には引数で、data-langがhtmlseaのエレメント群を取得して、渡しています。はてな記法のコード記法では、丁度data-langの設定された<pre>が表示されます。ですので、ここで引数なし起動を行うと、他の言語で書いたコード記法が巻き添えを食らうことになるのです…。


このような形で、自分でCodeMirrorで定義した言語モードを、はてなブログ内で使って自動ハイライトを行うことが出来ました。

注意点としては、はてなブログの無料版では、スマホ版表示の際には自由編集ヘッダが表示されません。ですので、ここにスクリプトを仕込んでも動かないということになります。これに関する対処としては、はてなブログスマホ版表示をしない設定を行うか、もしくは、多重起動対策をした上で記事全てにスクリプトを仕込むなどの方法が考えられます。

もう一つ注意しなければいけないのは、はてなブログにおけるはてな記法でのコード表記ですが、今後も確実に<pre data-lang="言語">という形式で提供されるかは不明だということです。ですので、仕様変更にはある程度目を光らせておく必要が出てきます。

CodeMirrorで独自言語のシンタックスハイライトを使う②

前回、CodeMirrorの導入を行いました。今回は、独自言語のハイライトの骨子です。


CodeMirrorプロジェクト内の【/mode】ディレクトリには、言語ごとにディレクトリが置いてあって、中には、ハイライトが定義されているjsファイルが置いてあります。ひとまず、【/mode/css/css.js】を見てみましょう。

CodeMirror/css.js at master · codemirror/CodeMirror · GitHub

スクリプトでは基本的に、即時関数の中で変数や関数を定義したりと色々やっているのですが、その中でも核となるのは、CodeMirror.defineMode()の実行です。(CodeMirror.defineMIMEの実行もされていますが、こちらはあくまで単純にHTML内でハイライトを行うだけなら必要ない模様です。)

CodeMirror.defineMode()は、第一引数にモード名文字列("css"など)、第二引数に、実際の言語モードの内容を記した辞書オブジェクトを取ります。【css.js】の今のバージョンにおいては、defineMode()の第二引数は辞書オブジェクトを返す即時関数で記述されていますね。

そして、辞書オブジェクトの'token'プロパティに設定された関数が、実際にハイライトを行う関数です。


'token'プロパティの関数(以下、token関数と記述します)は、CodeMirrorのスクリプトがハイライトを実際に行う際に順次呼び出されます。

第一引数streamは、色分けすべきコードを読み出したり現在の読み出し位置を前後に動かしたりできるストリームです。
第二引数stateは、token関数がハイライト作業中に何度も呼ばれるにあたって、メモとして利用できる辞書オブジェクトです。

大まかな流れを説明すると、ハイライト作業が始まると、まずCodeMirrorが、token関数が呼び出します。最初の呼び出し時点で、streamの読み出し位置はコードの最初です。

token関数は、streamの位置を調整してトークン名の文字列をreturnで返します。するとCodeMirrorは、token関数が呼ばれた時点の読み出し位置から関数がreturnした際のstreamの読み出し位置までを一つのトークンとして認識し、返されたトークン名をつけます。そして、またすぐにtoken関数が呼ばれます。その際streamの読み出し位置は、先ほどreturnした時のままですので、再度位置を調整してreturnします。この繰り返しにより、CodeMirrorはハイライトすべきトークンの解析を行います。

実際にハイライトする際には、トークン名に最初に"cm-"をつけたものが、HTML内のそのトークンの部分にクラスとして付与されますので、CSSで色を付けることで、ハイライトが行えるというわけです。

streamには、eat()やmatch()といったメソッドがあります。match()は「読み出し位置に指定した単語があれば読み出し位置を進める」というメソッドですので、調整はこちらで行うことが多いでしょう。この記事では大まかにしか説明しませんので、詳しくは、以下リンクを参照してください。

CodeMirror: User Manual

ちなみに上で述べたとおり、token関数第二引数のstateは、メモとして使えます。なので、トークン識別を行うための文脈情報をこちらに記録したりして使うことが出来ます。stateの初期状態は、defineMode関数に渡される辞書オブジェクト内の、'startState'プロパティに設定された関数の返り値が使われます。使う際には、こちらで初期状態を設定しましょう。


そんな感じでハイライトを定義するスクリプトを書いたら、あとはそれをHTML内に読み込んでやれば、CodeMirror.colorize()関数やCodeMirror()を使って、ハイライトを実際に使うことが出来ます。自分でつけたモード名文字列を指定して、これらを読み出せば、ハイライトが出来るはずです。