서비스 기술 검토 및 데모 (1)
2024. 5. 2. 00:27ㆍ카테고리 없음
- 목표:
- 위치 기반 서비스 개발을 위한 기술 개발 가능성을 확인
- 실제 서비스 개발에 필요한 범위와 자원을 산정하기 위함
- 목표가 아닌 항목:
- 티스토리에 아이디어 및 개발 히스토리 게시하기
- 깃허브에 개발 소스 푸시
- 다음 단계:
- Spatial DB 구축하기
- Hadoop을 이용한 ETL 파이프라인 구축하기
메모
- 여행/데이터 코스를 추천하기 위한 서비스이므로 데모를 어떻게 구현할 것인 지에 대한 아이디어
- 중요한 것은 입력을 받았을 때 어느 정도의 추천이 이루어져야 한다는 것
- 추천 시스템 구축은 시간이 오래 걸리고, 하드 코딩 하더라도 모든 지역에 대해 커버를 하지 못함
- 제한된 지역에 대한 DB를 구축하여, 해당 지역의 상가 정보, 대중교통 정보 등 데이터 수집
- 코스를 추천하는 알고리즘과 식사 시간, 이동 시간, 휴식 시간 등 모든 것을 고려할 수 있는 알고리즘이 필요함
- 위치 기반 서비스를 위하여 어떤 기술이 필요한지 리스트업 필요
데모 수행 지역 고정
- 서울시 노원구 공릉동을 기준으로 진행
- 실제 연고지를 활용하여 조금 더 익숙한 환경과 기술 검증 설정
기술검증
- 먼저 설정 지역의 상가 정보와 교통 정보 등 필요한 정보를 수집하여 데이터베이스에 저장
- Spatial DB 구축은 나중의 일이므로, MongoDB를 사용하여 데이터 저장을 목표한다.
MongoDB 구축
- 내 개인 서버 내 mongodb를 컨테이너로 띄워놓기로 한다.
- 개발 PC에서는 데이터 자동 수집 등이 중간에 끊어질 수 있으므로 항상 전원이 켜져 있는 환경에서 진행한다.
version: '3.8'
services:
mongodb:
image: mongo:7
container_name: mongodb
volumes:
- /home/jasonhan/project/:/data/db
ports:
- "27017:27017"
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: jason
MONGO_INITDB_ROOT_PASSWORD: **********
-
docker-compose up -d 를 입력하여 컨테이너 실행
- 데이터가 쌓이는 것을 조금 더 명확하게 확인하기 위해 mongodb compass 도 설치하였다.
- python에서 데이터를 저장하고 조회하기 위하여, mongodb 연결 테스트를 수행
from pymongo import MongoClient
import json
with open('test_credential/mongo_info.json') as file:
data = json.load(file)
HOST = data['server_ip']
# MongoDB 포트 번호
PORT = data['server_port']
# 사용자 이름
USERNAME = data['user_id']
# 비밀번호
PASSWORD = data['user_pw']
# 데이터베이스 이름
DATABASE = 'admin'
# MongoDB 연결 문자열
uri = f"mongodb://{USERNAME}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
# MongoDB 클라이언트 생성
client = MongoClient(uri)
selected_db = 'place_info'
# 데이터베이스 선택
db = client[selected_db]
# 간단한 데이터 조회 예제
collection = db['shop']
for document in collection.find():
print(document)
# 클라이언트 연결 종료
client.close()
- 위 코드의 출력으로 아래 데이터가 조회 되었다.
{'_id': ObjectId('663245f2bdb2abe6e14a63eb'), 'test': 'test_query'}
- 이제 필요한 데이터만 수집하면 구축한 mongoDB에 적재할 수 있게 되었다!
필요한 데이터 수집
- api를 통해 수집이 가능하지만, api 키를 신청해야 하므로 추후에 신청하여 api를 통해 수집 가능하도록 할 것이다.
# -*- coding: utf-8 -*-
#==============================================================================================================
## 서울 열린 데이터 광장 데이터 검색 및 엑셀 파일 다운로드
#### API를 통해 데이터를 받을 수 있지만, 데이터 제공처마다 api key를 매번 발급받아 변수로 지정해야줘야 할 수 있어,
#### 안정적인 데이터 수급을 위하여 웹크롤링을 통한 엑셀파일 다운로드 시도하는 메소드
#==============================================================================================================
import os, time, logging
from bs4 import BeautifulSoup
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import basic_method as bm
import undetected_chromedriver as uc
from webdriver_manager.chrome import ChromeDriverManager
import requests
from fake_useragent import UserAgent
def csvFile_reader(filename, data_id):
''' csvFile_download 함수는 requests.get을 사용하며
서울시 공공데이터의 CSV 파일을 다운로드 받기 위한 메소드
filename : 다운로드 받을 경로 및 파일명, data_id : 게시물 id 값
'''
with open(filename, "wb") as file:
# ua = UserAgent(use_cache_server=True)
ua = UserAgent()
headers = {"User-Agent": ua.random}
response = requests.get(f"<https://data.seoul.go.kr/dataList/dataView.do?onepagerow=1000&srvType=S&infId={data_id}&serviceKind=1&pageNo=1&ssUserId=SAMPLE_VIEW&strWhere=&strOrderby=&filterCol=%ED%95%84%ED%84%B0%EC%84%A0%ED%83%9D&txtFilter=>", headers=headers)
print(response.text)
print("="*100)
if __name__ == "__main__":
open_search_terms1 = "인허가 정보"
open_search_terms2 = ""
base_path = "C:/Users/Jason/Downloads/output"
open_csv_path = f"{base_path}/csv"
log_path = f"{base_path}/log"
start_url = "<http://data.seoul.go.kr/dataList/datasetList.do>" # 서울 열린 데이터 광장 URL
os.makedirs(open_csv_path, exist_ok=True) # 데이터 저장경로 디렉토리 생성
os.makedirs(log_path, exist_ok=True) # 로그 저장경로 디렉토리 생성
log_date = bm.get_datetime() # 로그 파일명에 사용할 날짜 생성 메소드
logging.basicConfig(level=logging.INFO) # 로그 기본 레벨 설정
logger, stream_handler, file_handler = bm.logger_get(log_path, open_search_terms1, log_date) # 로그 메소드 및 변수 선언
start = time.time() # 실행 시작 시간 선언
options = uc.ChromeOptions()
options.add_argument('--headless')
driver_path = ChromeDriverManager(driver_version='124.0.6367.119').install()
print(driver_path)
driver = uc.Chrome(executable_path=driver_path, version_main=124, enable_cdp_events=True, options=options)
driver.get(start_url) # 설정한 URL로 웹드라이버 실행
driver.implicitly_wait(10) # 실행 후 페이지 로딩까지 10초 이하 딜레이
searchBox_element = driver.find_element(By.NAME, 'searchKeyword') # 검색어 입력 창 element 선언
searchBox_element.send_keys(open_search_terms1) # 검색어 입력
searchBox_element.find_element(By.XPATH, '/html/body/div[3]/section/form/div[1]/div/button').click() # 검색 버튼 클릭
current_pageNum = 1 # 시작 페이지 인덱스 번호 선언
html = driver.page_source # 현재 페이지 파싱을 위한 페이지 소스 변수 선언
soup = BeautifulSoup(html, 'html.parser') # bs4를 사용하여 html parser 사용하여 html 데이터 변수 선언
items = soup.find_all('div', {'class': 'search-count-text'})[0].find('strong').text.replace(',', '') # 검색 조회 개수 데이터있는 element 선언
end_pageNum = round(int(items)/10)+1 # 마지막 페이지 숫자 인덱스 선언
logger.info(f'{open_search_terms1} Search Results - Total Data Count : {items}, Total Pages: {end_pageNum}')
btn_maxIndex = 14 # next page 버튼 elements 최대 개수
btn_nthIndex = 3 # Pagination을 위한 next page 버튼 elements index 초기화
while current_pageNum <= end_pageNum: # 마지막 페이지와 같아질 때까지 pageNum 증가하며 반복하는 while문 시작
logger.info(f"{'='*10} Current Page Number : {current_pageNum} {'='*10} ")
html = driver.page_source # 현재 페이지 파싱을 위한 페이지 소스 변수 선언
soup = BeautifulSoup(html, 'html.parser') # bs4를 사용하여 html parser 사용하여 html 데이터 변수 선언
target = soup.find_all('a', class_='goView') # 데이터 타이틀 파싱을 위한 class elements 변수 선언
for tar in target:
title = tar.text.replace(" ", "_").replace("\\n","").replace("\\n","") # 데이터 타이틀 문자열 선언
filename = f'{open_csv_path}/{title}.csv' # 저장할 데이터(엑셀) 경로 및 파일명 선언
data_id = tar['data-rel'].split('/')[0] # 데이터 다운로드를 위한 게시물 id 값 선언
if open_search_terms1.replace(" ", "_") in title and open_search_terms2 in title:
# bm.csvFile_download(filename , data_id) # basic_method의 csvFile_download 메소드 사용하여 엑셀파일 다운로드
csvFile_reader(filename, data_id)
break
logger.info(f"Data Successfully Download - FileName : {title}")
else: # 타이틀에 검색어 미포함의 경우 건너뜀
logger.info(f"Data Doesn't Contained Essential Words, Skip Download - FileName : {title}")
if btn_nthIndex == 12: # Pagination 마지막 버튼일 경우 다음버튼 페이지 누르기
WebDriverWait(driver,2).until(EC.presence_of_element_located((By.CSS_SELECTOR,'#datasetVO > div.wrap-a > div > section > div.list-statistics > div > div > button.paging-next'))).click()
btn_nthIndex = 4 # 다시 첫번째 인덱스의 버튼 클릭을 위한 btn_nthIndex 초기화
else:
btn_nthIndex += 1 # Pagination 다음 숫자 버튼 누르도록 인덱스 증가
try:
WebDriverWait(driver,10).until(EC.presence_of_element_located((By.CSS_SELECTOR,f'#datasetVO > div.wrap-a > div > section > div.list-statistics > div > div > button:nth-child({btn_nthIndex})'))).click()
except:
# print(driver.get_url())
continue
current_pageNum += 1 # next button 누른 뒤 페이지 숫자 증가
end = time.time() # 종료 시간 선언
running_time = round((end-start)/60, 2) # 총 실행 시간 계산
logger.info(f"[End Run] Running Time: During {running_time} Minutes Running") # 총 실행 시간 프린트
logger.removeHandler(file_handler) # 로그 파일 핸들러 종료
logger.removeHandler(stream_handler) # 로그 스트림 핸들러 종료
logging.shutdown() # 로깅 종료
- 과거에 작성했던 코드로 현재는 열린데이터 광장의 구조가 바뀌어 정상적으로 동작하지 않는다.
- 이전에는 위 기재된 “url” 로 요청 시 자동으로 csv 파일이 다운로드 받아지는 구조였지만, 해당 URL로 요청해도 바로 다운로드 되지 않고 json형태의 response만 조회된다.
{
"result": "ok",
"page": {
"pageNo": 1,
"pageCount": 1303,
"totalCount": 130267,
"listCount": 130267
},
"list": [
{
"LASTMODTS": "2023-05-28 04:15:09",
"DTLSTATENM": "폐업",
"TOTEPNUM": "null",
"WMEIPCNT": "null",
"BPLCNM": "(주)커피지아",
"MANEIPCNT": "null",
"ISREAM": "null",
"JTUPSOASGNNO": "null",
"FACILTOTSCP": "null",
"JTUPSOMAINEDF": "null",
"MULTUSNUPSOYN": "null",
"CLGENDDT": "",
"SITEAREA": "26.00",
"DCBYMD": "2023-05-27",
"CLGSTDT": "",
"TRDSTATEGBN": "03",
"TRDSTATENM": "폐업",
"APVCANCELYMD": "",
"SITEPOSTNO": "137-787",
"FCTYSILJOBEPCNT": "null",
"OPNSFTEAMCODE": "3210000",
"SITETEL": "",
"FCTYPDTJOBEPCNT": "null",
"SITEWHLADDR": "서울특별시 서초구 양재동 232 AT센터 제1전시장",
"DTLSTATEGBN": "02",
"RDNPOSTNO": "06774",
"BDNGOWNSENM": "null",
"RONUM": "1",
"TRDPJUBNSENM": "null",
"HOMEPAGE": "null",
"MONAM": "null",
"FCTYOWKEPCNT": "null",
"UPDATEGBN": "U",
"UPDATEDT": "2022-12-04 21:01:00",
"APVPERMYMD": "2023-05-19",
"WTRSPLYFACILSENM": "null",
"LVSENM": "null",
"UPTAENM": "커피숍",
"HOFFEPCNT": "null",
"RDNWHLADDR": "서울특별시 서초구 강남대로 27, AT센터 제1전시장 (양재동)",
"SNTUPTAENM": "null",
"Y": "440676.379919661",
"X": "203392.793460583",
"MGTNO": "3210000-104-2023-00148",
"ROPNYMD": ""
},
{
"LASTMODTS": "2023-05-29 04:15:08",
"DTLSTATENM": "폐업",
"TOTEPNUM": "null",
"WMEIPCNT": "null",
"BPLCNM": "솔아띠몽",
"MANEIPCNT": "null",
"ISREAM": "null",
"JTUPSOASGNNO": "null",
"FACILTOTSCP": "null",
"JTUPSOMAINEDF": "null",
"MULTUSNUPSOYN": "null",
"CLGENDDT": "",
"SITEAREA": "3.00",
"DCBYMD": "2023-05-28",
"CLGSTDT": "",
"TRDSTATEGBN": "03",
"TRDSTATENM": "폐업",
"APVCANCELYMD": "",
"SITEPOSTNO": "130-851",
"FCTYSILJOBEPCNT": "null",
"OPNSFTEAMCODE": "3050000",
"SITETEL": "",
"FCTYPDTJOBEPCNT": "null",
"SITEWHLADDR": "서울특별시 동대문구 전농동 591-53 청량리역, 롯데백화점",
"DTLSTATEGBN": "02",
"RDNPOSTNO": "02555",
"BDNGOWNSENM": "null",
"RONUM": "2",
"TRDPJUBNSENM": "null",
"HOMEPAGE": "null",
"MONAM": "null",
"FCTYOWKEPCNT": "null",
"UPDATEGBN": "U",
"UPDATEDT": "2022-12-04 21:01:00",
"APVPERMYMD": "2023-04-24",
"WTRSPLYFACILSENM": "null",
"LVSENM": "null",
"UPTAENM": "기타 휴게음식점",
"HOFFEPCNT": "null",
"RDNWHLADDR": "서울특별시 동대문구 왕산로 214, 청량리역, 롯데백화점 1층 (전농동)",
"SNTUPTAENM": "null",
"Y": "453187.395154017",
"X": "204081.282117393",
....
}
]
}
- 위와 같은 형태의 json을 pagenation해서 조회해야 할 것으로 생각된다.
다음에는 정상적으로 데이터를 수집하여 MongoDB에 바로 적재하는 코드를 작성하여 자동으로 수집되도록 설정하려고 한다.