Hatena::ブログ(Diary)

カストリブログ

2009-10-31

Google Web履歴からWeb活動履歴をダウンロード

| 02:12

注意

ここのプログラムは、不具合があるため、正常に動作しない可能性が高いです。不具合を修正したソースコードは、下記の記事から取得できます。

http://d.hatena.ne.jp/hippu/20091207/1260206750


はじめに

GoogleツールバーをWebブラウザにインストールすると、Googleが検索活動の履歴(検索クエリと検索結果の閲覧履歴)やを記録し始める。

設定次第では検索活動の履歴だけでなく、ハイパーリンクやブックマーク、ブラウザの戻る・進む操作などを利用したWebページへのアクセスも記録する。

記録された履歴は、Google Web Historyというサービスから閲覧したり、検索することができる。

Google Web Historyから活動履歴をダウンロードしようと思ったのだが、備え付けのダウンロード機能がなかった。

幸いにもGoogle Web Historyは活動履歴をRSS形式で出力してくれるので、それを利用して自分でダウンロードプログラムを作成した。

内容

  • プログラム概要
  • 出力内容

プログラム概要

  • 作成言語:python 2.6(jsonを使用、simplejsonなどを導入すれば2.5、2.4でも動くはず)
  • 機能概要
    • 全履歴のダウンロード
    • 日付範囲を指定したダウンロード
    • 履歴の検索結果のダウンロード
  • 出力形式
    • JSON
  • 使い方

下のソースコードに適当なファイル名filenameを付けて、コマンドラインからpython filename -hとすればヘルプが表示されるのでそちらを参考。

#-*- coding:utf-8 -*-
import urllib2, urllib
from datetime import timedelta, datetime
import time
import json
from xml.dom.minidom import parseString 
import sys
import getopt
from urlparse import urlparse, urlunparse
import urllib
import re

def getText(elm):
    nodelist = elm.childNodes
    rc = ""
    for node in nodelist:
        if node.nodeType == node.TEXT_NODE:
            rc = rc + node.data
    return rc

def toDict(elm):
    ob = {}
    ob["id"] = getText(elm.getElementsByTagName("guid")[0])
    ob["title"] = getText(elm.getElementsByTagName("title")[0])
    ob["link"] = getText(elm.getElementsByTagName("link")[0])
    ob["date"] = getText(elm.getElementsByTagName("pubDate")[0])
    category = getText(elm.getElementsByTagName("category")[0])
    ob["category"] = category
    ob["description"] = getText(elm.getElementsByTagName("description")[0])
    
    
    if category == "browser result":
        if elm.getElementsByTagName("smh:bkmk"):
             ob["bkmk"] = getText(elm.getElementsByTagName("smh:bkmk")[0])
             ob["bkmkId"] = getText(elm.getElementsByTagName("smh:bkmk_id")[0])
             
             ob["bkmkLabels"] = []
             for label in elm.getElementsByTagName("smh:bkmk_label"):
                 ob["bkmkLabels"].append(getText(label))
                 
             ob["bkmkTitle"] = getText(elm.getElementsByTagName("smh:bkmk_title")[0])
             ob["category"] = "bookmark"
    elif category.endswith("result"):
        ob["queryId"] = getText(elm.getElementsByTagName("smh:query_guid")[0])
    elif category.endswith("query"):
        pass
    else:
        raise Error("")
    
    return ob

def decideDateRange(startDate, endDate, all):
    if all:
        endDate = datetime.now() + timedelta(days=1)
        startDate = datetime(1900, 1, 1)
    else:
        if not startDate and not endDate:
            startDate = datetime.now() - timedelta(days=1)
            endDate = datetime.now()
        elif not startDate:
            startDate = endDate
            endDate = endDate + timedelta(days=1)
        elif not endDate:
            endDate = startDate + timedelta(days=1)
        elif endDate < startDate:
            temp = startDate
            startDate = endDate
            endDate = temp
            
    return startDate, endDate 

