[Basic] Selenium 기초(+tqdm)

고보·2024년 2월 14일

1 Selenium 개념과 기본 설정

1-1 설정 전 알고 있을 개념

  • Selnium: 브라우저 자동 제어해서, 웹 애플리케이션 테스팅 자동화하는 데 쓰는 프레임워크
  • webdriver: selenium의 핵심 요소로, 실제 브라우저 제어하는데 사용하는 드라이버. webdriver로 브라우저 열고, 페이지 로드하고, 요소 찾고, 클릭하고, 텍스트 입력하고 등 작업 수행.
    webdriver객체에 전달값들을 전달해서, 우리는 자동화된 브라우저를 제어하므로, 우리가 다루는 대상의 최종 목표 혹은 직접적 타겟.
  • Service: webdriver 서비스를 관리하는 클래스. Chrome으로 한다고 하면, ChromeDriver의 경로를 명시적으로 설정해서, webdriver 객체에 전달.
  • Options: webdriver 옵션을 관리하는 클래스. 헤드리스모드, 확장 프로그램 비활성화, 페이지 로드 전략 등 브라우저 동작 방식을 커스터마이징한다. 마찬가지로 webdriver 객체에 전달.
  • webdriver-manager: python 환경에서 Selenium webdriver들 쉽게 관리할 수 있도록 도와주는 유틸리티 라이브러리. 드라이버 다운로드, 설치, 관리를 자동화.
  • ChromeDriverManager: webdriver-manager 패키지의 일부로 시스템에 적합한 ChromeDriver버전을 자동으로 찾아 다운로드한다.
  • WebDriver 객체: webdriver.Chrome()으로 생기는 driver 객체
  • WebElement 객체: find_element, find_elements 등으로 반환되는 객체

1-2 기본 설정(복붙용)

#기본설정. 다음부터 그냥 이거 복사붙여넣기해서

#Anaconda에 크롬 드라이버 자동으로 다운받는 webdriver-manager 설치. 

# 관련 모듈들 import
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager

#브라우저 꺼짐 방지 
chrome_options = Options()
chrome_options.add_experimental_option("detach", True)

#불필요한 에러 메시지 없애기
chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"]) #셀레니움 로그 무시
  • 위의 코드는 총 4가지로 구성되어 있다.
    - 1 먼저 Anaconda로 이 컴퓨터에 webdriver-manager를 설치한다. pip install webdriver-manager
    • 2 관련 모듈들을 import한다.
      selenium에 있는 webdriver,
      selenium.webdriver.chrome에 있는 service와 options의 클래스
      Service, Options,
      webdriver_manager.chrome에 있는 ChromeDriverManager
  • 3 브라우저 꺼짐 방지는 웹 크롤링 중에 브라우저가 꺼지지 않도록, Options객체를 생성해서, webdriver의 동작 방식을 도중에 꺼지지 않게 한다. 여기선 Options 객체를 만들어놓고 밑에서 전달할 때 쓸 것.
    • Options(): 위에서 불러온 Options 클래스의 객체를 생성한다.
    • Options.add_experimental_option(): Options클래스의 메서드로, Options 객체에 옵션을 추가한다. 첫 번째 인자는 옵션 이름을 나타내는 문자열, 두 번째 인자는 그 옵션에 대한 값이다.
    • ('detach', True): 스크립트 실행 후에도 브라우저 창이 열린 상태로 유지된다.
  • 4 불필요한 에러 메세지 없애기: 마찬가지로 Options 객체에 설정값을 저장한다.
    ("excludeSwitches", ["enable-logging"]): 여기서 excludeSwitches는 전달되는 커맨드라인스위치를 제외시킨다. ['enable-logging']은, 로깅을 활성화하는 것이기 때문에, 이걸 제외시키므로 ChromeDriver가 기본적으로 활성화하는 로깅을 비활성화한다. 즉, 에러 메시지가 안뜨게 한다.

