LoginSignup
2
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-08

やりたいこと

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

事前準備

ライブラリのインストール

Google AnalyticsのAPIを含め、4つのライブラリを使います。

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

認証キーの準備

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

中身はこんな感じです。

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から実行できます。

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」という2つのフォルダが作成されます。
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
  2. iniファイルのパスやデータの取得期間を持つ変数を用意
    make_val.png
  3. 作成したexeファイルを実行するようにします。各引数も、変数を使いつつ設定しましょう。
    2023-03-08_17h08_03.png

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

おわりに

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1