본문 바로가기

Education

[웹개발종합반] week4: Flask

반응형
        function postArticle() {
            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(response["msg"]);
                }
            })
        }

4주차 수업 목표

  • Flask 프레임워크를 활용하여 API 만들기
  • '모두의 책 리뷰' API를 만들고 클라이언트에 연결하기
  • '나홀로 메모장' API를 만들고 클라이언트와 연결하기

 

수업 자료

 

[스파르타코딩클럽] 웹개발 종합반 - 4주차

매 주차 강의자료 시작에 PDF파일을 올려두었어요!

www.notion.so


 

 

1. 4주차 오늘 배울 것

✔ HTML와 mongoDB까지 연동해서 서버 만들기

요청을 받고, 파일을 주거나 혹은 DB를 주도록 서버를 만들자.

서버의 동작

로컬 개발환경

우리는 컴퓨터가 한 대이다. 그래서 같은 컴퓨터에 서버를 만들고, 요청도 할 것이다. 즉 클라이언트 = 서버가 된다. 이것을 로컬 개발환경이라고 한다.

 

5주차 후반부에선 우리가 만든 서버의 결과물을 특정 컴퓨터를 사서 그곳에 서버를 올려볼 것이다! 그러면 우리는 24시간 컴퓨터를 켜놓지 않고도 24시간 내내 서버가 켜져있게 된다.

 

 

2. 폴더 세팅

projects 디렉토리 내에 prac, alonememo, bookreview, moviestar 디렉터리들을 만든다.

  • prac: flask 연습 코드 작성하기
  • alonememo: 나홀로 메모장 관련 코드 작성하기
  • bookreview: 모두의 책리뷰 관련 코드 작성하기
  • moviestar: 마이페이보릿무비스타 관련 코드 작성하기

 

3. Flask

3.1 서버만들기

새 파이썬 프로젝트 시작하기

prac 디렉터리에서 파이썬 프로젝트를 시작한다.

  1. File - New Project - Location: C:\...\projects\prac
  2. Python Interpreter - Location:에서 prac\venv로 되어있는지 체크하기
  3. Base interpreter: python38
  4. create 버튼 누르기

세팅이 완료되면 app.py 파일을 생성한다.

 

Flask란? : 프레임워크

Flask란 서버를 구동시켜주는 편한 코드를 모은 것이다. 서버를 구동하려면 필요한 복잡한 일들을 쉽게 가져다 쓸 수 있다.

프레임워크를 쓰지 않으면 태양초를 빻아서 고추장을 만드는 격!
프레임워크는 3분 요리/소스 세트라고 생각하면 된다!

 

flask 패키지 설치하기

File - Setting - Project: prac - Python Interpreter - +버튼 클릭 - flask 검색 - Install Package - OK

 

flask 시작 코드

from flask import Flask
app = Flask(__name__)

@app.route('/')
def home():
    return 'This is Home!'

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

Run한 후 localhost:5000에 접속하면 다음과 같이 뜬다. 단 9줄 만으로 서버를 만든 것이다!

Run 한 후 console 출력
localhost:5000에 접속했을 때

내가 만든 서버의 결과물이 나온다. 현재 내 컴퓨터는 서버의 역할도 클라이언트의 역할도 하고 있는 셈이다.

 

5000?

포트 번호이다. 현재 인터넷에 뚫어놓은 문이라고 생각하면 된다. 그게 지금 5000번 문을 뚫어놓은 것이다.

포트 번호가 반드시 5000번일 필요는 없다. 포트 번호를 바꿔도 되긴 하지만 괜히 만져서 다른 컴퓨터에서 돌아가고 있는 번호와 겹치는 문제를 발생시키지 말고 그냥 두자.

 

여기서 인터넷을 사서 올린다면 남들도 모두 볼 수 있는 서버가 된다. (현재는 내 컴퓨터에서만 볼 수 있음)

 

/mypage route를 추가해보자.

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def home():
    return 'This is Home!'

