서비스 기술 검토 및 데모 (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에 바로 적재하는 코드를 작성하여 자동으로 수집되도록 설정하려고 한다.