매우 당황스러운 일이 생겼다. 몇 안되는 스크래핑 내용 (제목, 링크, 날짜) 중에서 날짜가 문제가 생겼다. 각 사이트가 날짜 기록 방식이 다르다..... 각 사이트는 아래와 같은 형식으로 날짜값을 가져온다. 이게 웃기는게, 현재 날짜 시점으로 오늘이면 시간을 보여주고, 오늘이 아니면 날짜를 보여준다. 이렇게 값이 바뀌니까 더 애매해졌다.


- 퀘이사존 00:38 , 08-18
- 클리앙 10:43 , 08-18
- 펨코  01.20, 2020.08.18
- 루리웹  01:08, 2020.08.18
- 뽐뿌  15:56:32, 20/08/16

 

아무튼, 날짜까지 빼버릴 순 없기에, 어떻게든 해볼라고 정말 고군분투했다. 뭔가 아는게 있어야 시작이라도 할텐데... 다행히 어디서 주워들은 정규화부터 차근차근 알아보며 공부하기 시작했다. 다행히 허접하지만 어떻게든 작동하는 알고리즘을 만들 수 있었다.

# 당일이면 시간, 당일이 아니면 날짜로 가져온다. 그런데 날짜나 시간 값이 제각각이다. 
def extractDate(a): 
    if ":" in a:   ## 먼저 받은 값에 : 가 있는지 확인한다. (시간인지 아닌지 여부) 
        return a[0:5]  ## 시간이 맞으면, 앞에서 다섯자리만 가져온다. 
    else:  ## : 가 없으면 날짜일 것이다. 
        if len(a) > 6:  ## 날짜의 길이가 6자리를 초과하면, 아래와 같이 정규화 식을 따른다. 
            ## 섹션1 : (\d{4}|\d{2}) 중간값 : [\/|.|-]  섹션2 : (\d{2})  중간값 : [\/|.|-]  섹션3 : (\d{2})  의 정규화 형식을 따른다. 
            # 섹션1은 숫자가 4개 또는 2개, 섹션2는 숫자가 2개, 섹션3은 숫자가 2개 인 형태이다. 
            # 중간값은 / 또는 . 또는 - 를 구분한다. 
            # a라는 변수에서 위와 같은 내용을 검사한다. 
            date = re.findall('(\d{4}|\d{2})[\/|.|-](\d{2})[\/|.|-](\d{2})', a) 
            parseddate = "+" + date[0][1] + "-" + date[0][2]
            ## 값은 리스트 안의 튜플 형식으로 나온다. 그래서 리스트 첫번째 값의 튜플0은 년, 튜플1은 월, 튜플2는 일 이므로 튜플1,2만 가져온다. 
            return parseddate 
        else: 
            return "+" + a ## 만약 날짜의 길이가 6자리를 초과하지 못했다면, 맞는 날짜이므로 그대로 출력한다.

 

맨 마지막 return값에 +를 넣는 이유도 참 난감하다. mongodb에 데이터들이 들어갈텐데, 시간대별로 정렬을 할라고 했더니 date 섹션에 날짜와 시간이 섞여 있으니까 정렬도 제대로 안 되는 것이다. 역시 개발은 쉬운게 아니다. 뭘 해결하면 또 뭐가 나오고 또 뭘 해결하면 또 뭐가 나온다..

 

이것 때문에 또 여러가지를 찾아보았지만 뾰족한 수가 없어, 정렬할 때 특수기호는 우선순위가 숫자와 다르다는 특성을 이용해 그냥 날짜쪽에는 무조건 특수기호 +를 넣기로 했다. 그렇게 하니 일단 정렬은 잘 되었다. 근데 이것은 미봉책이고 좀 더 찾아봐야 할 것 같다.

 

아무튼, 겨우겨우 만들고 잘 작동하는걸 보니, 이게 또 느낌이 신기하다. 이맛에 개발자 하는건가?

 

 

스파르타 코딩클럽에서 코딩 공부를 시작하고 나서, 이론을 배울 때는 잘 이해하고 기록해두면서 뭔가 항상 "금방 쉽게 하겠지~" 라는 생각을 가지고 있었는데, 막상 시작을 하면 너무 기초적인 것 부터 "아 이거 여긴 어떻게 하지?" 라는 생각이 든다. 내 전체적인 프로젝트를 생각하면 모르는게 너무 많을 것 같아 두려움이 앞선다.

 

시간은 한정적이고 할 일은 많다. 회사에서 업무를 위해 오픈소스와 리눅스 공부는 계속 해야하고, 자녀들과도 시간을 보내고 아내를 도와주기도 해야 하고, 놀아야 하고, 다른 공부도 해야하고, 영어도 해야하고... 너무나 할일이 많다. 이번 스파르타 코딩 프로젝트를 진행하면서, 모든 우선순위를 제쳐두고 집중하면서도 잘 되지 않으면 어쩌나 라는 걱정도 많이 했다. 

 

그래도 내가 해야 할 모든 일들을 생각하는 것 보다, 눈앞에 해야 할 일만 생각하면 조금 더 마음이 편해질 수 있었고, 내가 가장 먼저 해야하는 것부터 조금씩 해나간다면 그래도 뭔가 진행이 된다는 사실에 또 마음이 편해졌다.

 


 

내가 해야 할 프로젝트 중 가장 먼저 완료해야 하는것은 스크래핑을 통해 원하는 데이터를 가져오는 것이다. 이게 되지 않는다면 프로젝트 자체를 진행할 수 없다.

 

프로젝트 계획안대로, 아래의 모든 사이트들을 스크래핑했다.

 

- 퀘이사존 : https://quasarzone.com/bbs/qb_saleinfo

- 카톡 톡딜 : https://store.kakao.com/

- 클리앙 알뜰구매 : https://www.clien.net/service/board/jirum

 - 펨코 핫딜 : https://www.fmkorea.com/hotdeal

- 루리웹 핫딜1 : https://bbs.ruliweb.com/market/board/600004

- 루리웹 핫딜2 : https://bbs.ruliweb.com/market/board/1020

- 뽐뿌 오프라인뽐뿌 : http://www.ppomppu.co.kr/zboard/zboard.php?id=ppomppu5

 

대부분의 사이트들은 가장 기초적인 beautifulsoup을 사용한 스크래핑이 되지 않도록 어느정도 방안을 마련한 것 같았다. 어떻게 해도 스크래핑이 되지 않는 경우가 있어, 결국 모든 스크래핑은 selenium으로 진행했다.

 

그런데 하다보니, 각 사이트들이 일관성이 없어도 너무 없었다. 결국 "특가" 라는 개념에는 특정 가격의 제품을 소개할 수도 있지만, 단순히 할인이나 모음전, 어떤 사람은 마트에서 싸게 나온 제품을 사진찍어서 올리는 사람도 있었다. 다시말해, 가격이 없는 게시글, 이미지가 없는 게시글, 아예 제품이 뭔지 정해지지 않은 게시물 등 중구난방이었다.

 

결국 처음에 목적으로 했던 가격으로 검색하는 기능이나, 이미지를 포함하여 보기 좋게 하려는 것은 불가능하다고 판단했고, 과감하게 스크래핑 목록에서 이미지와 가격은 빼버렸다. 즉, 딱 제목과 링크, 날짜만 가져오게 된 것이다. 뭔가 많이 부족해 보여서 고민을 많이 했는데, 그래도 여러 특가를 한번에 볼 수 있다는 메리트는 계속 있다고 생각되어, 다행히 프로젝트는 엎어지지 않고 그대로 진행하기로 했다.

 

여러가지 사이트의 크롤링 구문 중 퀘이사존의 특가정보를 스크래핑하는 구문이다.

## 패키지 Import
from selenium import webdriver
from bs4 import BeautifulSoup
import re
import pymysql.cursors


## 셀레니움 스크래핑 기본정보
option = webdriver.ChromeOptions()
option.headless = True
option.add_argument("window-size=1920x1080")
option.add_argument("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36")
browser = webdriver.Chrome('C:\chromedriver', options=option)
browser.maximize_window()


# 퀘이사존 스크래핑 함수 제작
def quasarzone(url):

    # 데이터 가공
    browser.get(url)
    soup = BeautifulSoup(browser.page_source, "lxml")
    # 필요한 데이터 가져오기. 원하는 데이터가 모두 포함된 가장 적절한 태그는 tr 이다.
    # 그 위의 <tbody>를 하지 않는 이유는, 데이터가 리스트화되지 않고 한개의 뭉텅이가 되어서 스크래핑시 하나밖에 못가져온다.
    datas = soup.find_all("tr")

    # 스크래핑 시작
    for data in datas:
        
        # 각 데이터에 대한 변수 선언
        dealTitle = data.find("a", attrs={"class": "subject-link"}) # 제목
        dealLink = data.find("a", attrs={"class": "subject-link"}) # 글링크
        dealDate = data.find("span", attrs={"class" : "date"}) # 날짜
        dealBlind = data.find("i", attrs={"class" : "fa fa-lock"}) # 블라인드 처리된 글
        
        # 전체 데이터 중 None이 발생할 수 있어 None을 제거함
        if dealTitle is not None:
            
            # 각 게시물 중 "블라인드 처리된 글입니다." 라는 글이 있는데, 해당 글은 제외함
            if dealBlind:
                # 즉, dealBlind가 있다면 아무것도 안하고 그냥 넘어감.
                continue
            
            finalDealTitle = dealTitle.text.strip()
            finalDealLink = "https://quasarzone.com/" + dealLink["href"]
            finalDealDate = extractDate(dealDate.text.strip())
            
            print(finalDealTitle)
            print(finalDealLink)
            print(finalDealDate)


