Excel & IT Info

아이엑셀러 닷컴, 엑셀러TV

Python

비정형 PDF 텍스트 추출하는 방법

권현욱(엑셀러) 2024. 5. 18. 18:40
반응형

들어가기 전에

비정형(unstructured)이라는 것은 구조화되어 있지 않다는 뜻입니다. 한 페이지에 표가 있고, 다른 페이지에는 두 개의 표가 있거나 아예 없을 수도 있습니다. 혹은 이 페이지는 단일 열 레이아웃인데, 저 페이지는 2열, 또 저 페이지는 3열 레이아웃인 PDF를 떠올려 보세요. 이런 구조화되지 않은 PDF 파일에서 콘텐츠를 추출하는 방법을 소개합니다.

권현욱(엑셀러) | 아이엑셀러 닷컴 대표 · Microsoft Excel MVP · Excel 솔루션 프로바이더 · 작가

(이미지: 아이엑셀러 닷컴)

 

※ 이 글은 아래 기사 내용을 토대로 작성되었습니다만, 필자의 개인 의견이나 추가 자료들이 다수 포함되어 있습니다.


  • 원문: Unstructured PDF Text Extraction
  • URL: https://medium.com/@khadijamahanga/unstructured-pdf-text-extraction-3a20db14791e

텍스트와 표 추출하기

텍스트와 표를 모두 체계적으로 추출하기 위해 pdfplumber 패키지 기능을 사용하여 페이지에서 표를 식별하고 추출했습니다. 그런 다음 pdfminer 함수를 사용하여 페이지에서 식별된 표의 경계 내에 있는 요소를 제외한 텍스트 요소를 순차적으로 추출했습니다. 출력 문자열을 인쇄할 때는 읽기 흐름과 표의 위치를 보존하도록 했습니다.

 

(이미지: medium) PDF 파일의 샘플 2열 레이아웃 페이지와 텍스트 파일의 출력 문자열에 대한 스크린샷

 

패키지 설치에 대해 자세히 설명하지 않고 바로 함수로 들어가 보겠습니다. 함수의 초기 단계에서는 각 패키지의 객체를 인스턴스화하여 단일 PDF 파일을 포괄적으로 처리했습니다. 그 후, 아래 코드에서 볼 수 있듯이 pdfplumber의 find_tables 함수를 사용하여 페이지에 존재하는 테이블 객체를 우선적으로 식별하면서 각 페이지를 반복했습니다.

def pdf_process(path):
    plumberObj = pdfplumber.open(path)
    minerPages = extract_pages(path)
    fitzDoc = fitz.open(path)

    ...
    for i in enumerate(minerPages):
        tables = plumberPage.find_tables(table_settings={"text_vertical_ttb": False})
        page_text = miner_extract_page(page_layout, tables)

 

표 개체를 확보한 후에는 pdfminer로 옮겼습니다. pdfminer는 표 추출에 완벽하지는 않지만 PDF의 다양한 페이지 레이아웃을 처리하는 데 탁월합니다. 그리고 제 샘플 PDF 파일은 페이지 전체에 걸쳐 레이아웃이 일관되지 않았습니다. 따라서 각 페이지 레이아웃에 대해 도우미 검사 함수인 is_obj_in_bbox를 사용하여 해당 페이지의 모든 테이블 객체를 잘라내도록 했습니다.

def is_obj_in_bbox(obj, _bbox, page_height):
    """
    checks if an element boundary box is within another boundary
    """
    objx0, y0, objx1, y1 = obj
    x0, top, x1, bottom = _bbox
    return (objx0 >= x0) and (objx1 <= x1) and (page_height - y1 >= top) and (page_height - y0 <= bottom)

 

여기에서 확인 기능에 page_height를 사용하고 있음을 알 수 있으며 이는 pdfplumber 패키지와 pdfminer 패키지 간의 경계 치수가 다르기 때문입니다. 이 두 패키지 간의 bbox 반전에 대한 자세한 내용은 [여기]를 참고하세요.

 

