Python의 BeautifulSoup를 사용해 만든 크롤러를 개선해봅니다.

 

 

1. 사전 준비

 

본 연습 프로젝트는 레드 마인의 이슈를 크롤링 해오는 것을 목표로 합니다.

크롤링을 목표로 하기 때문에 레드 마인의 API는 사용하지 않습니다.

 

이전 코드는 다음 글을 확인해 주세요.

 

 

 

2. 시그널 처리기 추가하기.

 

현재의 소스는 한번 크롤링해오고 종료되는 방식입니다. 이제 이 크롤러가 주기적으로 동작하도록 수정해 봅시다.

 

소스코드의 내용을 다음과 같이 수정해 주세요.

 

import requests
import datetime
import time
from bs4 import BeautifulSoup

class Crawler:
    def __init__(self):
        self.__main_url = 'https://src.smoh.kr/'
        self.__login_url = 'https://src.smoh.kr/login'
        self.__search_url = 'https://src.smoh.kr/projects/crawlerissues/issues?set_filter=1'
        self.__login_data = {
            'username': 'jd_user',
            'password': 'P@ssword'
        }
        self.__issues = {}
        self.__stop = False;
        return

    def main(self):
        while not self.__stop:
            with requests.Session() as s:
                main_req = s.get(self.__main_url)
                html = main_req.text
                obj = BeautifulSoup(html, 'html.parser')
                auth_token = obj.find('input', {'name': 'authenticity_token'})['value']
                self.__login_data = {**self.__login_data, **{'authenticity_token': auth_token}}

                login_req = s.post(self.__login_url, self.__login_data)
                if login_req.status_code == 200:
                    search_req = s.get(self.__search_url)
                    search_obj = BeautifulSoup(search_req.text, 'html.parser')
                    thead = search_obj.select('div.autoscroll > table > thead > tr > th')
                    tbody = search_obj.select('div.autoscroll > table > tbody > tr > td')
                    issue_obj = {}
                    issue_list = []
                    for i in range(1, len(tbody) +1):
                        if(i % len(thead) != 0):
                            issue_obj[thead[i % len(thead)].text] = tbody[i].text
                        else:
                            issue_list.append(issue_obj)
                            issue_obj = {}
                    self.__issues = {
                        'execution_dttm': str(datetime.datetime.now().isoformat()),
                        'issues': issue_list
                    }
                else:
                    print('Failed to login(Code: %d)' %(req.status_code))
                print(self.__issues)
                time.sleep(5)
        return

crawler = Crawler()
crawler.main()

 

별로 변경된 내용은 없습니다. init에 __stop을 추가해주고 해당 값이 False이면 크롤링 작업을 5초마다 반복합니다.

 

중지하고 싶을 땐 Ctrl + C를 눌러서 중지합니다.

 

 

종료 방법도 Ctrl + C 밖에 없을뿐더러 종료 시 위와 같은 이상한 에러가 발생합니다. 

 

시그널을 처리할 수 있도록 수정하여 깔끔하게 종료될 수 있도록 수정해 봅니다.

 

import requests
import datetime
import time
import signal
from bs4 import BeautifulSoup

class Crawler:
    def __init__(self):
        self.__main_url = 'https://src.smoh.kr/'
        self.__login_url = 'https://src.smoh.kr/login'
        self.__search_url = 'https://src.smoh.kr/projects/crawlerissues/issues?set_filter=1'
        self.__login_data = {
            'username': 'jd_user',
            'password': 'P@ssword'
        }
        self.__issues = {}
        self.__stop = False;

        signal.signal(signal.SIGINT, self.stop)
        signal.signal(signal.SIGTERM, self.stop)
        return

    def stop(self, signum, frame):
        self.__stop = True
        return

    def main(self):
        while not self.__stop:
            with requests.Session() as s:
                main_req = s.get(self.__main_url)
                html = main_req.text
                obj = BeautifulSoup(html, 'html.parser')
                auth_token = obj.find('input', {'name': 'authenticity_token'})['value']
                self.__login_data = {**self.__login_data, **{'authenticity_token': auth_token}}

                login_req = s.post(self.__login_url, self.__login_data)
                if login_req.status_code == 200:
                    search_req = s.get(self.__search_url)
                    search_obj = BeautifulSoup(search_req.text, 'html.parser')
                    thead = search_obj.select('div.autoscroll > table > thead > tr > th')
                    tbody = search_obj.select('div.autoscroll > table > tbody > tr > td')
                    issue_obj = {}
                    issue_list = []
                    for i in range(1, len(tbody) +1):
                        if(i % len(thead) != 0):
                            issue_obj[thead[i % len(thead)].text] = tbody[i].text
                        else:
                            issue_list.append(issue_obj)
                            issue_obj = {}
                    self.__issues = {
                        'execution_dttm': str(datetime.datetime.now().isoformat()),
                        'issues': issue_list
                    }
                else:
                    print('Failed to login(Code: %d)' %(req.status_code))
                print(self.__issues)
                time.sleep(5)
        return