2 Selenium 시작 및 다양한 메서드

  • 뭔가 개념별로 체계적으로 정리가 되는 느낌이 아니어서, 실제 작업들을 해본 흐름별로 기능을 정리해본다.

2-1 webdriver 객체 생성

#Options 객체는 위의 설정에서 만들어놨다

#Service 객체 만들기
service = Service(executable_path=ChromeDriverManager().install())

#webdriver 객체 만들면서, 만들었던 Options, Service 객체 전달
driver = webdriver.Chrome(service=service, options=chrome_options)
  • 위의 코드를 설명하면
    • executable_path=: webdriver(여기선 ChromeDriver)의 파일 경로를 지정하는 매개변수. default는 None.
    • ChromeDriverManager().install(): ChromeDriverManager() 객체의 install 메서드. 이 메서드 호출하면, 적합한 ChromeDriver 찾아서 자동으로 다운로드하고, 설치 경로를 return한다. 즉, ChromeDriverManager 객체가 자동으로 경로 설정한 것을 명시한 것.
      디폴트는 None인데, 이 경우 Selenium이 PATH에 지정된 디렉토리 검색해서 chromedriver를 찾아간다. 즉, PATH 환경 변수에 chromedriver 위치 올바르게 추가되있으면, 명시하지 않아도 굴러간다. 여기서도 해보면 지정 안해도 잘 굴러감.
    • webdriver.Chrome(): 그냥 webdriver 모듈의 Chrome 클래스 객체를 생성하는 것. 여기서 service, options를 각자 매개변수로 받고, 위에서 만들어놓은 service, chrome_options를 집어넣는다.
  • 여기까지하면 자동화된 브라우저가 뜬다. 이제 driver에 메서드를 이용해서 명령을 입력하면, 자동화된 브라우저가 그를 실행한다.

2-2 원하는 사이트 이동~정보 접근~파싱까지

2-2-1 원하는 사이트 이동

  • driver.get('url'): webdriver 클래스 메서드. 자동화 브라우저가 해당 url로 이동
  • driver.current_url: webdriver 클래스의 속성. 현재 url return된다.
  • driver.close(): webdriver 클래스 메서드. 해당 브라우저 끈다. 부하 안걸리게 작업 끝나면 반드시 쓴다.

2-2-2 좀 디테일한 접근: send_keys, switch_to, click

  • WebElement.send_keys('문자열'): find_element 등으로 반환된 webelement 객체의 메서드로, send_keys로 타이핑한 글자를 칠 수 있다.
  • WebElement.click(): 클릭
  • driver.switch_to.~(): selenium webdriver에서 현재 작업 중인 웹 페이지 내의 특정 요소로 포커스를 전환할 수 있다.
    필요한 이유는, 웹 페이지에서 컨텐츠 분리하거나 외부 소스 포함하기 위해 태그를 사용하는데, 이런 프레임들은 독립된 문서로 취급되서 메인 페이지와 별개의 HTML 문서로 로드된다. 해당 프레임으로 포커스를 전환해야 접근할 수 있다.
    • switch_to.frame('frame_ex1'): <frame> 혹은 <iframe>태그에 포커스를 맞춘다.
    • switch_to.default_content(): 최상위 레벨 문서로 포커스를 되돌린다. 즉, 기존의 페이지로 포커스를 다시 바꾼다.
    • switch_to.parent_frame(): 현재 프레임에서 부모 프레임으로.
    • switch_to.alert: 웹 페이지에 발생한 알림, 확인, 프롬프트 대화 상자와 상호작용하기 위한 메서드.
    • switch_to.active_element: 현재 활성화된 요소의 참조를 반환. 예를 들어, 사용자가 텍스트 필드에 입력을 시작했을 때, 해당 텍스트 필드 요소를 찾는데 사용 가능.
    • switch_to.window: 지정된 윈도우나 탭으로 포커스 전환.
    • 여기서 전달하는 방법은 총 3가지
      • 1 위의 'frame_ex1'처럼 문자열로 name이나 id를 전달. 위의 경우 <frame name='frame_ex1'> 처럼 되어 있을 것.
      • 2 정수로 전달. 그럼 인덱스로 적용된다. 0으로 하면 첫 번째 frame으로 이동.
      • 3 driver.find_element 등으로 webElement 객체 직접 전달 가능. 예를 들면 driver.find_element(By.ID, 'frame_ex1')로 프레임요소 찾은 후, 이 요소를 switch_to.frame()의 인자에 넣으면 같은 결과.
  • 예시: 네이버 검색창에 방탄소년단 검색
