アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

不自然な濁点半濁点で「びら゙がな゙」「カ゚タ゚カ゚ナ゚」にする謎の文字

日本語なデータをばっさばっさ処理しているとき、変な文字に出くわした。

…なんじゃこりゃ?

しかもフォントやフォントサイズやその他諸々によって見た目が変わる。
↑はわたしには変形した小さい「パ」みたいに見える。ちょっと拡大すると○に小さい○?がついてる。
というかこれを再変換しようとすると「環境依存文字」の注釈が出るので、見えない場合すらありそう。

これ何かというと、濁点です。

か ゙   // 「か」+全角スペース+謎の文字
が     // 「か」のあとに謎の文字をコピペした状態
が      // ただの「が」

…2番目と3番目が同じに見える。(少なくともわたしの環境からは)
でもサクラエディタなどにコピペすると「か」と「変形した小さいパ」に見える。

まさかと思って試してみた。
いろんな文字の次に貼り付けてみる。

あ゙
ー゙
ゲ
ヶ゙
文゙字゙

ぎも゙ぢわ゙る゙い゙よ゙ぉ゙ぉ゙ぉ゙

「濁点を付けた一文字として解釈させる文字」というところでしょうか…。
そういえばそういう文字をネットで見かけたことがあるような気もする。

ところでこれをURLエンコードしてみると

%E3%82%99

になります。

前後に何があるかというと…

// %E3%82%97
゗
// %E3%82%98
゘
// %E3%82%99
゙
// %E3%82%9A
゚
// %E3%82%9B
゛

あっやっぱりなんか似たやつがある…

゚
ぱ // 「は」+謎の文字
ぱ  // ただの「ぱ」

あ゚
ー゚
ケ゚
ヶ゚
文゚字゚

う゚わ゚あ゚あ゚き゚も゚ち゚わ゚る゚い゚よ゚ぉ゚ぉ゚ぉ゚



…という遊び方をTwitterとかでやる分にはいいんですけど
なんで業務用ソフトのデータに入ってるんだろう…。
しかも「が」と「が」みたいな普通に使う文字というパターンだったので、目視で違いが分からなくて困った…
(↑も一部テキストエディタなどに貼り付ければ違いがわかるよ!)

<追記>
どうやら、Unicodeには
文字+濁点を1文字で表現する「NFC」と
濁点だけで1文字とする「NFD」があり(この謎の濁点はこっち)、
主にMac絡みで出てくるようです。

参考:
http://d.hatena.ne.jp/yohei-a/20170506/1494065661
https://qiita.com/takuyabe/items/ac13aa99306ad69743e7
http://tama-san.com/combining_character_sequence/
</追記>

検索にマッチした行を削除するマクロ

サクラエディタで、検索にヒットしたワードを含む行を丸ごと削除したいということがあり、
その数が膨大だったのでサクッとマクロを組んだのをメモ。

結果としてはVBSで書きました。

Dim x
Do While True
    ' 次を検索
    ' (戻り値は使用しないが、変数に入れないとエラーになる…)
    ' 第1引数は検索文字列(空なら現在の設定)
    ' 第2引数はオプション(後述)
    x = Editor.SearchNext("", 38)

    ' 選択範囲が存在する場合は行ごと削除する
    If Editor.GetSelectedString() <> "" Then
        ' 選択を解除するための動作
        Editor.Right()
        Editor.Left()
        ' 行削除
        Editor.DeleteLine()
    Else
        Exit Do
    End If
Loop

通常の検索やこのへんのマクロ( 指定の文字列と同じものをすべて強調表示するマクロ - アナログCPU:5108843109 )と併用する前提。
検索後にこれを実行すれば、対象行がごっそり消えます。
数万行あるようなテキストだと数分かかりました。人力より全然良いけど。

解説

「検索する→ヒットした文字列を含む行をまるごと削除する」
の流れを繰り返しているだけです。
ヒットした文字列は選択状態になるため、選択範囲に何もない場合は繰り返しを終了します。

Dim x
Do While True
    ...
Loop

ただの変数宣言とwhileループです。
以下はその中身。

Editor.SearchNext("", 38)
If Editor.GetSelectedString() <> "" Then
    ...
