KevinFire2030 / Fire2025

0 stars 0 forks source link

9장 동적 크롤링과 정규 표현식 #21

Open KevinFire2030 opened 1 year ago

KevinFire2030 commented 1 year ago

이번 장에서는 좀 더 복잡한 형태의 데이터를 크롤링하기 위한 동적 크롤링 및 정규 표현식의 사용방법에 대해 알아보도록 하겠다.

KevinFire2030 commented 1 year ago

9.1 동적 크롤링이란?

지난 장에서 크롤링을 통해 웹사이트의 데이터를 수집하는 방법에 대해 배웠다. 그러나 일반적인 크롤링으로는 정적 데이터, 즉 변하지 않는 데이터만을 수집할 수 있다. 한 페이지 안에서 원하는 정보가 모두 드러나는 것을 정적 데이터라 한다. * 반면 입력, 클릭, 로그인 등을 통해 데이터가 바뀌는 것을 동적 데이터라 한다. 예를 들어 네이버 지도에서 매장을 검색을 한 후 좌측에서 원하는 선택할 때 마다 이에 해당하는 내용이 뜬다.

이는 웹페이지에서 사용자가 클릭 등과 같은 조작을 하면 AJAX 호출이 발생하여 그 결과가 페이지의 일부분에만 반영되어 변경되기 때문이다. 즉 매장을 클릭하면 웹브라우저가 연결된 자바스크립트 코드를 실행하여 해당 매장의 상세 정보가 동일한 페이지에 동적으로 표시된다.

구분 정적 크롤링 동적 크롤링
사용 패키지 requests selenium
수집 커버리지 정적 페이지 정적/동적 페이지
수집 속도 빠름 (별도 페이지 조작 필요 X) 상대적으로 느림
파싱 패키지 beautifulsoup beautifulsoup / selenium

셀레니움이란 다양한 브라우저(인터넷 익스플로러, 크롬, 사파리 오페라 등) 및 플랫폼에서 웹 응용 프로그램을 테스트할 수 있게 해주는 라이브러리다. 즉 웹 자동화 테스트 용도로 개발이 되었기에 실제 브라우저를 사용하며, 페이지가 변화하는 것도 관찰이 가능하기에 동적 크롤링에 사용할 수 있다.

9.1.1 셀레니움 실습하기


(py39_32) D:\Fire2025\23W23>pip install selenium
(py39_32) D:\Fire2025\23W23>pip install webdriver_manager

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

webdriver.Chrome(service=Service(ChromeDriverManager().install())) 코드를 실행하면 크롬 브라우저의 버전을 탐색한 다음, 버전에 맞는 웹드라이버를 다운로드하여 해당 경로를 셀레니움에 전달해준다. 또한 {numref}selenium_open와 같이 크롬 창이 열리며, 좌측 상단에 'Chrome이 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다.'라는 문구가 뜬다. 이제 파이썬 코드를 이용해 해당 페이지를 조작할 수 있다.


url = 'https://www.naver.com/'
driver.get(url)
# driver.page_source[1:1000]

driver.find_element(By.LINK_TEXT , value = '뉴스').click()

driver.get() 내에 URL 주소를 입력하면 해당 주소로 이동한다. 또한 driver.page_source를 통해 열려있는 창의 HTML 코드를 확인할 수도 있다. 이제 네이버 메인에서 [뉴스]버튼을 누르는 동작을 실행해보자. 개발자도구 화면을 통해 확인해보면 [뉴스] 탭은 아래 HTML에 위치하고 있다.

뉴스 위 정보를 통해 해당 부분을 클릭해보도록 하자.

브라우저 상에서 보이는 버튼, 검색창, 사진, 테이블, 동영상 등을 엘레먼트(element, 요소)라고 한다. find_element()는 다양한 방법으로 엘레먼트에 접근하게 해주며, By.* 를 통해 어떠한 방법으로 엘레먼트에 접근할지 선언한다. LINK_TEXT의 경우 링크가 달려 있는 텍스트로 접근하며, value = '뉴스', 즉 뉴스라는 단어가 있는 엘레먼트로 접근한다. click() 함수는 마우스 클릭을 실행하며 결과 적으로 뉴스 탭을 클릭한 후 페이지가 이동되는 것을 확인할 수 있다. find_element() 내 접근방법 및 셀레니움의 각종 동작 제어 방법에 대해서는 나중에 다시 정리하도록 한다.

driver.back()

back()은 뒤로가기를 의미하며, 기존 페이지인 네이버 메인으로 이동한다.

image


driver.find_element(By.CLASS_NAME, value = 'search_input').send_keys('퀀트 투자 포트폴리오 만들기')

driver.find_element(By.CLASS_NAME, value = 'btn_search').send_keys(Keys.ENTER)

