나홀로 메모장이라는 작은 프로젝트를 만들어보면서 flask
프레임워크를 사용하여 서버 만드는 과정을 연습해보겠다.
[alonememo]
L
static
L
templates
. .. .L
index.html
L
app.py
서버구동을 하기 위한 프레임워크인 flask
와
데이터베이스를 조작하기 위한 라이브러리인 pymongo
를 설치해준다.
크롤링을 하기 위해 파이썬 패키지인 requests
와 bs4
를 설치해준다.
pip install flask
pip install pymongo
pip install requests
pip install bs4
모든 프로젝트를 시작할 때에는 API를 설계하는 일이 가장 처음 할 일이다.
그래야 어떤 기능들이 필요하고 그 기능을 어떤 순서로 구현을 할 것인지 계획을 잡기 때문이다.
나홀로메모장에서 필요한 기능
- url
, comment
를 서버쪽으로 보내주고 서버는 받은 데이터를 저장하는 것 ➡ POST
- 메모장카드들을 보여주는 것, 즉 데이터를 보여주는 것 ➡ GET
데이터베이스에 저장되어야 할 요소
- 이미지
- 기사제목
- 링크
- 내용
- 코멘트
API 설계하기
URL에서 페이지 정보를 가져오기 위한
API를 잘 설계 했는지 검증해보기 위해 조각기능을 먼저 구현해보는 것이 좋다.
메타태그란 <head></head>
부분에 들어가는, 눈으로 보이는 것<body></body>
외에 사이트의 속성을 설명해주는 태그이다.
- 예 : 카톡 공유 시 표시될 제목, 설명, 이미지 등
메타태그중 og:image
|og:title
|og:desc
를 크롤링 하려고 한다.
import requests
from bs4 import BeautifulSoup
url = 'https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539'
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)
soup = BeautifulSoup(data.text, 'html.parser')
# 여기에 코딩을 해서 meta tag를 먼저 가져와보겠습니다.
select_one
을 이용해 metatag
가져오기import requests
from bs4 import BeautifulSoup
url = 'https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539'
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)
soup = BeautifulSoup(data.text, 'html.parser')
# og:title copy select로 가져오기
title = soup.select_one('head > meta:nth-child(9)')
print(title)
# 결과
None
원래 크롤링 하던 방법으로 copy select를 붙여넣으면 결과가 나오지 않는데... 이것이 바로 크롤링의 묘미란다...허허..
브라우저에서 개발자 도구에 나오는 메타태그의 순서와 파이썬 코드가 접속했을때의 메타태그의 순서가 다르기 때문에 이런다고 한다.
따라서 메타태그를 가져올때는 속성값을 통해 가져온다.
# og:title metatag 속성값으로 가져오기
title = soup.select_one('meta[property="og:title"]')
print(title)
# 결과
<meta content="그린 북" property="og:title"/>
---------------------------------------------------------------
# 제목만 가져오기
title = soup.select_one('meta[property="og:title"]')['content']
print(title)
# 결과
그린 북
import requests
from bs4 import BeautifulSoup
url = 'https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539'
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)
soup = BeautifulSoup(data.text, 'html.parser')
# og:image | og:title | og:description 가져오기
title = soup.select_one('meta[property="og:title"]')['content']
img = soup.select_one('meta[property="og:image"]')['content']
desc = soup.select_one('meta[property="og:description"]')['content']
print(title,img,desc)
# 결과
그린 북 https://movie-phinf.pstatic.net/20190115_228/1547528180168jgEP7_JPEG/movie_image.jpg?type=m665_443_2 1962년 미국, 입담과 주먹만 믿고 살아가던 토니 발레롱가(비고 모텐슨)는 교양과 우아함 그 자체인천재...
app.py
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)
import requests
from bs4 import BeautifulSoup
from pymongo import MongoClient
client = MongoClient('localhost', 27017)
db = client.dbsparta
## HTML을 주는 부분
@app.route('/')
def home():
return render_template('index.html')
@app.route('/memo', methods=['GET'])
def listing():
sample_receive = request.args.get('sample_give')
print(sample_receive)
return jsonify({'msg':'GET 연결되었습니다!'})
## API 역할을 하는 부분
@app.route('/memo', methods=['POST'])
def saving():
sample_receive = request.form['sample_give']
print(sample_receive)
return jsonify({'msg':'POST 연결되었습니다!'})
if __name__ == '__main__':
app.run('0.0.0.0',port=5000,debug=True)
index.html
<!Doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- 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">
<!-- 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>
<!-- 구글폰트 -->
<link href="https://fonts.googleapis.com/css?family=Stylish&display=swap" rel="stylesheet">
<title>스파르타코딩클럽 | 나홀로 메모장</title>
<!-- style -->
<style type="text/css">
* {
font-family: "Stylish", sans-serif;
}
.wrap {
width: 900px;
margin: auto;
}
.comment {
color: blue;
font-weight: bold;
}
#post-box {
width: 500px;
margin: 20px auto;
padding: 50px;
border: black solid;
border-radius: 5px;
}
</style>
<script>
$(document).ready(function () {
showArticles();
});
function openClose() {
if ($("#post-box").css("display") == "block") {
$("#post-box").hide();
$("#btn-post-box").text("포스팅 박스 열기");
} else {
$("#post-box").show();
$("#btn-post-box").text("포스팅 박스 닫기");
}
}
function postArticle() {
$.ajax({
type: "POST",
url: "/memo",
data: {sample_give:'샘플데이터'},
success: function (response) { // 성공하면
alert(response["msg"]);
}
})
}
function showArticles() {
$.ajax({
type: "GET",
url: "/memo?sample_give=샘플데이터",
data: {},
success: function (response) {
alert(response["msg"]);
}
})
}
</script>
</head>
<body>
<div class="wrap">
<div class="jumbotron">
<h1 class="display-4">나홀로 링크 메모장!</h1>
<p class="lead">중요한 링크를 저장해두고, 나중에 볼 수 있는 공간입니다</p>
<hr class="my-4">
<p class="lead">
<button onclick="openClose()" id="btn-post-box" type="button" class="btn btn-primary">포스팅 박스 열기
</button>
</p>
</div>
<div id="post-box" class="form-post" style="display:none">
<div>
<div class="form-group">
<label for="post-url">아티클 URL</label>
<input id="post-url" class="form-control" placeholder="">
</div>
<div class="form-group">
<label for="post-comment">간단 코멘트</label>
<textarea id="post-comment" class="form-control" rows="2"></textarea>
</div>
<button type="button" class="btn btn-primary" onclick="postArticle()">기사저장</button>
</div>
</div>
<div id="cards-box" class="card-columns">
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
<div class="card">
<img class="card-img-top"
src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="#" class="card-title">여기 기사 제목이 들어가죠</a>
<p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
<p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
</div>
</div>
</div>
</div>
</body>
</html>
만들어야 할 POST API
클라이언트에서 받은 URL
, comment
를 이용해 페이지 정보를 찾고 저장하기
만드는 순서
- 클라이언트와 서버 연결 확인
- 서버부터 만들기
- 클라이언트 만들기
- 완성 확인하기
app.py
(서버코드)
@app.route('/memo', methods=['POST'])
def post_Articles():
sample_receive = request.form['sample_give']
print(sample_receive)
return jsonify({'msg': 'POST 연결되었습니다!'})
index.html
(클라이언트 코드)
function postArticle() {
$.ajax({
type: "POST",
url: "/memo",
data: {sample_give:'샘플데이터'},
success: function (response) { // 성공하면
alert(response['msg']);
}
})
}
<button type="button" class="btn btn-primary" onclick="postArticle()">기사저장</button>
이때 기사저장을 클릭했을 때, 'POST 연결되었습니다!' alert창 이 뜨면 클라이언트 코드와 서버 코드가 잘 연결 되어있다는 것이다 !
서버가 전달받아야 할 정보
- url
(url_receive
)
- comment
(comment_receive
)
서버가 할일
- 받은 url을 통해 메타태그(url
/title
/desc
/img
/comment
) 크롤링하기
- 몽고디비에 데이터 저장하기
- 성공 메세지 반환하기
app.py
@app.route('/memo', methods=['POST'])
def post_Article():
# url, comment 받아오기
url_receive = request.form['url_give']
comment_receive = request.form['comment_give']
# 받아온 url을 통해 title, image, desc 크롤링해오기
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_receive, headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')
# og:image | og:title | og:description 가져오기
title = soup.select_one('meta[property="og:title"]')['content']
img = soup.select_one('meta[property="og:image"]')['content']
desc = soup.select_one('meta[property="og:description"]')['content']
# db에 저장하기
doc = {
'title' : title,
'img' : img,
'desc' : desc,
'url' : url_receive,
'comment' : comment_receive
}
db.alonememo.insert_one(doc)
return jsonify({'msg':'저장이 완료되었습니다😊'})
클라이언트가 주어야 할 정보
- url
(url_give
) : 유저가 입력한 url
- comment
(comment_give
) : 유저가 입력한 코멘트
클라이언트가 할일
- 유저가 입력한 url
, comment
를 input-box
(#post-url
/#post-comment
)에서 가져오기
- 가져온 데이터 정보를 url_give
, url_comment
에 담아 메모 생성 요청하기
- 성공시 페이지 새로고침하기
index.html
function postArticle() {
// 유저가 입력한 input-box값 (url,comment) 가져오기
let url = $('#post-url').val()
let comment = $('#post-comment').val()
// 메모생성 요청하기
$.ajax({
type: "POST",
url: "/memo",
data: {url_give:url, comment_give:comment},
success: function (response) {
// 성공하면 alert msg 보이기
alert(response["msg"]);
// 페이지 새로고침하기
window.location.reload()
}
})
}
url
, comment
입력하면 DB에 데이터가 잘 들어오는 것을 확인 할 수 있다.
만들어야 할 GET API
- 저장된 카드 보여주기
만드는순서
- 클라이언트와 서버 연결 확인
- 서버부터 만들기
- 클라이언트 만들기
- 완성 확인하기
app.py
(서버코드)
@app.route('/memo', methods=['GET'])
def show_Articles():
# 1. 모든 document 찾기 & _id 값은 출력에서 제외하기
# 2. articles라는 키 값으로 영화정보 내려주기
return jsonify({'result':'success', 'msg':'GET 연결되었습니다!'})
index.html
(클라이언트코드)
function showArticles() {
$.ajax({
type: "GET",
url: "/memo",
data: {},
success: function (response) {
if (response["result"] == "success") {
alert(response["msg"]);
}
}
})
}
페이지를 새로고침 했을 때 'GET 연결되었습니다' alert창이 뜨면 클라이언트 코드와 서버 코드가 잘 연결되어있는 것이다 !
서버가 전달 받아야 할 정보
메모를 보여주기 위해 추가로 전달받아야하는 정보는 없다
서버가 할일
- 몽고디비에서 _id
값을 제외한 모든 데이터 가져오기
- all_articel
이라는 키 값으로 article
정보를 클라이언트에 보내주기
app.py
@app.route('/memo', methods=['GET'])
def show_Article():
# 몽고디비에 저장된 데이터 가져오기
article = list(db.alonememo.find({}, {'_id': False}))
# artical 정보 보내주기
return jsonify({'all_article':article})
클라이언트가 주어야 할 정보
메모를 보여주기 위해 서버에게 줘야할 정보는 없다. 조건없이 모든 메모를 가져오기 때문!
클라이언트가 할일
- 메모 정보를 요청하고, 서버가 보내준 article
로 정보 받기
- 메모카드 html 붙이기
index.html
function showArticle() {
// 메모정보 요청하기
$.ajax({
type: "GET",
url: "/memo",
data: {},
success: function (response) {
// 서버가 보내준 정보를 변수에 담기
let article = response['all_article']
// 반복문을 돌려 title, img, desc, url, comment 가져오기
for (let i=0; i <article.length; i++) {
let title = article[i]['title']
let img = article[i]['img']
let desc = article[i]['desc']
let url = article[i]['url']
let comment = article[i]['comment']
// 메모카드 html 붙이기
let temp_html = `<div class="card">
<img class="card-img-top"
src="${img}"
alt="Card image cap">
<div class="card-body">
<a target="_blank" href="${url}" class="card-title">${title}</a>
<p class="card-text">${desc}</p>
<p class="card-text comment">${comment}</p>
</div>
</div>`
$('#cards-box').append(temp_html)
}
}
})
}
메모카드가 잘 붙는 것을 확인할 수 있다