みろりHP


緑色さんの多目的ブログ
みろりえいちぴー
ごゆるりとおくつろぎあさーせ。
| カテゴリ:プログラミング |
C# GCCS-CS1.0 CoCキャラメイクサポートツール



みろりHPのファイル置き場 - GCCS-CS1.0
--- 2017.04.01.追記 ---
Windows7において、管理者権限でないとテキスト出力ができない不具合があるっぽい。ゴメン。


これはクトゥルフTRPGキャラクターメイク用ツールだ。みろり通の貴兄らは「またこれ!?」とお思いであろうが、そう、またこれである。これまでに三回おんなじもの作ってる。ただ今回は作成に使ったプログラミング言語がこれまでと違う。最初のふたつがPythonで、次がPHPで、今回はC#だ。なんか知らんけど、PythonのプログラムはPythonのインストールされてるPCでしか動かないのに対し、C#で作るアプリはWindowsのデフォルト機能を利用しているそうだ。だから、アプリケーション本体のサイズが軽くなるんだってさ。それで興味をもったのだよ。Pythonプログラムを仲間に配るとき、いつもフォルダ単位のパッケージになっちゃうのが気になっていたからね。今回はたったひとつのexeファイルのみだ。しかもサイズが100KBちょい。Pythonの同プログラムは、パッケージまるごとで16MBだぜ。すなわちさ、前回よりも機能がアップしているクセにファイルサイズは160倍軽くなってるわけだ。スゲーな。



今回のバージョンの面白い機能を下に並べてみる。前バージョンを実際に使ってもらったフィードバックを反映した結果だ。
  • ルルブの職業を選択でき、それぞれの対応技能をツール上で閲覧できる。
  • さらに対応技能にチェックがつくんで技能振りのユーティリティが上昇。
  • 武器もネットにあったものをいくつかピックアップして選択できるようにしてる。これも詳細ボタンクリックで説明を閲覧できる。
  • 画面全体をタブ分けして、アイテム欄やコメント欄を拡大した。
  • 以前からのステータス一括ランダム作成に加え、個別ロールも可能に。
  • 以前は収入の項目がなかったのだけど、追加した。
  • テキスト出力するとき自分で置き場所とかを選べるようにした。Windowsユーザにはお馴染みの「名前をつけて保存」みたいなダイアログも出るよ。なんかホンモノのアプリケーションみたいだぜ。
なお、開発はWindows10上で行い、スクリーンショットもそこで撮影したものなんで、他Windowsでちゃんとデザインが表示されるかはわからね。


こんな感じ。



冒頭でC#すごーいみたいなこと書いたけど、Pythonと全然性格が違くてめちゃめちゃ苦労した。Pythonと違って制作は全部Visual Studioなるソフトの上で行ったんだけど、ソフトの使い方を覚えるのが一等かったるい。苦労話なんて、ほとんどが「ソフトの使い方わかんねえ!」みたいなもんだ。C#のスクリプトを書くこと自体はあんまり苦労しなかった。こいつは静的型付け言語っていって動的型付けのPythonとは違うみたいなんだけど、わりと手に馴染むのを感じた。なんかカッチリしてて、書くの楽しめたよ。根が真面目だからよう、へへへ。
Visual Studioのある程度の操作を把握しちまえば、GUIをお絵かきみたいに作成できるから楽になっていく。が、それをいいことにお絵かき感覚で100個以上のテキストボックスを置いたせいで、製作中の管理は至難を極めた。ボックスがちっちゃいから全部ちっとずつ大きくしよう、なんてことを思いついたが最後、延々と100個のテキストボックスをクリックする作業のスタートだ。それもこれもすべて作りが荒いせい。C#なんて触ったことないからムリもないんだけどさ。次に触ることがあれば、全部のコントロールをグループごとにまとめて管理するようにしないとやってられんわ。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Sublime Text3のMac環境づくり



以前も似たようなもの(Sublime Text3の初期設定)を書いたけれど、あれから結構使い込んだので改めて環境づくりをまとめておくぜ。いやあMacは色々勝手が違ってねえ。



インストール
公式サイトでDL。

Package Control入手
コンソール開いてPackage Controlのサイトからスクリプトをコピペ。

Packageいろいろ入手
俺が使うのは以下。Command+Shift+Pからinstall入力してザクザクとインストール。
  • Alignment : Command+Ctrl+Aで実行。設定を後述。
  • BracketHighlighter
  • Codecs33
  • Color Highlighter
  • ColorPicker : コマンドパレットでcolorpickerと打って実行。
  • ConvertToUTF8
  • GitGutter
  • IMESupport
  • Increment Selection : 複数カーソルで数字を選択し、コマンドパレットからincrement..で実行。
  • Japanize : 設定を後述。
  • Quick File Open : Command+Shift+Pで実行。設定を後述。
  • SublimeLinter
  • SublimeLinter-php
  • Sublimerge Pro
  • Theme - Nexus
  • TrailingSpaces : 設定を後述。

Alignmentの設定
ST3ユーザフォルダの Base File.sublime-settings にこう書く。
{
    "alignment_chars": [
            ":",  "=", "=>"
        ],
    "alignment_space_chars": [
            "=", "=>"
        ],
}
実行ショートカットはCommand+Ctrl+Aなんだがsublimelinterのショートカットとかぶっちまって動作しない。Default (OSX).sublime-keymap にこう追記。
{ "keys": ["ctrl+super+shift+a"], "command": "sublimelinter_show_all_errors" },
{ "keys": ["ctrl+super+a"], "command": "alignment" },

Japanizeの設定
  • 1. /Packages/Japanize の中の.jpファイル6つをコピー。
  • 2. その連中を /Packages/Default に貼り付け(Defaultフォルダは自分で作成)。
  • 3. Defaultフォルダ内のファイル名から .jp を削除する。
  • 4. /Packages/Japanize の中の Main.sublime-menu を /Packages/User にコピー。