driver.get('https://www.naver.com')
#개발자 도구로 네이버 메인 화명에서 검색창 HTML을 보면 ID가 query다
input_tag = driver.find_element(By.CSS_SELECTOR, '#query')
input_tag.send_keys('방탄소년단')
#엔터 치는 작업
input_tag.send_keys('\n')
driver.implicitly_wait(3)
  • driver.implicitly_wait: 요소를 찾기 위해 최대 3초간 webdriver가 대기하도록 설정. 이 시간동안 요소가 나타나면 다음 코드로 진행, 못찾으면 NoSuchElementException을 발생

2-2-3 파싱(BeautifulSoup 이용)

  • driver.page_source: webdriver가 로드한 웹 페이지의 전체 HTML 소스를 문자열 형태로 반환하는 속성.
  • BeautifulSoup(html, 'html.parser'): html 문자열 객체를 받아서, BeautifulSoup 객체로 바꾼다. 이를 파싱이라 한다. BeautifulSoup 객체는 Python의 복잡한 HTML/XML 문서를 파싱하고, 그걸 탐색하기 위한 다양한 메서드랑 속성을 제공한다.
  • html.parser은 BeautifulSoup에서 사용되는 파싱 방식을 지정하는 인자. html.parser은 Python 표준 라이브러리에 내장된 HTML 파서. 보다 빠른 파싱이나 복잡한 HTML 문서 처리하려면, lxml, html5lib 같은 외부 파서 선택할 수도 있다.
  • 파싱(Parsing): 주어진 데이터(파일, 문자열 등)을 분석하여 그 구조를 이해하고, 필요한 정보를 추출하거나 다른 형식으로 변환하는 과정. 특히 텍스트 데이터를 처리한다.
  • 파서(Parser): 파싱을 하는 기계. 컴퓨터 과학 및 프로그래밍에서 사용되는 개념으로, 특정 형식의 데이터를 읽고 해석하는 프로그램이나 모듈을 가리킨다. 파서는 일련의 입력 데이터를 가져와서 문법적으로 해석하고 이를 프로그램이나 다른 형태의 데이터로 변환한다.
  • 아래는 이를 이용한 일련의 흐름
#위에 설정 다 마쳤다 가정하고 driver 객체 생성했다
service = Service(executable_path=ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)

# 원하는 페이지 url 저장해서 이동
url = 'https://finance.naver.com/marketindex/'
driver.get(url)

#파싱
from bs4 import BeautifulSoup
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')

#종료
driver.close()

2-3 정보 찾기

2-3-1 BeautifulSoup의 메서드: select, find 이용

  • .find(): BeautifulSoup 객체, Tag 객체에 사용 가능. 특정 조건에 맞는 단일 요소를 찾아서, 첫 번째로 찾아진 요소를 반환.
  • .select(선택자) : BeautifulSoup 객체, Tag 객체에 사용 가능. CSS 선택자를 매개변수로 받아 해당 선택자와 일치하는 모든 요소를 찾아 리스트 형태로 반환. 타입은 bs4.element.ResultSet인데, 리스트처럼 작용한다. 안의 요소들의 타입은 bs4.element.Tag.
    아래 예시는, body 안의 p 요소를 모두 찾아 리스트로 반환했다. for문으로 리스트를 돌리고, tag.text로 해당 p 요소의 text를 하나씩 print.
tag_list = soup.select('body p')
for tag in tag_list:
    print(tag.text)
