0. 목표
Selenium을 활용한 브라우저 제어 및 웹 스크래핑
네이버 지도 API 적용
네이버 지도 API의 다양한 기능을 활용
8000원 미만 가성비 지도 가능?
브라우저 제어? : 로그인, 스크롤 내리기 등 브라우저의 동작을 할 때 Selenium 같은 브라우저 제어 프로그램을 이용해 정해진 시간에 게시판 글을 작성하는 등 자동화에 쓰일 수 있다.
프로젝트 셋팅 : requests, bs4, pymongo, flask, selenium 패키지 설치
필요한 기능 : 맛집 정보 스크래핑, 지도 보여주기, 각 맛지 별 마커, 정보창, 카드 만들기
from flask import Flask, render_template, request, jsonify, redirect, url_for
from pymongo import MongoClient
app = Flask(__name__)
client = MongoClient('내AWS아이피', 27017, username="아이디", password="비밀번호")
db = client.dbsparta_plus_week3
@app.route('/')
def main():
return render_template("index.html")
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
1. selenium을 활용한 스크래핑
웹 스크래핑은 웹 페이지에서 우리가 원하는 부분의 데이터를 수집해운다. 한국에서 크롤링과 같은 의미로 쓰이지만 크롤링은 자동화하여 주기적으로 웹 상 페이지를 돌아다니며 분류/색인 하고 업데이트 된 부분을 찾는 등의 일을 한다.
패키지 : requests, bs4, selenium를 활용해 멜론차트 스크래핑을 해보자.
멜론 차트 처럼 동적인 페이지를 기존의 방식으로 스크래핑을 할 경우 데이터를 잘 가져오지 못합니다. 만약 Requests를 활용해서 멜론차트의 좋아요 숫자를 가져오는 경우 0개로 표시가 됩니다.
이와 같이 스크래핑이 정상적으로 동작하지 않는 이유는 멜론 차트 페이지의 좋아요 갯수는 실시간으로 동적으로 가져오기 때문입니다. HTML을 처음 로드될 때는 좋아요 숫자와 같은 동적 데이터는 로드 되지 않습니다. 멜론 페이지는 페이지 로드를 한 후 다시 서버에서 좋아요 개수를 요청해 실시간 좋아요 갯수를 가져오게 됩니다. 하지만 Requests가 요청하는 페이지는 실시간으로 가져오는 동적인 좋아요 갯수가 아닌 그 이전 html이 로딩 되었을 때의 데이터를 가져오게되어 0개로 표시가 되는 것입니다. 스크래핑 할 때 동적인 데이터를 가져오기 위해서 셀레니움(Selenium 패키지)를 활용해 브라우저에 모든 데이터가 띄워진 이후 소스코드를 가져오는 방법을 써야 합니다.
import requests
from bs4 import BeautifulSoup
url = "https://www.melon.com/chart/day/index.htm"
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(url, headers=headers)
req = data.text
soup = BeautifulSoup(req, 'html.parser')
songs = soup.select("#frm > div > table > tbody > tr")
print(len(songs))
for song in songs:
title = song.select_one("td > div > div.wrap_song_info > div.rank01 > span > a").text
artist = song.select_one("td > div > div.wrap_song_info > div.rank02 > span > a").text
likes = song.select_one("td > div > button.like > span.cnt").text
print(title, artist, likes)
위의 requests와 bs4를 활용한 멜론 차트 스크래핑 코드는 제목, 아티스트, 좋아요 수를 가져오도록 작성되었습니다. 하지만 출력되는 코드에서 좋아요 수는 0개로 가져오지 못합니다. 멜론 차트와 같은 페이지에서 좋아요 수는 동적 데이터이기 때문에 브라우저 상에서는 보이는 데이터를 모두 가져오지 못하는 것입니다.
그렇다면 셀레니움을 활용해 동적 데이터를 가져오도록 해보겠습니다.
사전 준비 :
크롬드라이버 설치 : 크롬 브라우저를 제어하는 chromedriver 파일을 다운로드 받습니다.
크롬 버전 확인 후 드라이버를 다운로드합니다. : 크롬 버전 확인(chrome://settings/help), 크롬드라이버 다운(https://chromedriver.storage.googleapis.com/index.html)
chromedriver.exe 파일을 프로젝트 폴더로 복사해서 가져옵니다.
from bs4 import BeautifulSoup
from selenium import webdriver
from time import sleep
driver = webdriver.Chrome('./chromedriver') # 드라이버를 실행합니다.
url = "https://www.melon.com/chart/day/index.htm"
# headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
# data = requests.get(url, headers=headers)
driver.get(url) # 드라이버에 해당 url의 웹페이지를 띄웁니다.
sleep(5) # 페이지가 로딩되는 동안 5초 간 기다립니다.
req = driver.page_source # html 정보를 가져옵니다.
driver.quit() # 정보를 가져왔으므로 드라이버는 꺼줍니다.
# soup = BeautifulSoup(data.text, 'html.parser')
soup = BeautifulSoup(req, 'html.parser') # 가져온 정보를 beautifulsoup으로 파싱해줍니다.
songs = soup.select("#frm > div > table > tbody > tr")
print(len(songs))
for song in songs:
title = song.select_one("td > div > div.wrap_song_info > div.rank01 > span > a").text
artist = song.select_one("td > div > div.wrap_song_info > div.rank02 > span > a").text
# likes = song.select_one("td > div > button.like > span.cnt").text
likes_tag = song.select_one("td > div > button.like > span.cnt")
likes_tag.span.decompose() # span 태그 없애기
likes = likes_tag.text.strip() # 텍스트화한 후 앞뒤로 빈 칸 지우기
print(title, artist, likes)
주석 처리된 영역은 기존의 코드 입니다. 변경된 내용으로는 크롬 드라이버를 사용해 해당 url을 열면 자동화된 크롬창이 실행이 됩니다. 페이지의 모든 영역이 로딩이 될 때까지 기다리면 req에 해당 페이지 소스를 가져온 후 bs4를 활용해서 html 소스들을 파싱합니다. 이 때 가져오는 데이터는 기존의 스크래핑을 활용 했을 때와 달리 동적인 데이터인 좋아요 수까지 정상적으로 출력합니다.
2. selenium을 활용해 스크롤링 제어
이제 동적인 웹 페이지로부터 데이터를 전달 받을 수 있는 방법을 알게 되었습니다. 하지만 만약 아이유 이미지를 검색했다고 가정했을 때 스크롤을 내리지 않은 영역에서는 데이터를 가져오지 못합니다. 이런 경우 셀레니움을 활용해서 스크롤을 원하는만큼 내린 후 데이터를 가져올 수 있습니다.
from bs4 import BeautifulSoup
from selenium import webdriver
from time import sleep
driver = webdriver.Chrome('./chromedriver')
url = "https://search.naver.com/search.naver?where=image&sm=tab_jum&query=%EC%95%84%EC%9D%B4%EC%9C%A0"
driver.get(url)
sleep(10)
driver.execute_script("window.scrollTo(0, 1000)") # 1000픽셀만큼 내리기
sleep(1)
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
sleep(10)
req = driver.page_source
driver.quit()
soup = BeautifulSoup(req, 'html.parser')
images = soup.select(".tile_item._item ._image._listImage")
print(len(images))
for image in images:
src = image["src"]
print(src)
3. 네이버 지도 API 요청하기
네이버 API 기술문서 : https://navermaps.github.io/maps.js.ncp/docs/
네이버 API 사용신청 : 네이버 클라우드 플랫폼에서 회원가입(https://www.ncloud.com/) -> 지도 API 링크에서 이용 신청 (https://www.ncloud.com/product/applicationService/maps) -> Application 등록 (Web Dynamic Map, Geocoding 체크, Web 서비스 URL에 http://localhost:5000 입력 후 추가) -> 지도 API 인증 아이디 확인
네이버 API html에 적용하기
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>간단한 지도 표시하기</title>
<script type="text/javascript"
src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID"></script>
<script src=" https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<style>
#map {
width: 100%;
height: 400px;
}
</style>
<script>
$(document).ready(function () {
let map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(37.4981125, 127.0379399),
zoom: 10
});
})
</script>
</head>
<body>
<div id="map"></div>
</body>
</html>
YOUR_CLIENT_ID 부분에 네이버 콘솔에서 확인한 인증정보 Client ID를 복사해서 적기
app.py에 api를 작성 후 실행하면 동작 확인
4. 네이버 지도 연습하기
확대/ 축소버튼 넣기, 마커 넣기, infowindow 넣기, 마커 클릭 할 때마다 info window 여닫기
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
name="viewport">
<title>간단한 지도 표시하기</title>
<script src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=[내 client id]"
type="text/javascript"></script>
<script src=" https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<style>
#map {
width: 100%;
height: 400px;
}
</style>
<script>
$(document).ready(function () {
// 지도 띄우기
let mymap = new naver.maps.Map('map', {
center: new naver.maps.LatLng(37.4981125, 127.0379399), // 시작 시 위도 경도
zoom: 10, // 시작 시 줌
zoomControl: true, // 확대 축소 컨트롤
zoomControlOptions: { // 확대 축소 버튼 옵션
style: naver.maps.ZoomControlStyle.SMALL,
position: naver.maps.Position.TOP_RIGHT
}
});
// 마커 띄우기
let mymarker = new naver.maps.Marker({
position: new naver.maps.LatLng(37.4981125, 127.0379399), // 마커를 위치할 위치
map: mymap, // 마커를 띄울 맵 객체
icon: "{{ url_for('static', filename='rtan_heart.png') }}" // 마커 이미지 변경
});
// info window 만들기
let infowindow = new naver.maps.InfoWindow({
content: `<div style="width: 50px;height: 20px;text-align: center"><h5>안녕!</h5></div>`,
});
infowindow.open(mymap, mymarker); // info window 열기
infowindow.close(); // info window 닫기
// 지도 여닫기기
naver.maps.Event.addListener(mymarker, "click", function () {
console.log(infowindow.getMap()); // 정보창이 열려있을 때는 연결된 지도를 반환하고 닫혀있을 때는 null을 반환
if (infowindow.getMap() == true) {
infowindow.close();
} else {
infowindow.open(map, marker);
}
});
})
</script>
</head>
<body>
<div id="map"></div>
</body>
</html>
5. 맛집정보 스크래핑 하기
스크래핑 출처 : SBS TV 맛집 사이트 (http://matstar.sbs.co.kr/location.html)
스크래핑 :
from bs4 import BeautifulSoup
from selenium import webdriver
import time
driver = webdriver.Chrome('./chromedriver')
url = "http://matstar.sbs.co.kr/location.html"
driver.get(url)
time.sleep(5)
req = driver.page_source
driver.quit()
soup = BeautifulSoup(req, 'html.parser')
# 맛집 식당 가져오기
places = soup.select("ul.restaurant_list > div > div > li > div > a")
print(len(places))
# 맛집 시당 정보 가져오기 : 식당이름, 조소, 카테고리, 프로그램, 회차정보
for place in places:
title = place.select_one("strong.box_module_title").text
address = place.select_one("div.box_module_cont > div > div > div.mil_inner_spot > span.il_text").text
category = place.select_one("div.box_module_cont > div > div > div.mil_inner_kind > span.il_text").text
show, episode = place.select_one("div.box_module_cont > div > div > div.mil_inner_tv > span.il_text").text.rsplit(" ", 1)
print(title, address, category, show, episode)
위의 코드를 통해 SBS의 맛집 리스트를 불러 올 수 있습니다. 하지만 지도에 표시하기 위해서는 위도와 경도 정보가 필요합니다. 따라서 이전에 네이버에서 신청한 geocoding을 활용하여 각각 식당들의 위도, 경도 데이터를 가져오겠습니다.
6. 맛집 정보의 위도 경도 가져오기
네이버 geocoding API를 활용해 각 맛집들의 위도 경도를 가져옵니다. Geocoding 참고 링크 (https://api.ncloud-docs.com/docs/ai-naver-mapsgeocoding-geocode)
# geocoding 맛집의 위도 경위 요청하기
headers = {
"X-NCP-APIGW-API-KEY-ID": "[내 client id]",
"X-NCP-APIGW-API-KEY": "[내 client secret]"
}
r = requests.get(f"https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode?query={address}", headers=headers)
response = r.json()
if response["status"] == "OK":
if len(response["addresses"]) > 0:
x = float(response["addresses"][0]["x"])
y = float(response["addresses"][0]["y"])
print(title, address, category, show, episode, x, y)
else:
print(title, "좌표를 찾지 못했습니다")
7. 여러 페이지 스크래핑 하기
셀레니움을 활용해서 버튼을 클릭하여 더 많은 맛집 정보를 받아옵니다.
더 보기 버튼을 선택자로 클릭합니다. 페이지 소스를 가져오기 전 최대한 많은 맛집 정보가 들어오도록 합니다.
from selenium.webdriver.common.by import By
btn_more = driver.find_element(By.CSS_SELECTOR, "#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(5)
이와 같은 방식을 반복해서 더 많은 데이터를 가져올 수 있습니다. for 문을 통해 반복하며 try except를 추가하여 반복문이 실행되는 동안 예외 처리를 해줍니다.
from selenium.common import NoSuchElementException
for i in range(10):
try:
btn_more = driver.find_element(By.CSS_SELECTOR, "#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(5)
except NoSuchElementException:
break
맛집 가져오기 완성 코드 :
import time
import requests
from bs4 import BeautifulSoup
from pymongo import MongoClient
from selenium import webdriver
from selenium.common import NoSuchElementException
from selenium.webdriver.common.by import By
# pymongo db
client = MongoClient('[내 AWS IP 주소]', 27017, username="[아이디]", password="[비밀번호]")
db = client.dbsparta_plus_week3
# naver scrapping using selenium
driver = webdriver.Chrome('./chromedriver')
url = "http://matstar.sbs.co.kr/location.html"
driver.get(url)
time.sleep(5)
for i in range(10):
try:
btn_more = driver.find_element(By.CSS_SELECTOR, "#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(5)
except NoSuchElementException:
break
req = driver.page_source
driver.quit()
soup = BeautifulSoup(req, 'html.parser')
# 맛집 식당 가져오기
places = soup.select("ul.restaurant_list > div > div > li > div > a")
print(len(places))
# 맛집 시당 정보 가져오기 : 식당이름, 조소, 카테고리, 프로그램, 회차정보
for place in places:
title = place.select_one("strong.box_module_title").text
address = place.select_one("div.box_module_cont > div > div > div.mil_inner_spot > span.il_text").text
category = place.select_one("div.box_module_cont > div > div > div.mil_inner_kind > span.il_text").text
show, episode = place.select_one("div.box_module_cont > div > div > div.mil_inner_tv > span.il_text").text.rsplit(
" ", 1)
print(title, address, category, show, episode)
# geocoding 맛집의 위도 경위 요청하기
headers = {
"X-NCP-APIGW-API-KEY-ID": "p4570gx5o2",
"X-NCP-APIGW-API-KEY": "vCHv0AYu0zU9NOSu6WxfFDPqHG2FULTeXkOWZmsY"
}
r = requests.get(f"https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode?query={address}", headers=headers)
response = r.json()
if response["status"] == "OK":
if len(response["addresses"]) > 0:
x = float(response["addresses"][0]["x"])
y = float(response["addresses"][0]["y"])
print(title, address, category, show, episode, x, y)
doc = {
"title": title,
"address": address,
"category": category,
"show": show,
"episode": episode,
"mapx": x,
"mapy": y}
db.matjips.insert_one(doc)
else:
print(title, "좌표를 찾지 못했습니다")
8.
'웹 개발 > 웹개발플러스' 카테고리의 다른 글
웹 개발 플러스 4 - 회원가입, 로그인 기능 추가하기 (0) | 2023.03.23 |
---|---|
웹 개발 플러스 2 - 나만의 단어장 (0) | 2023.03.13 |
웹 개발 플러스 1 - 나만의 일기장 만들기 (1) | 2023.03.10 |