JavaScriptで文字列のバイト数を得るにはどーする?
photo credit: wuestenigel White tape measure via photopin (license)
ブラウザのJavaScriptとNode.jsで文字列のバイト長を知る方法です。文字数ではありません。 それぞれの環境でやり方が違いますが、統一コードも紹介してます。
はじめに
「JavaScriptで文字列のバイトサイズを取得する方法」を検索すると「URLエンコードして文字数を数える」って方法が検索結果の上位にたくさん出てきます。 しかし「こうすればできる」って感じがちょっとアヤシイ。
たいていはNode.jsに言及していないため、
「割と古い方法なのかな?最新式のJavaScriptではもっとスッキリした方法があるのでは?」
と思いまして「ブラウザのJavaScriptとNode.jsの両方の環境で、文字列のバイト長を求める方法」を調べてみました。
結果として、納得できるスッキリした方法がありましたけど、両環境では別方法。 まあ、どちらでも同じやり方が可能なモジュールもありましたので、それも含めて以降で説明しています。
ブラウザでは Blob を使う
ブラウザのJavaScriptでは (new Blob(["文字列"])).size
で文字列のバイトサイズが得られます。
少し直感的でないので、関数として書けば以下のようになりますね。
/** * 文字列のバイト長を得る(ブラウザ版)。 * 文字数ではありません。 * @param {string} s 文字列 * @returns {number} 入力文字列のバイト長 */ function byteLengthOf(s) { return (new Blob([s])).size; }
Blobはブラウザでバイナリデータを扱うためのクラスです詳細は以下。 上で使っているのはコンストラクタとsizeプロパティだけですね。
Node.js では Buffer クラスを使います。
Node.jsなら、Buffer.byteLength("文字列")
でOKらしい。こちらはかなり直感的です。
/** * 文字列のバイト長を得る(Node.js版)。 * @param {string} s 文字列 * @returns {number} 入力文字列のバイト長 */ function byteLengthOf(s) { return Buffer.byteLength(s); }
実行時に切り替えるには?
実行時に動作環境がブラウザであるのかNode.jsであるのかを調べて切り替えるには、以下のようにすればよろしいです。
/** * 文字列のバイト長を得る。 * @param {string} s 文字列 * @returns {number} 入力文字列のバイト長 */ const byteLengthOf = ("window" in (Function("return this;")()) ? s => (new Blob([s])).size : // ブラウザ版 s => Buffer.byteLength(s)); // Node.js版
npm buffer で統一できる
と、ここまで書いて気になって、追加で調べてみたのですが、
Node.jsのBufferクラスをブラウザのJavaScriptで使えるようにした buffer
というnpmモジュールがありました。
最初に調べるべきだったかもしれない。
Bundlerが使える場合
WebPackやParcelなどのBundlerを使っているなら、以下のようにrequireで読み込めば(パスにちょっと注意)、 ブラウザのJavaScriptでも Node.js版のコードが使えます。そしてNode.jsでも動作しました。
const Buffer = require('buffer/').Buffer // 注意:最後のスラッシュは重要! /** * 文字列のバイト長を得る。 * @param {string} s 文字列 * @returns {number} 入力文字列のバイト長 */ function byteLengthOf(s) { return Buffer.byteLength(s); }
Bundlerが使えない場合
Bundlerを使えない場合は、ビルド済みスクリプト( https://bundle.run/buffer
)をHTMLのSCRIPTタグで読み込めば Buffer クラスが使えるようになるらしい(こちらに関しては未検証です)。
<script src="https://bundle.run/buffer"></script>
/** * 文字列のバイト長を得る。 * @param {string} s 文字列 * @returns {number} 入力文字列のバイト長 */ function byteLengthOf(s) { return Buffer.byteLength(s); }
あとがき
検索でトップに出てくる情報が完全に正しい情報だとは限らないと思いました。 この場合は、完全な間違いとも言い切れませんが。
JavaScriptは既にかなりな歴史が積み重なっていて、徐々に仕様が整備されてきたので、どうしても古い情報が残りがちですね。 これから始めようとする人たちにとっては弊害となることがありそうな気もします。
このような点からも、最近はPythonが流行っているということもあるのかなと(知らんけど)
このような心配とはまた別に、JavaScriptではnpmとかBundlerの類はストレスなく使えるようになっておいたほうが良さそうですね。 モダンJavaScriptバンザイです。いくつになってもお勉強です。
祝!MZ-700シリーズ発売37周年っ!に合わせてMZ-700フルJavaScriptエミュレータをWebWorkerでレンダリング!
1986 Tiny XEVIOUS namco
(拙作「MZ-700フルJavaScriptエミュレーター」で撮りました)
今年もやってきました11月15日の祝日が(えっ?) SHARP MZ-700シリーズの発売日ですよ。ちょうど37年目のお祝いです。 発売日は 1982年11月15日。 当時ワタシはおそらく中学2年生(←昔過ぎて正確な年齢が算出できない)。 そら50歳にもなるわけだ(笑)
昨年のお祝いエントリーはコチラ↓です。
MZ-700との出会いはたまたまでした。 1982年の年末に、機械関係、メカトロ関係のエンジニアだった父が買ってきたんですね。 プロッタプリンタのついていたMZ-731というモデルでした。 仕事で使うつもりだったみたいですが。
それから数年、兄とともに濃厚濃密な年月を過ごしました。 最初にBASICを覚えてから、機械語ってのが速いらしいと小耳にはさんでZ80のハンドアセンブルにいそしみました。 市販のゲームもやるっちゃあやるんですが、プログラムを作れば作った通りに動くってこと自体が楽しくて、Oh!MZを筆頭に、I/O、マイコン、ベーマガ読んで、あーだこーだとやっていました。
しかしそのうちに、文字しか表示できないMZ-700に引け目を感じるようになってしまい、X-1が良かったなーなんて思いながら一念発起、MZ-700の後継機種として発売されたMZ-1500を入手しまして、徐々に気持ちがMZ-700から離れていきました。ちなみにこの時MZ-1500に同梱されてたPSG6音を鳴らせるピアノロールみたいなソフトを使って和音の面白さにめざめて、その後の音楽趣味につながります。9割9分打ち込みだけど、Jazzコード理論(初歩)は独学。
Tiny XEVIOUS for 700 の衝撃
そのころは、世間的にも「MZ-700はオワコン」的な雰囲気になっていたと思うのですが、Oh!MZに唐突に「Tiny XEVIOUS for MZ-700」 という目も耳もなんなら鼻や口すら疑うような記事が載り、ワタシのあごは落ちました。ガクー
※ Tinyという単語はTiny XEVIOUS以前にも雑誌掲載のゲームなどでよく使われていた印象がありますが、このTinyは確かにTinyなんだけどさ、まったくTinyではありませんよね(?)。
これは「MZ-700に不可能はない!」との名言が生まれるきっかけとなったであろう古旗一浩さん作(というか移植)のゲームですけど、実は自分は当時これを打ち込んでもいなくて遊んでいません。
というのも、既にXEVIOUSはゲーセンでやり倒していたし、目の前にはMZ-1500があるしという状況。 「サンダーフォースやれるからもういいよ」的な気持ちだったなあ。
しかし、このとてつもないチャレンジ精神にあふれたほぼ同世代の古旗一浩という人の存在を知ったがために、「自分は薄っぺらくてダメなやつだ情けない」てな気持ちになってしまったのも影響していたように思います。
まるくんの MZ700Win
なんだかんだで、その後、制御系のソフト会社へもぐりこみましたが、既に時代は個人ユースでもNECのPC-9801の独壇場。8ビットのPCなんて既に製造されていませんし、自分の中でMZはもう過去のものとなっていました。しかし、そこから10年、世間はインターネットとWindows。2000年ごろふと知った「まるくん」こと丸山武志さん作のWindows用MZ-700エミュレータ MZ-700Win。 当時VC++で仕事していたし、すげーなるほどこういうの作れるんだへぇーーーみたいな印象で、懐かしいゲームなどを再び楽しめて感動しました。
拙作 MZ-700フルJavaScriptエミュレータ
と、ここまで長くなりましたが、さらに10年後、世の中はHTML5とAjaxでできていて、ふと「MZ-700 のエミュレーションはJavaScriptで可能ではないだろうか?」と一念発起。
MZ-700って、発売当時は当たり前だったのかもしれませんが、Owner's Manualに回路図が載っているんですよね。 なので内蔵されてるICなどのデータシートを読み取って、それぞれJavaScriptでエミュレータを書いていく。 当然Z80の命令もJavaScriptでコーディング。
この当時はWebWorkerの存在もほぼ知らず、タイマー駆動でCANVASに画面を描いていました。 最初は1枚のCANVASでなく40文字×25行のPNG画像で画面を構成してました。
今ではそれなりに最新機能も取り入れて高速化。 ずいぶん前からnpmとして提供しており、本日 v1.2.9 をリリースしました(追記:最新はnpmのGlobalインストール時の不具合を解消したv1.2.10)
node.jsを使える人は、
npm install --global mz700-jsでグローバルインストールできます。コンソールから
mz700-js` コマンドでWebAppが起動します。
mz700-js @1.2.9
OffscreenCanvasに対応しているブラウザでは、画面の描画はサブスレッド側で行われます
従来WorkerスレッドからのVRAM書き込み要求をメインスレッドに伝えて描画していましたが、ある程度キャッシュしないと速度低下につながりましたので、100msはキャッシュをしていました。 今回サブスレッドで描画するようにしましたが、これも逐一更新するとちらつきが多くなってむしろ低速。 バッファリングを10msに短縮すればなんとか納得いく速度を実現できました。
従来Chromeが速度と安定度でおすすめでしたが、相対的に低速だったEdgeやFirefoxで、今回それなりのパフォーマンスが出るようになっていて「おいおいChromeうかうかしてられへんぞー」という気分。とはいえやっぱりChromeの挙動が一番安定していて安心できるのも事実です。
描画方式を変えると、Tiny XEVIOUSのタイトル画面がビカビカしていました。
書き換えタイミングを変えたことでかなり高速に書き換えが発生しているようで、細かい動きの再現性が高まりました。実機の動きに近づいたかも?!
DynamoDB Query APIのパラメータを生成しましょ(プレースホルダをぶっ飛ばせ)
photo credit: Kurayba Micro Machine via photopin (license)
AWS のNoSQLデータベース DynamoDB のテーブルを Query する場合のパラメータを生成するWebアプリをつくりました。
DynamoDBでテーブルの項目を取りだすには Query API を使いますが、このパラメータがちょっと曲者なんですよね。
属性名が予約済みのキーワードにマッチしていると条件式などに直接記述できなくて「属性名のプレースホルダ」を定義して、読み替える必要があるのです。 さらに条件式では具体的な値(文字列や数値)が直接使えず「属性値のプレースホルダ」に変換しないといけません。
実際、慣れるとたいしたことではないのですけど最初はかなり悩みました。 今でも困ってる人はいるんじゃないかと思います。
DynamoDBはよくできたサービスですけど、これ(↑)って参入障壁かもしれない。 かつて悩んで解決したので、このようなソリューションを公開する責任があるのではないだろうか?と鼻息荒く意識は高く、以下に公開する次第ですー・・・
使い方
少しUIがしょぼいですが、徐々に改善するつもりです(ゆるして)。
左にチェックボックスがついているのは省略可能なパラメータ。最初はチェックが外れていますがチェックをつけるとサンプルで入力した内容が復活します。
各テキストボックスを編集すると即座に結果のパラメータが変化します(エラーがあると変化しないので気を付けてください)。
属性名にDynamoDBのキーワードが含まれていると属性名のプレースホルダが生成されるのを確認してみてください。
生成されたパラメータはDynamoDBのQuery APIの第一引数にそのまま指定できるJSONです。[Copy]ボタンでクリップボードにコピーできまーす。
- TableName は必須です。テーブル名を指定します。
- KeyConditionExpression も必須で、テーブルのキーに対する抽出条件を指定します。キー以外の属性は使えません。パーティションキーには、
=
しか使えません。レンジキーには、範囲指定の演算子、<
,<=
,=
,>=
,>
, そして、BETWEEN <s> AND <e>
が使えます。両者はAND条件で組み合わせます(ORは無理)。パーティションキーの条件は省略可能です。 - FilterExpression は、KeyConditionExpressionで抽出した結果に対してフィルターをかける機能。DynamoDBではSCANした量によって課金されますからFilterで絞っても対象外です。でもFilterはAWS側で行われるので、インターネットの転送量は絞ったほうが少なくなります。まあややこしいところですが、そのうち理解できますよ(多分)。
- Limit は抽出する行数を指定します。
- ProjectionExpression はSQLのSELECT句。取り出したい属性値の名称をカンマ区切りで指定します。省略すれば属性値全体を取り出します。属性名がキーワードかどうかを気にする必要はありません。
関連サイト
npm aws-node-util でSQL的な構文でDynamoDBを操作する機能を提供していますが、この度こちらにこのページで使っているWebAppを公開してるのでよろしくね。
AWS SAMテンプレートでREST APIの複数メソッドをひとつのLambdaに統合するには?
photo credit: Andreas Komodromos Urban Nights - Hudson Yards, New York City via photopin (license)
ホントちょっとしたことなんですが、SAMのHelloWorldのチュートリアルをやっただけではハッキリわからなくて、少しだけ試行錯誤が必要でしたので書いておきます。
API Gateway REST APIの複数メソッド(GET,PUT,POST,DELETEなど)を、ひとつのLambda関数に統合(Lambdaプロキシ統合)する場合の AWS SAMのテンプレートの書き方です。
API Gatewayのコンソールでは、各メソッドの設定で、同じLambda関数を指定するだけですが、じゃあSAMのテンプレートではどう書くのでしょうか?ってところです。
SAMテンプレートの書き方(例)
以下のYamlでリソースパス /hello
に対する GETリクエストとPUTリクエストを、Lambda関数 HelloWorldFunction
に統合しています。
sam-app/template.yaml:
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sam-app Sample SAM Template for sam-app # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: Events: #ココ↓と HelloWorldGet: Type: Api Properties: Path: /hello Method: get #ココ↓ HelloWorldPut: Type: Api Properties: Path: /hello Method: put
一般化すると、AWS::Serverless::Function
のProperties/Events
以下にメソッド単位で別々のApiとして宣言すればよいのです。
APIのリソースパスが同じでもメソッドが別なら複数のApi定義が必要ということですね。
SAMテンプレートでは独立したREST APIを宣言しているのではなくて、あくまでもサーバーレス関数の定義の一部なのでこうなるのでしょう(多分)。
今となっては上の定義が理解できていますが、当初「Events 以下のエントリがAPIのリソースパスを一意に表している」と思い込んでいて、Methodを配列にしてみたりして失敗しました。
Lambdaプロキシ統合のパラメータで分岐する
Lambda関数側では、引数のeventのhttpMethod
にHTTPメソッドが文字列で格納されていますので、これによって分岐します。
exports.lambdaHandler = async (event) => { try { const response = { statusCode: 200 }; switch(event.httpMethod) { case "PUT": await put(JSON.parse(event.body)); break; case "GET": await get(event.queryStringParameters); break; } return response; } catch (err) { console.log(err); return err; } };
リクエストパラメータは、同じくeventのqueryStringParameter
やbody
に格納されています。
queryStringParameter
はObjectですがbodyは文字列(JSON)なので気を付けましょう。
これらパラメータの使い方はメソッドの設計に依存するので一概に「こうしましょう」とは言えません。
他にも pathParameters
やheaders
を利用することがあるかもしれませんね。
Lambdaプロキシ統合されたLambda関数の入力形式については、以下の公式ページに説明があります。 実際に動かしてみて把握する必要はあるかもしれませんよ。
あとがき
普通はメソッドごとにLambda関数を分けることが多いかもしれません。 そのほうがLambdaでの分岐が不要で多少はパフォーマンスが良いはずですし。
しかし、各メソッド間で同じデータモデルを扱うなど共通処理が必要な場合は一つにまとめるとスッキリすると思います。
共通処理についてはLambda Layersを使うのも一つの解です。 ここでは、「とりあえず手軽にやるには?」という観点でサクッと調べた結果を書いておきました。
AWS SAMがAWS CLIを見失う?ならPATH設定を要チェック!
photo credit: dolbinator1000 Over The Hill via photopin (license)
LinuxにAWS CLIをインストールしてコンソールから普通に使えていたのに、SAMでサーバーレスアプリのパッケージを作成しようとすると「AWS CLIが見つからない」というエラーが出て失敗しました。ビルドできるのになんでやねんと。
どうやら、AWS CLIのPATH設定と、AWS SAMのPATH検索方法の行き違いみたいなことで発生した問題のようでした。
実行環境
$ sam --version SAM CLI, version 0.22.0) $ $ aws --version aws-cli/1.16.258 Python/3.6.5 Linux/4.19.78-2-lts botocore/1.12.248 $ $ uname -srm Linux 4.19.78-2-lts x86_64 $
sam package でエラー
sam package
でパッケージを作ろうとするとエラーを履いて失敗しました。
$ sam package --s3-bucket <bucket-name> --output-template-file packaged.yaml Error: Cannot find AWS CLI installation, was looking at executables with names: ['aws'] $
エラーメッセージは「AWS CLIのインストールが見つからない。コマンド 名 aws
を探していましたが・・・」みたいな感じ。
いやしかし、AWS CLI はインストールしており、コンソールから普通に使えている状態。以下のようにPATHも通っていますよ。
$ which aws /home/<user>/.local/bin/aws
ただ、気になったのは aws
がホームディレクトリ以下の自宅の床下みたいな所 にあるということ。
どうやら、pipで自分だけ使えるようにインストールしたら、こうなるようです。
全ユーザーが使えるようにしたらこうはならないらしいです。
とりあえずの対策
Cannot find AWS CLI installation, was looking at executables with names
エラーメッセージで検索すると、Close済のGitのIssueがヒット。 文言が少し違っていますが、同じ問題のように思えます(↓これ)。
「とりあえず /usr/bin
や /usr/local/bin
に上記 aws
コマンドを置けばOK」とありますので、シンボリックリンクを張ってみると、、、
あっさり解決・・・
[~] $ cd /usr/local/bin [/usr/local/bin] $ sudo ln -S aws /home/<user>/.local/bin/aws [/usr/local/bin] $ cd ~/sam-app [~/sam-app] $ sam package --s3-bucket my-sam-app-bucket --output-template-file \ packaged.yaml Successfully packaged artifacts and wrote output template to file packaged.yaml. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /home/<user>/..../packaged.yaml --stack-name <YOUR STACK NAME>
しかしこれ、やっぱり aws
コマンドの場所が気持ち悪い。
街の通り(/usr/local/bin
)から自宅の床下(~/.local/bin
)に抜け穴を掘ってるみたいです。
正しい解決法
さらにIssueを読み進めると、ヒントがもひとつ書いてあった。
SAMはaws
のありかを自前でチェックしているらしいのです。
なんでそんなことしてるのかは知りませんが、そのおかげで PATH に含まれてる ~
を正しく解釈できないらしい。
ほら言わんこっちゃない。なんで自前でやっちゃうかな。本来それはOSに任せるべきなのでは?
とか言ってても仕方がない。
とにかくそういうことらしいので、自分の .bashrc を確認すると、・・・
.bashrc
export PATH="~/.local/bin:$PATH"
普通に ~
を使っていましたね。
結局このせいでAWS SAMはAWS CLIを見つけられず、インストールされていないなんて言っていたわけです。
以下のように ~
を使わないように修正すれば問題解決。
上に書いた /usr/local/bin
からのシンボリックリンクも不要です。
export PATH="/home/<user>/.local/bin:$PATH"
結構時間がかかって解決しました。割と大きめの落とし穴にハマった気分でした。