【使い回せるプログラム】インターフェースを意識してGoogle Analytics（UA）からデータ取得する

# やりたいこと
インターフェースを重視して、Google Analyticsからデータを取得するPythonプログラムをつくりました。
個人でデータを取りたい場合はPythonを書いて運用すれば良いのですが、会社内で行う際はPythonに明るい人しかメンテナンスできなかったり、昨今ノーコードツール等を使う方も多いです。
なので、**「Pythonはわからないけど、とりあえずプログラムを実行しちゃえば、勝手にデータが取れる！」** と、パーツとして使えるようなプログラムを作成しました。
記事の最後に、ノーコードETLツールであるDataSpiderを使ってどのように実行できるのかを紹介しておきます。

# 事前準備
### ライブラリのインストール
Google AnalyticsのAPIを含め、4つのライブラリを使います。

```console
pip install --upgrade google-api-python-client
pip install --upgrade oauth2client 
pip install python-dateutil 
pip install pandas
```

### 認証キーの準備
認証キーとは、Google Analyticsからデータを取得するためのjsonファイルです。
Google 公式のページからダウンロード可能です。また、サービスアカウントの設定等が必要ですが、本記事では割愛します。

https://developers.google.com/analytics/devguides/reporting/data/v1/quickstart-client-libraries

中身はこんな感じです。

```auth.json
{
  "type": "service_account",
  "project_id": "aaaaaaaaaaaaaaaaaaa",
  "private_key_id": "bbbbbbbbbbbbbbbbbbbbbb",
  "private_key": "-----BEGIN PRIVATE KEY-----\nccccccccccccccccccccc\n-----END PRIVATE KEY-----\n",
  "client_email": "dddddddddddd@eeeeeeeeeeee.iam.gserviceaccount.com",
  "client_id": "0123456789",
  "auth_uri": "ffffffffffffff",
  "token_uri": "https://gggggg",
  "auth_provider_x509_cert_url": "https://hhhhh",
  "client_x509_cert_url": "https://iiiiiiii"
}
```

### 設定ファイル(config.ini)の作成
**テキストエディタなどで新しく作成します。**
認証キー（json）のパスや、VIEW_ID、ログファイルの書き込み先を指定するファイルです。
「config.ini」などという名前で作成ください。

```config.ini
[DEFAULT]
SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"]
KEY_FILE_LOCATION = C:\Users\ga\json\auth.json
VIEW_ID = 123456789
PAGE_SIZE = 100000

[LOG]
PATH = C:\\logs\\ga_project
FILENAME = ga_project
```

# Pythonプログラム
下記の2つのPythonファイルから構成していますが、実行するのは「get_GA_data_main.py」だけでOKです。

* get_GA_data_main.py
* get_GAdata.py

 **ポイントは、設定ファイル(config.ini)のパスやデータの取得期間をユーザー側が指定できるようにしている点です。**
過去1週間分取得するようにしていたけれど、サーバーの障害などでデータの抜け漏れが発生することは往々に起こります。
そのときに、過去2週間分取得するシーンを考えると、特にデータの取得期間といったパラメータをユーザーが簡単に変更できることが大切です。