def getWebHistoryOrderByDate(opener, startDate, maxTime, num, query, length):
    if query:
        mode = "find"
    else:
        mode = "lookup"
        
    count = 0
    entries = []
    while True:
        params = {"hl" : "en", "output" : "rss", "lr" : "lang_ja", "max" : maxTime, "num" : num, "q" : query, "sort" : "date"}
        req = urllib2.Request('http://www.google.com/history/%s?%s' % (mode, urllib.urlencode(params)))
#       rssをutf8で出力させるのに必要
        req.add_header("User-Agent", "Mozilla/5.0")
        pagehandle = opener.open(req)
        
        if pagehandle.code != 200:
            raise Error("")
        
        dom = parseString(pagehandle.read())
        if not dom.getElementsByTagName("item"):
            dom.unlink()
            return entries
        
        for entry in dom.getElementsByTagName("item"):
            date = getText(entry.getElementsByTagName("pubDate")[0])
            d = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S GMT") + timedelta(hours=9)
            tempTime = int(time.mktime(time.strptime(d.strftime("%y%m%d %H:%M:%S"), "%y%m%d %H:%M:%S")) * (10 ** 6))
            #サマータイムの都合で、maxTimeの一時間後の履歴までが取得されるため
            #重複した履歴を無視
            if tempTime > maxTime:
                continue
            
            if d < startDate or length <= count:
                dom.unlink()
                return entries
            
            entries.append(toDict(entry))
            count += 1
        
        d = datetime.strptime(entries[-1]["date"], "%a, %d %b %Y %H:%M:%S GMT") + timedelta(hours=9)
        maxTime = int(time.mktime(time.strptime(d.strftime("%a, %d %b %Y %H:%M:%S"), "%a, %d %b %Y %H:%M:%S")) * (10 ** 6))
        dom.unlink()
    
    return entries

def getWebHistoryOrderByRelation(opener, start, num, query, length):
    entries = []
    count = 0
    while True:
        params = {"hl" : "ja", "output" : "rss", "lr" : "lang_ja", "start" : start, "num" : num, "q" : query, "sort" : ""}
        req = urllib2.Request('http://www.google.com/history/find?%s' % (urllib.urlencode(params)))
#       rssをutf8で出力させるのに必要
        req.add_header("User-Agent", "Mozilla/5.0")
        pagehandle = opener.open(req)
        
        if pagehandle.code != 200:
            raise Error("")
        
        dom = parseString(pagehandle.read())
        if not dom.getElementsByTagName("item"):
            dom.unlink()
            return entries
        
        for entry in dom.getElementsByTagName("item"):
            if length <= count:
                dom.unlink()
                return entries
            else:
                entries.append(toDict(entry))
                count += 1
            
            
            
        l = len(dom.getElementsByTagName("item"))
        start += l
        dom.unlink()
        
    return entries    

def dateStrCmp(dateStr1, dateStr2):
    d1 = datetime.strptime(dateStr1, "%a, %d %b %Y %H:%M:%S GMT")
    d2 = datetime.strptime(dateStr2, "%a, %d %b %Y %H:%M:%S GMT")
    
    if d1 > d2: return 1
    elif d1 < d2: return -1
    else:   return 0
    
def correctRelation(entries):
    queries = {}
    p = re.compile("[0-9]+")
    for entry in entries:
        if entry["category"].endswith("query"):
            mob = p.match(entry["description"])
            if mob:
                count = int(mob.group())
            else:
                count = 0
            queries[entry["id"]] = [entry, count]
            
    unknownSearchResults = []
    for entry in entries:
        if entry["category"].endswith("result") and entry["category"] != "browser result":
            if entry["queryId"] in queries:
                queries[entry["queryId"]][1] -= 1
            else:
                unknownSearchResults.append(entry)
    
    unknownQueries = [[v[0], v[1]] for v in queries.values() if v[1] > 0]
    unknownQueries.sort(lambda x, y: -dateStrCmp(x[0]["date"], y[0]["date"]))
    
    for e in unknownSearchResults:
        if len(unknownQueries) == 0:
            break
        
        query, count = unknownQueries[0]
        if dateStrCmp(e["date"], query["date"]) < 0:
            unknownQueries = unknownQueries[1:]
        else:
            query, count = unknownQueries[0]
            e["queryId"] = query["id"]
            unknownQueries[0][1] -= 1
            if unknownQueries[0][1] <= 0:
                unknownQueries = unknownQueries[1:]
                
