본문 바로가기

서비스 제작

[MDQA]1.1 웹 페이지는 크롤링, PDF는 파일을 읽어서 텍스트 데이터를 가져오기(Loader 모듈)

MDQA를 위해 “유저가 입력한 웹 페이지 또는 파일을 데이터 베이스에 저장”하는 과정에 대해 다루고 있다. [MDQA]1. 웹 페이지 또는 파일을 데이터 베이스에 저장하기 에서 설명한 것처럼 그 과정은 아래와 같다.

  1. 유저가 웹 페이지의 URL을 입력 또는 PDF 파일을 업로드
  2. 웹 페이지는 크롤링, PDF는 파일을 읽어서 텍스트 데이터를 가져옴(Loader 모듈)
  3. 텍스트 데이터를 적절한 크기의 chunk로 자른다.(chunker 모듈)
  4. 자른 텍스트를 특정 크기의 벡터로 임베딩한다. (embedder 모듈)
  5. chunk를 중요한 순서대로 정렬한 데이터도 저장한다. (Text ranker 모듈, 이 부분은 필수적이지 않다.)
  6. 이렇게 처리한 데이터들을 데이터베이스에 저장한다.

이번 포스팅에서는 2번 과정인 “웹 페이지는 크롤링, PDF는 파일을 읽어서 텍스트 데이터를 가져옴(Loader 모듈)”를 설명해본다.

웹 페이지는 크롤링, PDF는 파일을 읽어서 텍스트 데이터를 가져옴(Loader 모듈)

Loader를 통해 최종적으로 얻어지는 데이터는 아래와 같다.

- file_path: 웹 페이지 URL 또는 PDF 파일의 서버상 위치
- file_uuid: 파일 구분을 위한 고유 UUID
- project_id: 유저의 프로젝트 ID
- title: 웹 페이지 제목/PDF 파일 이름
- screenshot_path: 웹 페이지/PDF 스크린샷 사진 파일의 서버상 위치
- favicon: 웹 페이지 favicon
- init_date: 데이터를 얻어온 최초 시간
- updated_data: 데이터를 재 크롤링 혹은 다시 가져온 최근 시간(초기에는 init_date로 초기화된다.)
- full_text: 웹 페이지/PDF의 전체 텍스트
- processed_path: 전체 처리된 데이터가 저장된 json 파일의 위치
- html_path: 웹은 크롤링해서 저장한 html 파일 위치, PDF에서는 pdf를 html 파일로 변환하여 저장된 위치
- pdf_path: 웹은 pdf로 변환해서 pdf파일로 저장된 위치. pdf에서는 저장된 pdf의 위치.
- data: 크롤링 또는 파일로부터 가져온 (텍스트, 픽셀위치)의 리스트
    - text: 텍스트
    - bbox: 픽셀 위치
    - page: pdf의 경우 페이지 번호

웹 페이지를 크롤링하여 데이터를 가져오는 모듈은 Web Loader, PDF에서 데이터를 가져오는 모듈은 Pdf Loader로 명명한다.

Web Loader

Puppeteer 라이브러리를 크롤링에 사용한 이유

크롤링할 때는 자바스크립트의 puppeteer을 사용했다. 작성된 모든 코드는 Web loader에서 크롤링 부분만 제외하고 모두 python이다. 하지만 크롤링시에는 자바스크립트를 사용했는데 그 이유는 파이썬에서의 크롤링이 사용함에 있어서 비효율적인 부분이 있기 때문이다. 파이썬에서 사용해본 크롤러는 아래와 같다.

  • selenium: 초기에 셀레니움을 사용해서 크롤링을 했으나 셀레니움은 컴퓨터 리소스가 많이 들어 멀티프로세싱으로 크롤링을 시행할 때 컴퓨터 속도가 매우 느려졌다.
  • scrapy: scrapy는 하나의 웹 주소를 주고 내부 사이트를 파고파고 들어가며 크롤링할 때 특화된 느낌이 들었다. 크롤링 할 때 웹에서 자바스크립트가 실행된 후에 크롤링을 해야하는 경우도 있다. 이런 경우에서 자바스크립트가 실행된 이후 크롤링을 하는데 어려움이 있어 사용하지 않게 되었다.