```get_GA_data_main.py
import get_GAdata
from datetime import datetime, date
import sys
import argparse

parser = argparse.ArgumentParser(description='This script gets GA data and insert data. ')
parser.add_argument('-configfile_GA_path', help = "Set path including config file name e.g. C:/Users/hogehoge/config.ini ", required = True)
parser.add_argument('-startdate', help = "Set startdate to insert into database as hyphen dilimiter yyyy-mm-dd e.g. 2021-01-01 ", required = True)
parser.add_argument('-enddate', help = "Set enddate to insert into database as hyphen dilimiter yyyy-mm-dd e.g. 2021-01-07 ", required = True)
parser.add_argument('-csv_path', help = "Set path to save csv path e.g. C:/Users/hogehoge", required = True)
parser.add_argument('-csv_prefix_name', help = "Set prefix csv name as [prefix name]yyyy-mm-dd~yyyy-mm-dd[suffix name]. ", default = "", required = False)
parser.add_argument('-csv_suffix_name', help = "Set suffix csv name as [prefix name]yyyy-mm-dd~yyyy-mm-dd[suffix name]. ", default = "", required = False)
args = parser.parse_args()

def main():
    config_GA_path = args.configfile_GA_path
    csv_path = args.csv_path
    csv_prefix_name = args.csv_prefix_name
    csv_suffix_name = args.csv_suffix_name
    
    try:
        con_GA = get_GAdata.get_GAdata(config_GA_path)
        con_GA.initialize_analyticsreporting()
    except Exception as e:
        sys.exit(f"{e}")
    
    try: 
        datetime.strptime(args.startdate, "%Y-%m-%d")
        datetime.strptime(args.enddate, "%Y-%m-%d")
    except ValueError as e:
        sys.exit(f"Error massage: {e}\nCheck parameters you set:\n  startdate: {args.startdate} \n  enddate : {args.enddate}")
    
    startdate = date(int(args.startdate.split("-")[0]), int(args.startdate.split("-")[1]), int(args.startdate.split("-")[2]))
    enddate = date(int(args.enddate.split("-")[0]), int(args.enddate.split("-")[1]), int(args.enddate.split("-")[2]))
    if(startdate > enddate):
        sys.exit(f"'startdate' must be before (or the same) 'enddate':\nCheck parameters you set:\n startdate: {args.startdate} \n  enddate : {args.enddate}")
    
    
    try: 
        con_GA.get_report(startdate=str(startdate), enddate=str(enddate))
        filename = csv_path + "/" + csv_prefix_name + str(startdate) + "~" + str(enddate) + csv_suffix_name +".csv"
        con_GA.write_csv(filename)
    except Exception as e:
        sys.exit(f"{e}")

if __name__ == "__main__":
    main()

```