# 페이지별 스크래핑
address1 = "https://quasarzone.com/bbs/qb_saleinfo?page=1"
address2 = "https://quasarzone.com/bbs/qb_saleinfo?page=2"
quasarzone(address1)
quasarzone(address2)

 

각 사이트마다 특징이 있는데, 그 중 퀘이사존 특가게시판은 유독 블라인드 처리된 글이 많았다. 해당 블라인드 글까지 가져올 수는 없으므로, 블라인드 된 글을 따로 제거하기 위해 블라인드 된 글의 공통 클래스를 찾았고, 해당 클래스로 블라인드된 글만 제거할 수 있었다.

 

def fmkorea(url):

    # 데이터 가공
    browser.get(url)
    soup = BeautifulSoup(browser.page_source, "lxml")
    # 필요한 데이터 가져오기. 원하는 데이터가 모두 포함된 가장 적절한 태그는 tr 이다.
    datas = soup.find_all("tr")

    # 스크래핑 시작
    for data in datas:
    
        # 각 데이터에 대한 변수 선언
        dealTitle = data.find("td", attrs={"class" : "title hotdeal_var8"}) # 제목
        # 참고로, 에펨코리아는 품절된 제품은 아예 제목의 클래스를 바꿔버리므로 품절된 제품은 아예 검색에서 빠진다. 편하네. (품절제품 클래스명 : hotdeal_var8Y)
        dealLink = data.find("a", attrs={"class" : "hx"}) # 글링크
        dealDate = data.find("td", attrs={"class" : "time"}) #날짜
        
        # 데이터 중 None 이 있다면 None 부분은 skip 하도록 함.
        if dealTitle is not None:
            finalDealTitle = dealTitle.find("a").text.strip().replace("(무료)", "").replace("(0원)", "")
            finalDealLink = "https://www.fmkorea.com/" +  dealLink["href"]
            finalDealDate = extractDate(dealDate.text.strip())
            
            print(finalDealTitle)  # 배송비 (무료), (0원) 부분은 없어도 되므로 제거함.
            print(finalDealLink)
            print(finalDealDate)

            
# 3. 에펨코리아 : 핫딜
address1 = "https://www.fmkorea.com/index.php?mid=hotdeal&listStyle=list&page=1"
#address2 = "https://www.fmkorea.com/index.php?mid=hotdeal&listStyle=list&page=2"
fmkorea(address1)
#fmkorea(address2)

에펨코리아 사이트에서는 품절된 제품이 따로 표시되는데, 클래스가 맞지 않아 어떻게 스크래핑할까 고민하다가 알고보니 품절된 제품은 아예 클래스를 변경해버리는 것을 확인했다. 내가 뭘 할것도 없이 클래스가 다르니 스크래핑할 때 걸러지는 것이었다. 그걸 몰라서 혼자 뻘생각을 하다가 "앗.. 이게 이렇게 되네?" 이러고 허무하게 넘어갔다.

 


 

막상 7개의 사이트를 모두 스크래핑하니, 2페이지씩만 해도 한번에 게시글이 300개 이상 나왔다. 매일매일 간단하게 특가 정보를 확인하려는게 목적인데 아무래도 너무 양이 많은 것 같아서 결국 카카오톡 톡딜은 탈락했다.

 

그래도 양을 줄여서 추가해볼까 했는데, 일단 최종 결과물이 나오고 나서 다시한번 확인해볼 예정이다. 만약 카테고리별로 글을 나눌 수 있으면 게시글 양이 많아도 좋을텐데, 카테고리를 나누는 것은 딥러닝 정도는 쓸 수 있어야.. 할 수 있을 것 같다. 일단은 보류.

AWS 준비

 

 

 

 

 

github 관련

 

 

README.md

프로젝트를 소개하는 파일을 관행적으로 만드는 것. 프로젝트 최상위 폴더안에 넣어둔다.

파일 편집시 메모장등을 쓸 수 있다. 마크다운 형식을 따르는 일반 텍스트 파일이다.

마크다운 형식을 따르면 굵은 글씨 등의 서식을 쉽게 적용할 수 있다.

 

마크다운은 웹 콘텐츠를 쓰는 방법 중 하나이며, 다음 사이트를 배울 수 있다.

https://www.markdowntutorial.com/kr/

 

Markdown Tutorial

마크다운(Markdown)은 웹의 콘텐츠를 쓰는 방법 중 하나입니다. 마크다운은 “평문(plaintext)”으로 구성되어 있습니다. 평문이란 우리들이 쓰고 보는 것에 익숙해져 있는 종류의 텍스트입니다. 평��

www.markdowntutorial.com

.gitignore

- 여기 적힌 폴더와 파일은 git이 트래킹 하지 않음. 내 컴퓨터에 파일은 존재하나 git은 없는 것처럼 취급하여 github에도 업로드가 되지 않음.

- 보통 컴퓨터마다 다르므로 다른 사람들이 필요없는 불필요한 설정파일, 예를들어 venv 폴더 등과 보안을 위해 공개되면 안되는 파일 목록을 여기 적어두면 된다.

1. 프로젝트 이름

"매일특가" (가칭)

 

 

 

2. 프로젝트 설명

대한민국의 각 커뮤니티에는 여러 게시판이 있고, 그 중 특가 게시판이 있는 경우가 많다. 여기에는 한정된 시간동안 잠깐 진행하는 딜이나 오프라인 구매처의 특별 세일 등이 올라온다. 이 특가 제품들은 일반적인 구매처에서 (네이버 쇼핑 등) 사는 금액보다 조금 또는 훨씬 저렴한 경우가 많다. 그런데 커뮤니티가 매우 많으므로 대부분 특정 커뮤니티에서 한정된 정보를 얻게 된다. 

아니면 주기적으로 여러 사이트들을 돌아다니며 특가정보를 확인할 수는 있지만 번거로우며, 시간도 일정 부분 소요된다. 따라서 이러한 특가정보를 여러 커뮤니티에서 돌아다니지 않고 하나의 사이트에서 한번에  볼 수 있도록 하여 효과적으로 특가정보를 얻을 수 있다.

 

 

 

3. 프로젝트 생김새

 

 

4. 개발해야 할 기본기능

 

(1) 스크래핑

- 커뮤니티 별 특가 사이트들의 게시글들을 스크래핑하며, DB(mongodb)에 삽입 ()

- 스크래핑 항목 : 제목, 가격, 링크, 시간, 이미지

- 스크래핑 단위 : 글 리젠률이 높지 않으므로, 각 사이트당 1~2페이지를 스크래핑

- 스크래핑 주기 : 매 1시간, 새벽 1시부터 오전7시까지는 스크래핑을 수행하지 않음.

 

(2) 데이터베이스 작업

-  중복 제거 : 글 리젠률에 따라 이전 정보가 스크래핑 될 수 있으므로, "링크" 단위로 중복을 제거한다.

 

(3) API 제작

- 스크래핑한 데이터를 DB에 저장하는 API (매 1시간 반복)

- DB에 저장된 데이터를 사이트로 출력하는 API (매 1시간 반복)

- 데이터 출력은 [제목, 링크, 사이트명, 가격, 시간] 으로 출력한다. 일반 게시판의 형태가 된다.

 

 

 

 

5. 추가기능

- 기본 검색 : 제목들 중 키워드로 검색 (DB에서 해당 조건으로 검색하여 다시 데이터를 뿌려주는 형태)

- 조건 검색 : 가격별(최저가별 등), 날짜별(오늘, 최근3일 등)

- 원하는 검색 키워드 알림 : 회원 가입 기능 및 카톡 알림 기능 (또는 메일)

- 게시물의 클릭 수 기록

- 모바일/데스크탑 모드

 

 

 

6. 프로젝트 완료 후 추가 기능

- 다른 특가 사이트 추가

- 각 특가 사이트에서 구분한 카테고리를 기반으로 각 게시물의 카테고리 정보를 삽입, 카테고리 검색기능 추가

- 종료된 딜 표시 : 해당 사이트가 연결이 안되거나, 가격이 바뀌는 등이 확인되면, 종료되었다고 표시

- 각 내용들에 tag를 붙이고 categorized 화 하기 (메타데이터 사용)

 

 

 

7. 프로젝트에 필요한 데이터 소스

 

(1) 스크래핑 소스

- 퀘이사존 : https://quasarzone.com/bbs/qb_saleinfo

- 카톡 톡딜 : https://store.kakao.com/

- 클리앙 알뜰구매 : https://www.clien.net/service/board/jirum

 - 펨코 핫딜 : https://www.fmkorea.com/hotdeal

- 루리웹 핫딜1 : https://bbs.ruliweb.com/market/board/600004

- 루리웹 핫딜2 : https://bbs.ruliweb.com/market/board/1020

- 뽐뿌 오프라인뽐뿌 : http://www.ppomppu.co.kr/zboard/zboard.php?id=ppomppu5

 

(2) 기술자료

- 카톡 알림 : https://developers.kakao.com/docs/latest/ko/message/common

- 현재 접속자 수 확인 모듈 : : https://www.jbfactory.net/11404