Else
    Exit Do
End If

SearchNext関数は、通常の検索で「下検索」ボタンを押したときと同じ動作をします。
第1引数は検索する文字列で、空文字列を指定すると、現在の検索対象が採用されます。
第2引数は検索用のボックスにあるものと同じオプションです。

オプションの決め方はちょっとややこしく、
2進数の「000000」に対し、(オプション番号)桁目を1にして、
それを10進数にしたものを設定します。

もうちょっと具体的な例にすると、まず、以下からオンにするものを選びます。

  • 0 単語単位で探す
  • 1 英大文字と小文字を区別する
  • 2 正規表現
  • 3 見つからないときにメッセージを表示
  • 4 検索ダイアログを自動的に閉じる
  • 5 先頭(末尾)から再検索する

例えば「英大文字と小文字を区別する」「先頭(末尾)から再検索する」をオンにする場合、
1桁目と5桁目が1である2進数を用意します。
この桁数は右から数えるので、「100010」となります。
これを10進数に変換すると34になるので、これを第2引数にセットします。

今回は正規表現にしたかったので「100110」、10進数で「38」をセットしています。

参考:
S_SearchNext


その下はただのif文。
GetSelectedString関数で選択範囲の文字列を取得できるので、
その中身が空でない場合のみ処理を行います。

Elseブロックの「Exit Do」は他言語で「break」等に相当するものです。
要するにWhileブロックから抜けます。

以下はIfブロックの中身について。

Editor.Right()
Editor.Left()
Editor.DeleteLine()

カーソルを右に動かし、左に動かし、カーソルが存在する行をまるごと削除する。
範囲選択状態だとDeleteLine関数が動作しないのでこのような挙動となっています。
(もっとスマートなやり方はあるのだろうか…)

サクラエディタのキーマクロでは?

なんとif文が使えないらしく、今回は上記のようにVBSで実装しました。

S_SearchNext('', 38);
S_Right();
S_Left();
S_DeleteLine();

一応、こんな感じであれば「次を検索して削除」を1回だけやってくれるマクロになります。
が、検索結果が見つからなかった場合も1行削除されてしまうという…。

コードギアス叛道振り返りコメンタリー上映のメモ

5/25に叛道のコメンタリー上映&皇道の最速上映を見てきました。
暗い中手探りでメモを残していたのを書き起こしておいたやつを今更ながらpost。

  • 何故か半笑いで登場する一同
  • 「叛道を今日初めて見る人いますか?」→ちらほら
  • 「アニメ版見てない人いますか?」→いない
  • 「前回のコメンタリー上映来た人いますか?」→過半数、多くて8割くらい
  • 今回のコメンタリーはTV版の内容多めですという予告
  • 「しゃべってないけど大丈夫ですか?」「ちゃんと聞いてますよ」「しゃべってください」
  • ユフィの特区日本宣言はルルーシュたちが最高に幸せなタイミングで、という演出のため学園祭になった
  • シュナイゼルがコーネリアをほめるときに後ろで頷いているギルフォード
  • シュナイゼルとコーネリアは同い年(29歳)
  • ↑二人の年齢を設定した後に誕生日を設定したらシュナイゼルが年下になっていたので後から直した
  • 脚本大河内さんはジェレミアを早めに殺すつもりでいたが、CLAMPデザインだし人気もあり…
    • 「むしろバトレーをあそこまで描く必要あった? 関わらねばよかったのくだりいらなくない?」
  • "咲世子は忍者" は最初ネタのつもりだったが新井さんのキャラも強すぎてそのまま実現へ
  • 「そもそも2部のコメンタリー難しい」
  • 血染めのユフィ、今ではできない表現(放送コード的に)
  • ネタ・おふざけのコメンタリーに持っていこうとしているのに映像に引っ張られまくるコメンタリー陣
  • シャルルのセリフ(あやつ、やりおったか)は実はルルーシュではなくユフィに対する言葉
  • ユフィが死ぬところでついに完全に無言になるコメンタリー陣
  • 血染めのユフィ後のルルーシュ演説時のクオリティ高いモブは千羽さん作画
  • 「しゃべってないけど大丈夫ですか?」「ちゃんと見てますよ」「しゃべってください」
  • ルルーシュ作画の基本は腰クイ
  • コンタクトレンズのサイズに困った(キャラの目に合わせるべきか、手に合わせるべきか)
  • キャラクターが細長いので、体型に合わせた家具などの作画が難しい
    • これに慣れたら他のアニメで描けなくなる、との声も
  • 当初は名前がつけられていなかったグラストンナイツ
    • "とりあえずイケメン集団"という指定
    • キャラ名も後から付けた
      • ルフレッド、バート、クラウディオ、デヴィッド、エドガーで頭文字がABCDE
  • ルルーシュと枢木で「二人のルル」
  • ブラックリベリオンの頃、制作現場もブラックリベリオンだった
    • 人の入れ替わりが激しい
    • 怒号が飛び交う
    • コーネリアばりに谷口さんが叫んでる
  • シュタットフェルト家は金で名誉を買った
  • ジノは大型犬
  • ルルーシュ、絶対ヴィンセント爆破するつもりでロケット渡してる
  • V.V.のギアスマークは首のうしろ
  • フレイヤの放送時期を逆算するとどうやっても8/6頃になるので核爆弾の設定をやめた