```get_GAdata.py
from googleapiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
import logging
import os
import datetime
import configparser
import json
from dateutil.relativedelta import relativedelta
import copy
import pandas as pd
import traceback

config_ini = configparser.ConfigParser()

class get_GAdata:
  def __init__(self, config_ini_path):
    if not os.path.exists(config_ini_path):
      raise FileNotFoundError(config_ini_path + " is not found. ")
    config_ini.read(config_ini_path, encoding="utf-8")
    
    # ------------------------ ログ出力パラメータ ------------------------
    month = datetime.datetime.now().strftime('%m')
    formatter = '%(levelname)s : %(asctime)s : %(message)s'
    logFilePath = config_ini['LOG']['PATH'] + '\\' + config_ini['LOG']['FILENAME'] + '_' + month + '.log'
    logging.basicConfig(filename=logFilePath,level=logging.INFO,format=formatter)
    try:
      self.SCOPES = json.loads(config_ini['DEFAULT']['SCOPES'])
      self.KEY_FILE_LOCATION = config_ini['DEFAULT']['KEY_FILE_LOCATION']
      self.VIEW_ID = config_ini['DEFAULT']['VIEW_ID']
      self.PAGE_SIZE = int(config_ini['DEFAULT']['PAGE_SIZE'])
    except KeyError as e:
      # print(f"Key {e} is not found. Please check config file. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
      logging.error(f"Key {e} is not found. Please check config file. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
      raise KeyError(f"Key {e} is not found. Please check config file. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
    except ValueError as e:
      # print(f"Key PAGE_SIZE: '{config_ini['DEFAULT']['PAGE_SIZE']}' cannot be parsed integer. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
      logging.error(f"Key PAGE_SIZE: '{config_ini['DEFAULT']['PAGE_SIZE']}' cannot be parsed integer. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
      raise ValueError(f"Key PAGE_SIZE: '{config_ini['DEFAULT']['PAGE_SIZE']}' cannot be parsed integer. \nErrorMassage: {traceback.format_exception_only(type(e), e)}")
    else:
      # print("Config file is successfully opened. ")
      logging.info("Config file is successfully opened. ")

    self.yesterday = datetime.datetime.today() + relativedelta(days=-1)
    
  def initialize_analyticsreporting(self):
    try:
      credentials = ServiceAccountCredentials.from_json_keyfile_name(
          self.KEY_FILE_LOCATION, self.SCOPES)
      # Build the service object.
      self.analytics = build('analyticsreporting', 'v4', credentials=credentials)
    except Exception as e:
      logging.error(f"Error occured at function 'initialize_analyticsreporting'. \nErrorMassage: {e}")
      raise FileNotFoundError(f"Error occured at function 'initialize_analyticsreporting'. \nErrorMassage: {e}")
    else:
      logging.info(f"initialize_analyticsreporting is succeeded. ")
  
  def get_report_all_pages(self, pageToken=None, startdate='7daysAgo', enddate='today'):
    metrics = [
                  {'expression': 'ga:users'},
                  {'expression': 'ga:sessions'},
                  {'expression': 'ga:pageviews'},
                  {'expression': 'ga:timeOnPage'},
                  {'expression': 'ga:avgTimeOnPage'}
              ]
    dimensions = [
                  {'name': 'ga:date'},
                  {'name': 'ga:dateHourMinute'},
                  {'name': 'ga:pagePath'},
                  {'name': 'ga:source'},
                  {'name': 'ga:deviceCategory'},
                  {'name': 'ga:operatingSystem'},
                  {'name': 'ga:pageTitle'},
                  {'name': 'ga:pagePathLevel1'}
              ]
    self.response = self.analytics.reports().batchGet(
      body={
          'reportRequests': [{
            'viewId' : self.VIEW_ID,
            'dateRanges' : [{'startDate': startdate, 'endDate': enddate}],
            'metrics' : metrics,
            'dimensions' : dimensions, 
            'pageToken' :  pageToken,
            'pageSize' : self.PAGE_SIZE,  # Default = 1000行, 最大 100,000 行。　 Project: 1 日 50,000 件のリクエスト;  ビュー 1 日 10,000 件のリクエスト,
            'samplingLevel' : 'LARGE'
          }]
      }
    ).execute()

  def parse_data(self, raw_datas=None):
    response = self.response
    if raw_datas is None:
        raw_datas = []
    for report in response.get('reports', []):
        columnHeader = report.get('columnHeader', {})
        dimensionHeaders = columnHeader.get('dimensions', [])
        metricHeaders = columnHeader.get('metricHeader', {}).get('metricHeaderEntries', [])
        nextPageToken = report.get('nextPageToken')
        for row in report.get('data', {}).get('rows', []):
            raw_data = []
            dimensions = row.get('dimensions', [])
            dateRangeValues = row.get('metrics', [])
            for header, dimension in zip(dimensionHeaders, dimensions):
                raw_data.append(dimension)
            for i, values in enumerate(dateRangeValues):
                for metricHeader, value in zip(metricHeaders, values.get('values')):
                    raw_data.append(value)
            raw_datas.append(raw_data)
        # カラム名を作成： dimension names　& metric names
        column_names = copy.deepcopy(dimensionHeaders)
        column_names = column_names + [metric.get('name') for metric in metricHeaders]
    self.data_output = [raw_datas, column_names, nextPageToken]
  
  def get_report(self, startdate, enddate):
    pageToken = "0"
    try:
      while pageToken != None:
        self.get_report_all_pages(pageToken=pageToken, startdate=str(startdate), enddate=str(enddate))
        self.parse_data()
      
        if('ga_data' not in locals()):
          ga_data = pd.DataFrame(data=self.data_output[0], columns=[ column_name.replace(":" ,"_") for column_name in self.data_output[1] ])
        else:
          tmp = pd.DataFrame(data=self.data_output[0], columns=[ column_name.replace(":" ,"_") for column_name in self.data_output[1] ])
          ga_data = ga_data.append(tmp)
        pageToken = self.data_output[2]
      self.ga_data = ga_data
    except Exception as e:
      logging.error(f"Error occured at function 'get_report'. \nErrorMassage: {e}")
      raise Exception(f"Error occured at function 'get_report'. \nErrorMassage: {e}")
    else:
      logging.info(f"Getting GA data is succeeded. ")

  def write_csv(self, filename):
    self.ga_data.to_csv(filename, index=False)
```

