구글에서는 개발자에게 재무보고서를 제공하고 있습니다.

 

'예상판매실적 보고서', '수익 보고서', '대한민국 Play 잔액 차감 보고서'를 제공하는데, 이 중 실제 회계처리는 수익 보고서를 이용해야합니다.

 

그런데 수익 보고서 CSV파일을 분석해보니 조금 귀찮은 문제(타임존 처리, 필드 포맷 등)가 있습니다.

이에 간단히 프로그램을 만들었습니다.

 

프로그램은 한국 개발자에게 익숙한 java로 만들었습니다.

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.StringUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * 구글 재무보고서 중에서 '수익'보고서 CSV파일을 읽어서 파싱
 *  - 구글 '재무보고서 수익'에 대한 설명: https://support.google.com/googleplay/android-developer/answer/6135870#zippy=%2C%EC%88%98%EC%9D%B5
 *  - apache commons-csv를 이용해서 CSV파일을 파싱( https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0 )
 *
 * @author eomsh
 */
@Slf4j
public class EarningsReportFileParser {

	private static String targetFilePath = "파싱할 CSV파일 경로"; //파싱할 CSV파일

	private static DateTimeFormatter parseFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //CSV 일시 파싱을 위한 포맷(예. Sep 1, 2021 12:00:20 AM PST)

	//private static ZoneId kstZoneId = ZoneId.of("Asia/Seoul");
	private static ZoneId pstZoneId = ZoneId.of("America/Los_Angeles"); //PST는 ZoneId가 'America/Los_Angeles' 임
	private static ZoneId pdtZoneId = ZoneId.of("GMT-07:00"); //PDT는 ZoneId가 'GMT-07:00' 임.

	private static Map<String, Integer> csvHeadMap = null; //CSV 헤더 맵

	/**
	 * 구글 개발자콘솔에서 다운로드한 재무보고서-수익 CSV파일을 파싱하는 프로그램
	 *
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {

		LocalDateTime start = LocalDateTime.now();
		log.info("\n\n==== Start: {} ====", start);

		List<CSVRecord> records = getCsvRecords(new File(targetFilePath)); //파싱하여 데이터 records를 얻음

		log.info("CSV 데이터 records 수: {}", records.size());
		//log.debug("records 0번째 데이터:{}", records.get(0));

		for (CSVRecord record : records) {

			//Transaction 일시를 유닉스타임스탬프로 변환(CSV문서의 데이터는 PDT, PST 등의 일치하지 않는 타임존 정보로 되어 있기에 내부 처리의 용이함을 위해 유닉스타임스탬프로 변환
			getUnixTs(record.get("Transaction Date"), record.get("Transaction Time"));
		}

		LocalDateTime end = LocalDateTime.now();
		log.info("\n\n==== End: {} ==== 소요시간: {}(second)", end, Duration.between(start, end).getSeconds()); //로컬 PC에서 50만건 대략 10초 소요됨

	}

	/**
	 * 수익 CSV파일을 파싱하여 List데이를 얻음
	 *
	 * @param targetFile
	 * @return
	 * @throws IOException
	 */
	private static List<CSVRecord> getCsvRecords(File targetFile) throws IOException {

		//구글 개발자콘솔의 재무보고서 기능을 통해서 내려받은 '수익.csv파일'

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			
            //구글 수익보고서를 분석하여 필요에 맞게 적절하게 CSV포맷 지정
			//@formatter:off
			CSVFormat csvFormat = CSVFormat.EXCEL.builder() //기본 엑셀타입 포맷
				.setHeader().setSkipHeaderRecord(true) //헤더 포함&skip
				.setQuote('"') //쌍따옴표 escape처리
				.setNullString("") //empty는 null처리
				.build();
			//@formatter:on
			CSVParser parser = csvFormat.parse(bufferedReader); //파서 처리
            
			//CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			csvHeadMap = parser.getHeaderMap();

			// @formatter:off
			log.debug("\n"
					+ "CSV 데이터 records 수: {}\n"
					+ "CSV 헤더 정보\n\t"
						+ "CSV헤더 필드수: {}\n\t"
						+ "헤더 필드리스트: {}\n"
					+ "{}번째 row 데이터 정보\n\t"
						+ "데이터 필드수: {}\n\t"
						+ "데이터: {}\n",
						records.size(),
						parser.getHeaderMap().size(),
						parser.getHeaderMap(),
						sampleDataRow,
						records.get(sampleDataRow).size(),
						records.get(sampleDataRow)
			);

			// @formatter:on

			return records;
		}
	}

	/**
	 * transaction 일시 정보를 유닉스타임스탬프로 변환하여 리턴
	 *
	 * @param transactionDate CSV의 Transaction Date 필드의 값
	 * @param transactionTime CSV의 Transaction Time 필드의 값
	 * @return
	 */
	private static long getUnixTs(String transactionDate, String transactionTime) {

		String timeField = transactionTime;

		String timeArray[] = StringUtils.split(transactionTime, ":");
		if (timeArray[0].length() != 2) { //1:01:30 AM PDT 와 같이 시간필드에 0이 누락된 경우는 0을 붙여줘서 파싱 포맷에 맞게 변환
			timeField = "0" + timeField;
		}

		final String targetStr = transactionDate + " " + timeField; // 예) Sep 1, 2021 12:00:20 AM PDT

		if (StringUtils.contains(transactionTime, "PST")) {

			LocalDateTime targetDT = LocalDateTime.parse(targetStr, parseFMT); //파싱
			ZonedDateTime pstZDT = targetDT.atZone(pstZoneId);
			LocalDateTime pstDT = pstZDT.withZoneSameInstant(pstZoneId).toLocalDateTime();

			return pstDT.toEpochSecond(pstZoneId.getRules().getOffset(pstDT));

		} else if (StringUtils.contains(transactionTime, "PDT")) {

			LocalDateTime targetDT = LocalDateTime.parse(targetStr, parseFMT); //파싱
			ZonedDateTime pdtZDT = targetDT.atZone(pdtZoneId);
			LocalDateTime pdtDT = pdtZDT.withZoneSameInstant(pdtZoneId).toLocalDateTime();

			return pdtDT.toEpochSecond(pdtZoneId.getRules().getOffset(pdtDT));
		}

		throw new IllegalStateException(String.format("Not supported timezone data. transactionDate:'%s' | transactionTime:'%s'", transactionDate, transactionTime));
	}
}

+ Recent posts