みろりHP


緑色さんの多目的ブログ
みろりえいちぴー
ごゆるりとおくつろぎあさーせ。
| カテゴリ:プログラミング |
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) |
| カテゴリ:プログラミング |
Python ダイスロール用SkypeBot0.1 api不使用

2016.10.30.追記 ---
これの改良版をアップ。
http://guild-elf.jugem.jp/?eid=823
---



みろりHPのファイル置き場

今週ひさびさに仲間とSkypeでTRPGやってんだけど、やっぱりSkype上でダイスロールができないと不便なんでSkypeBot作った。せっかくなので公開する。コンパイルしてあるんでpython環境がなくても使えるはず。ただちょっと、初期設定が面倒だと思うのでその解説を今回は書く。


こういう感じのBOTだよ。



必要なもの
  • windows。
  • skype for windows。ふつうにデスクトップで開くアレ。
  • skype for web。ブラウザで開くskype。初期設定に必要。
  • sqlite3の中身が見れるツール。DB browser for SQLiteなど。

設定項目(skypeBot.conf)はこんな感じ。
test
これはみどりんが作ってる最中に必要だった項目なんで気にしなくてよい。デフォルトの'False'のままでOK。
dbPath
skype for windowsのデータが保存されてるファイルのパスを書くとこ。たぶん'C:/Users/ユーザ名/AppData/Roaming/Skype/スカイプid/main.db'だと思う。main.dbがあるのを確認し、スラッシュがバックスラッシュになってないか確認。
roomId
めんどくさい項目その1。このBotを適用したいskypeチャットルームのidを書くとこ。これは上のmain.dbに記録されてるんで、まずそれをDB browser for SQLiteとかで開く。そしたらBrowse Dataというタブを開いて、TableのコンボボックスからMessagesなるテーブルを選ぶ。すると全ログの記録が出てくると思うんで、そこからBotを適用したいチャットルームの発言記録を探して、その記録のconvo_idの値を書く。適用したいチャットでなんか適当に発言すればこのリストの一番下に追加されるから、そうやって見つけたらラクと思う。
interval
ぶっちゃけこのプログラムはこのmain.dbをずーっと監視するつくりなんだけど、何秒ごとに監視するか決めるのがここ。
key
Botが反応する文字列を書くとこ。もしここを'xxx'という値にしたら、Botは'xxx 'から始まる発言に反応する。たとえば'xxx 1d100'と言えば1d100の結果を返してくれる。
url
めんどくさい項目その2。ぶっちゃけこのプログラムはダイスロールの結果をウェブ版のskypeに投げるつくりになってんだけど、その投げる宛先を書くのがここ。俺はchromeを使ってるんでchromeでの見かたを書く。まずskype for webを開いてログインして、Botを適用したいチャットルームを開く。そしたらF12を押してDeveloper Toolsを起動して、Networkっていうタブを開く。真ん中らへんに、pollとか?qsp=なんちゃらとかそういうなにがしかが並んでるはず。そこでブラウザ上のチャットルームに戻って、なんでもいいのでなんか発言する。するとすぐNetworkのタブにmessagesという何かがゾゾゾっといくつか増えるんで、それの……たぶん一番下のやつをクリックするとHeadersが開く。そのHeadersからRequest URL(たぶん一番上)の値を探して、書く。
token
いま開いてるHeadersの中からRegistrationToken(たぶん下の方)を探して書く。ここまで書いたらもうskype for webは閉じてよいと思う。

こいつが受け付けるダイスロールの書式は以下
ふつーにダイスロールがしたい
'xxx 1d100'みたいに書く。dは大文字でもいいよ。
足し算がしたい
'xxx 1d10 5'みたいに書く。プラス記号は使わず半角スペースを入れてね。
引き算がしたい
'xxx 1d10 -5'みたいに書く。
Botを終了したい
起動するとコマンドプロンプトが開くのだけど、それを閉じればいい。あるいは'xxx end'と入力しても終了する。

