twittering-mode.elを読む(続き)
昨日(id:hyoshiok:20080524#p1)に続いてtwitterling-mode.elを読んでいく。
elisp profiler (elp) を使った計測でtwittering-http-get-default-sentinelのコストが高いということがわかったので、そこを起点に読み始めている。で、case-stringというところまで行った。macroの定義で立ち往生していたところid:hayamiz (twittering-mode.elの作者)の解説。ネ申降臨。
さて、下記だ。
(case-string status (("200 OK") (mapcar #'twittering-cache-status-datum (reverse (twittering-xmltree-to-status body))) (twittering-render-friends-timeline) (message (if suc-msg suc-msg "Success: Get."))) (t (message status))))
これはstatusが"200 OK"だったら(mapcar ...)を実行、そうでなかったら(t)、(message status)を実行する。statusはHTTPのヘッダなので、それがOKなのかそれ以外なのか。
#'twittering-cache-status-datuml ... は (function twittering-cache-status-datum) と同じ。(mapcar #'abc (...)) はabcという関数を(...)の各要素に順に適用して、結果をリストで返す。
(reverse (...)) は (...)のリストを逆順にする。例えば(reverse '(1 2 3 4))は(4 3 2 1)というリストになる。
さてここでbodyの値は何かというと twittering-http-get-default-sentinel で定義されているのでそれをもう一度掲載すると下記だ。
(defun twittering-http-get-default-sentinel (proc stat &optional suc-msg) (let ((header (twittering-get-response-header)) (body (twittering-get-response-body)) (status nil) ) ...) )
bodyはtwittering-get-response-bodyを評価したものになっている。twittering-get-response-bodyの定義を読むことにする。
(defun twittering-get-response-body (&optional buffer) "Exract HTTP response body from HTTP response, parse it as XML, and return a XML tree as list. `buffer' may be a buffer or the name of an existing buffer. If `buffer' is omitted, the value of `twittering-http-buffer' is used as `buffer'." (if (stringp buffer) (setq buffer (get-buffer buffer))) (if (null buffer) (setq buffer (twittering-http-buffer))) (save-excursion (set-buffer buffer) (let ((content (buffer-string))) (let ((content (buffer-string))) (xml-parse-region (+ (string-match "\r?\n\r?\n" content) (length (match-string 0 content))) (point-max))) )))
説明文を読むとHTTPレスポンスをXMLとしてパースしてXML木をリストとして返すということらしい。xml-parse-regionの定義については、オンラインヘルプを読むのが王道だろう。メニューからならhelp->describe->describe-functionで当該関数を検索するか、キーボードなら C-h f xml-parse-region すればその定義がわかる。
ヘルプを写経すると、
xml-parse-region is a compiled Lisp function in `xml.el'. (xml-parse-region beg end &optional buffer parse-dtd parse-ns) Parse the region from beg to end in buffer. If buffer is nil, it defaults to the current buffer. Returns the XML list for the region, or raises an error if the region is not well-formed XML. If parse-dtd is non-nil, the DTD is parsed rather than skipped, and returned as the first element of the list. If parse-ns is non-nil, then QNAMES are expanded.
てな事が書いてある。行きがけの駄賃でxml.elまで行ってしまおう。わたしの環境(Ubuntu 8.04)のEmacs 22.1 だとxml.elクリックするだけでソースコードまで連れていってくれた。find /usr/share/emacs|grep xml.elなどとして力づくで検索してもいい。
ファイルの先頭にコメントが書いてあるので、それを読む。
;;; Commentary: ;; This file contains a somewhat incomplete non-validating XML parser. It ;; parses a file, and returns a list that can be used internally by ;; any other Lisp libraries. ;;; FILE FORMAT ;; The document type declaration may either be ignored or (optionally) ;; parsed, but currently the parsing will only accept element ;; declarations. The XML file is assumed to be well-formed. In case ;; of error, the parsing stops and the XML file is shown where the ;; parsing stopped. ;; ;; It also knows how to ignore comments and processing instructions. ;; ;; The XML file should have the following format: ;; <node1 attr1="name1" attr2="name2" ...>value ;; <node2 attr3="name3" attr4="name4">value2</node2> ;; <node3 attr5="name5" attr6="name6">value3</node3> ;; </node1> ;; Of course, the name of the nodes and attributes can be anything. There can ;; be any number of attributes (or none), as well as any number of children ;; below the nodes. ;; ;; There can be only top level node, but with any number of children below. ;;; LIST FORMAT ;; The functions `xml-parse-file', `xml-parse-region' and ;; `xml-parse-tag' return a list with the following format: ;; ;; xml-list ::= (node node ...) ;; node ::= (qname attribute-list . child_node_list) ;; child_node_list ::= child_node child_node ... ;; child_node ::= node | string ;; qname ::= (:namespace-uri . "name") | "name" ;; attribute_list ::= ((qname . "value") (qname . "value") ...) ;; | nil ;; string ::= "..." ;; ;; Some macros are provided to ease the parsing of this list. ;; Whitespace is preserved. Fixme: There should be a tree-walker that ;; can remove it.
これをみるとxml-listを(node node ...)という形式のリストにしてくれるようだ。ふむふむ。
twittering-get-response-header はHTTPレスポンスのヘッダー部分をストリングにして、 twittering-get-response-bodyはHTTPレスポンスのボディ部分をXMLとしてパースしてXML木のリストにして返すようだ。
ということでbodyはXML木のリストである。
(reverse (twittering-xmltree-to-status body)))
次はtwittering-xmltree-to-statusを読む。
(defun twittering-xmltree-to-status (xmltree) (mapcar #'twittering-status-to-status-datum ;; quirk to treat difference between xml.el in Emacs21 and Emacs22 ;; On Emacs22, there may be blank strings (let ((ret nil) (statuses (reverse (cddr (car xmltree))))) (while statuses (if (consp (car statuses)) (setq ret (cons (car statuses) ret))) (setq statuses (cdr statuses))) ret)))
ここで
(let ((ret nil) (statuses (reverse (cddr (car xmltree))))) ... )
となっているので(statuses (reverse (cddr (car xmltree))))なので(car xmltree)は先のコメントからXML木のnodeだ。(cddr XML木のnode)ということなので、(cdr (cdr XML木のnode))となる。上記の定義よりそれは子ノードということになる。それを逆順にしたものがstatusesということになるらしい。
(while statuses (if (consp (car statuses)) (setq ret (cons (car statuses) ret))) (setq statuses (cdr statuses)))
(while statuses (if ...) (setq ...)) はstatusesがnilでない間、(if ...) (setq ...) を繰り返す。statusesを逆順にしたのをretになる。それをそれぞれtwittering-status-to-status-datum適用する。
twittering-status-to-status-datum は、長い。画面に収まらない。
ざっくり、構造を抽出すると下記になる。
(defun twittering-status-to-status-datum (status) ;; 関数の再定義 (flet ((assq-get (item seq) (car (cddr (assq item seq))))) ;; 変数の定義 (let* ((status-data (cddr status)) ;; 中略 regex-index) ;; 変数へ初期値を代入 (setq id (string-to-number (assq-get 'id status-data))) ;; 中略 ;; make username clickable ;; make URI clickable ;; make screen-name clickable ;; make source pretty and clickable (mapcar (lambda (sym) `(,sym . ,(symbol-value sym))) '(id text source created-at truncated user-id user-name user-screen-name user-location user-description user-profile-image-url user-url user-protected)))))
一番最後の(mapcar (lambda (sym) ...) '(id ...))がどうもこの関数のキモのようである。
ということで長くなったので一休み。このペースで終わるかなあ。不安だ。
本日のプロファイリングデータ
Function Name Call Count Elapsed Time Average Time ======================================== ========== ============ ============ twittering-http-get-default-sentinel 210 825.63031699 3.9315729380 twittering-render-friends-timeline 159 733.25885600 4.6116909182 twittering-format-status 74418 682.15694900 0.0091665584 xml-parse-tag 56682 285.23423599 0.0050321836 twittering-timer-action 210 268.58633499 1.2789825476 twittering-friends-timeline 210 268.55300099 1.2788238142 twittering-http-get 210 258.650464 1.2316688761 twittering-get-response-body 210 68.477939999 0.3260854285 xml-parse-region 210 67.574131000 0.3217815761 xml-parse-string 107673 38.983813999 0.0003620574 xml-substitute-special 108262 26.867760000 0.0002481735 twittering-xmltree-to-status 160 18.433747000 0.1152109187 twittering-status-to-status-datum 2912 17.943616000 0.0061619560 twittering-image-type 74418 4.0766180000 5.477...e-05 twittering-cache-status-datum 2911 3.7562859999 0.0012903765 xml-parse-attlist 56472 2.0487380000 3.627...e-05 twittering-decode-html-entities 17466 1.9062979999 0.0001091433 xml-maybe-do-ns 57061 1.6209119999 2.840...e-05 twittering-get-response-header 210 0.4974110000 0.0023686238 twittering-http-buffer 840 0.0928869999 0.0001105797 twittering-get-or-generate-buffer 1418 0.0755730000 5.329...e-05 twittering-wget-buffer 418 0.0671740000 0.0001607033 twittering-buffer 160 0.0115700000 7.231...e-05 xml-parse-dtd 11 0.006544 0.0005949090 twittering-mode 1 0.001119 0.001119 twittering-mode-init-variables 1 0.000857 0.000857 twittering-start 1 0.000128 0.000128 twittering-click 1 3.3e-05 3.3e-05
呼び出し関係は下記のとおり。
twittering-http-get-default-sentinel calls case-string twittering-render-friends-timeline calls twittering-buffer calls twittering-buffer twittering-http-buffer calls twittering-get-or-generate-buffer twittering-friends-timeline calls twittering-buffer twittering-wget-buffer calls twittering-get-or-generate-buffer twittering-wget-buffer twittering-icon-mode calls twittering-icon-mode twittering-render-friends-timeline calls twittering-buffer twittering-scroll-mode calls twittering-scroll-mode debug-print calls twittering-debug-buffer calls twittering-get-or-generate-buffer twittering-debug-buffer. twittering-debug-mode calls twittering-debug-mode twittering-mode twittering-update-lambda twittering-friends-timeline twittering-update-status-interactive twittering-erase-old-statuses twittering-click twittering-enter twittering-view-user-page twittering-format-status calls assocref twittering-icon-mode twittering-image-type twittering-local-strftime calls twittering-debug-buffer twittering-debug-mode twittering-friends-timeline twittering-icon-mode twittering-http-get calls twittering-http-buffer twittering-http-get twittering-http-get-default-sentinel twittering-stop twittering-get-or-generate-buffer twittering-scroll-mode debug-print twittering-format-status twittering-friends-timeline. twittering-get-response-header calls twittering-http-buffer twittering-get-response-body calls twittering-http-buffer twittering-cache-status-datum calls twittering-friends-timeline twittering-xmltree-to-status calls twittering-status-to-status-datum calls twittering-decode-html-entities calls twittering-ucs-to-char.
うーむ、まだまだである。
twittering-mode.elのちょっとした改良
デフォルトのユーザ名/パスワードのペアは.emacsに
;;; ;;; Twittering mode ;;; (require 'twittering-mode) (setq twittering-username "hyoshiok")
などとしてtwittering-username/twittering-passwordを書いておくのだが、twittering-passwordをファイルに書いておくのも精神衛生上よろしくない。そこでtwittering-passwordがnilだったら起動時に聞いてくるように変更してみた。(twittering-startの中)
$ diff -u twittering-mode.el.orig twittering-mode.el --- twittering-mode.el.orig 2008-02-05 19:59:46.000000000 +0900 +++ twittering-mode.el 2008-05-25 16:49:05.000000000 +0900 @@ -731,6 +731,10 @@ (defun twittering-start (&optional action) (interactive) + (if (null twittering-username) + (setq twittering-username (read-from-minibuffer "username: "))) + (if (null twittering-password) + (setq twittering-password (read-passwd "password: "))) (if (null action) (setq action #'twittering-friends-timeline)) (if twittering-timer
どうっすかね。