driver.close()
  • 네이버 금융 페이지에서, 환율 뽑아와서, 저장
    다 위에 나와있는 기능으로 한 것.
    to_csv에서 encoding은 파일 문자 인코딩 지정하는데, cp949는 한글 windows환경에서 사용되는 인코딩. header=True는 첫 번째 행에 열 이름을 헤더로 포함. 즉, 생성된 CSV 파일 첫 번째 행이 '통화면' '환율'. index=False는 인덱스를 파일에 포함시키지 않음.
#네이버 금융 웹 페이지로 이동하기
driver = webdriver.Chrome(options=chrome_options)
url = 'https://finance.naver.com/marketindex/'
driver.get(url)

#환율 정보 프레임으로 변환
driver.switch_to.frame('frame_ex1')

#파싱
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')

#저장할 빈 리스트 선언
result = []

#select로 접근
currencys = soup.select('body > div > table > tbody > tr')

#select로 뽑은 내용 추출
for data in currencys:
    country = data.select('td.tit > a')[0].text.strip()
    exchange = data.select('td.sale')[0].text
    result.append([country, exchange])
    
#pandas로 csv로
import pandas as pd
df = pd.DataFrame(result, columns = ['통화명', '환율'])
df.to_csv('../data/환율정보.csv', encoding = 'cp949', header = True, index=False)

2-3-2 Selenium의 메서드(find_element, find_elements, get_attribute) + By 이용

  • from selenium.webdriver.common.by import By
  • 웹 페이지의 요소를 찾는 데 사용되는 다양한 방법(또는 전략)을 제공하는 By 클래스를 임포트. By 클래스는 요소를 식별하는 데 사용하는 유형을 제공한다.
    • By.ID: 요소의 ID 속성을 사용하여 요소를 찾는다.
    • By.XPATH: XPath 표현식을 사용하여 요소를 찾는다.
    • By.LINK_TEXT: 완전한 링크 텍스트를 사용하여 링크 요소를 찾는다.
    • By.PARTIAL_LINK_TEXT: 링크 텍스트의 일부를 사용하여 링크 요소를 찾는다.
    • By.NAME: 요소의 name 속성을 사용하여 요소를 찾는다.
    • By.TAG_NAME: 요소의 태그 이름(예: div, a 등)을 사용하여 요소를 찾는다.
    • By.CLASS_NAME: 요소의 클래스 이름을 사용하여 요소를 찾는다.
    • By.CSS_SELECTOR: CSS 선택자를 사용하여 요소를 찾는다.
      • driver.find_element(By.CSS_SELECTOR, '#query') 이것은 사실상 By.ID, header와 같은 뜻. #가 ID, .이 class 나타내므로. 다양하게 접근할 수 있어서 별도로 있는 듯?
  • find_element(선택자 지정, '문자열'): webdriver혹은 webelement에 사용 가능한 메서드로, webelement를 반환한다. 지정한 선택자에 해당하는 첫 번째 요소를 찾아 반환. 일치 요소 없으면 NoSuchElementException 예외 발생.
  • find_elements(선택자 지정, '문자열'): webdriver혹은 webelement에 사용 가능한 메서드로, webelement로 구성된 리스트를 반환한다. 지정한 선택자에 해당하는 첫 번째 요소를 찾아 반환. 없으면 빈 리스트 반환.
  • .get_attribute(): webelement 클레스에 대해 사용. HTML의 특정 요소에 설정된 속성값을 조회하는 메서드. string으로 반환

3 실제 예시

3-1 실제 예시: 서울시 구별 주유소 가격 정보를 자동 다운로드하기

#드라이버 객체 생성 및 실행
driver = webdriver.Chrome(service=service, options=chrome_options)

#구별 주유소 가격 사이트로 이동
driver.get('https://www.opinet.co.kr/searRgSelect.do')

#By 모듈 임포트
from selenium.webdriver.common.by import By

#개발자도구에서 원하는 부분 우클릭 - copy sellector하니까 SIDO_NM0
sido_list_raw = driver.find_element(By.ID, 'SIDO_NM0')

