332 lines
16 KiB
Python
332 lines
16 KiB
Python
import csv
|
||
import io
|
||
import os
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
import boto3
|
||
import pymysql
|
||
from common import (DIRECTORY_SETTINGS, DIRECTORY_WORK, ERROR, INFO,
|
||
LINE_FEED_CODE, SETTINGS_ITEM, WARNING, convert_quotechar,
|
||
debug_log, uncompress_gzip, uncompress_zip)
|
||
from end import end
|
||
from error import error
|
||
from pymysql.constants import CLIENT
|
||
|
||
# クラス変数
|
||
s3_client = boto3.client('s3')
|
||
|
||
|
||
# チェック例外クラス
|
||
class CheckError(Exception):
|
||
pass
|
||
|
||
|
||
def check(bucket_name, target_data_source, target_file_name, settings_key, db_info, log_info, mode):
|
||
"""チェック処理
|
||
Args:
|
||
bucket_name : バケット名
|
||
target_data_source : 投入データのディレクトリ名よりデータソースに該当する部分
|
||
target_file_name : 投入データのファイル名
|
||
settings_key : 投入データに該当する個別設定ファイルのフルパス
|
||
db_info : データベース情報
|
||
log_info : ログに記載するデータソース名とファイル名
|
||
mode : 処理モード
|
||
Raises:
|
||
CheckError : チェックでエラーがあった場合に発生する例外
|
||
"""
|
||
|
||
try:
|
||
debug_log(f'引数 bucket_name : {bucket_name}', log_info, mode)
|
||
debug_log(
|
||
f'引数 target_data_source : {target_data_source}', log_info, mode)
|
||
debug_log(f'引数 target_file_name : {target_file_name}', log_info, mode)
|
||
debug_log(f'引数 settings_key : {settings_key}', log_info, mode)
|
||
debug_log(f'引数 log_info : {log_info}', log_info, mode)
|
||
debug_log(f'引数 mode : {mode}', log_info, mode)
|
||
|
||
# ① チェック処理開始ログを出力する
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-01 - チェック処理を開始します')
|
||
|
||
# データ読込
|
||
settings_obj_response = s3_client.get_object(
|
||
Bucket=bucket_name, Key=settings_key)
|
||
settings_list = []
|
||
for line in io.TextIOWrapper(io.BytesIO(settings_obj_response["Body"].read()), encoding='utf-8'):
|
||
settings_list.append(line.rstrip('\n'))
|
||
# 設定ファイルに記載のない行を空文字として扱い、予約行とする
|
||
for _ in range(len(SETTINGS_ITEM) - len(settings_list)):
|
||
settings_list.append('')
|
||
|
||
work_key = target_data_source + DIRECTORY_WORK + target_file_name
|
||
work_obj_response = s3_client.get_object(
|
||
Bucket=bucket_name, Key=work_key)
|
||
work_obj_file = work_obj_response["Body"].read()
|
||
work_data_bytes = io.BytesIO(work_obj_file)
|
||
compressed_flag = settings_list[SETTINGS_ITEM["compressedFlag"]]
|
||
|
||
# ② ファイル圧縮フラグがONの場合、チェック対象ファイルの展開処理を行う。
|
||
if compressed_flag and compressed_flag == '1':
|
||
work_data_bytes = io.BytesIO(uncompress_file(
|
||
work_data_bytes, settings_list, log_info))
|
||
|
||
encoding = settings_list[SETTINGS_ITEM["charCode"]]
|
||
line_feed = LINE_FEED_CODE[settings_list[SETTINGS_ITEM["lineFeedCode"]]]
|
||
delimiter = settings_list[SETTINGS_ITEM["delimiter"]]
|
||
|
||
work_data = io.TextIOWrapper(work_data_bytes, encoding=encoding,
|
||
newline=line_feed)
|
||
work_csv_row = []
|
||
for i, line in enumerate(csv.reader(work_data, quotechar=convert_quotechar(settings_list[SETTINGS_ITEM["quotechar"]]),
|
||
delimiter=delimiter)):
|
||
# ヘッダあり、かつ、1行目の場合
|
||
if int(settings_list[SETTINGS_ITEM["headerFlag"]]) == 1 and i == 0:
|
||
work_csv_row.append(line)
|
||
continue
|
||
work_csv_row.append(line)
|
||
break
|
||
|
||
# ③ C-0のデータ件数チェックを開始する
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-02 - C-0のチェックを開始します')
|
||
if is_empty_file(work_csv_row, settings_list):
|
||
# 拡張SQL実行フラグがONになっている場合は拡張SQLを実行して処理終了する。
|
||
if settings_list[SETTINGS_ITEM["executeExSqlIfFileEmptyFlag"]] == '1':
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-15 - '
|
||
'投入ファイルが0バイトです。0バイト時の拡張SQL実行フラグが有効なため、拡張SQLを実行します。')
|
||
execute_ex_sql(bucket_name, target_data_source,
|
||
settings_list, db_info, log_info)
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-03 - 投入ファイルが0バイトのため処理を終了します')
|
||
end(bucket_name, target_data_source,
|
||
target_file_name, '', log_info, mode)
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-04 - 終了処理完了')
|
||
sys.exit()
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-05 - C-0:正常終了')
|
||
|
||
# ④ C-1の項目数チェックを開始する
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-06 - C-1 項目数チェックを開始します')
|
||
work_csv_row_item_len = len(work_csv_row[0])
|
||
if work_csv_row_item_len == int(settings_list[SETTINGS_ITEM["csvNumItems"]]):
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-07 - C-1 項目数チェック:正常終了')
|
||
else:
|
||
raise CheckError(
|
||
f'E-CHK-01 - 項目数が一致しません 個別設定ファイル項目数:{settings_list[SETTINGS_ITEM["csvNumItems"]]} 投入データ項目数:{work_csv_row_item_len}')
|
||
|
||
# ⑤ C-2の項目並び順チェック開始する
|
||
if int(settings_list[SETTINGS_ITEM["headerFlag"]]) == 1:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-08 - C-2 項目並び順チェックを開始します')
|
||
settings_header_list = settings_list[SETTINGS_ITEM["csvNameItems"]].rstrip(
|
||
).split(',')
|
||
work_header_list = work_csv_row[0]
|
||
for i in range(len(settings_header_list)):
|
||
if not settings_header_list[i] == work_header_list[i]:
|
||
raise CheckError(
|
||
f'E-CHK-02 - 項目順序が一致しません {i + 1}番目の項目 個別設定ファイル項目:{settings_header_list[i]} 投入データ項目:{work_header_list[i]}')
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-09 - C-2 項目並び順チェック:正常終了')
|
||
|
||
# ⑥ C-3の末尾行項目数チェック開始する
|
||
if settings_list[SETTINGS_ITEM["bulkImportFlag"]] and int(settings_list[SETTINGS_ITEM["bulkImportFlag"]]) == 1:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-13 - C-3 末尾行項目数チェックを開始します')
|
||
|
||
# ファイルの末尾行を取得し、ファイル項目数と比較する
|
||
last_line_work_data_bytes = next(reverse_readline_stream(
|
||
work_data_bytes, LINE_FEED_CODE[settings_list[SETTINGS_ITEM["lineFeedCode"]]]))
|
||
# 区切り文字(例えばカンマ)を文字とデリミタで区別できるように、csvリーダーで読む
|
||
last_line_separated_data = [
|
||
row for row in csv.reader(
|
||
io.TextIOWrapper(io.BytesIO(last_line_work_data_bytes)),
|
||
quotechar=convert_quotechar(
|
||
settings_list[SETTINGS_ITEM["quotechar"]]),
|
||
delimiter=delimiter)
|
||
]
|
||
last_line_count = len(last_line_separated_data[0])
|
||
if last_line_count != int(settings_list[SETTINGS_ITEM["csvNumItems"]]):
|
||
raise CheckError(
|
||
f'E-CHK-03 - 投入データ末尾の項目数が一致しません 個別設定ファイル項目数:{settings_list[SETTINGS_ITEM["csvNumItems"]]} 投入データ項目数:{last_line_count}')
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-14 - C-3 末尾行項目数チェック 正常終了')
|
||
|
||
# ⑦ チェック処理終了ログを出力する
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-10 - チェック処理を終了します')
|
||
except CheckError as e:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {ERROR} {e}')
|
||
error(bucket_name, target_data_source, target_file_name, log_info)
|
||
|
||
except Exception as e:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {ERROR} E-CHK-99 - エラー内容:{e}')
|
||
error(bucket_name, target_data_source, target_file_name, log_info)
|
||
|
||
|
||
def is_empty_file(work_csv_row: list, settings_list: list):
|
||
"""② C-0のデータ件数チェック
|
||
ヘッダ行がある場合は、1行目を読み飛ばして判定する
|
||
|
||
Args:
|
||
work_csv_row (list): CSVファイルの1行目(ヘッダを含む場合は2行目まで)
|
||
settings_list (list): 個別設定ファイルのリスト
|
||
|
||
Returns:
|
||
bool: CSVファイルの1行目が0件だった場合はTrue
|
||
"""
|
||
has_header = int(settings_list[SETTINGS_ITEM["headerFlag"]]) == 1
|
||
# ヘッダのみのファイルも0バイトファイルをみなす
|
||
if has_header:
|
||
return len(work_csv_row[1:]) == 0
|
||
|
||
return len(work_csv_row) == 0
|
||
|
||
|
||
def uncompress_file(work_data_file: io.BytesIO, settings_list: list, log_info) -> bytes:
|
||
"""指定された形式で圧縮ファイルを展開し、ローカルに書き出す。
|
||
Args:
|
||
work_data_file (bytes): S3から読み込んだ登録
|
||
settings_list (list): 個別設定ファイルのリスト
|
||
log_info (str): ログに記載するデータソース名とファイル名
|
||
|
||
Returns:
|
||
bytes: 解凍後のファイルのバイト配列
|
||
"""
|
||
compression = settings_list[SETTINGS_ITEM['compression']]
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-11 - 投入ファイルが圧縮されているため、展開処理を行います。圧縮形式:{compression}')
|
||
|
||
file_bytes: bytes = None
|
||
if compression == 'zip':
|
||
# zipファイルを展開し、ファイルを書き出す。
|
||
file_bytes = uncompress_zip(work_data_file, settings_list, log_info)
|
||
elif compression == 'gzip':
|
||
file_bytes = uncompress_gzip(work_data_file, settings_list, log_info)
|
||
|
||
# MEMO: zip以外の圧縮形式に対応する際に追記すること
|
||
else:
|
||
raise ValueError("圧縮形式が一致しません")
|
||
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-12 - 投入ファイルの展開が正常終了しました')
|
||
return file_bytes
|
||
|
||
|
||
def reverse_readline_stream(f: io.BytesIO, line_feed: str, chunk_size=4096):
|
||
"""バイトモードのファイルストリームを後ろから読み、1行ずつ返すジェネレータ。
|
||
指定されたバイト単位でファイルを読み込み、1行ずつにして返している。
|
||
Args:
|
||
f (io.BytesIO): バイトモードのファイルストリーム
|
||
line_feed (str): 改行コード
|
||
chunk_size (int, optional): 読み込むファイルチャンク数(byte) Defaults to 4096.
|
||
|
||
Yields:
|
||
bytes: 末尾から1行分のレコード
|
||
"""
|
||
# ファイルのポインタを末尾に移動させてからポインタを取得する
|
||
f.seek(0, os.SEEK_END)
|
||
pointer = f.tell()
|
||
buffer = b''
|
||
yielded_any = False
|
||
|
||
while pointer > 0:
|
||
read_size = min(chunk_size, pointer)
|
||
# チャンク数分ファイルを読み込むために、ポインタをずらす
|
||
pointer -= read_size
|
||
f.seek(pointer)
|
||
chunk = f.read(read_size)
|
||
|
||
# 読み込んだチャンク + バッファ(途中行)
|
||
buffer = chunk + buffer
|
||
|
||
# 改行文字で区切る
|
||
parts = buffer.split(line_feed.encode())
|
||
# parts[0] は先頭途中行 → 次ループへ残す
|
||
buffer = parts[0]
|
||
# parts[1:] が「完全に揃った行」のリスト
|
||
# 改行文字で区切っているため、行途中のparts[1:] は空文字になるため、以下のループは素通りする。
|
||
for line in reversed(parts[1:]):
|
||
if not yielded_any and line == b'':
|
||
# 最初にだけ出てくる空行を飛ばす
|
||
continue
|
||
|
||
# 一度でも結果を返している場合にフラグを立てる
|
||
yielded_any = True
|
||
yield line
|
||
|
||
# ポインターがファイル先頭まで来たあとの残り1行
|
||
if not (not yielded_any and buffer == b''):
|
||
# 一度でも結果を返しているかつ、バッファにデータが残っている場合、先頭行のデータを返す
|
||
yield buffer
|
||
|
||
|
||
def execute_ex_sql(bucket_name, target_data_source, settings_list, db_info, log_info):
|
||
# 個別設定ファイルに拡張SQLファイル名が設定されているかチェック
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-16 - 拡張SQL設定が存在するかチェックします')
|
||
ex_sql_file_name = settings_list[SETTINGS_ITEM["exSqlFileName"]]
|
||
|
||
if ex_sql_file_name:
|
||
try:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-17 - 拡張SQL設定の存在を確認しました')
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-18 - 拡張SQLファイル名:{ex_sql_file_name} の存在チェック')
|
||
ex_sql_key = target_data_source + DIRECTORY_SETTINGS + ex_sql_file_name
|
||
s3_client.head_object(Bucket=bucket_name, Key=ex_sql_key)
|
||
ex_sql_file_exists = True
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-19 - 拡張SQLファイル名の存在を確認しました')
|
||
except Exception:
|
||
ex_sql_file_exists = False
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {WARNING} W-CHK-02 - 拡張SQLファイルが存在しません')
|
||
|
||
try:
|
||
if ex_sql_file_exists:
|
||
# 拡張SQLファイルからSQL文生成
|
||
ex_sql_obj_response = s3_client.get_object(
|
||
Bucket=bucket_name, Key=ex_sql_key)
|
||
ex_sql = ''
|
||
for line in io.TextIOWrapper(io.BytesIO(ex_sql_obj_response["Body"].read()), encoding='utf-8'):
|
||
ex_sql = f'{ex_sql} {line.rstrip()}'
|
||
|
||
# DB接続を開始する
|
||
conn = pymysql.connect(host=db_info["host"], port=db_info["port"], user=db_info["user"], passwd=db_info["pass"],
|
||
db=db_info["name"], connect_timeout=5, client_flag=CLIENT.MULTI_STATEMENTS, local_infile=True)
|
||
# トランザクション開始
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-20 - 拡張SQL:{ex_sql_file_name} のトランザクションを開始します')
|
||
with conn.cursor() as cur:
|
||
cur.execute(ex_sql)
|
||
conn.commit()
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-21 - 拡張SQL:{ex_sql_file_name} のCOMMIT処理が正常終了しました')
|
||
conn.close()
|
||
except Exception as e:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {WARNING} W-CHK-03 - 拡張SQLにエラーが発生しました:{e}')
|
||
else:
|
||
print(
|
||
f'{datetime.now():%Y-%m-%d %H:%M:%S} {log_info} {INFO} I-CHK-22 - 拡張SQL設定の存在はありませんでした')
|
||
|
||
|
||
# ローカル実行用コード
|
||
# 値はよしなに変えてください
|
||
if __name__ == '__main__':
|
||
check(
|
||
bucket_name='test-shimoda-bucket',
|
||
target_data_source='test',
|
||
target_file_name='compress_test.gz',
|
||
settings_key='test/settings/compress_test_gzip.txt',
|
||
log_info='Info',
|
||
mode='i'
|
||
)
|