@app.route('/mypage')
def mypage(): # 위의 함수와 다른 함수여야만 한다.
    return 'mypage 입니다.'

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

localhost:5000/mypage에 접속했을 때

 

home의 return값을 버튼으로 주어 페이지 화면에 버튼이 나타나도록 해보자.

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def home():
    return '<button>나는 버튼이다.</button>'

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

 

 

 

3.2 HTML 파일 주기

이전에 버튼을 return값으로 주어 페이지 화면을 변경하였다. 하지만 return 공간에 모든 HTML 코드를 작성하는 것은 가독성이 떨어진다. 따라서 HTML 파일을 주도록 해보자.

먼저 static, templates 디렉터리를 생성하자.

  • static: 이미지, css 파일을 넣어두는 디렉터리
  • templates: HTML 파일을 넣어두는 디렉터리. 반드시 'templates'와 동일하게 작성되어야만 한다. 이유는 아래에 설명한다.

그리고 templates에 index.html을 생성한다.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <title>Document</title>
</head>
<body>
<h1>서버를 만들었다!</h1>
</body>
</html>

app.py

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

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

flask 프레임워크의 render_template에 이미 정의된 함수를 이용하면 render_template('HTML파일명')이라고 입력하면 서버가 templates 디렉터리에 있는 HTML파일명과 동일한 HTML 파일을 가져와서 페이지를 그리게 된다. 따라서 무조건 templates와 동일하게 디렉터리명을 생성해야 하는 이유가 바로 이것이다.

flask를 이용하여 서버를 만들었기 때문에, 앞으로 localhost:5000을 통해서 html 내용을 확인하도록 하자.

 

3.3 본격 API 만들기

remind

은행이 고객을 받기 위해 만들어놓은 창구처럼, 서버도 클라이언트를 받기 위해 만들어놓은 창구가 바로 API이다.

그리고 이 창구는 규칙을 가지고 있다. 입금을 하려면 입출금 창고로 가야하는 것처럼, API도 규칙이 있고 이에 해당하는 주소가 존재한다.

  • GET: 데이터 조회(Read)를 요청할 때
  • POST: 데이터 생성(Create), 변경(Update), 삭제(Delete)를 요청할 때

 

GET, POST 요청에서 클라이언트의 데이터를 받는 방법

▪ GET 요청 API 코드

@app.route('/test', methods=['GET'])
def test_get():
    title_receive = request.args.get('title_give')
    print(title_receive)
    return jsonify({'result':'success', 'msg': '이 요청은 GET!'})

 이제 API를 만들었으므로 클라이언트에서 요청을 해보자. localhost:5000 개발자 모드 콘솔에서 코드를 작성한다.

 

GET 요청 확인 Ajax 코드

$.ajax({
    type: "GET",
    url: "/test?title_give=봄날은간다",
    data: {},
    success: function(response){
       console.log(response)
    }
  })
  // {'msg': '이 요청은 GET!', 'result':'success'}

서버에서 작성된 값을 받았다. 즉 response는 서버에서 받은 값이라는 것을 알 수 있다.

  • type은 "GET"
  • url: GET요청은 "?"로 가져온다고 했었다(네이버 영화). title_give라는 이름으로 '봄날은간다'는 값을 가져간다.
  • ➡ 그러면 서버에서 클라이언트에서 title_give로 가져간 값을 title_receive 변수로 지정한다. 그러므로 서버 터미널창에서 '봄날은간다'가 출력된다.

서버 터미널 콘솔창

  • success: 성공 시 response를 통해 서버로부터 데이터를 전송받는다.

 

▪ POST 요청 API

@app.route('/test', methods=['POST'])
def test_post():
    title_receive = request.form['title_give']
    print(title_receive)
    return jsonify({'result':'success', 'msg': '이 요청은 POST!'})
  • request.form['title_give']: 클라이언트로부터 title_give로 가져온 값을 가져온다.

 