find_element() 내에 By.CLASS_NAME을 입력하면 클래스 명에 해당하는 엘레먼트에 접근하며, 여기서는 검색창에 접근한다. 그 후 send_keys() 내에 텍스트를 입력하면 해당 내용이 웹페이지에 입력된다. 이제 웹페이지에서 검색 버튼 해당하는 돋보기 모양을 클릭하거나 엔터키를 누르면 검색이 실행된다. 먼저 돋보기 모양의 위치를 확인해보면 search_btn id와 btn_submit 클래스에 위치하고 있다.

find_element(By.CLASS_NAME, value = 'btn_search')를 통해 검색 버튼에 접근한다. 그 후 send_keys(Keys.ENTER)를 입력하면 엔터키를 누르는 동작이 실행된다. 페이지를 확인해보면 검색이 실행된 후 결과를 확인할 수 있다.

이번에는 다른 단어를 검색해보도록 하자. 웹에서 기존 검색어 내용을 지운 후, 검색어를 입력하고, 버튼을 클릭해야 한다. 이를 위해 검색어 박스와 검색 버튼의 위치를 찾아보면 다음과 같다.

image


driver.find_element(By.CLASS_NAME, value = 'box_window').clear()
driver.find_element(By.CLASS_NAME, value = 'box_window').send_keys('이현열 퀀트')
driver.find_element(By.CLASS_NAME, value = 'bt_search').click()
  1. 검색어 박스(box_window)에 접근한 후, clear()를 실행하면 모든 텍스트가 지워진다.
  2. send_keys('이현열 퀀트')를 실행하여 새로운 검색어를 입력한다.
  3. 검색 버튼(bt_search)에 접근한 후, click()을 실행하여 해당 버튼을 클릭한다.

이번에는 [VIEW] 버튼을 클릭하는 동작을 실행해보도록 한다. 기존처럼 링크나 클래스명을 통해 엘레먼트에 접근할 수도 있지만, 이번에는 XPATH를 이용해 접근해보도록 하자. XPATH란 XML 중 특정 값의 태그나 속성을 찾기 쉽게 만든 주소다. 예를 들어 윈도우 탐색기에서는 특정 폴더의 위치가 'C:\Program Files'과 같이 주소처럼 보이며 이는 윈도우의 PATH 문법이다. XML 역시 이와 동일한 개념의 XPATH가 있다. 웹페이지에서 XPATH를 찾는 법은 다음과 같다.

개발자도구 화면에서 위치를 찾고 싶은 부분에서 마우스 우클릭을 한다. [Copy → Copy Xpath]를 선택한다.

XPATH 찾기 및 복사하기 위 과정을 통해 XPATH가 복사된다. 메모장을 확인해보면 VEW 부분의 XPATH는 다음과 같다.

//*[@id="lnb"]/div[1]/div/ul/li[2]/a 이를 이용해 해당 부분을 클릭하는 동작을 실행해보자.


driver.find_element(By.XPATH, value = '//*[@id="lnb"]/div[1]/div/ul/li[2]/a').click()

탭이 [통합] 검색이 아닌 [VIEW]로 변경되었다. 이번에는 [옵션]을 클릭한 후 정렬을 [최신순]으로 하는 동작을 실행해보자. 둘의 위치는 다음과 같다.

image

//*[@id="snb"]/div[2]/ul/li[2]/div/div/a[2]


driver.find_element(By.CLASS_NAME, value = 'option_filter').click()
driver.find_element(By.XPATH, value = '//*[@id="snb"]/div[2]/ul/li[2]/div/div/a[2]').click()

옵션 클릭 후 최신순 버튼을 클릭하는 동작을 수행하여 검색어가 최신순으로 정렬되었다. 이제 page down 기능을 수행해보도록 하자.

driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
# driver.find_element(By.TAG_NAME, value = 'body').send_keys(Keys.PAGE_DOWN)

먼저 document.body.scrollHeight는 웹페이지의 높이를 나타내는 것으로써, window.scrollTo(0, document.body.scrollHeight);는 웹페이지의 가장 하단까지 스크롤을 내리라는 자바스크립트 명령어다. driver.execute_script()를 통해 해당 명령어를 실행하면 웹페이지가 아래로 스크롤이 이동된다. send_keys(Keys.PAGE_DOWN) 는 키보드의 페이지다운(PgDn) 버튼을 누르는 동작이며 이 역시 페이지가 아래로 이동시킨다.

그러나 결과를 살펴보면 스크롤이 끝까지 내려간 후 얼마간의 로딩이 있은 후에 새로운 데이터가 생성된다. 이처럼 유튜브나 인스타그램, 페이스북 등 많은 검색결과를 보여줘야 하는 경우 웹페이지 상에서 한 번에 모든 데이터를 보여주기 보다는 스크롤을 가장 아래로 위치하면 로딩을 거쳐 추가적인 결과를 보여준다. 따라서 스크롤을 한 번만 내리는 것이 아닌 모든 결과가 나올 때까지 내리는 동작을 실행해야 한다.

prev_height = driver.execute_script('return document.body.scrollHeight')

