Xpath
- XML 문서의 특정 요소나 속성에 접근하기 위한 경로
- HTML 문서에서 특정 요소 찾기 쉽게 문법 제공
re 모듈 - 정규식 표현
import re
p = re.compile("ca.e")
# . 는 문자 하나를 의미 > care, cafe, case
# ^ 는 문자열 시작 의미 > ^de : desk, destination
# $ 는 문자열의 끝 의미 > se$ : case, base
m = p.match("case")
if m:
print(m.group()) # case 출력됨, 주어진 문자열에 처음부터 일치하는지 확인하기에 caseless도 일치라고 봄
else:
print("매칭되지 않음")
requests vs. selenium
- requests는 정적 웹페이지에 사용, 속도 빠름, 이미지 파일 데이터 같은 raw data도 가져올 수 있음
- selenium은 동적 웹페이지에 사용됨, 속도 느리지만 자동화 등 다양한 기능 사용 가능
beautifulsoup 사용법
pip install beautifulsoup4 # 스캘핑 용도
pip install lxml # 구문 분석
필요 모듈들 설치
soup = BeautifulSoup(res.text, "lxml")
lxml 파서를 이용해 res.text 를 변환함
print(soup.title) # title 가져옴
print(soup.title.get_text()) # title 내부의 text만
print(soup.div) # 문서에서 처음 발견되는 div 태그
print(soup.div.attrs) # dictionary 형태로 가져옴 ex) {'id': 'root'}
print(soup.div['class']) # 이렇게 속성 값 가져올 수 있음
print(soup.find("div", attrs={"class": "u_skip"})) // 이렇게 조건 만족하는 첫번재 element 가져오게 할 수 있음
ul = soup.find("ul", attrs={"class": "WeekdayMainView__daily_list--R52q0"}); print(ul.a)
r1 = soup.find("li", attrs={"class": "TripleRecommendList__item--Uc4sT"})
r2 = r1.next_sibling.next_sibling; print(r1.find("span", attrs={"class":"text"}))
next_sibling 통해 다음 같은 depth의 element로 넘어갈 수 있음
.previous_sibling 도 가능 (가끔 중간에 개행 껴서 next_sibling.next_sibling 이렇게 두 번 넘어가야 될 수도 있음)
.parent 통해 부모로 갈 수도 있음
r1.find_next_sibling("li") 이렇게 sibling을 찾는데 조건을 줄 수도 있음
find_all 하면 조건 맞는 것들 전부 리스트로 반환
cartoons = soup.find_all("a", attrs={"class":"EpisodeListList__link--DdClU"})
for cartoon in cartoons:
print(cartoon.span.get_text())
print("https://comic.naver.com"+cartoon['href'])
https://www.crummy.com/software/BeautifulSoup/bs4/doc.ko/
requests 이용해 웹사이트 이미지 다운로드하기
- requests 라이브러리 활용, res.content로 가져오고 "wb" 바이너리 모드로 저장하면 됨
imgs = soup.find_all("img", attrs={"class": "thumb_img"})
for idx, img in enumerate(imgs):
img_url = imgs[idx]["src"]
if img_url.startswith("data:image/"):
img_url = imgs[idx]["data-original-src"]
img_res = requests.get(img_url)
with open(f"웹스크래핑/movies/movie{idx+1}.webp", "wb") as f:
f.write(img_res.content)
Selenium 사용법
- chrome driver 설치해서, 적당한 경로에 넣어주고 webdriver.Chrome("./chromedriver.exe") 이런 식으로 해줘야 함
- webdriver_manager 라이브러리를 사용해 자동으로 적합한 드라이버 설치하고 설정할 수 있음
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
>>> driver.get("https://www.naver.com/") 이러면 naver로 이동하고
>>> elem = driver.find_element("class name", "MyView-module__link_login___HpHMW") 이러면 로그인 버튼 찾고
>>> elem.click() 이러면 로그인 버튼 클릭해서 로그인 페이지로 이동하고
>>> driver.find_element("id", "query") 검색창 검색해버리고
<seleniuhttp://m.webdriver.remote.webelement.WebElement (session="0f5ef1bb3778ce94fbedfb4c64e92656", element="B8F1B5436BCA0F440E6FF273573DEB2A_element_6765")>
>>> driver.find_element("id", "query").send_keys("김민서") 이렇게 검색어 입력해버리고
>>> from seleniuhttp://m.webdriver.common.keys import Keys
>>> driver.find_element("id", "query").send_keys(Keys.ENTER) 엔터 입력
Selenium 4 부터는 find_element_by_* 문법이 전부 사라졌음. 아래와 같이 바뀜
driver.find_element(By.CLASS_NAME, "")
driver.find_element(By.ID, "")
driver.find_element(By.CSS_SELECTOR, "")
driver.find_element(By.NAME, "")
driver.find_element(By.TAG_NAME, "")
driver.find_element(By.XPATH, "")
driver.find_element(By.LINK_TEXT, "")
driver.find_element(By.PARTIAL_LINK_TEXT, "")
browser.close() => 현재 탭만 닫는거고
browser.quit() => 전부 종료
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".ContentTitle__title_area--x24vt"))
)
- expected condition까지 최대 10초 대기시킴
options = webdriver.ChromeOptions()
options.headless = True
driver = webdriver.Chrome(service=service, options=options)
- 이런 식으로 headless option을 주면 백그라운드에서 크롬 엔진이 실행됨
driver.get_screenshot_as_file("test.png")
- 이렇게 별도 스샷으로 저장해도 됨
Troubleshoot 1 - requests.get()으로 안가져와지면?
- 헤더에 "User-Agent"를 추가해주면 됨
- 구글에 user agent string 검색 (브라우져 따라, 모바일/pc 따라 달라짐)
import requests
url = "https://cse.snu.ac.kr/department-notices?c%5B%5D=1&keys="
headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"}
res = requests.get(url, headers=headers)
Troubleshoot 2 - 동적 웹페이지이고, 스크롤 필요한 웹페이지라면 (feat. 네이버 웹툰)
- 네이버 웹툰 페이지(https://comic.naver.com/webtoon)인데, requests로 가져오니 html 뼈대만 존재
- 동적으로 js 실행되면서 웹페이지 내용이 채워짐, selenium으로 단순히 가져와도 동적 로드되지 않음
driver.get(url)
driver.execute_script("window.scrollTo(0, 0);")
- 이런 의미없는 js 코드 실행시키니까 동적으로 로드 잘 됨
- 하지만 스크롤 가능한 모든 페이지들의 콘텐츠들이 로드되지는 않았음
TYPE1. 무한로딩 페이지라면
SCROLL_PAUSE_TIME = 0.5
# Get scroll height
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
# Scroll down to bottom
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Wait to load page
time.sleep(SCROLL_PAUSE_TIME)
# Calculate new scroll height and compare with last scroll height
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
- 페이스북 같이 스크롤 내렸을 때 페이지의 전체 높이가 동적으로 변하는 등 무한 스크롤이 있는 페이지에서 용이함
- document.body.scrollHeight 구한 다음 페이지 전체 높이까지 한 번에 스크롤
- last height과의 높이를 비교하여 추가 콘텐츠 로드 확인, 추가 내용 없다면 루프 중단 (마지막 페이지라는 뜻이므로)
TYPE2. Lazy loaded 콘텐츠들이 존재하는 스크롤 가능 페이지라면
screen_height = driver.execute_script("return window.innerHeight;") # 브라우저 창의 높이
i = 1
while True:
# 페이지 높이를 기반으로 스크린 높이만큼 스크롤
driver.execute_script(f"window.scrollTo(0, {screen_height*i});")
i += 1
time.sleep(0.1)
scroll_height = driver.execute_script("return document.body.scrollHeight;")
if (screen_height * i) > scroll_height:
break # 모든 콘텐츠가 로드될 때까지 스크롤
- 점진적인 스크롤 방식을 활용해 중간 콘텐츠 로딩 유도가 필요함
- 브라우저 창의 높이를 가져온 뒤, 각 루프마다 창의 높이만큼 스크롤하며 scrollHeight 되기까지 점진적으로 스크롤 함
=> 네이버 웹툰 페이지의 경우 이렇게 가져와야
Troubleshoot 3 - 쿠팡 페이지 콘텐츠 받아오기 실패
- requests 사용 시 user-agent 분 아니라 accept-language 설정까지 해주어야 데이터 받아옴
import requests
url = "https://www.coupang.com/np/search?q=%EB%85%B8%ED%8A%B8%EB%B6%81&channel=recent&component=&eventCategory=SRP&trcid=&traid=&sorter=scoreDesc&minPrice=&maxPrice=&priceRange=&filterType=&listSize=36&filter=&isPriceRange=false&brand=&offerCondition=&rating=0&page=1&rocketAll=false&searchIndexingToken=1=9&backgroundColor="
headers = {
# 언어 설정 추가를 해줘야 결과값 리턴받음
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept-Language": "ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3"
}
res = requests.get(url, headers=headers)
res.raise_for_status()
- selenium 사용 시 headless 모드 풀어야 잘 받아와짐
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
options = Options()
options.headless = False # 필요에 따라 헤드리스 모드 조정
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
options.add_argument("lang=ko_KR")
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
url = "https://www.coupang.com/np/search?q=%EB%85%B8%ED%8A%B8%EB%B6%81&channel=recent&component=&eventCategory=SRP&trcid=&traid=&sorter=scoreDesc&minPrice=&maxPrice=&priceRange=&filterType=&listSize=36&filter=&isPriceRange=false&brand=&offerCondition=&rating=0&page={}&rocketAll=false&searchIndexingToken=1=9&backgroundColor=".format(i)
driver.get(url)
html = driver.page_source
soup = BeautifulSoup(html, "lxml")
Troubleshoot 4 - 쿠팡 여러 페이지에서 콘텐츠 받아오기 실패
- for loop 통해 여러 페이지 접속하려고 했더니 다음 페이지 접속 시 해당 웹페이지가 일시적으로 다운되었거나 새 웹 주소로 완전히 이동했을 수 있다고 뜸
TRY1. 매 루프에서 driver = webdriver.Chrome(service=service, options=options) 이렇게 웹 드라이버 생성하고 driver.quit()로 종료하는 방식
=> 무척 비효율적임
TRY2. 각 페이지 방문 후 웹 드라이버의 쿠키/캐시를 명시적으로 초기화함
- 페이지 간 발생할 수 있는 상태 유지 문제 방지
- 이전에 사용된 쿠키 재사용 시 감지하는 보안 정책이 있나? (추측임)
for i in range(1,6):
url = "https://www.coupang.com/np/search?q=%EB%85%B8%ED%8A%B8%EB%B6%81&channel=recent&component=&eventCategory=SRP&trcid=&traid=&sorter=scoreDesc&minPrice=&maxPrice=&priceRange=&filterType=&listSize=36&filter=&isPriceRange=false&brand=&offerCondition=&rating=0&page={}&rocketAll=false&searchIndexingToken=1=9&backgroundColor=".format(i)
driver.get(url)
html = driver.page_source
soup = BeautifulSoup(html, "lxml")
driver.delete_all_cookies()
items = soup.find_all("li", attrs={"class": re.compile("^search-product")})
for item in items:
# 광고 제품 제외
ad_badge = item.find("span", attrs={"class": "ad-badge-text"})
if ad_badge:
continue
# 애플 제품 제외
name = item.find("div", attrs={"class":"name"}).get_text().strip()
if "Apple" in name:
continue
price = item.find("strong", attrs={"class":"price-value"}).get_text()
# 평점 없는 상품 제외
rate = item.find("em", attrs={"class":"rating"})
if rate:
rate = rate.get_text()
else:
continue
# 후기 없는 상품 제외
rate_total = item.find("span", attrs={"class":"rating-total-count"})
if rate_total:
rate_total = rate_total.get_text()[1:-1]
else:
continue
if float(rate) >= 4.5 and int(rate_total) >= 50:
print(str(i)+"번째 페이지")
print(f"제품명 : {name}")
print(f"가격 : {price}")
print(f"평점 {rate}점 ({rate_total}개)")
print("https://www.coupang.com"+item.find("a", attrs={"class":"search-product-link"})["href"])
print("-"*100)
Troubleshoot 5 - 네이버 로그인 시 captcha에 탐지되는 현상
driver.find_element(By.ID, "id").send_keys("my_id")
driver.find_element(By.ID, "pw").send_keys("my_pw")
driver.find_element(By.ID, "log.login").click()
- 이렇게 id/비번 입력 후 로그인하려고 하면 naver captcha에 탐지됨
# 네이버로 이동
driver.get("https://www.naver.com/")
# 로그인 버튼
ele = driver.find_element(By.XPATH, '//*[@id="account"]/div/a')
ele.click()
# id, pw 입력 후 로그인
input_js = ' \
document.getElementById("id").value = "{id}"; \
document.getElementById("pw").value = "{pw}"; \
'.format(id = "my_id", pw = "my_pw")
driver.execute_script(input_js)
driver.find_element(By.ID, "log.login").click()
# 기기 등록 안 함
time.sleep(1)
driver.find_element(By.ID, "new.dontsave").click()
- 이렇게 driver.execute_script() 이용해 js 코드를 실행시키면 문제없이 로그인이 잘 됨