注意点としては、上述したとおりコイツはログを指定秒ごとにチェックするんで、たとえば2秒設定のとき、2秒以内に別の発言が来たらその前のダイスロールは無視される。誰かがロールしたら、譲りあいの心で数秒沈黙するのをおすすめ。あと5桁以上の数値は使えないように設定してある。これは試運転で仲間とのチャットで使ってみたら、某Wちゃんがむちゃなロールをさせてフリーズさせやがったことに対応した結果。

まあ、なんかおかしいなフリーズかなって思ったら問答無用でコマンドプロンプトを終了させて開き直せばいいと思う。



公開は上達の一歩ってことで、DLファイルにはpythonソースも同梱してある。気が向いたら、以上のようなこの非常にメンドロくさい設定項目を自動で埋めてくれるような改良をするかもしんない。やり方はなんとなく思いつくんだけど、仲間と遊ぶとき俺が使えりゃいーやってテンションで作ってたんでそういった親切機能に乏しいんだよねー。

| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python PHP 名前空間とnamespace



「名前空間ん? なんじゃそら」ってのも今となっては昔の話だ。オライリーを読んだらだいぶん理解できたと思う。でもちょっと調べなおしていたら、PHPには namespace なる宣言が存在することを知った。pythonにそういう構文があることは知らないぞ? ってことは俺、pythonにおける名前空間に関して何か見逃していることがあるのでは!? ……と危機感を覚えて調査してみた話。



以下のようなシチュエーションを想定すると、名前空間の重要性がわかりやすいかもしれん。
  • とっても便利なモジュールをふたつ見つけた
  • 両方使いたいのだが、なんとそいつらは名前が全く同じ
  • これじゃあスクリプト内で同時に使うことができん。どっちかが上書きされちゃうからな
  • でも併用したいの!
  • だったらモジュール名自体はそのままにして、それぞれにアダ名をつけて管理しようぜ!



まずはPHPの namespace について。同名のファイルは同じディレクトリに置けないから、 fromAsan fromBsan (Aさんからもらった便利モジュールと、Bさんからもらった便利モジュールって意味)というディレクトリにわけておいておくとする。
main
  ├─ main.php
  ├─ fromAsan
  │    └─ superBenriTool.php
  └─ fromBsan
       └─ superBenriTool.php
こんな感じにね! そんでmain.phpの中でふたつのsuperBenriToolを使いたいわけなんだけど、そこで namespace が登場する。これは読み込まれる側であるsuperBenriTool.phpたちに書く。

# fromAsan/superBenriTool.php

<?phpnamespace A;

function superBenriFunc() {
    echo "なんかすごい処理Aが発生した!";
}

# fromBsan/superBenriTool.php

<?phpnamespace B;

function superBenriFunc() {
    echo "なんかすごい処理Bが発生した!";
}

するとmain.phpでは、同名のファイルを複数読み込むことができるようになるのだよ。
# main.php

<?php
include "fromAsan/superBenriTool.php";
include "fromBsan/superBenriTool.php";

A¥superBenriFunc();
B¥superBenriFunc();

# main.php の実行結果
なんかすごい処理Aが発生した!なんかすごい処理Bが発生した!
やった! phpではnamespace宣言をすることで、同名のファイルをいくらでもひとつのファイル内で使うことができるんだ! まあモジュールファイルのほうにnamespaceを書かないといけないからちょっと面倒だけどね! すごいぞ!

……いやマテ、
# main.py

import fromAsan.superBenriTool as A
import fromBsan.superBenriTool as B

A.superBenriFunc()
B.superBenriFunc()
それってpythonがasでサラッとやってることじゃねーか! というわけでpythonではPHPのnamespace宣言と同じようなことがメインファイルだけで行える。冒頭の「俺、pythonにおける名前空間に関して何か見逃していることがあるのでは!?」という危機感は杞憂だったってことだ。



上の例では各モジュールを相対パスで読み込んでいるので、どっちのsuperBenriToolがfromAsanでどっちのsuperBenriToolがfromBsanなのかすぐわかる。ただ俺の場合は、「奥義:相対インポートなんていーらない」(コンソールプログラムのexe化とかで触れてる)をよく使っているんだ。これは sys.path にモジュールの入ってるディレクトリを自動で全登録し、いちいち相対インポートの書き方をしなくてもサクッとインポートが行えるようにする技だ。すわ奥義を使ったら今回のような状況に対応できないのではと思ったが……
# ==============================
# 奥義:相対インポートなんていーらない
# ==============================
def makeDirs():
    dirs = []
    dirs.append(os.getcwd())
    for directory in dirs:
        files = os.listdir(directory)
        for f in files:
            path = directory + "¥¥" + f
            if os.path.isdir(path):
                dirs.append(path)
    return dirs