- 구글 애널리틱스 (총 방문자수 등) : https://analytics.google.com/analytics/web/

 

 

 

8. API 설계

기능 요청 (Request) 응답 (Response)
요청 URL 요청 방식
(GET / POST)
요청
데이터
응답데이터
크롤링한
데이터를
웹에 게시
/api/publish_data GET 제목(dealtitle)
링크(dealurl)
가격(dealprice)
소스사이트(dealsource)
날짜(dealdate)
클릭수(countclick)
(JSON 형식)
'result'= 'success'
데이터 클릭
수 업데이트
api/click POST 클릭수(countclick) (JSON 형식)
'result'= 'success'
제목 검색 /api/search_name GET 제목(dealtitle)
링크(dealurl)
가격(dealprice)
소스사이트(dealsource)
날짜(dealdate)
클릭수(countclick)
(JSON 형식)
'result'= 'success'
날짜 검색
(오늘)
/api/search_today GET 제목(dealtitle)
링크(dealurl)
가격(dealprice)
소스사이트(dealsource)
날짜(dealdate)
클릭수(countclick)
(JSON 형식)
'result'= 'success'
날짜 검색
(최근 3일)
/api/search_3days GET 제목(dealtitle)
링크(dealurl)
가격(dealprice)
소스사이트(dealsource)
날짜(dealdate)
클릭수(countclick)
(JSON 형식)
'result'= 'success'

 

 

 

 

 

'SCC 9기' 카테고리의 다른 글

[SCC 9기] - Project TIL : 스크래핑  (0) 2020.08.22
[SCC 9기] - 6주차  (0) 2020.08.22
[SCC 9기] 5주차  (0) 2020.08.11
[SCC 9기] 4주차  (0) 2020.08.11
[SPC 9기] 3주차  (0) 2020.07.25

셀레니움

데이터가 외부에 있는 것들. 주식 매출,영업이익등의 데이터 표같은거. 이건 데이터를 웹으로 보여준게 아니고, 디비에 있는걸 가져오는거라서. 일반적으로 스크래핑이 되지 않음. 셀레니움은 가능하다.

 

 

# 실제 웹페이지의 스크립트 코드를 스크래핑하면, 안되는경우.

beautifulsoup 쓰면,  tbody태그같은게 없어질  있음.

먼저 bs4 파싱한 데이터를 print 한다음, 결과를 메모장에 넣고 그걸 직접 위치를 파악해야 .

 

tr여러개가 있는 테이블의 처음위치를 찍을 , tr까지 찍어줘야 .

trs = soup.select('#old_content > table > tbody > tr') 이런식으로.

tbody 전체 테이블인데 나는 tbody까지 했었음;;

 

 

 

 

My favorite movie star 사이트 만들기

 

 

 

기본 기능

- 영화인 정보를 카드로 보여주기 (read)

- 좋아요 기능 (update) : 영화인 카드의 "위로" 버튼을 누르면 좋아요 옆 숫자가 추가되고, 좋아요가 많은 순서대로 위에서부터 정렬된다.

- 영화인 정보 삭제하기 (delete) : 삭제 버튼을 누르면 해당 영화인이 삭제된다.

 

api 정리

- 조회기능 : 영화인 정보 전체를 가져옴

- 좋아요기능 : 클라이언트에서 받은 이름을 찾아서 좋아요를 증가함

- 삭제 : 클라이언트에서 받은 이름으로 영화인을 찾고, 해당 정보를 db에서 삭제.

 

 

각 api 상세 설명

 

1. 영화인 정보를 카드로 보여줌 (영화인 이름, 영화인 이미지, 좋아요 개수, 최근 작품 내용)

 

A. 요청 정보

  • 요청 URL= /api/list , 요청 방식 = GET
  • 요청 데이터 : 없음

B. 서버가 제공할 기능 : 데이터베이스에 영화인 정보를 조회(Read)하고, 영화인 정보를 응답 데이터로 보냄

C**. 응답 데이터 :** (JSON 형식) 'result'= 'success', 'stars_list'= 영화인 정보 리스트

 

서버로직

영화인 정보 전체를 조회하기 위해 서버가 받을 정보는 없습니다. 조건없이 모든 정보를 보여줄 것이니까요! =

따라서 서버 로직은 다음 단계로 구성되어야 합니다.

1. mystar 목록 전체를 검색합니다. ID는 제외하고 like 가 많은 순으로 정렬

2. 성공하면 success 메시지와 함께 stars_list 목록을 클라이언트에 전달

 

클라이언트로직

영화인 정보 전체를 조회하기 위해 서버가 받을 정보는 없습니다. 조건없이 모든 정보를 보여줄 것이니까요!

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.

1. #star_box의 내부 html 태그를 모두 삭제

2. 서버에 1) GET 방식으로, 2) /api/list 라는 주소로 stars_list를 요청

3. 서버가 돌려준 stars_list를 stars라는 변수에 저장

4. for 문을 활용하여 stars 배열의 요소를 차례대로 조회

5. stars[i] 요소의 name, url, img_url, recent, like 키 값을 활용하여 값 조회

6. 영화인 카드 코드 만들어 #star-box에 붙이기

 

 

2. 좋아요(Update) 기능: 클라이언트에서 받은 이름(name_give)으로 찾아서 좋아요(like)를 증가

 

A. 요청 정보

  • 요청 URL= /api/like , 요청 방식 = POST
  • 요청 데이터 : 영화인 이름(name_give)

B. 서버가 제공할 기능 : 영화인 이름(요청 데이터)과 일치하는 영화인 정보의 좋아요 수를 한 개 증가시켜 데이터베이스에 업데이트하고(Update), 성공했다고 응답 메세지를 보냄

 

C. 응답 데이터 : (JSON 형식) 'result'= 'success'

 

 

서버로직

영화인 카드의 좋아요 수를 증가시키기 위해 서버가 클라이언트에게 전달받아야하는 정보는 다음과 같습니다.

- 영화인의 이름 (name_give)

따라서 서버 로직은 다음 단계로 구성되어야 합니다.

1. 클라이언트가 전달한 name_give를 name_receive 변수에 넣습니다.

2. mystar 목록에서 find_one으로 name이 name_receive와 일치하는 star를 찾습니다.

3. star의 like 에 1을 더해준 new_like 변수를 만듭니다.

4. mystar 목록에서 name이 name_receive인 문서의 like 를 new_like로 변경합니다

 

클라이언트로직

좋아요 수를 증가시키기 위해 클라이언트가 전달할 정보는 다음과 같습니다.

- 영화인의 이름 (name_give)

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.

1. 서버에 1) POST 방식으로, 2) /api/like 라는 url에, 3) name_give라는 이름으로 name을 전달합니다. (참고) POST 방식이므로 data: {'name_give': name} 사용

2. '좋아요 완료!' alert 창을 띄웁니다.

3. 변경된 정보를 반영하기 위해 새로고침합니다.

 

 

3. 삭제(Delete) 기능: 클라이언트에서 받은 이름(name_give)으로 영화인을 찾고, 해당 영화인을 삭제

 

A. 요청 정보

  • 요청 URL= /api/delete , 요청 방식 = POST
  • 요청 데이터 : 영화인 이름(name_give)

B. 서버가 제공할 기능 : 영화인 이름(요청 데이터)와 일치하는 영화인 정보를 데이터베이스에서 삭제(Delete)하고, 성공했다고 응답 메세지를 보냄

 

C. 응답 데이터 : (JSON 형식) 'result'= 'success'

 

서버로직

영화인 카드를 삭제하기 위해 필요한 정보는 다음과 같습니다.

- 영화인의 이름 (name_give)

따라서 서버 로직은 다음 단계로 구성되어야 합니다.

1. 클라이언트가 전달한 name_give를 name_receive 변수에 넣기

2. mystar 에서 delete_one으로 name이 name_receive와 일치하는 star를 제거

3. 성공하면 success 메시지를 반환

 

클라이언트로직

영화인 카드를 삭제하기 위해 필요한 정보는 다음과 같습니다.

- 영화인의 이름 (name_give)

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.

1. 서버에 1) POST 방식으로, 2) /api/delete 라는 url에, 3) name_give라는 이름으로 name을 전달 (참고) POST 방식이므로 data: {'name_give': name}

2. '삭제 완료! 안녕!' alert창 띄우기

3. 변경된 정보를 반영하기 위해 새로고침

 

 

DB 생성 구문 (init_db.py)

- 해당 구문을 실행하면 내 DB에 정보들이 저장된다.

# 이 구문은 db를 생성하는 구문이다. 다른건 없음. 즉 영화인에 대한 정보를 스크래핑하여 DB를 생성한다.

import requests
from bs4 import BeautifulSoup
from pymongo import MongoClient

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


# DB에 저장할 영화인들의 출처 url을 가져옵니다.
def get_urls():
    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('https://movie.naver.com/movie/sdb/rank/rpeople.nhn', headers=headers)
    soup = BeautifulSoup(data.text, 'html.parser')

    # 네이버 영화인정보 테이블 데이터
    trs = soup.select('#old_content > table > tbody > tr')



    urls = []
    for tr in trs:
        a = tr.select_one('td.title > a')
        if a is not None:
            base_url = 'https://movie.naver.com/'
            url = base_url + a['href']
            urls.append(url)

    return urls
