매우 당황스러운 일이 생겼다. 몇 안되는 스크래핑 내용 (제목, 링크, 날짜) 중에서 날짜가 문제가 생겼다. 각 사이트가 날짜 기록 방식이 다르다..... 각 사이트는 아래와 같은 형식으로 날짜값을 가져온다. 이게 웃기는게, 현재 날짜 시점으로 오늘이면 시간을 보여주고, 오늘이 아니면 날짜를 보여준다. 이렇게 값이 바뀌니까 더 애매해졌다.
아무튼, 날짜까지 빼버릴 순 없기에, 어떻게든 해볼라고 정말 고군분투했다. 뭔가 아는게 있어야 시작이라도 할텐데... 다행히 어디서 주워들은 정규화부터 차근차근 알아보며 공부하기 시작했다. 다행히 허접하지만 어떻게든 작동하는 알고리즘을 만들 수 있었다.
# 당일이면 시간, 당일이 아니면 날짜로 가져온다. 그런데 날짜나 시간 값이 제각각이다.
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 섹션에 날짜와 시간이 섞여 있으니까 정렬도 제대로 안 되는 것이다. 역시 개발은 쉬운게 아니다. 뭘 해결하면 또 뭐가 나오고 또 뭘 해결하면 또 뭐가 나온다..
이것 때문에 또 여러가지를 찾아보았지만 뾰족한 수가 없어, 정렬할 때 특수기호는 우선순위가 숫자와 다르다는 특성을 이용해 그냥 날짜쪽에는 무조건 특수기호 +를 넣기로 했다. 그렇게 하니 일단 정렬은 잘 되었다. 근데 이것은 미봉책이고 좀 더 찾아봐야 할 것 같다.
아무튼, 겨우겨우 만들고 잘 작동하는걸 보니, 이게 또 느낌이 신기하다. 이맛에 개발자 하는건가?
스파르타 코딩클럽에서 코딩 공부를 시작하고 나서, 이론을 배울 때는 잘 이해하고 기록해두면서 뭔가 항상 "금방 쉽게 하겠지~" 라는 생각을 가지고 있었는데, 막상 시작을 하면 너무 기초적인 것 부터 "아 이거 여긴 어떻게 하지?" 라는 생각이 든다. 내 전체적인 프로젝트를 생각하면 모르는게 너무 많을 것 같아 두려움이 앞선다.
시간은 한정적이고 할 일은 많다. 회사에서 업무를 위해 오픈소스와 리눅스 공부는 계속 해야하고, 자녀들과도 시간을 보내고 아내를 도와주기도 해야 하고, 놀아야 하고, 다른 공부도 해야하고, 영어도 해야하고... 너무나 할일이 많다. 이번 스파르타 코딩 프로젝트를 진행하면서, 모든 우선순위를 제쳐두고 집중하면서도 잘 되지 않으면 어쩌나 라는 걱정도 많이 했다.
그래도 내가 해야 할 모든 일들을 생각하는 것 보다, 눈앞에 해야 할 일만 생각하면 조금 더 마음이 편해질 수 있었고, 내가 가장 먼저 해야하는 것부터 조금씩 해나간다면 그래도 뭔가 진행이 된다는 사실에 또 마음이 편해졌다.
내가 해야 할 프로젝트 중 가장 먼저 완료해야 하는것은 스크래핑을 통해 원하는 데이터를 가져오는 것이다. 이게 되지 않는다면 프로젝트 자체를 진행할 수 없다.
대부분의 사이트들은 가장 기초적인 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개 이상 나왔다. 매일매일 간단하게 특가 정보를 확인하려는게 목적인데 아무래도 너무 양이 많은 것 같아서 결국 카카오톡 톡딜은 탈락했다.
그래도 양을 줄여서 추가해볼까 했는데, 일단 최종 결과물이 나오고 나서 다시한번 확인해볼 예정이다. 만약 카테고리별로 글을 나눌 수 있으면 게시글 양이 많아도 좋을텐데, 카테고리를 나누는 것은 딥러닝 정도는 쓸 수 있어야.. 할 수 있을 것 같다. 일단은 보류.
대한민국의 각 커뮤니티에는 여러 게시판이 있고, 그 중 특가 게시판이 있는 경우가 많다. 여기에는 한정된 시간동안 잠깐 진행하는 딜이나 오프라인 구매처의 특별 세일 등이 올라온다. 이 특가 제품들은 일반적인 구매처에서 (네이버 쇼핑 등) 사는 금액보다 조금 또는 훨씬 저렴한 경우가 많다. 그런데 커뮤니티가 매우 많으므로 대부분 특정 커뮤니티에서 한정된 정보를 얻게 된다.
아니면 주기적으로 여러 사이트들을 돌아다니며 특가정보를 확인할 수는 있지만 번거로우며, 시간도 일정 부분 소요된다. 따라서 이러한 특가정보를 여러 커뮤니티에서 돌아다니지 않고 하나의 사이트에서 한번에 볼 수 있도록 하여 효과적으로 특가정보를 얻을 수 있다.
3. 프로젝트 생김새
4. 개발해야 할 기본기능
(1) 스크래핑
- 커뮤니티 별 특가 사이트들의 게시글들을 스크래핑하며, DB(mongodb)에 삽입 ()
- 스크래핑 항목 : 제목, 가격, 링크, 시간, 이미지
- 스크래핑 단위 : 글 리젠률이 높지 않으므로, 각 사이트당 1~2페이지를 스크래핑
- 스크래핑 주기 : 매 1시간, 새벽 1시부터 오전7시까지는 스크래핑을 수행하지 않음.
(2) 데이터베이스 작업
- 중복 제거 : 글 리젠률에 따라 이전 정보가 스크래핑 될 수 있으므로, "링크" 단위로 중복을 제거한다.
(3) API 제작
- 스크래핑한 데이터를 DB에 저장하는 API (매 1시간 반복)
- DB에 저장된 데이터를 사이트로 출력하는 API (매 1시간 반복)
- 데이터 출력은 [제목, 링크, 사이트명, 가격, 시간] 으로 출력한다. 일반 게시판의 형태가 된다.
5. 추가기능
- 기본 검색 : 제목들 중 키워드로 검색 (DB에서 해당 조건으로 검색하여 다시 데이터를 뿌려주는 형태)
- 조건 검색 : 가격별(최저가별 등), 날짜별(오늘, 최근3일 등)
- 원하는 검색 키워드 알림 : 회원 가입 기능 및 카톡 알림 기능 (또는 메일)
- 게시물의 클릭 수 기록
- 모바일/데스크탑 모드
6. 프로젝트 완료 후 추가 기능
- 다른 특가 사이트 추가
- 각 특가 사이트에서 구분한 카테고리를 기반으로 각 게시물의 카테고리 정보를 삽입, 카테고리 검색기능 추가
- 종료된 딜 표시 :해당 사이트가 연결이 안되거나, 가격이 바뀌는 등이 확인되면, 종료되었다고 표시
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에 넣는다. 그리고 위와 같이 경로를 지정해주면 되며, 형식은 원래 이렇게 하는거니까 그대로 한다.
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라는 값을 명시하면, 그 값에 대해 뭔가를 하면 특정 키값을 구할 수 있는 것.