[Oracle Database] Pythonを使ってAWRのHTMLレポートをCSVにする

# はじめに
AWRのHTMLレポートをCSVにしたくて、いろいろ調べてたところ、Python×BeautifulSoup×Pandasを利用することで、とても簡単にやりたいことができたので、やり方を書いておく
(なお、Python初心者なため、至らぬところ多々あると思います。ご注意ください)

# 事前準備
最初Python2.xと標準RPMで実装したところ、BeautifulSoupの実行がとても遅かったり、空白が文字化けしたりしました。Python3.xとpipで最新の3rdモジュールで試したらとてもうまくいったのでこちらを利用する

※動作環境は、OEL7.xを前提にしています。

```CONSOLE
OEL7ではpython3が標準でインストールされていないため、EPELから追加インストールします。その後、以下のパッケージを仮想環境に追加していきます
・pandas
・bs4
・lxml

$ sudo su -
# yum install -y python3
# mkdir /usr/local/python
# cd /usr/local/python
# python3 -m venv --system-site-packages mypython
# . mypython/bin/activate
(mypython) # pip install pandas
(mypython) # pip install bs4
(mypython) # pip install lxml
(mypython) # exit
```

# CSVにする方法

まず、以下のようなAWRレポートがあったとして、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456818/fb5a2031-f4f7-3aa1-2a4e-a4fad2361419.png)

このHTMLページのソースを見ると、tableタグのsummary属性にテーブルの説明が記載されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456818/ff74ea22-b197-5cb8-6578-9f080b69fa48.png)

まず、BeuatifulSoupを使ってHTMLページを読み込み、tableのsummary属性の値でフィルターしてtableを抽出します。抽出したtableをPandasで読み取って、DataFrameにした後にcsvに変換します。

# CSVサンプル

## とりあえず、Load Profile TableをCSVにしてみる

エラーハンドリングとか考慮せずに単純に書くとこれだけでできました。

```CONSOLE
# vi test.py
```
```PYTHON
#!/usr/local/python/mypython/bin/python3
# -*- coding: utf-8 -*-

import pandas
from bs4 import BeautifulSoup
import csv
import sys

args=sys.argv

input_file = open(args[1])
soup = BeautifulSoup(input_file,'lxml')

section_table=soup.find('table', {'summary' : 'This table displays load profile'})
section_df = pandas.read_html(str(section_table),header=0)
print(section_df[0].to_csv(index=False,encoding='utf-8',header=True,quoting=csv.QUOTE_NONNUMERIC))
input_file.close()
```
```CONSOLE
# chmod 755 test.py
# ./test.py awr_report_sample01.html
"Unnamed: 0","Per Second","Per Transaction","Per Exec","Per Call"
"DB Time(s):",0.0,0.1,0.0,0.01
"DB CPU(s):",0.0,0.0,0.0,0.01
"Background CPU(s):",0.0,0.1,0.0,0.0
"Redo size (bytes):",3632.8,19178.5,"",""
"Logical read (blocks):",527.8,2786.4,"",""
…
"Executes (SQL):",49.6,261.8,"",""
"Rollbacks:",0.0,0.0,"",""
"Transactions:",0.2,"","",""
```
## ほしい出力がだせるようにする

実際には、複数ファイルを読み込んだり、複数セクションを指定したり、ファイルに書き込んだりしたいので、こんな感じにしたらうまくいった気がする。

※すべてのバージョンでどうかとか、すべてのセクションがうまく取得できるかとか、文字コードの違いでどうかとか、ためしてません。

