Python + Seleniumwire를 이용한 에러 크롤링

Seunghoon Lee
원티드랩 기술 블로그
9 min readApr 20, 2022

안녕하세요. QA 엔지니어 이승훈입니다.

원티드랩은 채용&커리어 서비스인 원티드 외에도, HR 관리 서비스 원티드스페이스, 프리랜서 전문 매니징 서비스 긱스, 기업정보 제공 서비스인 크레딧잡 등도 운영하고 있습니다. 오늘은 크레딧잡 사이트의 DB를 포함한 백앤드 서비스를 대대적으로 리뉴얼하는 프로젝트를 QA하면서 만들었던 에러 크롤링 스크립트에 대한 글을 적어보려 합니다.

왜 크롤링을 고려하게 됐는가?

크레딧잡은 오랜 기간 눈에 띄는 기능 개선이 없었던 터라, QA 검증 대상 범위 밖에 있었습니다. 그래서 기존 기능에 대한 기능 파악도 안돼있고, 참고할 만한 문서나 테스트 산출물도 없는 상황이었습니다. 이런 상황에서 사이트를 전수 조사하는데 일정 부분 도움을 받을 수 있는 도구가 필요했고, 차후 진행될 사이트 개선에도 이를 지속적으로 활용할 수 있을 것이라 생각했습니다.

<목표>
사이트의 대표 주소를 입력하면 크롤링 스크립트가 사이트 내 하위 페이지와 연결된 페이지를 돌며 발생하는 스크립트, 네트워크 에러를 자동적으로 검출한다.

python + requests, selenium

python에서는 requests 라이브러리를 이용하여 http 리퀘스트의 응답값을 확인할 수 있습니다. 하지만 이는 서버에서 제공하는 코드만 받아오기 때문에 브라우저에 의해 최종적으로 랜더링된 페이지의 소스 코드와는 상당한 차이가 있을 수 있습니다. (javascript에 의해 랜더링되는 부분이 누락 됨)

반면, selenium 라이브러리를 이용할 경우 실제로 브라우저를 띄우기 때문에 누락없이 모든 응답 소스 코드를 가져올 수 있습니다.
(주의, 브라우저에 옵션을 걸어 headless로 구동할 경우에도, 최종 렌더링된 소스 코드가 다를 수 있음)

requests와 selenium의 응답값에서 <a> 태그 개수 비교

대략적인 전체 스크립트 구조 (초안)

selenium 웹드라이버 객체에서 콘솔에러 읽어오기

Selenium 웹드라이버 객체의 get_log(”browser”) 메소드를 이용하여 콘솔에 찍히는 에러들을 확인할 수 있습니다. 로그 중 SEVERE 수준의 에러가 있을 경우 로그 정보를 캡쳐하고 fail로 간주합니다. 결과는 json 형태로 OK, NOK라는 key에 분리하여 담습니다.

에러가 있는 페이지들을 리스트에 넣고 돌려봤을 때 에러가 잘 기록이 됩니다. 그러나 브라우저에 의해 해석된 메세지다 보니 구체적이지만 직관적이지 않은 경우가 있습니다. 주요 테스트 항목이 API 변경이다 보니, 네트워크 에러 같은 경우 [Response Status Code], [Content Type], [Request URL] 의 정보만 간결하게 직관적으로 결과를 확인하고자 하였습니다. 하지만, Selenium에서는 리퀘스트/응답의 구체적인 정보를 확인할 수 있는 기능이 제공되지 않습니다.

Seleniumwire = Selenium + 브라우저 요청/응답 검사 및 조작할 수 있는 라이브러리

<출처 pypi.org>
Selenium Wire 는 Selenium의 Python 바인딩을 확장하여 브라우저의 기본 요청에 액세스할 수 있도록 합니다. Selenium과 동일한 방식으로 코드를 작성하지만 요청 및 응답을 검사하고 즉시 변경할 수 있는 추가 API를 얻습니다.
샘플 코드
# Import from seleniumwi
refrom seleniumwire import webdriver
# Create a new instance of the Chrome driver
driver = webdriver.Chrome()
# Go to the Google home page
driver.get('<https://www.google.com>')
# Access requests via the `requests` attribute
for request in driver.requests:
if request.response:
print(
request.url,
request.response.status_code,
request.response.headers['Content-Type']
)
결과
<https://www.google.com/> 200 text/html; charset=UTF-8
<https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png> 200 image/png
<https://consent.google.com/status?continue=https://www.google.com&pc=s&timestamp=1531511954&gl=GB> 204 text/html; charset=utf-8
<https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png> 200 image/png
<https://ssl.gstatic.com/gb/images/i2_2ec824b0.png> 200 image/png
<https://www.google.com/gen_204?s=webaft&t=aft&atyp=csi&ei=kgRJW7DBONKTlwTK77wQ&rt=wsrt.366,aft.58,prt.58> 204 text/html; charset=UTF-8
...

