Chrome Extention作成時に個人的に詰まった所と対策

Extentionのアップロード時に「An error occurred: please try again later.」というエラーメッセージが出る

知らないうちに、Googleのログイン状態が解除されたことが原因。また、Googleにログインしてから、アップをやり直す。

Extentionをアップロードすると「Pending」という状態になる。そして、アップロードはできるが、Extentionのページからインストールをしても古いバージョンのままになる

 作成したExtentionにローカルファイルへのアクセス権限を与えた(manifest.jsonで、permissionsにfile:/*/*がある)ことが原因。
 現状では、ローカルファイルへのアクセスを行うExtentionは、人手で悪質なプログラムでないかを判断した後に配布を許可する模様。どうしても必要というわけでないなら、ローカルファイルへのアクセス権限をExtentionに与えない。

Background PageからExtentionのポップアップを開く方法について

 現状、その方法はない。そのうち、用意してくれるっぽい。

chrome.tabs.updateやchrome.tabs.executeScriptを呼ぶとクラッシュする

 prototype.jsをソースにインクルードしていることが原因かも。prototype.jsを利用しないで実装する。(クラッシュ時に特にエラーメッセージもでないので、3時間くらいクラッシュ原因が分からず苦労した。)

ContentScriptからBackgroundPageへメッセージを送るメッソドchrome .tabs .sendRequestに対する返信用コールバック関数を呼び出しても、エラーメッセージは出ないが動作しない

 返信用コールバック関数の引数に、DOMオブジェクトなどを渡すとこうなる。返信用コールバック関数の引数には、JSONオブジェトしか渡せないので、引数をJSON形式にする。

Content Scriptsでのクロスドメイン通信

Contents Scripsとクロスドメイン通信

 ChromeのExtentionの実装方法の一つに、Content Scriptsというものがあります。一言で言えば、FirefoxGreasemonkeyChrome版です。Content Scriptsを使用すると、任意のWebページに対して、JavaScriptを実行することができます。これにより、本来Webページにはない機能をWebページに追加することができます。


 ChromeのExtentionでは、普通のWebページ内のJavascriptとは違い、クロスドメイン通信が可能です。
 BackgroudPagesやポップアップなどからクロスドメイン通信をする場合は、XMLHttpRequestなり、jQueryやprototypeなどで用意されているHttpRequest用のオブジェクトを使用すれば可能です。
 一方、Contents Scriptsから直接クロスドメイン通信をすることはできません。しかし、BackgroundPagesを用意し、通信自体はBackgroudPagesに行わせるという回りくどい方法で実現可能です。
 具体的には、Content Scriptsからchrome.extention.sendRequestという関数を呼び出し、httpリクエストのパラーメータとレスポンスを処理するコールバックをBackgroundPagesに送ります。その後、BackgroundPagesは
クロスドメイン通信を行い、そのレスポンスをContentScriptsから受け取ったコールバックに渡して呼び出します。この一連の流れを下に図示しておきます。


ContentScriptsによるクロスドメイン通信概要図



 以下では、Google検索時にYahooによる検索も同時に行い、Googleの広告が表示される部分にYahooの検索結果を埋め込むという簡単なExtentionを例に、Contents Scriptsからのクロスドメイン通信を説明してみます。

サンプルExtention

サンプルのソースコード
(ソースコード中のYahoo検索 APIを利用するためのアプリケーションIDを記述している箇所を変更しなければ動きません)

Content Scriptsによるクロスドメイン通信のサンプルExtention

manifest.json
1 {
2   "name": "Double Search",
3   "version": "1.0",
4   "description": "Double Search",
5   "background_page": "background.html",
6   "content_scripts": [
7     {
8       "matches": ["http://www.google.co.jp/search*", "http://www.google.co.jp/search*"],
9       "js": ["prototype.js", "contentscript.js"],
10       "run_at" : "document_end"
11     }
12   ],
13   "permissions": [
14     "http://search.yahooapis.jp/WebSearchService/V1/webSearch*"
15   ]
16 }

 5行目でクロスドメイン通信を行うBackgroundPagesとして「background.html」を指定しています。
 6行目から12行目がContentScriptsを設定する上で重要な箇所です。7行目のmatchesでContentScriptsを動作させるサイトを指定しています。8行目で使用するJavascriptファイルを指定しています。ここで記述した順番にロードされます。contentscript.jsではJavascriptライブラリのprototype.jsを利用するので、contentscript.jsより先にprototype.jsを記述しておきます。9行目では動作タイミングを指定しています。
 13行目から15行目でクロスドメイン通信を行うサイトのURL(この例では、Yahoo検索 APIのURL)を指定しています。

contentscript.js(重要部分のみ)
13 function searchByYahoo() {
14 	var query = getQuery();
15 	
16 	chrome.extension.sendRequest(query, function(res) {
17 		console.log("get response");
18 		console.log(res);
省略
30 	});
31 }

14行目で、Google検索の検索結果ページからクエリを取得しています。
16行目では、chrome.extention.sendRequestを呼び出すことで、BackgroundPages(この例では「background.html」)へYahoo検索に使用するクエリと、Yahooの検索結果を処理するコールバックを渡しています。

background.html
1 <html>
2 <head>
3 <script type="text/javascript" src="prototype.js"></script>
4 <script>
5 chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
6 	console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension");
7 	
8 	new Ajax.Request("http://search.yahooapis.jp/WebSearchService/V1/webSearch", {
9 		method : "get",
10 		parameters : {"query" : request, "appid" : "appid"},
11 		onSuccess : function(res) {
12 			sendResponse(res.responseText);
13 		},
14 		onFailure : function(res) {
15 			sendResponse(null);
16 		}
17 	});
18 });
19 </script>
20 </head>
21 <body></body>
22 </html>

 ここで一番重要なのは、5行目で呼び出している関数chrome.extention.onRequest.addListenerです。この関数では、ContentScriptsからsendRequestを呼び出したときに呼び出すコールバックが引数となります。このコールバックの第一引数requestは、ContentScriptsから送られてきたメッセージ(chrome.extention.sendRequestの第一引数)、第3引数sendResponseは返信を処理するコールバック(chrome.extention.sendRequestの第三引数)となります。
 8から17行目で、実際にクロスドメイン通信を行っています。12行目で、sendResponseを呼び出して、クロスドメイン通信の相手サイトからのレスポンスの内容をContentScriptsへ送っています。ここではレスポンスのオブジェクトそのものではなく、テキストレスポンスを渡しています。これは、sendResponseには、JSONオブジェクトかundefiedしか引数として渡せないためです。もし、XMLレスポンスなどを渡すとsendResponseは動作しません。ここで、私は1時間くらいはまりました)。

最後に

特になし

Google Web履歴からWeb活動履歴をダウンロード(Cookieによる認証版)

はじめに

 以前公開した、Web活動履歴ダウンロードプログラムに不具合が発生しました。その原因は、Cookie認証を利用していないことでした。そこで、プログラムをCookie認証するように変更しました。


ソースコード


・注意事項

  1. 詳細な使い方は、「python getwebhistory.py -h」の出力を参照してください。
  2. htmlパーサーのlxmlというpythonのライブラリを導入する必要があります。


・12月7日以降の修正

  • 12月9日:履歴中にvideo resultなどが含まれるときにエラーになる不具合の修正

発生した問題

 以前に記載したWeb活動履歴(以下、単に履歴)のダウンロードプログラムが、気づいたら502エラーを吐いて動作しなくなっていたことに気付いた。


 よくよく調べてみたら、Linuxwgetコマンドを使用した場合でも、下記のように502エラーと401エラーを吐いて、履歴のRSSが取得できなくなっていた。

wgetコマンドによる履歴の取得
$ wget --http-user=username --http-passwd=passwd 'https://www.google.com/history/lookup?hl=ja&output=rss'

--2009-12-08 00:57:16--  https://www.google.com/history/lookup?hl=ja&output=rss
www.google.com をDNSに問いあわせています... 66.249.89.99, 66.249.89.103, 66.249.89.104, ...
www.google.com|66.249.89.99|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 401 Unauthorized
www.google.com:443 への接続を再利用します。
HTTP による接続要求を送信しました、応答を待っています... 502 Bad Gateway
2009-12-08 00:57:17 エラー 502: Bad Gateway。

原因考察

 以前のダウンロードプログラムとwgetコマンドともに、履歴のRSSを取得できなくなった。しかし、Webブラウザ上では何事もなく履歴のRSSを閲覧することはできる。この2つの違いは、おそらく認証方法です。前者はCookieを利用せず、Didgest認証をしています。後者はCookieを利用した認証をしています。


以上のことから考えて、今回の不具合の原因は、Googleが主な認証方式として、Cookie認証を利用するためと考えました。


実は当初から、10月30日のダウンロードプログラムは、402エラーを吐くときがしばしば起こっていました。この原因は、基本的にはGoogleは認証方式としてCookie認証を利用しているものの、ごく一部のGoogleのサーバでは、Didgest認証も許可していたためと考えられます。

Cookie認証の利用

 履歴をGoogleから取得する前に、Cookie認証を行うように、ダウンロードプログラムを変更しました。

ソースコード


変更の味噌である箇所を解説します。

冒頭部分
import lxml.html import fromstring
import cookielib

まずは、htmlやxmlのパーサーのlxmlと、cookieを利用するためにcookielibをインポートします。cookielibはpythonの標準ライブラリですが、lxmlについては、別途導入が必要です。

Cookie認証部
def certifyByCookie(user, passwd):
    cj = cookielib.CookieJar()
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
    #プロキスを使用する場合
    #proxySupport = urllib2.ProxyHandler({'プロキシサーバのスキーム(httpなど)': 'プロキシサーバのURL'})
    #opener = urllib2.build_opener(proxySupport, urllib2.HTTPCookieProcessor(cj))
    
    #
    #認証時にPOSTするデータとCookieを取得
    #
    url = "https://www.google.com/accounts/ServiceLogin?hl=ja&nui=1&service=hist"
    req = urllib2.Request(url)
    req.add_header("User-Agent", "Mozilla/5.0")
    pagehandle = opener.open(req)
    doc = fromstring(pagehandle.read())
    #認証ページ中のinputタグの値を全て取得
    data = dict([(input.name, input.value) for input in doc.xpath('//input')])
    #ユーザIDとパスワードを設定
    data["Email"] = user 
    data["Passwd"] = passwd

    #
    #実際に認証
    #
    url = "https://www.google.com/accounts/ServiceLoginAuth?service=hist"
    #OSがWindowsであり、ここでエラーが出る場合、コメントアウト
    #data["signIn"] = u"ログイン".encode("sjis")
    req = urllib2.Request(url, urllib.urlencode(data))
    req.add_header("User-Agent", "Mozilla/5.0")
    pagehandle = opener.open(req)

    return opener

ここでは、Webブラウザの行うGoogle Web履歴で認証と同じ処理を行っています。

  1. 認証時にPOSTするデータ(data)とCookie(openerが内部的に保持)を取得
    1. 認証ページにアクセス
    2. 認証ページのhtmlをパースして、inputタグの値を取得
    3. ユーザIDとパスワードを設定
  2. 実際に認証
    1. httpリクエストを発行
    2. 認証に必要なCookie情報がopenerに内部的に保持される

後は、このcertifyByCookieが返すopenerを利用して、以前と同じようにGoogle Web履歴にRSS取得するためのHttpリクエストを発行することにより、不具合は修正なくなりました。

追記

wgetコマンドの方ですが、ブラウザの生成したcookieなどを利用すれば、Web履歴は取得できます。

Google ブックマークのChrome用のアドオンを作ってみた2

Google ブックマークのChrome用のアドオン2

 先週作成したGoogle ブックマークのChrome用のアドオンの機能強化をしてみました。

  • 以前からの機能
    • ブックマーク登録機能
    • ブックマーク参照機能
  • 今回追加した機能
    • ブックマークのタグクラウド表示
    • ブックマークの削除機能
    • ラベルのAND、NOT指定
      • 例えば、ラベル「pytnon」が付いているが、ラベル「コメディアン」が付いていないブックマークだけを表示させることが可能です。
スクリーンショット

・ラベルのクラウド表示


・ラベルのAND、NOTを指定したブックマークの表示


追記

11月15日19:00時点で、ソースコードと見た目を微修正した。心なしか、動作が軽くなった。

Google ブックマークのChrome用のアドオンを作ってみた

 Google ブックマークのChrome用のアドオンを作成したので、それを公開して見ます。機能は非常にシンプルで以下の2つしかありません。

  1. ブックマーク登録機能
  2. ブックマーク参照機能

 アドオンの入手は下のページからできます。
Google ブックマークのChrome用のアドオン


スクリーンショット

Windows環境下でlxml+python(lxmlのバイナリは未使用)

はじめに

 最近、pythonでHTMLテキストをパースすることが必要になった。当初、HTMLパーサとして、BeautifulSoupを利用していた。しかし、しばしばある特定のWebページのパース時に、HTMLParseErrorという例外が発生した。そのため、他のpython用のHTMLパーサを調べた所、lxmlというものがあることを知った。そこで、このlxmlをWindows環境下でインストールしようとしたのだが、かなり手間取った。

 この記事では、Windows環境下で、lxmlのインストーラを使用しないで、lxmlをpythonから利用可能にする手順を説明する。

 lxmlのインストールが完了した時点で気付いたのだが、最新でないバージョンのlxmlにはWindows用のインストーラが配布されている。そちらを利用すれば、この記事で説明するような複雑な手順なしにlxmlをインストールできると思われる。

前提

  1. OSはwindows vista
  2. pythonのバージョン:2.6

lxmlのページ

公式ページ
配布場所

lxmlのインストール

 easy_installを既に導入している場合は、単純にコマンドライン上から「easy_install lxml」と実行します。
 easy_installが未導入の場合は、まずは上記のlxmlのページから「lxml2.2.3.tar.gz」をダウンロードします。このファイルを解凍したら、コマンドライン上で、解凍場所まで移動し、「python setup.py install」と実行します。

vcvarsall.batが見つかりませんというエラーメッセージが出た場合

原因はVisual Studio C++2008がを入れていないためです。なので、Visual Studio C++2008を入れます(Visual Studio C++2005では駄目です)。

libxml/xmlversion.hが見つかりませんというエラーメッセージが出た場合

 lxmlのインストールに必要なライブラリlibxml2がlibxslt存在しないためです。libxml2は、iconvとzlibというライブラリも使用しています。したがって、libxml2、libxslt、iconv、zlibの4つのライブラリのWindows用バイナリをこちらのサイトからダウンロードします。


 各ライブラリは、bin、include、libの3つのフォルダから構成されています。環境変数を設定する手間を考えて、4つのライブラリの中身を1つのフォルダにまとめる。このフォルダのフルパス名を仮に「my」とします。そして、環境変数のLIBに「my/lib」を、INCLUDEに「my/include」を追加します。必須ではないですが、各ライブラリ内のコマンドを手軽に使用したい場合は、環境変数PATHに「my/bin」を追加します。

 この設定までを済ませれば、lxmlのインストールは成功するはずです。



lxmlのインストール後の設定

 とりあえず、lxmlがインストールできたかを確認するために、pythonインタプリンタ上から、以下のコマンドを実行します。

from lxml import etree

エラーにならなければ、インストール完全成功です。

ImportError:DLL load failedというエラーメッセージが出力される場合

 先ほど導入したライブラリのdll(「my/bin」にあります)がpythonから見えていないことが原因です。環境変数PATHに「my/bin」を追加していてもエラーになります。
対処法は以下の3つです。

  1. python.exeと同一フォルダに「my/bin」内のdllを置く
  2. pythonのインストール場所/Scripts」内のdllを置く
  3. lxmlのファイル群と同一の場所にdllを置く

 私の場合は、lxmlのファイル群の場所は「pythonのインストール場所/Lib/site-packages/lxml-2.2.3-py2.6-win32.egg/lxml」でした。

Google Web履歴のRSSの仕様

リクエスト時のパラメーター

単に履歴を取得する場合と、履歴を検索する場合とでリクエスト先のURLや使用可能なパラメータが異なる。

単に履歴を取得する場合

URL:http://www.google.com/history/lookup?output=rss&lr=lang_ja&hl=ja

パラメータ 意味
max 取得する履歴の日時の上限値(ローカル時間から求めたエポック時間)
min 取得する履歴の日時の下限値(ローカル時間から求めたエポック時間)
num 取得する履歴数(指定しなければ20?)
start 取得する履歴のオフセット
yr
day 日付
month
output 履歴の出力形式。現在指定できるのはrssのみ
lr 言語関係。lang_jaに指定してしておくのが無難
hl 言語関係。jaに指定しておくのが無難
st 取得する履歴の種類(web,img,ad,fg,blogs,news,bookds,vid)
履歴を検索する場合

URL:http://www.google.com/history/find?output=rss&lr=lang_ja&hl=ja

パラメータ 意味
q 検索クエリ。指定されていない場合は、単に履歴を取得
sort 検索結果の表示順。dateを指定すると日付順、指定しなければ関連度順

(単に履歴を取得するときのパラメータはほぼ使用可能)

注意点

maxとminを同時に指定しない方がよいかも。
それは、maxとminで指定した時間の範囲内に含まれる履歴数がnum以下の場合、numに足りない分だけ最新の履歴データが出力されるためである。