[영수증 정리 프로그램] PDF 파일 관리 + cpp string 파싱 + 엑셀을 이용한 정리 프로그램 개발



이번에 영수증을 정리해야할 일이 생겼다. 영수증에 있는 내용 중 이름, 영수증 번호, 발급일자, 금액 등을 뽑아서 표에 정리해주면 되는 일인데, 영수증이 한두개면 그냥 일일이 직접 쳐서 넣으면 되지만 이게 1000개가 넘어가고 중복되는 자료가 매우 많아지게 되면서 거쳐야 할 작업이 배로 늘어나게 됐다. 따라서 지금까지 공부한 걸 써먹게 되었다.

일단 작업 방식은 아래와 같다.

  1. PDF 또는 hwp 자료가 다수 존재하므로 일단 하나의 파일로 병합해준다. (이를 위해선 파일의 형식을 하나로 통일해주는 것이 좋다고 생각한다.)
  2. PDF 또는 hwp로 되어있는 형식의 자료를 가져온다.
  3. 이 자료에서 영수증 번호, 이름, 기부내용, 후원금액, 발급일자에 대한 정보를 가져온다.
  4. 가져온 자료를 표에 정리해서 모아준다.
일단 3번은 보자마자 C++로 string 파싱 해주면 금방 끝나겠다고 감이 왔는데, 문제는 1번과 2번의 작업이다.

1번 작업의 경우 : PDF를 하나의 파일에 모아서 여러 페이지로 나눠주면 파일을 딱 한번만 열어줄 수 있으니 병합해주는 방법을 찾아야한다.

2번 작업의 경우 : 일반 txt 파일이나 csv 파일은  C++에서 불러와서 구분자에 따라, 혹은 줄에 따라서 파싱이 가능하지만 hwp나 pdf 같은 파일은 좀 더 특별한 메타 데이터를 가지고 있어서 열어도 이상하게 나올거라고 생각했다. 즉 PDF나 hwp를 txt나 csv 같은 적합한 형식으로 바꿔줘서 열어줄 수 있다면 해결할 수 있겠다고 판단했다. 

그렇다면 이 과정들을 어떤 방식으로 처리해줘야할까? 구글링을 통해 python에 아주 적합한 라이브러리가 있다는 것을 찾았다. 
python의 PyPDF2를 통해 특정 폴더 경로 안의 PDF자료를 전부 모아서 한 파일의 여러 페이지로 바꿔줄 수 있다. 이를 위해선 hwp 파일을 전부 PDF 파일 형식으로 바꿔줘야 할 것이다. 
또한 pdftotext를 이용해서 PDF의 데이터를 전부 텍스트로 바꿔서 저장할 수 있다. 이 때 영수증이 표의 형식으로 되어있어서 과연 표에 있는 내용들이 어떤 방식으로 저장되는 지는 감이 안왔지만, 한 번 변환하고 확인해보기로 했다.

일단 PyPDF2를 이용한 병합 코드는 아래와 같다.



병합 코드

from PyPDF2 import PdfFileMerger, PdfFileReader
import os

receiptdir = '/Users/singiyeol/Desktop/베다니/receiptMOD'
pdfs = os.listdir(receiptdir)

pdfs = [file for file in os.listdir(receiptdir) if file.endswith(".pdf")]

for pdff in pdfs :
    print(pdff)

merger = PdfFileMerger()

for pdf in pdfs :
    merger.append(PdfFileReader(pdf), 'rb')

with open("testoutput.pdf", "wb") as fout:
    merger.write(fout)


일단 해당 소스 코드가 있는 경로에 영수증 PDF 파일을 전부 넣어놓는다.
PyPDF2에서 PdfFileMerger와 PdfFileReader를 불러와준다.
해당 경로에 있는 모든 파일을 os.listdir로 찾아주고 그 중 .pdf 로 끝나는 파일만 pdfs 리스트에 저장해 준다.
pdfs에 있는 데이터들을 전부 돌면서 PdfFileReader로 pdf를 가져와 merger에 전부 병합해준다.
병합한 모든 데이터들을 testoutput.pdf에 다시 출력해준다.


병합 과정이 끝났다면 testoutput.pdf에 영수증 PDF 파일이 여러 페이지에 나뉘어서 저장되어 있을 것이다. 필자의 경우 1300 페이지 정도의 영수증 파일이 나왔다. 이제 이 파일을 전부 텍스트로 바꿔줘야한다. 코드는 아래와 같다.



