그저 내가 되었고
항해99) 1주차:: 웹개발+ 3주차; Selenium으로 스크래핑 본문
- 브라우저 제어:
- 내가 필요한 정보를 얻기 위해 로그인, 스크롤 내리기 등 브라우저를 동작시켜야 할 때! 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을 변수로 받았다는 의미
'개발 > 항해99 9기' 카테고리의 다른 글
항해99) 2주차:: 프로그래밍 기초; JavaScript for Node.js START!!! (0) | 2022.09.23 |
---|---|
항해99) 1주차:: 미니프로젝트 회고(p.s. 끝나지 않는 CSS 집착.....) (0) | 2022.09.23 |
항해99) 1주차:: 웹개발+ 2주차; SSR(Server Side Rendering) WORDBOOK 전체 코드 (0) | 2022.09.21 |
항해99) 1주차:: 웹개발+ 2주차; SSR(Server Side Rendering) (0) | 2022.09.21 |
항해99) 1주차:: 웹개발+ 1주차; 파일 업로드 (0) | 2022.09.19 |