ApacheのアクセスログをPythonのモジュールで読み込む

# まとめ

- ``Python``のモジュールで、ログ解析できるものはないか調べてみたら、次のブログがあった
  - [Apache logをpaserする - そこはかとなく書くよん](http://tdoc.info/blog/2013/12/12/apache_log.html) 
- このブログでは、2種類のparser（``apache-log-parser``と``apachelog``）が紹介されている
- 両方試してみた結果、``apache-log-parser``を使うことにした


## ``apachelog``がダメだったところ

- [GitHub : apachelog](https://github.com/nickmoorman/apachelog)
- ``import``したらエラーがでた
- ``Error Exception``の書き方が``Python3``に対応してないみたい
  - https://github.com/nickmoorman/apachelog/issues/1
- ソースコードをちょちょいと書き直せば使えそうだけれど、そもそもメンテされてないみたいなので様子見

## ``apache-log-parser``がよかったところ

- [GitHub : apache-log-parser](https://github.com/rory/apache-log-parser)
- 上のブログには「ドキュメントがない」と書いてあったが、いまのGitHubページを確認すると、READMEがきちんとある
- 書いてあるとおりにやれば実行できたので、こっちを使うことにした


# やったこと

1. モジュールのインストール
2. ログフォーマットの確認
3. パースできるかテスト


## モジュールをインストールした

```bash
$ pip3 install apache-log-parser
```

## サーバのログフォーマットを確認した

- ログフォーマットの設定ファイルを探す

```bash
$ locate httpd.conf
/etc/httpd/conf/httpd.conf
```
- ファイルを開き``logformat``などでで単語検索する
- アクセスログは``combined``フォーマットで記録されていた
- 書式が何を指しているかは [Apache公式ドキュメント - mod_log_config - カスタムログ書式](https://httpd.apache.org/docs/2.4/ja/mod/mod_log_config.html#formats)を参照
  - ``\"`` は ``" (quote)`` をエスケープしてる

```conf
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
```

## 1行をパースできるかテストした

- [apache-log-parser](https://github.com/rory/apache-log-parser)のREADMEに従う
- ``apache_log_parser.make_parser``の引数を、自分の使っているログフォーマットに変更する
- ``test_data``には実際のログの1行をコピペする

```pyhon:apache_log_parser
import apache_log_parser
from pprint import pprint

line_parser = apache_log_parser.make_parser("%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"")
test_data ='xxx.xxx.xxx.xxx - - [18/Feb/2019:23:58:36 +0900] "GET /ja/index.html HTTP/1.1" 301 240 "-" "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"'
log_line_data = line_parser(test_data)
pprint(log_line_data)
```

```python:pprint出力
{'remote_host': 'xxx.xxx.xxx.xxx',
 'remote_logname': '-',
 'remote_user': '-',
 'request_first_line': 'GET /ja/index.html HTTP/1.1',
 'request_header_referer': '-',
 'request_header_user_agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT '
                              '6.1; Trident/6.0)',
 'request_header_user_agent__browser__family': 'IE',
 'request_header_user_agent__browser__version_string': '10.0',
 'request_header_user_agent__is_mobile': False,
 'request_header_user_agent__os__family': 'Windows',
 'request_header_user_agent__os__version_string': '7',
 'request_http_ver': '1.1',
 'request_method': 'GET',
 'request_url': '/ja/index.html',
 'request_url_fragment': '',
 'request_url_hostname': None,
 'request_url_netloc': '',
 'request_url_password': None,
 'request_url_path': '/ja/index.html',
 'request_url_port': None,
 'request_url_query': '',
 'request_url_query_dict': {},
 'request_url_query_list': [],
 'request_url_query_simple_dict': {},
 'request_url_scheme': '',
 'request_url_username': None,
 'response_bytes_clf': '240',
 'status': '301',
 'time_received': '[18/Feb/2019:23:58:36 +0900]',
 'time_received_datetimeobj': datetime.datetime(2019, 2, 18, 23, 58, 36),
 'time_received_isoformat': '2019-02-18T23:58:36',
 'time_received_tz_datetimeobj': datetime.datetime(2019, 2, 18, 23, 58, 36, tzinfo='0900'),
 'time_received_tz_isoformat': '2019-02-18T23:58:36+09:00',
 'time_received_utc_datetimeobj': datetime.datetime(2019, 2, 18, 14, 58, 36, tzinfo='0000'),
 'time_received_utc_isoformat': '2019-02-18T14:58:36+00:00'}
```

- パースした後は辞書型のオブジェクトになっていた
- キーが分かりやすい名前になっている（``apachelog``より読みやすい）
- タイムスタンプは、様々な``datetime``オブジェクトに変換されている

## キーと書式の対応を調べた

| 書式 | キー |
|---|---|
| ``%h`` | ``remote_host`` |
| ``%l`` | ``remote_logname`` |
| ``%u`` | ``remote_user`` |
| ``%t`` | ``time_received`` |
| ``%r`` | ``request_first_line`` |
| ``%s`` | ``status`` |
| ``%b`` | ``response_bytes_clf`` |
| ``%{Referer}i`` | ``request_header_referer`` |
| ``%{User-Agent}i`` | ``request_header_user_agent`` |


## ファイルをパースできるようにした

- ファイルからログを読み込むための関数を作った（``read_apache_log``）
- 引数：ファイル名（``ifn``）とログフォーマット（``logformat``）にした
- パースに成功した値（辞書型）はリスト``P``に追加（``append``）した
- ``ValueError``になった行はリスト``E``に追加（``append``）した
- パースした結果のサマリを表示することにした
  - ``成功``に対して``失敗``の数が少なければ、アクセス解析の結果に問題はないだろうという考え
- 返り値：パースに成功した値のリスト``P``


```python:モジュールを読み込む
import apache_log_parser
from pprint import pprint
```

```python:ログを読む関数を作った
def read_apache_log(ifn, logformat='%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'):
    parser = apache_log_parser.make_parser(logformat)
    P = []
    E = []
    with open(ifn) as f:
        for line in f:
            try:
                parsed_line = parser(line)
                P.append(parsed_line)
            except ValueError:
                E.append(line)

    pprint('=== Read Summary ===')
    pprint('Parsed     : {0}'.format(len(P)))
    pprint('ValueError : {0}'.format(len(E)))
    pprint('====================')

    return P
```

```python:実行した結果
ifn = 'access_log'  ## 100行のログファイル
read_apache_log(ifn)

# === Read Summary ===
# Parsed     : 100
# ValueError : 0
# ====================
```

## CSVファイルに保存した

- ``pandas``を使ってCSVファイルに保存した
- ``pandas.DataFrame``に、パースしたログのリストを突っ込んだ
- ``pandas.DataFrame.to_csv``を使って出力した

```python
import pandas as pd
ifn = 'access_log'
P = read_apache_log(ifn)
df = pd.DataFrame(P)
df.to_csv('output.csv')
```

# さらにやりたいこと

- ``Apache``のログが簡単にパースできることがわかったので、アクセス解析・集計をやってみる
- ``request_header_user_agent__is_mobile``を集めれば、モバイルによるアクセス状況が分かる
- ``remote_host``ごとに時系列（``time_recieved``など）で並べれば、トラッキング情報も再構築できる？