2024년 대학 도서관 근로 당시, 시스템 데이터와 실제 서가의 지정도서 리스트를 대조하는 작업을 맡았다. 안그래도 업무가 많은 학기 초인데, 수천 권의 데이터를 일일이 클릭하며 비교하는 것이 너무 비효율적으로 느껴졌다. 파이썬 스크립트를 짜서 돌리면 금방 끝날 일이었기 때문이다. 결국 사서 선생님께 조심스레 허가를 받은 뒤 파이썬으로 스크립트를 짜서 해결했다.

1. 제작 동기

도서관 시스템 상의 ‘지정도서 신청 목록’과 실제 서가에 배치된 ‘지정도서 리스트’가 일치하는지 확인하여 결과 엑셀 파일을 만드는 작업이 필요했다. 서명이나 청구기호가 미세하게 다르거나, 중복 신청된 경우를 수작업으로 찾아내는 데 드는 시간을 줄이고 정확도를 높이기 위해 제작했다.

2. 데이터 정제 및 매칭 규칙

서명(도서 제목) 정제 규칙

단순히 텍스트가 일치하는지 보는 게 아니라, 비교를 방해하는 요소들을 모두 제거했다.

  • 특수문자 및 공백 제거: !@#$%^&*()_-+={[}]|\\:;"'<,>.?/『』·「」와 모든 공백( \t\n)을 삭제한다.
  • 목적: “데이터 구조”와 “『데이터구조』” 또는 “데이터구조 ” 등을 동일한 도서로 인식하기 위함이다.

청구기호 정제 규칙

청구기호 뒤에 붙는 부가 정보를 쳐내고 핵심 기호만 남겨 대조했다.

  • 구분자 기준 절삭: 청구기호에 = 또는 c.(복본 표시 등)이 포함되어 있다면, 해당 문자 앞부분까지만 남기고 나머지는 버린다.
  • 공백 정리: 양 끝의 불필요한 공백을 제거하여 순수한 기호 값만 추출한다.

3. 데이터 대조 흐름 (알리미 로직)

프로그램은 다음의 순서로 서가 데이터와 시스템 데이터를 비교하여 result.csv 파일과 사람의 확인을 필요로 하는 notUsed.csv파일을 생성한다.

  1. 서명 매칭: 정제된 서명을 기준으로 시스템에 해당 도서가 있는지 확인한다. 존재하지 않으면 error_notInSys로 분류한다.
  2. 청구기호 매칭: 서명이 일치하는 데이터 중, 정제된 청구기호까지 일치하는지 확인한다. 일치하지 않으면 error_callNum으로 분류한다.
  3. 신청일 관리: 매칭된 도서의 신청일들을 수집하여 가장 빠른 날짜를 최초 신청일로, 나머지를 이외 신청일로 정리한다.
  4. 중복 체크: 동일한 도서에 대해 시스템 기록이 여러 개 존재할 경우 error_duplicate 비고를 남긴다.
  5. 미사용 데이터 추출: 시스템에는 등록되어 있으나 실제 서가 데이터에는 매칭되지 않은 항목들을 별도의 파일(notUsed)로 뽑아낸다.

4. 코드 전문

import csv  
  
def create_replica():  
    fl, lib = open_to_read("지정도서_서가")  
    fnl, newLib = open_to_write("newLib")  
    newLib.writerow(["No","서명", "청구기호", "등록번호"])  
  
    for row in lib:  
        newLib.writerow(row)  
  
    fs, syst = open_to_read("지정도서_시스템")  
    fns, newSyst = open_to_write("newSyst")  
    newSyst.writerow(["No","서명", "청구기호", "신청일"])  
  
    for row in syst:  
        row[1] = preprocess_name(row[1])  
        newSyst.writerow(row)  
  
    fs.close()  
    fl.close()  
    fnl.close()  
    fns.close()  
  