위에서 설명하듯이 Seleniumwire는 Selenium 라이브러리를 기본적으로 포함하고 있기 때문에, Seleniumwire 설치 후, 기존 코드에서 selenium 대신에 seleniumwire만 임포트한 후 실행하면 기존 코드 그대로 정상 동작합니다. 그 후 리퀘스트 응답값을 확인하여 status code가 400 이상인 정보들을 캡쳐하는 코드를 추가하였습니다.

seleniumwire 를 이용한 네트워크 에러 캡쳐

BeautifulSoup을 이용하여 페이지 내 연결된 페이지 긁어오기

chrome webdriver 객체에서 페이지 소스를 읽어와 BeautifulSoup을 이용하여 현재 페이지 내 연결된 페이지들의 정보를 읽어옵니다. 그 중 pagelist 에 등록되어 있지 않은 새로운 주소들만 리스트에 추가하여 주면, 전체 for loop에서 추가된 페이지들에 대한 동일한 작업들이 진행됩니다.

# FIND AND QUEUE LINKS
src = driver.page_source
soup = BeautifulSoup(src, “html.parser”)
links = soup.find_all(‘a’, href=True)
for link in links:
msg = ‘{} — — → ‘.format(link[‘href’])
if str(link[‘href’]) == ‘/’ or str(link[‘href’]) == ‘#’:
msg += ‘Duplicated link’
continue
else:
full_url = site_domain+str(link[‘href’]) if str(link[‘href’]).startswith(‘/’) else str(link[‘href’])
if pagelist[0].count(full_url) == 0:
pagelist[0].append(full_url) # pagelist 정보에 추가
pagelist[1].append(page) # referrer 정보로 현재 페이지 정보를 기록

문제해결

변동적인 결과값
동일한 싸이트를 대상으로 스크립트를 반복적으로 돌려보았는데, 결과값이 변동적입니다. 스캔한 전체 페이지 수도 달라지고, 특정 페이지에서 에러가 캡쳐되기도 하고, 안되기도 하는 등…

하나의 Chrome webdriver 객체에서 URL을 변경해가며 확인하는 과정에서 각 페이지가 랜더링이 완료되는 시점과 스크립트가 에러와 링크를 읽어오는 타이밍이 일관적이지 않아, 때에 따라 누락되는 정보가 있는 것으로 판단하였음.
해결책 : 매 페이지 확인 시 새로운 webdriver를 구동하니, 에러 판단 및 연결된 페이지의 리스트 결과가 일관되게 나옵니다.

끝없는 페이지 리스트 → 메모리 에러로 실행 중지
연결된 페이지들의 리스트를 모두 추가하다 보니, 확인하는 페이지의 리스트가 아주 길어지고, 한 페이지 확인 시 마다 webdriver 객체를 열고 닫다보니 실행 시간/속도도 많이 늘어나 메모리 에러로 실행 중지되는 경우가 빈번히 생깁니다.

해결책 : 페이지 리스트 간소화
1. 연결된 페이지 중 사이트 외 (도메인이 다른 페이지) 페이지들은 테스트 대상 밖으로 간주하여 requests.status_code만 확인하고 페이지 내 에러 크롤링, 페이지 내 연결된 페이지 추가 단계는 건너뜀.
2. URL path에서 아이디 혹은 해쉬값으로 구분된 동일한 패턴의 페이지들은 최대 10개까지만 확인하고 나머진 스킵.
(예, 기업 상세 페이지 - https://kreditjob.com/company/{company hash value})
동일한 패턴의 페이지들 판단

알려진 에러들로 인한 결과의 어뷰징
경우에 따라 4xx 에러로 떨어지도록 구현된 알려진 이슈들 때문에 fail의 결과가 어뷰징이 되어 기대와 다른 이상 결과들만 확인하는데 어려움이 있었습니다.

해결책 : 알려진 이슈들을 등록하여 해당의 경우 warning 메시지만 남기고 fail로 기록하지 않음. (re 라이브러리를 활용)

마치며

<a> 태그로 연결된 페이지만 확인 가능하다는 점에서 제한적이고, 수행속도 면에서 좀 더 개선해야할 부분도 아직 있지만, 테스트 대상에 대한 깊은 이해가 부족했던 상황에서 든든한 백업 역할을 수행한 도구였고, 범용적으로 쓸 수 있어서, 원티드랩의 다른 서비스를 검증할 때도 잘 활용하고 있습니다.

더욱이 API와 프런트의 두 레이어에서 seleniumwire를 이용하여 보다 폭 넓은 자동화 테스트를 시도해 볼 수 있는 가능성을 보았던 것도 큰 성과였다 생각합니다.

글 읽어주셔서 고맙습니다. 🙇‍♂

--

--