변환 코드

import pdftotext

with open("testoutput.pdf", "rb") as f :
    pdf = pdftotext.PDF(f)


with open('test.txt', 'w') as f :
    f.write(" ".join(pdf))


매우 간단하다. pdftotext 라이브러리를 불러와서 원하는 PDF파일을 열고, 그 파일을 pdftotext로 변환해서 pdf 변수에 저장해주고, test.txt에 저장해주기만 하면 된다.



이 과정을 통하면 놀랍게도 표에 있던 모든 데이터가 텍스트로 저장된다! 개인정보 때문에 결과물을 보여줄 수는 없지만 표의 여백 및 행렬 간격을 줄바꿈과 여백으로 표현되어있었다. 즉 C++에서 사용하기에 무리가 없다는 얘기다.

이제 C++에서 파싱을 해줘야한다. 코드는 일단 아래와 같다.




소스코드

//
//  main.cpp
//  receipt
//
//  Created by 신기열 on 19/07/2019.
//  Copyright © 2019 신기열. All rights reserved.
//

#include <iostream>
#include <fstream>
#include <vector>

using namespace std;

int main(int argc, const char * argv[]) {

    string str_buf;
    fstream fs;
    ofstream output("output.txt");
    
    fs.open("/Users/singiyeol/Desktop/베다니/receiptMOD/test.txt", ios::in);
    
    int i = 0;
    bool Isword = false;
    vector temp(5);
    
    while(!fs.eof()){
        
        getline(fs,str_buf,'\n');
        cout << str_buf << '\n';
        
        str_buf.erase(std::remove(str_buf.begin(), str_buf.end(), ' '), str_buf.end());
 
        if(str_buf.find("베다니기") != string::npos){
            temp[0] = str_buf;
            i++;
        }
        
        if(str_buf.find("1.기부자") != string::npos){
            
            getline(fs,str_buf,'\n');
            str_buf.erase(std::remove(str_buf.begin(), str_buf.end(), ' '), str_buf.end());
            
            string temp1= str_buf;
            
            getline(fs,str_buf,'\n');
            str_buf.erase(std::remove(str_buf.begin(), str_buf.end(), ' '), str_buf.end());
            
            string temp2 = str_buf;
            
            getline(fs,str_buf,'\n');
            str_buf.erase(std::remove(str_buf.begin(), str_buf.end(), ' '), str_buf.end());
            
            string temp3 = str_buf;
            
            temp[1] = temp1 + temp2 + temp3;
            i++;
        }
        
        if(str_buf.find("지정기부금") != string::npos && Isword == false){
            temp[2] = "지정기부금";
            Isword = true;
            i++;
        }
        
        if((str_buf.find("합계") != string::npos)){
            temp[3] = str_buf;
            i++;
        }
        else if((str_buf[0] - 48 >= 1 && str_buf[0] - 48 <= 9) && (str_buf.find(',') != string::npos) && (str_buf.find("원") != string::npos)){
            if(str_buf.find("년") == string::npos && str_buf.find("월") == string::npos && str_buf.find("일") == string::npos){
                temp[3] = str_buf;
                i++;
            }
        }
        
        if(str_buf[0] - 48 == 2){
            if((str_buf.find("년") != string::npos)){
                if(str_buf.find("월") != string::npos){
                    if(str_buf.find("일") != string::npos){
                        if(str_buf.find("지정기부금") == string::npos && str_buf.find(",") == string::npos){
                            temp[4] = str_buf;
                            i++;
                        }
                    }
                }
           }
        }
        
        
        
        if(i == 5){
            for(int j = 0; j < 5; j++){
                if(j <= 3) output << temp[j] << ", ";
                else if(j == 4) output << temp[j] << '\n';
            }
            //temp.clear();
            i = 0; Isword = false;
            while(str_buf.find("일련번호") == string::npos && !fs.eof()){
                getline(fs,str_buf,'\n');
            }
        }
    }
    
    output.close();
    fs.close();
    
    return 0;
}


참고로 여기서 들 수 있는 의문은, 왜 파이썬 내에서 다 작성하지 않고 이렇게 언어를 따로 쓰는가하면, 파이썬을 제대로 써 본적이 없어서 잘 모른다.. 그냥 라이브러리를 받와서 그 기능을 쓰는 건 알지만 문법 사용하는 것도 다시 봐야되는데, 솔직히 귀찮아서 그냥 평소에 쓰는 걸로 했다. 어쨌든 위의 코드를 분석해보자.