위와 같은 이유로 웹과 가깝다고 생각한 자바스크립트의 puppeteer 라이브러리를 사용해서 크롤링을 진행했다.

웹 페이지에서 데이터를 가져올 때 전처리

  • headless 모드 사용: 서버에는 모니터가 없고, 컴퓨터 리소스 절약을 위함
  • 사람이 사용하는 브라우저처럼 보이도록 사용자 에이전트 설정: 이를 설정하지 않으면 크롤링 되지 않는 사이트가 존재함.
  • 광고를 제거하기 위해 puppeteer-extra-plugin-adblocker 라이브러리를 사용했다.
  • 팝업으로 알려진 URL을 포함하는 요청을 수동적으로 차단했다. 예시로 아래같이 medium을 크롤링 할 때 로그인을 하지 않으면 하단의 팝업이 나온다. 이를 없애기 위해서 'cdn-client-medium.com’ 에서는 값을 가져오지 않도록 했다. 또, ‘쿠키를 허용하겠습니까?’ 라는 팝업을 제거하기 위해서는 url에 ‘cookie’라는 텍스트가 포함된 url에서는 값을 가져오지 못하게했다. 물론 이 방식으로는 모든 경우에 대해 다 대응할 수 없다.

  • 스크롤 위치에 따라서 데이터가 로드되는 웹 페이지가 있다. 전체 데이터를 얻기 위해서 처음에 상단부터 하단까지 전체 스크롤하도록 했다.
  • 컨텐츠와 관련되지 않은 내용을 제거하기 위해 'iframe', 'header', 'footer', 'script', 'nav’ 태그는 제거했다.
  • 상단 고정바를 제거하기 위해 head, hed, below라는 내용이 클래스에 포함되어 있으면 제거했다.
  • 모든 이미지나 CSS, JS 링크 등 다른 링크로부터 데이터를 가져오는 경우 html 코드에 적혀있는 상대 경로를 절대 경로로 변경
  • CSS 스타일 태그 제거

위의 전처리 과정 이후 텍스트를 가져왔다. 텍스트를 가져올 때 각 텍스트의 위치도 함께 가져왔다. 우리 서비스에서는 유저가 질문을 했을 때 답변을 제공하고, 그것을 가져온 위치 등도 함께 제공하기 때문이다.

'p, div, span, h1, h2, h3, h4, h5, h6, em, figcaption, strong, a, b, td’ 태그의 요소에 대해서 텍스트와 위치를 가져왔다. 위치는 (left_x, top_y, right_x, bottom_y)의 형태로 가져왔다. 그리고 요소를 통해 텍스트를 가져오면 어느정도 묶여있는 chunk형태를 띄게 된다. 이것을 붙이고, 다시 떼는 과정을 이후 chunker에서 진행한다.

위 과정은 모두 자바스크립트로 짜여졌다. 그리고 이 코드를 아래처럼 node를 파이썬 코드 상에서 실행해서 크롤링 결과를 가져왔다.

result = subprocess.run(['node', self.js_path, file_path, file_uuid, screenshot_dir, html_save_dir, pdf_save_dir, favicon_save_dir], capture_output=True, text=True, encoding='utf-8')

그외 처리한 것

  • 스크린샷 찍기
  • 타이틀 가져오기
  • 파비콘 가져오기
  • pdf로 변환해서 저장
  • html코드 저장

PDF Loader

pdf도 기본적으로는 웹과 가져오는 값은 동일하다. pdf도 위치 정보를 가져오기 위해 pdf파일 내의 element를 사용해서 가져왔다. 파이썬 라이브러리로는 fitz를 사용했다.

그외 처리한 것

  • 스크린샷 찍기
  • 타이틀 가져오기
  • html코드 저장: pdf2htmlEX를 사용했다.

다음 포스팅에서는 chunker에 대해 다뤄보자.