にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

GASを使って作業開始・終了時刻記録Webアプリを作った話

概要

Google Apps Script (GAS)を使って、自分で使うための、作業開始・終了時刻を記録するWebアプリを作成した。

f:id:nixeneko:20210311005657p:plain
作成したアプリ
この記事にあるもの
  • GASによるWebアプリ作成の流れ
  • 作ったアプリのコード
ないもの
  • コードの書き方・解説

モチベーション

  • 作業時間(開始時刻, 終了時刻)を記録したかった
  • 開始時と終了時にボタンを押すだけで記録できるとよい
  • スマホで使えると良い

これを満たすのに、勤怠管理システムが使えるかと思い、無料で使える勤怠管理Webサービスを使っていた。しかし、一日に2回以上の勤怠データを登録することができず、自分の使いたい目的では使えないことが分かった。

そのため、自分で使うためのものを作ることにした。

要件

  • 「出勤」「退勤」ボタンが表示される(実際には「作業開始」「作業終了」だが長いのでこうした)
  • ボタンを押すとその時の時刻およびボタンに対応する「入」「退」のどちらかが記録される
    • 時刻の記録だけに特化する。作業時間の計算等は別にプログラムを書いてどうにかする

GASとは

Google Apps Script (GAS)とは、Googleサービス等と組み合わせて使える、Javascriptをベースとしたスクリプト言語で、Googleドライブとかで実行できる。また、GASを使ってWebサービスを開発したり公開したりできる。

Webサービスであればスマホで使えるのでいいだろうということでGASで作ることにした。

実装方針

  • 勤怠時刻データはそれ用のGoogleスプレッドシートを作成しそこに保存する
  • GASによるWebサービスでは、HTTPリクエストを受けると決められた関数がトリガーされる。GETとPOSTで別の関数となっている:
    • GET (doGet関数)では、ボタンを表示するページを返す
    • POST (doPost関数)では、時刻の記録を行う。記録した後はGETにリダイレクトする(POSTでページを表示した場合、リロードすると再度記録されてしまうため)

実際にやって動かしてみる

まず、Googleドライブを開く*1

記録用スプレッドシートの用意

先に記録用のスプレッドシートを用意する。

「新規」→「Google スプレッドシート」をクリック
f:id:nixeneko:20210310221702p:plain

  • スプレッドシートのタイトルを変更(任意)
  • 見出しを適当に入力。ここでは「時刻」「入退」「備考」にした。
  • シート名を入力。シート名は何でもいいがここでは"time_log"とした。後で使う。
  • スプレッドシートのIDをコピーしておく。後で使う。
    • スプレッドシートのURLがhttps://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXX/editみたいな感じになっているので、XXXXXXXXXXXXXXXXXXXXの部分がIDになる。

Apps Scriptの作成

「新規」をクリック*2→「その他」→「Google Apps Script」をクリック
f:id:nixeneko:20210310215715p:plain

エディタが開くので、「無題のプロジェクト」と書かれている部分をクリックしてプロジェクトの名前を適当に変更する(任意)

実行の流れとしては、コードを変更する→保存する→デプロイする→ページを開いて確認という流れになる。

動作確認

最初にWebアプリを動かしてみる。エディタに次のコードを入力する。

function doGet() {
  const html = '<!DOCTYPE html><html><head><base target="_top"></head><body><h1>Hello World!</h1></body></html>';
  return HtmlService.createHtmlOutput(html);
}
デプロイ

保存ボタン「💾」をクリック、あるいはCtrl+Sを押す→「デプロイ」→「新しいデプロイ」をクリック
f:id:nixeneko:20210310225542p:plain

「種類の選択」の右の歯車「⚙」をクリック→「ウェブアプリ」をクリック
f:id:nixeneko:20210310225656p:plain

「デプロイ」をクリック
f:id:nixeneko:20210310225923p:plain

すると、ウェブアプリのURLが表示されるのでコピーして控えておく。後で使う。
f:id:nixeneko:20210310230247p:plain

ウェブアプリのURLを開くと次のように表示される。
f:id:nixeneko:20210310230440p:plain

コードを変更した際は、まず保存し、「デプロイ」→「新しいデプロイ」→「デプロイ」を繰り返すと変更が反映される。*3

アプリを動かしてみる

さて、こちらに作ったアプリを用意した。これを動かしていく。

「ファイル」の右の「+」をクリック→「HTML」をクリック
f:id:nixeneko:20210310231515p:plain

名前を「index」と入力して確定
f:id:nixeneko:20210310231609p:plain

index.htmlのコードを消して次の内容に書き換える:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style type="text/css">
      button {font-size: 15vw; height: 30vw; width: 45vw;}
      input {width: 80vw;}
    </style>
  </head>
  <body>
    <h1>タイムカード</h1>
    <form action="<?= app_url ?>" method="POST" name="f">
      <div>
        <button type="submit" name="action" value="in" <?= in_disabled ?> >出勤</button>
        <button type="submit" name="action" value="out" <?= out_disabled ?> >退勤</button>
      </div>
      <div>備考: <input name="comment"></input></div>
    </form>
    <div>最終: <?= last_time_log ?></div>
  </body>
</html>

コード.gsの内容を次のコードに書き換える。spread_sheet_id, sheet_nameの値は先ほど控えた値に変更する。

const spread_sheet_id = "ここにスプレッドシートのIDを入力"; //記録するスプレッドシートのID
const sheet_name = "time_log" //スプレッドシートのシートの名前

const app_url = ScriptApp.getService().getUrl(); //このアプリのデプロイURL