#마찬가지로 <option value='서울특별시'>로 되어 있었음. 
sido_list = sido_list_raw.find_elements(By.TAG_NAME, 'option')
#option 중 [0]은 쓸데없는 정보 들어있어서 1로
sido_list[1].get_attribute('value') #서울특별시
  • 모듈 임포트까지는 생략하고, 그 이후부터 보면
  • SIDO_NM0는 개발자도구로 추출한 정보. ID='SIDO-NM0'이었기 때문에 By.ID를 이용. 그리고 여기서 By.ID로 이미 ID라고 지정했기 때문에, 앞의 #는 생략한다.
  • find_element(선택자 지정, '문자열'): webdriver혹은 webelement에 사용 가능한 메서드로, webelement를 반환한다. 지정한 선택자에 해당하는 첫 번째 요소를 찾아 반환. 일치 요소 없으면 NoSuchElementException 예외 발생.
  • find_elements(선택자 지정, '문자열'): webdriver혹은 webelement에 사용 가능한 메서드로, webelement로 구성된 리스트를 반환한다. 지정한 선택자에 해당하는 첫 번째 요소를 찾아 반환. 없으면 빈 리스트 반환.
  • .get_attribute(): webelement 클레스에 대해 사용. HTML의 특정 요소에 설정된 속성값을 조회하는 메서드. string으로 반환.
  • ID가 SIDO_NMO인 첫 번째 요소의 option이라는 tag이름이 option인 걸 모두 반환.
    => 그 중 첫 번째 webelement 객체를 꺼내서, get_attribute('value')로 value인 속성을 꺼내 string으로 반환.
    <option value='서울특별시'>로 되어 있었던 게, '서울특별시'가 나옴.
#시, 도 정보 들고오기
sido_names = [option.get_attribute('value') for option in sido_list]
sido_names = sido_names[1:]

# 시, 군, 구 정보 
gu_list_raw = driver.find_element(By.ID, 'SIGUNGU_NM0')
gu_list = gu_list_raw.find_elements(By.TAG_NAME, 'option')

gu_names = [option.get_attribute('value') for option in gu_list]
gu_names.remove('')

  • 위에서 1개만 뽑아서 된다고 확인을 했으니, 리스트로 뽑는다.
  • 시, 도 정보 말고, 시, 군, 구도 원리상 그냥 똑같다. remove('')는 첫 행이 ''이어서 저걸로 제거한 것. 위에서는 1부터 슬라이싱한 거고.
  • 다음은 홈페이지에서 구별 엑셀 파일 다운을, 자동 다운로드
    • 먼저 anaconda에 pip install tqdm
import time
from tqdm import tqdm_notebook
for gu in tqdm_notebook(gu_names):
	element = driver.find_element(By.ID, 'SIGUNGU_NM0')
    element.send_keys(gu)
    time.sleep(3)
    element_get_excel = driver.find_element(By.ID, 'glopopd_excel').click()
    time.sleep(2)




  • tqdm은 반복 작업의 진행 상황을 시각적으로 표시하는 모듈. tqdm_notebook은 주피터 노트북 환경에 최적화된 진행률 표시바. 아래에 진행률 표시바가 뜸.
  • gu_names는 위에서 만든 시군구 이름 리스트.
  • tqdm_notebook()에는 iterable이 기본적으로 들어가고, 그게 진행되는 정도를 표시해준다. desc, total, leave, file, ncols, mininterval 등의 매개변수를 추가로 설정할 수 있다.
  • driver.find_element(By.ID, 'SIGUNGU_NM0')로 웹에서 요소를 찾아서 webElement 객체로 반환.
  • element.send_keys(gu)로 찾은 요소에 gu_names에서 받은 gu를 하나씩 입력.
  • .click()는 클릭 이벤트를 발생시킨다. 즉, driver.find_element().click()에서 엑셀 다운을 클릭한다.
  • time.sleep(3)은 3초간 일시 중지. 웹 페이지 입력 처리하고 다음 작업 준비하는데 필요한 시간 만들려고.
  • 그렇게 만든 걸 DF로 만들고, 시각화나 저장