▪ POST 요청 확인 Ajax

$.ajax({
    type: "POST",
    url: "/test",
    data: { title_give:'봄날은간다' },
    success: function(response){
       console.log(response)
    }
  })

  • type은 'POST'
  • url은 /test로 간다.
  • data: title_give: '봄날은간다'로 값을 가져간다. 마찬가지로 서버의 창고에서 첫 번째로 일어나는 건 title_receive값이 '봄날은간다'로 지정된다. 그렇게 해서 서버 터미널에 '봄날은간다'가 출력된다.

 

만약 data에 서버에서 지정한 값이 아닌 title_give2 값으로 주게 되면 어떻게 될까?

$.ajax({
    type: "POST",
    url: "/test",
    data: { title_give2:'봄날은간다' },
    success: function(response){
       console.log(response)
    }
  })
  // POST http://localhost:5000/test 500 (INTERNAL SERVER ERROR)

에러가 발생한다. INTERNAL SERVER ERROR이므로 서버쪽에서 발생한 에러이다. 서버에서 작성한 이름이 없기 때문이다. 따라서 항상 서버에서 작성한 이름과 클라이언트에서 작성하는 이름과 동일하게 작성되어야만 한다.

 

 

4. 모두의 책 리뷰

4.1 프로젝트 세팅

C:\...\projects\bookreview 경로로 새로운 파이썬 프로젝트를 생성하고 app.py 파일을 생성한다. static, templates 디렉터리를 생성하고, templates 디렉터리 내에는 index.html 파일을 생성한다.

Ctrl+Alt+S - Python Interpreter에서 flaskpymongo(리뷰를 저장하기 위해)와 패키지를 설치한다. 

 

4.2 뼈대 준비하기

- app.py

from flask import Flask, render_template, jsonify, request
app = Flask(__name__)

from pymongo import MongoClient
client = MongoClient('localhost', 27017)
db = client.dbsparta

## HTML을 주는 부분
@app.route('/')
def home():
    return render_template('index.html')

## API 역할을 하는 부분
@app.route('/review', methods=['POST'])
def write_review():
    sample_receive = request.form['sample_give']
    print(sample_receive)
    return jsonify({'msg': '이 요청은 POST!'})


@app.route('/review', methods=['GET'])
def read_reviews():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg': '이 요청은 GET!'})


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

sample_give 명으로 클라이언트로부터 데이터를 받고 있다.

- index.html

<!DOCTYPE html>
<html lang="ko">

    <head>
        <!-- Webpage Title -->
        <title>모두의 책리뷰 | 스파르타코딩클럽</title>

        <!-- 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=Do+Hyeon&display=swap" rel="stylesheet">

        <script type="text/javascript">

            $(document).ready(function () {
                $("#reviews-box").html("");
                showReview();
            });

            function makeReview() {
                $.ajax({
                    type: "POST",
                    url: "/review",
                    data: {sample_give:'샘플데이터'},
                    success: function (response) {
                        alert(response["msg"]);
                        window.location.reload();
                    }
                })
            }

            function showReview() {
                $.ajax({
                    type: "GET",
                    url: "/review?sample_give=샘플데이터",
                    data: {},
                    success: function (response) {
                        alert(response["msg"]);
                    }
                })
            }
        </script>

        <style type="text/css">
            * {
                font-family: "Do Hyeon", sans-serif;
            }

            h1,
            h5 {
                display: inline;
            }

            .info {
                margin-top: 20px;
                margin-bottom: 20px;
            }

            .review {
                text-align: center;
            }

            .reviews {
                margin-top: 100px;
            }
        </style>
    </head>

    <body>
        <div class="container">
            <img src="https://previews.123rf.com/images/maxxyustas/maxxyustas1511/maxxyustas151100002/47858355-education-concept-books-and-textbooks-on-the-bookshelf-3d.jpg"
                 class="img-fluid" alt="Responsive image">
            <div class="info">
                <h1>읽은 책에 대해 말씀해주세요.</h1>
                <p>다른 사람을 위해 리뷰를 남겨주세요! 다 같이 좋은 책을 읽는다면 다 함께 행복해질 수 있지 않을까요?</p>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">제목</span>
                    </div>
                    <input type="text" class="form-control" id="title">
                </div>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">저자</span>
                    </div>
                    <input type="text" class="form-control" id="author">
                </div>
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">리뷰</span>
                    </div>
                    <textarea class="form-control" id="bookReview"
                              cols="30"
                              rows="5" placeholder="140자까지 입력할 수 있습니다."></textarea>
                </div>
                <div class="review">
                    <button onclick="makeReview()" type="button" class="btn btn-primary">리뷰 작성하기</button>
                </div>
            </div>
            <div class="reviews">
                <table class="table">
                    <thead>
                    <tr>
                        <th scope="col">제목</th>
                        <th scope="col">저자</th>
                        <th scope="col">리뷰</th>
                    </tr>
                    </thead>
                    <tbody id="reviews-box">
                    <tr>
                        <td>왕초보 8주 코딩</td>
                        <td>김르탄</td>
                        <td>역시 왕초보 코딩교육의 명가답군요. 따라하다보니 눈 깜짝할 사이에 8주가 지났습니다.</td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </body>