일단 텍스트로 잘 변환은 됐지만 표에 있는 내용을 가져와서 그런지 각 영수증 정보에 대한 테이블이 정확하게 일치하지 않는다. 요컨데 어떤 내용은 한줄에 다 들어있는데, 다른 영수증 정보에선 몇줄에 걸쳐서 들어있기도 했다. 즉 원하는 정보만 뽑아오기 위해선 모든 공통된 케이스를 조사해서 뽑아올 수 있게 만들어줘야 한다.


  1. 일단 영수증 번호는 다행히도 같은 위치에 같은 형식으로 항상 존재한다. "베다니"가 포함되어 있으면 무조건 영수증 번호다. 따라서 각 줄을 돌면서 "베다니"가 포함되어 있으면 영수증번호 string 벡터에 넣도록 하자.
  2. 기부자 이름 명단이 가장 골치 아픈데, 이름은 각각 다르니 특정 이름을 뽑아올 수는 없고, 특정 문장을 뽑아와야 하는데, 일단 줄 번호로는 뽑아올 수 없다. (영수증 테이블마다 줄 개수도 전부 다르기 때문에) 그렇다면 이 문장에 포함된 특정 단어를 찾아야하는데, 이마저도 골치아픈게 공통적으로 포함된 내용도 전부 다르다. 다만 딱 하나 공통적인 부분이, "1.기부자"라는 문장 밑의 3~4줄 정도에는 무조건 이름이 포함되어있다. 즉 그 줄을 전부 가져와서 넣어주면 일단은 이름이 들어가있을 것이다.
  3. 기부 유형은 전부 지정기부금이다. 그냥 지정기부금을 넣어주자.
  4. 금액 문장에는 "합계"라고 적혀있는 경우도 있고, 없는 경우도 있으며, 날짜가 포함된 금액 정보는 다른 경우이므로 "년", "월", "일"이 포함되어있지 않는 것만 뽑아준다.
  5. 날짜는 ","와 "지정기부금"이라는 정보가 없어야만 발급날짜이다. 이 경우도 따로 체크해서 뽑아준다.
  6. 각 케이스마다 index에 대한 정보 (i)를 1씩 증가시켜서 모든 리스트가 다 뽑혔으면 출력하고자 하는 파일에 한줄씩 출력하고 index를 리셋해준다. 그리고 "일련번호"라는 문장이 나올 때까지 전부 스킵해준다. (이게 영수증의 첫 시작임)
  7. 이 과정을 파일이 끝날 때까지 반복해준다.

이 케이스들을 전부 나눠주는게 좀 골치아프지만, 어쨌든 이 내용을 전부 끝냈으면 이제 엑셀에서 복사 + 붙여넣기를 해주자.
이 때, 엑셀을 잘 사용하지 않아서 왜 그런진 모르지만 텍스트에서 가져오기 같은 걸 쓰면 데이터가 너무 많아서 가져올 수 없단다. (그래봤자 1000행 정도 밖에 안되는데..) 그래서 그냥 복사 + 붙여넣기를 해서 "텍스트 나누기"기능으로 나눠줬다.


그리고 이제 필요없는 데이터 내용들을 다 지워줘야 한다. 나의 경우 ","나 "주소", "주민등록번호", "합계" 등의 텍스트는 전부 지워줘야했다. 
홈 - 찾기 및 선택 - 바꾸기를 눌러준다.


맨 끝에 돋보기 같은게 있다. 바꾸기를 눌러서 찾을 내용에 지우고 싶은 내용을 넣고, 바꿀 내용엔 어차피 지워줄 것이므로 그냥 공백으로 놔두면 전부 삭제 된다. 이 때 특정 셀만 적용하고 싶으면


이렇게 해당 셀 맨 위 (B라고 적힌 부분)을 눌러서 바꾸기를 해주면 B에 해당한 열만 적용이 된다.

영수증 번호 순으로 정렬하기 위해선 데이터 - 정렬 - 바꾸고자 하는 열 선택 & 텍스트 오름차순 정렬을 이용해주었다.




후기

이렇게 해서 모든 작업이 끝냈다. 아마 일일이 타이핑을 했으면 며칠이 걸렸을텐데 다행히도 몇시간만에 모든 작업을 처리할 수 있었다. 이제 다시 게임 만들기 프로젝트로 넘어갈 수 있겠다. 참고로 소스코드는 아래에 있다.

소스코드 : https://github.com/betterafter/Project_receiptManager

댓글