Quick File Openの設定
ST3ユーザフォルダの QuickFileOpen.sublime-settings にこんな感じで書く。
{
    "files": [
        "/Users/Username/Dropbox/付箋.txt",
        "/Users/Username/Dropbox/ToDo.txt",
        "----",
        "/Users/Username/.bash_profile",
        "/Users/Username/.bashrc",
    ]
}

TrailingSpacesの設定
ST3ユーザフォルダの trailing_spaces.sublime-settings に書く。
{
    "trailing_spaces_regexp": " |[ ¥t]+",
    "trailing_spaces_highlight_color": "comment",
}

Pythonビルドの設定
ST3ユーザフォルダに Python.sublime-build 作ってこれを書く。俺はpyenvを使っているのでこうなる。
{
    "cmd": ["python", "-u", "$file"],
    "file_regex": "^[ ]*File ¥"(...*?)¥", line ([0-9]*)",
    "selector": "source.python",
    "shell":true,
    "windows":
    {
        "encoding": "cp932",
        "path"    : "C:/Python34"
    },
    "osx":
    {
        "path": "/Users/Username/.pyenv/shims/python",
    },
}

PHPビルドの設定
同じく PHP.sublime-build 作る。
{
    "cmd": ["php", "$file"],
    "selector": "source.php"
}

基本設定
これ毎回設定するの面倒だから丸コピしちゃおう。
{
    "always_show_minimap_viewport": true,
    "auto_indent": true,
    "close_windows_when_empty": false,
    "color_scheme": "Packages/User/SublimeLinter/Nexus (SL).tmTheme",
    "default_line_ending": "unix",
    "drag_text": true,
    "draw_indent_guides": true,
    "draw_minimap_border": true,
    "draw_white_space": "all",
    "font_size": 17,
    "highlight_modified_tabs": true,
    "ignored_packages":
    [
        "Vintage"
    ],
    "open_files_in_new_window": false,
    "overlay_scroll_bars": "enabled",
    "remember_open_files": true,
    "scroll_past_end": true,
    "show_encoding": true,
    "show_line_endings": true,
    "smart_indent": true,
    "translate_tabs_to_spaces": true
}

検索窓でエンター押すと日本語が消えやがる問題をどうにかする
Default (OSX).sublime-keymap の以下3箇所のキーを"enter"から"ctrl+enter"に書き換え。以降検索結果送りはCtrl+Enterで行う。
// Find panel key bindings
// Replace panel key bindings
// Incremental find panel key bindings

豆知識を覚えとく
  • 折り返しのオンオフはコマンドパレットから word wrap って打てば素早く行える。
  • 複数カーソルを出す方法は2通り。マウスホイールドラッグか、あるいはopt+左ドラッグ。




なお、記事の中で円マークになってるところは全部バックスラッシュに直すように。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Batch フォルダ単位でバックアップする



パソコン使いだして長いけれど、思えば、バックアップってやったことなかったわけよ。幸運にも俺の使っているHDDくんはタフらしく、7歳になってもぎゅんぎゅん動いて一度もぶっ飛んだことがない。でもHDD壊れてスクリーンショット飛んだ奴も身近にいるし(おう、二三度飛ばしたきみのことだぞ)、ちょっとバックアッププログラムを考えてみようってわけ。バッチファイルを触るの初めてだけどがんばったぜ。

やりたいことは以下。
  • Cドライブから、外付けHDDであるDドライブへコピー。
  • 0時に自動で実行してほしい。
  • 0時にPCがスリープ状態でも実行してほしい。
  • 実行したらPCをシャットダウンしてほしい。でないと二日間放置したとき、変更がないのにバックアップしちゃってムダだから。
  • PCまるごとバックアップじゃなくて、フォルダ単位で指定したい。
  • せっかくなので5世代ほどバックアップしたい。
  • Dドライブはこういう構成にする。

Dドライブ
│
├─ ★バックアップ用のHDDです★(バックアップ用ドライブの存在フラグ)
│
├─ 01_Backup(バックアップフォルダ)
│    │
│    ├─ 2016.01.01._00.00.00
│    │    ├─ フォルダ単位のバックアップ
│    │    └─ フォルダ単位のバックアップ
│    │
│    ├─ 2016.01.02._00.00.00
│    │    ├─ フォルダ単位のバックアップ
│    │    └─ フォルダ単位のバックアップ
│    │
│    └─ 以下、5つまで続く。
│
└─ 02_BackupLog(ログフォルダ)
     │
     ├─ 2016.01.01._00.00.00
     │    ├─ フォルダ単位のバックアップログ
     │    └─ フォルダ単位のバックアップログ
     │
     ├─ 2016.01.02._00.00.00
     │    ├─ フォルダ単位のバックアップログ
     │    └─ フォルダ単位のバックアップログ
     │
     └─ ログはこれまでのを全部残す。



ほんで自分なりに書いてみたのがこれ。
: 「おまじない」。batファイルの最初にはこれを書くといいらしいぞ。*1
@echo off
cd /d %~dp0
setlocal enabledelayedexpansion

: バックアップフォルダのパス、ログフォルダのパス、保持する世代数を書く。
set BKFOLDER=D:¥01_Backup
set LOGFOLDER=D:¥02_BackupLogFolder
set GENERATION=5

: このバッチ実行中にスリープしないようにする。*6-1
powercfg -x -standby-timeout-ac 0
powercfg -x -standby-timeout-dc 0

: そもそも外付けHDDのがなかったら終了するぞ。*2
if not exist "D:¥★バックアップ用のHDDです★" ( exit )

: バックアップフォルダの名前を作る(上述した2016.01.01._00.00.00みたいなやつ)。*3
set T=%time: =0%
set NOW=%date:~0,4%.%date:~5,2%.%date:~8,2%_%T:~0,2%.%T:~3,2%.%T:~6,2%.