def create_result():  
    # necessary file open  
    fl, lib = open_to_read("newLib")  
    fs, syst = open_to_read("newSyst")  
    fr, resultFile = open_to_write("result")  
    resultFile.writerow(["NO",  "서명", "청구기호", "등록번호", "최초 신청일", "이외 신청일", "비고"])  
    fn, notUsed = open_to_write("notUsed")  
    usedList = [0]*1929  
  
    for rowL in lib:   
        # match book name        name = preprocess_name(rowL[1])  
        nameMatchingRows = []  
        for rowS in syst:  
            if name == rowS[1]:  
                nameMatchingRows.append(rowS)  
        fs.seek(0)  
  
        # check not in system error  
        if len(nameMatchingRows) == 0:  
            resultFile.writerow(rowL + ["", "", "error_notInSys"])  
            continue  
  
        # match call number  
        callNum = preprocess_call_num(rowL[2])  
        callNumMatchingRows = []  
  
        for rowN in nameMatchingRows:  
  
            if callNum == preprocess_call_num(rowN[2].strip()):  
                callNumMatchingRows.append(rowN)  
  
        # check call number error  
        if len(callNumMatchingRows) == 0:  
            resultFile.writerow(rowL + ["", "", "error_callNum"])  
            continue  
  
        # check application date  
        applicationList = []  
  
        for rowC in callNumMatchingRows:  
            applicationList.append(rowC[3])  
            usedList[int(rowC[0])-1] = 1  
  
        # remove duplication  
        applicationSet = sorted(set(applicationList))  
        initialApply = applicationSet[0]  
        otherApply = "/".join(applicationSet[1:])  
  
        # check duplication error  
        if len(applicationSet) < len(applicationList):  
            resultFile.writerow(rowL + [initialApply, otherApply, "error_duplicate"])  
        else:  
            resultFile.writerow(rowL + [initialApply, otherApply, ""])  
        fs.seek(0)  
  
    fo, oldSyst = open_to_read("지정도서_시스템")  
  
  
    for i, rowO in enumerate(oldSyst):  
        if usedList[i] == 0:  
            notUsed.writerow(rowO)  
  
    fl.close()  
    fn.close()  
    fr.close()  
    fs.close()  
    fo.close()  
  
def preprocess_name(name):  
    newName = ""  
    for c in name:  
        if c not in "!@#$%^&*()_-+={[}]|\\:;\"\'<,>.?/『』·「」 \t\n":  
            newName += c  
    name = newName  
    return name  
  
def preprocess_call_num(callnum):  
    idxE = callnum.find("=")  
    idxC = callnum.find("c.")  
  
    if idxE == -1 and idxC == -1:  
        return callnum.strip()  
    if idxE == -1:  
        return callnum[:idxC].strip()  
    if idxC == -1:  
        return callnum[:idxE].strip()  
  
    return callnum[:min(idxC, idxE) + 1].strip()  
  
  
def open_to_read(fileName):  
    f = open(f'static/{fileName}.csv', 'r', encoding='cp949')  
    r_file = csv.reader(f)  
    next(r_file)  
  
    return f, r_file  
  
def open_to_write(fileName):  
    f = open(f'static/{fileName}.csv', 'w', newline='')  
    w_file = csv.writer(f)  
    return f, w_file  
  
if __name__ == '__main__':  
    create_replica()  
    create_result()

4. 결과 및 성과

  • 시간 단축: 수작업으로 며칠이 걸릴 분량을 코드 작성과 검토를 포함해 몇 시간 만에 끝냈다.
  • 정확도 향상: 사람이 눈으로 볼 때 놓치기 쉬운 오타나 중복 신청 건을 로직을 통해 완벽하게 걸러냈다.
  • 업무 효율: 코드로 1차 필터링을 거친 뒤, 오류(error) 메시지가 뜬 항목들만 집중적으로 확인하여 전체 검토 시간을 획기적으로 줄였다.

5. 마치며

데이터 정제의 핵심은 ‘비교 가능한 상태’로 만드는 전처리 작업임을 배웠다. 단순 반복 업무를 코딩으로 해결했을 때의 쾌감과, 이를 허가해주신 사서 선생님 덕분에 간단하지만 실제 실무에 기술을 적용해본 값진 경험이었다.