Python & Serverless FrameworkでLambdaファンクションをサクッと作ってデプロイする
Serverless FrameworkはAmazon AWSの開発&管理を補助するフレームワークです。 最近アルバイト先でserverlessを用いたアプリケーションに関わり、その際に使いかたを軽く勉強したので、ブログに書き残しておきます。
環境
- Node.js 10.6.0
- Python 2.7.15
- serverless 1.29.2
Installation
ServerlessはNode.jsのアプリケーションとして配布されており、npm
コマンドからインストールできます。
npm install -g serverless # 確認 serverless --help sls --help # slsという短縮コマンドが自動でエイリアスされるらしい
サービスを作ってデプロイ
インストールが完了したので、簡易的なLambdaのファンクションを作成してAWSにデプロイするところまでやってみましょう。 今回は毎朝8時にLINE Notificationを送ってくれる目覚ましファンクションを作ってみます。
サービスの新規作成
sls create
コマンドでサービスを新規作成できます。
このとき-t
オプションでアプリのテンプレートを、-p
オプションでサービスを保存するパスを指定できます。
テンプレートはaws-python
*1、保存先は適当な作業ディレクトリ上のlambdaMezamashi
というディレクトリにします。
cd /path/to/working/dir #適当な作業ディレクトリに移動 sls create -t aws-python -p lambdaMezamashi cd lambdaMezamashi ls # => handler.py serverless.yml
コマンドを打つと、lambdaMezamashi
というディレクトリが作成され、handler.py
とserverless.yml
という2つのファイルが自動生成されます。
handler.py
はLambdaのファンクションを書くファイルで、serverless.yml
は設定ファイルになります。
とりあえず動かす
アプリが完成したのでとりあえず動かしてみます。
テンプレートにはhello
という関数名で簡単なメッセージを返す関数が定義されています。
sls invoke local -f {関数名}
で指定した関数名の関数をローカル環境で実行できます。
sls invoke local -f hello # => { # => "body": "{\"input\": {}, \"message\": \"Go Serverless v1.0! Your function executed successfully!\"}", "statusCode": 200 # => }
LINE Notificationを送る関数を記述
LINE Notificationの使いかたはこちらの記事でも解説しています。
まず、LINE Notifyのページからアクセストークンを取得し、環境変数LINE_NOTIFY_TOKEN
に書き入れます。
export LINE_NOTIFY_TOKEN=XXXXXXXXX # bash zsh等の場合 set -x LINE_NOTIFY_TOKEN XXXXXXXXX # fishの場合
次に以下のようにhandler.py
を書き換え、LINE NotifyのAPIを叩くコードを追加します。
# -*- coding: utf-8 -*- import os import json import requests def mezamashi(event, context): output_url = "https://notify-api.line.me/api/notify" token = os.getenv("LINE_NOTIFY_TOKEN", "") headers = {"Authorization" : "Bearer "+ token} payload = {"message" : "こんにちは"} response = requests.post(output_url ,headers = headers ,params=payload) return response.json() def hello(event, context): body = { "message": "Go Serverless v1.0! Your function executed successfully!", "input": event } response = { "statusCode": 200, "body": json.dumps(body) } return response
次にpip
コマンドでrequests
をインストールします。
pip install requests
次にserverless.yml
を書き換え、mezamashi
関数に関数名を割り当てます。
service: lambdaMezamashi provider: name: aws runtime: python2.7 environment: # LINE Notifyのアクセストークンを記入 # invoke localで実行する場合はこれを記述しなくても動作するが、デプロイ時に必要となる LINE_NOTIFY_TOKEN: XXXXXXXXXXXXXXXX functions: hello: handler: handler.hello mezamashi: handler: handler.mezamashi
そうしたら、serverless invoke local
を用いて動作確認を行います。
sls invoke local -f mezamashi # => { # => "status": 200, # => "message": "ok" # => } # (LINEに通知が来る)
スケジュール実行の設定
Lambdaファンクションの実装が完成したので、これをスケジュール実行するように設定を書きます。
serverless.yml
のfunctions
の部分にイベント設定を追記するだけです。
functions: #<中略> mezamashi: handler: handler.mezamashi events: # イベントの設定を記述 - schedule: cron(0 23 * * ? *) # cron式で日本時間の朝8時を指定
ついでにAPI Gatewayも使う
LambdaはAPI Gatewayと組み合わせることでwebアプリのバックエンドとして使用するケースもよくあります。 少し脱線しますが、ymlを用いてLambdaファンクションをAPI Gatewayと結びつける方法も説明しておきます。
events
の項目に- http: {メソッド} {APIのURI}
の形式で記述します。
こうすることでデプロイした際に自動でAPI Gatewayが作成されます。
functions: hello: handler: handler.hello events: - http: GET hello
オフラインでテストする
ファンクションが書けたので早速デプロイと行きたいところですが、その前にローカルホストでLambdaファンクションをホストして、APIのテストを行う方法を解説します。 これは、業務でデバッグやAPIのテストコードを走らせる場合にも必要なことなので覚えておいて損はありません。
まず、必要なプラグインをインストールします。インストールはsls plugin install -n {プラグイン名}
で行います。どうやらこのコマンドはnpm i --save-dev
をラップしてnode_moduleをインストールしつつ、serverless.yml
にプラグイン情報を追記してくれるものらしいです。
sls plugin install -n serverless-offline npm install serverless-offline-python --save-dev # slsコマンドからインストールできないためnpmを直接使う
次にserverless.yml
の末尾に以下のように書き込んでserverlessとプラグインを紐付けます*2。
plugins: - serverless-offline - serverless-offline-python
serverless-offline
とserverless-offline-python
をインストールしたことで、serverless offline
というコマンドが使えるようになります。これによりローカル環境でcurlコマンド等を用いてAPIのデバッグが可能になります。
sls offline #=> Serverless: Starting Offline: dev/ap-northeast-1. #=> (中略) #=> Serverless: Offline listening on http://localhost:3000 # =====他のterminal窓から以下を実行===== curl localhost:3000/hello #=> helloファンクションが動く
ちなみにserverless-offline-scheduler
というオフライン環境でスケジュール実行のデバッグができるプラグインがありますが、Node.jsで書いたものしか動かないみたいです*3。
デプロイする
最後にデプロイの方法を説明します*4。
AWSアカウントの設定
最初にAWSアカウントの設定をします。
AWSのマネジメントコンソールにログインし、IAMから新規ユーザーを追加します。
ユーザー名を適当に設定し、「プログラムによるアクセス」にチェックを入れます。
「既存のポリシーを直接アタッチ」から「AdministratorAccess」を選択します*5 *6。
ユーザーの作成が完了したら、「アクセスキーID」と「シークレットアクセスキー」を控えておきます。シークレットアクセスキーはこのときにしか確認できないため、忘れた場合には再度アカウントを作成する必要があります。
これを、sls config credentials
コマンドを用いて設定します。XXXXXの箇所は適宜アクセスキー、アクセスキーシークレットに書き換えてください。
sls config credentials --provider aws --key XXXXX --secret XXXXX
デプロイ、実行、削除
アカウントの設定が済んだら遂にデプロイです。
Serverless Frameworkはsls deploy
コマンドを実行するだけで簡単にデプロイできるようになっています。
しかし、今回のようにPythonで実装している場合は、自分で書いたコードだけでなくサードパーティ製のライブラリも一緒にデプロイする必要があります。
serverless-python-requirements
というプラグインをインストールするとデプロイ時に自動でこの作業をやってくれるため、今回はこのプラグインを利用することにします。
sls plugin install -n serverless-python-requirements # プラグインのインストール pip freeze > requirements.txt # 依存関係をrequirements.txtというファイルに書き出す
上記のコマンドを実行したら、デプロイします。
sls deploy #=> Serverless: Installing requirements of requirements.txt in .serverless... #=> Serverless: Packaging service... #=> (中略) #=> service: lambdaMezamashi #=> stage: dev #=> region: ap-northeast-1 #=> stack: lambdaMezamashi-dev #=> api keys: #=> None #=> endpoints: #=> GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello #=> functions: #=> hello: lambdaMezamashi-dev-hello #=> mezamashi: lambdaMezamashi-dev-mezamashi
これで完成です。毎朝8時にLINEに通知が来るようになりますし、https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello
のURIにcurl
コマンドやAjax通信でアクセスするとhello
ファンクションをWeb APIとして叩くことができます。
デプロイしたアプリの削除も簡単に行うことができ、sls remove
コマンドを実行するだけです。
sls remove #=> Serverless: Getting all objects in S3 bucket... #=> Serverless: Removing objects in S3 bucket... #=> Serverless: Removing Stack... #=> Serverless: Checking Stack removal progress... #=> ............................. #=> Serverless: Stack removal finished...
さいごに
今回はServerless Frameworkの使い方を軽く説明しました。 Serverless Frameworkは他にもDynamoDBやS3といったサービスを操作することができ、その名の通りサーバレスなwebアプリケーションを構築することができます。
S3でwebページをホストし、Lambda & API Gatewayでバックエンドの処理を行い、DynamoDBやCognitoでユーザ情報を管理するという構築にすればどんなWebアプリでも作れそうですね。
参考資料
- 今から始めるServerless Frameworkで簡単Lambda開発環境の構築 | Developers.IO
- Rate または Cron を使用したスケジュール式 - AWS Lambda
- Serverless - AWS Guide
*1:was-python3だとローカルで動かす際にpyenv等を用いてver 3.6のPython環境を用意する必要があります。今回は面倒なので2.x系で我慢
*2:# sls plugin ~~ コマンドを走らせた際に自動で追記してくれるため、serverless-offline-python以外は不要かも
*3:実際にソースコードを確認したところ、Lambdaファンクションが記述されたjsファイルをrequireで読み込んで実行するという実装がされているようだった https://github.com/ajmath/serverless-offline-scheduler
*4:これ以降の部分で解説するコマンドを実行するとLambdaファンクションがAPI Gatewayを通して全世界から実行可能な状態になります。Lambdaファンクションに脆弱性が存在した場合には思わぬ被害を被る可能性があることを心に止めておいてください。
*5:公式ドキュメントでもAdmin権限を設定する(https://serverless.com/framework/docs/providers/aws/guide/credentials/)という解説されています。
*6:Serverless Frameworkは開発中でやれることがどんどん増えています。そのためAdmin権限でないと機能に制限がかかり、開発が面倒になる可能性があります。Amdin権限で開発しつつ、製品を公開する際に権限を必要最低限に絞るような運用が望ましいと思われます。
ほとんどのエンジニアには解けるらしいパズルを解いてみた
ジャバ・ザ・ハットリ氏作成の「ほとんどのエンジニアには解けるパズル」*1シリーズを全問解いたので感想とかwrite upとかを書きます。
出題者のブログはこちら
問題一覧はこちら(全8問)
目次
感想
問題はマイルドなCTFみたいでした。CTFはakictfやksnctfの問題をちまちま解いたり、SECCONの過去問を漁ったりしていたので、こうした形式のパズルには多少慣れていました。ここに上げたサイトの問題と比較すれば「ほとんどのエンジニアには解けるパズル」は簡単な部類なのかなと思います。
解くにあたってcurl
コマンドの基礎を学ぶことができたのは収穫でした。もともとcurl
コマンドに対する理解は「-O
コマンド付けてファイルを落とすやつでしょ」ぐらいだったので、一般的なサーバにリクエストを投げるという使い方を確認することができました。
その意味では、最初の3問は有意義でしたが、第4問以降は単なる数学パズルの色合いが濃く、学習という観点ではそこまで得るものはなかったのかもしれません。ただ、数学パズルやCTFの練習問題としては結構楽しかったと感じています。
Write up(ネタバレ注意)
復習、忘備録を兼ねてここに解法を残して置こうと思います。ここから先はネタバレが含まれているため、自力で解きたい方は読まないようにお願いします。
実のところ問題作成者はネタバレ非推奨と発言しているため、ここに解法を書くのはあまり褒められた行為ではないのかもしれません。しかし、curl
コマンドの使い方等の技術的な知識は、
- 「未知の知識が必要な場面が生じる」
- 「使い方を調査する≒答えを見る」
- 「実際に使う」
- 「次回以降マニュアルを見ずに使えるようになる」
というプロセスを踏んで覚えるものであると私は思います。3の後に後戻りせず4に到達できるのであれば、2で閲覧する答えが解説書であっても、info curl
の出力であっても、本記事のネタバレであっても本日的には変わらないのでは?むしろ、答えが参照しやすくなっている方が2の段階でつまずくことがなくなっていいのでは?という考えからここに解法を載せることにしました(一応ボタンを押さないと表示されないようにはしておきます)。
解説はHTTP通信などをほとんど知らない人が読んでもふんわりと理解できる程度には詳しく書くように心がけました。解いているうちに詰まってしまったら読んでみるのもいいのかもしれません。curlの使用例を知りたいという人も解説を見ながら第1問を解いてみるのもいいかもしれません。
第1問
第2問
第2問以降は暇があったら書きます(第1問程度の文量で書くと結構長くなりそうなので)。
*1:○○なら誰でもできるっていう釣りタイトル個人的にはきらい
AWS Lambdaを使ってAmazonコインを安く買う
先日AWS Lambdaを初めて使ったので忘備録を兼ねて書きます
今回書いたコードはここで見れます
Amazonコインの価格変動
1年ぐらい前からHearthstoneというデジタルカードゲームにハマっています。 Hearthstoneは課金でカードパックを買えるのですが、Amazonアプリストア版のAndroidアプリを使用すると、Amazonコインでの支払いが可能で、クレジットカードで直接買うよりより少し安くなります。
そんなAmazonコインですが、実は日々値段が変動しているみたいです。
Amazonコインの価格の変動・推移を調べてみる2018-2 | JJゃもの部屋 \(^o^)/ 破滅に向かって2011
↑の記事を見た感じ、10000コインが8100円のときにまとめ買いしておくとおトクなようです。
Amazonコインが安い日を見逃さないために、今回はpythonでwebスクレイピングを行いコインの価格を自動で追跡するスクリプトを作成しました。また、そのスクリプトをAWS Lambdaで自動化しました。
Amazonコインの価格を自動で取得する
PythonでAmazonコインの価格を追うスクリプトを書きました。
まず、venv
を用いて仮想環境を作成し、必要となるライブラリをインストールします。
$ python3 -m venv amazoncoin $ cd amazoncoin $ source bin/activate # bash zsh等の場合 $ source bin/activate.fish # fish shellの場合 (amazoncoin)$ pip install requests beautifulsoup4 (amazoncoin)$ pip freeze > requirements.txt #必要なライブラリを書き出しておく(あとで使う)
10000Amazonコインの販売ページは https://www.amazon.co.jp/dp/B00KQVXDW0/ であり、価格はid=priceblock_ourprice
のspan
タグに記載されているので、以下のようなスクリプトで価格を取得できます。*1
import requests from bs4 import BeautifulSoup def find_price(): coin_url = "https://www.amazon.co.jp/dp/B00KQVXDW0/" resp = requests.get(coin_url, timeout=1) soup = BeautifulSoup(resp.text, 'html.parser') target = soup.findAll('span',{'id':"priceblock_ourprice"}) return int(target[0].text[2:].replace(',', '')) print(find_price())#=> 8100など
価格をLine Notifyで通知する
次に、取得した価格をLine Notifyを使って自分のLINEアカウントに通知することにしました。 [参考] https://qiita.com/hiro-abe/items/e42f857bd6b40bc178a3
下準備として、LINE Notifyのwebページでアクセストークンを取得し、環境変数LINE_NOTIFY_TOKEN
に値を書き入れておきます。
$ export LINE_NOTIFY_TOKEN=XXXXXXXXX # bash zsh等の場合 $ set -x LINE_NOTIFY_TOKEN XXXXXXXXX # fishの場合
そして、以下のスクリプトを実行するとLINEに通知が届きます。
import requests from bs4 import BeautifulSoup import os def lambda_handler(event, context): output_url = "https://notify-api.line.me/api/notify" token = os.getenv("LINE_NOTIFY_TOKEN", "") price = find_price() headers = {"Authorization" : "Bearer "+ token} message = '現在10000 Amazonコインは' + str(price) + '円です' payload = {"message" : message} r = requests.post(output_url ,headers = headers ,params=payload) return 'Notify Amazon Coin Price is running' lambda_handler(None, None) # => LINEの指定したトークルームに通知が来る
これで、script.lambda_handler関数を実行することで、Amazonコインの価格がLINEに届くようになりました。
AWS Lambdaで毎日自動で動かす
上記のスクリプトをASW Lambdaを用いて自動化します。 Lambdaを用いて自動化します。今回は毎朝8時に実行させるようにします。
スクリプトのパッケージ化
最初に作成したスクリプトをzipにまとめます。Lambdaは使用するスクリプトをzip形式でアップロードする方式になっているためです。*2 このとき注意しなければならないのは、標準ライブラリ以外のライブラリも一緒にzipにまとめる必要がある点です。
デプロイ用のディレクトリを作成し、そこに今回のスクリプトとライブラリをすべて設置し、zipにまとめることにします。
mkdir deploy #デプロイ用ディレクトリを作成 cp script.py deploy #今回作ったスクリプトをデプロイ用ディレクトリにコピー pip3 install -r requirements.txt -t deploy/ #-tオプションでデプロイ用ディレクトリにライブラリをインストール cd deploy/ zip -r package.zip * #パッケージを作成 mv package.zip ../
これで、作業ディレクトリにpackage.zipが作成されます。 動作確認として、以下のようなコマンドを実行したらスクリプトが正しく動作することを確認してください。
(amazoncoin) $ deactivate #仮想環境が有効になっている場合のみ $ cd deploy/ $ python3 -c "import script;script.lambda_handler(None, None)" # > Notify Amazon Coin Price is running # (LINEに通知が来る)
AWS Lambdaの設定
パッケージが完成したので、早速Lambdaのセッティングをしてみます。
まず、Lambdaの管理ページから新しい関数を作成します。 ロールは「テンプレートから新しいロールを作成」を選択し、「Basic Edge Lambdaアクセス権限」を使用します。他は適当に入力してかまいません。
次に、先程作成したpackage.zipをLambdaにアップロードします。
このとき、ハンドラには[pythonのファイル名].lambda_handler
を指定します。
そして、環境変数の欄にLINE Notifyのアクセストークンを入力します。
最後にタイムアウトの値を少し大きめの値に変更しておきます。*3メモリは最小の128mbで足りるのでデフォルトのままでかまいません。
イベントタイミングの指定
次に通知が来るタイミングを指定します。
トリガーにCloudWatch Eventsを指定します。
ルール名と説明は適当に埋め、ルールタイプを「スケジュール式」、
スケジュール式をcron(0 23 * * ? *)
を指定します。
これで毎日グリニッジ標準時の23時、つまり日本時間の8時にスクリプトが実行されるようになりました。
おわりに
今回はpythonを用いてwebスクレイピングと、AWS Lambdaによる自動化を行いました。
今回、個人的につまずいた点は以下のとおりです。
- ライブラリをzipに含めないとライブラリの関数が使えない点*4
- イベントのトリガーに何を選べばいいのかわからない点
今後の課題として、価格がn円以下のときのみ通知を流すようにしたり、TwitterやDiscordに自動投稿させたりなんかをやりたいと思っています。
今回のプロジェクトはこちらのリポジトリで管理してあります。よかったらforkしてみてください。また、改善点などあればissueに書いてくださると助かります。
2019/01/07 追記
このアプリはしばらく前から放置しており、コードが古くなっています。 urllib3 v.1.23 及び requests v2.20.0 は脆弱性が発見されているようなので、requirements.txtをそのままコピペしないようご注意ください。
東京大学eeic3年後期実験「大規模ソフトウェアを手探る」2016年度まとめ
東大生がOSS開発やってみた…みたいな記事
「大規模ソフトウェアを手探る」って?
「大規模ソフトウェアを手探る」は、東京大学工学部電気電子/電子情報工学科(eeic)で開講されている、学部3年生向けの実験科目です。
ネット上に公開されているOSS(オープンソース・ソフトウェア)をひとつ選んで、それを改造するという内容の講義です。
2~3人でチームを組んで、数万〜数十万行のプログラムに体当たりで改造を試みることで、以下のようなことを体験するのがこの講義の狙いです。
- 全容を把握できない程度に大きなプログラムを扱う
- 他人が書いたコードに触れる
- デバッガの扱い方を学ぶ
- 先生やTA、他の学生からフィードバックをもらい、それを開発に活かす
- チームでの開発を体験する
また、この実験では、成果物をブログ形式で公開することで、レポートの代替として認められています。 ネット上には「インストールしてみた」「ライブラリを使ってみた」系の記事がたくさんあり、後を追う誰かの助けになっています。 我々が実験を通して得た知見も、誰かの役に立つのではないか?ということで、ブログ形式のレポートを認めているようです。
この記事では、そんなレポートの数々を一覧にしてみました。
OSS開発に興味がある方、学科選びに悩んでいる一二年生、そしてどの実験を受講しようか迷っている未来のeeic民の一助となれば幸いです。
目次
連載形式の記事については、初回あるいは目次のページを取り上げています。
コンパイラ・言語
gcc
結局なにをどう変更したのか(まとめ) - 27++'s Report
内容
- 生成ファイルに自動で名前を付ける
- 末尾の
;
を補完する
Python その1
Python3.x系でprint命令文を追加する話. - Qiita
Pythonにプライベート変数を実装しようと試みた話。 - Qiita
内容
- Python3.x系でもprint文を使えるようにする
- private変数を実装する
Python その2
Pythonを改造してみた はじめに - 開拓馬’s blog
内容
テキストエディタ
Vim
Vimにnanoみたいなコマンドチートシートを表示した話 - チョコの包み紙の裏
内容
- コマンドチートシートを表示する
nano
nanoを手探ってみる#1: ビルドする - meloidae’s blog
内容
- tabキーでファイル名を補完する
- 起動後でもnanorcのリロードをできるようにする
- 雪を降らせる
ブラウザ
FireFox
内容
- url欄の文字を隠す
- 右クリックからタブの複製を行う
- 新規タブを開くとクリップボードの内容を自動で検索する
- タブを複数選択して一気に閉じる機能を付ける
Chromium
Chromiumを手探った#1 - Chromiumをビルドしよう - しゅわしゅわdedede。
内容
- 新規タブで表示されるページをユーザが設定できるようにする
OpenTween
内容
- お気に入り一覧を登録順にする
- 同内容のつぶやき(パクツイ)を検索する
- フォロー/フォロワーの変化を可視化する
- 時間制限付きのミュート機能を追加する
ユーティリティ
Libre Office
LibreOfficeを手探る - libsoft’s blog
内容
- spaceを入力すると下付き文字モードが解除されるようにする
Rhythmbox
Rhythmboxで1曲ループを実現する −0− - かるぅあみるく(仮)
内容
- 一曲ループ機能を追加する
GIMP
GIMPの履歴機能に制限を加えてみた - team-cの日記
内容
- 変更履歴の数が増えすぎてフリーズする問題を防ぐ
まとめは以上になります。
昨年度にこの講義を受講されたid:SWIMATH2さんが、15年度のまとめ記事を作成してくださっています。
東京大学eeic3年後期実験「大規模ソフトウェアを手探る」2015年度まとめ - クフでダローバルな日記
こちら記事に「来年以降のeeicの後輩たちがまたまとめてくれるのを楽しみにしています。」とあったので、大変僭越ながら便乗させていただきました。
17年度以降も、後輩たちがまとめてくれるのを楽しみにしています。
Mac bookにBasicTeXを導入してみた
先日Mac BookのOSを再インストールしたのでTeXを入れ直し、ついでにLuaLaTeXに乗り換えた話をします。
BasicTeXを入れる
Mac用のTeX環境として、MacTeXというものがあります。 とりあえずこれをインストールすればMacでTeXが使えるようになるのですが、インストーラのサイズが2.8Gもあり、ダウンロードに1時間程度かかって面倒です。 そのうえあまり使わないGUIアプリがついてきてLaunchpadが汚染されます。 *1
私の場合、Emacsのorg-modeやPandocを使ってMarkdownをTeXに変換するのが主な利用法なので最低限のコマンドが使えれば十分です。
そこで最小限の機能だけ持ったBasicTeXを入れることにしました。
MacTeXのサイトからBasicTeX.pkgをダウンロードします。 BasicTeX.pkgを起動しインストーラの指示に従ってインストールします。
インストールが完了すると/usr/local/texlive/
に2016bacis
(数字はバージョンによって異なる)というディレクトリが作成されます。 *2
必要なパッケージを入れる
BasicTeXは本当に最小限で日本語環境が使えないので必要なパッケージを入れます。
ターミナルで以下のコマンドを実行します。
% sudo tlmgr update --self —all % sudo tlmgr install collection-langjapanese
ちなみにtlmgr
はTeXLive managerの略だそうです。 *3
collection-langjapanese
にはluatex-ja
というパッケージが入っており、LuaTeXを日本語で使えるようになります。
ghostscript
、ImageMagick
を入れる
次にghostscript
とImageMagick
をインストールします。これらは画像処理やpdf関連の処理を行うプログラムで、TeXをpdfに出力する際に使われるそうです。
brew install ghostscript brew install imagemagick
インストールにはhomebrewを使用しました。「homebrewって何?」「brewコマンドが使えない」って人は"homebrew"で検索してみてください。
LuaLaTeXで書いてみた
LuaTeXの詳細はTeX Wikiの説明に丸投げします。ざっくりまとめるとLuaというプログラミング言語とLaTeXを混ぜたシステムです。
フォントの指定がわりと楽にできたり、Unicode文字に対応していたりとLaTeXより便利な点がいくつかあるのですが、 一番の利点はdviファイルを経由せずにpdf出力ができる点です。 *4
% platex report.tex % dvipdf report.dvi #LaTeXではこんな感じにやっていたのが % lualatex report #LuaTeXだとこれでOK
とりあえずluatex-ja Wikiにあるサンプルを出力してみました。
\documentclass{ltjsarticle} \usepackage{luatexja} % ltjclasses, ltjsclasses を使うときはこの行不要 \begin{document} \section{はじめてのLua\TeX-ja} ちゃんと日本語が出るかな? \subsection{出たかな?} 長い文章を入力するとちゃんと右端のところで折り返されるかな? 大丈夫そうな気がするけど.ちょっと不安だけど何事も挑戦だよね. \end{document}
上記をtest.tex
の名称で保存しpdf出力してみます。
% lualatex test.tex : : ! LaTex Error: File `filehook.sty’ not found.
どうやらfilehook.sty
が必要なのにインストールされていないのでエラーが発生したようです。tlmgr
を使ってfilehook
をインストールします。
% sudo tlmgr install filehook
これに限らず、hoge.sty not found.
というエラーが発生した場合、sudo tlmgr install hoge
でスタイルファイルをインストールする必要があります。
何度か「実行」→「エラー」→「スタイルファイルを入れる」を繰り返すと出力が成功します。
画像が表示されない場合
以降はLuaTeXを使い始めたばかりのときに躓いた部分を紹介します。同じような失敗をしている人の参考になれば幸いです。
LaTeXで画像を使用する場合、ヘッダ部分に
\usepackage[dvipdfmx]{graphicx}
と記述します。
一方でLuaLaTeXで画像を使用する場合、ドライバ指定オプションは不要です。
\usepackage{graphicx} %LuaTeXではこう
ここでドライバ指定を付けたまま出力すると、画像の部分が白抜き状態になります。
\documentclass{ltjsarticle} \usepackage{luatexja} \usepackage{graphicx} %[dvipdfmx]は不要 \begin{document} \section{はじめてのLua\TeX-ja} ちゃんと画像が出るかな? \begin{figure}[h] \includegraphics[width=10cm]{test.jpg} \caption{Lenna} \end{figure} \end{document}
オプションを付けたままだとこんな感じ
筆者はネットから拾ってきたLaTeXのサンプルコードを何も考えずにコピペした結果、この症状に見舞われ数時間無駄にしました。
脳死コピペ、ダメゼッタイ
Pythonを改造してみた プロンプトをうるさくしてみた
前回まではPythonに新たな予約語を追加してきました。Pythonは与えられたコードをASTなどの中間データに変換しながら、最終的には擬似的なバイトコードを生成しceval.c
で処理するという方式を取っていることがわかりました。
しかし、この記事では文法関係の話はまったくしません。というのも、Python改造に取り組むきっかけとなった講義でgdbの使い方を習ったのですが、私達はデバッガまったく使わずにここまでの改変をやってきたのです。せっかくなのでデバッガを使ってコードを読み解きたいと思います。
gdbでPythonを追う
ビルドまでの経緯はこちら
Emacs上でM-x gud-gdb
を入力しデバッガを起動します。
main
にブレークポイントを置いて引数を入れずに(つまりインタラクティブモードが起動するはず)run
します。
すると、Programs/python.c
が開きました。
そのままnext
とstep
を連打していくと、おおよそ、以下のような動きをすることがわかりました。
Modules/main.c
に移動し、各種初期化処理を行うPython/getopt.c
内のサブルーチンでオプションを受け取るmain.c
に戻り、オプションに従ってフラグを切り替える- 引数からソースコードの名前を取得、存在しない場合、インタラクティブモードを開始するフラグを建てる
Python/pythonrun.c
のIntaractiveloop()
関数に来る- プロンプト">>> "が表示される
上記の流れを見た上で、やることを決めました。
メッセージの書き換え
Pythonをファイル名を指定せずに実行すると、インタラクティブモードが起動します。インタラクティブモードを終了したい場合は、ctrl+Dを入力するか、exit()
関数、またはquit()
関数を実行する必要があります。この仕様がちょっと不親切で終了しようとしてexit
と入力するとUse exit() or Ctrl-D (i.e. EOF) to exit
と表示され、終了してくれません。 *1
exit
と入力すれば終了できるようにするのは複雑な例外処理を行う必要がありそうなので仕方ないのかもしれません。しかし、終了の仕方を説明していないのも不親切だと私は思います。「消し方が分からない、やめ方が分からない」ソフトはユーザーをイライラされるものだと思います。
そこで、Python起動時に表示されるメッセージに「exit()
やquit()
と入力すると終了できます」という説明文を付け足すことにしました。
サクッと完成
grepで該当部分の起動時のメッセージを検索したところ、main.c
内でそれらしい文字列が#define
されているのを発見。書き換えます。
#define COPYRIGHT \ "Type \"help()\", \"copyright\", \"credits\" or \"license()\" " \ "for more information.\nType \"exit()\" or \"quit()\" or Ctrl-D to exit."
ついでに、"help"、"license"の後ろに括弧を付けました。 *2 ビルドして実行したところ、メッセージが変化しました。
% ~/mypython/bin/python3 Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux Type "help()", "copyright", "credits" or "license()" for more information. Type "exit()" or "quit()" or Ctrl-D to exit. >>>
ほんとにちょっとした変更ですが、多少は使いやすくなったのかな……?
オプションを追加 おもしろ機能
-a
や--hoge
のようなハイフンから始まるオプションはCUIアプリにとって欠かせないものです。
オプションを受け取る部分の処理はgetopt.c
ようなので、ちょっと書き足して新しい予約語を足してみようと思います。
それでは追加するオプションにはどんな機能を付けましょうか。先程と同じ文字列の変更なら簡単にできそうです。プロンプトの">>> "を適当に他の記号に変えてみることにしました。
そんなわけで、Pythonの名前にちなんで--spam
オプションを付けるとプロンプトが">>> "から"SPAM>> "に変わるという機能を作ってみることにしました。 *3
spamオプションの追加
getopt.c
に--spam
オプション用の処理部分を追加します。ハイフン2つ+単語のオプションには--help
、--version
が存在するので、それを真似します。
else if (wcscmp(argv[_PyOS_optind], L"--version") == 0) { ++_PyOS_optind; return 'V'; } #if SPAM_MODE else if (wcscmp(argv[_PyOS_optind], L"--spam") == 0) { ++_PyOS_optind; return 'P'; } #endif
ヘルプ表示、バージョン表示は-h
-V
でもできます。どうやら--help
を受け取った場合、内部で-h
に変換するという方式を取っているようなのでspamにもアルファベット一字のオプションを割り当てることにします。spamの頭文字である-s
、-S
はすでに使われているのでs"p"amの-P
を割り当てました。
ビルドして実行したところ、--spam
オプションを付けても"Unknown option"エラーが発生しませんでした。
% ~/mypython/bin/python3 --hoge #存在しないオプションを付けるとエラーを起こす Unknown option: -- <<略>> % ~/mypython/bin/python3 --spam #普通に起動した Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> % ~/mypython/bin/python3 -P #普通に起動した Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>>
spamモードの追加
main.c
に--spam
オプションを受け取ったときの処理を付け加えます。プロンプトの表示はPython/pythonrun.c
で行われるのでspamフラグが立ったことを伝えなければなりません。とりあえずグローバル領域でフラグ変数を定義して伝えることにしました。
/* command line options */ #if SPAM_MODE #define BASE_OPTS L"bBc:dEhiIJm:OqRsStuvVPW:xX:?" int Active_spam_mode = 0; #else #define BASE_OPTS L"bBc:dEhiIJm:OqRsStuvVW:xX:?" #endif : : while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) { if (c == 'c') { : switch(c){ : case 'V': version++; break; case 'P': Active_spam_mode++; break;
pythonrun.c
でPyUnicode_FromString()
関数の引数に">>> "という文字列を与えているのを発見。適当に他の文字列にしてビルドしてみるとプロンプトが変わりました。どうやらこの部分でプロンプトの文字を定義しているようなので、フラグが立っているときだけ"SPAM>> "を与えるように変更します。
#if SPAM_MODE extern int Active_spam_mode;//main.cで定義したグローバル変数をextern char* spam_message()//あとで拡張できるように関数にしておく { return "SPAM>> "; } #endif : : #if SPAM_MODE if(Active_spam_mode) { _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(spam_message())); } else { _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(">>> ")); } #else _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(">>> ")); #endif
実行してみます。
% ~/mypython/bin/python3 Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> >>> % ~/mypython/bin/python3 --spam Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> SPAM>> print("やったぜ。") やったぜ。 SPAM>>
プロンプトがが変わりました!spamモード実装完了です!
文字列をランダムに表示させる
ただプロンプトが変わるだけでは物足りないので、複数パターンのプロンプトがランダムに表示されるように変更してみます。
どうやら標準ライブラリをインクルードしても問題なくビルドできるようなのでstdlib.h
とtime.h
を用いた一番簡単な乱数生成を使うことにします。 *4
プロンプトのレパートリーは適当に5~6個用意しました。
//time.h stdlib.hのインクルード #if SPAM_MODE #include <stdlib.h> #include <time.h> #endif #if SPAM_MODE extern int Active_spam_mode; char* spam_message() { int c; static int f; if(!f){ f = 1; srand((unsigned int)time(NULL)); } c = (int)random()%10; switch(c){ case 0: case 1: return " _____ _____ __ __ \n / ____| __ \\ /\\ | \\/ |\n| (___ | |__) / \\ | \\ / |\n \\___ \\| ___/ /\\ \\ | |\\/| |\n ____) | | / ____ \\| | | |\n|_____/|_| /_/ \\_\\_| |_>> "; case 2: case 3: case 4: return "spam>> "; case 5: case 6: return "SPAM>> "; case 7: return "\n\n _____ _ _ _____ ___ ________ _ _ _ _ \n|_ _| | | | ___| | \\/ | ___| \\ | | | | |\n | | | |_| | |__ | . . | |__ | \\| | | | |\n | | | _ | __| | |\\/| | __|| . ` | | | |\n | | | | | | |___ | | | | |___| |\\ | |_| |\n \\_/ \\_| |_\\____/ \\_| |_\\____/\\_| \\_/\\___/\n==============================================\n\n1.egg bacon and spam\n2.egg bacon sausage and spam\n3.spam bacon sausage and spam\n4. "; case 8: return "\\(^o^)/SPAM\\(^o^)/SPAM\\(^o^)/>> "; case 9: return "Nobody expects the Spanish Inquisition!\nOur two weapons are fear and surprise...and ruthless efficiency...\nOur THREE weapons are fear, surprise... "; default: return ">>> "; } } #endif
プロンプトがうるさいインタラクティブモード
ビルドして実行してみました。
こんな感じでプロンプトがうるさくなります
スパムのスケッチに登場する食堂、メニューを書き足してみようみたいなイメージ
もはやスパムとは無関係なネタ、スペイン宗教裁判も
実は乱数の初期化がうまく行っておらず毎回同じ順番でプロンプトが登場したり、Macだとアスキーアートが崩れたり、粗がありますが完成ということで……
Pythonを改造してみた 排他的論理のブール演算子作った
Pythonでは、ブール演算で排他的論理を表現する場合、以下のような書き方があります。*1
#and or not演算子を組み合わせて記述 a and not b or not a and b #bool()関数の返り値が不一致であるか判定する bool(a) != bool(b) #operatorモジュール使用 from operator import xor xor(a, b)
しかし、これらの記法は一見どのような演算をやっているのかわかりづらいです。また三項以上への拡張性も。よろしくありません。
and
やor
のようにa exor b
と記述すれば排他的論理をを計算してくれる演算子があれば、コードの可読性が向上しそうです。
そこで今回はPythonのコンパイラを改造して、排他的論理和の演算子exor
を実装してみました。
文法の追加
ビルドまでの経緯はこちら
Grammar/Grammar
とPaser/Python.asdl
から、and
やor
が記述されている部分を探し出し、exor
の処理を新たに追加します。
# #if EXOR_TEST test: exor_test ['if' exor_test 'else' test] | lambdef test_nocond: exor_test | lambdef_nocond lambdef: 'lambda' [varargslist] ':' test lambdef_nocond: 'lambda' [varargslist] ':' test_nocond # #if EXOR_TEST exor_test: or_test ('exor' or_test)* or_test: and_test ('or' and_test)* and_test: not_test ('and' not_test)* not_test: 'not' not_test | comparison comparison: expr (comp_op expr)*
どうやらこの部分でand
、or
、not
の優先順位を決めているようです。今回はexor
はor
より優先順位が低いという設定にしました。全体の優先順位は
先 not
> and
> or
> exor
後
になります。
Python.asdl
の変更は以下の一行だけ。ブール演算子の種類にexor
を付け加えます。
boolop = And | Or | Exor
astの書き換え
Python/ast.c
はand
/or
の真似をするだけです。
#if EXOR_TEST//変更後のコード case exor_test: case or_test: case and_test: : : if (!strcmp(STR(CHILD(n, 1)), "and")) return BoolOp(And, seq, LINENO(n), n->n_col_offset, c->c_arena); if (!strcmp(STR(CHILD(n, 1)), "exor")) return BoolOp(Exor, seq, LINENO(n), n->n_col_offset, c->c_arena); assert(!strcmp(STR(CHILD(n, 1)), "or")); return BoolOp(Or, seq, LINENO(n), n->n_col_offset, c->c_arena); #else//もとのコード case or_test: case and_test: : : if (!strcmp(STR(CHILD(n, 1)), "and")) return BoolOp(And, seq, LINENO(n), n->n_col_offset, c->c_arena); assert(!strcmp(STR(CHILD(n, 1)), "or")); return BoolOp(Or, seq, LINENO(n), n->n_col_offset, c->c_arena); #endif
Python/symtable.c
にはブール演算子特有の処理がひとつもなかったので書き換える場所はありませんでした。
コンパイラ
肝心のコンパイラ部分の書き換えです。exor
はand
/or
よりずっと複雑な処理が必要なのでexor
使用時にコンパイラが生成するコードを自分で作る必要がありました。
and
/or
が動く仕組み
Pythonはcompile.c
で作成した擬似的な機械語をPython/ceval.c
が処理するという動きで動作しています。
プログラムの処理中に一時的に使用するデータは、一時的にFILO形式のスタック領域にロードされて処理されます。
例えばa + b
の処理をする場合、
- aをスタックに格納
- bをスタックに格納
- スタックの先頭から2つのデータを削除し、和をスタックに格納
という順序で処理されます。 Pythonが内部で生成している擬似的なオペコードはdisモジュールを用いることで見ることができます。
>>> import dis >>> def f(a, b): ... a + b ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (a)#aをスタックに格納 3 LOAD_FAST 1 (b)#bをスタックに格納 6 BINARY_ADD #ススタックの先頭から2つのデータを削除し、和をスタックに格納 7 POP_TOP 8 LOAD_CONST 0 (None) 11 RETURN_VALUE >>>
and
/or
の場合はどのようになっているかというと、例としてa and b
の場合、
- aをスタックに格納
- スタック先頭の真偽値を参照し真ならスタックの先頭を削除、偽なら4にジャンプ
- bをスタックに格納
- 次の処理
と処理されます。*2
>>> import dis >>> def f(a, b): ... a and b ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (a)#aをスタックに格納 3 JUMP_IF_FALSE_OR_POP 9 #スタック先頭の真偽値を参照し、真ならスタックの先頭を削除、偽ならジャンプ 6 LOAD_FAST 1 (b)#bをスタックに格納 >> 9 POP_TOP #次の処理(今回は関数を終える処理) 10 LOAD_CONST 0 (None) 13 RETURN_VALUE >>>
この時、「4.次の処理」の行でスタックの先頭はaかbのどちらかなのですが、その真偽値は論理積の真偽値と一致しています(真偽値表などを書いて確認してみてください)。
なぜ、ブール型(True
やFalse
)ではなくa、bの値をそのまま使うのかというと、「スタックの先頭を削除してTrue
/False
をスタックに格納」という処理を省略して処理を早めるためだと思われます。 思わぬところでバグが生じそうな気もしますが……
http://qiita.com/keisuke-nakata/items/e0598b2c13807f102469
この仕様は、a1 and a2 and a3 ...
のように複数の演算子が複数並ぶ場合に、最初の方で偽の値を見つけたら、後半の多くを確認することなく結果が得られるのでかなりの時間短縮が見込めます。
排他的論理和は根本的に別物
先程「exor
はand
/or
よりずっと複雑」と述べました。それは exor
は絶対に全部の式を判定しなければならない ので、and
やor
のように 場合によっては後半の値を確認しない 演算と根本的に異なるということです。これまでのように他の部分を真似るのではなく、自分で実装を考える必要があります。
とりあえず、exor
は専用の関数を用意して処理することにしました。
//compile.c static int compiler_visit_expr(struct compiler *c, expr_ty e) { : : switch (e->kind) { case BoolOp_kind:// #if EXOR_TEST if(e->v.BoolOp.op == Exor) return compiler_exor(c, e);//exorだけ別の処理を行う #endif return compiler_boolop(c, e);//and/orはどちらもこの関数で処理される : :
内部の処理はどうしましょうか。例えば、a exor b
の場合、とりあえず「aをスタックに格納」→「bをスタックに格納」という処理は必要そうです。
Pythonの既存のオペコードには、POP_JUMP_IF_FALSE
というものが存在します。これは「スタック先頭の真偽値を参照し、真ならジャンプ。真偽にかかわらずスタック先頭のデータは消す」という処理を行います。
またUNARY_NOT
というオペコードもあります。これは「スタック先頭の真偽値を反転」という処理です。*3
また、UNARY_NOT
で反転された値はブール型に変換されます。
>>> not 0.000 #もとが数値や True >>> not "A" #文字列でも結果はブール型になる False >>>
POP_JUMP_IF_FALSE
とUNARY_NOT
を組み合わせればexor
を実現できそうです。
a exor b exor c exor ...
の場合、
- aをスタックに格納
- bをスタックに格納
- スタック先頭の真偽値を参照し、真なら4にジャンプ。真偽にかかわらずスタック先頭のデータは消す
- スタック先頭の真偽値を反転
- cをスタックに格納
- スタック先頭の真偽値を参照し、真なら4にジャンプ。真偽にかかわらずスタック先頭のデータは消す
- スタック先頭の真偽値を反転
- 以下繰り返し
という順番に処理を行えば排他的論理和の計算ができます。ちょうど下図のように式を変形していくイメージです。 *4
試行錯誤の結果以下のようなコードを作成しました。
#if EXOR_TEST static int compiler_exor(struct compiler *c, expr_ty e) { basicblock **buf, *end; Py_ssize_t i, n; asdl_seq *s; assert(e->kind == BoolOp_kind); assert(e->v.BoolOp.op == Exor); end = compiler_new_block(c); s = e->v.BoolOp.values; n = asdl_seq_LEN(s) - 1; buf = (basicblock**)malloc(sizeof(basicblock*)*n); VISIT(c, expr, (expr_ty)asdl_seq_GET(s, 0));//「aをスタックに格納」に相当する処理 assert(n >= 0); for (i = 1; i < n; ++i) { buf[i-1] = compiler_new_block(c); VISIT(c, expr, (expr_ty)asdl_seq_GET(s, i));//「bをスタックに格納」 ADDOP_JABS(c, POP_JUMP_IF_FALSE, buf[i-1]); ADDOP(c, UNARY_NOT); compiler_use_next_block(c, buf[i-1]); } VISIT(c, expr, (expr_ty)asdl_seq_GET(s, n)); buf[n-1] = compiler_new_block(c); ADDOP_JABS(c, POP_JUMP_IF_TRUE, buf[n-1]); ADDOP(c, UNARY_NOT); compiler_use_next_block(c, buf[n-1]); ADDOP(c, UNARY_NOT); compiler_use_next_block(c, end); return 1; } #endif
この実装だと、最初以外全部偽だった場合だけ最初の項の値がそのまま出力されてしまうので、 最後に一度は否定を挟むようにして、結果をブール型で統一してあります。速さは犠牲になりますが、わかりやすさを重視しました。
使ってみた
ビルドして実際にexor
演算子を使ってみました。
>>> 1 exor 1 False >>> 0 exor 1 True >>> True exor 0 True >>> False exor False False
どうやらちゃんと動いているようです。
disassembleモジュールで分析した結果が以下になります。
>>> import dis >>> def f(a, b, c): ... a exor b exor c ... >>> dis.dis(f) 2 0 LOAD_FAST 0 (a) 3 LOAD_FAST 1 (b) 6 POP_JUMP_IF_FALSE 10 9 UNARY_NOT >> 10 LOAD_FAST 2 (c) 13 POP_JUMP_IF_TRUE 17 16 UNARY_NOT >> 17 UNARY_NOT 18 POP_TOP 19 LOAD_CONST 0 (None) 22 RETURN_VALUE
次は何やろう
本シリーズは講義のレポートも兼ねて執筆しています。その講義ですが、ちょうどexor
の実装ができたところで終盤になり、残り時間も少なくなってきました。*5
講義中にgdbの使い方を教わったのに全く活用していないのは少し残念ですし、言語仕様以外も軽くいじってみたいという思いもありました。
そこで残り時間でPythonはCUI部分をいじって--hoge
みたいな新しいオプションとちょっとした機能の追加をやってみました。次回はその様子を説明します。