그저 내가 되었고

항해99) 1주차:: 웹개발+ 3주차; Selenium으로 스크래핑 본문

개발/항해99 9기

항해99) 1주차:: 웹개발+ 3주차; Selenium으로 스크래핑

hyuunii 2022. 9. 21. 13:10

- 브라우저 제어:

  • 내가 필요한 정보를 얻기 위해 로그인, 스크롤 내리기 등 브라우저를 동작시켜야 할 때! selenium 같은 브라우저 제어 프로그램을 이용 가능
  • 웹스크래핑 뿐만 아니라 브라우저 제어 기능을 응용하면 정해진 시간에 게시판에 글을 작성하는 등 다양한 업무를 자동화하는 데 쓰일 수 있음!

03. 셀레니움으로 스크래핑하기 - 2

0] 스크래핑 복습

- 웹 스크래핑(web scraping): 웹 페이지에서 우리가 원하는 부분의 데이터를 수집해오는 것을 뜻함

- 한국에서는 같은 작업을 크롤링 crawling 이라는 용어로 혼용해서 쓰는 경우가 많습니다. 원래는 크롤링은 자동화하여 주기적으로 웹 상에서 페이지들을 돌아다니며 분류/색인하고 업데이트된 부분을 찾는 등의 일을 하는 것을 뜻함.

- 구글 검색을 할 때는 web scraping 으로 검색해야 우리가 배우는 페이지 추출에 대한 결과가 나올 거예요!

 

1] 셀레니움 이용하려면? 일단 드라이버 설치

링크: https://chromedriver.storage.googleapis.com/index.html

 

2] 예시 코드(멜론 좋아요까지 스크랩핑)

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)

 

- 스크롤 내리기

1. 1000px만큼

driver.execute_script("window.scrollTo(0, 1000)")  # 1000픽셀만큼 내리기

 

2. 맨 밑까지

sleep(1)
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
sleep(10)

 

- 위의 것 전부 넣은 코드

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)
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)

04. 네이버 지도 API

- https://console.ncloud.com/naver-service/application : 들어가면 API Application 이용 신청 내역 확인 가능

- 뼈대 API 코드:

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
    return jsonify({'result': 'success', 'matjip_list': []})

if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

- 뼈대 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=((((내 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>
        <style>
            .wrap {

            }

            .banner {

            }

            .matjip-list {

            }

            #map {
		            width: 100%;
                height: 50vh;
                margin: 20px auto 20px auto;
            }
        </style>
        <script>
            let y_cen = 37.4981125   // lat
            let x_cen = 127.0379399  // long
            let map;
            $(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
                    }
                });
            })
        </script>

    </head>

    <body>
        <div class="wrap">
            <div class="banner"></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>

- 배너 이미지도 static에 잘 넣어주자~!


07. 맛집 정보 스크래핑하기

13) 스크래핑해 올 사이트 살펴보기: http://matstar.sbs.co.kr/location.html

(스크롤, ⩢ 클릭해서 더보기 등 이런건 셀레니움이 편하겠다~~~! 하는거쥐)

 

- 뼈대 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))

👆🏻 얘를 붙여넣음. 이렇게👇🏻

....
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)

👆🏻 얘를 붙여넣음. 이렇게👇🏻

....
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)

08. 맛집 정보 좌표로 변환하기

15) 추가 정보 받기(저 맛집들이 지도에서 위치가 어딘지 모르면,, 아무 쓸모 없자나요,,, 그것 표시해주자~!)

- 맛집을 지도 위에 나타내기 위해서는 경위도 좌표가 필요.. 다행히 네이버에서 제공하는 API 중에 주소를 좌표로 변환해주는 gecoding API가 있음~!

 

- 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()

👆🏻 이거고 들어가면 이렇게 됨👇🏻

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)

    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()

밑에서 두 번째 줄... requests의 get 요청 할 때 headers에 headers 넣어서 보냄. 그 heards에 id&secret 값이 넘어감.

그리고 그 뒷쪽에 빨간색으로 {address} 이건 코드 좀 위에 보면 위에서 세번째 줄에 address를 우리가 잘 받았음!

이후 print(response) 돌려보면 밑의 결과 쭉.... 오른쪽으로 밀어보면 x와 y로 위도와 경도 각각 잘 찍혀있음~~~!!

 

- 주소에 오류가 있어 결과를 하나도 받지 못하는 경우가 있으므로 결과가 있을 때만 값을 출력하도록 함:

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, "좌표를 찾지 못했습니다")

 

얘👆🏻를 넣을거고(x = float~ 이게 뭐?! 음ㅎㅎ 위도와 경도를 그냥 꺼내면 문자인데 우린 이걸 숫자로 받아야 함!!! 것도 소수!!!ㅋㅋ 파이참에서 소수는 float), 이게 결과 👇🏻

....
    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, "좌표를 찾지 못했습니다")

09. 맛집 정보 DB에 저장하기