: 最古のバックアップの名前。
set OLDEST=
for /f "usebackq delims=" %%i in (`dir D:¥01_Backup /ad /o-d /b`) do ( set OLDEST=%%i )

: 現存するバックアップの数。
set COUNT=
for /f "usebackq" %%i in (`dir /ad /b D:¥01_Backup ^| find /c /v ""`) do ( set COUNT=%%i )
: 値の前後のスペースを削除する。*5
call :Foo !COUNT!

: 現存数 < 欲しい世代数 だったら、新しくフォルダを作る。
if !COUNT! lss %GENERATION% ( mkdir "%BKFOLDER%¥%NOW%" )

: 現存数 >= 欲しい世代数 だったら、最古のバックアップの名前を変更する。*4
if !COUNT! geq %GENERATION% (
    move "%BKFOLDER%¥%OLDEST%" "%BKFOLDER%¥%NOW%"
    : フォルダの更新日時だけ更新したいんだが、
    : 方法がわからんかったので中に適当なファイルを作成即削除して更新する
    echo aaa > "%BKFOLDER%¥%NOW%¥aaa.txt"
    del "%BKFOLDER%¥%NOW%¥aaa.txt"
)

: ログフォルダを作る。
mkdir "%LOGFOLDER%¥%NOW%"

: ここが本題、バックアップする。この例ではデスクトップをまるごとバックアップする。
: in ("Desktop") のDesktopの部分に書いた名前でバックアップされる。
: "C:¥Users¥Username¥Desktop" の部分がコピー元のパス。
set NAME=
for /f "delims=" %%i in ("Desktop") do ( set NAME=%%i )
call :Foo !NAME!
robocopy ^
    "C:¥Users¥Username¥Desktop" ^
    "%BKFOLDER%¥%NOW%¥!NAME!" ^
    /LOG:"%LOGFOLDER%¥%NOW%¥!NAME!.txt" ^
    /MIR /R:0 /W:0 /NP /NDL /TEE /XJD /XJF /FFT
: 以下、バックアップしたいフォルダの数だけこれをコピーしてフォルダ名とかパスを書く。

: スリープの設定をもとに戻す。俺は普段60分設定にしてるので60。*6-2
powercfg -x -standby-timeout-ac 60
powercfg -x -standby-timeout-dc 60
endlocal

: バックアップ終わったらシャットダウンする。
shutdown.exe -s -t 60
exit

: 変数から前後のスペースを取り除く関数 ……関数って呼んでいいのかこれ? *5に関連。
: 使いたいところで call :Foo !A! って感じで呼ぶ。
:Foo
set COUNT=%*

*1 このおまじないなんなん?
「ふつーにbat実行すると書いたことが全部cmdに表示されちゃうけどそれ無意味だから表示されないようにする」、「このbatの場所をカレントディレクトリにする」、「batってデフォルトだとひとつの変数につき一回しか定義できないんだけどそれを何度も定義しなおせるようにする」。
*2 なんでHDD存在チェックしてんの?
このファイル名変えさえすれば、HDD繋ぎっぱなしでもバックアップを中止、再開できるようにしたら便利じゃないかなって思ったから。
*3 なんで現在時刻をいったんTに格納してんの?
ゼロ埋めのため。
*4 なんで新しいバックアップフォルダ作らないで最古のデータに上書きするって方式とってんの?
コピーに使っているrobocopyコマンドは差分コピーでバックアップデータからの変更点だけを上書きしてくれる。そのほうがあらたにイチからバックアップを作るより早く済み、PCくんの負担も少ないと思ったから。
最古のフォルダ名の取り方、および変数への格納について
for /f "usebackq delims=" %%i in (`dir D:¥01_Backup /ad /o-d /b`) do ( set OLDEST=%%i )
「D:¥01_Backupの中身を新しい順に並べ、順繰りに変数iへ格納し、最後に格納されたものが最古のフォルダ名」っていう内容。大事なのがオプションの delims= で、これがないとスペースの入ったフォルダ名に対応できない。「新しいフォルダ (1)」とかね(それでちょっと詰まった)。



んでそのバッチファイルを定時実行するのには、Windowsのタスクスケジューラを使う。
  • 全般タブ
    • ユーザがログオンしているかどうかにかかわらず実行する、にチェック。
    • パスワードを保存しない、にチェック。
    • 最上位の特権で実行する、にチェック。
  • トリガータブ
    • 実行間隔と時刻を記述。
    • 有効、にチェック。
  • 操作タブ
    • プログラム/スクリプトにいま書いたスクリプトの場所を設定。例、"C:¥robocopy_backup.bat"
    • 開始ってところに上記スクリプトのディレクトリ名をダブルクォーテーションなしで記述。例、C:¥
    • このパスにスペースが含まれていたり、カッコが含まれているとうまくいかないっぽい。具体的には、実行はできるんだが、タスクスケジューラの実行結果が0x1になっちゃうことがあった。
  • 条件タブ
    • タスクを実行するためにスリープを解除する、にチェック。



問題点としては、AppDataのバックアップはこのスクリプトで出来ないこと。バックアップを取りたかったフォルダのひとつに、ユーザフォルダ内のAppDataがあったんだが、これはうまくいかんかった。なんか権限がどうとか色々言われてさ。仕方ないからこれに関しては思い出したときに手動で行うこととした。