while True:
    driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
    time.sleep(2)

    curr_height = driver.execute_script('return document.body.scrollHeight')
    if curr_height == prev_height:
        break
    prev_height = curr_height
  1. return document.body.scrollHeight은 현재의 창 높이는 반환하는 자바스크립트 명령어이며, 이를 prev_height에 저장한다.
  2. while문을 통해 반복문을 실행한다.
  3. 셀레니움을 통해 페이지의 최하단으로 스크롤을 내린다.
  4. 페이지가 로딩되는 시간을 기다리기 위해 2초간 슬립을 준다.
  5. curr_height에 현재 창 높이를 저장한다.
  6. curr_height와 prev_height가 동일하다는 의미는 페이지가 끝까지 내려왔다는 의미이다. 따라서 이 경우 break를 통해 while문을 멈추며, 그렇지 않을 경우 다시 스크롤을 내리는 동작을 반복한다.
  7. prev_height에 새로운 창 높이를 입력한다.

이제 모든 검색 결과가 나타났으면 이전 장에서 살펴보았던 정적 크롤링을 통해 데이터 수집이 가능하다. 제목 부분을 확인해보면 api_txt_lines total_tit _cross_trigger 클래스에 위치하고 있으며, 이를 통해 모든 제목을 크롤링해보자.

image

html = BeautifulSoup(driver.page_source, 'lxml')
txt = html.find_all(class_ = 'api_txt_lines total_tit _cross_trigger')
txt_list = [i.get_text() for i in txt]

txt_list[0:10]
  1. driver.page_source를 통해 현재 웹페이지의 HTML 정보를 가져올 수 있으며, 이를 BeautifulSoup 객체로 만들어준다.
  2. find_all() 함수를 통해 제목 부분에 위치하는 데이터를 모두 불러온다.
  3. for문을 통해 텍스트만 추출한다.

driver.quit()

driver.quit()을 실행하면 열려있던 페이지가 종료된다.

9.1.2 셀레니움 명령어 정리

마지막으로 셀레니움의 각종 명령어는 다음과 같다.

9.1.2.1 브라우저 관련

9.1.2.2 엘레먼트 접근

driver.find_element(by = 'id', value = 'value') 중 by = 'id' 부분에 해당하는 방법에 따라 엘레먼트에 접근한다. 또한 find_element()는 해당하는 엘레먼트가 여러 개 있을 경우 첫 번째 요소 하나만을 반환하며, find_elements()는 여러 엘레먼트가 있을 경우 리스트로 반환한다.

9.1.2.3 동작

엘레먼트에 접근한 후 각종 동작을 수행할 수 있다.

9.1.2.4 자바스크립트 코드 실행

execute_script() 내에 자바스크립트 코드를 입력하여 여러가지 동작을 수행할 수 있다.

{note} 파이썬 내 셀레니움은 아래 페이지에 상세하게 설명되어 있다.

https://selenium-python.readthedocs.io/

KevinFire2030 commented 1 year ago

9.2 정규 표현식

정규 표현식(정규식)이란 프로그래밍에서 문자열을 다룰 때 문자열의 일정한 패턴을 표현하는 일종의 형식 언어를 말하며, 영어로는 regular expression를 줄여 일반적으로 regex라 표현한다. 정규 표현식은 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 프로그래밍에서 사용되는 공통 문법이기에 한 번 알아두면 파이썬 뿐만 아니라 다른 언어에서도 쉽게 적용할 수 있다. 본 책의 내용은 아래 페이지의 내용을 참고하여 작성되었다.

https://docs.python.org/3.10/howto/regex.html

9.2.1 정규 표현식을 알아야 하는 이유

만약 우리가 크롤링한 결과물이 다음과 같다고 하자.

"동 기업의 매출액은 전년 대비 29.2% 늘어났습니다."

만일 이 중에서 [29.2%]에 해당하는 데이터만 추출하려면 어떻게 해야 할까? 얼핏 보기에도 꽤나 복잡한 방법을 통해 클렌징을 해야 한다. 그러나 정규 표현식을 이용할 경우 이는 매우 간단한 작업이다.


import re

data = '동 기업의 매출액은 전년 대비 29.2% 늘어났습니다.'
re.findall('\d+.\d+%', data)

['29.2%']

'\d+.\d+%'라는 표현식은 '숫자.숫자%'의 형태를 나타내는 정규 표현식이며, re 모듈의 findall() 함수를 통해 텍스트에서 해당 표현식의 글자를 추출할 수 있다. 이제 정규 표현식의 종류에는 어떠한 것들이 있는지 알아보도록 하자.

9.2.2 메타문자

프로그래밍에서 메타 문자(Meta Characters)란 문자가 가진 원래의 의미가 아닌 특별한 용도로 사용되는 문자를 말한다. 정규 표현식에서 사용되는 메타 문자는 다음과 같다.

정규 표현식에 메타 문자를 사용하면 특별한 기능을 갖는다.

9.2.2.1

9.2.3 정규식을 이용한 문자열 검색

9.2.4 정규 표현식 연습해 보기