ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Python | 크롤링 도중 None 객체가 여러번 반환된다면?
    python 2022. 12. 21. 01:31

    - 문제상황

     

    Django 프로젝트를 진행하며 웹 스크랩핑을 하면서 문서에 요소가 존재하지 않아 None 객체를 반환하거나, IndexError가 반환되어 크롤링을 하던 프로그램이 멈추는 경험을 했다. 프로젝트 회고록은 이후 작성할 예정이다.

    내가 팀장이 되어 팀원들이 크롤링 작업을 하는 상황이었다. 프로젝트를 진행하는 도중, 팀원 두명이 나에게 와 여러 개의 객체가 있을수도 있고 None일수도 있는 상황인데 모든 줄에 if-else를 추가해야 하냐고 물어보았다.

    import requests
    from bs4 import BeautifulSoup
    
    link = '<page_link>'
    res = requests.get(link)
    soup = BeautifulSoup(res.text, 'html.parser')
    item = soup.select('#contents-area')[0]
    
    none_1 = item.select('span.view2_summary_info1')[0].text.strip()
    none_2 = item.select('span.view2_summary_info2')[0].text.strip()
    none_3 = item.select('div.view_step div.view_tag')[0].text.replace(" ","").split('#')[1:]
    for i in range(10):
    	none_4 = item.select(f'div.view_step div.view_step_cont.media.step{i+1} img')[0].get('src').strip()

    예를 들어 이러한 형식의 코드가 있다고 하자. 이때 none_1 ~ none_4 객체가 모두 None을 반환할 가능성이 있다고 생각해보자.

    page_link1 에서는 none_1 ~ none_3 까지는 제대로 가져와지는데, 해당 페이지에 media 클래스가 없어서 IndexError가 발생하는 것이다.

    page_link2에서는 none_1~none3이 item.select 하는 과정에서 view2_summary_info1,2 클래스가 모두 없고, view_tag 클래스가 없어서 None을 반환하여 IndexError 발생, none_4는 에러가 발생되지 않고 제대로 가져온다고 생각해보자.

     

    none_1 ~ none_4 객체 중에서 None 객체가 아닌 값들은 모두 값을 가져와서 저장하고, None 객체의 경우에는 그대로 None 객체로 저장한다고 하자.

     

    이러한 경우 우리는 파이썬에서 쉽게 try-except 문을 떠올릴 수 있다. 또는 if-else 문으로 각각의 경우에 예외처리를 한다고 생각할 수 있다.

    import requests
    from bs4 import BeautifulSoup
    
    link = '<page_link>'
    res = requests.get(link)
    soup = BeautifulSoup(res.text, 'html.parser')
    item = soup.select('#contents-area')[0]
    
    try:
    	none_1 = item.select('span.view2_summary_info1')[0].text.strip()
    	none_2 = item.select('span.view2_summary_info2')[0].text.strip()
    	none_3 = item.select('div.view_step div.view_tag')[0].text.replace(" ","").split('#')[1:]
        for i in range(10):
            none_4 = item.select(f'div.view_step div.view_step_cont.media.step{i+1} img')[0].get('src').strip()
    except IndexError as e:
    	none_1 = None
        none_2 = None
        none_3 = None
        none_4 = None

    위에서 None이 아니면 저장을 한다고 했으니 이렇게는 당연히 적지 않을 것이다. 그렇다면 if-else문을 사용한다면 아래와 같이 적을 수 있다.

     

    import requests
    from bs4 import BeautifulSoup
    
    link = '<page_link>'
    res = requests.get(link)
    soup = BeautifulSoup(res.text, 'html.parser')
    item = soup.select('#contents-area')[0]
    
    
    none_1 = item.select('span.view2_summary_info1')
    none_1 = none_1[0].text.strip() if none_1 else None
    none_2 = item.select('span.view2_summary_info2')
    none_2 = none_2[0].text.strip() if none_2 else None
    none_3 = item.select('div.view_step div.view_tag')
    none_3 = none_3[0].text.replace(" ","").split('#')[1:] if none_3 else None
    for i in range(10):
    	none_4 = item.select(f'div.view_step div.view_step_cont.media.step{i+1} img')
        none_4 = none_4[0].get('src').strip() if none_4 else None

    삼항연산자를 사용하여 None이 아닌 경우에는 그대로 인덱싱 연산 이후를 수행하면 된다고 생각할 수도 있다. 물론 이 방법이 나쁘다고 생각하지는 않는다.

    하지만 none을 반환할 가능성이 있는 객체가 10가지가 넘어간다면?? 또 뒤의 메소드 체인이 길어지거나 비슷한 / 같은 메소드 체인이 반복된다면 위의 방법이 깨끗한 코드를 작성하지는 않을 것이라고 생각된다.

     

    - 내가 생각한 해결 방안

     

    그래서 나는 아래와 같은 해결 방안을 팀원들에게 제시했다.

     

    import requests
    from bs4 import BeautifulSoup
    
    def soup_element_none(soup, selector, name):
        soup_element = soup.select(selector)
        elements_dict = {
            'none12': lambda s: s[0].text.strip(),
            'none3': lambda s: s[0].text.replace(" ","").strip().split('#')[1:],
            'none4': lambda s: s[0].get('src').strip(),
        }
        if soup_element:
            return elements_dict[name](soup_element)
        else:
            return None
    
    
    link = '<page_link>'
    res = requests.get(link)
    soup = BeautifulSoup(res.text, 'html.parser')
    item = soup.select('#contents-area')[0]
    
    none_1 = soup_element_none(item, 'span.view2_summary_info1', 'none12')
    none_2 = soup_element_none(item, 'span.view2_summary_info2', 'none12')
    none_3 = soup_element_none(item, 'div.view_step div.view_tag', 'none3')
    for i in range(10):
    	none_4 = soup_element_none(item, f'div.view_step div.view_step_cont.media.step{i+1} img', 'none4')

    soup_element_none 함수를 설명하자면 다음과 같다.

    soup 객체, queryselector, 메소드 체인을 정의해둔 key를 파라미터로 받아온다.

    딕셔너리의 value로는 람다 형식의 메소드 체인을 정의해두었다. 

    queryselector로 select 한 결과가 None이 아니라면 메소드 체인을 실행하고, None 이라면 그대로 None을 반환하였다.

     

    내가 생각하는 위 방식의 장점으로는

    위에서 가정한 대로 none을 반환할 가능성이 있는 객체가 10가지가 넘어가거나 메소드 체인이 중복되는 경우 같은 key로의 처리, 같은 함수로의 처리이다.

    다른 방식의 메소드 체인이 필요하다고 하더라도 딕셔너리에 key/value를 추가하는 것만으로도 확장할 수 있다는 것이다.

    soup 객체가 달라져도 사용할 수 있다는 점이다. 위에서 item만 사용하였지만 for loop을 사용해야할 일이 생기면 그대로 for loop의 value를 첫 번째 파라미터로 넣어주면 사용 가능하다.

     

    하지만 단점으로는

    실제 함수를 사용하는 부분에서 (none_1, none_2가 정의되는 부분) 내가 어떤 방식으로 element를 select 하는지 한눈에 알아볼 수 없다는 점이 있다.

    디버깅이 어려워 진다는 단점이 있다. 나의 경우에는 완성된 스크랩핑 코드를 리팩토링하는 과정에서 저렇게 함수를 작성했지만, 무조건 저 메소드 체인으로 가져온다는 것은 장담할 수 없다. 따라서 에러가 발생했을 때 어느 곳에서 에러가 나는지 더 찾기 어려워진다는 단점이 존재한다.

     


    - 오류 처리 내용

    처음에 soup_element_none 함수를 구현하고자 생각했을 때,

    def soup_element_none(soup, selector, name):
        soup_element = soup.select(selector)
        elements_dict = {
            'none12': soup_element[0].text.strip(),
            'none3': soup_element[0].text.replace(" ","").strip().split('#')[1:],
            'none4': soup_element[0].get('src').strip(),
        }
        if soup_element:
            return elements_dict[name](soup_element)
        else:
            return None

    이렇게 작성하면 되는 것 아닌가 하고 생각했었다. 하지만 실행하면서 soup_element[0] 부분에서부터 이미 에러가 발생하고 있었다. 이때에도 IndexError가 반환되어서 왜 그럴까 생각을 했었는데, 만약 soup_element = soup.select(selector) 에서 None이 할당된다면 딕셔너리조차 만들 수 없는 것이다.

    어떤 방법이 있을 까 고민하던 찰나 함수 객체만 만들어 두고 실행은 None이 아닐 때에만 하면 되겠다는 생각이 들었다.

    함수를 아래에 정의할 필요 없이 람다 함수로 정의하여 return elements_dict[name](soup_element) 해주는 방식으로 return을 하면서 람다 함수의 파라미터로 soup_element를 넣어 함수를 실행시키면 된다는 생각이 떠올라 위 방식처럼 작성하게 되었다.

    'python' 카테고리의 다른 글

    Django | URLConf 에서 lambda의 활용  (0) 2022.12.07
    Python | *, **의 의미  (0) 2022.08.02
    Python | List Comprehension이 왜 더 빠를까?  (0) 2022.07.26

    댓글

Designed by Tistory.