こんなところでうまく完了した。だが苦労したぜ、バッチファイルを甘くみていた。だってPythonを書けるんだから、楽勝だと思っていたのだ。蓋を開けたらPythonと全然違うじゃねーか! そもそもなんだ、指定ディレクトリ内のファイル数をカウントするためだけに3時間もググらせんじゃねーよ。ようやく方法を見つけたと思ったら、
for /f "usebackq" %%i in (`dir /ad /b D:¥01_Backup ^| find /c /v ""`) do ( set COUNT=%%i )
とかお前、「`dir /ad /b D:¥01_Backup`コマンドでディレクトリの一覧を出し、その結果を`find /c /v ""`に放ってその行数を出し、その結果をfor文で回して変数に格納」するってどんな発想だよ。ほんとびっくりした。ってここまで書いて気付いたんだけど最初からPythonでやればよかったんじゃね? い、いやなんかバックアップのやり方とかググっていたらWindowsのデフォルト機能でシステムバックアップ、とかたくさん出てきたから、Windowsの機能であるバッチしか思い浮かばなくなっちゃったみたいだ…。まあでもひょっとしたら、Pythonの旦那も裏ではこういうコマンドを地道に打ってくれてるのかもしれないしな。ここは世界が広がったと満足しておこう。



(2016.11.25.)自分で使っていたところ、不具合が発生してたんで追記、修正。

*5に関連した処理を追加
なんかねえ、たとえば"2"って数値が入っていてほしい変数に"2 "って文字列が入っちゃってる部分が発生してた(末尾にスペースが入っちゃってる)せいでif文の分岐が狂っちゃっていた。不要なスペースを削除するための処理は上記スクリプト内で*5をつけて追記しといた。なお、変数セットするときに
( set COUNT=%%i )
って書き方してる部分を
( set COUNT=%%i)
って書くようにすりゃあ(閉じカッコ前のスペースを削除)このトラブルはぶっちゃけ根治する。だが俺は新しく覚えたものを使いたがる病なので既存の書き方はそのままに、対応した。
*6-1 *6-2
バッチ実行中にPCがスリープする事件が起きやがったので、それに対応。Windowsの旦那はどうしてこうところどころ気が利かねーの? Macに浮気するぞこの野郎。
その他こまかいところを修正
バックアップフォルダの場所は直書きじゃなくてスクリプト冒頭で設定するような書き方にしたり、途中不等号の種類間違えてたりしたんで修正しといた。マヌケー。



(2016.12.21.)二周目のバックアップがとっても早くなるってのがコレの売りだったはずだが、はたしてどんくらい早くなるのか?

一周目(全部バックアップ)がこれくらい。0時に開始して、全部終わるのに3時間以上かかっている。なおこの日のバックアップサイズは全部で18GBくらいでした。


ほんで二周目(差分バックアップ)がこれくらい。0時に開始して、30分かからず終了している。


早くなったね。やったね! 寝てる間だから長かろうがなんだろうがどうでもいいのだけど、PCくんの負担が軽くなるはずだからいいことだよねきっと。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python ダイスロール用SkypeBot0.3 CoCシステム対応



みろりHPのファイル置き場 - CocSkypeBot0.3

最近TRPG仲間が増えてよくセッションしており、自前のセッション補助ツールをちょこちょこ改良してる。今回は、以前作ったダイスロール用SkypeBotの改良版。


普通のダイスロールの他、成功失敗判定、キャラステ作成、ステータスごとの作成の機能をつけてみた。判定機能についてはみんな結構使ってくれてて満足してる。ちなみに help コマンド打てば使えるコマンドが表示されるよ。
使うにはconfファイルにみっつほど設定を書かないといけない。以下、設定について詳しめに書く。



dbPath
C:/Users/{PCのユーザ名}/AppData/Roaming/Skype/{skypeのユーザ名}
を書く。実際にそのフォルダがあることと、パスに使われてるのがバックスラッシュじゃなくてスラッシュなのを確認してください。
url
chromeでskype for webページ開いて、F12押す。Developer Toolsが開くから、Networkタブを開く。BOTを使いたいチャットで何か発言するとMessagesってのがふたつ出てくるからその下を選ぶ。(下画像参照)

そん中のRequest URLを書く。(下画像赤でくくったとこ)
token
Request URLのずーっと下を見るとRegistrationTokenってとこがあるから、そのクソ長いのをコピーして token のとこに貼り付ける。(下画像赤でくくったとこ)

dbPathはいいんだけど、urlとtokenは毎回変わるから毎回設定しないといけない。いつもセッションでBOT準備するとき、俺ちょっとモタモタしてんじゃん? それはこれを毎回やっているからなんだ勘弁してくれ。



なお改良の具体的な内容は以下。
  • DBに毎秒アクセスして監視してたのを、DBファイル自体の監視に切り替えてコンピュータさんの負担を減らした。
  • 前バージョンは一番新しい発言だけを監視していたので「誰かがコマンド打ったあとすぐ違う発言したらコマンドが実行されない」て問題があったがそれを解消。喋りまくってもコマンド打ちまくってもすべてに反応するようにした。
  • たまに原因不明でBOTが落ちることがあった(「BOTちゃん死んでる」)けど、例外処理することで解消した。
  • 前バージョンでは使用するチャットグループを変更するたびに部屋IDをconfに書く必要があって面倒だったが、書かなくても動くよう変更。
  • たとえば 1d3 1d2 で振ったとき [+3, +2] とダイスごとの結果も表示するようにした。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python SkypeBot最小構成版



俺が思いついたSkypeBotのシステムを紹介してみる。いやすでにダイスロール用BOTとか公開してるんだけど、SkypeBotとして最小限のものだけシンプルに組んでみた。エラー処理とか全然してないし、とにかくBotとしての最小構成システムとするのが目的。



これ設計書。


こんな風になる。


これコード。180行くらい。解説みたいなもんは下に。
# coding: utf-8

'''
最小構成のSkypeBotセット。
WatchDogクラスがSkypeのDBを監視して更新があったらSimpleSkypeBotを呼び出す。
SimpleSkypeBotクラスは発言の内容を調べて対応した内容をSkypeへ送る。
'''
author = 'Midoriiro<http://guild-elf.jugem.jp/>'date = '2016.09.07.'

