본문 바로가기
잡지식 저장고/크롤링

Python Multiprocessing으로 병렬처리, 크롤링으로 맛보기

by Slate_Knowledge 2023. 2. 18.
728x90

Why Multiprocessing?

Python은 Global Interpreter Lock(GIL) 로 인해서 원칙적으로는 c에서와 같이 fork 등 프로세스 분기가 막혀있다. 그럼에도 불구하고 유사한 효과를 제공할 수 있는 여러가지 방법들을 제공하는데, 이 중 multiprocessing, 그 중에서도 Pool을 다뤄본다.

크롤러나 I/O를 포함하는 파이썬 스크립트나 c backend가 없는 프로그램의 경우 단일 스레드만 사용하면 시스템 리소스 사용 효율이 너무 떨어지는 경우가 생기는데, 이 때 이걸 어느정도 해결하는 방법이 multiprocessing이다.

SIMD vs Pipeline

공식문서에서 수 많은 용례를 확인할 수 있고, threadpoolexecutor, processpool executor, parmap등 많은 변주들이 있지만 개인적으로 써봤을때는 그냥 Process, Pool 사용하는게 제일 편해서 다시 돌아가게 된다.

내가 사용하는 병렬화 방식은 크게 두가지인데,

  1. 단순한 작업이 매우 많이 반복되어야한다 --> Pool을 사용하는 SIMD스러운 병렬처리(this post)
  2. 작업이 여러개의 과정으로 복잡하게 이뤄져있다 --> Process 및 Arrayqueue를 이용한 파이프라인 구축
    from multiprocessing import Pool
    import requests
    from bs4 import BeautifulSoup
    import os
    
    def down_img(info):
        idx, link = info
        ext = link.split('.')[-1]
        response = requests.get(link)
        if response.status_code == 200:
            with open(f'download/{idx:06}.{ext}', 'wb') as f:
                f.write(response.content)
    
    def crawl(page_link):
        page = requests.get(page_link)
        soup = BeautifulSoup(page.text, "html.parser")
        links = [s.attrs['href'] for s in soup.find_all('img')]
        p = Pool(os.cpu_count())
        p.map(down_img, enumerate(links))
    
    if __name__=="__main__":
        page_link = "https://some.address.to.img.site"
        crawl(page_link)
    근데 이 예시 같은 경우에는 대체 지금 어디까지 다운로드가 완료되었는지 알 수가 없다. 원래라면 tqdm같은 라이브러리를 쓰면 되는데, multiprocessing으로 같이 엮어주려면 아래와 같이 약간의 변주가 필요하다.
  3. 보통 크롤링같은 작업이나, youtube-dl을 사용하는 IO 작업은 하나하나가 리소스를 많이 잡아먹지는 않아서 수백개를 한꺼번에 돌리고 싶어진다. 이럴 때, 1번의 접근법이 좋다. 아래는 가장 기본적으로 사용할 수 있는 예시(크롤링)이다.
from multiprocessing import Pool
import requests
from bs4 import BeautifulSoup
import os
from tqdm import tqdm

def down_img(info):
    idx, link = info
    ext = link.split('.')[-1]
    response = requests.get(link)
    if response.status_code == 200:
        with open(f'download/{idx:06}.{ext}', 'wb') as f:
            f.write(response.content)

def crawl(page_link):
    page = requests.get(page_link)
    soup = BeautifulSoup(page.text, "html.parser")
    links = [s.attrs['href'] for s in soup.find_all('img')]
    p = Pool(os.cpu_count())
    with tqdm(total=len(links),leave=False) as pbar:
        for _ in p.imap_unordered(down_img, links):
            pbar.update()

if __name__=="__main__":
    page_link = "https://some.address.to.img.site"
    crawl(page_link)

이렇게 하면 중간중간 프로그레스까지 확인가능한 병렬처리 크롤러가 완성됐다. 그런데 하나 남은 문제가, 이 프로그램을 실행하면 한 50프로 확률로 프리징이 일어난다. 특히 links 변수가 무지 긴 리스트면 더 그렇다. semlock이 생기기 때문인데(reference), 아래와 같이 바꾸면 이 문제도 피할 수 있다.

from multiprocessing import get_context
import requests
from bs4 import BeautifulSoup
import os
from tqdm import tqdm

def down_img(info):
    idx, link = info
    ext = link.split('.')[-1]
    response = requests.get(link)
    if response.status_code == 200:
        with open(f'download/{idx:06}.{ext}', 'wb') as f:
            f.write(response.content)

def crawl(page_link):
    page = requests.get(page_link)
    soup = BeautifulSoup(page.text, "html.parser")
    links = [s.attrs['href'] for s in soup.find_all('img')]
    p = get_context("spawn").Pool(os.cpu_count())
    with tqdm(total=len(links),leave=False) as pbar:
        for _ in p.imap_unordered(down_img, links):
            pbar.update()

if __name__=="__main__":
    page_link = "https://some.address.to.img.site"
    crawl(page_link)
728x90
반응형

댓글