16) 여러 페이지 스크래핑하기: 버튼을 클릭하여 더 많은 맛집 정보를 받아올 수 있도록

- 더보기 버튼의 선택자로 버튼 클릭하기

btn_more = driver.find_element_by_css_selector("#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(5)

👆🏻이게 들어가고, 결과가👇🏻

? 왜 저 위치에 들어감? 드라이버가 닫히기 전에, 어떤 페이지 소스를 로드하기 전에. 왜? 페이지 소스를 로드하고 나면 그 페이지 안에서 정보를 가져오니까.. 로드 되기 전에 최대한 많은 맛집을 띄우는 것!!

? 저 css_selector(여기)는 뭔데? 저건 더보기 버튼!!!!ㅋㅋ btn_more 안에 더보기 버튼이 저장이 되고 클릭한 후 3초 기다리라는 것~!

driver.get(url)
time.sleep(3)

btn_more = driver.find_element_by_css_selector("#foodstar-front-location-curation-more-self > div > button")
btn_more.click()
time.sleep(3)

req = driver.page_source
driver.quit()

 

- 더보기 버튼을 10번 누르려면?!

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

? 10번 더 누른다는건,, 반복문 쓴다는거고!!

? try&except는 오류 대처 위해 쓰는 것!! try는 별 일 없으면 저렇게 시도하라는 거고, except는 뭔가 오류 났을떄! 단적으로, 더보기 버튼이 더 없을수도 있잖아ㅋ 그럴땐 고만둬라~!

 

- db에 저장하기

doc = {
    "title": title,
    "address": address,
    "category": category,
    "show": show,
    "episode": episode,
    "mapx": x,
    "mapy": y}
db.matjips.insert_one(doc)

👆🏻 얘를 붙일거고, 붙이면 이렇게 됨👇🏻

 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)

 

- 그런데!!!!! 얼마전에 셀레니움 문법이 업데이트돼서!!!!! find_element_by_css_selector는 더이상 안먹음!!!

btn_more = driver.find_element(By. CSS_SELECTOR, "#foodstar-front-location-curation-more-self > div > button")

👆🏻이렇게 수정 필요 & 임포트 필요👇🏻

from selenium.webdriver.common.by import By

 

- 스크래핑 완성 코드:

from selenium import webdriver
from bs4 import BeautifulSoup
import time
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from pymongo import MongoClient
import requests

client = MongoClient('mongodb://test:sparta@ac-qs2cnvy-shard-00-00.u33zjtg.mongodb.net:27017,ac-qs2cnvy-shard-00-01.u33zjtg.mongodb.net:27017,ac-qs2cnvy-shard-00-02.u33zjtg.mongodb.net:27017/Cluster0?ssl=true&replicaSet=atlas-4g7ly1-shard-0&authSource=admin&retryWrites=true&w=majority')
db = client.dbsparta_plus_week3

driver = webdriver.Chrome('./chromedriver')

url = "http://matstar.sbs.co.kr/location.html"

driver.get(url)
time.sleep(3)

for i in range(5):
    try:
        btn_more = driver.find_element(By. CSS_SELECTOR, "#foodstar-front-location-curation-more-self > div > button")
        btn_more.click()
        time.sleep(3)
    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)

    headers = {
        "X-NCP-APIGW-API-KEY-ID": "imjf14rzhn",
        "X-NCP-APIGW-API-KEY": "egRMUijnsZQ1A3O7Y6bW4lJPtob6WucG2si7RbvR"
    }
    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, "좌표를 찾지 못했습니다")

10. 웹사이트 모습 만들기

17) 배너, 지도, 카드영역 만들고 꾸미기

<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>
<style>
        #map {
            width: 100%;
            height: 50vh;
            margin: 20px auto 20px auto;
        }

        .wrap {
            width: 90%;
            max-width: 750px;
            margin: 0 auto;
        }

        .banner {
            width: 100%;
            height: 20vh;
            background-image: url("{{ url_for('static', filename='IMG_0945.jpg') }}");
            background-position: center;
            background-size: cover;
            background-repeat: repeat;
        }

        h1.title {
            color: white;
            font-size: 3rem;
        }

        .matjip-list {
            overflow: scroll;
            width: 100%;
            height: calc(20vh - 30px);
            position: relative;
        }

        .card-title, .card-subtitle {
            display: inline;
        }

    </style>

 

19) DB에서 맛집정보 받아오기

@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})
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)
            }
        }
    });
}

👆🏻여기의 function (response)의 response는 서버단에서 준 것({'result': 'success', 'matjip_list': matjip_list}))

 

- 카드 만들기: db에서 받아온 데이터를 이용해 각 맛집 별로 카드 하나씩 만드는 함수를 만듦

function make_card(i, matjip) {
    let html_temp = `<div class="card" id="card-${i}">
                        <div class="card-body">
                            <h5 class="card-title"><a href="#" 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);
}

👆🏻 i도 변수로 받네........ 맨 첫줄은 make_card라는 함수가 i와 matjip을 변수로 받았다는 의미