import sqlite3
import requests
import sys
import random
import time
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

# ==============================
# 設定
# ==============================

# keyを含む発言に反応しvalueを返す。
conf_pattern = {
    'how are you': 'BOT: so fine.',
    'who are you': 'BOT: i am bot.',
    'hello'      : 'BOT: hi.',
    'bye'        : 'BOT: take care.',
}

# skype for windowsのDBがあるディレクトリのパス
conf_dbDirPath = 'C:/Users/{ユーザ名}/AppData/Roaming/Skype/{垢名}'

# skype for web httpの送信先。httpヘッダを調べて書いてね。
conf_url = '後述'

# http用のトークン。httpヘッダを同上。
conf_token = '後述'

# ==============================
# 設定ここまで
# ==============================

# session
session = requests.session()
session.post(conf_url)
# BOT起動時のタイムスタンプ
startTimestamp = round(time.time())
# 反応済みIDが入るリスト
doneIdList = []
# skype for webへ送るリクエストヘッダ。
headers = {
    'Accept'            :'application/json, text/javascript',
    'Accept-Encoding'   :'gzip, deflate',
    'Accept-Language'   :'ja,en-US;q=0.8,en;q=0.6',
    'BehaviorOverride'  :'redirectAs404',
    'Cache-Control'     :'no-cache, no-store, must-revalidate',
    'ClientInfo'        :('os=Windows; osVer=7; proc=Win32; lcid=en-us;'
            + ' deviceType=1; country=n/a; clientName=skype.com;'
            + ' clientVer=908/1.42.0.98//skype.com'),
    'Connection'        :'keep-alive',
    'ContextId'         :'tcid=146372019467711519',
    'Content-Type'      :'application/json',
    'Expires'           :'0',
    'Host'              :'client-s.gateway.messenger.live.com',
    'Origin'            :'https://web.skype.com',
    'Pragma'            :'no-cache',
    'Referer'           :'https://web.skype.com/ja/',
    'User-Agent'        :('Mozilla/5.0 (Windows NT 6.1)'
            + ' AppleWebKit/537.36 (KHTML, like Gecko)'
            + ' Chrome/50.0.2661.102 Safari/537.36'),
    'RegistrationToken' :conf_token,
}

class SimpleSkypeBot:
    '''main.dbから発言の内容を取得し、対応する内容をSkypeへ送る。'''

    def main(self):
        '''トップレベルメソッド。'''
        # たまにsqlite3.OperationalError: disk I/O errorが出るので
        # そんときは処理をやり直すためのtry,except。たぶん邪道。
        while True:
            try:
                recordList = self.selectRecordList()
                break
            except sqlite3.OperationalError:
                print('sqlite3.OperationalErrorが出たヨ。')
                continue
        if not recordList:
            return False
        for record in recordList:
            # 発言の内容によって返答を作る。
            reply = self.getReply(record['body_xml'])
            if not reply:
                return
            # 反応済みリストにidを追加する。
            doneIdList.append(record['id'])
            # 返答をスカイプへ送信する。
            self.sendSkype(reply)
        return

    def selectRecordList(self):
        '''main.dbからレコードを取得する。'''
        # connectionをグローバルで作るとマルチスレッドエラーになっちゃうのでここで作る。
        connection = sqlite3.connect(conf_dbDirPath + '/main.db')
        cursor = connection.cursor()
        # SQLの「body_xmlにconf_patternの内容を含む」部分を作る。
        # AND (1=0 OR `body_xml` LIKE '%key%' OR `body_xml` LIKE '%key%')
        # こんな感じの。
        likePart = ''
        if conf_pattern:
            likePart = 'AND (1=0 '
            for key in conf_pattern:
                likePart += 'OR `body_xml` LIKE ¥'%%%s%%¥' ' % key
            likePart = likePart + ')'
        # SQLの「反応済みリストのIDを除く」部分を作る。AND `id` NOT IN (**,**)
        # こんな感じの。
        idPart = ''
        if doneIdList:
            idPart = 'AND `id` NOT IN ('
            for doneId in doneIdList:
                idPart += str(doneId) + ','
            idPart = idPart[0:-1] + ')'
        # 発言を取得するSQL。
        # 「BOT起動時のタイムスタンプ後」「body_xmlにconf_patternの内容を含む」
        # 「反応済みリストのIDを除く」というSQL。
        sql = ('SELECT id,body_xml FROM `Messages` '
            + 'WHERE `timestamp`>? %s %s' % (likePart, idPart))
        bind = (startTimestamp,)
        # 取得する。
        cursor.execute(sql, bind)
        trash = cursor.fetchall()
        # コネクション閉じる。
        connection.close()
        # 成形して返す。
        if not trash:
            return False
        else:
            return self.assoc(trash, ['id', 'body_xml'])

    def assoc(self, trash, columns):
        '''いつものsqlite3モジュール補助。
        [[1,A][2,B]]ってなってるのを{{id:1,name:A},{id:2,name:B}}ってディクショナリに。'''
        rows = []
        for i in range(len(trash)):
            rows.append({})
            for j in range(len(trash[i])):
                rows[i][columns[j]] = trash[i][j]
        return rows

    def getReply(self, body_xml):
        '''body_xmlの内容に従って返答を返す。'''
        for key,value in conf_pattern.items():
            if key in body_xml:
                return value
        return False

    def sendSkype(self, reply):
        '''skype for webに送信する。'''
        postjson = ('{' +
            'content        : "%s",' % reply +
            'clientmessageid: "%s",' % random.randint(1000000000000,
                9999999999999) +
            'messagetype    : "RichText",' +
            'contenttype    : "text",' +
        '}')
        session.post(conf_url, data=postjson, headers=headers)
        return True