def makePaths():
    dirs = makeDirs()
    for directory in dirs:
        if directory in sys.path:
            pass
        else:
            sys.path.insert(0, directory)

makePaths()

# 奥義ここまで

import fromAsan.superBenriTool as A
import fromBsan.superBenriTool as B
A.superBenriFunc()
B.superBenriFunc()
それも杞憂だった。いや、そりゃそうだよな。sys.pathに登録したディレクトリだからって相対インポートでパス指定できなくなるわけじゃないんだから、しっかりディレクトリ指定してインポートしたいモジュールだけしっかりディレクトリ指定すればよいのだ。こういう右往左往するたびに、おそらく俺は四角い車輪の再発明みたいなことをしてるんだろうなーと思ったりするが、まあその、我流ってちょっとカックイイし、他者の技術を受け入れられる柔軟性さえ持ち続けていれば問題ないと考え看過している。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python 複数バージョンを併用する技



一流の格闘家が両利きに矯正するように、パイソニスタを志すものはpython2とpython3の併用をするべきであろう! つーかその方カッコイイであろう! と思いついたのでやってみた。いや、こないだsublime text3で上での切り替えを試みて以来、バージョン切り替えにハマってるんですね緑さんは。python2を久々にいじったんでちょっとハシャいでいるわけよ。3はとっても便利なんだけど、俺は2でプログラミングを始めたんでなんとなく思い入れがあるのだよ。
というわけで暑苦しく始まりました、「1日1python」のお時間です。(ただし隔週放送)



# 2.7なら
py -2.7 module.py

# 3.4なら
py -3.4 module.py


コンソールにこんなん打てばバージョンを使い分けて実行することができる。py.exeってのはこれはpython3.3から実装された新機能だ。っか〜! イカしてるな〜pyてょんは! 痒いところに手がとどくな〜! っか〜!

なお画像の例で実行してるpyファイルは上述のsublime text記事に載せたやつ。しかしアレな、画像を見てもらうとわかるけど、実行ファイル名ってクォーテーションで囲んでも囲わなくてもよいのな。知らなかったよ。


| 緑色 | プログラミング | comments(0) |
| カテゴリ:プログラミング |
Python RSSリーダを作る



「RSSぅ? アレだろ、ブログの新着記事一覧が表示されるツールみたいなやつ」
違った。表示されるツールはRSSリーダであって、RSSはRSSリーダが読む文書のフォーマットのことだ。RSSは各ブログ、サイトが配信しており、たとえばうちのRSSは http://guild-elf.jugem.jp/?mode=rss から見ることができる。し、知らなかった。ともかくこれは特定のフォーマットに従って書かれておるので、それをパース(整理)することで新着記事の情報を取得できるって寸法だ。今回作ったリーダの機能はこんな感じ。

  • 拾ってきたRSSをjson形式でDB保存し、キャッシュとする
  • 前回のキャッシュが6時間ならそのキャッシュを使う
  • 前回のキャッシュが6時間以上前なら新たにRSSを拾ってきて保存する
  • キャッシュからhtmlを作って新着記事のリストをブラウザで表示する


実行すると、バシッとブラウザが開いてこんな風に表示されるよ。今回はとくに咬まれたところはなかったんで、気楽に、スクリプトのスニペットを並べていく。スクリプトはアップローダに置いたんでお好きにどうぞ。

みろりHPのファイル置き場



