웹개발 플러스 - 03주 개발일지
방금 막 스파르타 코딩클럽 웹개발 플러스 3주차 수업을 모두 들었다.
3주차 수업은 NAVER MAP API를 이용한 맛집지도 만들기 프로젝트였다.
이번주차 수업은 수업내용보다는 숙제가 어려웠다 ㅜㅜ;;
1. 완성본
완성본 app.py :
from flask import Flask, render_template, request, jsonify, redirect, url_for
from pymongo import MongoClientapp = Flask(name)
client = MongoClient('aws아이피주소', 27017, username="아이디", password="비밀번>호")
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 : 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)
완성본 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>
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<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="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 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=내 네이버 API 클라이언트 ID=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">
<!-- 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("{{ url_for('static', filename='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 = [];
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 temp_html = ``
if ("liked" in matjip) {
temp_html = `<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 {
temp_html = `<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(temp_html);
}
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>
</div>
</div>
<div id="map"></div>
<div class="matjip-list" id="matjip-box">
<div class="card" id="card-0">
<div class="card-body">
<h5 class="card-title"><a href="#" class="matjip-title">혼가츠</a></h5>
<h6 class="card-subtitle mb-2 text-muted">일식</h6>
<p class="card-text">서울 마포구 와우산로21길 36-6 (서교동)</p>
<p class="card-text" style="color:blue;">생방송 투데이</p>
</div>
</div>
</div>
</div>
</body>
</html>
2. 내가 꼭 기억해야하는 지식!
01. 셀레니움으로 스크래핑하기
-
2) 셀레니움 써서 스크래핑하기
-
멜론 차트처럼 동적인 웹페이지를 스크래핑할 때는 브라우저에 띄운 후 소스코드를 가져오는 방법을 써야합니다.
-
셀레니움 설치하기
-
크롬드라이버 다운로드
-
크롬 브라우저를 실제로 제어하는 chromedriver 파일을 다운로드 받아야합니다.
-
컴퓨터의 운영체제와 크롬 버전에 맞는 드라이버를 받아주세요.
-
[코드스니펫] - 크롬브라우저 버전 확인하기
chrome://settings/help
-
[코드스니펫] - 크롬 드라이버 다운로드 링크
https://chromedriver.storage.googleapis.com/index.html
-
압축을 풀어서 chromedriver 파일을 project03 폴더 안에 넣어주세요.
-
파이썬으로 이 크롬 드라이버를 동작시킬 selenium 패키지는 이미 설치했죠?
-
아까 짠 스크래핑 코드에 아래처럼 코드를 바꿔 써주면 셀레니움 적용 끝!
-
[코드스니펫] 셀레니움 적용하기
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
print(title, artist, likes)
-
결과가 이런 식으로 나오는군요!
-
'총건수'를 지우기 위해서는 이렇게 해줄 수 있습니다.
-
[코드스니펫] - 총건수 지우기
likes_tag = song.select_one("td > div > button.like > span.cnt")
likes_tag.span.decompose() # span 태그 없애기
likes = likes_tag.text.strip() # 텍스트화한 후 앞뒤로 빈 칸 지우기
-
3) 브라우저 제어 - 스크롤, 버튼
-
단순히 HTML을 띄우는 것 뿐만 아니라 셀레니움을 이용해서 스크롤, 버튼 클릭 등 다양한 동작을 할 수 있습니다.
-
네이버 이미지 검색창을 예시로 써서 브라우저 제어를 해보겠습니다.
-
[코드스니펫] - 네이버 이미지 검색창 스크래핑 코드
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(3)
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)
-
스크롤 내리기
-
셀레니움에서 스크롤을 내리고 싶을 땐 아래와 같은 코드를 이용합니다.
-
[코드스니펫] - 1000픽셀 만큼 스크롤 내리기
driver.execute_script("window.scrollTo(0, 1000)") # 1000픽셀만큼 내리기
-
화면의 맨 밑까지 내리고 싶다면 이렇게 해줄 수 있습니다.
-
[코드스니펫] - 맨 밑까지 내리기
sleep(1)
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
sleep(10)
02. 네이버 지도 연습하기 !!
-
7) 네이버 지도 갖고 놀기
-
확대/축소 버튼 넣기
-
map의 옵션을 추가해주면 됩니다.
-
[코드스니펫] 확대/축소 버튼
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
}
});
-
마커 띄우기
-
지도에 마커를 띄우기 위해서는 marker
오브젝트를 만들어주어야 합니다.
-
marker
를 얹을 지도(map
)와 경위도 좌표를 명시해주세요.
-
[코드스니펫] 마커 띄우기
let marker = new naver.maps.Marker({
position: new naver.maps.LatLng(37.4981125, 127.0379399),
map: map
});
-
마커 이미지 바꾸기
-
마커 이미지를 다른 모양으로 바꾸고 싶다면, marker
에 옵션으로 넣어주세요. 이미지 파일은 static 폴더에 넣어주면 되겠죠?
-
[코드스니펫] - 마커 이미지
https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/webplus/week03/rtan_heart.png
-
[코드스니펫] - 마커 이미지 바꾸기
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();
-
[코드스니펫] 마커를 누를 때마다 infoWindow 여닫기
naver.maps.Event.addListener(marker, "click", function () {
console.log(infowindow.getMap()); // 정보창이 열려있을 때는 연결된 지도를 반환하고 닫혀있을 때는 null을 반환
if (infowindow.getMap()) {
infowindow.close();
} else {
infowindow.open(map, marker);
}
});
03. 맛집 정보 스크래핑하기
-
13) 스크래핑해 올 사이트 살펴보기
-
맛집 정보는 SBS TV 맛집 사이트에서 스크래핑해오겠습니다.
-
[코드스니펫] - SBS TV 맛집 링크
http://matstar.sbs.co.kr/location.html
-
각 카드 안에 식당의 이름, 주소, 출연 프로그램, 카테고리 등의 정보가 있습니다.
-
페이지 하단의 버튼을 눌러 더 많은 맛집 정보를 받아올 수 있습니다.
-
14) 셀레니움으로 스크래핑하기
-
[코드스니펫] - scraping.py
from selenium import webdriver
from bs4 import BeautifulSoup
import time
from selenium.common.exceptions import NoSuchElementException
from pymongo import MongoClient
import requests
client = MongoClient('내AWS아이피', 27017, username="아이디", password="비밀번호")
db = client.dbsparta_plus_week3
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)
04. 맛집 정보 좌표로 변환하기
-
15) 추가 정보 받기
-
맛집을 지도 위에 나타내기 위해서는 경위도 좌표가 필요합니다. 다행히 네이버에서 제공하는 API 중에 주소를 좌표로 변환해주는 geocoding
API가 있습니다.
-
[코드스니펫] - geocoding API 참조서 링크
https://api.ncloud-docs.com/docs/ai-naver-mapsgeocoding-geocode
-
사용 신청은 전에 지도 API 신청하면서 같이 했기 때문에, 바로 사용할 수 있습니다.
-
요청을 보낼 때 Client ID와 Client Secret Key 모두 보내주어야합니다.
-
[코드스니펫] - 네이버 클라우드 플랫폼 콘솔
https://console.ncloud.com/mc/solution/naverService/application?version=v2
-
[코드스니펫] - Geocoding 연결하기
headers = {
"X-NCP-APIGW-API-KEY-ID": "[내 클라이언트 아이디]",
"X-NCP-APIGW-API-KEY": "[내 클라이언트 시크릿 키]"
}
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, "좌표를 찾지 못했습니다")
05. 고급 기능 쓰기
-
23) 카드 보이게 스크롤 움직이기
-
[코드스니펫] - 카드 보이게 스크롤 움직이기
$("#matjip-box").animate({
scrollTop: $("#matjip-box").get(0).scrollTop + $(`#card-${i}`).position().top
}, 2000);
-
24) 카드 제목 클릭했을 때 정보창 띄우기
a 태그를 클릭했을 때 새창이 뜨는게 아니라 javascript 함수를 실행하고 싶다면?
→ ...
-
[코드스니펫] - i번째 카드를 눌렀을 때 해당 정보창을 열고 닫는 기능
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)
}
}
-
[코드스니펫] - make_card 안의 a 태그 수정
<a href="javascript:click2center(${i})" class="matjip-title">${matjip['title']}</a>
-
25) og태그, favicon 넣기
-
이번주도 완성도 있는 사이트를 위해 Open Graph 태그와 favicon을 넣어봅시다. og 이미지는 배너 이미지를 복사해서 사용하고 favicon은 아래 파일을 다운 받아 static 폴더에 넣어줍시다.
-
[코드스니펫] - favicon
https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/webplus/week03/favicon.ico
-
index.html 위에 링크를 첨부합니다.
-
[코드스니펫] - og태그와 favicon 링크
<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="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">
-
26) 완성 코드
-
[코드스니펫] - app.py
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)
-
[코드스니펫] - 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_img.png') }}"/>
<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">
<script type="text/javascript"
src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID&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">
<!-- 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>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Jua&display=swap" rel="stylesheet">
<style>
.wrap {
width: 90%;
max-width: 750px;
margin: 0 auto;
}
.banner {
width: 100%;
height: 20vh;
background-image: url("{{ url_for("static", filename="banner.png") }}");
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;
}
.btn-sparta {
color: #fff;
background-color: #e8344e;
border-color: #e8344e;
}
.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 = []
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 get_matjips() {
$('#matjip-box').empty();
markers = []
infowindows = []
$.ajax({
type: "GET",
url: `/matjip`,
data: {},
success: function (response) {
let matjips = response["matjip_list"]
console.log(matjips.length)
for (let i = 0; i < matjips.length; i++) {
let matjip = matjips[i]
make_card(i, matjip)
let marker = make_marker(matjip)
add_info(i, marker, matjip)
}
}
});
}
function make_marker(matjip) {
let marker = new naver.maps.Marker({
position: new naver.maps.LatLng(matjip["mapy"], matjip["mapx"]),
map: map
});
markers.push(marker)
return marker
}
function make_card(i, matjip) {
let 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>
<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 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) {
console.log("clicked", infowindows.length)
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)
}
}
</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 class="card" id="card-0">
<div class="card-body">
<h5 class="card-title"><a href="#" class="matjip-title">혼가츠</a></h5>
<h6 class="card-subtitle mb-2 text-muted">일식</h6>
<p class="card-text">서울 마포구 와우산로21길 36-6 (서교동)</p>
<p class="card-text" style="color:blue;">생방송 투데이</p>
</div>
</div>
</div>
</div>
</body>
</html>
Author And Source
이 문제에 관하여(웹개발 플러스 - 03주 개발일지), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@dnwlsdl0419/웹개발-플러스-03주-개발일지
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
2) 셀레니움 써서 스크래핑하기
-
멜론 차트처럼 동적인 웹페이지를 스크래핑할 때는 브라우저에 띄운 후 소스코드를 가져오는 방법을 써야합니다.
-
셀레니움 설치하기
-
크롬드라이버 다운로드
-
크롬 브라우저를 실제로 제어하는 chromedriver 파일을 다운로드 받아야합니다.
-
컴퓨터의 운영체제와 크롬 버전에 맞는 드라이버를 받아주세요.
-
[코드스니펫] - 크롬브라우저 버전 확인하기
chrome://settings/help
-
[코드스니펫] - 크롬 드라이버 다운로드 링크
https://chromedriver.storage.googleapis.com/index.html
-
-
압축을 풀어서 chromedriver 파일을 project03 폴더 안에 넣어주세요.
-
-
파이썬으로 이 크롬 드라이버를 동작시킬 selenium 패키지는 이미 설치했죠?
-
-
아까 짠 스크래핑 코드에 아래처럼 코드를 바꿔 써주면 셀레니움 적용 끝!
-
[코드스니펫] 셀레니움 적용하기
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 print(title, artist, likes)
-
-
결과가 이런 식으로 나오는군요!
-
'총건수'를 지우기 위해서는 이렇게 해줄 수 있습니다.
-
[코드스니펫] - 총건수 지우기
likes_tag = song.select_one("td > div > button.like > span.cnt") likes_tag.span.decompose() # span 태그 없애기 likes = likes_tag.text.strip() # 텍스트화한 후 앞뒤로 빈 칸 지우기
-
3) 브라우저 제어 - 스크롤, 버튼
-
단순히 HTML을 띄우는 것 뿐만 아니라 셀레니움을 이용해서 스크롤, 버튼 클릭 등 다양한 동작을 할 수 있습니다.
-
네이버 이미지 검색창을 예시로 써서 브라우저 제어를 해보겠습니다.
-
[코드스니펫] - 네이버 이미지 검색창 스크래핑 코드
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(3) 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)
-
-
스크롤 내리기
-
셀레니움에서 스크롤을 내리고 싶을 땐 아래와 같은 코드를 이용합니다.
-
[코드스니펫] - 1000픽셀 만큼 스크롤 내리기
driver.execute_script("window.scrollTo(0, 1000)") # 1000픽셀만큼 내리기
-
-
화면의 맨 밑까지 내리고 싶다면 이렇게 해줄 수 있습니다.
-
[코드스니펫] - 맨 밑까지 내리기
sleep(1) driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") sleep(10)
-
-
7) 네이버 지도 갖고 놀기
-
확대/축소 버튼 넣기
-
map의 옵션을 추가해주면 됩니다.
-
[코드스니펫] 확대/축소 버튼
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 } });
-
-
마커 띄우기
-
지도에 마커를 띄우기 위해서는
marker
오브젝트를 만들어주어야 합니다. -
marker
를 얹을 지도(map
)와 경위도 좌표를 명시해주세요. -
[코드스니펫] 마커 띄우기
let marker = new naver.maps.Marker({ position: new naver.maps.LatLng(37.4981125, 127.0379399), map: map });
-
-
마커 이미지 바꾸기
-
마커 이미지를 다른 모양으로 바꾸고 싶다면,
marker
에 옵션으로 넣어주세요. 이미지 파일은 static 폴더에 넣어주면 되겠죠? -
[코드스니펫] - 마커 이미지
https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/webplus/week03/rtan_heart.png
-
[코드스니펫] - 마커 이미지 바꾸기
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();
-
[코드스니펫] 마커를 누를 때마다 infoWindow 여닫기
naver.maps.Event.addListener(marker, "click", function () { console.log(infowindow.getMap()); // 정보창이 열려있을 때는 연결된 지도를 반환하고 닫혀있을 때는 null을 반환 if (infowindow.getMap()) { infowindow.close(); } else { infowindow.open(map, marker); } });
-
13) 스크래핑해 올 사이트 살펴보기
-
맛집 정보는 SBS TV 맛집 사이트에서 스크래핑해오겠습니다.
-
[코드스니펫] - SBS TV 맛집 링크
http://matstar.sbs.co.kr/location.html
-
-
각 카드 안에 식당의 이름, 주소, 출연 프로그램, 카테고리 등의 정보가 있습니다.
-
페이지 하단의 버튼을 눌러 더 많은 맛집 정보를 받아올 수 있습니다.
14) 셀레니움으로 스크래핑하기
-
[코드스니펫] - scraping.py
from selenium import webdriver from bs4 import BeautifulSoup import time from selenium.common.exceptions import NoSuchElementException from pymongo import MongoClient import requests client = MongoClient('내AWS아이피', 27017, username="아이디", password="비밀번호") db = client.dbsparta_plus_week3 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)
15) 추가 정보 받기
-
맛집을 지도 위에 나타내기 위해서는 경위도 좌표가 필요합니다. 다행히 네이버에서 제공하는 API 중에 주소를 좌표로 변환해주는
geocoding
API가 있습니다.-
[코드스니펫] - geocoding API 참조서 링크
https://api.ncloud-docs.com/docs/ai-naver-mapsgeocoding-geocode
-
-
사용 신청은 전에 지도 API 신청하면서 같이 했기 때문에, 바로 사용할 수 있습니다.
-
요청을 보낼 때 Client ID와 Client Secret Key 모두 보내주어야합니다.
-
[코드스니펫] - 네이버 클라우드 플랫폼 콘솔
https://console.ncloud.com/mc/solution/naverService/application?version=v2
-
[코드스니펫] - Geocoding 연결하기
headers = { "X-NCP-APIGW-API-KEY-ID": "[내 클라이언트 아이디]", "X-NCP-APIGW-API-KEY": "[내 클라이언트 시크릿 키]" } 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, "좌표를 찾지 못했습니다")
-
23) 카드 보이게 스크롤 움직이기
-
[코드스니펫] - 카드 보이게 스크롤 움직이기
$("#matjip-box").animate({ scrollTop: $("#matjip-box").get(0).scrollTop + $(`#card-${i}`).position().top }, 2000);
24) 카드 제목 클릭했을 때 정보창 띄우기
a 태그를 클릭했을 때 새창이 뜨는게 아니라 javascript 함수를 실행하고 싶다면?
→ ...
-
[코드스니펫] - i번째 카드를 눌렀을 때 해당 정보창을 열고 닫는 기능
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) } }
-
[코드스니펫] - make_card 안의 a 태그 수정
<a href="javascript:click2center(${i})" class="matjip-title">${matjip['title']}</a>
25) og태그, favicon 넣기
-
이번주도 완성도 있는 사이트를 위해 Open Graph 태그와 favicon을 넣어봅시다. og 이미지는 배너 이미지를 복사해서 사용하고 favicon은 아래 파일을 다운 받아 static 폴더에 넣어줍시다.
-
[코드스니펫] - favicon
https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/webplus/week03/favicon.ico
-
-
index.html 위에 링크를 첨부합니다.
-
[코드스니펫] - og태그와 favicon 링크
<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="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">
-
26) 완성 코드
-
[코드스니펫] - app.py
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)
-
[코드스니펫] - 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_img.png') }}"/> <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"> <script type="text/javascript" src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID&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"> <!-- 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> <link rel="preconnect" href="https://fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css2?family=Jua&display=swap" rel="stylesheet"> <style> .wrap { width: 90%; max-width: 750px; margin: 0 auto; } .banner { width: 100%; height: 20vh; background-image: url("{{ url_for("static", filename="banner.png") }}"); 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; } .btn-sparta { color: #fff; background-color: #e8344e; border-color: #e8344e; } .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 = [] 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 get_matjips() { $('#matjip-box').empty(); markers = [] infowindows = [] $.ajax({ type: "GET", url: `/matjip`, data: {}, success: function (response) { let matjips = response["matjip_list"] console.log(matjips.length) for (let i = 0; i < matjips.length; i++) { let matjip = matjips[i] make_card(i, matjip) let marker = make_marker(matjip) add_info(i, marker, matjip) } } }); } function make_marker(matjip) { let marker = new naver.maps.Marker({ position: new naver.maps.LatLng(matjip["mapy"], matjip["mapx"]), map: map }); markers.push(marker) return marker } function make_card(i, matjip) { let 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> <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 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) { console.log("clicked", infowindows.length) 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) } } </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 class="card" id="card-0"> <div class="card-body"> <h5 class="card-title"><a href="#" class="matjip-title">혼가츠</a></h5> <h6 class="card-subtitle mb-2 text-muted">일식</h6> <p class="card-text">서울 마포구 와우산로21길 36-6 (서교동)</p> <p class="card-text" style="color:blue;">생방송 투데이</p> </div> </div> </div> </div> </body> </html>
Author And Source
이 문제에 관하여(웹개발 플러스 - 03주 개발일지), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dnwlsdl0419/웹개발-플러스-03주-개발일지저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)