pyhatebuのソースコードを読んでみる (2)

The Python Challengeは6問目で止まったままのbonlifeです。毎日少しずつでもPythonに触れていようと心がけてます。
前回(「pyhatebuのソースコードを読んでみる (1)」)の続きです。

  • pyhatebu.py

class PyHatebu(WSSEAuthRequest) は別ファイルで定義しているWSSEAuthRequestクラスを継承したクラス。最初にアンダーバーが1つついた変数に各サービス用のURLが設定されています。このアンダーバーを1つつけるのはクラス外からはアクセスしないようにしてくださいませ、っていうことを意味する慣例みたいですね。ちなみにアンダーバーを2つつけると難読化してくれます。とは言っても、ちょっと名前を変えるだけなので、やろうと思えば簡単にアクセスできてしまいますので、ご注意あれ。
続いて、XML文字列を定義している部分。ここがちょっと引っかかるんですよね。

  • [116-119行目]
    _post_xml = """<entry xmlns="http://purl.org/atom/ns#">
<link type="text/html" rel="related" href="%s" />
<summary type="text/plain">%s</summary>
</entry>"""

インデント強制によって誰でもキレイなソースコードが書けることでお馴染みのPython。ヒアドキュメントっぽい書き方ができる三重クォートでの文字列定義ですが、1行目だけは代入先の指定などがあって、開始位置がずれてしまいます!ベコンって凹んでるように見えますよ。うぅ、なんとも気持ち悪い…と思うのは私だけでしょうか。無理やり以下のように改行してみても、それはそれで気持ち悪かったりするわけで…。

    _post_xml = """\
<entry xmlns="http://purl.org/atom/ns#">
<link type="text/html" rel="related" href="%s" />
<summary type="text/plain">%s</summary>
</entry>\
"""

この部分では、後でモジュロ演算子で置換するために %s を使っています。可変部分があるテキストを事前に定義するのに便利ですね。
続いて、PyHatebuクラスの初期化部分です。

  • [126-134行目]
    def __init__(self, username='', password=''):
        """
        Initialization
        """
        self.handle = None
        self.post_uri = ''
        self.feed_uri = ''
        #Initialize WSSEAuthRequest
        WSSEAuthRequest.__init__(self, username, password)

ここでは、まさに初期化をしています。インスタンス変数を3つ初期化した後、親クラスの初期化を実行しています。Pythonでは継承元のクラスの初期化は自動的に行われないので、明示的にやってあげる必要があります。具体的には、PyHatebuクラスが受け取った2つの引数をそれぞれusername、passwordとしてWSSEAuthRequestに渡しています。
次はこのクラス内の def make_handle(self) です。これは次に定義されている def make_hatena_request(self, uri, method='POST', data=None) の中で呼ばれる関数です。何をしてるのか、というと、WSSE認証を実施しています。どこでそんなことやってるのよ!って話ですが、142行目が重要です。

  • [136-143行目]
    def make_handle(self):
        """
        Trying to make handle, if not exist.
        """
        if not self.handle:
            # Tring to make WSSE authentication
            self.handle = self.urlopen('http://b.hatena.ne.jp/atom')
            src = self.handle.read()

urlopenってあまりにも一般的な名前で見過ごしてしまいそうになりますが、self付きで呼び出されているように、このクラスのメソッドです。とは言っても、pyhatebu.pyの中を検索しても、urlopenなんて関数は定義されていません。この関数は、もう1つのファイル、wsseauth.py内で定義されている関数です。ということで、とりあえずは気にせずに置いておきます。ちなみに、if文ですでにhandleがある(Noneではない)かどうかをチェックして、なければurlopenする(WSSE認証する)ようになっています。何度も処理を実行する場合に、その度にWSSE認証をさせないようにしてるっぽいです。
次は、def make_hatena_request(self, uri, method='POST', data=None) です。はてブへのリクエストをまとめて扱う関数っぽいです。後ろで定義されているpost、get、edit、delete、feed内で呼び出されます。

    def make_hatena_request(self, uri, method='POST', data=None):
        """
        Try to make a request to hatena bookmark
        """
        # Try to make handle, if not exist
        self.make_handle()
        # Try to make request
        request = self.make_request(uri, method=method)
        if data:
            # Adding data
            request.add_data(data)
            # Setting content-type
            request.add_header('Content-Type','text/xml')
        return request