이제 miner_extract_page 함수를 살펴보겠습니다.

def miner_extract_page(page_layout, tables):
    """
    this function extract texts, tables, images from a single page layout
    Paremeter:
      page_layout -> A pdfminer page object
      tables -> An array of pdfplumber table objects
    Returns:
      
    """
    page_height = page_layout.height
    extractedTables = []
    page_output_str = ""
    
    for element in page_layout:
        if isinstance(element, LTTextContainer):
            tabBox = []
            # if current element exists in any of the tables, 
            # append the t to tabBox
            for t in tables:
                is_obj_n_box = is_obj_in_bbox(element.bbox, t.bbox, page_height)
                if is_obj_n_box:
                    tabBox.append(t)

            # if tabBox is empty, extract the element with get_text() function
            if not len(tabBox):
                if isinstance(element, LTTextContainer):
                    elementText = element.get_text()
                    page_output_str += elementText
                else:
                    # check for figures layout at this point
                    # check for part 2 when I talk about images/figures

            # else, element exist in a certain table
            # therefore, we extract the found table 
            # using pdfplumber table extract function and 
            # concatenate to our end results
            else:
                if not tabBox[0] in extractedTables:
                    table_str = tabulate(tabBox[0].extract(**({"vertical_ttb": False})), tablefmt="grid")
                    page_output_str += table_str
                    page_output_str += "\n"
                    # to avoid repetition we used extractedTables to 
                    # filter already extracted tables. 
                    extractedTables.append(tabBox[0])
    return page_out_str

 

위의 함수에서는 최종 문자열에 인쇄 목적으로 표 패키지를 사용하고 있습니다. 그렇지 않으면 필요하지 않습니다. LLM 모델용 PDF를 추출하는 경우 최종 출력에 더 많은 문자가 추가되므로 피하는 것이 좋습니다. 또한 테이블 추출의 출력은 2차원 배열이므로 혼동을 피하기 위해 일종의 체계적인 인쇄 방법을 사용해야 합니다.

 

이미지 추출

이 작업에는 PDF 파일에서 특정 픽토그램/이미지를 식별하는 작업도 포함되었습니다. 파일에 존재하는지 여부를 확인해야 하는 픽토그램이 약 10가지 정도 있었습니다. 제 해결책은 먼저 PDF 페이지의 모든 이미지를 추출하고 이미지 해싱 또는 기타 템플릿 비교 방법(cv2 패키지의 matchTemplate)을 사용하여 추출된 이미지가 제가 가지고 있는 픽토그램과 일치하는지 확인하는 것이었습니다.


이미지 추출에 효과적인 패키지 중 하나는 PyMuPDF(fitz)입니다. 하지만 모든 이미지가 제대로 식별되거나 추출되지 않는 문제가 발생했습니다. PDF 컴파일 과정에서 사용된 이미지 유형이 PNG나 SVG이기 때문이 아닐까 추측했지만 확실하게 확인할 수는 없었습니다.


이미지 추출 프로세스를 위해 check_for_image라는 함수는 다음과 같이 구현했습니다.

def check_for_image(pdf_path):
    """
    Function that get images from pdf pages and save them to local drive

    Parameters:
    pdf_path -> Path of the PDF file
    """

    pdf_document = fitz.open(pdf_path)
    xreflist = []

    page_num = 0
    il = pdf_document.get_page_images(page_num)
    logger.info(f"Found {len(il)} images")

    for img in il:
        xref = img[0]
        if xref in xreflist:
            continue

        width = img[2]
        height = img[3]
        
        #skip tiny images
        if min(width, height) <= 5:
            continue

        imgdata = img["image"]

        imgfile = os.path.join(f"drawing-img-{xref}-{page_num + 1}.{img['ext']}")
        fout = open(imgfile, "wb")
        fout.write(imgdata)
        fout.close()
        xreflist.append(xref)
    pdf_document.close()

 