…自分のメモのうち、ひとつだけ解読できなかったんですが
「のみかい カレン」って何だろう…

正規表現で、複数のワードが順不同で含まれているかどうかを判定する

※PHP5.6前提。他だと方言差がある可能性もあり。


とりあえず結論から書くと、
文字列「abcde」に「b」「d」が順不同で含まれているかどうかは、
例えば以下のような書き方のいずれでもヒットさせられます。

preg_match("/(b.*d)|(d.*b)/", "abcde");
preg_match("/^(?=.*b)(?=.*d)/", "abcde");
 // ↑「^」はなくても動きますが、つけた方が速いようです

前者は正規表現初心者の方でもさほど難しくない考え方かと思います。

/b/             文中に「b」が含まれていればヒット、「abc」はOKだが「acd」はNG
/b.d/           文中に「b(任意の1文字)d」が含まれていればヒット、「abcde」はOKだが「abccde」はNG
/b.*d/          文中に「b(任意の0文字以上)d」が含まれていればヒット、「abde」でも「abccccde」でもOK
/d.*b/          ↑と同様に、「d(任意の0文字以上)b」が含まれていればヒット
/(b.*d)|(d.*b)/ ↑と↑↑のいずれかがヒットすればよいので、ORの意の「|」でつなぐ

後者の書き方は今まで知らなったのですが、
「先読み」「後読み」という概念のようです。

追記
別記事にまとめてみました。
正規表現の先読み・後読み - アナログCPU:5108843109
</追記

これを書いている時点ではかなりふんわりした認識なのですが、
例えば「/b(?=d)/」だと、「(直後にdをもつ)b」にマッチします。
(ヒットするかどうかでは「/bd/」と同じですが、これは「bd」にマッチします)
かつ、「(?=d)」の部分は「^」や「$」と同じように「位置」を表すものです。

先述のパターンに戻ってみます。

/^(?=.*b)(?=.*d)/

先読み後読みの前に、
「.*b」というパターンは「b以前の文字列すべて」であり
「.*d」というパターンは「d以前の文字列すべて」ですね。
そして「(?=○)」は○の位置を表す、と。

つまり、文字列「abcde」について言えば
「^」は「先頭位置」、つまりaの部分
「(?=.*b)」は「abの位置」、つまりaの部分
「(?=.*d)」は「abcdの位置」、つまりaの部分
…ということで、これら3つはすべて同じ位置を指しているということになります。
実質的に「/^/」と同じということになり、そりゃ順不同でもヒットするわ、なるほどなー…と納得しました。

実際には書かないでしょうが

/(?=.*b)(?=.*d)^/

で試してみてもヒットしました。

参考:
正規表現の先読み・後読みを極める! - あらびき日記

PostgreSQLのエスケープ

めっちゃくちゃどうでもいいんですけど、
いつも何故だか「エスケープ」と「エンコード」という単語がごっちゃになる。