まず、self.make_handle() でWSSE認証を行い、make_request(uri, method=method) を呼び出します。この、make_request()もwsseauth.py内で定義されている関数です。selfで始まる関数は、そのクラス内、あるいは継承元の親クラスの関数になる、ということです。で、よくよく中身を見ていくと、self.make_handle内で呼び出しているurlopen内でmake_requestが実行されているみたい。とすると、最初(make_handle()時)はデフォルトの設定(GET)で軽くWSSE認証しておいて、その後、実際の処理を実行する時に再度WSSE認証がされる感じなのかしら。なんだかよくわからなくなってきたので、そのあたりは、また後で調べてみます。3つ目の引数としてdataが指定されている場合には、urllib2.Requestを継承したMethodAwareRequestクラスのadd_data()を使ってRequestにdataを追加します。(MethodAwareRequestクラスはwsseauth.pyで定義されています。)同様にadd_headerでContent-Typeを指定します。最後にMethodAwareRequestクラスのインスタンスを返します。
続いて、実際にユーザが使うことになるpost、get、edit、delete、feed関数です。まずはpost。

  • [160-168行目]
    def post(self, url, comment='', tags=[]):
        """
        Post a new url and comment.
        """
        xmldata = self._post_xml % (url,
                            ''.join(['[%s]' % x for x in tags]) + comment)
        req = self.make_hatena_request(self._post_uri, 'POST', xmldata)
        # Make request and HatebuItem instance
        return resp2HatebuItem(urllib2.urlopen(req).read())

post関数はurlという文字列とcommentという文字列とtagsというリストを引数で受け取って処理します。_post_xmlで設定した文字列に対して、以下の2つの操作を行い、xmldataに代入します。

  • urlで指定された文字列を _post_xml の1つ目の %s に設定
  • tagsをそれぞれ[]で囲んでくっつけたものに、さらにcommentを結合したものを _post_xml の2つ目の %s に設定

結果として、xmldataは以下のようになります。

<entry xmlns="http://purl.org/atom/ns#">
<link type="text/html" rel="related" href="urlで指定した文字列" />
<summary type="text/plain">[tag1][tag2][tag3]commentで指定した文字列</summary>
</entry>

このxmldataをmake_hatena_request()経由でmake_request()を呼び出し、POSTするための準備(MethodAwareRequestのインスタンス生成、methodとしてPOSTを設定、X-WSSEヘッダのセット)をします。そして、最後にurllib2のurlopenにRequestインスタンスを渡し、その結果をread()したものをresp2HatebuItem()に渡します。そしてresp2HatebuItem()が上手いことパースした結果をHatebuItemクラスのインスタンスとしてreturnで返します。要するにpost関数を使ってはてブに投稿を行うと、HatebuItemクラスのインスタンスが返ってくることになります。
get関数は渡されたitem(HatebuItem)のedit_idを取得し、その内容を元にURLを生成し、make_hatena_requestに渡します。それ以外の部分はpostと同じなので省略します。

  • [170-175行目]
    def get(self, item):
        """
        Get a exist url and comment.
        """
        req = self.make_hatena_request(self._edit_uri+item.edit_id, 'GET')
        return resp2HatebuItem(urllib2.urlopen(req).read())

edit関数も似たようなものですね。1点注目するとすればxmldataをencodeする部分でしょうか。

  • [183行目]
        xmldata = xmldata.encode('utf-8', 'ignore')

2つ目の引数で'ignore'と指定しています。utf-8エンコードできない文字列があった時に、その文字を無視するように設定しています。これを指定しておかないと、エンコードできない文字列があった時にエラーになります。
delete関数も簡単ですね。特徴としては、結果を返していないところでしょうか。最後は、feed関数。

  • [195-203行目]
    def feed(self):
        """
        get feeds
        """
        req = self.make_hatena_request(self._feed_uri, method='GET')
        # Make request and HatebuItem instance
        elems = ElementTree.fromstring(urllib2.urlopen(req).read())
        for e in elems.getiterator('{http://purl.org/atom/ns#}entry'):
            yield resp2HatebuItem(ElementTree.tostring(e))

_feed_uri で指定したAtom用のURLを渡して、Requestインスタンスを取得します。その後、urllib2.urlopenにそのRequestインスタンスを渡して全内容をread()で読み込み、xml文字列で結果を取得します。その内容をElementTreeのfromstring()関数を使ってパースして、elemsに格納します。最後の部分で、elems.getiterator()でパターンにマッチした全てのエレメントをリストで返し、その1つ1つに対してHatebuItemのインスタンスを次々と返します。yeildを使っているのでgeneratorになっていて、呼び出されるたびに1つずつインスタンスを返すっぽいです。このあたりはまだ勉強不足でよく分かってません…。
後ちょっとではありますが、だいぶ長くなってしまったのでとりあえずこのあたりにしておきます。やっぱり他人コードを読むと勉強になるなぁ、と感じてる今日この頃です。