crawler = Crawler()
crawler.main()

 

init에 signal을 추가했으며 __stop를 다루는 stop함수를 추가했습니다. 이제 Ctrl + C로 종료를 해도 에러 없이 종료되는 모습을 확인할 수 있습니다.

 

 

 

3. Logger 추가하기.

 

크롤러의 작업 내용을 기록하는 로거를 만들어 적용해 보도록 하겠습니다.

 

logger.py 파일을 새로 만든 후 다음과 같이 코딩해 주세요.

 

import logging
import time
import json
import datetime
import os

class Logger:
    def __init__(self, name, log_file):
        self.name = name
        self.log_file = log_file
        logging.basicConfig(level = logging.INFO, format = '%(message)s')
        self.logger = logging.getLogger(self.name)
        self.log_hadler = logging.FileHandler(self.log_file)
        self.logger.addHandler(self.log_hadler)
        return
    
    def log(self, event, value):
        log_obj = {
            'component': self.name,
            'dttm': str(datetime.datetime.now().isoformat()),
            'log': {
                'event': event,
                'desc': value
            }
        }
        self.logger.info(json.dumps(log_obj))
        return

 

로거는 컴포넌트 이름과 파일 경로를 받아와서 로그 파일을 남깁니다. 남겨지는 로그의 구조는 log_obj에 정의되어 있습니다.

 

현재는 info레벨의 로그만 남기도록 되어 있으므로 필요에 따라 수정해서 남기면 됩니다.

 

이제 로거를 사용해 보도록 합니다. crawler.py 파일을 다음과 같이 수정합니다.

 

import requests
import datetime
import time
import signal
import os
from bs4 import BeautifulSoup
from logger import Logger

class Crawler:
    def __init__(self):
        self.__main_url = 'https://src.smoh.kr/'
        self.__login_url = 'https://src.smoh.kr/login'
        self.__search_url = 'https://src.smoh.kr/projects/crawlerissues/issues?set_filter=1'
        self.__login_data = {
            'username': 'jd_user',
            'password': 'P@ssword'
        }
        self.__issues = {}
        self.__stop = False;
        self.__log_file = 'D:\\crawler.log'

        self.logger = Logger('crawler', self.__log_file)

        signal.signal(signal.SIGINT, self.stop)
        signal.signal(signal.SIGTERM, self.stop)
        return

    def stop(self, signum, frame):
        self.__stop = True
        return

    def main(self):
        self.logger.log('start', { 'pid': os.getpid() })
        while not self.__stop:
            with requests.Session() as s:
                main_req = s.get(self.__main_url)
                html = main_req.text
                obj = BeautifulSoup(html, 'html.parser')
                auth_token = obj.find('input', {'name': 'authenticity_token'})['value']
                self.__login_data = {**self.__login_data, **{'authenticity_token': auth_token}}

                login_req = s.post(self.__login_url, self.__login_data)
                if login_req.status_code == 200:
                    search_req = s.get(self.__search_url)
                    search_obj = BeautifulSoup(search_req.text, 'html.parser')
                    thead = search_obj.select('div.autoscroll > table > thead > tr > th')
                    tbody = search_obj.select('div.autoscroll > table > tbody > tr > td')
                    issue_obj = {}
                    issue_list = []
                    for i in range(1, len(tbody) +1):
                        if(i % len(thead) != 0):
                            issue_obj[thead[i % len(thead)].text] = tbody[i].text
                        else:
                            issue_list.append(issue_obj)
                            issue_obj = {}
                    self.__issues = {
                        'execution_dttm': str(datetime.datetime.now().isoformat()),
                        'issues': issue_list
                    }
                else:
                    self.logger.log('login_fail', { 
                        'pid': os.getpid(), 
                        'error_code':  req.status_code
                    })
                self.logger.log('get_issues', { 
                    'pid': os.getpid(),
                    'count': len(issue_list)
                })
                print(self.__issues)
                time.sleep(5)
        self.logger.log('stop', { 'pid': os.getpid() })
        return

crawler = Crawler()
crawler.main()

 

이제 코드를 실행시키면 로그 파일 경로에 다음과 같이 정상적으로 로그가 남는 것을 확인할 수 있습니다.

 

{"component": "crawler", "dttm": "2020-02-03T10:45:16.780440", "log": {"event": "start", "desc": {"pid": 30568}}}
{"component": "crawler", "dttm": "2020-02-03T10:45:17.729584", "log": {"event": "get_issues", "desc": {"pid": 30568, "count": 3}}}
{"component": "crawler", "dttm": "2020-02-03T10:45:23.683570", "log": {"event": "get_issues", "desc": {"pid": 30568, "count": 3}}}
{"component": "crawler", "dttm": "2020-02-03T10:45:28.705034", "log": {"event": "stop", "desc": {"pid": 30568}}}

 

 

 

 

 

 

반응형

+ Recent posts