파이썬을 이용한 웹 크롤링
포스트
취소

파이썬을 이용한 웹 크롤링

들어가기 전…

비동기 프로그래밍과 로깅에 관련된 코드가 포함됨.

크롤러

크롤링(crawling) 혹은 스크레이핑(scraping)은 웹 페이지를 그대로 가져와서 거기서 데이터를 추출해 내는 행위다. 크롤링하는 소프트웨어는 크롤러(crawler)라고 부른다.

내가 크롤링 하려는 것

유로파 유니버셜리스4 라는 게임에 등장하는 국가들의 Flag를 추출해 다운받아볼 것이다.

유로파 유니버셜리스4는 중세시대 말기부터 산업혁명까지 국가를 운영하는 게임인데 꽤 재밌다 ㅎㅎ..

계획

먼저 크롤링을 위해 requests, bs4 라이브러리 설치가 필요하다. requests 말고 파이썬 내장 모듈인 urllib를 써도 되긴하지만 좋은 라이브러리가 있으니까…

1
2
pip install requests
pip install bs4

사진 크롤링 목표가 되는 table과 image url이다.

해당 웹 페이지의 HTML 코드를 긁어서 목표로 하는 Table 요소를 찾아 그 안의 img src 들을 수집해 다운받을 것이다.

코딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import requests
import sys
import shutil
import os
import asyncio
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
FORMAT = "[%(levelname)s][ %(filename)s: %(funcName)s() ] %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


def get_country_info(with_img_url=False):
    try:
        req = requests.get('https://eu4.paradoxwikis.com/Countries')
    except requests.ConnectionError:
        logger.error('wiki connect fail... please check your network.')
        sys.exit()

    if req.status_code != 200:
        logger.error('wiki connect fail... status is not 200.')
        sys.exit()

    html = req.text
    soup = BeautifulSoup(html, 'html.parser')
    country_table = soup.find_all(
        'table',
        {'class': ['wikitable', 'sortable', 'jquery-tablesorter']}
    )[0]
    trs = country_table.find_all('tr')
    tr_tds = trs[1:]
    image_src_base = 'https://eu4.paradoxwikis.com'
    country_list = []
    for tr_td in tr_tds:
        tds = tr_td.find_all('td')
        _, raw_country_info, raw_country_tag, _, _ = tds

        country_name = raw_country_info.find_all('a')[1].text.strip()
        country_tag = raw_country_tag.text.strip()
        img_src = image_src_base + raw_country_info.a.img.attrs['src']
        img_name = img_src.split('/')[-1]

        if with_img_url:
            country_list.append({
                'country_name': country_name,
                'country_tag': country_tag,
                'img_src': img_src,
                'img_name': img_name
            })
        else:
            country_list.append({
                'country_name': country_name,
                'country_tag': country_tag,
                'img_name': img_name
            })

    return country_list


def image_download(url, path):
    if os.path.exists(path):
        return

    session = requests.Session()
    retry = Retry(connect=5, backoff_factor=0.5)
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('https://', adapter)
    response = session.get(url, stream=True)

    with open(path, 'wb') as out_file:
        shutil.copyfileobj(response.raw, out_file)
    del response

    logger.info('{} Downloaded'.format(url))


async def async_requester(loop, func, *args):
    await loop.run_in_executor(None, func, *args)


def main_code():
    country_infos = get_country_info(with_img_url=True)
    img_root_dir = 'country_flags/'

    if not os.path.isdir(img_root_dir):
        os.mkdir(img_root_dir)
        logger.info('* Create directory: {}'.format(img_root_dir))

    tasks = []
    loop = asyncio.get_event_loop()
    for country_info in country_infos:
        url = country_info['img_src']
        path = img_root_dir + country_info['img_name']
        tasks.append(async_requester(loop, image_download, url, path))
    loop.run_until_complete(asyncio.wait(tasks))
    logger.info('* finished!')


if __name__ == '__main__':
    main_code()

get_country_info() 함수는 Country 정보가 담긴 Table을 긁어 각 인스턴스의 데이터를 리스트로 정리해준다. with_img_url 매개변수를 이용해 img src url 정보를 가져올지말지 선택할 수 있게 했다.

image_download() 함수는 url과 저장할 경로를 매개변수로 받아 웹상의 이미지 파일을 로컬로 다운받도록 해준다.

async_requester()는 코루틴으로써 비동기적으로 request를 보낼 수 있게 해준다. 만약 동기적으로 대량의 파일을 다운받는다면 상당한 시간이 걸릴 것이다.

사진

1
2
3
4
5
6
7
[INFO][ eu4_contry_flag.py: main_code() ] * Create directory: country_flags/
[INFO][ eu4_contry_flag.py: image_download() ] https://eu4.paradoxwikis.com/images/thumb/4/44/Auvergne.png/50px-Auvergne.png Downloaded
.
.
.
[INFO][ eu4_contry_flag.py: image_download() ] https://eu4.paradoxwikis.com/images/thumb/c/c6/Bengal.png/50px-Bengal.png Downloaded
[INFO][ eu4_contry_flag.py: main_code() ] * finished!

원하는대로 결과물이 나왔다.

후기

크롤러를 만드는 건 꽤 재밌다. 원하는 데이터를 마음껏 뽑을 수 있기 때문이다. 단, 위에서 사용한 방법은 javascript 등으로 동적 랜더링 하는 웹 데이터는 뽑아내지 못한다. 이 경우엔 selenium 라이브러리를 사용해 chromedriver로 동적 크롤링을 해야한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.