import pandas as pd
from glob import glob

#파일 이름이 지역_위치별~
stations_files = glob('../data/지역_위치별*xls')

#파일 이름 들고온 걸 리스트로 돌면서, read_excel에 넣는다
#header=2는 3번째 행을 열 이름으로 사용하고, 1, 2행은 무시
#그리고 그 각각의 리스트를 묶어서 리스트에 다시 넣는다
tmp_raw = []
for file_name in stations_files:
    tmp = pd.read_excel(file_name, header=2)
    tmp_raw.append(tmp)

#concat하면 각각 리스트로 묶인 걸 하나로
station_raw = pd.concat(tmp_raw)

#필요한 행만 뽑아서 데이터프레임으로
stations = pd.DataFrame({'Oil_store':station_raw['상호'], 
                        '주소':station_raw['주소'],
                        '가격':station_raw['휘발유'], 
                        '셀프':station_raw['셀프여부'], 
                        '상표':station_raw['상표']})

#내부 확인하고 오류값들 정제
#정제, 시각화
  • 전체 흐름을 다시 정리하면
  • 1 드라이버 객체 생성하고 해당 홈페이지 이동 => 2 개발자도구로 원하는 부분의 ID, TAG_NAME 등을 파악 => 3 find_element, find_elements, select, get_attribute 메서드 등을 이용해 원하는 데이터에 접근. 특히 여러 겹으로 둘러쌓여있는 경우 인덱싱, for문 등으로 원하는 정보 접근. => 4 원하는 정보인 구 리스트를 추출 => 5 사이트에서 구 선택창 접근 후 send_keys와 for문으로, 추출한 구 리스트를 자동 입력 + excel파일 다운창에 접근하고 .click()으로 클릭하도록 => 6 다운 받은 걸, glob로 들고와서, 하나씩.

3-2 실제 예시2: 특정 사이트의 테이블 들고 오기

service = Service(executable_path=ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)

driver.get('https://www.kopia.or.kr/info/statistics.php')
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')

#head 들고와서 칼럼명 리스트 만들기
th_raw = soup.select('tbody th')
col = []
for i in th_raw:
    col.append(i.text)

#행별로 들고와서 추출
tr_raw = soup.select('tbody tr')
tr = []
for i in tr_raw:
    td = i.select('td')
    temp = []
    for j in td:
        temp.append(j.text)
    tr.append(temp)
#빈행 자르기
tr = tr[1:]

#pandas로 데이터프레임화 => 저장
import pandas as pd
df = pd.DataFrame(tr, columns=col)
df.to_excel('../data/수주실적.xlsx', index=False)

3-3 3-2 훨씬 빠르게 해결하기

import pandas as pd
html = 'http://kopia.or.kr/info/statistics.php'
#pandas에서 read_html이라고, html에서 table만 찾아서 제공하는 기능. 
#2개 이상이면 0, 1 이렇게 나온다
df1 = pd.read_html(html)
df1[0].to_excel('../data/kopia.xlsx')
df1[0]
  • pandas에서 read_html하면, html에서 table만 찾아서 제공한다. 2개 이상이면 0, 1 이렇게 나와서 [0]으로 인덱싱

3 기타 기능

  • driver.save_screenshot('../images/001.png'): selenium 브라우저 스크린샷으로 저장
  • driver.implicitly_wait(3): 요소를 찾기 위해 최대 3초간 webdriver가 대기하도록 설정. 이 시간동안 요소가 나타나면 다음 코드로 진행, 못찾으면 NoSuchElementException을 발생
profile
일본에서 일하는 게임 기획자. 시시해서 죽어버리지 않게, 재밌고 의미 있는 컨텐츠에 관심 있습니다. 그 도구로 데이터, AI도 찝적댑니다.

0개의 댓글