# 결과적으로 딱 50명에 대한 링크만 다 가져온다.
# 아래에서 해당 링크를 열어서 스크래핑을 수행하는 것.


# 출처 url로부터 영화인들의 사진, 이름, 최근작 정보를 가져오고 mystar 콜렉션에 저장합니다.
def insert_star(url):
    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')

    name = soup.select_one('#content > div.article > div.mv_info_area > div.mv_info.character > h3 > a').text
    img_url = soup.select_one('#content > div.article > div.mv_info_area > div.poster > img')['src']
    recent_work = soup.select_one('#content > div.article > div.mv_info_area > div.mv_info.character > dl > dd > a:nth-child(1)').text

    doc = {
        'name': name,
        'img_url': img_url,
        'recent': recent_work,
        'url': url,
        'like': 0
    }

    db.mystar.insert_one(doc)
    print('완료!', name)



# 기존 mystar 콜렉션을 삭제하고, 출처 url들을 가져온 후, 크롤링하여 DB에 저장합니다.
def insert_all():
    db.mystar.drop()  # mystar 콜렉션을 모두 지워줍니다.
    urls = get_urls()
    for url in urls:
        insert_star(url)


### 실행하기
insert_all()

 

 

메인 구문 (app.py)

- 메인구문으로 디비 저장후, 실제로 사이트 운영하는.

from pymongo import MongoClient
from flask import Flask, render_template, jsonify, request

app = Flask(__name__)
client = MongoClient('localhost', 27017)
db = client.dbsparta


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


# API 역할을 하는 부분
@app.route('/api/list', methods=['GET'])
def show_stars():
    # 1. db에서 mystar 목록 전체를 검색합니다. ID는 제외하고 like 가 많은 순으로 정렬합니다.
    # 참고) find({},{'_id':False}), sort()를 활용하면 굿!
    # {} 다 가져오라는 것이며, 뒤에 .sort는 전체 값에 대해 sort를 적용한다. 괄호 잘 볼것.  (sort는 1이 오름차순(점점 커지는 형태), -1이 내림차순(점점작아지는형태. 젤큰값이 위로 간다)
    # 즉, 홈페이지가 새로고침되면, 여기 좋아요가 큰거부터 차례대로 넣는다는 뜻임. 만약 숫자가 동률이면, 여기가 내림차순이니까 한글이름도 내림차순이어서 ㅎ 부터 되는것임. sort에 추가 옵션을 주면 한글도 다르게 정렬할수있다고 함. 검색.
    stars = list(db.mystar.find({}, {'_id': False}).sort('like', -1))
    # 2. 성공하면 success 메시지와 함께 stars_list 목록을 클라이언트에 전달합니다.
    return jsonify({'result': 'success', 'stars_list': stars})


@app.route('/api/like', methods=['POST'])
def like_star():
    # 1. 클라이언트가 전달한 name_give를 name_receive 변수에 넣습니다.
    name_receive = request.form['name_give']

    # 2. mystar 목록에서 find_one으로 name이 name_receive와 일치하는 star를 찾습니다.
    star = db.mystar.find_one({'name': name_receive})
    # 3. star의 like 에 1을 더해준 new_like 변수를 만듭니다.
    new_like = star['like'] + 1

    # 4. mystar 목록에서 name이 name_receive인 문서의 like 를 new_like로 변경합니다.
    # 참고: '$set' 활용하기!
    db.mystar.update_one({'name': name_receive}, {'$set': {'like': new_like}})

    # 5. 성공하면 success 메시지를 반환합니다. 성공했따는것을 알려주기 위함.
    return jsonify({'result': 'success'})


@app.route('/api/delete', methods=['POST'])
def delete_star():
    # 1. 클라이언트가 전달한 name_give를 name_receive 변수에 넣습니다.
    name_receive = request.form['name_give']
    # 2. mystar 목록에서 delete_one으로 name이 name_receive와 일치하는 star를 제거합니다.
    db.mystar.delete_one({'name': name_receive})
    # 3. 성공하면 success 메시지를 반환합니다.
    return jsonify({'result': 'success'})


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

 

 

html 구문 (index.html)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>마이 페이보릿 무비스타 | 프론트-백엔드 연결 마지막 예제!</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css"/>
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
    <style>
        .center {
            text-align: center;
        }

        .star-list {
            width: 500px;
            margin: 20px auto 0 auto;
        }

        .star-name {
            display: inline-block;
        }

        .star-name:hover {
            text-decoration: underline;
        }

        .card {
            margin-bottom: 15px;
        }
    </style>
    <script>
        $(document).ready(function () {
            // index.html 로드가 완료되면 자동으로 showStar() 함수를 호출합니다.
            showStar();
        });

        function showStar() {
            // 1. #star_box의 내부 html 태그를 모두 삭제합니다.
            // 데이터베이스 내용 뿌리기 전에 항상 비워준다.
            $('#star-box').empty()

            // 2. 서버에 1) GET 방식으로, 2) /api/list 라는 주소로 star_list를 요청합니다.
            $.ajax({
                type: "GET",
                url: "/api/list",
                data: {},
                success: function (response) {
                    // 3. 서버가 돌려준 star_list를 star라는 변수에 저장합니다.
                    let stars = response['stars_list']
                    // 4. for 문을 활용하여 star 배열의 요소를 차례대로 조회합니다.
                    for (let i = 0; i < stars.length; i++) {
                        let star = stars[i]
                        // 5. star[i] 요소의 name, url, img_url, recent, like 키 값을 활용하여 값을 조회합니다.
                        let name = star['name']
                        let url = star['url']
                        let imgUrl = star['img_url']
                        let recentWork = star['recent']
                        let like = star['like']

                        // 6. 영화인 카드를 만듭니다.
                        let tempHtml = `<div class="card">
                                <div class="card-content">
                                  <div class="media">
                                    <div class="media-left">
                                      <figure class="image is-48x48">
                                        <img
                                          src="${imgUrl}"
                                          alt="Placeholder image"
                                        />
                                      </figure>
                                    </div>
                                    <div class="media-content">
                                      <a href="${url}" target="_blank" class="star-name title is-4">${name} (좋아요: ${like})</a>
                                      <p class="subtitle is-6">${recentWork}</p>
                                    </div>
                                  </div>
                                </div>
                                <footer class="card-footer">
                                  <a href="#" onclick="likeStar('${name}')" class="card-footer-item has-text-info">
                                    위로!
                                    <span class="icon">
                                      <i class="fas fa-thumbs-up"></i>
                                    </span>
                                  </a>
                                  <a href="#" onclick="deleteStar('${name}')" class="card-footer-item has-text-danger">
                                    삭제
                                    <span class="icon">
                                      <i class="fas fa-ban"></i>
                                    </span>
                                  </a>
                                </footer>
                              </div>`

                        // 7. #star-box에 temp_html을 붙입니다.
                        $('#star-box').append(tempHtml)
                    }
                }
            });
        }

        function likeStar(name) {
            // 1. 서버에 1) POST 방식으로, 2) /api/like 라는 url에, 3) name_give라는 이름으로 name을 전달합니다.
            // 참고) POST 방식이므로 data: {'name_give': name} 과 같은 양식이 되어야합니다!
            $.ajax({
                type: "POST",
                url: "/api/like",
                data: {'name_give': name},
                success: function (response) {
                    if (response['result'] == 'success') {
                        // 2. '좋아요 완료!' 얼럿을 띄웁니다.
                        alert('좋아요 완료!')
                        // 3. 변경된 정보를 반영하기 위해 새로고침합니다. 중요!
                        window.location.reload()
                    }
                }
            });
        }

        function deleteStar(name) {
            // 1. 서버에 1) POST 방식으로, 2) /api/delete 라는 url에, 3) name_give라는 이름으로 name을 전달합니다.
            // 참고) POST 방식이므로 data: {'name_give': name} 과 같은 양식이 되어야합니다!
            $.ajax({
                type: "POST",
                url: "/api/delete",
                data: {'name_give': name},
                success: function (response) {
                    if (response['result'] == 'success') {
                        // 2. '삭제 완료! 안녕!' 얼럿을 띄웁니다.
                        alert('삭제 완료! 안녕!')
                        // 3. 변경된 정보를 반영하기 위해 새로고침합니다.
                        window.location.reload()
                    }
                }
            });
        }

    </script>
</head>
<body>
<section class="hero is-warning">
    <div class="hero-body">
        <div class="container center">
            <h1 class="title">
                마이 페이보릿 무비스타😆
            </h1>
            <h2 class="subtitle">
                순위를 매겨봅시다
            </h2>
        </div>
    </div>
</section>
<div class="star-list" id="star-box">
    <div class="card">
        <div class="card-content">
            <div class="media">
                <div class="media-left">
                    <figure class="image is-48x48">
                        <img
                                src="https://search.pstatic.net/common/?src=https%3A%2F%2Fssl.pstatic.net%2Fsstatic%2Fpeople%2Fportrait%2F201807%2F20180731143610623-6213324.jpg&type=u120_150&quality=95"
                                alt="Placeholder image"
                        />
                    </figure>
                </div>
                <div class="media-content">
                    <a href="#" target="_blank" class="star-name title is-4">김다미 (좋아요: 3)</a>
                    <p class="subtitle is-6">안녕, 나의 소울메이트(가제)</p>
                </div>
            </div>
        </div>
        <footer class="card-footer">
            <a href="#" onclick="likeStar('김다미')" class="card-footer-item has-text-info">
                위로!
                <span class="icon">
              <i class="fas fa-thumbs-up"></i>
            </span>
            </a>
            <a href="#" onclick="deleteStar('김다미')" class="card-footer-item has-text-danger">
                삭제
                <span class="icon">
              <i class="fas fa-ban"></i>
            </span>
            </a>
        </footer>
    </div>