</html>

클라이언트에서는 리뷰 작성하기 버튼을 누르게 되면 Review() 함수가 실행된다.

Review 함수는 /review 경로를 통해 sampel_give에 '샘플데이터'라는 데이터를 주고, 서버로부터 전송받은 sample_give 데이터인 'msg': '이 요청은 POST!' 를 받는다. 

 

4.3 POST 연습: 리뷰저장

API 만들고 사용하기: 제목, 저자, 리뷰 정보 저장하기 (Create ➡ POST)

▪ 서버 만들기

서버에서는 클라이언트에서 작성한 제목, 저자, 리뷰 데이터를 받아 저장한다.

app.py

@app.route('/review', methods=['POST'])
def write_review():
    title_receive = request.form['title_give']
    author_receive = request.form['author_give']
    review_receive = request.form['review_give']

    doc = {'title':title_receive,
           'author':author_receive,
           'review':review_receive }
    db.bookreview.insert_one(doc)

    return jsonify({'msg': '저장 완료!'})

 

▪ 클라이언트 만들기

클라이언트에서는 리뷰 작성하기 버튼을 눌렀을 때 제목, 저자, 리뷰 데이터를 받아 보내준다. jQuery를 사용하자.

index.html

function makeReview() {
	let title = $('#title').val();
	let author = $('#author').val();
	let review = $('#bookReview').val();

	$.ajax({
		type: "POST",
		url: "/review",
		data: {title_give:title,author_give:author,review_give:review},
		success: function (response) {
			alert(response["msg"]);
			window.location.reload();
		}
	})
}


...

<div class="review">
	<button onclick="makeReview()" type="button" class="btn btn-primary">리뷰 작성하기</button>
</div>

 

▪ 완성 확인하기

클라이언트에서 제목, 저자, 리뷰 데이터를 작성하고 리뷰 작성하기 버튼을 누르면 현재 페이지에서는 아무 것도 볼 수 없다.

robo 3T를 실행하고 Collections명이 bookreview에 들어가서 보면 데이터가 잘 들어온 것을 확인할 수 있다.

robo 3T - Collections - bookreview

 

4.3 GET 연습: 리뷰 보여주기

API 만들고 사용하기: 저장된 리뷰를 화면에 보여주기 (Read ➡ GET)

▪ 클라이언트와 서버 확인하기

- 서버

@app.route('/review', methods=['GET'])
def read_reviews():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg': '이 요청은 GET!'})

➡ 서버는 클라이언트에서 받아올 데이터가 없다. DB에서만 받아오면 된다.

- 클라이언트

$(document).ready(function () {
	$("#reviews-box").html("");
	showReview();
});