しかも大体の場合、
エスケープについて調べたいときは
「なんだっけ…あの…エンコードじゃないやつ…」となり
エンコードについて調べたいときは
「なんだっけ…あの…エスケープじゃないやつ…」となる。
困る。

あと、実はもう一つごっちゃになる単語があった気がするのだが今はそれが出てこないモード。


それはともかくとして、PostgreSQLエスケープについて調べたり試したりしたのをメモ。

参考:
PostgreSQLの文字列のエスケープ: ぷ~ろぐ

とりあえず普通の文字列

SELECT 'hoge'; -- hoge

シングルクォートを文字として扱う

MySQLと違って、文字列はシングルクォートで囲む必要があるので
シングルクォートは必ずエスケープが必要になります。

SELECT 'h''oge'; -- h'oge
SELECT 'h'oge';  -- (エラーになる)

このように、シングルクォートはシングルクォートでエスケープできます。

バックスラッシュではエスケープできません。

SELECT 'h\'oge'; -- (エラーになる)

そもそもバックスラッシュは普通の文字として扱える

バックスラッシュはそのまま文字として使うことができます。

SELECT 'h\oge';    -- h\oge
SELECT 'h\no\tge'; -- h\no\tge

改行やタブ文字を入れたいときは…

文字列の前に「E」を付加することで、「\n」「\t」などが使えるようになります。
ついでにシングルクォートもバックスラッシュでエスケープできます。

SELECT E'h\no\tge'; -- h(改行)o(タブ文字)ge
SELECT E'h''oge';   -- h'oge
SELECT E'h\'oge';   -- h'oge

ゼロ幅スペースとは

PHPにて、半角文字以外が含まれるかどうかを判別したく以下コードを書いてたんですよ。
正規表現部分は「!から~までの文字と半角スペース 以外」ですね。

if (preg_match("/([^!-~\s]+)/", $str, $matches))
{
    var_dump($matches[0]);
}

その中、こんな出力をされるデータがありました。

string(3) "​"

いやいや、空文字に見えるんですが…。しかも3バイトもあることになってる。

これをurlencode関数にかけてみると「%E2%80%8B」というのが出てきました。

なんやこれ…と思って調べてみると、なんと「幅がゼロのスペース」。
なにそれ??なんのために存在すんの????

だいぶ意味がわからないのですが、
存在してしまっているものは仕方ないので、必要に応じて考慮が必要ですね。