</div>
</body>
</html>

 

결과

 

 

 

'SCC 9기' 카테고리의 다른 글

[SCC 9기] - 6주차  (0) 2020.08.22
[SCC 9기] - Project 개발 일지 : 프로젝트 기획안  (0) 2020.08.13
[SCC 9기] 4주차  (0) 2020.08.11
[SPC 9기] 3주차  (0) 2020.07.25
[SPC 9기] 2주차  (0) 2020.07.24

파이썬 가상환경 생성하기

파이참에서 사용할 폴더를 오픈한다.

 

이렇게 폴더가 딱 하나 있다.

파이참에서, FILE - SETTING -PROJECT INTERPRETER

여기서 다음과 같이 톱니바퀴 - ADD 를 누르면,

 

아래와 같은 화면이 나온다. NEW ENVIRONMENT 가 체크되어야 하고, 경로는 내가 만든 경로인 BOOKREVIEW의 venv 가 되어야 한다. venv가 가상으로  환경정보가 저장되는 곳이다.

즉 bookreview 프로젝트의 가상환경정보가 된다.

 

모두 ok 하면 완료

 

파이썬 패키지, 프레임워크, 라이브러리

패키지보다 큰 게 라이브러리, 라이브러리보다 큰 게 프레임워크(라이브러리+여러개발도구포함) (정확하진 않지만, 대충 이렇게 이해하면 됨)

 

프레임워크는 남이 만들어놓은 틀 안에서 그대로 움직이기 때문에 편함 그래서 디렉토리구조도 만들라는대로만들어야함. 라이브러리는 내맘대로 짜는데 남이 만들어놓은걸 군데군데 가져오는것.

 

 

 

 

API 작동 리뷰

api 작동

1. 클라이언트가, 즉 내 chrome 창에서

2. 요청을 하면, 즉 api의 주소를 입력하고 엔터치면 https://api.thecatapi.com/v1/images/search

3. API의기능, 즉 정해진 동작인 랜덤 고양이 사진 데이터 조회를 실행하고

4. 응답, 즉 고양이 사진 데이터를 클라이언트에 응답한다. JSON으로 응답한다. 이게 무조건 JSON으로 응답하는건 아닐것임. 

 

API 사용 약속

1. 요청정보 : 요청URL, 요청방식(GET, POST 등)

2. 서버가 제공할 기능 : 데이터 조회, 데이터 생성 등

3. 응답할 데이터 형식 : 어떤 KEY로 어떤 데이터를 줄지 등 

- 이런 약속을 모두 API 문서에 적어두는 것이다.

 

 

플라스크(flask)

- 이번에 서버(백엔드) API를 만드는데 사용한다.

- 프레임워크이다. 서버를 만들어주는 프레임워크.

- 웹을 만들고 서버를 구동시키기 편하게 하는 프레임워크, 개발도구이다.

플라스크 공식문서 : https://flask.palletsprojects.com/en/1.1.x/#

플라스크 비공식 한글 번역문서 : https://flask-docs-kr.readthedocs.io/ko/latest/foreword.html

 

 

플라스크를 쓸려면 아래 3가지의 디렉토리 구조를 만들어야 한다.

 

1. app.py 파이썬파일이 하나 필요. 실행의 주체가 됨. 보통 app.py라고 한다.   app.py 안해도 되는데 가급적 그렇게 해라.

2. static 폴더 : 이미지, css 파일 넣음

3. templates 폴더 : html 파일 넣음.  템플릿츠임. 에스빠지면안된다.

 

플라스크에서 위 디렉토리와 파일을 쓸거라고 되어있어서 이건 그냥 약속임. 그대로 해야 함.

이런식.

 

 

예시코드

from flask import Flask



app = Flask(__name__)



@app.route('/')

def hello_world():

    return '<button>나는 버튼이다</button>'



if __name__ == '__main__':

    app.run('0.0.0.0', port=5000, debug=True)

 

코드 실행하면, 이런거 뜨는데 ok 하면 됨

그리고 상태 메시지에서 running on  뜨면 잘 된 것.

이게 서버를 하나 만든것이다.

 

 

이렇게 확인할 수 있다. 웹브라우저에서.

localhost:5000 / 0.0.0.0:5000 (방화벽때문에 잘 안될 수 있음)

 

 

## 플라스크로부터 플라스크 패키지만 임포트함.

from flask import Flask



## 약속이다. 플라스크 개체를 만든 것임. 무조건 넣으면 됨.

app = Flask(__name__)



## 해당 주소의 딱 /까지, 즉 www.naver.com/ 까지, 위 예시에서는 localhost:5000/ 까지 하면, 아래가 실행된다.

## 아래에서 정한 주소 뒤에 / 면 아래를 실행해라! 이런느낌.   그리고 리턴을 하는데, html을 리턴할 수 있음!

@app.route('/')

def hello_world():

    return '<button>나는 버튼이다</button>'





## 이부분이 메인이다. 여기서 실행이 시작임. 이것도 고정된 부분임. app.run 안에 있는 내용이 바뀔 수 있음

## 이게 서버를 만든 것임.

if __name__ == '__main__':

    app.run('0.0.0.0', port=5000, debug=True)
      
# 이 코드를 만나면, 서버가 계속 돈다. 다시말해서 app.route를 여기 if __name 밑으로 가면, 적용안됨. 즉 이 app.run은 맨 마지막에 있어야 한다는거네.

 

서버를 중지시키려면, 붉은버튼 누르면 됨.

중지시키면 당연히 localhost:5000/은 접속안됨

 

이런것들은, 서버에 접속하고 그러면 로그가 발생하는 것임. 실시간으로 발생함.

이게 api를 만든 것임. 주소 뒤에 / 에선 뭔가를 하고, /mypage는 뭔가를 하고.. 그런식이다.

 

 

 

 

플라스크로 url을 만들고, html 띄우기

- templetes 폴더에, index.html을 만들고 아래와 같이 생성한다.  이름이 꼭 index.html 일필요 없음. 바꿔도됨

- 참고 : 이미지는 static에 넣는다. 그리고 위와 같이 경로를 지정해주면 되며, 형식은 원래 이렇게 하는거니까 그대로 한다.

<!DOCTYPE html>
<html lang="en">
<head>
<!DOCTYPE html>
<html lang="ko">
    <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.5.1/jquery.min.js"></script>
        <title>첫 Flask 프로젝트</title>
    </head>

    <body>
        <h1>서버를 만들었다!</h1>
	 <img src="{{ url_for('static', filename='rome.jpg') }}"/>
    </body>

</html>

 

- 또한, {{url_for('static',filename='rome.jpg')}} 이방식을 그대로 사용한다. 플라스크에서 스태틱폴더에 접근할 때,  rome.jpg라는 파일을 가져와 라는 약속임.

 

- 이제 app.py를 아래와 같이 만든다.

- render_template 라는 함수를 사용한다 함수는 html 그대로 가져와서 보여준다.

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')   ## 여기는 위에서 만든 인덱스를 들어가는 것이다.
def home():  # 함수명 수정 - 이름만 보고 접속되는 페이지를 확인할 수 있게!
    return render_template('index.html')

@app.route('/mypage')  ## 이렇게 주소를 마음대로 만들 수 있다. 
def my_page():
    return 'This is My Page!'

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

- 참고 : @app.route(url경로) 에서, url경로와 함수명은 유일해야 한다.여러개의 함수가 있으면 어떤 함수를 실행해야 할 지 알 수 없기 때문.

 

 

 

get방식의 api만들고, 클라이언트에서 호출하기

 

다시 api를 리마인드해보자

1. 요청정보

2. 서버가 제공할 기능 : 예 : 클라이언트에게 정해진 메시지를 보내는 기능

3. 응답할 메시지 데이터 : json 형식,  'result'= 'success', 'msg'= '이 요청은 get!' 이런걸 보낸다고 정함.

 

구문

from flask import Flask, render_template, jsonify, request

app = Flask(__name__)

@app.route('/')
def home():  # 함수명 수정 - 이름만 보고 접속되는 페이지를 확인할 수 있게!
    return render_template('index.html')

##여기 접속하고 get방식일때, 아래 함수를 작동해라.
@app.route('/test', methods=['GET'])
def test_get():
    # title_give 은 네이버 영화에서 code=xxxxxx 이런것임. 즉 우리가 정의하는 값이다.
    title_receive = request.args.get('title_give')  ### 봄날은 간다 라고 하면, 그게 title_receive로 들어가고 print됨. 즉 요청한걸 잘 받았음.
    ## 이거 프린트는 그냥 콘솔에 나오게 할라고... 별거아님. 
    print(title_receive)
    return jsonify({'result': 'success', 'msg': '이 요청은 GET!'})

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

 

결과

브라우저에서. localhost:5000/test?title_give=원하는값

파이참에서도 출력된다.

여기서 보면, title_give의 value 값으로 봄날은 간다가 들어가서, title_receive 변수에 들어갔고, 아래 print로 프린트된것이다.

 