class WatchDog(FileSystemEventHandler):
    '''ファイルの変更を感知したらSkypeBotオブジェクトのmainメソッドを走らせる。'''
    def on_modified(self, events):
        '''ファイルに変更(スカイプに発言)があったらSkypeBotオブジェクトの動作開始。'''
        if events.src_path.endswith('main.db'):
            bot.main()
            return

if __name__ in '__main__':
    bot = SimpleSkypeBot()
    dog = WatchDog()
    observer = Observer()
    observer.schedule(dog, conf_dbDirPath, recursive=True)
    observer.start()
    observer.join()

conf_urlに書くもの
skype for webでメッセージ送ったときのhttpリクエストURL。
conf_tokenに書くもの
skype for webでメッセージ送ったときのhttpリクエストヘッダの中にあるRegistrationTokenの値。'registrationToken=なんちゃらなんちゃら'ってやつ。めっちゃ長い。
requestsモジュールが必要
Python34/Scriptsで
pip install requests
watchdogモジュールが必要
Python34/Scriptsで
pip install watchdog
問題点1: BOTの発言に日本語を含めることができない
文字コードエラーによるもの。半角英数字は送れるので十分と判断し放置してる。
問題点2: sqlite3にアクセスする際たまにdisk I/Oエラーが出る
原因全然わかんない。お手上げ。(俺の見た目には)同じ条件で出たり出なかったりする。再現できないのでどうにもこうにもならない。勘弁して。苦肉の策で、その部分を無限ループにし、データ取得に成功したら抜け、エラーが出る限りずっとアクセスさせる、っつー情けない方法を採用してる。

今回は最低限の機能ってことで、ある文字列を含む発言がきたらそれに対応する応答を返す、ってだけの機能だけど、複雑なことしたかったらgetReplyメソッドの中にいろいろごちゃごちゃ書いてゆけばよい。冒頭のダイスロール用SkypeBotでもそうしている。



いつものコトだけど素人のやることだから多分バリ邪道だろう。デスクトップアプリのDBからログとってインターネットアプリにヘッダ偽造したhttpリクエスト飛ばすとか…。でも思いついたときはテンションだだ上がりで書くのも楽しめた。ぶっちゃけwatchdogモジュールはこんなスクリプトに使うのは宝の持ち腐れって感じだ。osモジュールとかでファイルの更新日時を1秒ごとくらいに取得すればいいんだし。でもまあ使ったことないモジュールだったし、watchdogって名前がなんか良くて使いたかった(犬派)。むしろこのモジュールのせいでマルチスレッドエラーとかで詰まりまくったとこあるけどな!(selectRecordListメソッドあたり参照)


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python DialogFrame0.2 対話劇作成ツール



みろりHPのファイル置き場 - DialogFrame0.21(2016.08.08. 微細なミス発見のため0.21へバージョンアップ)
みろりHPのファイル置き場 - DialogFrame-cassettes

以前も、仲間と遊んだTRPGの再現プログラムを書いた(Python pygameスクリプトのexe化)けど、あれ作るの結構楽しかったんだよ。だから今回は、ああいうのをラクに量産するためのフレームワークツールを作ってみた。

こういうのがカンタンに作れる。画像を用意してー、音楽用意してー、設定ファイル書いてー、台本書けばできる。詳しいことはダウンロードパッケージのHowToファイルに書いてある。

おもな機能。
  • 台本をざくざく書いてくだけでページ送りに組み込んでくれる。
  • 台本にイベントタグを書くことで画像や音楽を呼び出せる。
  • 台詞と画像をリンクさせれば、「Aさんが喋ってるときはAさんの画像を表示、黙ってるときは消す、あるいは暗くする」みたいなことが自動で出来る。
  • 台本が長くなっちまったら複数に分けて、オープニング画面から分岐させることが出来る。
  • オープニング画面のカンタン作成。不使用設定も可能。
  • 技能名と技能値を設定しておくと、diceイベントタグでロールアニメ、成功失敗表示とかしてくれる。
  • 途中セーブ、ロード可能。不使用設定も可能。
  • ページ戻り機能あり。
  • 簡単なキーコンフィグあり。
  • 画面サイズは640x480固定。
  • 台本とか素材はフォルダごとのセットになってるので、セットを入れ替えれば再生する対話劇を変更できる。
  • つまりフレームワーク本体がラジカセで、セットがカセットの役割をする。
  • エラーが起きたら画面が消えちゃうが、カセットフォルダ内logフォルダにエラーログが残る。それを作者に送ってくれりゃなんとかするかも。

ダウンロードセットにはすでに「cassette-DialogFrameTutorial(チュートリアル)」のカセットが入ってる。初期状態ではチュートリアルが再生される。DialogFrame-cassettesにはMATETRPGのカセットがみっつ入ってる。カセットの入れ替え方法は以下。
  1. カセットフォルダをdataフォルダに放り込む。
  2. カセットフォルダ内configフォルダから「(...)DialogFrameConfig.py」をコピーする。
  3. DialogFrame.exeのあるフォルダ(dataフォルダ)にペーストする。
  4. (...)の部分をとって「DialogFrameConfig.py」に改名する。
Config.pyファイルに「このConfigに対応するカセットを読み込む」って設定が書いてあるんで、これで入れ替えが済んだことになる。なお、チュートリアルはConfigファイルが存在しないとき再生されるようになっている。


チュートリアルはあんま役に立たないかも…。



例によってダウンロードファイルの中にソースもまるごと置いてるので興味があったら見てみて。

今回コードが1000行超えちゃって口角引きつりっぱなし。1ファイルの中にクラス8個も入ってんだぜ。うっかりpygame.init()をグローバルでやっちゃったもんでこういうことになっちゃってる。メインクラスのコンストラクタでやるべきだったのかな? せっかくクラスを覚えたのに何をやっているのだか…。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python ImageTest0.2



みろりHPのファイル置き場 - ImageTest0.2