※ 補足
get_GAdata.pyのmetricsとdimensionsに要素を追加して、取得するデータ項目を追加できます。

# どうやって実行する？
素直にPythonで実行する方法と、exe化して実行する方法の2つを紹介します。
exe化するメリットは、**プログラムを実行するサーバーにPythonが入っていなくても利用できること**です。
そのため、冒頭のpipでのインストールなども不要になり、デプロイが非常に簡単です。

### Pythonで実行する
必要なパラメータを指定して、CUIから実行できます。

```powershell
python .\get_GA_data_main.py -configfile_GA_path "C:/Users/hogehoge/config.ini"  -startdate 2023-03-01 -enddate 2023-03-07 -csv_path "C:/Users/hogehoge/data"
```

パラメータについて紹介します。
* configfile_GA_path・・・config.iniのファイルパスを指定します。
* startdate・・・データの取得開始期間を指定します。yyyy-mm-ddの形式で指定してください。
* enddate・・・データの取得終了期間を指定します。yyyy-mm-ddの形式で指定してください。
* -csv_path・・・csvの吐き出し先を指定します。
* -csv_prefix_name・・・【任意指定】CSVの接頭辞を指定できます。
* -csv_suffix_name・・・【任意指定】CSVの接尾辞を指定できます。

### exe化して実行する
pyInstallerというライブラリを使って、Pythonファイルをexe化できます。
（ライブラリにpandasが入っているとexe化に比較的時間がかかるので注意してください。）

```
pip install pyinstaller
pyinstaller get_GA_data_main.py --onefile
```

上記を実行すると、「build」と「dist」という２つのフォルダが作成されます。
distフォルダの直下に「get_GA_data_main.exe」が作成されています。
こちらを使って同じように実行します。

```
.\dist\get_GA_data_main.exe -configfile_GA_path "C:/Users/hogehoge/config.ini"  -startdate 2023-03-01 -enddate 2023-03-07 -csv_path "C:/Users/hogehoge/data"
```

# ノーコードツールからの実行
今回は、サンプルとしてDataSpiderというノーコードETLツールでの使い方を簡単に紹介します。

1. 「外部アプリケーション起動」をキャンバスに配置
![call.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254022/d15ffec1-001f-f905-302a-67b19770c0be.png)
1. iniファイルのパスやデータの取得期間を持つ変数を用意
![make_val.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254022/79102915-ce4b-2325-c3dd-f1c2535ffc9d.png)
1. 作成したexeファイルを実行するようにします。各引数も、変数を使いつつ設定しましょう。
![2023-03-08_17h08_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2254022/eff937ad-1267-372e-343f-e88359fb6fea.png)

上記の設定で、指定した期間のデータを指定したフォルダにCSVとして出力することが可能です。
CSV出力後は、データベースに格納するとBIツールなどを使った可視化に繋げやすいかなと思います。

# おわりに
今回は、Pythonを知らない人も使い回せるスクリプトを紹介しました。
ノーコードツールを使って、いろいろなサービスから自動でデータを取る仕組みは今後増えると思います。
ツール側がすべてのサービスと連携できるようになるのは現実的でなく、その場合PythonなどからAPIを叩いてデータを取ることになるため、本記事で紹介したようなI/F部分を意識した、パーツとして使えるプログラムを作るとより便利なのかなと思います。