function showReview() {
	$.ajax({
		type: "GET",
		url: "/review?sample_give=샘플데이터",
		data: {},
		success: function (response) {
			alert(response["msg"]);
		}
	})
}

➡ 서버에서 받아온 Db 데이터를 클라이언트에서 띄워준다.

 

▪ 서버부터 만들기

@app.route('/review', methods=['GET'])
def read_reviews():
    reviews = list(db.bookreview.find({},{'_id':False}))
    return jsonify({'all_reviews':reviews})

 

▪ 클라이언트 만들기

showReview() {
    $.ajax({
        type: "GET",
        url: "/review", // 서버가 클라이언트에서부터 받아올 데이터가 없음.
        data: {},
        success: function (response) {
            let reviews = response['all_reviews'];
            for (let i = 0; i < reviews.length; i++) {
                let title = reviews[i]['title'];
                let author = reviews[i]['author'];
                let review = reviews[i]['review'];

                temp_html = `<tr>
                                <td>${title}</td>
                                <td>${author}</td>
                                <td>${review}</td>
                            </tr>`;

                $('#reviews-box').append(temp_html);

            }
        }
    })
}

 

▪ 완성 확인하기

 

 

5. 나홀로 메모장

5.1 프로젝트 세팅

나홀로 메모장 미리보기

 

스파르타코딩클럽 | 나홀로 메모장

 

spartacodingclub.shop

 

프로젝트 세팅

  • File - New project - Location: C:\...\projects\alonememo
  • Python Interpreter - Location에 \venv로 끝나는지 확인하기
  • app.py 파일을 생성하고, static과 templates 디렉터리 생성 후 templates 내에 index.html 파일 생성하기
  • Setting(Ctrl+Alt+S) - Python Interpreter - pymongo, flask, requests(크롤링을 위해), bs4 Install Packages

 

5.2 API 설계하기

프로젝트를 시작하기 전에 API를 설계하는 것을 가장 먼저 해야 한다. 그래야만 우리 서비스에 어떤 기능들이 필요하고, 그 기능들을 어떤 순서로 구현할 것인지 계획할 수 있기 때문이다. 나홀로 메모장 웹 사이트에 들어가서 어떤 기능들이 필요한지 살펴보면서 API를 설계해보자.

  1. 클라이언트에서 url와 comment를 서버로 전송하고, 서버에서 DB로 그 데이터를 저장한다.
  2. 클라이언트에서 모든 영화 데이터의 카드들을 보여주어야 한다.
    • 서버로부터 받은 DB 데이터 중에서 이미지, 링크, 제목, 요약, 코멘트 정보를 저장해야 한다.

 

카드 생성하기 (Create)

  • 요청 URL: /memo
  • 요청 방식: POST
  • 요청 데이터: URL(url_give), 코멘트(comment_give)

 

▪ 저장된 카드 보여주기 (Read)

  • 요청 URL: /memo
  • 요청 방식: GET
  • 요청 데이터: X
  • 서버가 제공하는 기능: DB에 저장되어 있는 모든 (제목, 설명, URL, 이미지, 코멘트) 정보 가져오기
  • 응답 데이터: 기사들의 정보(제목, 설명, URL, 이미지, 코멘트)를 카드로 만들어서 붙이기 ➡ when? 로딩이 완료된 후 바로!
  • (JSON 형식) 'articles': 아티클 정보

 

 

5.3 조각 기능 구현해보기

프로젝트 준비: URL에서 페이지 정보 가져오기(meta 태그 스크래핑)

우리는 기사의 URL만 입력했는데, 자동으로 불러와지는 부분들이 있다. 바로 '기사 제목', '썸네일 이미지', '내용'이다.

기사 제목, 썸네일 이미지, 내용

이 데이터들을 가져오기 위해서는 'meta'태그를 크롤링하면 얻을 수 있다!

 

meta 태그란?

 

