Open1

python - google

antyuntyunantyuntyun

GoogleDrive/ スプレッドシートを処理するクラスを書く時のメモ

setup

    def __init__(self,
                 jsonf_path: str,
                 gdrive_service_account_credential: str,
                 ):
        logger.debug('init')

        # 環境変数をクラス変数に格納

        # サービスアカウントのスコープ指定
        SCOPES = [
            'https://www.googleapis.com/auth/drive',
            'https://www.googleapis.com/auth/spreadsheets']

        # 環境変数もしくは配置されているjsonファイルからサービスアカウントを認証
        try:
            if (jsonf_path):
                logger.debug('try to get authentication by json-credential file...')
                sa_creds = service_account.Credentials.from_service_account_file(jsonf_path)
            else:
                logger.debug('try to get authentication by env...')
                tmp_path = 'tmp.json'
                with open(tmp_path, mode='w') as f:
                    f.write(gdrive_service_account_credential)
                sa_creds = service_account.Credentials.from_service_account_file(tmp_path)
                os.remove(tmp_path)
        except Exception as e:
            logger.error('Fail to get authentication')
            logger.error(e)
            exit

        logger.debug('Successs to get authentication')
        scoped_creds = sa_creds.with_scopes(SCOPES)
        self.drive_service = build('drive', 'v3', credentials=scoped_creds)
        self.sheets = build('sheets', 'v4', credentials=scoped_creds)
        self.gc = gspread.authorize(scoped_creds)

スプレッドシートをdfとして保存

    def download_spreadsheet_as_df(self, spreadsheet_id: str, sheet_name: str) -> pd.DataFrame:
        """
        GoogleDrive上のスプレッドシートをpandasのdataframeとして出力
        左上詰めで1行目がヘッダとなっていることを前提とする
        Parameters:
            spreadsheet_id: ダウンロードするスプレッドシートファイルのid
            sheet_name: ダウンロードするスプレッドシートのシート名
        Returns:
            pd.DataFrme
        """
        response = self.sheets.spreadsheets().values().get(spreadsheetId=spreadsheet_id, range=sheet_name).execute()
        logger.debug('response:')
        logger.debug(pformat(response))
        df = pd.DataFrame(response['values'][1:], columns=response['values'][0])
        return df

ファイルの移動

    def move_file(self, file_id: str, prev_parent_folder_id: str,
                  new_parent_folder_id: str) -> dict:
        print(file_id)
        print(prev_parent_folder_id)
        print(new_parent_folder_id)
        file = self.drive_service.files().update(fileId=file_id,
                                                 addParents=new_parent_folder_id,
                                                 removeParents=prev_parent_folder_id,
                                                 fields='id, parents',
                                                 supportsAllDrives=True).execute()
        return file

dfをアップロード

スプレッドシートの場合は、mimeTypeは'application/vnd.google-apps.spreadsheet'を指定

    def upload_df(self, df: pd.DataFrame, name: str, mimeType: str, parent_folder_id: str,
                  overwrite: bool = True) -> str:
        """
        pandasのdatafrmeを指定したIDのフォルダ以下にアップロードする
        Parameters:
            df: アップロードするpd.DataFrame
            name: アップロードした時のファイル名
            mimeType: GoogleDrive上でのファイルタイプ
            parent_folder_id: アップロード先の親フォルダ
            overwrite: 同一名のファイルがあった時に上書きするかどうかのフラグ
        Returns:
            str: アップロードしたファイルのID
        """
        # TODO: to_csvを挟まない処理に変更
        tmp_csv_file_path = './tmp.csv'
        df.to_csv(tmp_csv_file_path, index=False)

        # PATCH処理になるかどうかの条件判定
        patch_flag = False
        # 指定フォルダ内に同一名ファイルが存在するか確認
        if (self.exsits_in_folder(name, parent_folder_id)):
            logger.debug('file already exists')
            if (not overwrite):
                logger.debug('do not overwrite.')
                return ''
            else:
                # 上書きする場合はidを取得
                logger.debug('will be overwritten')
                exists_file_id = self.get_id_by_name_in_folder(name, parent_folder_id)
                patch_flag = True

        # TODO: 指定されたparent_folder_idがフォルダとして実在するかの確認

        file_metadata = {
            'name': name,
            'parents': [parent_folder_id],
            'mimeType': mimeType,
        }
        media = MediaFileUpload(
            tmp_csv_file_path,
            mimetype='text/csv',
            resumable=True
        )
        # 上書きかどうかで利用API変更
        if not patch_flag:
            response = self.drive_service.files().create(
                body=file_metadata, media_body=media, fields='id', supportsAllDrives=True
            ).execute()
        else:
            # update()だとファイルが上手く上書きされないので、patch処理ではなく 削除 -> ファイルアップロードに変更
            # # https://developers.google.com/drive/api/v2/reference/files/update
            # file_metadata = {
            #     'name': name,
            # }
            # response = self.drive_service.files().update(
            #     fileId=exists_file_id,
            #     addParents=parent_folder_id,
            #     body=file_metadata,
            # ).execute()

            response = self.drive_service.files().delete(
                fileId=exists_file_id, supportsAllDrives=True
            ).execute()
            response = self.drive_service.files().create(
                body=file_metadata, media_body=media, fields='id', supportsAllDrives=True
            ).execute()

        os.remove(tmp_csv_file_path)

        return response['id']