function getSheet(){
  const spreadsheet = SpreadsheetApp.openById(spread_sheet_id);
  const sheet = spreadsheet.getSheetByName(sheet_name);
  return sheet;
}

function getLastRow(){
  let sheet = getSheet();
  let last_row_idx = sheet.getLastRow();
  let values = sheet.getRange(last_row_idx, 1, 1, 3).getValues()[0];
  return values;
}

function addLastRow(time_str, in_out, comment){
  let sheet = getSheet();
  let last_row_idx = sheet.getLastRow();
  sheet.getRange(last_row_idx+1, 1, 1, 3).setValues([[time_str, in_out, comment]]);
}

function formattedDate(date){
  return Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

function doGet(e) {
  let html = HtmlService.createTemplateFromFile('index');
  html.app_url = app_url;
  let date, in_out, comment;
  [date, in_out, comment] = getLastRow();
  let date_str;
  if (date instanceof Date) {
    date_str = formattedDate(date);
  } else {
    date_str = date;
  }
  html.last_time_log = date_str + " " + in_out + " " + comment;
  if (in_out == "入"){ //最後が出勤なら退勤のみ表示
    html.in_disabled = "disabled";
    html.out_disabled = "";
  } else {
    html.in_disabled = "";
    html.out_disabled = "disabled";
  }

  return html.evaluate()
              .addMetaTag('viewport', 'width=device-width, initial-scale=1, user-scalable=no')
              .setTitle('タイムカード');
}

function doPost(e){
  let action = e.parameter.action;
  let now = new Date();
  let date_str = formattedDate(now);
  const action_to_japanese = {"in": "入", "out": "退"};
  let in_out = action_to_japanese[action];
  if (!in_out) throw new Error('POSTパラメータが異常です'); //不正な呼び出し
  let comment = e.parameter.comment;
  addLastRow(date_str, in_out, comment); //書き込み

  // GETにリダイレクトする
  return HtmlService.createHtmlOutput(
    "<script>window.top.location.href='"+ app_url + "';</script>"
  );
}

入力したら保存し、デプロイをする。
(2021-10-11追記: デプロイURLはScriptApp.getService().getUrl();で取得できることがわかったのでそのようにコードを変更した)

認証

デプロイをしようとすると、スプレッドシートにアクセスするために認証が必要になる。

「アクセスを承認」をクリック
f:id:nixeneko:20210310232713p:plain

ウィンドウが開くので今使っているアカウントを選択
f:id:nixeneko:20210310233040p:plain

「このアプリは Google で確認されていません」と表示されるので「詳細」をクリック
f:id:nixeneko:20210310233341p:plain

「time recorder (安全ではないページ) に移動」をクリック(time recorderは設定したプロジェクト名)
f:id:nixeneko:20210310233612p:plain

「許可」をクリック
f:id:nixeneko:20210310233738p:plain

これでデプロイができる。一度やればOK。

ブラウザで開いて確認

次のような画面になる。スマホのサイズでちょうどいい感じ(だがパソコンで開くとすごいバランスになる)。
f:id:nixeneko:20210311005657p:plain

ボタンを押すとスプレッドシートに時刻が書き込まれることが確認できる。
f:id:nixeneko:20210311013206p:plain


これで完成。デザインは適当だが自分しか使わないのでヨシ!
スマホでアプリのURLを開けば実行できる。アプリを作成したのと同じGoogleアカウントでのログインが必要。
ちょっと読み込みが遅い気がする。


ほか工夫点。

  • 入退は交互に来るはずなので、ボタンは交互にグレーアウトするようにした。
  • 作業内容等を書けるようにコメント欄も作った。
  • スプレッドシートの最終行=最後に記録された内容を表示するようにした。

GAS引っかかりメモ

Googleアカウントを切り替えられない?

エディタを開く際、同時に複数のGoogleアカウントにログインしている場合、メインのアカウント以外に切り替えることができないようで、使いたいアカウント以外をログアウトする必要があるっぽい?

デプロイする前に保存するのを忘れて変更が反映されない

よくある。保存してデプロイし直す。

WebアプリのHTML内のJavaScriptから、ブラウザのアドレスバーにあるWebアプリのURLを取得

無理っぽい。iframeに入っていてかつクロスドメインになっているため。なぜ…

タイトルやmetaタグの設定

GASのWebアプリで表示されるページは、二重にiframeが入ったその中にアプリで設定したHTMLが表示される。そのため、HtmlOutputを作成する際のHTMLの中でタイトルやスマホ対応のviewportを指定するmetaタグを設定しても効果がない。

代わりにHtmlOutputオブジェクトの.setTitleメソッド、.addMetaTagメソッドを呼び出して、それぞれタイトルとメタタグを設定する。メソッドチェーンもできる。

return HtmlService.createTemplateFromFile('index').evaluate()
              .addMetaTag('viewport', 'width=device-width, initial-scale=1, user-scalable=no')
              .setTitle('タイムカード');
リダイレクト

window.top.location.hrefにリダイレクト先のURLを代入するJavascriptコードを実行するHTMLを返せばいいっぽい。

  return HtmlService.createHtmlOutput(
    "<script>window.top.location.href='"+ app_url + "';</script>"
  );

参考: google apps script - Automatically Redirecting to a Page - Stack Overflow

(2021-09-03追記: <iframe>sandbox属性にallow-top-navigation-by-user-activationが指定されているためリダイレクトが動かなくなった。これどうしようかな…。)

まとめ

  • 自分だけ使えるWebアプリをタダで作って使えるので便利*4
  • Googleドライブ上のデータを読み書きできる(認証が必要)
  • 自分しか使わないので必要なものを最低限だけ実装すればいい
    • コーナーケースに対するエラー処理もしなくていいかもしれない(運用でカバー)
  • 100行もコードを書かずにできた

*1:使いたいアカウント以外をログアウトしておく必要があるかもしれない。

*2:画像ではマイドライブ▼をクリックしてるけど出るメニューは同じ。ファイル一覧のペインを右クリックしてもよい。

*3:エディタで「デプロイ」→「デプロイをテスト」とすると、最新のコードをテストできるURLが得られるので、開発に有用かもしれない。

*4:設定次第で、他人に使わせたり、一般公開することも可能。

Scrapbox記法→はてな記法 変換器をつくった

私はアイデアや下書きをまとめるのにScrapboxを利用している。一方で、情報をまとめて公開するのにははてなブログを主に利用している。
はてなブログでは「見たまま」「はてな記法」「Markdown」の三種類の編集方法が選べるが、私は「はてな記法」を使っている。理由は慣れである。

Scrapboxの記法もはてな記法も、簡潔な軽量マークアップ言語であり、プレーンテキストで編集できるので便利ではあるものの、それぞれに互換性がないので、相互に情報を移動させたければ変換が必要である。

しかしながら、そのような変換器が探しても見つからなかったため、Scrapboxからはてな記法へと変換するコンバータ(変換器)を作成した。

コンバータのページ

制限

  • 完全な一対一の変換はできないので、変換結果をはてなブログに投稿する前に確認して修正する必要があると思う。たたき台としての利用を想定している。
  • コードブロックのシンタックスハイライトは、とりあえず私が使う言語だけ対応させた。Scrapboxはてなブログが対応している他の言語が必要なら教えてください。
  • 画像も変換に未対応な種類があるかもしれません。
  • 本コンバータの逆変換、つまりはてな記法Scrapboxへの変換は作る気はない。それを私は必要としていないので。

やや特殊な変換

  • 次の3つは単体で表示するためにはコードブロックに入れる必要があるため、その一文字だけがインラインコードになっている場合、<code>~</code>で囲むことはしない。
    • `[`[
    • `]`]
    • `\```
  • 見出し。Scrapboxには見出し専用の記法はないので、行頭にインデントがなく、かつ文字を大きく表示する記法単体しか行に存在しない場合に見出しとした。
    • [**** 大見出し]* 大見出し
    • [*** 中見出し]** 中見出し
    • [** 小見出し]*** 小見出し

ソース

何をやってるのかはソース読むと分かるかもしれません。行きあたりばったりで書いてるので読んでわかるかは分かりませんが…。
github.com

テスト用データ

テストに使ったScrapboxページのコードを載せておく。変換結果を表示して見比べてみると良いかと思う。

Scrapbox表記テストページ
変換器のテストに使います。
[**** 大見出し]
[*** 中見出し]
[** 小見出し]
[* 太字1][[太字2]]ふつうの文字[** ややでかい文字][*** もっとでかい文字][**** 更にでかい文字][***** もっとでかい文字]
[/ italic] [- stroke] [*/* bold bigger italic] [**-/ bold bigger stroke italic] [_ underline] [_/* underline italic bold]

数式[$ \sin (x)]
> 引用1
>引用2
 >引用3
$ cat commandline1
% cat commandline2
	$ cat commandline3
`コード`記法。`[`~`]` バッククォート自体は`\`` と入力する。`\` \` 
code:js
	function test(){
   console.log("Hello World!");
 }
code:py
 import numpy as np
 np.array([])
code:test.md
	# hoge
	## hogehoge
table:tabletitle0
	1111	2222	3333	4444
	aaaa	bbbb	cccc	dddd

	箇条書きlevel1
		箇条書きlevel2
			箇条書きlevel3
				箇条書きlevel4
	1. 数字
		2. 数字
			3. 数字
画像
[https://nixeneko.sakura.ne.jp/hatenablog/20180316_oembed/simarin.jpg]画像1
[[https://nixeneko.sakura.ne.jp/hatenablog/20180316_oembed/simarin.jpg]]画像2
URL
https://www.yahoo.co.jp
[Yahoo https://www.yahoo.co.jp]
[https://www.yahoo.co.jp Yahoo]
[hoge https://www.yahoo.co.jp https://www.google.co.jp]
[https://www.google.co.jp https://www.yahoo.co.jp https://www.yahoo.co.jp]
ほかどうでもいいやつ
[ページタイトル]
[ページタイトル.icon]

アニメキャラの目のハイライト消しデータセット

次のブログ記事で利用したデータセットを公開します。

データセットの内容の一部。左側に目にハイライトのある画像、右側にハイライトのない画像を並べている。

説明

アニメの顔の画像で、目にハイライトがあるもの(オリジナル)と、そこから目のハイライトを塗りつぶして消したものとのペアです。500ペアあります。
ペアは完全に1対1の対応がとれているので、pix2pixなどの1対1で対応がとれた教師データを必要とする画像変換モデルに適しています。

画像は2017年冬(1月~3月)に放送されていたアニメのスクリーンショットから顔を切り出したもので、正方形で一辺が256px以上720px未満となっています。


このデータセットは、日本国の著作権法(2021年1月1日施行)の第三十条の四に基づいて公開します。
参考: 進化する機械学習パラダイス ~改正著作権法が日本のAI開発をさらに加速する~ | STORIA法律事務所

Twitterハッシュタグの仕様について

f:id:nixeneko:20210130202127p:plain

まえがき

Twitterに"#᥋ᵍᶜₒ𝖛ịⅆ"というハッシュタグが流れているのを見た。内部的には"#5GCovid"と等しいようだった。タグの指す内容は置くとして、これが同一視されてるのは奇妙な気もした。
これは、他にも同様のハッシュタグを作れるのでは?と思って、変換器を作った。
nixeneko.github.io

作成する際にTwitterハッシュタグの仕様を探って、なんとなく雰囲気がつかめたのでメモとして残しておく。

Twitterハッシュタグの仕様

(2021/01/29現在のため、変更される可能性がある)

  • #で始まり、ハッシュタグに使えない文字が来るかツイート末まで続く
  • #はツイート頭にあるか、#の前にはスペースが必要
  • $で始まるハッシュタグもあるが、ASCII英字6文字まで、とか制限が強い

ハッシュタグに使える文字

  • Unicode 7.0に存在する文字である必要がある(8.0以降で追加された文字はハッシュタグにならない)
  • ハッシュタグに使える文字は、基本的には、次のgeneral categoryをもつUnicode文字であるようだ:
    • Lu……Letter, Uppercase
    • Ll……Letter, Lowercase
    • Lt……Letter, Titlecase
    • Lm……Letter, Modifier
    • Lo……Letter, Other
    • Mn……Mark, Non-Spacing
    • Mc……Mark, Spacing Combining
    • Me……Mark, Enclosing
    • Nd……Number, Decimal Digit
  • このほかにも個別に追加されていると思われる記号がある。例:
    • 「_」アンダースコア
    • 「・」中黒
    • 「゠」ダブルハイフン U+30A0 Katakana-Hiragana Double Hyphen
  • 数字やマークだけではハッシュタグにはならない

ハッシュタグの同値判定の仕様の推測

  • 大文字小文字を区別しない
  • 文字を同一視するためにUnicode正規化が施されているはず。
    • 結合文字を処理する都合から互換分解(NFKD)だと思われる。ツイート自体にはNFCが使われている
    • あるいは、Unicode Collation Algorithmとかを一部利用しているのかも?
  • ある種の結合文字(combining marks)は無視される
    • "#天デ部"と"#天テ\u0301\u3099部"(Combining Acute + Combining濁点)が同一視される(\uXXXXはUnicode文字を表す)
    • 無視される結合文字の一覧は後掲する
    • ダイアクリティカルマーク合成済み文字の場合は、正規化により分解されて、マークが除去される
  • ひらがな・カタカナの、濁点・半濁点つきの文字とそうでない清音の文字は区別される。また、ひらがなとカタカナも区別される
    • (Unicode正規化の互換分解で半角濁点U+FF9Eは合成用濁点U+3099に変換されるので以下の話は当然なので消した)
    • 清音の仮名の次に半角濁点・半濁点が来ると、濁音・半濁音の仮名として扱われる(ひらがなカタカナともに) (間に一部の結合文字が挟まっててもよさそう)
    • 仮名と(半角または合成用)濁点・半濁点の間にU+034F COMBINING GRAPHEME JOINERが入ると濁点が消えるっぽい。なぜ…?
  • ある種の10進数字は、ASCII文字の数字と同一視される
    • Decimal digit valueの示す数字と同一視されるのかと思ったが、Decimal digit valueが存在していてもASCII数字と同一視されないものがあった。次である:
      • U+0de6-U+0def SINHALA LITH DIGIT
      • U+a9f0-U+a9f9 MYANMAR TAI LAING DIGIT
      • U+104a0-U+104a9 OSMANYA DIGIT
      • U+11066-U+1106f BRAHMI DIGIT
      • U+110f0-U+110f9 SORA SOMPENG DIGIT
      • U+11136-U+1113f CHAKMA DIGIT
      • U+111d0-111d9 SHARADA DIGIT
      • U+112f0-U+112f9 KHUDAWADI DIGIT
      • U+114d0-U+114d9 TIRHUTA DIGIT
      • U+11650-U+11659 MODI DIGIT
      • U+116c0-U+116c9 TAKRI DIGIT
      • U+118e0-U+118e9 WARANG CITI DIGIT
      • U+16a60-U+16a69 MRO DIGIT
      • U+16b50-U+16b59 PAHAWH HMONG DIGIT
  • 他にも、特殊な同一視がみられる。個別にルールが追加されているのかもしれない。次のような例を見つけた:
    • "Å"→"AA", "å"→"aa"
    • "Ä"→"AE", "ä"→"ae"
    • "Æ"→"AE", "æ"→"ae"
    • "ı"→“i" (トルコ語用のdotless iの対応)
    • "Ö"→"OE", "ö"→"oe"
    • "Ø"→"OE", "ø"→"oe"
    • "Ü"→"UE", "ü"→"ue"
    • "ß"→"ss" (エスツェット)

無視されるCombining Marksの一覧を次に挙げる。抜けがあるかもしれない。

ignorable_marks = [
    "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0305", "\u0306", "\u0307", 
    "\u0308", "\u0309", "\u030a", "\u030b", "\u030c", "\u030d", "\u030e", "\u030f", 
    "\u0310", "\u0311", "\u0312", "\u0313", "\u0314", "\u0315", "\u0316", "\u0317", 
    "\u0318", "\u0319", "\u031a", "\u031b", "\u031c", "\u031d", "\u031e", "\u031f", 
    "\u0320", "\u0321", "\u0322", "\u0323", "\u0324", "\u0325", "\u0326", "\u0327", 
    "\u0328", "\u0329", "\u032a", "\u032b", "\u032c", "\u032d", "\u032e", "\u032f", 
    "\u0330", "\u0331", "\u0332", "\u0333", "\u0334", "\u0335", "\u0336", "\u0337", 
    "\u0338", "\u0339", "\u033a", "\u033b", "\u033c", "\u033d", "\u033e", "\u033f", 
    "\u0340", "\u0341", "\u0342", "\u0343", "\u0344", "\u0345", "\u0346", "\u0347", 
    "\u0348", "\u0349", "\u034a", "\u034b", "\u034c", "\u034d", "\u034e", "\u034f", 
    "\u0350", "\u0351", "\u0352", "\u0353", "\u0354", "\u0355", "\u0356", "\u0357", 
    "\u0358", "\u0359", "\u035a", "\u035b", "\u035c", "\u035d", "\u035e", "\u035f", 
    "\u0360", "\u0361", "\u0362", "\u0363", "\u0364", "\u0365", "\u0366", "\u0367", 
    "\u0368", "\u0369", "\u036a", "\u036b", "\u036c", "\u036d", "\u036e", "\u036f", 
    "\u0610", "\u0611", "\u0612", "\u0613", "\u0614", "\u0615", "\u0616", "\u0617", 
    "\u0618", "\u0619", "\u061a", "\u064b", "\u064c", "\u064d", "\u064e", "\u064f", 
    "\u0650", "\u0651", "\u0652", "\u0653", "\u0654", "\u0655", "\u0656", "\u0657", 
    "\u0658", "\u0659", "\u065a", "\u065b", "\u065c", "\u065d", "\u065e", "\u065f", 
    "\u0670", "\u06d6", "\u06d7", "\u06d8", "\u06d9", "\u06da", "\u06db", "\u06dc", 
    "\u06df", "\u06e0", "\u06e1", "\u06e2", "\u06e3", "\u06e4", "\u06e7", "\u06e8", 
    "\u06ea", "\u06eb", "\u06ec", "\u06ed", "\u093c", "\u094d", "\u3099", "\u309a"]

ひらがな・カタカナ用の合成用濁点・半濁点も無視される。ものの、濁点や半濁点がついた仮名をそうでない仮名と同一視することはしない(「テ」と「デ」は別)。そのため、濁点や半濁点のつき得る仮名についた合成用濁点・半濁点は削除しない。
これは、Unicode正規化により、NFCなどで合成してから合成用濁点・半濁点を除去するという順序にするとうまくいくと思う。

考えられるハッシュタグの同値判定の流れ

同値判定と書いてるけど、collation (照合)とよぶものらしい。そのために、Unicode Collation Algorithm (UCA)という標準が存在し、それをカスタマイズして使っている可能性もあるが、よくわからない。

さて、ともかく次のような流れで再現できると思われる:

  • 仮名+半角濁点・半濁点→濁音・半濁音の仮名に変換(Unicode正規化(NFKD/NFKC)で普通の仮名になるので略)
  1. Unicode正規化(NFKD)による互換分解
  2. 取り除けるマークを取り除く(合成用濁点・半濁点についてはここでは残す)
  3. 小文字にする
  4. "ı"→“i"、"ß"→"ss"などのルールの適用
  5. Unicode正規化(NFC?)による合成
  6. 合成されなかった合成用濁点・半濁点を除去

難読化の仕組み

上記したハッシュタグの同値判定を逆方向にたどっていくと、最終的に同値に判定される様々なバリエーションが得られる。詳しくはソースでも見てください。行きあたりばったりなので見てわかるかは知らないですが…
github.com

Python3で文字列をUTF-16のコード列(整数のリスト)に変換

Unicode基本多言語面(BMP)外、つまりUnicodeスカラ値がU+10000以降の文字について、(UTF-16の)サロゲートペアのコードを求めたい、という需要があった。これは、Win32 APIのSendInput関数がUnicodeの値をUnsigned Shortでしか指定できないので、BMP外の文字はサロゲートペアで入力しないといけないということに対応するため。

Python3においては文字列(str型)はUnicode文字列を指すので、以下文字列とだけ書く。Python2の場合は適当に読み替えてください。

文字列をUTF-16のコードポイント(整数)のリストに変換

def str_to_utf16codepoints(s):
    bs = s.encode(encoding='utf_16_be')
    return [int.from_bytes(bs[n:n+2], 'big') for n in range(0, len(bs), 2)]

文字列をUTF-16-BE(ビッグエンディアン)の文字列に変換し、2バイトごとに区切って整数に変換する。

str_to_utf16codepoints("sushi寿司🍣")

[115, 117, 115, 104, 105, 23551, 21496, 55356, 57187]

を返す。

UTF-16のコードポイントのリスト→文字列

上の逆変換。

def utf16codepoints_to_str(ns):
    bs = b''.join([n.to_bytes(2, byteorder="big") for n in ns])
    s = bs.decode('utf_16_be')
    return s

要素技術

文字列→UTF-16-BE (bytes型)に変換

str.encode()を使う。返り値はバイト列(bytes型)になる。

"🍣".encode(encoding='utf_16_be')

バイト列を整数(int型)に変換

Python 3.2以降だとint.from_bytes()が使えるらしい。1つ目の引数はバイト列、2つめはエンディアンの指定。

int.from_bytes(b'\xff\xf0', 'big')

みたいな感じでいける。

さもなくば

def int_from_bytes(bytes):
    val = 0
    for b in bytes:
        val = (val << 8) + b
    return val

とかでできると思う。

2020年よかったもの: 買ったもの, 映画, アニメ, 音楽, ソシャゲ

2020年が終わった。他の人がやってるのを見て自分もやってみようと思ったので、2020年に経験したよかったものをまとめておく。

買ったもの

Pixel 4a

今まで使ってたスマホHuawei P10 Liteだったというのもあり、めっちゃ体感が良い。きびきび動く、本体スピーカーがステレオで中低音がそこそこ出る、記憶容量128GBあるのでスマホゲームそれなりにインストールしても余裕ある。など。MicroSDカード刺さんないのを除けば不満はない。
たぶんハイエンドのものと比較したらそれなりなんだろうけど、今まで使ってたものと比べると非常に快適。まじで買ってよかった。

Xiaomi Mi Smart Band 4

活動量計。時計+心拍数計+歩数計+睡眠計とかそんな感じ。自分の使い方だと満充電で1か月弱電池が持つので楽。風呂入るとき以外はずっとつけてる。防水なので風呂入るときもつけたままでいいんだけど。
睡眠がスコアで出るのが面白いし、心拍数変化のグラフがでるの面白い。最近見てないが。
最近はもっぱら腕時計として使っている。ボタン押さないと時間表示されないので腕時計としては微妙。

王 海清『蒙日辞典』


2020年はモンゴル語(特にモンゴル文字)関連書籍をぼちぼち集めていた。
この辞書はモンゴル文字で引ける日本語のモンゴル語辞典として貴重である。しかし、出版されたのがちょっと前というのもあって中古で結構値段した。メルカリで不用品を売って作った金で買った。
いつでも引けるというのが有難くて買ったのではあるけど、実際はそこまで使わないかもしれない。しかし満足感というか安心感がある。

映画

ジョゼと虎と魚たち

アニメ映画。2020年に見た中で一番良かった。本当に90分か??って思うくらい内容が詰めこまれててすごかった。
映画によっては見終わると内容が思い出せないものもあるけれど、この作品は後からいいシーンがいろいろ浮かんできたので、それだけ印象に残ったのかなと思う。
ネタバレになるので多くは語らないが、『聲の形』や『心が叫びたがってるんだ。』が好きならきっと気に入ると思う。(1/8現在)まだやってるのでぜひ見に行ってほしい。

テレビアニメ

春夏は放送される新作がすごく少なかった。

『映像研には手を出すな!』

もともと原作漫画読んでて、アニメ化ということで、期待半分不安半分で見てみた。監督は湯浅政明さん。
アニメを作る話(どちらかというとアニメ作るより設定を作ってる描写のが多いけど)なので、アニメという媒体はすごい合ってるというか、映像による説得力は強かった。作中作であるアニメが映像として動いてるのは漫画では表現できないので。
アニメという媒体の中でアニメを作っているというのを活かした演出が結構あり、全体として湯浅監督の作風と合ってたと思う。創作のワクワク感が感じられるいい作品だった。
FOD独占配信ってことで結構損してると思う。

『ガルパピコ大盛り』

バンドリ見てないけどガルパピコ(1期)は見てたので見た。一話3分なのでサッと見れていい。
バンドリとは異なる独自の世界が展開してる気がする。原作のスピンオフなのに好き勝手やってるのが最高。ギャグ作品にありがちな投げっぱなしな話が多くて好き。
話数によって面白いかどうかはまちまちだけど、実験的な話も多く、すごく面白い話や頭おかしい話があり、そういうのが良くて見てた。

『アサルトリリィ BOUQUET』

アクションドールを原作とするメディアミックス作品。主役の名前が「ゆゆ(夢結)」と「りり(梨璃)」、戦う女の子を「リリィ」と呼び、学校が「百合ヶ丘女学院」などとにかく百合のモチーフぶっこんどきゃいいだろ感を感じる。
「ヒュージ」という怪物が襲ってくる世界で「リリィ」と呼ばれる少女たちが戦う、というのが大筋。まどマギと多少雰囲気近いかも、制作シャフトだし。
百合か?と言われるとそこまで百合要素が中心というわけではない気もする。謎は残しつつも綺麗に終わったのでよかった。ふとももがすごい。

ラブライブ!虹ヶ咲学園スクールアイドル同好会

一話見て百合か!?って感じで見始めた。ラブライブシリーズだけど、ラブライブのある世界というだけで他のシリーズの内容はほぼ出てこないのでこれだけ見て問題なく楽しめる。
スクールアイドル同好会をつくるけど、個性的なメンバーみんなが意見違うのでグループ組まずにソロアイドルで活動していこう!という話。嫌な奴が全く出てこないのですごく視聴感がよい。
最初は一話ごと一人のキャラを掘り下げていったんだけど、5~7話の畳みかけがすごくよかった。りなりー と かすかすがすき。

聞いてた音楽

別に2020年のものではないものが多いが、主にレンタルで聞いてるので一昔前のものになりがち。今年はラジオをよく聞いてたのであんまり音楽を聞いてなかった。
基本的にアニソンが多い。見事に女性ボーカルしかないな。

(~)はCD発売年のはず。配信はもっと早いのあるかも。公式で上がってる奴だけYoutube映像を載せておく。

DIALOGUE+「はじめてのかくめい!」(2019)


アニメ『超人高校生たちは異世界でも余裕で生き抜くようです!』OP。
作詞作曲は田淵智也さん、編曲は田中秀和さん。完全に田淵節。畳みかけるようなめっちゃ忙しい曲で、ハイテンションで愉快。

鹿乃「罰と罰」(2020)

YouTube検索: 鹿乃 罰と罰
作詞が鹿乃さん、作曲が田中秀和さん、編曲が佐高陵平(y0c1e)さん。田中さんいつも強い曲作るな~~~と思っている。ジャジーでダウナーで耳に残る曲。

ネクライトーキー「オシャレ大作戦」(2018)


作詞作曲: 朝日廉さん。
歌詞の「お金もない、努力もしない二十五を過ぎたら死ぬしかない」がすごく印象に残っている。
J-Waveで流れてたライブのCMで知ったバンド。声が特徴的ですき。メジャーデビューアルバム『ZOO!!』(2020)も聞いたけど、インディーズ時代の方が曲が(というか歌詞が)尖ってる感じがした。
アニメ『秘密結社 鷹の爪 ~ゴールデン・スペル~』でOPを担当したのもありどんどん有名になってきてる感じがする。いいですね。

ReoNa「SWEET HURT」(2018)


アニメ『ハッピーシュガーライフ』ED。作詞作曲 ハヤシケイさん、編曲 PRIMAGIC。
歌詞の「腫れた背中のかさぶた 千切れた羽根が生えていた跡 傷を抉っては確かめた まだ血は赤いこと」が印象的すぎる。

一ノ瀬志希宮本フレデリカ「クレイジークレイジー」(2019)


アイドルマスターシンデレラガールズ』の曲。作詞作曲がTaku Inoueさん。
イノタク~~~~~~~!!!!! 曲が強すぎる。Future BassとかJersey Clubとかいうジャンルらしい。クラブっぽい音にメロディで殴ってくる。強い。

shami momo「町かどタンジェント」(2019)


アニメ「まちカドまぞく」OP。
曲がいい。落ちついてるのに疾走感があって、サビの盛り上がりに恍惚感があって最高。
作詞作曲は辻林美穂さん。この曲で辻林美穂さんを知ったのでアルバム『Clarté』を聞いてみたけど、落ち付いててすごいいい感じだった。他のアルバムも聞いてみたい。

にゅーろん☆くりぃむそふと「むにゃむにゃゲッチュー恋吹雪!」(2018)

Youtube検索: むにゃむにゃゲッチュー恋吹雪
ガールフレンド(仮)の曲。ガールフレンド(仮)のキャラソンが結構好きで、キャラクターソングシリーズのVol. 1~7を度々聞き返してるんだけど、新曲が出てたのを遅ればせながら知ったので聞いた。
作詞作曲: 田淵智也さん、編曲: やしきんさん。もろ田淵節って感じで笑う。何食ってたらこんな歌詞でてくんのか謎。

ソシャゲ

D4DJ Groovy Mix

通称グルミク。ブシロードが満を持して送り出してきた、DJをテーマとしたスマホ向けの音ゲー(開発はDonuts)。
とにかく収録曲の選定が謎。スーパーでよく聞く「呼び込み君 No.4」が追加されたというのを見て爆笑しながらインストールすることにした。(編曲がチップチューンとかやってたSEXY-SYNTHESIZERさんでびっくり)
youtu.be

幅広い年齢層に遊んでもらいたいということらしくそれなりに懐メロのカバーも入っている。古いところだと山本リンダ「どうにもとまらない」とか、50年前の曲がバリバリのEDMっぽいミックスになっててすごい好き。他にもクラブ映えするサウンドの曲が多く、プレイしてて楽しい。結構MOGRAとかによくいる人が参加してる。
他にも懐かしのゲーム音楽も多い。あんまり分からないが…
最近もどんどん曲が追加されている。ゲームのテーマがDJなので、カバー曲のみならず原曲とかインストをどんどん追加できるのは強み。

12月初めに始めて毎日やってたらだんだん高レベルの曲もクリアできる様になってきた。たのしい。少なくとも全曲開放するまではやると思う。
ユーザーIDがたぶん oDgpcYR9 なのでフレンド申請ください。nxnkという名前です。


以上。

Windows 10 (バージョン2004)のMS-IMEでやまぶきRの「,」等の入力がうまくいかない

2023-01-14追記

Windows 11では解決してるので上げられる場合はWindows 11に上げましょう。

症状

なんか最近、たぶん12月のWindows Update以来、Windows 10上のMS-IME (Microsoft IME 日本語) + やまぶきR(ローマ字入力用)で「,」「.」などが上手く入力できない時がある。
設定ファイルで、シングルクォート('~')で囲んだ文字は、文字直接入力としてIMEの未確定文字として入力されるはずであるが、それが未確定文字として入力されなくなった。

具体的には次のような症状を示す。(「,」は設定ファイルで「','」として書かれるものを指す。シングルクォート「'」で囲まれた文字直接入力設定の文字ならなんでもよい)

  • 未確定文字列があるときに「,」を入力しようとすると入力されない。または…
    • 未確定文字列の前か後に確定状態で入力される
    • 未確定文字列が消えて「,」だけが残る
    • 未確定文字列が確定される、など
  • 未確定文字列がない場合に「,」を入力しようすると確定した状態で入力される

この動作はアプリによって様々であるが、「,」がIMEの未確定文字として入力できないのは共通している。
Firefox, Chrome, Explorer, Notepad++, Slack, Discord, メモ帳などで試したが、どのプログラムでも再現した。

ATOKではそうならないという情報を得たので、MS-IMEの問題っぽい。実際、以前はMS-IMEを使っていても問題は起きていなかった。Windows 10 May 2020 Update (バージョン2004)によってMS-IMEが新しいものに置き替わり、これに不具合が残っているようだ。結局、MS-IMEの古いバージョンを使うようにしたら解決した。

ソフトウェアのバージョン

  • OS: Windows 10 バージョン 2004 (OS ビルド 19041.685)
  • Microsoft IME 日本語(バージョン 10.0.19041.1*1?)
    • IMJPTIP.DLL, IMJPAPI.DLL, imjpcus.dll, IMJPRANKER.DLL, imjputyc.dllの更新日付がより新しく、12/17に新しくなっている (バージョン 10.0.19041.662)
  • やまぶきR Ver 1.11.1

ワークアラウンド

とりあえずの回避策として、MS-IMEの設定から「以前のバージョンのMicrosoft IMEを使う」をオンにしたら解消される。(とはいえ、互換用に残してるだけだと思うのでいつまで使えるかは分からないが…)

手順は、

  1. 言語バーのMS-IMEの「あ」または「A」の表示となっている部分を右クリック→「設定(S)」
  2. 「全般」
  3. 「以前のバージョンのMicrosoft IMEを使う」をオンにする

あるいは、

  1. Win+X (または画面左下端のWindowsロゴを右クリック)→「設定(N)」
  2. 「時刻と言語」
  3. 「言語」
  4. (「優先する言語」の下から)「日本語」→「オプション」
  5. (「キーボード」の下から)「Microsoft IME」→「オプション」
  6. 「全般」
  7. 「以前のバージョンのMicrosoft IMEを使う」をオンにする

とする。

もしくは、ATOKGoogle日本語入力などほかのIMEを使うと良さそう。

検証

すべてMS-IME (Microsoft IME 日本語)による。

やまぶきR

上記の通り。省略。

DvorakJ

「,」「.」等、その他Unicode文字として出力するように設定されたものについて、同様の症状が発生する。

紅皿

README.pdfによると「Ver. 0.1.4.2 … Windows 10 May 2020 Updateに対応するため、アプリに出力する文字をすべて半角または制御記号とした。」とあり、Windows 10 May 2020 Update (バージョン2004 (20H1))に問題があることが示唆されている。文字直接入力に相当する機能がないため、問題なく動く。

Win32 API

というか、DvorakJや紅皿が使っているプログラミング言語AutoHotKeyがどうやってUnicode文字列を送ってるかを調べたら、どうやらWin32 APIのSendInput()を使ってるっぽい。やまぶきRも同じかもしれない。

SendInput関数の引数の一つとしてINPUT構造体を与えるが、その内部に設定されるKEYBDINPUT構造体に wScan=ユニコードスカラ値, dwFlags=KEYEVENTF_UNICODE を設定して、その状態でSendInput関数を実行すると、例えば漢字などのUnicode文字が入力できる*2

Python3用のコードを置いておく。コードは、

にあるものに変更を加えたものである。

# https://stackoverflow.com/questions/62189991/how-to-wrap-the-sendinput-function-to-python-using-ctypes
import ctypes
import ctypes.wintypes
import time

KEYEVENTF_UNICODE = 0x4
KEYEVENTF_KEYUP = 0x2
INPUT_KEYBOARD = 1

# not defined by wintypes
ULONG_PTR = ctypes.c_ulong if ctypes.sizeof(ctypes.c_void_p) == 4 else ctypes.c_ulonglong

class KEYBDINPUT(ctypes.Structure):
    _fields_ = [('wVk' ,ctypes.wintypes.WORD),
                ('wScan',ctypes.wintypes.WORD),
                ('dwFlags',ctypes.wintypes.DWORD),
                ('time',ctypes.wintypes.DWORD),
                ('dwExtraInfo',ULONG_PTR)]

class MOUSEINPUT(ctypes.Structure):
    _fields_ = [('dx' ,ctypes.wintypes.LONG),
                ('dy',ctypes.wintypes.LONG),
                ('mouseData',ctypes.wintypes.DWORD),
                ('dwFlags',ctypes.wintypes.DWORD),
                ('time',ctypes.wintypes.DWORD),
                ('dwExtraInfo',ULONG_PTR)]

class HARDWAREINPUT(ctypes.Structure):
    _fields_ = [('uMsg' ,ctypes.wintypes.DWORD),
                ('wParamL',ctypes.wintypes.WORD),
                ('wParamH',ctypes.wintypes.WORD)]

class DUMMYUNIONNAME(ctypes.Union):
    _fields_ = [('mi',MOUSEINPUT),
                ('ki',KEYBDINPUT),
                ('hi',HARDWAREINPUT)] 

class INPUT(ctypes.Structure):
    _anonymous_ = ['u']
    _fields_ = [('type',ctypes.wintypes.DWORD),
                ('u',DUMMYUNIONNAME)]

#print(sizeof(INPUT))

SendInput = ctypes.windll.user32.SendInput
SendInput.argtypes = ctypes.wintypes.UINT,ctypes.POINTER(INPUT),ctypes.c_int
SendInput.restype = ctypes.wintypes.UINT

def send_unicode(s):
    i = INPUT()
    i.type = INPUT_KEYBOARD
    for c in s:
        i.ki = KEYBDINPUT(0,ord(c),KEYEVENTF_UNICODE,0,0)
        SendInput(1,ctypes.byref(i),ctypes.sizeof(INPUT))
        i.ki.dwFlags |= KEYEVENTF_KEYUP
        SendInput(1,ctypes.byref(i),ctypes.sizeof(INPUT))

if __name__ == '__main__':
    time.sleep(3)
    send_unicode('漢')

これを試してみたところ、日本語入力がオンのときにSendInputで入力された「漢」は、MS-IMEでは確定状態になったが、Google日本語入力では未確定状態となった。それっぽい。

*1:C:\Windows\System32\IME\IMEJP\imjpuexc.exe の右クリックメニューからプロパティ→詳細→製品バージョン。関係ないけどWindows 8の時はMS-IMEのバージョンが15.いくつになっていたようで、番号が若返ってる気がする、というかOSバージョンと統一されたっぽい

*2:Unicodeスカラ値を指定するwScanはUnsigned Short (16bit)である。16ビットで表せない基本多言語面(BMP)以外のコード値はどう入力するかというと、サロゲートペアを使うといいらしい。 https://stackoverrun.com/ja/q/6096887 を参照。