<aside> <img src="https://img.icons8.com/ios/250/FFFFFF/light-on.png" alt="https://img.icons8.com/ios/250/FFFFFF/light-on.png" width="40px" /> 위키 피디아 관련된 정보를 어떤 방식으로 pik으로 변환할 지에 대한 구체적인 가이드 라인이 나와서 해당 구조에 맞게 크롤링을 시작하였다.

</aside>

위키 피디아의 HTML 구조를 파악하자면 문서로 작성된 부분이 같은 계층의 레벨이 나열이 되는 모습이다. 일반적인 내용은 <p></p>에 주제와 소주제의 경우는 <h2></h2> 그리고 <h3></h3>에 각각 배치가 된다.

Untitled

BeautifulSoup을 활용한 같은 레벨의 계층 크롤링

기존의 find 계열의 명령어를 이용해서 태그를 스크랩할 때는 재귀적으로 모든 태그를 가져오는 특징을 가진다. 예를 들어 아래와 같은 구조에서 div를 찾는다면 바깥과 안쪽의 해당하는 태그를 가져올 것이다.

<div id=1_1>
	<p>
		pragraph contents
		<div id=2_2>div contents</div>
	</p>
</div>
<div id=1_2>
	...
</div>

하지만 위키 피디아의 양식에서 바라볼 때, 우리가 원하는 방식은 1-1과 1-2의 태그를 가져오는 것이다. 위와 같은 방식은 recursive=False를 통해서 해결할 수 있다.

paragraph = soup.select_one("#mw-content-text > div.mw-parser-output") \\
                .find_all(["h1", "h2", "h3", "p"], recursive=False)

실제 코드는 위와 같다. 같은 레벨에서 원하는 태그를 순서대로 가져오도록 하며, 내부 레벨로는 탐색을 하지 않게 된다.

<aside> <img src="https://img.icons8.com/ios/250/FFFFFF/light-on.png" alt="https://img.icons8.com/ios/250/FFFFFF/light-on.png" width="40px" /> 크롤링 결과

멀티 쓰레딩을 이용한 크롤링

전반적인 크롤링 방식은 이전 태스크에서 정의한 방식과 유사하게 리모트 컨테이너를 이용한 멀티쓰레딩 방식을 사용하였다. 하지만 이 과정 중에서 문제가 발생하였다.

  1. 정확한 수의 데이터를 가져오지 못하였다.

    단일 쓰레드로 시도했을 때와 비교하였을 때, 적은 숫자의 데이터를 크롤링하였다.

  2. 파일 스트림을 쓰레드 간의 공유를 하는 것과 같은 현상이 나타났다.

    예를 들어 “스물다섯 스물 하나”와 관련된 데이터는 제목과 같은 이름의 파일에 저장되는 데, 아래와 같이 다른 컨텐츠가 저장이 되었다.

    스물다섯_스물하나.json

    스물다섯_스물하나.json

    또한 다른 파일에서 “스물다섯 스물하나”와 관련된 내용을 확인할 수 있었다.

    KS_X_1001의_특수_문자.json

    KS_X_1001의_특수_문자.json

문제 원인 가설

  1. 클래스 인스턴스끼리 데이터의 파일 스트림을 공유하는 문제

    컨텍스트가 바뀌면서 기존에 파일 스트림이 종료되지 않고 쓰레드가 열려있는 파일 스트림에다 데이터를 추가하는 것으로 판단하였다.

  2. 클래스 인스턴스의 지속적인 생성으로 인한 메모리 문제

    클래스의 __call__을 구현하여 함수 대신 인스턴스로 인자를 넘겨주고 있다. 이 과정에서 매번 호출에서 클래스 인스턴스가 생성될 수 있다고 판단하였다.

문제 원인 분석

  1. 클래스 인스턴스끼리 데이터의 파일 스트림을 공유하는 문제

    현재 크롤러는 클래스 인스턴스를 실행하면서 동작하게 되며 중요한 자료를 인스턴스의 프로퍼티로 저장을 하여 사용한다. 구조는 아래와 같다.

    함수 내부에서 사용하는 로컬 변수는 독립적으로 따로 관리되지만 인스턴스의 변수는 하나의 객체를 바라본다.

    함수 내부에서 사용하는 로컬 변수는 독립적으로 따로 관리되지만 인스턴스의 변수는 하나의 객체를 바라본다.

    즉 위의 구조의 문제점은 다음과 같이 정의할 수 있다.

  2. 클래스 인스턴스의 지속적인 생성으로 인한 메모리 문제

    당연히 단일 쓰레드로 실행을 하게되면 같은 인스턴스를 지속적으로 사용하게 된다.

    Untitled

    멀티 쓰레딩에서는 어떠할까?

    with ThreadPoolExecutor(max_workers=num_driver) as executor:
    		executor.map(RefCrawler(), data_gen)
    

    Untitled

    결과는 예상과 다르게 같은 인스턴스를 사용한다. 생각해보면 당연한 결과인 것이 map을 하게 될 때, 생성된 인스턴스를 전달하기 때문에 다행이 매 실행동안 새로운 인스턴스를 생성하지 않는다.

    refCrawler = RefCrawler()
    with ThreadPoolExecutor(max_workers=num_driver) as executor:
    		executor.map(refCrawler, data_gen)
    

    즉 위의 코드와 같다는 의미를 가진다.

    문제 원인 해결

    위의 분석을 통해서 나타난 문제는 클래스의 인스턴스 속성에 값을 저장한 상태에서 context change가 일어났고, 다른 쓰레드가 인스턴스 속성의 값을 바꿔 발생하는 문제이다.

    위의 문제를 해결하기 위해서 다음과 같은 조치를 취하였다.

    1. 클래스 메서드를 적극 사용하여 공유하는 값을 최소한을 줄였다.

      멀티 쓰레딩 처리를 위해서 포트를 공유해야하기 때문에 유효한 포트의 경우는 클래스의 타입 변수로 설정을 한다.

      class WikiRefCrawler:
          valid_ports = None
      
    2. 인스턴스 호출에 따른 상황에서는 인스턴스 변수를 최소한으로 사용한다.