하지만 앞서 언급한 이미지 문제 때문에 일반 이미지 확인 외에 추가 기능을 구현했습니다. 이 함수는 그림을 확인하는 것이었습니다. 그림은 선부터 복잡한 도형까지 무엇이든 될 수 있으므로(자세한 내용은 get_drawings 함수에서 확인할 수 있습니다), 직사각형 크기를 일정 수만큼 확대하고, 작은 그림이나 큰 그림 안에 있는 그림에 필터를 적용하는 등 다양한 단계를 거쳐 그림이 포함된 페이지의 확대된 영역에 대한아픽스맵을 얻었습니다. 그런 다음 생성된 픽스맵을 픽토그램과 비교하여 결정을 내립니다.

 

def check_for_drawings(pdf_path):
    """
    Function that finds drawings from pdf pages and save pixmap of 
    on enlarged drawing's rectangle to local drive

    Parameters:
    pdf_path -> Path of the PDF file
    """

    pdf_document = fitz.open(pdf_path)

    for page_num in range(pdf_document.page_count):
        page = pdf_document[page_num]

        d = page.get_drawings()
        new_rects = []
        for p in d:
            # filter emplty rectangle
            if p["rect"].is_empty:
                continue
            w = p["width"]
            if w:
                r = p["rect"] + (-w, -w, w, w)  # enlarge each rectangle by width value
                for i in range(len(new_rects)):
                    if abs(r & new_rects[i]) > 0:  # touching one of the new rects?
                        new_rects[i] |= r  # enlarge it
                        break

                # now look if contained in one of the new rects
                remainder = [s for s in new_rects if r in s]
                if remainder == []:  # no ==> add this rect to new rects
                    new_rects.append(r)

        new_rects = list(set(new_rects))  # remove any duplicates
        new_rects.sort(key=lambda r: abs(r), reverse=True)
        remove = []
        for j in range(len(new_rects)):
            for i in range(len(new_rects)):
                if new_rects[j] in new_rects[i] and i != j:
                    remove.append(j)
        remove = list(set(remove))
        for i in reversed(remove):
            del new_rects[i]
        new_rects.sort(key=lambda r: (r.tl.y, r.tl.x))  # sort by location


        mat = fitz.Matrix(5, 5)  # high resolution matrix
        for i, r in enumerate(new_rects):
            if r.width is None or r.height <= 15 or r.width <= 15:
                continue  # skip lines and empty rects
            pix = page.get_pixmap(matrix=mat, clip=r)
            hayPath = f"drawing-rect{page_num}-{i}.png"
            if pix.n - pix.alpha >= 4:      # can be saved as PNG
                pix = fitz.Pixmap(fitz.csRGB, pix)
            pix.save(hayPath)
            pix = None                     # free Pixmap resources

    pdf_document.close()

 

이 포괄적인 접근 방식을 통해 필는 거의 모든 샘플 PDF에서 정보를 성공적으로 추출할 수 있었습니다. 하지만 많은 노력에도 불구하고 특히 추출해야 하는 대량의 PDF 파일 세트를 다루는 경우 이 작업을 수행하는 간단한 방법을 아직 찾지 못했습니다.

 

그러나 표 테두리의 경우 pdfplumber 깃허브 페이지에 문제와 토론이 있다는 점에 주목할 가치가 있습니다. 다음 검색어 링크를 사용하여 모두 찾을 수 있습니다. 그 동안 이 특정 문제에 대한 귀중한 인사이트와 잠재적인 해결책을 제공할 수 있는 오픈 소스 프로젝트를 살펴볼 것을 권장합니다.

Excel과 VBA의 모든 것 아이엑셀러 닷컴 · 강사들이 숨겨 놓고 보는 엑셀러TV

반응형