다시 정리하면, /test에 get 방식이 요청을 하고 title_give라는 값을 명시하면, 그 값에 대해 뭔가를 하면 특정 키값을 구할 수 있는 것.

request.args.get : 정의된 함수임.

 

봄날은간다 이거를 맞춰서 어떤 결과를 출력하도록 코딩할 수 있다.

실제로 문서에 title_give 여기다가 값을 넣어서 요청해라 이런식으로 해야 함.

 

이거를 ajax를 써서 호출하면?

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

- 이 요청은 get!, success 가 뜰 것이다. 또한 prin(내용) 도 파이참에 나올것이다.

 

 

 

 

post 형태의 api 만들고, 클라이언트에서 호출하기

다시 api를 리마인드해보자

1. 요청정보  : 주소 /test , 방식 : POST, 요청데이터는 없음.

2. 서버가 제공할 기능 : 예 : 클라이언트에게 정해진 메시지를 보내는 기능

3. 응답할 메시지 데이터 : json 형식,  'result'= 'success', 'msg'= '이 요청은 POST!' 이런걸 보낸다고 정함.

 

 

post방식은 ajax에서 url에 포함하지 않고, data에다가 넣어야 함. 즉 링크주소에 보여지지 않고 숨겨져야 하기 때문.

회원가입정보등

 

여기서는 request.form로 값을 가져온다.

post방식에서는 보통 json을 리턴하지 않고, 내부에서 작업한다. 데이터베이스 업데이트 등. 근데 여기 예시에서는 그냥 볼라고..

 

API 제작

from flask import Flask, render_template, jsonify, request

app = Flask(__name__)

## HTML을 주는 부분
@app.route('/')
def home(): #함수명수정-이름만보고접속되는페이지를확인할수있게!
    return render_template('index.html')

## API 역할을 하는 부분
##여기접속하고get방식일때,아래함수를작동해라.
@app.route('/test', methods=['GET'])
def test_get():
    title_receive = request.args.get('title_give')
    print(title_receive)
    return jsonify({'result': 'success', 'msg': '이 요청은 GET!'})

@app.route('/test', methods=['POST'])
def test_post():
#title_give은네이버영화에서code=xxxxxx이런것임.즉우리가정의하는값이다.
    title_receive = request.form['title_give']
    print(title_receive)
    return jsonify({'result': 'success', 'msg': '이 요청은 POST!'})

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

 

ajax로 요청하기 post방식으로.

여기서는 data 항목에 실제 값을 넣어준다.  get처럼 주소에 넣지 않고.

$.ajax({
    type: "POST",
    url: "/test",
    data: { title_give:"봄날은간다" },   <- 데이터를 주소에 안넣고 여기다가 key:value형태로 준다.
    success: function(response){
       console.log(response)
    }
  })

 

결과

 

이제 보면, 동일한 /test 인데 get방식인지, post 방식인지에 따라 다르게 움직일 수 있는 것임. 코드도 좀 다르고.

여기 예시까지는 요청 데이터를 piint로 출력해서 확인했다. 실전에선 요청 데이터로 여러가지 작업을 할 수 있다. 예를들어, 요청 데이터 title_give인 제목의 영화 정보만 보여준다던지.

 

즉 @app.route( )밑에

받은, 또는 보낼 값을  일단 변수로 다 받고, 그 값을 어떻게 디비로 보낼지 아니면 어찌할지등을 해야 하는것임.

 

 

 

 

api 만드는 순서

이렇게 하나씩 하나씩 잘 진행해야 함.

즉, 먼저 api 부터 정의를 다 하고, 그걸 이제 html에서 받아서 결과를 내는 것임. 이게 순서.

 

* 포스트 api 만들기

1. 클라이언트 서버 상태 확인

2. 서버먼저 만들기

3. 클라이언트 만들기

4. 완성 확인

 

* get api 만들기

1. 클라이언트 서버 상태 확인

2. 서버먼저 만들기

3. 클라이언트 만들기

4. 완성 확인

 

 

 

 

실습 - 모두의 책 리뷰 완성

 

전체적인 틀

홈페이지 : html, css / 여기서는 홈페이지가 클라이언트가 된다.

 

리뷰작성화면 : POST API 호스트가 된다.

- title, author, review 가져오기

- db에 정보 삽입하기

- 성공 메시지 반환하기

 

저장된 리뷰확인화면 :GET API

데이터를 가져오는 것. api에 get을 사용해서 db에서 가져옴

 

리뷰작성 API와 클라이언트

아래 클라이언트와 서버 코드는 서로 쌍을 이룬다.

 

API 서버 : 리뷰작성 API  (POST)

- 요청 url : /review

- 요청방식 : post

- 요청데이터 : title, author, review

- 서버가 제공할 기능 : 클라이언트가 보낸 데이터를 가지고 db에 생성하고, 저장이 성공했다고 응답을 보냄

- 응답 데이터 : (json 형식) 'result'= 'success',  'msg'= '리뷰가 성공적으로 작성되었습니다.'

 

API를 사용할 클라이언트

- 리뷰 작성 시 필요한 정보 : title, author, review

- 클라이언트는 input을 받아 title, author, review를 가져온다.

- 입력값이 하나라도 없으면 alert을 띄운다.

- ajax로 api서버에 연결하여 저장을 요청하고 화면을 새로고침한다.

 

리뷰 목록 가져오는 API와 클라이언트

아래 클라이언트와 서버 코드는 서로 쌍을 이룬다.

 

API 서버 : 리뷰 목록 가져오는 API (GET)

- 요청 URL : /review

- 요청방식 : GET

- 요청 데이터 : 없음

- 서버가 제공할 기능 : 데이터베이스에 리뷰 정보를 조회(READ) 하고, 성공 메시지와 리뷰 정보를 응답 데이터로 보냄.

- 응답 데이터 : (JSON 형식) 'result'= 'success' 'reviews'= 리뷰리스트

 

API를 사용할 클라이언트

- 리뷰를 가져오기 위해 필요한 정보 : title, author, review

- 리뷰 목록을 서버에 요청한다.

- 요청 성공 여부를 확인한다.

- 요청을 성공했을 때, 리뷰를 올바르게 화면에 나타낸다.

 

 

 

 

/templates/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() {
            // 1. 화면에 입력어 있는 제목, 저자, 리뷰 내용을 가져옵니다.
            let title = $("#title").val();
            let author = $("#author").val();
            let review = $("#bookReview").val();

            // 2. 제목, 저자, 리뷰 중 하나라도 입력하지 않았을 경우 alert를 띄웁니다.
            // .focus는 커서를 그 위치에 위치시킴.
            if (title == "") {
                alert("제목을 입력해주세요");
                $("#title").focus();
                return;   // 더이상 실행할필요 없으니 리턴하는것임. 아래는 굳이 할 필요 없자너
            } else if (author == "") {
                alert("저자를 입력해주세요");
                $("#author").focus();
                return;
            } else if (review == "") {
                alert("리뷰를 입력해주세요");
                $("#bookReview").focus();
                return;
            }

            // 3. POST /review 에 저장(Create)을 요청합니다.  여기서 이제 웹으로 받은 값을, ajax로  post api를 호출함.
            $.ajax({
                type: "POST",
                url: "/review",
                data: {title_give: title, author_give: author, review_give: review},
                success: function (response) {
                    if (response["result"] == "success") {
                        alert(response["msg"]);
                        window.location.reload();
                    }
                }
            })
        }

        function showReview() {
            // 1. 리뷰 목록을 서버에 요청하기
            $.ajax({
                type: "GET",
                url: "/review",
                data: {},
                success: function (response) {
                    // 2. 요청 성공 여부 확인하기
                    if (response["result"] == "success") {
                        let reviews = response["reviews"];
                        // 3. 요청 성공했을 때 리뷰를 올바르게 화면에 나타내기
                        for (let i = 0; i < reviews.length; i++) {
                            makeCard(reviews[i]["title"], reviews[i]["author"], reviews[i]["review"]);
                        }
                    } else {
                        alert("리뷰를 받아오지 못했습니다");
                    }
                }
            })
        }

        function makeCard(title, author, review) {
            let tempHtml = `<tr>
                        <td>${title}</td>
                        <td>${author}</td>
                        <td>${review}</td>
                    </tr>`;
            $("#reviews-box").append(tempHtml);
        }


        function validateLength(obj) {
            let content = $(obj).val();
            if (content.length > 140) {
                alert("리뷰는 140자까지 기록할 수 있습니다.");
                $(obj).val(content.substring(0, content.length - 1));
            }
        }
    </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자까지 입력할 수 있습니다." onkeyup="validateLength(this)"></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>

 

app.py

from flask import Flask, render_template, jsonify, request
from pymongo import MongoClient

app = Flask(__name__)

DB_HOST = 'ukdisk.asuscomm.com:32768'
DB_ID = 'root'
DB_PW = '883030aa!'
client = MongoClient('mongodb://%s:%s@%s' % (DB_ID, DB_PW, DB_HOST))
db = client.sparta


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


