셀레니움 배운다. 마침 해킹 점검 툴 코드 짤때 셀레니움에 대해 처음 알게되었는데
오이게 웬걸~ 때마침 배우네 개이득
이번에 배울것
브라우저 제어 - selenium
웹 스크래핑이란?
웹페이지에서 우리가 원하는 부분의 데이터를 수집해오는 것
- 한국에서는 같은 작업을 크롤링 crawling 이라는 용어로 혼용해서 쓰는 경우가 많습니다. 원래는 크롤링은 자동화하여 주기적으로 웹 상에서 페이지들을 돌아다니며 분류/색인하고 업데이트된 부분을 찾는 등의 일을 하는 것을 뜻해요. 구글 검색을 할 때는 web scraping 으로 검색해야 우리가 배우는 페이지 추출에 대한 결과가 나올 거예요!
- 참고 Web Scraping(wikipedia) / Web Crawler(wikipedia) Web Scraping vs Web Crawling: What’s the Difference?
멜론 차트처럼 동적인 웹페이지를 스크래핑 할때는 브라우저에 띄운 후 소스코드를 가져오는 방법을 써야함.
좋아요 숫자가 뜨는게 처음 html이 로드될때는 없는거임
서버에 다시 좋아요 개수를 요청하고 입력을 하는건데
리퀘스트가 가져오는거는 딱 날것의 html만 가져오는거라서
브라우저를 띄워서 좋아요 수를 가져오는건데 셀레니움으로만 할 수 있음.
1. 크롬 드라이버 다운로드하기
크롬을 셀레니움이 통제할 수 있도록 도와주는게 크롬드라이버임
버전 확인 후에 다운로드 하기.
멜론사이트를 스크래핑 해보자.
드라이버 기본 세팅 코드 + 멜론 사이트 스크래핑 코드
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_tag = song.select_one("td > div > button.like > span.cnt")
likes_tag.span.decompose() # span 태그 없애기
likes = likes_tag.text.strip() # 텍스트화한 후 앞뒤로 빈 칸 지우기
print(title, artist, likes)
request와 비슷해보이지만
sleep과 같이 조금씩 다름
.decompose()가 뭘까?
<span>abcd</span>
있으면 결과물은 abcd로 바꿔주는거 (태그없애기)
브라우저 제어 - 스크롤, 버튼
네이버 이미지 검색창 스크래핑해보기
화면에 보이는것만 스크래핑 함.
그래서 스크롤 되었을때 이미지도 추가해서 스크래핑
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()
excute_script는 쌍따옴표 안에 있는거를 크롬브라우저가 실행을 해줘라는 것임
window.scrollTo -> window 창에서 높이를 1000픽셀만큼 내려서 스크롤을 해줘~
맛집지도 만들기 프로젝트~시작!
API 기술문서
https://navermaps.github.io/maps.js.ncp/docs/
사용신청하기
1. 회원가입
2. 여기 들어가보면 api신청가능
https://www.ncloud.com/product/applicationService/maps
좌표값 변환하게 해주는거
선택 신청
인증정보의 clientsecret과 id를 활용해야함.
prac_map.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>
클라이언트 아이디 나의것으로 바꿔줌
app.py에 html로드할 수 있도록 코드 추가해주고 실행하면!@
잘 뜨는거 확인 가능
지도에 마커 + info window 추가해보기
확대/축소 버튼 넣기
let map = 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 marker = new naver.maps.Marker({
position: new naver.maps.LatLng(37.4981125, 127.0379399),
map: map
});
마커 이미지 바꾸기
let marker = new naver.maps.Marker({
position: new naver.maps.LatLng(37.4981125, 127.0379399),
map: map,
icon: "{{ url_for('static', filename='rtan_heart.png') }}"
});
infowindow만들고 열기
let infowindow = new naver.maps.InfoWindow({
content: `<div style="width: 50px;height: 20px;text-align: center"><h5>안녕!</h5></div>`,
});
infowindow.open(map, marker);
infowindow닫기
infowindow.close();
강력새로고침 = ctrl +shift+R
열고닫는
naver.maps.Event.addListener(marker, "click", function () {
console.log(infowindow.getMap()); // 정보창이 열려있을 때는 연결된 지도를 반환하고 닫혀있을 때는 null을 반환
if (infowindow.getMap()) {
infowindow.close();
} else {
infowindow.open(map, marker);
}
});
event.addListener : 어떤 행동이 발생했음을 알리는거를 등록하겠다.
marker라는 녀석을 기준으로 클릭이 일어났을때 function을 하겠다.
만약 열려있다면
닫아주고
닫혀있다면
열어줘라 라는것.
맛집 지도 구성 본격적으로 시작해보기!
구성
맛집 정보 스크래핑
지도 보여주기
각 맛집 별 마커, 정보창(infoWindow), 카드만들고 서로 연결하기
맛집정보
http://matstar.sbs.co.kr/location.html
카드 하나당 맛집 하나
제목과 주소 있음
스크롤 내리다보면 더보기 버튼 있음 (추가 맛집 정보 로드됨)
맛집 정보 주소 좌표로 변환해주는 api를 네이버에서 가져옴
geocoding 사용법
https://api.ncloud-docs.com/docs/ai-naver-mapsgeocoding-geocode
curl -G "https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode" \
--data-urlencode "query={주소}" \
--data-urlencode "coordinate={검색_중심_좌표}" \
-H "X-NCP-APIGW-API-KEY-ID: {애플리케이션 등록 시 발급받은 client id값}" \
-H "X-NCP-APIGW-API-KEY: {애플리케이션 등록 시 발급받은 client secret값}" -v
-H는 헤더라는건데, 내가 이 api를 허락 받고 쓰는건지, 아닌건지 네이버가 확인하고 싶다는것.
여러페이지 스크래핑 하기
더보기버튼 선택자로 버튼 클릭하기
btn_more = driver.find_element_by_css_selector("#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(5)
숙제가 항상..어렵다
서버와 클라이언트 구문을 제대로 연결하는게 어려움.
index.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>
<meta property="og:title" content=" 맛집 지도"/>
<meta property="og:description" content="mini project for Web Plus"/>
<meta property="og:image" content="{{ url_for('static', filename='og_image.jpg') }}"/>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Jua&display=swap" rel="stylesheet">
<script type="text/javascript"
src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=ID입력h&submodules=geocoder"></script>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<style>
.wrap {
width: 90%;
max-width: 750px;
margin: 0 auto;
}
.banner {
width: 100%;
height: 20vh;
background-image: url('/static/banner.jpg');
background-position: center;
background-size: contain;
background-repeat: repeat;
}
h1.title {
font-family: 'Jua', sans-serif;
color: white;
font-size: 3rem;
}
h5 {
font-family: 'Jua', sans-serif;
}
.matjip-list {
overflow: scroll;
width: 100%;
height: calc(20vh - 30px);
position: relative;
}
.card-title, .card-subtitle {
display: inline;
}
#map {
width: 100%;
height: 50vh;
margin: 20px auto 20px auto;
}
.iw-inner {
padding: 10px;
font-size: smaller;
}
</style>
<script>
//전역변수
let y_cen = 37.4981125 // lat
let x_cen = 127.0379399 // long
let map;
let markers = []; //네이버지도 api를 쓰고싶으면 지켜줘야하는 전역변수선언
let infowindows = [];
$(document).ready(function () {
map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(y_cen, x_cen),
zoom: 12,
zoomControl: true,
zoomControlOptions: {
style: naver.maps.ZoomControlStyle.SMALL,
position: naver.maps.Position.TOP_RIGHT
}
});
get_matjips();
})
function make_card(i, matjip) {
let html_temp = ``;
if ("liked" in matjip) {
html_temp = `<div class="card" id="card-${i}">
<div class="card-body">
<h5 class="card-title"><a href="javascript:click2center(${i})" class="matjip-title">${matjip['title']}</a></h5>
<h6 class="card-subtitle mb-2 text-muted">${matjip['category']}</h6>
<i class="fa fa-bookmark" onclick="bookmark('${matjip['title']}', '${matjip['address']}', 'unlike')"></i>
<p class="card-text">${matjip['address']}</p>
<p class="card-text" style="color:blue;">${matjip['show']}</p>
</div>
</div>`;
} else {
html_temp = `<div class="card" id="card-${i}">
<div class="card-body">
<h5 class="card-title"><a href="javascript:click2center(${i})" class="matjip-title">${matjip['title']}</a></h5>
<h6 class="card-subtitle mb-2 text-muted">${matjip['category']}</h6>
<i class="fa fa-bookmark-o" onclick="bookmark('${matjip['title']}', '${matjip['address']}', 'like')"></i>
<p class="card-text">${matjip['address']}</p>
<p class="card-text" style="color:blue;">${matjip['show']}</p>
</div>
</div>`;
}
$('#matjip-box').append(html_temp);
}
function get_matjips() {
$('#matjip-box').empty();
$.ajax({
type: "GET",
url: '/matjip',
data: {},
success: function (response) {
let matjips = response["matjip_list"]
for (let i = 0; i < matjips.length; i++) {
let matjip = matjips[i]
console.log(matjip)
make_card(i, matjip);
let marker = make_marker(matjip);
add_info(i, marker, matjip)
}
}
});
}
//마커 만들기
function make_marker(matjip) {
let marker_img = '';
if ("liked" in matjip) {
marker_img = '{{ url_for("static", filename="marker-liked.png") }}'
} else {
marker_img = '{{ url_for("static", filename="marker-default.png") }}'
}
let marker = new naver.maps.Marker({
position: new naver.maps.LatLng(matjip["mapy"], matjip["mapx"]),
map: map,
icon: marker_img
});
markers.push(marker);
return marker
}
//맛집정보
function add_info(i, marker, matjip) {
let html_temp = `<div class="iw-inner">
<h5>${matjip['title']}</h5>
<p>${matjip['address']}
</div>`;
let infowindow = new naver.maps.InfoWindow({
content: html_temp,
maxWidth: 200,
backgroundColor: "#fff",
borderColor: "#888",
borderWidth: 2,
anchorSize: new naver.maps.Size(15, 15),
anchorSkew: true,
anchorColor: "#fff",
pixelOffset: new naver.maps.Point(10, -10)
});
//맛집 정보 마커클릭했을때 보이기
infowindows.push(infowindow)
naver.maps.Event.addListener(marker, "click", function (e) {
if (infowindow.getMap()) {
infowindow.close();
} else {
infowindow.open(map, marker);
map.setCenter(infowindow.position)
//버튼눌렀을때 스크롤 움직이기
$("#matjip-box").animate({
scrollTop: $("#matjip-box").get(0).scrollTop + $(`#card-${i}`).position().top
}, 2000);
}
});
}
function click2center(i) {
let marker = markers[i]
let infowindow = infowindows[i]
if (infowindow.getMap()) {
infowindow.close();
} else {
infowindow.open(map, marker);
map.setCenter(infowindow.position)
}
}
function bookmark(title, address, action) {
$.ajax({
type: "POST",
url: "/like_matjip",
data: {
title_give: title,
address_give: address,
action_give: action
},
success: function (response) {
if (response["result"] == "success") {
get_matjips()
}
}
})
}
</script>
</head>
<body>
<div class="wrap">
<div class="banner">
<div class="d-flex flex-column align-items-center"
style="background-color: rgba(0,0,0,0.5);width: 100%;height: 100%;">
<h1 class="title mt-5 mb-2">스파르타 맛집 지도</h1>
<button type="button" onclick="get_matjips()" class="btn btn-sparta">
새로고침하고 더 많은 맛집 보기
</button>
</div>
</div>
<div id="map"></div>
<div class="matjip-list" id="matjip-box">
</div>
</div>
</body>
</html>
app.py
from flask import Flask, render_template, request, jsonify, redirect, url_for
from pymongo import MongoClient
app = Flask(__name__)
client = MongoClient('3.34.182.231', 27017, username="test", password="test")
db = client.dbsparta_plus_week3
@app.route('/')
def main():
return render_template("index.html")
@app.route('/matjip', methods=["GET"])
def get_matjip():
# 맛집 목록을 반환하는 API
matjip_list = list(db.matjips.find({}, {'_id': False}))
# matjip_list 라는 키 값에 맛집 목록을 담아 클라이언트에게 반환합니다.
return jsonify({'result': 'success', 'matjip_list': matjip_list})
@app.route('/like_matjip', methods=["POST"])
def like_matjip():
title_receive = request.form["title_give"]
address_receive = request.form["address_give"]
action_receive = request.form["action_give"]
print(title_receive, address_receive, action_receive)
if action_receive == "like":
db.matjips.update_one({"title": title_receive, "address": address_receive}, {"$set": {"liked": True}})
else:
#like라는 필드를 없애버려라
db.matjips.update_one({"title": title_receive, "address": address_receive}, {"$unset": {"liked": False}})
return jsonify({'result': 'success'})
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
에횽횽 어렵지만 재밌당~
결과 사이트 ㅎㅎ
http://3.34.182.231:5000/
'✍2021,2022 > WEB' 카테고리의 다른 글
드디어 ! . 완주 (0) | 2021.08.01 |
---|---|
웹개발+4주차 (0) | 2021.08.01 |
웹개발+/2주차 필기,개발일지 (0) | 2021.07.18 |
WEB 개발+ / 1주차 필기, 개발일지 (0) | 2021.07.12 |
5주차 필기/개발일지 (0) | 2021.07.01 |