先週つくった、pygameで画像の配置感と座標を見ることが出来るツールだけど、実用したらユーザインタフェースがゴミで使いづらいのなんの。このツールが役に立つことは疑いようがないので、以下の様な更新を行った。
  • 表示画像を変更するとき、わざわざスクリプト内に書かせるのをヤメ、imageフォルダ内の画像を全部読むようにした。
  • そーゆー仕様にすると背景画像が最前面に出て何もみえねーぞコラって事態が起こりうるので、選択した画像を最前面に表示するようにした。
  • 画像の移動を上下左右キーのみで行うのがクソ面倒だったので、マウス操作に対応した。
  • 表示画像の全座標の一覧をテキスト出力できるようにした。

imageフォルダの画像全読みと、マウス操作対応はツールの操作性をかなり上げてくれた。ちなみにスクリプト内に設定を書く必要がなくなったので、exe化することにした。配布物内にあるsourceフォルダにソースは入れてある。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python 相対パスを使うexeファイルを相対パスのショートカットで呼び出す



「1日1python」のお時間です。
めっちゃ詰まってたところがようやく融けたのでサマリ兼パイソニスタ仲間のためのノートを書く。今回実現したいのは以下のような条件だ。
  • ディレクトリ構成はこう。
    トップディレクトリ
        ├─ a.exeのショートカット
        └─ data
            ├─ a.exe(pythonスクリプトをcx_freezeでexe化したもの)
            └─ image
                └─ なんか適当な画像
    
  • これをセットとして配布したい。ユーザにはショートカットからdataディレクトリのa.exeを起動してもらう。
  • 配布するので、ショートカットには絶対パスを使えない(相対パスを使わないといけない)。
  • a.exeはimageフォルダの中から画像を読んで使う。その際、画像のパスはすべてa.exeから見た相対パスになってる。

次のような問題が発生した。
  • そもそもショートカットに相対パスどう書くのかわかんねえ。
  • ショートカットからa.exeを実行するとカレントディレクトリがトップディレクトリになっちゃう。そのためa.exeは自分をカレントディレクトリとした相対パスでは画像に辿りつけない。

で、次のように解決した。
  • ショートカットに相対パス仕様にする方法は以下。
    • 「リンク先」を
      %windir%¥explorer.exe ".¥data¥a.exe"
      にして、
    • 「作業フォルダー」を空欄にする。
  • ショートカットから実行されたときはカレントディレクトリをdataディレクトリに移す。
    • ショートカットから実行されたかどうかは、まあたとえば「os.path.exists('./image')がFalseならショートカットからの実行だ」みたいな感じで適当に。
    • ディレクトリの移動はこう。
      os.chdir(os.path.dirname(sys.executable))
      もちろんosとsysはimportしておく。



ディレクトリ移動のとき使っている sys.executable は、ショートカットから実行していようがなんだろうが、最終的に実行されているexeファイルのパスを取得してくれるマジの優れもの。今回はじめて知った。a.pyスクリプトをそのまま実行すればC:/Python34/python.exeとか表示してくれるし、exe化してa.exeにすればa.exeのパスを教えてくれる。これはこの先も本当に世話になりそうだ。

ショートカットに相対パスを書く方法なんてのは情報がたくさんあってさらっと分かったのだけれど、ショートカット先のexeファイルから更に相対パスを使う、ってとこまでの記述が全然みつからなくてなあ。最初に考えたのは、「ショートカットから実行されたときは
os.getcwd() + '/data/data/image/画像'
で画像を読み込む」っつー泥臭い手だった。妙案だと思ったんだけど、これは「%windir%¥explorer.exe ".¥data¥a.exe"というショートカットから実行したa.exeのパスはC:/windows/system32/a.exeになる」という驚きの仕様によって粉砕された。アタマに'data/'をつけたところで、「C:/windows/system32/data/image/画像」なんてパスは存在しねーよってことになっちゃうわけ。windowsテメェ、こういう状況では実行したexeのパスが欲しいに決まってんだろ、空気読め、この野郎。

とまあその仕様のせいで、「カレントディレクトリを移動しちゃえばいんじゃね?」とひらめいてもカレントディレクトリをプログラムから求めることができずにいたのだが、sys.executable の発見に救われたって流れ。ディレクトリ移動の手法は上述の泥臭い手よりずっとスマートだ。ショートカットがどこにあってもプログラム側を変更する必要がないからね。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python exeファイルからpyファイルをimport



「1日1python」のお時間です。
exeファイルからpyファイルがimportできることを知って雷に打たれたような衝撃を受けたので紹介しとく。こういうときは「こんなのパイソニスタの皆さんなら当然知ってて、今更どや顔で見せびらかすなんて赤っ恥ものなんじゃ…」なんておくびにも出さず堂々と見せびらかすのが大事だ。



以下のようなふたつのスクリプトを準備してみる。coding:utf-8とかははぶくぞ。
# py.py

import py2
print(py2.a)

# py2.py

a = 'aaaa'
py.pyのほうを実行すると、当然結果は
aaaa
である。

ほんでpy.pyをcx_freezeでexe化する。以下のスクリプトを用意し
python cx_freeze.py build
コマンドで実行する。
# cx_freeze.py

import sys
from cx_Freeze import setup, Executable

exe = Executable(script = 'py.py')
setup(executables = [exe])
出来上がるexeファイルを実行すると、もちろんおんなじ結果が出る。


次にexeファイルのおとなりに以下のように書きなおしたpy2.pyを置いて実行してみる。
# py2.py

a = 'bbbb'
すると結果はこう。

どうやらcx_freezeでexe化したファイルの中には元のpy2.pyも生きているが、同時にimport文も生きていて、隣にモジュールがあればそれを優先して読み込むようになってるっぽい。これならpyファイルをexeの外部設定ファイルとして扱えるぜ。
この仕様、マジで便利じゃないか? これはcx_freezeの旦那が気が利くってことになるのかな。



