웹개발 플러스 - 03주 개발일지

방금 막 스파르타 코딩클럽 웹개발 플러스 3주차 수업을 모두 들었다.
3주차 수업은 NAVER MAP API를 이용한 맛집지도 만들기 프로젝트였다.
이번주차 수업은 수업내용보다는 숙제가 어려웠다 ㅜㅜ;;

1. 완성본

완성본 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")

@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. 고급 기능 쓰기

좋은 웹페이지 즐겨찾기