def discoverGoogleScholar(entries):
    for e in entries:
        if e["category"] == "browser result" and e["title"].startswith("http://scholar.google.co.jp"):
            o = urlparse(e["title"])
            
            if o.path in ["", "/", "/schp"]:
                e["title"] = "Google Scholar"
            elif o.path == "/scholar":
                query = {}
                for pair in o.query.split("&"):
                    k, v = pair.split("=")
                    k = urllib.unquote_plus(k)
                    v = urllib.unquote_plus(v)
                    query[k] = v
                
                if "q" in query and "start" not in query:
                    e["category"] = "scholar query"
                    e["title"] = query["q"].encode('raw_unicode_escape').decode('utf8')

#広告、画像などの断片ページのURLに頻出する文字列
FLAGMENT_PAGE_WORDS = [
                       "ad", "rot", "banner", "rss", "footer", "cookie", "headline", "gadget", "widget", "rcm", "doubleclick",
                       "right", "button", "send", "visitor", "affiliate", "img", "wrs", "topics", "thumb", "access"]

def findFlagmentPageWords(url):
    url = url.lower()
    for w in FLAGMENT_PAGE_WORDS:
        if url.find(w) != -1:
            return True
    return False

#断片的なページのURLにはほぼ出現しない文字列
NOT_FLAGMENT_PAGE_WORDS = ["pdf", "entry", "archive"]

def findNotFlagmentPageWords(url):
    url = url.lower()
    for w in NOT_FLAGMENT_PAGE_WORDS:
        if url.find(w) != -1:
            return True
    return False


def removeNoise(entries):
    entries = [e for e in entries if not (e["category"] == "browser result" and e["title"].startswith("https://"))]
    entries = [e for e in entries if not (e["category"] == "browser result" and findFlagmentPageWords(e["title"]))]
    entries = [e for e in entries if e["category"] != "browser result" or not e["title"].startswith("http://") or findNotFlagmentPageWords(e["title"])]
        
    entries = [e for e in entries if e["link"].lower().find("mail") == -1]
    entries = [e for e in entries if e["title"].lower().find("headline") == -1]
    entries = [e for e in entries if e["title"].lower().find(u"ヘッドライン") == -1]
    
    l = len(entries) 
    if  l <= 1:
        return entries
    
    resultEntries = []
    for i in range(0, l - 1):
        cur = entries[i]
        next = entries[i+1]
        
        if cur["link"].strip() == "":
            continue
        
        if cur["category"] == "browser result" and cur["link"] == next["link"]:
            cd = datetime.strptime(cur["date"], "%a, %d %b %Y %H:%M:%S GMT")
            nd =  datetime.strptime(next["date"], "%a, %d %b %Y %H:%M:%S GMT")
            td = cd - nd
            if td.seconds < 3600:
               continue
        resultEntries.append(cur)
    resultEntries.append(next)

    return resultEntries