DB接続の仕方。俺はsqliteが好きなのでそれを使ってるぜ。しかもpythonにはデフォルトでsqlite3に接続するモジュールがあるのだ。使うっきゃないだろ。
import sqlite3
conn   = sqlite3.connect("example.sqlite3")
cursor = conn.cursor()
query  = "SELECT name,sex FROM tableName WHERE id=?;"
cursor.execute(query, (1,))
trash  = cursor.fetchall()
cursor.close()
conn.close()
executeメソッド実行の行で値のバインドを行っているが、バインドする項目が1つでもきちんとタプル化しないといけないのが注意すべき点だ。じつは (1) はタプルにはならないんだよな。(1,) こうしないとタプルにはならない。オライリーを読んでいなければ詰まってたと思う。
このモジュールには気に食わない点があって、SELECTクエリで返ってくるリストにはカラム名の情報が含まれていないのだよ。だからいちいち手動でディクショナリ化しないとカラム名で値を取り出すということができない。そのくらい処理オプションでつけてくれてもんじゃないの、pythonの旦那? つーわけで俺は以下のような関数をよく使ってる。
# cursor.fetchall() で返ってくるリスト
trash = [('なまえ', 'せいべつ')]

def assoc(trash, columns):
    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

# 結果(ディクショナリ化したリスト)
print(assoc(trash, ["name", "sex"]))
# [{'name': 'なまえ', 'sex': 'せいべつ'}]

「今の時間が前の時間と何秒差か」求める方法。
import datetime
timeNow    = datetime.datetime.today()
timeBefore = datetime.datetime(2016, 3, 1, 0, 0)    # 2016.3.1. 00:00
delta      = round((timeNow - timeBefore).total_seconds())
年、月、日、時、秒の順番でdatetime.datetime()メソッドに渡すと、その時間のオブジェクトを作ってくれるのだ。

本日の主役feedparserの使い方。
import feedparser
original = feedparser.parse("http://guild-elf.jugem.jp/?mode=rss")
cacheDic = {}
cacheDic["siteTitle"]  = original.feed.title    # サイトのタイトル
cacheDic["siteUrl"]    = original.feed.link     # サイトのurl
cacheDic["articleNum"] = len(original.entries)
for i in range(len(original.entries)):          # エントリー配列が入った配列
    entry = original.entries[i]
    date  = entry.updated_parsed                # 更新日付
    month = "{0:02d}".format(date.tm_mon)       # 更新日付から月を取り出す
    day   = "{0:02d}".format(date.tm_mday)      # 更新日付から日を取り出す
    cacheDic["article" + str(i)] = {
        "title":entry.title,                    # エントリのタイトル
        "url"  :entry.link,                     # エントリのurl
        "date" :"%s/%s/%s" % (date.tm_year, month, day)
        }
いや何がステキって、feedparserって読むRSSがRSS1.0だろーがRSS2.0だろーがAtomだろーが同じ形式の配列にパースしてくれるんだよ。おかげでパース前にそのRSSがどの書式なのか判別する作業をこちらでやらずに済む。

最後に、影の主役webbrowserの使い方。
import webbrowser, os
realPath = os.path.realpath("html/example.html")
webbrowser.open(realPath)
これはローカルのhtmlファイルを開く方法だ。いやローカルのファイルパスなんて手打ちするわいって思うかもしんないが、os.path.realpath()通すと確実ですよ。それと、これはマジで面白いと思ったことなんだけど、webbrowser.open()ってさ、ブラウザを開く関数じゃないんだよ。具体的には、指定したパスのファイルを、個人で設定してる規定のプログラムで開く関数なのだよ。俺の環境だと.txtファイルのパスを与えたらsublime text3が開くし、.sqlite3ファイルのパスを与えたらDB Browser for SQLiteが開くってわけ。何を投げようとブラウザで開く機能なんだと思ってたから、.htmlファイル投げてsublime text3が開いたときはびっくりしたよ(俺はhtmlファイルはsublime text3と紐づけてる)。 これは絶対、ほかのとこでも使えるぜ。…このモジュールにwebbrowserという名前をつけた奴は何を考えてんだ、ほんで。


| 緑色 | プログラミング | comments(0) |
      1
2345678
9101112131415
16171819202122
23242526272829
30      
<< April 2017 >>
+ みろりHP内検索
+ 閲覧記事
+ カテゴリ
+ 年月選択
  • 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)
  • + ブックマーク
    + 最近のコメント
    + アクセスカウンター
    全体(since 2010.02.03.)
    今日… 昨日…