```PYTHON
#!/usr/local/python/mypython/bin/python3
# -*- coding: utf-8 -*-

import pandas
from bs4 import BeautifulSoup
import csv
import argparse
import re

# * セクションを追加したい場合はここに追記する
section_summaries= [
    'This table displays load profile',
    'This table displays instance efficiency percentages',
    'This table displays top 10 wait events by total wait time',
    'This table displays wait class statistics ordered by total wait time',
    'This table displays system load statistics',
    'This table displays CPU usage and wait statistics',
    'This table displays IO profile',
    'This table displays memory statistics',
    'This table displays different time model statistics. For each statistic, time and % of DB time are displayed',
    'This table displays operating systems statistics. For each statistic, actual value is displayed. If the end value is different, it is also displayed',
    ]

if __name__ == '__main__':
  # 引数の処理
    parser = argparse.ArgumentParser(description = 'HTML AWR Report to CSV')
    parser.add_argument("reports",nargs='*',help="AWR Report file path. You can specifi * to select multiple files.")
    args = parser.parse_args()

    dbinfo_section = 'This table displays database instance information'
    snapinfo_section = 'This table displays snapshot information'

  # 引数で指定されたファイルを一つずつ処理する
    for i, awrreport in enumerate(args.reports):
        try:
            input_file= open(awrreport)

          # BeautifulSoupを使用してHTMLファイルを読み込む
            soup = BeautifulSoup(input_file,'lxml')

          # DB名とインスタンス名を取得して変数に代入
          # 同名セクション(This table displays pdb instance information)が2つあるため、find_allを利用
            dbinfo_table=soup.find_all('table', {'summary' : dbinfo_section})
            if not isinstance(dbinfo_table,type(None)):
               # DB名を取得
                 dbinfo_df = pandas.read_html(str(dbinfo_table[0]))
                 db_name = str(dbinfo_df[0].iat[0,0])
               # インスタンス名を取得
                 instinfo_df = pandas.read_html(str(dbinfo_table[1]))
                 instance_name = str(instinfo_df[0].iat[0,0])

          # BEGIN SNAPとEND SNAPを取得
            snapinfo_table=soup.find_all('table', {'summary' : snapinfo_section})
            if not isinstance(snapinfo_table,type(None)):
                 snapinfo_df = pandas.read_html(str(snapinfo_table))
                 
                 begin_snap =  str(snapinfo_df[0].iat[0,2])
                 end_snap =  str(snapinfo_df[0].iat[1,2])

          # CSVに出力する際、ヘッダを付与するかどうかを決める(初回のみヘッダを付与)
            if i == 0:
                display_header = True
            else:
                display_header = False

            for j, section_summary in enumerate(section_summaries):

              # summary属性を検査してテーブルを抽出
                section_table=soup.find('table', {'summary' : section_summary})

              # Outputファイルの名前を指定 (section_summaryの値を加工して命名)
                out_file_path=section_summary.replace('This table displays ','').replace(' ','_')
                out_file_path=str(j).zfill(2) + '.' + re.search(r'^[^.]+',out_file_path).group() + '.csv'

              # Pandasを利用して、HTMLからDataFrameを作成
                if not isinstance(section_table,type(None)):
                    section_df = pandas.read_html(str(section_table),header=0)
                  # 列名が空白の場合、'Unnamed: 0'という名称に変換されるので、'UNNAMED_COLUMN'に変換
                    section_df[0].rename(columns = {'Unnamed: 0': 'UNNAMED_COLUMN'}, inplace=True)
                  # 抽出したDataFrameに、DB名・インスタンス名・SNAPSHOT取得時間を追加
                    section_df_new = section_df[0].assign(DB_NAME=db_name,INSTNACE_NAME=instance_name,BEGIN_SNAP=begin_snap,END_SNAP=end_snap)

                    try:
                  # CSVにしてファイルに出力
                        out_file = open(out_file_path,mode='a')
                        out_file.write(section_df_new.to_csv(index=False,encoding='utf-8',header=display_header,quoting=csv.QUOTE_NONNUMERIC))
                    finally:
                        out_file.close()
                else:
                    print('section_table('+ section_summary + ') Not found.')
        finally:
            input_file.close()
```

```CONSOLE
# ll
total 3008
-rw-rw-r--. 1 opc  opc  1010438 May 15 20:52 awr-sample01.html
-rw-rw-r--. 1 opc  opc  1001585 May 15 20:52 awr-sample02.html
-rw-rw-r--. 1 opc  opc  1054981 May 15 20:52 awr-sample03.html
-rwxr-xr-x. 1 root root    4598 May 15 20:47 test.py
# ./test.py awr-sample*.html
# ll *.csv
-rw-r--r--. 1 root root 6999 May 15 20:55 00.load_profile.csv
-rw-r--r--. 1 root root 1660 May 15 20:55 01.instance_efficiency_percentages.csv
-rw-r--r--. 1 root root 3543 May 15 20:55 02.top_10_wait_events_by_total_wait_time.csv
-rw-r--r--. 1 root root 3283 May 15 20:55 03.wait_class_statistics_ordered_by_total_wait_time.csv
-rw-r--r--. 1 root root  423 May 15 20:55 04.system_load_statistics.csv
-rw-r--r--. 1 root root  328 May 15 20:55 05.CPU_usage_and_wait_statistics.csv
-rw-r--r--. 1 root root 3050 May 15 20:55 06.IO_profile.csv
-rw-r--r--. 1 root root 1170 May 15 20:55 07.memory_statistics.csv
-rw-r--r--. 1 root root 4533 May 15 20:55 08.different_time_model_statistics.csv
-rw-r--r--. 1 root root 5557 May 15 20:55 09.operating_systems_statistics.csv

# cat 05.CPU_usage_and_wait_statistics.csv
"%Total CPU","%Busy CPU","%DB time waiting for CPU (Resource Manager)","DB_NAME","INSTNACE_NAME","BEGIN_SNAP","END_SNAP"
0.0,1.7,0.0,"ORCL","orcl1","15-May-20 19:00:22","15-May-20 20:00:33"
0.0,1.5,0.0,"ORCL","orcl1","15-May-20 20:00:33","15-May-20 21:00:45"
0.0,1.7,0.0,"ORCL","orcl1","15-May-20 21:00:45","15-May-20 22:00:04"

```