[Startup’s Story #449] 중국서 공유주택 열기 일으킨 창업가, 한국서 이어간다

 

platum.kr

위의 링크에 접속한 후 크롬 개발자 도구를 이용하여 HTML 생김새를 살펴보자. 메타 태그는 <head></head> 태그 사이에 들어가는, 눈에 보이는 것(body)외에 사이트의 속성을 설명해주는 태그들이다. 예로 들면 구글 검색 시에 표시될 설명문, 사이트 제목, 혹은 카톡 공유 시 표시될 이미지 등이다.

네이버 영화(https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539)의 개발자 모드

우리는 그 중에서 og:image, og:title, og:description을 크롤링 할 것이다. 이 데이터를 크롤링해서 '기사 제목', '썸네일 이미지', '내용'을 띄우도록 하자.

 

meta 태그 스크래핑하기

우선 연습을 위해서 meta_prac.py 파일을 생성하자.

크롤링 기본 코드

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를 먼저 가져와보겠습니다.
print(soup)

- 결과

<!DOCTYPE html>

<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<meta content="no" http-equiv="imagetoolbar"/>
<title>그린 북 : 네이버 영화</title>
<meta content="https://ssl.pstatic.net/imgmovie/today/naverme/naverme_profile.jpg" property="me2:image">
<meta content="네이버영화 그린 북" property="me2:post_tag">
<meta content="네이버영화" property="me2:category1">
<meta content="그린 북" property="me2:category2"/>
<meta content="그린 북" property="og:title"/>
<meta content="article" property="og:type"/>
...

 

og 요소들 크롤링하기

네이버 영화(https://movie.naver.com/movie/bi/mi/basic.nhn?code=171539)의 개발자 모드

og:title을 Copy selector한다. 

head > meta:nth-child(9)

- 코드

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

title = soup.select_one('head > meta:nth-child(9)')
print(title)

- 결과

짜잔~ 아무 일도 일어나지 않았습니다~

 

왜일까? 사람이 브라우저에서 접속하여 나오는 메타태그의 순서와, 파이썬 코드가 접속했을 때 나오는 메타태그의 순서가 다르기 때문이다. 따라서 위의 방법으로 실행하면 원하는 정보를 얻기 어렵다. 대신 다음과 같이 작성해보자.

title = soup.select_one('meta[property="og:title"]')

메타 태그 중에서 해당 속성과 일치하는 요소를 가져온다는 의미이다.

짜잔~  이제 원하는 정보를 가져왔다. 이 중에서 content만 가져오자.

title = soup.select_one('meta[property="og:title"]')['content']

 

이제 제목, 이미지, 설명 모두 가져오자.

- 코드

title = soup.select_one('meta[property="og:title"]')['content']
image = soup.select_one('meta[property="og:image"]')['content']
description = soup.select_one('meta[property="og:description"]')['content']
print(title, image, description)

- 결과

그린 북 https://movie-phinf.pstatic.net/20190115_228/1547528180168jgEP7_JPEG/movie_image.jpg?type=m665_443_2 1962년 미국, 입담과 주먹만 믿고 살아가던 토니 발레롱가(비고 모텐슨)는 교양과 우아함 그 자체인천재...

Process finished with exit code 0

 

 

5.4 뼈대 준비하기

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>

실행 화면

 

 

5.5 POST 연습: 메모하기

API 만들고 사용하기: 포스팅API (Create ➡ POST)

만들 API

  • 포스팅API - 카드 생성(Create): 클라이언트에서 받은 url, comment를 이용해서 페이지 정보를 찾고 저장하기
  • 리스팅API - 저장된 카드 보여주기(Read)

 

기사 저장 버튼을 누르면 postArticle() 함수가 실행된다.

index.html

        function postArticle() {
            $.ajax({
                type: "POST",
                url: "/memo",
                data: {sample_give:'샘플데이터'},
                success: function (response) { // 성공하면
                    alert(response["msg"]);
                }
            })
        }

그리고 클라이언트에서 보낸 데이터를 sample_give를 통해 서버에서 받는다.

@app.route('/memo', methods=['GET'])
def listing():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg':'GET 연결되었습니다!'})

 

서버 만들기

클라이언트에서부터 url_give와 comment_give를 통해 url, comment 데이터를 받는다.

## API 역할을 하는 부분
@app.route('/memo', methods=['POST'])
def saving():
    url_receive = request.form['url_give']
    comment_receive = request.form['comment_give']

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

    title = soup.select_one('meta[property="og:title"]')['content']
    image = soup.select_one('meta[property="og:image"]')['content']
    desc = soup.select_one('meta[property="og:description"]')['content']

    doc = {
        'title':title,
        'image':image,
        'desc':desc,
        'url':url_receive,
        'comment':comment_receive
    } # DB에 데이터 넣기(클라이언트에서 받아오기 위해서)

    db.articles.insert_one(doc)

    return jsonify({'msg':'저장이 완료되었습니다!'})

 

클라이언트 만들기

포스팅박스에서 사용자가 입력한 url와 comment 데이터를 받고, 이를 서버에게 보낸다.

        function postArticle() {
            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(response["msg"]);
                    window.location.reload(); // 입력 후 페이지 새로고침하기
                }
            })
        }

 