## API 역할을 하는 부분
@app.route('/review', methods=['POST'])
def write_review():
    # title_receive로 클라이언트가 준 title 가져오기
    title_receive = request.form['title_give']
    # author_receive로 클라이언트가 준 author 가져오기
    author_receive = request.form['author_give']
    # review_receive로 클라이언트가 준 review 가져오기
    review_receive = request.form['review_give']

    # DB에 삽입할 review 만들기
    # 몽고디비에 넣으려면 딕셔너리 형태로 만들어야 함.
    review = {
        'title': title_receive,
        'author': author_receive,
        'review': review_receive
    }
    # reviews에 review 저장하기
    db.book_reviews.insert_one(review)
    # 성공 여부 & 성공 메시지 반환 이걸 안해도 되지만, success되었다는걸 클라이언트에게 알려주기 위해서 하는 것임.
    return jsonify({'result': 'success', 'msg': '리뷰가 성공적으로 작성되었습니다.'})



@app.route('/review', methods=['GET'])
def read_reviews():
    # 1. DB에서 리뷰 정보 모두 가져오기
    reviews = list(db.book_reviews.find({}, {'_id': 0}))
    # 2. 성공 여부 & 리뷰 목록 반환하기
    return jsonify({'result': 'success', 'reviews': reviews})



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

 

완료

 

 

 

 

 

메타태그 크롤링

개요

 

특정 사이트의 url만 가져왔는데, 자동으로 사진, 제목, 내용일부 등을 가져올 수 있다.

이러한 값은 해당 사이트의 url 안에 있는 meta 태그의 값들이다.

 

meta 태그는, <head>~</head> 부분에 들어가는, 눈으로 보이는 것(body) 외에 사이트의 속성을 설명해주는 태그들입니다.

예) 구글 검색 시 표시 될 설명문, 사이트 제목, 카톡 공유 시 표시 될 이미지 등

우리는 그 중 og:image / og:title / og:description 을 크롤링 할 예정입니다.

 

og란? open graph

페북에서 만듬. 링크를 놓으면 이미지, 제목, 디스크립션이 자동으로 생기는 것. 썸네일 등.

 

og:image

og:title

og:description

 

암묵적 표준이 되어 meta 태그 안에 대부분 og 정보가 있게 되었다.

메타태그는 head 부분에 들어가는, 눈에 보이는 것(body) 이외에 사이트의 속성을 설명해주는 태그들이다.

 

메타태그 굳이 왜? 있는 이유는

개발자들이 사용하거나, sns에서 공유할때나 등등 특정 사용처가 있기 때문에 넣는 것임. 구글 검색 시 표시될 설명문, 사이트 제목, 등등

 

 

크롬 검사에서 확인

 

기본 예제

 

메타태그를 가져오려면 평소방법과는 좀 다른방법을 사용해야한다.

 

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

메타태그의 property가 og:image인것을 가져와라. 이런식.

 

import requests
from bs4 import BeautifulSoup

url = 'https://platum.kr/archives/120958'

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 = soup.select_one('meta[property="og:image"]')
og_title = soup.select_one('meta[property="og:title"]')
og_description = soup.select_one('meta[property="og:description"]')

url_image = og_image['content']
url_title = og_title['content']
url_description = og_description['content']

print(url_image)
print(url_title)
print(url_description)

 

참고 : 페이지 새로고침 함수.

window.location.reload();

 

 

 

실습 - 나홀로메모장 완성하기

 

 

기능

- 포스팅하기 : 카드 생성 (create)

- 리스팅하기 : 저장된 카드 보여주기 (read)

 

- url과 코멘트를 서버에 보내면, 서버는 받은 url로 스크래핑하여 이미지,영화제목,소개를 추출해서 db에 코멘트와 함께 저장한다.

- html 카드 형태로 db에 저장한 내용을 출력한다.

 

api 설계 - 카드생성API

클라이언트에서 받은 url, comment를 이용해 페이지 정보를 찾고 저장한다.

 

**A. 요청 정보**
- 요청 URL= `/memo` , 요청 방식 = `POST`
- 요청 데이터 : 제목(url_give), 코멘트(comment_give)

**B. 서버가 제공할 기능** 
- URL의 meta태그 정보를 바탕으로 제목, 설명, 이미지URL 스크래핑
- (제목, 설명, URL, 이미지URL, 코멘트) 정보를 모두 DB에 저장

**C. 응답 데이터**  
- API가 정상적으로 작동하는지 클라이언트에게 알려주기 위해서 성공 메시지 보내기
- (JSON 형식) 'result'= 'success'

 

 

API 설계 - 저장된카드보여주기 API

저장한 카드들을 보여준다.

 

**A. 요청 정보**
- 요청 URL= `/memo` , 요청 방식 = `GET`
- 요청 데이터 : 없음

**B. 서버가 제공할 기능** 
- DB에 저장돼있는 모든 (제목, 설명, URL, 이미지URL, 코멘트) 정보를 가져오기

**C. 응답 데이터**  
- API 동작 잘했다는 성공 메시지, 아티클(기사)들의 정보(제목, 설명, URL, 이미지URL, 코멘트)
- (JSON 형식) 'result'= 'success', 'articles': 아티클 정보

 

 

flask 서버 설계 (api 생성)

 

카드생성api

API 는 약속이라고 했습니다. 위에 미리 설계해 둔 API 정보를 보고 만들어보죠!

메모를 작성하기 위해 서버가 전달받아야하는 정보는 다음 두 가지 입니다.

  • URL(url_give)
  • 코멘트(comment_give)

그리고 URL를 meta tag를 스크래핑해서 아래 데이터를 저장(Create)합니다.

  • URL(url)
  • 제목(title)
  • 설명(desc)
  • 이미지URL(image)
  • 코멘트(comment)

따라서 서버 로직은 다음 단계로 구성되어야 합니다.

  1. 클라이언트로부터 데이터를 받기.
  2. meta tag를 스크래핑하기
  3. mongoDB에 데이터를 넣기

저장된카드보여주는api

메모를 보여주기 위해 서버가 추가로 전달받아야하는 정보는 없습니다. 조건없이 모든 메모를 보여줄 꺼니까요!

따라서 서버 로직은 다음 단계로 구성되어야 합니다.

 

1. mongoDB에서 _id 값을 제외한 모든 데이터 조회해오기 (Read)

2. articles라는 키 값으로 articles 정보 보내주기

 

 

api를 사용할 클라이언트 설계

 

카드생성 클라이언트
메모를 작성하기 위해 서버에게 주어야하는 정보는 다음 두 가지 입니다.
- URL (url_give) : meta tag를 가져올 url
- comment (comment_give) : 유저가 입력한 코멘트

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.
1. 유저가 입력한 데이터를 #post-url과 #post-comment에서 가져오기
2. /memo에 POST 방식으로 메모 생성 요청하기
3. 성공 시 페이지 새로고침하기

 

저장된카드보여주는 클라이언트

 

메모를 작성하기 위해 서버에게 주어야하는 정보는 없습니다. 조건없이 모든 메모를 가져오기 때문입니다.

따라서 클라이언트 로직은 다음 단계로 구성되어야 합니다.

1. /memo에 GET 방식으로 메모 정보 요청하고 articles로 메모 정보 받기

2. , makeCard 함수를 이용해서 카드 HTML 붙이기 (→ 2주차 Ajax 연습과 같습니다!)

 

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 () {
                $("#cards-box").html("");
                showArticles();
            });

            function openClose() {
                // id 값 post-box의 display 값이 block 이면(= 눈에 보이면)
                if ($("#post-box").css("display") == "block") {
                    // post-box를 가리고
                    $("#post-box").hide();
                    // 다시 버튼을 클릭하면, 박스 열기를 할 수 있게 텍스트 바꿔두기
                    $("#btn-post-box").text("포스팅 박스 열기");
                } else {
                    // 아니면(눈에 보이지 않으면) post-box를 펴라
                    $("#post-box").show();
                    // 다시 버튼을 클릭하면, 박스 닫기를 할 수 있게 텍스트 바꿔두기
                    $("#btn-post-box").text("포스팅 박스 닫기");
                }
            }

            function postArticle() {
                let url = $("#post-url").val();
                let comment = $("#post-comment").val();

                // 2. memo에 POST 방식으로 메모 생성 요청하기
                $.ajax({
                    type: "POST", // POST 방식으로 요청하겠다.
                    url: "/memo", // /memo라는 url에 요청하겠다.
                    data: {url_give: url, comment_give: comment}, // 데이터를 주는 방법
                    success: function (response) { // 성공하면
                        if (response["result"] == "success") {
                            alert("포스팅 성공!");
                            // 3. 성공 시 페이지 새로고침하기
                            window.location.reload();
                        } else {
                            alert("서버 오류!")
                        }
                    }
                })
            }

            function showArticles() {
                $.ajax({
                    type: "GET",
                    url: "/memo",
                    data: {},
                    success: function (response) {
                        let articles = response["articles"];
                        console.log(articles);
                        for (let i = 0; i < articles.length; i++) {
                            makeCard(articles[i]["image"], articles[i]["url"], articles[i]["title"], articles[i]["desc"], articles[i]["comment"]);
                        }
                    }
                })
            }

            function makeCard(image, url, title, desc, comment) {
                let tempHtml = `<div class="card">
                        <img class="card-img-top" src="${image}" alt="Card image cap">
                        <div class="card-body">
                        <a href="${url}" target="_blank" class="card-title">${title}</a>
                        <p class="card-text">${desc}</p>
                        <p class="card-text comment">${comment}</p>
                        </div>
                    </div>`;
                $("#cards-box").append(tempHtml);

            }
        </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 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 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 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 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 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 href="#" class="card-title">여기 기사 제목이 들어가죠</a>
                        <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
                        <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
                    </div>
                </div>
            </div>
        </div>
    </body>