ほかにも様々なスペースがありますが、( スペース - Wikipedia
とりあえずここではこのゼロ幅スペースを削除する方法を考えます。

// 1. ピンポイントで消す
$str = str_replace("\xe2\x80\x8b", "", $str);

// 2. 他のいらなさそうな制御文字も併せてごっそり消す
$str = preg_replace("/\p{C}/u", "", $str);

1はとにかくこのゼロ幅スペースのみを消したい場合。
2は制御文字全般を消してくれる…はず。
PHP: Unicode 文字プロパティ - Manual


参考:
[PHP]改行なしスペース(&nbsp;、0xA0)を、普通の空白(0x20)に置換する
空白を削除する
マルチバイト(全角スペース等)対応のtrim処理
空白を削除する
<U+200B>(幅なしスペース)というやつに悩まされた話 | 自転車で通勤しましょ♪ブログ
リンク切れでURLに「%E2%80%8B」が見つかった場合の対処法 | colori

こんなにも簡単に愛は生まれる

調べもの中に偶然面白記事を見つけたのでPHPでも書いてみた。

なんで愛が生まれるのか
愛を生む二人を探して

echo '生' & '死';

お手軽にフフッと笑えたけどそんなことより仕事しなければ。

ローカルのCSVファイルをアップロードして処理

ローカルからCSVファイルをアップロードしてその内容を処理する機能を作ることがあったのでメモ。
といってもアップロード機能自体はめちゃくちゃ簡単だった。エラーチェック系が沼。

HTML

<!-- 【※】はアップロードしたファイルを処理する部分のパス -->
<!--   例:"/csv.php" -->
<form enctype="multipart/form-data" action="【※】" method="POST">
  <input name="userfile" type="file" />
  <input type="submit" value="送信" />
</form>

これだけでファイル指定するボタンと、それを送信するボタンができます。
あとは【※】で指定したところで受け取って処理するだけ。

PHP

<?php
// エラー処理
switch ($_FILES['userfile']['error'])
{
	case UPLOAD_ERR_OK: // 正常(アップロード成功)
		// 正常
		break;
	case UPLOAD_ERR_INI_SIZE:  // サイズオーバー(PHP側の設定値)
	case UPLOAD_ERR_FORM_SIZE: // サイズオーバー(HTMLフォーム側の設定値)
		echo "サイズオーバーです。";
		exit();
		break;
	case UPLOAD_ERR_PARTIAL: // 一部のみアップロード
	case UPLOAD_ERR_NO_FILE: // アップロードされなかった
		echo "アップロード失敗です。";
		exit();
		break;
	case UPLOAD_ERR_NO_TMP_DIR: // アップロード先である一時フォルダが存在しない
	case UPLOAD_ERR_CANT_WRITE: // ディスクへの書き込みに失敗した
	case UPLOAD_ERR_EXTENSION:  // PHPの拡張モジュールがファイルのアップロードを中止した
	default:
		echo "不具合レベルのエラーです。";
		exit();
		break;
}

// ファイルオープン
$fp = fopen($_FILES['userfile']['tmp_name'], "r");

// ファイルの中身を配列に移す
$csv_list = array();
while (($csv_list[] = fgetcsv($fp)) !== false){}
fclose($fp);

//// この時点で、「$csv_list」はCSVの中身が丸ごと配列になったものなので、
//// あとはそれを好きに処理するだけ

// 終了:とりあえず取得した他の情報も表示してみる
echo $_FILES['userfile']['name'] . "の処理が完了しました。<br>";
echo "ファイルタイプ:" . $_FILES['userfile']['type'] . "<br>";
echo "サイズ:" . $_FILES['userfile']['size'] . "バイト<br>";
echo "レコード数:" . count($csv_list) . "<br>";

以上。

とりあえず冒頭にエラーコードによる分岐だけは入れてますが、
その他の例外処理はほとんど入れてないです。必要に応じて増やす必要あり。
参考:
ファイルアップロードの例外処理はこれぐらいしないと気が済まない


また、グローバル変数$_FILESの中身はこちらに記載があります。
参考:
PHP: POST メソッドによるアップロード - Manual


このコードでは、中身はさっさと配列に落とし込んでおり
処理部分はもはやファイルアップロード関係ないのでがっつり省略。
1行ずつ処理したいだけであれば別にwhileの中でやってもOK。
先に全体の件数を知りたいとか、前後のレコードの情報が必要になる場合があるとか、
データによって処理順を変えたいとか…
そういう場合はもちろんこのように先に配列化した方が明らかにやりやすい。

ちなみに処理が終了すると、アップロードされた一時ファイルはさっさと消えてくれます。
中身を処理してポイではなくサーバーに保存したい場合はコピーするなどの必要あり。

続きを読む

PostgreSQLの数値文字列変換

<20190611 追記>
何故か最近この記事へのアクセスが急増しています
PostgreSQLは結局ほとんど使っていないので、変なところとかもっと良い方法があるよとかそういうのはどしどし突っ込んでいただけるとありがたいです。
</追記>

まだうまく飲み込めてないのでなんともですが、
PostgreSQLMySQLに比べると暗黙の型変換をしてくれるところが少ないような気が。
その割に型変換するのに若干クセがあって忘れそうなので、いろいろ試してみたのをメモ。

数値→文字列

たぶん一番よく使うやつ。
TO_CHAR関数に、数値とフォーマットを与え、フォーマットどおりの文字列にしてくれます。

SELECT  
  TO_CHAR(1234, '99999')   -- '  1234'(ゼロ埋めしない+左に謎のスペース)
 ,TO_CHAR(1234, 'FM99999') -- '1234'(ゼロ埋めしない)
 ,TO_CHAR(1234, '9')       -- ' #'(桁が足りないとこうなる+左に謎のスペース)
 ,TO_CHAR(1234, 'FM9')     -- '#'(桁が足りないとこうなる)
 ,TO_CHAR(1234, '00000')   -- ' 01234'(左側をゼロ埋め+左に謎のスペース)
 ,TO_CHAR(1234, 'FM00000') -- '01234'(左側をゼロ埋め)

2番目の「FM999...」のパターンを一番よく使うかなと思うのですが、充分な桁数が必要というのが厄介。
仕様上最大になる桁数で書いておけばいいんですが、桁が大きくなる場合はあんまりスマートじゃない気が…

続いて負数と小数のいろいろ。

SELECT  
  TO_CHAR( 1234.5, '99999')       -- '  1235'
 ,TO_CHAR(-1234.5, '99999')       -- ' -1235'
 ,TO_CHAR(-1234.5, 'FM99999.99')  -- '-1234.5'
 ,TO_CHAR(-1234.5, 'FM99999.00')  -- '-1234.50'

FMを付けない場合、謎の左側スペースのうち
ひとつは符号用、残りは単にゼロで埋める代わりにスペースが入ってるだけ…みたいですね。
そして整数化するときは四捨五入されています。
小数として扱う場合は一番下のパターンをよく使うことになるかな。

その他のフォーマットも可能です。
クエリ内で必要にならない限りは使わないと思うけど。
(「見た目に関わる内容はsmartyやjsでやるべき(どうしてもという場合はPHP)」派)

SELECT  
  TO_CHAR(1234567, 'FM999,999,999') -- '1,234,567'(カンマ区切り)
 ,TO_CHAR(    123, '[9999]')        -- '[  123]'(何かで囲んでみる)
 ,TO_CHAR(    123, '[FM9999]')      -- '[123]'(↑だと案の定スペース入ったのでFM付ける)
 ,TO_CHAR(    123, 'FM[9999]')      -- '[123]'(↑位置を変えてみても大丈夫だった)
 ,TO_CHAR(    123, '[9999]FM')      -- '[123]'(↑最後でもええんかい!)
 ,TO_CHAR(    123, '/9/99/99/')     -- '/ /  1/23/'(使いどころはわからないが動く)
 ,TO_CHAR(    123, 'FM/9/99/99/')   -- '//1/23/'(↑のスペース削除版)
 ,TO_CHAR(   -123, '/9/99/99/')     -- '/ / -1/23/'(負数だとこんな感じ)

文字列→数値

今度は数値への変換なので
TO_NUMBER関数に文字列とフォーマットを与え、
フォーマットどおりに読んだ結果を返してくれます。

SELECT
  TO_NUMBER('123456'  , '9999999')     -- '123456'(素直にそのまま)
 ,TO_NUMBER('123456'  , '9')           -- '1'(桁数が足りないと指定桁数分だけ左から読む)
 ,TO_NUMBER('12_34_56', '99_99_99')    -- '123456'(変な形式の場合はフォーマットでも同様に指定)
 ,TO_NUMBER('12_34_56', '999999')      -- '1234'(桁数が足りないと左から読み、数字だけ抽出する模様)
 ,TO_NUMBER('12_34_56', '999999999')   -- '123456'(桁数が十分あればいい感じに数字だけ抽出する模様)
 ,TO_NUMBER('12_34.56', '999999999')   -- '123456'(勝手に小数と認識してくれたりはしない)
 ,TO_NUMBER('12_34.56', '9999999.999') -- '1234.56'(小数として扱いたいときはドットを適当な位置へ)
 ,TO_NUMBER('-2_34.56', '9999999.999') -- '-234.56'(負数は認識してくれる)

参考
PostgreSQLで to_char()すると前に半角スペースが入る - かわろぐ
https://www.postgresql.jp/document/9.4/html/functions-formatting.html

特定の文字より手前を切り出す

別に難しい話とか裏技とかではないですが、
たまに使うのを毎回ガチャガチャ書くのが面倒なので自分のコピペ用に。

例えばメールアドレス「hogefuga@example.com」の「@」より前、
つまり「hogefuga」のみを切り出したいときなどに。

$str = "hogefuga@example.com";
echo substr($str, 0, strpos($str, "@"));

全角文字を扱うシステムの場合はあまり深く気にせずとりあえずmb系にしてしまってもいいかも。

$str = "hogefuga@example.com";
echo mb_substr($str, 0, mb_strpos($str, "@"));