def getWebHistory(user, passwd, startDate=None, endDate=None, all=False, num=500, query=None, sortedByDate=True, length=sys.maxint, start=0):
    if all:
        num = 1000
        start = 0
        length = sys.maxint
        
    num = min(num, length)
    
    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passman.add_password(None, "www.google.com", user, passwd)
    authhandler = urllib2.HTTPBasicAuthHandler(passman)
    opener = urllib2.build_opener(authhandler)
    
    if sortedByDate:
        startDate, endDate = decideDateRange(startDate, endDate, all)
        print "startDate", startDate
        print "endDate", endDate
        maxTime = int(time.mktime(time.strptime(endDate.strftime("%y%m%d %H:%M:%S"), "%y%m%d %H:%M:%S")) * (10 ** 6))
        entries = getWebHistoryOrderByDate(opener, startDate, maxTime, num, query, length)
        
    elif query:
        entries = getWebHistoryOrderByRelation(opener, start, num, query, length)
    
    result = {"user" : user, "date" : datetime.now().strftime("%Y%m%d %H:%M:%S"), "entries" : entries, "query" : query, "all" : all, "sortedByDate" : sortedByDate, "start" : start}
    if sortedByDate:
        result["startDate"] = startDate.strftime("%Y%m%d %H:%M:%S")
        result["endDate"] = endDate.strftime("%Y%m%d %H:%M:%S")
    elif query:
        result["startDate"] = None
        result["endDate"] = None
    
    return result
        
def printHelp():
    print "Usage: getwebhistory.py -u username -p password [OPTION]"
    print u"-s <date> 履歴を取得する期間の始まりの日時をyyyymmddまたはyyyymmdd hh:mm:dd形式で指定"
    print u"-e <date> 履歴を取得する期間の終わりの日時をyyyymmddまたはyyyymmdd hh:mm:dd形式で指定"
    print u"-q <query> 履歴を検索するためのクエリを指定"
    print u"-l <length> 履歴の取得数を指定"
    print u"-n <num> 一回のHttpリクエストで取得する履歴の数(最大1000)を指定"
    print u"-h --help プログラムの使用方法を表示して、プログラムを終了"
    print u"-f <file> 取得した履歴を出力するファイル名を指定"
    print u"--all このオプションを指定すると履歴を全て取得します。-q、-f、--order-relation、--raw、--not-correct-relation以外のオプションは無視します"
    print u"--order-relation このオプションを指定すると履歴を関連度順に取得します。-qで空でないクエリを指定したときのみ有効です。"
    print u"--start=<num> 取得する履歴のオフセットを指定します。-qに空でないクエリを指定し、かつ--order-relationを指定したときのみ有効です。"
    print u"--raw このオプションを指定すると、Google Web Historyから取得した履歴をそのまま出力します。指定しない場合は、タイトルやURLから不要なページを推測し、それを除去します。"
    print u"--not-correct-relation このオプションを指定するとクエリのIDから単純に判断できないクエリと検索結果の対応の推測を行いません。オプションを指定しない場合は、この推測を行います。"
    
if __name__ == '__main__':
    print sys.argv
    opts, args = getopt.getopt(sys.argv[1:], ":u:p:s:e:q:l:n:hf:", ["all", "order-relation", "start=", "help", "raw", "not-correct-relation"])
    for o, a in opts:
        if o in ["-h", "--help"]:
            printHelp()
            sys.exit()
    
    user = None
    passwd = None
    startDate=None
    endDate=None
    query = None
    length = length=sys.maxint
    num = 300
    sortedByDate=True
    start=0
    fname = None
    all = False
    raw = False
    correctRelationFlag = True
    
    for o, a in opts:
        print o, a
        if o == "-u":
            user = a 
        elif o == "-p":
            passwd = a
        if o == "-s":
            try:
                startDate = datetime.strptime(a, "%Y%m%d")
            except ValueError:
                startDate = datetime.strptime(a, "%Y%m%d %H:%M:%S")
        elif o == "-e":
            try:
                endDate = datetime.strptime(a, "%Y%m%d")
            except ValueError:
                endDate = datetime.strptime(a, "%Y%m%d %H:%M:%S")
        elif o == "-q":
            query = a
        elif o == "-l":
            length = int(l)
        elif o == "-n":
            num = int(a)
        elif o == "-f":
            fname = a
        elif o == "--all":
            all = True
        elif o == "--order-relation":
            sortedByDate = False
        elif o == "--start":
            start = int(a)
        elif o == "--raw":
            raw = True
        elif o == "--not-correct-relation":
            correctRelationFlag = False
            
    result = getWebHistory(user, passwd, startDate, endDate, all, num, query, sortedByDate, length, start)
    