완성 확인하기

 

기사 저장 버튼을 누르면 저장이 완료되었다는 얼럿이 뜬다.

그리고 robo 3T에 접속하여 입력한 데이터가 있는지 확인한다.

 

5.6 GET 연습: 보여주기

API 만들고 사용하기 - 리스팅 API (Read → GET)

 

서버부터 만들기

db로 부터 모든 데이터를 받아오고, 클라이언트로 'all_articlces' 명으로 보낸다.

@app.route('/memo', methods=['GET'])
def listing():
    articles = list(db.articles.find({},{'_id':False}))
    return jsonify({'all_articles':articles})

 

클라이언트 만들기

서버에서 보낸 'all_articles'명으로 데이터를 받고, 카드를 추가한다.

        function showArticles() {
            $.ajax({
                type: "GET",
                url: "/memo",
                data: {},
                success: function (response) {
                    let articles = response["all_articles"];

                    for (let i = 0; i < articles.length; i++) {
                        let title = articles[i]['title'];
                        let image = articles[i]['image'];
                        let url = articles[i]['url'];
                        let comment = articles[i]['comment'];
                        let desc = articles[i]['desc'];

                        temp_html = `<div class="card">
                                        <img class="card-img-top"
                                             src="${image}"
                                             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);
                    }
                }
            })
        }

 

완성 확인하기

 

 

6. 4주차 끝 & 숙제 설명

1주차에 완성한 쇼핑몰 완성하기

미리보기

 

나홀로 쇼핑몰

 

spartacodingclub.shop

추가 기능

  • 주문하기(POST): 정보 입력 후 '주문하기' 버튼 클릭 시 주문목록에 추가하기
  • 주문내역보기(GET): 페이지 로딩 후 하단 주문 목록이 자동으로 보이기

 

뼈대 코드

- app.py

from flask import Flask, render_template, jsonify, request

app = Flask(__name__)

from pymongo import MongoClient

client = MongoClient('localhost', 27017)
db = client.dbhomework


## HTML 화면 보여주기
@app.route('/')
def homework():
    return render_template('index.html')


# 주문하기(POST) API
@app.route('/order', methods=['POST'])
def save_order():
    sample_receive = request.form['sample_give']
    print(sample_receive)
    return jsonify({'msg': '이 요청은 POST!'})


# 주문 목록보기(Read) API
@app.route('/order', methods=['GET'])
def view_orders():
    sample_receive = request.args.get('sample_give')
    print(sample_receive)
    return jsonify({'msg': '이 요청은 GET!'})


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

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

    <title>원페이지쇼핑몰</title>

    <link href="https://fonts.googleapis.com/css2?family=Do+Hyeon&display=swap" rel="stylesheet">

    <style>
        * {
            font-family: 'Do Hyeon', sans-serif;
        }

        .myitem {
            width: 500px;
            height: 300px;

            background-image: url("https://t1.daumcdn.net/liveboard/nts/5bcccfbd33da4865817b9c606b6b852e.JPG");
            background-position: center;
            background-size: cover;
        }

        .price {
            font-size: 16px;
        }

        .desc {
            width: 500px;
            margin-top: 20px;
            margin-bottom: 20px;
        }

        .order-box {
            width: 500px;
            margin-bottom: 40px;
        }

        .mybtn {
            width: 100px;
            margin: auto;
            display: block;
        }

        .wrap {
            margin: auto;
            width: 500px;
        }

        .rate {
            color: blue;
        }
    </style>
    <script>

        $(document).ready(function () {
            $.ajax({
                type: "GET",
                url: "https://api.manana.kr/exchange/rate.json",
                data: {},
                success: function (response) {
                    let nowRate = response[1]['rate'];
                    $('#rate-box').text(nowRate);
                }
            })
            order_listing();
        });

        function order_listing() {
            // 주문목록 보기 API 연결
						$.ajax({
                type: "GET",
                url: "/order?sample_give=샘플데이터",
                data: {},
                success: function (response) {
                    alert(response["msg"]);
                }
            })
        }

        function order() {
            // 주문하기 API 연결
						$.ajax({
                type: "POST",
                url: "/order",
                data: {sample_give:'샘플데이터'},
                success: function (response) { // 성공하면
                    alert(response["msg"]);
                }
            })
        }
    </script>
</head>

<body>
<div class="wrap">
    <div class="myitem"></div>
    <div class="desc">
        <h1>양초를 팝니다 <span class="price">가격:3,000원/개</span></h1>
        <p>이 양초는 특별한 힘을 가지고 있습니다. 하나 두 개 켜 놓으면 또 다른 촛불을 찾을 수 있죠!</p>

        <p class="rate">달러-원 환율: <span id="rate-box">1,000</span></p>
    </div>
    <div class="order-box">
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">주문자이름</span>
            </div>
            <input id="order-name" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">수량</label>
            </div>
            <select id="order-count" class="custom-select">
                <option selected>-- 수량을 선택하세요 --</option>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
            </select>
        </div>
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">주소</span>
            </div>
            <input id="order-address" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">전화번호</span>
            </div>
            <input id="order-phone" type="text" class="form-control" aria-label="Default"
                   aria-describedby="inputGroup-sizing-default">
        </div>
        <button onclick="order()" type="button" class="btn btn-primary mybtn">주문하기</button>
    </div>
		<table class="table">
		  <thead>
		    <tr>
		      <th scope="col">이름</th>
		      <th scope="col">수량</th>
		      <th scope="col">주소</th>
		      <th scope="col">전화번호</th>
		    </tr>
		  </thead>
		  <tbody>
		    <tr>
		      <th scope="row">홍길동</th>
		      <td>1</td>
		      <td>서울시 동작구</td>
		      <td>010-1234-5678</td>
		    </tr>
		    <tr>
		      <th scope="row">신사임당</th>
		      <td>2</td>
		      <td>서울시 영등포구</td>
		      <td>010-1234-5678</td>
		    </tr>
		    <tr>
		      <th scope="row">장영실</th>
		      <td>1</td>
		      <td>부산시 중구</td>
		      <td>010-1234-5678</td>
		    </tr>
		  </tbody>
		</table>
</div>
</body>

</html>

 

 

  • 우선 숙제 디렉터리로 New Project를 한다.
  • app.py 파일을 생성하고 templates, static 디렉터리를 만든 후, templates 디렉터리 내에 index.html 파일을 생성한다.
  • Python Interpreter에서 flask, pymongo를 Install package 한다.

 

 

 

 

 

 

 

 

반응형