とまあみどりんは今回何がやりたかったかってーと、ツールの設定ファイルを作りたかったわけですね。はじめはconfigparserモジュールを発見し、iniファイルを読み込むという手法でいくかと思ったんだけど、リストやディクショナリを読めないのがド不便だったんでヤメた。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python ImageTest0.1 pygame補助ツール



2016.08.01. バージョン0.2公開
Python ImageTest0.2



みろりHPのファイル置き場 - ImageTest0.1

スクリーンに表示した画像の座標を見ることができるツールを作ったんで、いつもどおり公開する。


えーとこれはだな、pygameでゲーム作ってて画像を配置したいとき、「このへんかな?」「もうちょい右かな?」っつってなんべんも座標を直しながら起動したり閉じたりする手間をはぶくための補助ツールだ。DLできるのはコードだけ。さすがにこれはexe化公開しないぞ。興味のあるパイソニスタ仲間がいたら参照してくれ。いちおう、python3対応であることとpython3用pygameモジュールが必要であることにご留意ください。ちっと厄介な3用pygameインストールについてはこちら(http://guild-elf.jugem.jp/?eid=718)を参照。
以下、今回ちょっと咬まれたとこ。



長押し感知について。
pygame.key.set_repeat(1, 500)

for event in pygame.event.get():
    if event.type == KEYDOWN:
        if event.key == K_z:
キーの長押し感知は key.set_repeat() でやる。引数(1, 500)で0.5秒ごとにKEYDOWN検知ができる。最初は key.get_pressed() が長押し感知に使えるかと思ったんだけど、なんかこれは検知間隔のコントロールができなかったんでボツに。

他にもpygame始めたばっかしの人に役立ちそうな要素は結構使ってるけど、そのへんはスクリプト見てねってことで。結構ざくざくコメント書いてるから。



いやー今回はサラッとクラスとかインスタンスを使えた。もう「クラス全然わかんねーレベル」は抜け出せたと思っていいだろう。感動だなー。


| 緑色 | プログラミング | comments(0) |
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
<< July 2017 >>
+ みろりHP内検索
+ 閲覧記事
+ 過去記事アーカイブ
+ カテゴリ
+ 年月選択
  • 2017年 07月 (5)
  • 2017年 06月 (4)
  • 2017年 05月 (7)
  • 2017年 04月 (8)
  • 2017年 03月 (7)
  • 2017年 02月 (10)
  • 2017年 01月 (6)
  • 2016年 12月 (8)
  • 2016年 11月 (8)
  • 2016年 10月 (5)
  • 2016年 09月 (5)
  • 2016年 08月 (7)
  • 2016年 07月 (9)
  • 2016年 06月 (6)
  • 2016年 05月 (8)
  • 2016年 04月 (10)
  • 2016年 03月 (10)
  • 2016年 02月 (8)
  • 2016年 01月 (9)
  • 2015年 12月 (9)
  • 2015年 11月 (6)
  • 2015年 10月 (5)
  • 2015年 09月 (4)
  • 2015年 08月 (8)
  • 2015年 07月 (5)
  • 2015年 06月 (3)
  • 2015年 05月 (7)
  • 2015年 04月 (8)
  • 2015年 03月 (12)
  • 2015年 02月 (8)
  • 2015年 01月 (4)
  • 2014年 12月 (5)
  • 2014年 11月 (5)
  • 2014年 10月 (7)
  • 2014年 09月 (4)
  • 2014年 08月 (7)
  • 2014年 07月 (6)
  • 2014年 06月 (4)
  • 2014年 05月 (12)
  • 2014年 04月 (9)
  • 2014年 03月 (6)
  • 2014年 02月 (6)
  • 2014年 01月 (8)
  • 2013年 12月 (7)
  • 2013年 11月 (10)
  • 2013年 10月 (10)
  • 2013年 09月 (9)
  • 2013年 08月 (11)
  • 2013年 07月 (10)
  • 2013年 06月 (9)
  • 2013年 05月 (15)
  • 2013年 04月 (11)
  • 2013年 03月 (5)
  • 2013年 02月 (7)
  • 2013年 01月 (6)
  • 2012年 12月 (9)
  • 2012年 11月 (10)
  • 2012年 10月 (10)
  • 2012年 09月 (4)
  • 2012年 08月 (2)
  • 2012年 07月 (7)
  • 2012年 06月 (13)
  • 2012年 05月 (13)
  • 2012年 04月 (15)
  • 2012年 03月 (4)
  • 2012年 02月 (12)
  • 2012年 01月 (9)
  • 2011年 12月 (5)
  • 2011年 11月 (13)
  • 2011年 10月 (2)
  • 2011年 09月 (2)
  • 2011年 08月 (1)
  • 2011年 06月 (1)
  • 2011年 05月 (4)
  • 2011年 04月 (10)
  • 2011年 03月 (8)
  • 2011年 02月 (11)
  • 2011年 01月 (14)
  • 2010年 12月 (14)
  • 2010年 11月 (17)
  • 2010年 10月 (17)
  • 2010年 09月 (19)
  • 2010年 08月 (22)
  • 2010年 07月 (18)
  • 2010年 06月 (16)
  • 2010年 05月 (19)
  • 2010年 04月 (15)
  • 2010年 03月 (22)
  • 2010年 02月 (18)
  • 2010年 01月 (18)
  • 2009年 06月 (2)
  • 2009年 04月 (1)
  • 2007年 12月 (10)
  • 2007年 11月 (7)
  • 2007年 10月 (9)
  • 2007年 09月 (4)
  • 2007年 07月 (5)
  • 2007年 06月 (11)
  • 2007年 05月 (6)
  • 2007年 04月 (4)
  • 2006年 01月 (20)
  • + ブックマーク
    + 最近のコメント
    + アクセスカウンター
    全体(since 2010.02.03.)
    今日… 昨日…