</html>

 

app.py

from flask import Flask, render_template, jsonify, request
import requests
from bs4 import BeautifulSoup
from pymongo import MongoClient  # pymongo를 임포트 하기(패키지 인스톨 먼저 해야겠죠?)

app = Flask(__name__)

client = MongoClient('localhost', 27017)  # mongoDB는 27017 포트로 돌아갑니다.
db = client.dbsparta_ninework  # 'dbsparta'라는 이름의 db를 만들거나 사용합니다.


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


@app.route('/memo', methods=['POST'])
def post_article():
    # 1. 클라이언트로부터 데이터를 받기
    url_receive = request.form['url_give']  # 클라이언트로부터 url을 받는 부분
    comment_receive = request.form['comment_give']  # 클라이언트로부터 comment를 받는 부분

    # 2. meta tag를 스크래핑하기
    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 = soup.select_one('meta[property="og:image"]')
    og_title = soup.select_one('meta[property="og:title"]')
    og_description = soup.select_one('meta[property="og:description"]')

    url_title = og_title['content']
    url_description = og_description['content']
    url_image = og_image['content']

    article = {'url': url_receive, 'title': url_title, 'desc': url_description, 'image': url_image,
               'comment': comment_receive}

    # 3. mongoDB에 데이터를 넣기
    db.articles.insert_one(article)

    return jsonify({'result': 'success'})


@app.route('/memo', methods=['GET'])
def read_articles():
    # 1. mongoDB에서 _id 값을 제외한 모든 데이터 조회해오기 (Read)
    result = list(db.articles.find({}, {'_id': 0}))
    # 2. articles라는 키 값으로 article 정보 보내주기
    return jsonify({'result': 'success', 'articles': result})


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

 

 

참고.

card가 정렬되는 순서는 위에서 아래로 채워지고, 왼쪽부터 오른쪽으로 순서대로 채워집니다. 부트스트랩 컴퍼넌트 페이지에 적혀있어요. "*Cards are ordered from top to bottom and left to right.*" ([컴퍼넌트 페이지 링크](https://getbootstrap.com/docs/4.1/components/card/#card-columns))

 

 

 

 

숙제 - 원페이지 쇼핑몰에 api 가능 사용하기

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

이 숙제는 내가 직접 코드를 짠 것임.

 

 

 

static 디렉토리

decorate.css

/*site font (google)*/
* {
    font-family: 'Noto Serif KR', serif;
}

/*Entire form design*/
.wrap {
    margin: auto;
    width: 750px;
}

/*Every div design*/
.form_desgin {
    padding: 10px;
}

/*image design*/
.image_design {
    /*display: block, margin:audo 2개를 넣으니 가운데 정렬 됨*/
    display: block;
    margin: auto;
    width: 650px;
    background-size: cover;
    background-position: center;
}

/* 달러-원 환율 색깔 */
.emphasize {
    color: blue;
}

/* 주문하기 버튼 */
.center {
    display: block;
    margin: auto;
}

ipadpro.jpg 파일

 

 

 

templates 디렉토리

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>

    <!--Google Font-->
    <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR&display=swap" rel="stylesheet">

    <!--Custom CSS-->
    <link rel="stylesheet" type="text/css" href="../static/decorate.css"/>

    <!--Custom JavaScript-->
    <script>

        function order_complete() {

            // insert data in the select boxes
            let custName = $('#customerName').val();
            let prodCounter = $('#inputGroupSelect01 option:selected').val();
            let custAddr = $('#customerAddress').val();
            let custPhone = $('#customerPhone').val();

            if (custName == "") {
                alert("이름을 입력해 주세요.")
            } else if (prodCounter == "-- 수량을 선택하세요 --") {
                alert("수량을 선택해 주세요.")
            } else if (custAddr == "") {
                alert("주소를 입력해 주세요.")
            } else if (custPhone == "") {
                alert("전화번호를 입력해 주세요.")
            } else if (!checkPhone(custPhone)) {
                alert('잘못된 휴대폰 번호입니다. 010-XXXX-XXXX 형식을 사용해 주세요.')
            } else {
                // insert data to database
                $.ajax({
                    type: "POST",
                    url: "/request",
                    data: {ordername: custName, ordercount: prodCounter, orderaddr: custAddr, orderphone: custPhone},
                    success: function (response) {
                        if (response["result"] == "success") {
                            alert(response["msg"]);
                            window.location.reload();
                        } else {
                            alert("뭔가 문제가 있는 것 같습니다. 다시 시도해 주세요.")
                        }
                    }
                })
            };

            // clear select boxes
            $('#customerName').val('');
            $('#inputGroupSelect01').val('');
            $('#customerAddress').val('');
            $('#customerPhone').val('');
        };


        function checkPhone(number) {
            var regPhone = /010[-]\d{4}[-]\d{4}$/g;
            return regPhone.test(number);
        };

        // 주문하기 버튼 누르면, 값들 읽어와서 디비에 저장, 해당 사이트에들어가면, 주문내역이 db를 통해 로딩됨. 자동으로.

        function addTable(a,b,c,d) {

        }

        $(document).ready(function () {
            $('#dwrate').empty()
            $.ajax({
                type: "GET",
                url: "https://api.manana.kr/exchange/rate.json",
                data: {},
                success: function (response) {
                    let check = response[1]["rate"]
                    let addrate = `<h6>달러-원 환율 : ${check}</h6>`
                    $('#dwrate').append(addrate);
                }
            })
            $('#addOrders').empty()
            $.ajax({
                type: "GET",
                url: "/request",
                data: {},
                success: function (response) {
                    let data = response['orderdata']
                    for (let i = 0; i < data.length; i++) {
                        let custNameOrder = data[i]['name']
                        let custCountOrder = data[i]['count']
                        let custAddrOrder = data[i]['address']
                        let custPhoneOrder = data[i]['phone']
                        let addData = `<tr><td>${custNameOrder}</td><td>${custCountOrder}</td><td>${custAddrOrder}</td><td>${custPhoneOrder}</td></tr>`
                        $('#addOrders').append(addData);
                    }
                }
            })
        });
    </script>

    <!--Site title-->
    <title>애플 쇼핑몰</title>

</head>

<body>

<!--entire site attribute-->
<div class="wrap">

    <!--image-->
    <div class="form_desgin">
                <img src="../static/ipadpro.jpg" class="image_design"/>
    </div>

    <!--description-->
    <div class="form_desgin">
        <h1>iPad Pro (12.9) Gen1 128GB Cellular</h1>
        <h3>가격: 499,000원</h3>
        <h6>리퍼 아이패드 프로 1세대 12.9 입니다. 동영상 편집이나 프로그래밍에는 맞지 않지만, 넷플릭스 머신이나 인터넷 서핑용, 수업 필기용으로 적당합니다. 사은품으로 애플펜슬(1세대)를 함께 드립니다. </h6>
    </div>

    <!--order form-->
    <div class="form_desgin">

        <!--doller-won rate-->
        <div class="emphasize" id="dwrate">
        </div>

        <!--order form main information-->
        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <span class="input-group-text" id="inputGroup-sizing-default">주문자 성함: </span>
            </div>
            <input id="customerName" 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 class="custom-select" id="inputGroupSelect01">
                <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="customerAddress" 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="customerPhone" type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
        </div>

        <button onclick="order_complete()" type="button" class="center btn btn-outline-secondary">주문하기</button>

    </div>

    <!--order list-->
    <div class="form_desgin">
        <table class="table">
            <thead>
            <tr style="text-align: center">
                <th scope="col">주문자 성함</th>
                <th scope="col">수량</th>
                <th scope="col">주소</th>
                <th scope="col">전화번호</th>
            </tr>
            </thead>
            <tbody id="addOrders" style="text-align: center">
            </tbody>
        </table>
    </div>

</div>

</body>

</html>

 

루트 디렉토리

app.py

# 라이브러리 import
from flask import Flask, render_template, request, jsonify
from pymongo import MongoClient

## 데이터베이스 연결 정보
client = MongoClient('localhost', 27017)
db = client.sparta

app = Flask(__name__)


### 본문 시작 ##################################

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

@app.route('/request', methods=['POST'])
def insert_order_info():
    order_name = request.form['ordername']
    order_count = request.form['ordercount']
    order_addr = request.form['orderaddr']
    order_phone = request.form['orderphone']
    db.week4_homework.insert_one({'name': order_name, 'count': order_count, 'address': order_addr, 'phone': order_phone})
    return jsonify({'result': 'success', 'msg': '주문이 정상적으로 완료되었습니다.'})

@app.route('/request', methods=['GET'])
def read_order_info():
    order_data = list(db.week4_homework.find({}, {'_id': 0}))
    return jsonify({'result': 'success', 'orderdata': order_data})

# 본문 끝 ###################################

# 서버 관련 정보
if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

 

 

결과

'SCC 9기' 카테고리의 다른 글

[SCC 9기] - Project 개발 일지 : 프로젝트 기획안  (0) 2020.08.13
[SCC 9기] 5주차  (0) 2020.08.11
[SPC 9기] 3주차  (0) 2020.07.25
[SPC 9기] 2주차  (0) 2020.07.24
[SPC 9기] 1주차 - 프론트엔드  (0) 2020.07.16

+ Recent posts