#    discoverGoogleScholar(result["entries"])
    if not raw:
        result["entries"] = removeNoise(result["entries"])
    
    if correctRelationFlag:
        correctRelation(result["entries"])
    
    #重複した履歴がないか確認
    entries = result["entries"]
    for i in range(0, len(entries) -1):
        d1 = datetime.strptime(entries[i]["date"], "%a, %d %b %Y %H:%M:%S GMT")
        d2 = datetime.strptime(entries[i+1]["date"], "%a, %d %b %Y %H:%M:%S GMT")
        if d1 < d2:
            print entries[i]["title"], entries[i+1]["title"]
            raise Exception("")

    if not fname:
        print json.dumps(result)
    else:
        fo = open(fname, "w")
        fo.write(json.dumps(result, indent=2))
        fo.close()

出力内容

このプログラムでは、取得した履歴のデータをJSON形式で出力します。

出力内容は、履歴の取得時に指定した条件と履歴のデータです。

履歴データの持つ情報は、履歴の種類によって異なります。

トップレベルの出力(187行目付近を参照)
  • user:ユーザ名
  • date:ダウンロード日時
  • entries:履歴データ
  • query:クエリ
  • all:全ての履歴を取得したか否か
  • start:取得開始位置
  • sortedByDate:
  • startDate:履歴の取得するときの日時の下限値
  • endDate:履歴の取得するときの日時の上限値
履歴データについて
  • id
  • title:閲覧ページ名、クエリ
  • link:閲覧ページや検索結果ページのURL
  • date:閲覧日時、検索実行日時
  • description:不明
  • category:閲覧データの種類
    • XXX query:検索クエリ(XXXにはweb, map, imagesなどが入る)
    • XXX result:検索結果中で閲覧したページ(XXXにはweb, map, imagesなどが入る)
      • queryId:検索クエリのID
    • browser result:進む・戻る、ブックマークからのページアクセスなどのブラウザ操作によるページ閲覧
    • bookmark:browser resultの内、Google Bookmarkに登録されているページへのアクセス
      • bkmk:
      • bkmkId
      • bkmkLabels:タグ
      • bkmkTitle:ブックマーク名

プログラム勉強生プログラム勉強生 2009/12/07 10:03 こんにちわ。
google の情報をこのような形で抜き出せるなんてしらず、感動しました。笑
しかし、現在、Web履歴のRSSのサーバは死んでいるようで、使えないのですが、
これはどういうことなのでしょうか?
このプログラムを走らせると502エラーを吐きます。
今日で5日目くらいです。
GoogleWeb履歴RSSサーバは落ちたのでしょうか?
分かる範囲で教えてくただけると勉強に成ります。
宜しくお願い致します。

hippuhippu 2009/12/08 02:44 こんにちは。

不具合の指摘してくださり、ありがとうございます。
指摘を頂くまで、全然気づきませんでした。自分にとって、このプログラムが動作しないと遠くない将来困るので、非常に助かりました。

さて本題の不具合の原因ですが、GoogleのRSSサーバが死んでいる訳ではなく、Cookieによる認証をしていなかったためと考えています。そのため、認証失敗を表す401のUnAuthorizedエラーになり、副次的に502エラーをGoogleは吐いたのでしょう。

この不具合を修正したプログラムと、もう少し詳細な原因考察は、下記のURLに記載したので、そちらを見て頂ければ幸いです。http://d.hatena.ne.jp/hippu/20091207/1260206750

プログラム勉強生プログラム勉強生 2009/12/09 01:26 めっちゃ詳しく書いて頂き有り難う御座いました。
大変勉強になりました。

ちょいちょい、見させて頂いております。
また、気になることがあったら書き込むかもしれませんが、
その時は相手して下さいね。笑

Connection: close