✍🏻 8일 공부 이야기.
👀 오늘 공부한 자세한 코드 내용은 아래 깃허브에 올라와 있습니다:)
https://www.chicagomag.com/chicago-magazine/november-2012/best-sandwiches-chicago/
웹 데이터를 분석하기에 좋은 사이트라 한 번 분석해보고자 한다.
from urllib.request import Request, urlopen
from bs4 import BeautifulSoup
사이트에 나와있는 랭크, 가게 메뉴, 가게 이름, 가게 사이트 데이터를 추출해서 저장해보자.
💡 URL을 분리하는 경우
: 50개 각각의 페이지를 얻는 부분에서 링크가 상대주소로 되어있기 때문에 URL을 분리해두어 붙여주는 형식의 코드가 좋겠다고 생각했기 때문
url_base = "https://www.chicagomag.com/"
url_sub = "chicago-magazine/november-2012/best-sandwiches-chicago/"
url = url_base + url_sub
response = urlopen(url)
response.status
는 403 에러가 뜬다.
📌 이를 해결하는 방법은 총 3가지가 있다.
(1) User-Agent 명을 직접 명시해주는 방법
위 사진 속 User-Agent 오른쪽 부분을 복사 붙여넣기해서 아래와 같이 코드를 작성해주면 된다.
req = Request(url, headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"})
response = urlopen(req)
(2) 사용하고 있는 브라우저를 명시해주는 방법
req = Request(url, headers = {"User-Agent" : "Chrome"})
response = urlopen(req)
(3) fake-useragent 모듈을 사용하는 방법
#pip install fake-useragent
from fake_useragent import UserAgent
ua = UserAgent()
req = Request(url, headers = {"User-Agent" : ua.ie})
response = urlopen(req)
urljoin
이용)tmp_one = soup.find_all("div", class_ = "sammy")[0]
# 가게 랭크
tmp_one.find(class_ = "sammyRank").get_text()
# tmp_one.select_one(".sammyRank").text
# 음식 이름, 가게 이름
tmp_one.find("div", {"class" : "sammyListing"}).text
# tmp_one.select_one(".sammyListing").text
import re
tmp_string = tmp_one.find(class_="sammyListing").get_text()
re.split(("\n|\r\n"), tmp_string) # 필요없는 문자열 삭제
re.split(("\n|\r\n"), tmp_string)[0] ## 음식 이름
re.split(("\n|\r\n"), tmp_string)[1] ## 가게 이름
# 가게 사이트
tmp_one.find("a")["href"] # 상대 경로로 추출됨
# tmp_one.select_one("a")["href"] #.get("href")라 써도 됨
from urllib.parse import urljoin
# 필요한 내용을 담을 빈 리스트
# 리스트로 하나씩 컬럼을 만들고 DataFrame으로 합칠 예정
rank = []
main_menu = []
cafe_name = []
url_add = []
list_soup = soup.find_all("div", class_ = "sammy") #soup.select(".sammy")
for item in list_soup:
rank.append(item.find(class_ = "sammyRank").get_text())
tmp_string = item.find(class_ = "sammyListing").get_text()
main_menu.append(re.split(("\n|\r\n"), tmp_string)[0])
cafe_name.append(re.split(("\n|\r\n"), tmp_string)[1])
url_add.append(urljoin(url_base, item.find("a")["href"]))
import pandas as pd
data = {
"Rank" : rank,
"Menu" : main_menu,
"Cafe" : cafe_name,
"URL" : url_add
}
df = pd.DataFrame(data)
# 컬럼 순서 변경
df = pd.DataFrame(data, columns = ["Rank", "Cafe", "Menu", "URL"])
# 데이터 저장
df.to_csv("../data/03. best_sandwiches_list_chicago.csv", sep=",", encoding = "utf-8")
가게 하위페이지에 대해 연습해보자.
가격 데이터를 추출하고 싶다.
🤔 데이터를 보니.. .
전까지가 가격 데이터인 것 같다.
하지만 .
후에 가격 데이터가 더 나올 수도 있고 띄어쓰기 후에 숫자/문자가 나타나면 주소라는 패턴을 알아냈는데 이를 이용해서 가격 데이터만 추출할 수 있을까?
.x
: 임의의 한 문자를 표현하며 x가 마지막으로 끝남x+
: x가 1번 이상 반복됨x?
: x가 존재하거나 존재하지 않음x*
: x가 0번 이상 반복됨x|y
: x 또는 y를 찾음📌 Regular Expression 을 이용해서 가격 데이터만 추출할 수 있다.
ua = UserAgent()
req = Request(df["URL"][0], headers = {"user-agent" : ua.ie})
html = urlopen(req).read()
soup_tmp = BeautifulSoup(html, "html.parser")
price_tmp = soup_tmp.find("p", class_ = "addy").text #soup.find.select_one(".addy")
import re
# 가격
tmp = re.search("\$\d+\.(\d+)?", price_tmp).group()
# $ 표시가 오고, $ 표시 뒤엔 숫자가 1번 이상 있다.
# 그 뒤엔 . 이 있고
# . 뒤엔 숫자가 더 있을수도 있고 없을수도 있다.
# 주소
# 가격 데이터 뒤에 띄어쓰기가 하나 있으므로 2칸째부터 추출
add = price_tmp[len(tmp) + 2:]
한 데이터에 대해서 음식 가격과 가게 주소를 뽑아봤으니, 반복문을 통해 모든 데이터에 대해 값을 추출해보자.
💡 앞서 df["URL"][0]
라 사용했던 부분은 iterrows()
로 대체 가능
💡 해당 반복문이 잘 진행되고 있는지 알려주는 모듈! pip install tqdm
from tqdm import tqdm
price = []
address = []
ua = UserAgent()
for idx, row in tqdm(df.iterrows()):
req = Request(row["URL"], headers = {"user-agent" : ua.ie})
html = urlopen(req).read()
soup_tmp = BeautifulSoup(html, "html.parser")
gettings = soup_tmp.find("p", class_ = "addy").get_text()
price_tmp = re.split(".,", gettings)[0]
tmp = re.search("\$\d+\.(\d+)?", price_tmp).group()
price.append(tmp)
address.append(price_tmp[len(tmp) + 2:])
df_result = df.loc[:, ["Rank", "Cafe", "Menu"]]
df_result["Price"] = price
df_result["Address"] = address
df_result.head()
df_result.to_csv('../data/03. best_sandwiches_list_chicago2.csv', sep = ",", encoding = "utf-8")
위 내용이 강의에서 배운 내용인데, Regular Expression을 쓰고, 가게 주소를 인덱스를 이용해 추출한 코드 때문에 3번째 행과 4번째 행의 Price의 값은 마지막에 .
이 없고, Address는 앞에 공백이 하나 출력되는 것을 볼 수 있었음.
지금은 상관이 없는 부분일지라도, 데이터는 모두 다 깔끔하게 맞춰주어야 이후 코드 진행을 할 때 걸림돌이 되지 않으므로 다른 방법으로 음식 가격과 가게 주소를 추출해내는 방법을 고민해봤다.
💡 첫 번째 공백을 기준으로 가격과 주소가 나뉘어진다.
# re.split(".,", gettings)[0] 를 출력해보면, 가격 앞에 \n 이 들어가있다.
# 이를 제거해주고 첫 번째 공백 기준으로 가격과 주소를 나눠준다.
price_tmp = price_tmp.replace("\n", "")
print(price_tmp)
price_tmp.split(" ", 1) # 가격과 주소 사이에는 첫 번째 공백이 생긴다.
따라서
tmp = price_tmp.split(" ", 1)[0] # 가격
add = price_tmp.split(" ", 1)[1] # 주소
이렇게 가격과 주소를 추출할 수 있었다.
전체 코드는 아래와 같다.
from tqdm import tqdm
price = []
address = []
ua = UserAgent()
for idx, row in tqdm(df.iterrows()):
req = Request(row["URL"], headers = {"user-agent" : ua.ie})
html = urlopen(req).read()
soup_tmp = BeautifulSoup(html, "html.parser")
gettings = soup_tmp.find("p", class_ = "addy").get_text()
price_tmp = re.split(".,", gettings)[0].replace("\n", "")
tmp = price_tmp.split(" ", 1)[0]
add = price_tmp.split(" ", 1)[1]
price.append(tmp)
address.append(add)
df_result = df.loc[:, ["Rank", "Cafe", "Menu"]]
df_result["Price"] = price
df_result["Address"] = address
df_result.head()
시카고 맛집 데이터를 지도로 시각화해보자.
📌 주소 데이터를 입력했을 때 지점이 여러 개인 경우, Multiple location이라고 적혀있어서 위도, 경도를 찾을 수 없다. 따라서 Multiple location가 아닌 주소에 대해서만 뒤에 Chicago를 붙여 위도, 경도를 알아내야한다!
구글에 주소를 검색해보면 해당 주소 뒤에 <, Chicago>가 붙여져있는 것을 확인할 수 있다. 그래서 우리도 이를 붙여서 검색해준 것!
lat = []
lng = []
for idx, row in tqdm(df.iterrows()):
if not row["Address"] == "Multiple location":
target_name = row["Address"] + ", " + "Chicago"
gmaps_output = gmaps.geocode(target_name)
location_output = gmaps_output[0].get("geometry")
lat.append(location_output["location"]["lat"])
lng.append(location_output["location"]["lng"])
else:
lat.append(np.nan)
lng.append(np.nan)
지난 시간에 배운 googlemaps를 이용하여 위도와 경도를 알아내고
이를 folium을 통해 지도에 시각화 해주었다.
mapping = folium.Map(location = [41.895558 , -87.679967],
zoom_start = 11
)
for idx, row in df.iterrows():
if not row["Address"] == "Multiple location":
folium.Marker(
location = [row["lat"], row["lng"]],
popup = row["Cafe"],
tooltip = row["Menu"],
icon = folium.Icon(
icon = "coffee",
prefix = "fa"
)
).add_to(mapping)
mapping
동적 데이터를 크롤링해보자.
🤯 BeautifulSoup 만으로 해결할 수 없는 경우가 발생
- 접근할 웹 주소를 알 수 없을 때
- 자바스크립트를 사용하는 웹 페이지의 경우
- 웹 브라우저로 접근하지 않으면 안될 때(예를 들어, 비행기 예매를 하고자 홈페이지에 들어가서 공항, 인원수, 날짜 등 직접 설정하고 검색을 해야 검색창을 볼 수 있는 경우를 말함)
위와 같은 경우에 Selenium
을 사용하면 편하다.
어떤 사이트같은 경우, 스크롤을 끝까지 내렸는데 스크롤이 늘어나면서 계속 페이지들이 나타나는 경우( = 동적 페이지의 기능 중 하나)가 있다. 이와 같은 페이지를 스크랩하고 싶을 때 Selenium을 사용하면 편하다.
pip install selenium
크롬 버전 확인하고 chromedriver 다운받기
도움말 - Chrome 정보에 가면 버전을 확인할 수 있다.
나는 버전이 115.xx 였기 때문에 위 사이트로 가서 다운받음. (📄 자세한 내용은 사이트 참고)
!pip install selenium
해주고 아래 코드를 실행했을 때 해당 창이 뜨는지 확인하기
<from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://www.naver.com')
#driver.quit() 창 닫기
📌 Selenium이 버전이 업데이트되면서 코드가 바뀌었다. 구글링할 때에도 이를 잘 찾아보고 학습할 것. (📄 Selenium 공식 문서)
.get("접근하고 싶은 웹 주소")
: 새로운 크롬창에 해당 주소로 이동함.execute_script("return document.body.scrollHeight")
: 해당 페이지에서 스크롤이 가능한 높이를 반환.execute_script("window.scrollTo(0, document.body.scrollHeight);")
: 해당 페이지에서 제일 상단 부분의 페이지를 반환.find_element_by_xpath('''xpath 값''')
.find_element(BY.XPATH, 'xpath값')
.find_element_by_id('''id 값''')
.find_element_(BY.ID, 'id값')
# 이전 버전 코드
# some_tag = driver.find_element_by_id('''id 값''')
some_tag = driver.find_element(BY.ID, 'id 값')
some_tag.send_keys('입력할 값')
# 이전 버전 코드
# xpath = '''클릭 버튼의 xpath 값'''
# some_tag = driver.find_element_by_xpath(xpath).click()
some_tag = driver.find_element(BY_XPATH, 'xpath값').click()
이를 응용해서 해당 지점에 값을 입력하고 입력된 값을 검색하는 버튼을 클릭하여 페이지가 이동되는 코드를 작성할 수도 있음.req = driver.page_source
soup = BeautifulSoup(req, "html.parser")
그러면 우리는 또 앞서 배웠던 find, select 함수를 이용해 원하는 데이터를 추출해낼 수 있음. https://pinkwink.kr/ 사이트를 살펴보면서 selenium을 익혀보자.
from selenium import webdriver
driver = webdriver.Chrome()
# 창 열기
driver.get('https://pinkwink.kr/')
driver.maximize_window()
: 화면 크기 최대 설정
driver.minimize_window()
: 화면 크기 최소 설정
driver.set_window_size(가로값,세로값)
: 화면 크기 설정
driver.refresh()
: 화면 새로고침
driver.back()
: 뒤로가기
driver.forward()
: 앞으로가기
클릭
from selenium.webdriver.common.by import By
first_content = driver.find_element(By.CSS_SELECTOR, '#content > div.cover-masonry > div > ul > li:nth-child(1)')
## 크롤링할 부분 -> Copy -> Copy selector
first_content.click()
driver.execute_script('window.open("웹주소")')
: 새로운 탭에 해당 웹 주소를 띄움
driver.switch_to.window(driver.window_handles[이동하고자하는 웹 창 인덱스])
: 해당하는 인덱스의 창으로 이동
driver.close()
: 현재 보여지고 있는 창 닫기
driver.quit()
: 전체 창 닫기(원하는 처리를 다 했으면 무조건 창을 닫아줘야함)
driver.execute_script('return document.body.scrollHeight')
: 현재 보여지는 페이지에서 스크롤이 가능한 높이(길이)
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
: 화면 스크롤 맨 하단으로 이동
driver.execute_script('window.scrollTo(0,0)')
: 화면 스크롤 맨 상단으로 이동
driver.save_screenshot('경로/스크린샷파일명.png')
: 현재 보이는 화면의 스크린샷을 해당 경로와 파일명을 가진 파일로 저장
특정 태그 지점까지 스크롤 이동
from selenium.webdriver import ActionChains
## https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains
## ActionChains를 이용하여 다양한 기능을 수행할 수 있음
some_tag = driver.find_element(By.CSS_SELECTOR, '#content > div.cover-list > div > ul > li:nth-child(1)')
action = ActionChains(driver)
action.move_to_element(some_tag).perform()