六帖のかたすみ

DVを受けていた男性。家を脱出して二周目の人生を生きています。自閉症スペクトラム(受動型)です。http://rokujo.org/ に引っ越しました。

プラグイン的pythonプログラミング

5/21に運よく(運悪く)小田急線で人身事故が起きたため、小田急線の運行情報をゲットするスクリプトが書けた。

import lxml.html
import requests

#<BR>タグを改行文字に変換して文字列化
def repBR(element):
    rawstr = lxml.html.tostring(element)
    repstr = rawstr.decode('utf-8').replace("<br>", "\n")
    return lxml.html.fromstring(repstr).text_content()

def notify(target):
    retDict = {}
    try:
        target_url = 'http://www.odakyu.jp/cgi-bin/user/emg/emergency_bbs.pl'
        target_html = requests.get(target_url).content
        root = lxml.html.fromstring(target_html)
        time = root.cssselect('.date')[0]
        left = root.cssselect('div.left_dotline_b')
        #div.left_dotline_bがあれば運行状況が出ている
        if len(left) > 0:
            str = left[0].text_content()
            str += "\n"
            if len(left) > 1:
                if len(left[1].cssselect('img[alt="ロマンスカーの運転状況"]')) == 0:
                    str += "\n詳細情報\n"
                else:
                    str += "\nロマンスカーの運転状況\n"
                str += repBR(left[1])
            if len(left) > 2 and len(left[2].cssselect('img[alt="ロマンスカーの運転状況"]')):
                str += "\nロマンスカーの運転状況\n" + repBR(left[2])
            dict = {}
            dict['time'] = time.text_content()
            dict['contents'] = str
            retDict['小田急小田原線'] = dict
    except Exception as ex:
        print(ex + " from odakyu")

    return retDict


更に運行情報メイン処理も作成できた。ソース内で使うplugins.iniとsettings.iniは次のようなフォーマットを前提とする。

plugins.ini
[plugins]
jreast=中央線快速電車|中央・総武各駅停車|総武快速線|東海道線|京浜東北線|横須賀線|宇都宮線|高崎線|常磐線快速電車|中央本線|上野東京ライン
odakyu=小田急小田原線
yomiuritop=読売新聞

settings,ini
[settings]
to=xxxx@yahoo.co.jp|xxxxn@gmail.com
notify=読売新聞|小田急小田原線|中央線快速電車|中央・総武各駅停車

[smtp]
server=smtp.mail.yahoo.co.jp
pass=xxxx
port=587
from=xxxx@yahoo.co.jp


肝はプラグイン処理で、これは今後作成する新聞サイト一括ゲット+表示アプリでも使用するプロトタイプとして作成した。
__import__
を使って、インターフェースを統一した*.plファイルを読み込む。ここでは小田急線のソースにもあるnotifyという関数を必ず実装しておく。
巡回するサイトが増えたら、決まった形式のデータを返すスクリプトを作成し、iniファイルに設定を追加するだけで済む。コアとモジュールを分離し、コアは比較的単純な処理でよく、一番面倒なスクレイピング処理の実装は各モジュールに分散化できる。
一定時間ごとに処理をする方法もpythonでは楽勝だ。time.sleep(秒)を実行しつつwhile無限ループをかますだけでおしまい。sleepがCPU使用率を食うこともない。

#plugins.iniの読み込み
import configparser
conf = configparser.ConfigParser()
conf.read('plugins.ini')
pluginDict = {}
for item in conf.items('plugins'):
    vals = item[1].split('|')
    pluginDict[item[0]] = vals
        

#settings.iniの読み込み
conf.read('settings.ini')
sendList = []
for to in conf.get('settings', 'to').split('|'):
    sendList.append(to)
notifyList = []
for notify in conf.get('settings', 'notify').split('|'):
    notifyList.append(notify)

#使用するpluginと引数を決定する
targetPlugins = {}
for plugin in pluginDict.keys():
    lines = pluginDict[plugin]
    for notify in notifyList:
        if notify in lines:
            if plugin not in targetPlugins:
                targetPlugins[plugin] = []
            targetPlugins[plugin].append(notify)


smtpFrom = conf.get('smtp', 'from')
smtpServer = conf.get('smtp', 'server')
smtpPass = conf.get('smtp', 'pass')
smtpPort = conf.get('smtp', 'port')

#1分ごとにnotifyListを対象にpluginを実行、内容に変化があればメール送信
import time
import datetime
contentsDict = {}
d = datetime.datetime.today()
print("処理開始 %s時%s分" % (d.hour, d.minute))
while True:
    for plugin in targetPlugins.keys():
        module = __import__(plugin)
        #プラグイン読み込み、実行
        result = module.notify(targetPlugins[plugin])
        for val in targetPlugins[plugin]:
            strNotify = ""
            #メモリ中のcontentDictに保存された内容と比較
            #内容が変わった場合、カラに変わった場合にはメール送信
            if val in result.keys():
                #内容あり
                item = result[val]
                if (val in contentsDict.keys() and contentsDict[val] != item["contents"]) or val not in contentsDict.keys():
                    #メモリがカラ or 内容に変更有
                    strNotify = item["time"] + "\n" + item["contents"]
                    contentsDict[val] = item["contents"]
            else:
                #内容無し
                #メモリがカラでない場合のみメール送信
                if val in contentsDict.keys():
                    strNotify = "平常通り運行しています。"
                    #メモリ内容を消す
                    del contentsDict[val]

            if strNotify != "":
                #メール送信(現時点ではコンソール出力のみ)
                print("[" + val + "]\n" + strNotify)
    #1分ごとに反復
    time.sleep(60)

なお、まだメール送信処理は未実装。コードはできているが、コンソールでそれなりに動作することが分かったらメール送信機能も付ける。これで、運行情報通知プログラムはほぼ完了だ。外出時に人身事故が起きても駅についてびっくりということがなくなる。来週はメール送信処理をつけて、さらにニュースアプリについても実装を開始しよう。

pythonのインデント強制な書式の利点の一つは、構造化プログラムでよくやる中カッコの閉じ忘れが原理的に発生しないところだ。これはありがたい。でもインデントを1つミスると予期しない処理をしてしまう危険性とも隣り合わせだね。

もう一つ。はてなブログでははてな記法モードを使わないとソースコードが綺麗に表示できないことが不便だ。見たままモードにもスーパーpre機能をつけてほしい。