FirefoxやChrome(とChromium)では、Webページの閲覧履歴がSQLite3のデータベースとして保存されているようです。
ご存知の通り、SQLiteはローカルファイルを使ったDBMSなので、コネクション実装とSQLが吐ける管理ツールがあれば中身を調べること自体は可能です。
とは言え、今回はPythonでやってしまおうと思います。
もくじ:
はじめに
とりあえずCSVにしておけばExcelなり他のスクリプトで再利用なりできると思いますので、まずは対象のテーブルをCSVへ書き出すことを考えます。
取得する情報はページのタイトルとURL、最終閲覧時刻(タイムスタンプ)です。閲覧回数など他の情報も同様に取得することもできますが、単にどのページを見たのかを知りたいだけならシンプルにできます。
また、ファイルの場所を判別するのが面倒だったので、今回はWindowsのみを対象としています。SQL(クエリ)を投げて必要な情報を取得するところ以降はどのプラットフォームでも同じだと思います。サンプルは最後のほうに記載します。
1. ブラウザ履歴の取得
ブラウザによって履歴を格納しているSQLiteファイルの所在や、テーブルのスキーマなど微妙に異なっています。
1.1 ファイルのコピー作成
ブラウザが起動中の場合など競合を避けるために、ブラウザが使用しているDBファイルではなく、コピーを作成した上で操作を行います。
1 2 3 4 5 | # chrome path_ = os.path.join(os.getenv("HOMEDRIVE"), os.getenv("HOMEPATH"), "AppData", "Local/Google/Chrome/User Data/Default/History") cache="data/history_repl" shutil.copy(src=path_, dst=cache) |
1.2 テーブル
今回は閲覧したページのタイトル、URLなどが取得できればよいので、とりあえずは履歴URLを格納しているテーブルを参照します。
Firefoxの場合はmoz_places
テーブル、Chromeの場合はurls
テーブルが対象です。
Select文でタイトル、URL、最終閲覧時刻を指定します。
ただし、テーブル名はもちろん最終閲覧時刻についても、FirefoxとChromeではカラム名(とフォーマット)が異なるので考慮が必要です。
1 2 3 4 | # chrome query_ = "select title,url,last_visit_time from urls" # firefox query_ = "select title,url,last_visit_date from moz_places" |
1.3 CSVへの保存
sqlalchemy
とpandas
を使ってDBへのクエリとCSVへの書き出しを実行します。もちろん標準のsqlite3
モジュールで頑張ってDBを参照することもできますが長くなるので。
これらのモジュールは環境によっては追加で導入が必要かも知れません。無い場合(ImportError
になる)はpipで導入できます。
1 2 | > pip install pandas > pip install sqlalchemy |
pandas
が便利なのは、テーブル(クエリの結果)からそのままDataframeを生成できるところですね。
たまにタイトルがないデータがあるので、不要な場合は削っておきます。
1 2 3 4 5 6 | import pandas as pd from sqlalchemy import create_engine cache="data/history_repl" df = pd.read_sql_query(query_, create_engine("sqlite:///{}".format(cache))) df = df.dropna(subset=['title']) |
若干の注意が必要なのは、CSVをExcelで開く場合はBOM付きでないと文字化けすること。エンコードをDataFrame.to_csv()
のオプションで指定します。
1 | df.to_csv(csv_dst, encoding='utf-8-sig') |
2. 直近の履歴のみを取り出す
CSVをロードして一旦Dataframeとして扱えるようになれば、ここ最近にアクセスした履歴のみを抽出することもできます。
要は最終閲覧時刻をもとに判断すればいいのですが、そのフォーマットがブラウザによってちょっと異なる点に注意が必要です。
Firefoxの場合はPOSIXタイムスタンプのマイクロ秒、Chromeの場合はUTCで1601年からのマイクロ秒です。
1 2 3 4 5 6 7 8 9 10 | # chrome time_col = 'last_visit_time' ts_ = datetime.utcnow() - timedelta(days=days) - datetime(1601, 1, 1) ts_ = ts_.total_seconds() # firefox time_col = 'last_visit_date' ts_ = (datetime.now() - timedelta(days=days)).timestamp() # get newer row than specified days count res = df[df[time_col] > ts_ * 1000000] |
3. サンプル
今回の使い方はこんな感じ。コマンドラインでCSVの作成とロードを実行できます。
1 2 3 4 | > python read_browse_history.py extract firefox > python read_browse_history.py show 3 Google (2019-03-04 09:57:08.367208) ITエキスパートのための問題解決メディア - @IT (2019-03-04 09:57:39.353355) |
サンプル全体は次の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | import os import sys import pandas as pd from traceback import print_exc class platforms: chrome = "chrome" firefox = "firefox" def resolve_conf(pf): # check running platform if sys.platform != "win32": raise Exception("unsupported OS") appdata_ = os.path.join(os.getenv("HOMEDRIVE"), os.getenv("HOMEPATH"), "AppData") query_ = "select title,url,last_visit_time from urls" if pf == platforms.firefox: dir_ = os.path.join(appdata_, "Roaming/Mozilla/Firefox/Profiles") prof_ = [x for x in os.listdir(dir_) if x.endswith(".default")] if len(prof_) > 0: path_ = os.path.join(dir_, prof_[0], "places.sqlite") else: path_ = "" query_ = "select title,url,last_visit_date from moz_places" else: # chrome as default path_ = os.path.join( appdata_, "Local/Google/Chrome/User Data/Default/History") if not os.path.isfile(path_): raise Exception("history file was not found (pf:{})".format(pf)) return (path_, query_) def extract_to_csv(pf="", cache="data/history_repl"): csv_dst = "browse_urls.csv" import shutil p_, q_ = resolve_conf(pf) os.makedirs(os.path.dirname(cache), exist_ok=True) shutil.copy(src=p_, dst=cache) from sqlalchemy import create_engine df = pd.read_sql_query(q_, create_engine("sqlite:///{}".format(cache))) df = df.dropna(subset=['title']) df.to_csv(csv_dst, encoding='utf-8-sig') def show_csv(days=10, csv_="browse_urls.csv"): if not os.path.isfile(csv_): raise Exception("specified CSV file was not found") df = pd.read_csv(csv_, encoding='utf-8-sig', index_col=0) from datetime import datetime, timedelta if 'last_visit_date' in df.columns: time_col = 'last_visit_date' ts_ = (datetime.now() - timedelta(days=days)).timestamp() else: time_col = 'last_visit_time' offset_ = datetime(1601, 1, 1) ts_ = datetime.utcnow() - timedelta(days=days) - offset_ ts_ = ts_.total_seconds() # get newer row than specified days count res = df[df[time_col] > ts_ * 1000000] res.to_csv("recent.csv", encoding='utf-8-sig') # print for convenience for i_, row_ in res.sort_values(by=[time_col]).iterrows(): # skip unexpected if row_[time_col] is None: continue # convert to seconds sec_ = row_[time_col] / 1000000 if time_col == 'last_visit_date': ts_ = datetime.fromtimestamp(sec_) else: # parse chromium style timestamp JP: UTC+9 ts_ = offset_ + timedelta(hours=9, seconds=sec_) print("{} ({})".format(row_['title'], ts_)) if __name__ == "__main__": try: if len(sys.argv) == 2: if sys.argv[1].lower() in ["extract", "e"]: extract_to_csv() elif sys.argv[1].lower() in ["show", "s"]: show_csv() else: raise Exception("unexpected argument") elif len(sys.argv) == 3: if sys.argv[1].lower() in ["extract", "e"]: extract_to_csv(pf=sys.argv[2]) elif sys.argv[1].lower() in ["show", "s"]: show_csv(days=int(sys.argv[2])) else: raise Exception("unexpected argument") else: raise Exception("invalid argument(s)") except BaseException: print_exc() |
おわり。