csvファイルをDLしdfとして読み込み

    def download_csv_as_df(self, file_id: str) -> pd.DataFrame:
        """
        GoogleDrive上のcsvをpandasのdataframeとしてDL
        Parameters:
            file_id: ダウンロードするcsvファイルのid
        Returns:
            pd.DataFrme
        """
        # TODO: csv判定の追加
        request = self.drive_service.files().get_media(fileId=file_id)
        fh = io.BytesIO()
        downloader = MediaIoBaseDownload(fh, request)
        done = False
        while done is False:
            status, done = downloader.next_chunk()

        return pd.read_csv(io.StringIO(fh.getvalue().decode()))

フォルダの作成

    def create_folder(self, folder_name: str, parent_folder_id: str) -> str:
        """
        GoogleDrive上に指定の名前でフォルダを作成し、作成されたフォルダのIDを返す
        folder_name: 作成するフォルダ名
        parend_folder_id: 作成するフォルダの親フォルダのID
        """

        # 既にフォルダが存在する場合には,既存フォルダのidを返す
        if (self.exsits_in_folder(folder_name, parent_folder_id)):
            logger.debug('folder already exists')
            return self.get_id_by_name_in_folder(folder_name, parent_folder_id)

        # https://developers.google.com/drive/api/v3/reference/files/create
        file_metadata = {
            'name': folder_name,
            'mimeType': 'application/vnd.google-apps.folder',
            'parents': [parent_folder_id],
        }
        response = self.drive_service.files().create(body=file_metadata,
                                                     supportsAllDrives=True,
                                                     fields='id').execute()

        return response['id']

ファイル名検索によるファイル有無判定

    def exsits_in_folder(self, search_name: str, parent_folder_id: str) -> bool:
        """
        GoogleDrive上の特定フォルダ内にファイル・フォルダが存在するかを名前検索
        Parameters:
            search_name: 検索名
            parent_folder_id: 検索対象の親フォルダ
        Returns:
            bool
        """
        check_result = False

        # https://developers.google.com/resources/api-libraries/documentation/drive/v3/python/latest/drive_v3.files.html#list
        # https://developers.google.com/drive/api/v3/reference/query-ref
        condition_list = [
            f'parents in "{parent_folder_id}"',
            'trashed = false',
        ]
        conditions = ' and '.join(condition_list)

        response = self.drive_service.files().list(
            supportsAllDrives=True,
            includeItemsFromAllDrives=True,
            q=conditions,
            fields="nextPageToken, files(id, name, mimeType, fullFileExtension)").execute()

        for file in response.get('files', []):
            if file.get('name') == search_name:
                check_result = True
                break

        return check_result

フォルダ内からファイル名で検索しidを取得

    def get_id_by_name_in_folder(self, search_name: str, parent_folder_id: str) -> str:
        """
        GoogleDrive上の特定フォルダ内に特定名のファイル・フォルダが存在するかを検索し、存在する場合idを返す。
        存在しない場合は空文字を返す。
        Parameters:
            search_name: 検索名
            parent_folder_id: 検索対象の親フォルダ
        Returns:
            str: 検索対象のID
        """
        check_result = False

        # https://developers.google.com/resources/api-libraries/documentation/drive/v3/python/latest/drive_v3.files.html#list
        # https://developers.google.com/drive/api/v3/reference/query-ref
        condition_list = [
            f'parents in "{parent_folder_id}"',
            'trashed = false',
        ]
        conditions = ' and '.join(condition_list)
        try:
            response = self.drive_service.files().list(
                supportsAllDrives=True,
                includeItemsFromAllDrives=True,
                q=conditions,
                fields="nextPageToken, files(id, name, mimeType, fullFileExtension)").execute()

            for file in response.get('files', []):
                if file.get('name') == search_name:
                    check_result = file.get('id')
                    break
        except BaseException:
            # FileNotFoundのとき404を受け取りgoogleapiclient.errors.HttpErrorがraiseされる
            logger.debug('NotFound')

        return check_result