이전 글 requests로 웹 스크래핑 하기
이전 게시글에 이어서, 동적인 페이지에서 BeautifulSoup과 Selenium을 사용하여 원하는 데이터를 웹 크롤링 & 웹 스크래핑 하기 위해 진행한 과정의 일부분을 기록해 보도록 하자 ✍️
Jupyter는 대화형 계산 환경을 제공하는 오픈 소스 프로젝트
pip install jupyter
설치
jupyter notebook
실행
Selenium을 사용하기 전
import requests
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')
url = "https://path..."
headers = {
"User-Agent": "..."
}
response = requests.get(url, headers=headers)
print(response.text)
했을때 결과가 알 수 없는 문자열로 출력되는 문제가 생겼다.
<meta name="keywords" content="ë²ê°ì¥í°,ë²
ì¥,ë§ì¼,ì¤ê³ ëë¼,Market,ì¤ê³ ì¹´í,C2C,ì°ìì
¸ì©í,ì¤í굿ì¦,ì...
❓ response.text는 이미 디코딩된 문자열이라고한다. 그런데 왜 이런 결과가 나타날까? 🤔
: 웹 페이지의 주요 내용이 JavaScript를 통해 동적으로 로드되는 케이스였고, 따라서 requests를 사용하여 단순히 페이지의 소스를 가져오는 방법으로는 원하는 정보를 얻을 수 없었던 것이다.
pip install selenium
사용할 웹 브라우저의 웹 드라이버를 설치가 필요하며, 대부분의 경우 ChromeDriver를 사용하는 것이 일반적이다.
나는 최신버전 115를 가지고있고 위의 링크에서는 이전버전뿐이다! 😂
버전이 다르면 추후 문제가 일어날 수도 있기때문에 맞는 버전으로 설치하기위해 최신 드라이버 위치 다운받을 수 있는 링크를 찾았고, 이때 최신버전이 116~ 이므로 내 크롬 업데이트를 해주어서(116) 맞는 드라이버를 설치할 수 있었다.
# ChromeDriver의 경로를 지정
CHROMEDRIVER_PATH = '/Users/seul/chromedriver-mac-arm64'
# 시스템의 PATH에 ChromeDriver의 경로를 추가
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
import os
from selenium import webdriver
from bs4 import BeautifulSoup
# ChromeDriver의 경로를 지정
CHROMEDRIVER_PATH = '/Users/seul/chromedriver-mac-arm64'
# 시스템의 PATH에 ChromeDriver의 경로를 추가
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
options = webdriver.ChromeOptions()
options.add_argument('headless') # 브라우저 창을 실제로 띄우지 않고 실행
# Chrome 웹드라이버를 실행
browser = webdriver.Chrome(options=options)
# 웹 페이지를 방문
browser.get("https://path...")
# 페이지의 내용 가져옴
page_source = browser.page_source
# BeautifulSoup으로 HTML을 파싱
soup = BeautifulSoup(page_source, 'html.parser')
# 웹 드라이버를 종료(메모리 누수 방지)
browser.quit()
# html결과 출력
# prettify()를 써서 더 가독성좋게 html을 볼 수 있다. (soup의 기능, html 구조를 파악하기 쉽게 바꿔준다.)
print(soup.prettify())
환경 설정:
- ChromeDriver의 경로를 지정하고 시스템 PATH에 추가하여 Selenium이 해당 드라이버를 사용할 수 있도록 한다.
- webdriver.ChromeOptions()
을 사용하여 웹드라이버의 동작 옵션을 설정한다. 여기서는 headless
모드를 사용하여 브라우저 창을 실제로 띄우지 않고 백그라운드에서 작업을 수행한다.
웹드라이버 실행 및 웹 페이지 접근:
- webdriver.Chrome()
을 사용하여 웹드라이버 인스턴스를 생성하고, browser.get()
메서드로 원하는 웹 페이지에 접근한다.
HTML 가져오기 및 파싱:
- browser.page_source
를 사용하여 웹 페이지의 HTML 소스를 가져온다.
- BeautifulSoup를 사용하여 해당 HTML을 파싱하고, 이후 필요한 데이터를 추출한다.
정리 작업:
- browser.quit()
을 호출하여 웹드라이버 세션을 종료하고, 관련된 모든 리소스를 해제한다.
✍️
나는 상품의 가격 동향을 파악하기 위해 상품명(title), 상품가격(price), 세부링크(link)를 가져오는 웹 크롤링 & 웹 스크래핑 작업을 진행했다.
1차
import os
from selenium import webdriver
from bs4 import BeautifulSoup
# ChromeDriver의 경로를 지정
CHROMEDRIVER_PATH = '/Users/seul/chromedriver-mac-arm64'
# 시스템의 PATH에 ChromeDriver의 경로를 추가
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
options = webdriver.ChromeOptions()
options.add_argument('headless') # 브라우저 창을 실제로 띄우지 않고 실행
# Chrome 웹드라이버를 실행
browser = webdriver.Chrome(options=options)
# 브랜드가 A인 search list의 base url
base_url = 'https://path...'
# search를 위한 각 제품 넘버
productKeyword = ['123', '1234', '12345', '123456', 'AB 123456']
# 동적 url 리스트
urls = [base_url + keyword for keyword in productKeyword]
for url, key in zip(urls, productKeyword):
print('-------------------------------------------------') # 구분선 출력
print(f"Searching for products with model number: {key}")
# 각 웹 페이지를 방문
browser.get(url)
# 페이지의 내용 가져옴
page_source = browser.page_source
# BeautifulSoup으로 HTML을 파싱
soup = BeautifulSoup(page_source, 'html.parser')
# 1. class가 goodsList__item인 모든 li 태그들을 다 뽑기
products = soup.find_all('li', {'class': 'goodsList__item'})
for product in products:
# 2. 가져온 개별 li에서 class가 goodsTitle인 div태그를 찾기 & strip=True로 공백, 개행문자가 제거된 깔끔한 문자열 얻기
title = product.find(
'div', {'class': 'goodsTitle'}).get_text(strip=True)
if key in title: # 3. 각 key가 포함되어있는 title일 경우에만 price가져오기
price = product.find(
'span', {'class': 'priceNum'}).get_text(strip=True)
print(f"{title} : {price}")
# 웹 드라이버를 종료(메모리 누수 방지)
browser.quit()
print("상품의 타이틀과 가격들:")
for key, value in all_results.items():
print(f"{key} : {value}")
수정
import os
from selenium import webdriver
from bs4 import BeautifulSoup
# ChromeDriver의 경로를 지정
CHROMEDRIVER_PATH = '/Users/seul/chromedriver-mac-arm64'
# 시스템의 PATH에 ChromeDriver의 경로를 추가
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
options = webdriver.ChromeOptions()
options.add_argument('headless') # 브라우저 창을 실제로 띄우지 않고 실행
# Chrome 웹드라이버를 실행
browser = webdriver.Chrome(options=options)
# 브랜드가 A인 search list의 base url
base_url = 'https://path...'
# search를 위한 각 제품 넘버
productKeyword = ['123', '1234', '12345', '123456', 'AB 123456']
# 동적 url 리스트
urls = [base_url + urllib.parse.quote(keyword) for keyword in productKeyword]
results = []
for url, key in zip(urls, productKeyword):
results.append(
{"title": f"검색된 모델명: {key} ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨", "price": "", "link": ""})
key_parts = key.split()
# 각 웹 페이지를 방문
browser.get(url)
# 페이지의 내용 가져옴
page_source = browser.page_source
# BeautifulSoup으로 HTML을 파싱
soup = BeautifulSoup(page_source, "html.parser")
# 1. class가 goodsList__item인 모든 li 태그들을 다 뽑기
products = soup.find_all("li", {"class": "goodsList__item"})
for product in products:
# 2. 가져온 개별 li에서 class가 goodsTitle인 div태그를 찾기 & strip=True로 공백, 개행문자가 제거된 깔끔한 문자열 얻기
title = product.find(
"div", {"class": "goodsTitle"}).get_text(strip=True)
# 제품 넘버의 모든 부분이 title에 포함되어 있는지 확인, 대소문자를 구분하지 않음
if all(part.lower() in title.lower() for part in key_parts):
price = format(int(product.find("span", {"class": "priceNum"}).get_text(
strip=True).replace(',', '')), ',')
link = product.select_one("a.productPage").attrs['href']
results.append({"title": title, "price": price, "link": link})
# print(f"{title} : {price} / {link}")
pp.pprint(results)
df = pd.DataFrame(data=results)
df.to_csv("data/data.csv", index=True)
# 웹 드라이버를 종료(메모리 누수 방지)
browser.quit()
B사이트에서 크롤링을 하며 궁금했던 것
특정 모델을 크롤링하기위해 예상되는 실제 사이트에 접속을 해보니 검색 결과 상품이 없다고 뜬다. 하지만 제안된 '... 검색결과보기' 라는 버튼이있었다.
따라서 실제로 결과가 csv에 담기지 않을 줄 알았지만 결과가 잘 들어와 있었다. 이것이 어떻게 가능한걸까? 🧐
: Selenium의 작동 방식 때문이라고 한다. Selenium은 JavaScript의 동작을 포함한 웹 페이지의 동적인 변화까지 포착하고 처리할 수 있다. 이것은 Selenium의 큰 장점 중 하나이며, 그래서 동적인 웹사이트의 크롤링에 주로 사용되는 것이라고 한다.
Selenium의 웹 드라이버는 실제 웹 브라우저와 같이 동작하기 때문에 웹 페이지의 동적인 행동, 예를 들면 JavaScript 리다이렉트나 AJAX 요청 등을 자동으로 처리한다.
import os
from selenium import webdriver
from bs4 import BeautifulSoup
import urllib.parse
import pprint
import pandas as pd
# ChromeDriver의 경로를 지정
CHROMEDRIVER_PATH = "/Users/seul/chromedriver-mac-arm64"
# 시스템의 PATH에 ChromeDriver의 경로를 추가
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
options = webdriver.ChromeOptions()
options.add_argument("headless") # 브라우저 창을 실제로 띄우지 않고 실행
# Chrome 웹드라이버를 실행
browser = webdriver.Chrome(options=options)
# 브랜드가 A인 search list의 base url
base_url = "https://www.path..."
# 상품 모델명 리스트
productKeyword = ['123', '1234', '12345', '123456', 'AB 123456']
# 동적 url 리스트
urls = [base_url + urllib.parse.quote(keyword) for keyword in productKeyword]
results = []
for url, key in zip(urls, productKeyword):
results.append(
{"title": f"검색된 모델명: {key} ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨", "price": "", "link": ""}
)
key_parts = key.split()
# 각 웹 페이지를 방문
browser.get(url)
# 페이지의 내용 가져옴
page_source = browser.page_source
# BeautifulSoup으로 HTML을 파싱
soup = BeautifulSoup(page_source, "html.parser")
# class가 img인 div 안의 a 태그로부터 상품 링크 가져오기
product_links = soup.select("div.img > ... > a")
# class가 txt인 div 태그로부터 상품 제목과 가격 가져오기
product_details = soup.select("div.txt")
for link, detail in zip(product_links, product_details):
title = detail.find("a").get_text(strip=True)
price = detail.find("strong", class_="...").get_text(strip=True)
href = "https://www.path..." + link.attrs['href']
if all(part.lower() in title.lower() for part in key_parts):
results.append({"title": title, "price": price, "link": href})
# DataFrame으로 변환 후 CSV 파일로 저장
df = pd.DataFrame(data=results)
df.to_csv("data/data2.csv", index=True)
# 웹 드라이버를 종료(메모리 누수 방지)
browser.quit()
C 사이트(네이버카페)에서 크롤링을 하며 겪은 문제 사항과 개선사항
특히 네이버카페에서 크롤링할 때에는 다른 사이트보다 주의할 점이 필요했다.
1. 로그인 처리가 필요하다.
2. iframe을 사용중이므로 추가 로직이 필요하다.
3. 검색하거나 게시글을 클릭할때의 동작들마다 url의 변화가 없다.
4. 게시글 외부에서 크롤링하는 자료 (title, link)와 게시글 내부에서 크롤링하는 자료(price)로 나눠서 가져와야한다.
5. 앱게시물, 웹게시물 html 이 다르다.
6. 크롤링 데이터가 제대로 안가져와질 경우 여러번 재시도를 해야한다 (시간이 더 오래걸리게 되어서 모델을 따로따로 크롤링했다.)
6번의 경우 웹 스크래핑 및 웹 크롤링에서 흔히 발생하는 문제 중 하나로, 웹 페이지의 동적 로딩 및 JavaScript를 통한 내용 변경 때문에 발생할 수 있다.
해결을 위해 추가 대기 시간 로직 및 재시도 로직을 추가해야했다.😤
✍️ 네이버 카페의 경우 생각보다 많은 예시 코드를 참고할 수 있었다. 하지만 나는 현재 배우는 단계이고, 스스로 크롤링을 이해하기 위한 학습 목적도 있었기에 1차원적으로 생각나는 순서 그대로 정직하게 따라가듯이 크롤링을 진행했었다. 예를들면 내가 카페에 접속해서 하는 행위들을 한 단계 한 단계 순서 그대로 필요한 동작을 수행시켰다.
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup as bs
import pandas as pd
import time
import pprint
...
# 로그인 정보
login_url = 'https://login path...'
id = 'id'
pw = 'pw'
# 로그인
browser.get(login_url)
browser.execute_script(
"document.getElementsByName('id')[0].value=\'" + id + "\'")
browser.execute_script(
"document.getElementsByName('pw')[0].value=\'" + pw + "\'")
browser.find_element(By.XPATH, '//*[@id="log.login"]').click()
base_url = "https://cafe.naver.com/특정카페"
# 상품 모델명 리스트
productKeyword = ['123', '1234', '12345', '123456', 'AB 123456']
results = []
for key in productKeyword:
# 네이버 카페 접속
browser.get(base_url)
# 검색어 입력
search_box = browser.find_element(By.ID, "topLayerQueryInput")
search_box.send_keys(key)
# 검색 버튼 클릭
search_btn = browser.find_element(
By.CSS_SELECTOR, "#cafe-search button")
search_btn.click()
time.sleep(2)
browser.switch_to.frame("cafe_main")
# 검색 옵션 드롭다운 메뉴 클릭
search_option_dropdown = browser.find_element(By.ID, "divSearchByTop")
search_option_dropdown.click()
time.sleep(1)
# '제목만' 옵션 선택
title_only_option = browser.find_element(
By.XPATH, "//a[contains(text(), '제목만')]")
title_only_option.click()
time.sleep(2)
# 검색 버튼 클릭
search_btn = browser.find_element(
By.XPATH, "//button[contains(text(), '검색')]")
search_btn.click()
time.sleep(2)
# "50개씩" 보기 옵션을 선택하기 위한 드롭다운 메뉴 클릭
dropdown_menu = browser.find_element(By.ID, "listSizeSelectDiv")
dropdown_menu.click()
time.sleep(1) # 드롭다운 메뉴 옵션들이 표시될 때까지 대기
# "50개씩" 옵션 선택
fifty_option = browser.find_element(
By.XPATH, "//a[contains(text(), '50개씩')]")
fifty_option.click()
time.sleep(2) # 옵션 선택 후 페이지 로딩 대기
# BeautifulSoup으로 HTML을 파싱
soup = bs(browser.page_source, 'html.parser')
# print(str(soup))
# 해당 class를 가진 모든 게시글 링크들을 찾음
article = soup.select('div.inner_list a.article')
titles = [link.text.strip() for link in article]
links = [link['href'] for link in article]
# pp.pprint(article)
for title, link in zip(titles, links):
retries = 2
success = False
while retries > 0 and not success:
# 게시글의 링크로 이동
browser.get('https://cafe.naver.com' + link)
time.sleep(3)
browser.switch_to.frame("cafe_main")
# 해당 페이지의 HTML 소스 가져오기
page_source = browser.page_source
# BeautifulSoup으로 HTML 파싱
soup_article = bs(page_source, 'html.parser')
# 가격 정보 추출
price_div = soup_article.find('div', class_='ProductPrice')
if price_div:
price_strong = price_div.find('strong', class_='cost')
if price_strong:
price = price_strong.text.strip()
success = True # 정보를 성공적으로 가져왔음
else:
price = '가격 정보 없음1'
else:
price = '가격 정보 없음2'
retries -= 1 # 재시도 횟수 감소
results.append({
'Title': title,
'Price': price,
'Link': 'https://cafe.naver.com' + link
})
# DataFrame으로 변환 후 CSV 파일로 저장
df = pd.DataFrame(data=results)
df.to_csv("data/data3_test.csv", index=True)
D 사이트에서 크롤링하기
1. 각 국가별, 모델명별 이중 for문을 돌고 결과를 pandas DataFrame으로 변환하여 CSV로 저장한다.
2. a 태그가 분명 화면상으로 존재해 보이지만 없을경우 에러가 뜬 상황때문에 a태그가 있는지 먼저 보고 if문을 통해 다음 코드로 넘어간다.
3. 다음페이지로 넘어갈 수 있는 코드 추가했다.
import os
from selenium import webdriver
from bs4 import BeautifulSoup
import urllib.parse
import pprint
import pandas as pd
pp = pprint.PrettyPrinter(depth=4)
CHROMEDRIVER_PATH = "/Users/seul/chromedriver-mac-arm64"
os.environ["PATH"] += os.pathsep + CHROMEDRIVER_PATH
options = webdriver.ChromeOptions()
options.add_argument("headless")
browser = webdriver.Chrome(options=options)
# 브랜드가 A인 search list의 base url (각 국가별)
base_urls = [
"https://path...USA...", # 미국
"https://path...HK..."", # 홍콩
"https://path...UK..."", # 영국
"https://path...KR..."" # 한국
]
# 상품 모델명 리스트
productKeyword = ['123', '1234', '12345', '123456', 'AB 123456']
# 각 국가별, 모델명별 이중 for문
for base_url in base_urls:
for keyword in productKeyword:
results = []
page_num = 1 # 초기 페이지 번호 설정
while True:
url = base_url + urllib.parse.quote(
keyword) + ...page={page_num}'
browser.get(url)
page_source = browser.page_source
soup = BeautifulSoup(page_source, "html.parser")
# 상품 카드들을 찾기
cards = soup.find_all('div', class_='...')
if not cards: # 상품 카드가 없으면, 즉 페이지에 상품이 없으면 반복 종료
break
for card in cards:
# a 태그찾기
a_tag = card.find(
'a', class_='...')
# 만약 a 태그가 존재하면 링크를 추출
if a_tag:
link = 'https://path...' + a_tag['href']
title = card.find(
'div', class_='...').text.strip()
sub_title = card.find(
'div', class_='...').text.strip()
price_tag = card.find('span', class_='...')
if price_tag and price_tag.next_sibling:
price = "$" + price_tag.next_sibling.strip()
else:
price = "_"
# 결과를 results에 저장
results.append(
{"title": title, "sub_title": sub_title, "price": price, "link": link})
# 상품 카드의 개수가 60개 미만이면, 다음 페이지로 이동하지 않고 종료
if len(cards) < 60:
break
page_num += 1 # 다음 페이지로 이동
# 결과를 pandas DataFrame으로 변환 후 CSV로 저장
df = pd.DataFrame(results)
country_code = base_url.split('=')[1].split('&')[0] # URL에서 국가 코드 추출
df.to_csv(f"data/D_site/{keyword}_{country_code}.csv", index=True)
총 5개의 사이트에서 크롤링, 스크래핑을 진행하면서 중간중간 너무도 많은 에러사항을 겪기도 했지만, 이를 해결하기위한 다양한 방법을 찾아보면서 매달린 결과 마침내 모두 성공적으로 완료할 수 있었다. 😀
확실히 api를 이용해서 rquests를 사용하는 방법이 제일 간단했고, BeautifulSoup, Selenium을 사용하면서 점점 추가 설정을 많이 했던 것 같다.✨