본문 바로가기

Project

[Machine Learning & Data Engineering] 시집 데이터를 분류해 추천하는 서비스를 웹으로 구현해보기 - 1

프로젝트 개요

프로젝트 명 : 시를 잊은 그대에게 - 취향에 맞는 시집을 분류해 추천드립니다

프로젝트 기간 : 2022년 6월 22 ~ 2022년 6월 27일

사용 데이터 : 알라딘, DBPia에서 파싱한 데이터

사용 모델 : K-means Clustering

웹 : Flask, Google Data Studio


문제의식

이 프로젝트는 알라딘에 있는 시집 데이터를 활용해 시집을 분류하고, 분류된 데이터를 바탕으로 좋아하는 시집의 제목을 입력하면 같은 카테고리에 있는 시집을 추천하는 서비스를 제작하는 것을 목표로 시작하였습니다.

1. 영화는 있는데...!! : 잘 나가는 영화 추천 서비스

사실 어떤 컨텐츠를 추천하는 서비스는 AI/빅데이터 분야에서는 전혀 새로운 것이 아닙니다. 대표적인 것이 영화가 있죠. 넷플릭스나 디즈니플러스 등의 다양한 OTT 서비스들은 영화가 가진 다양한 데이터와 이용자의 특성 및 시청 기록 등을 종합하여 이용자가 선호할만한 영화를 추천해줍니다. 이런 OTT 시장의 경쟁은 얼마나 다양한 컨텐츠를 확보하느냐 만큼이나 바로 이렇게 즉각적으로 이용자가 선호할만한 영상을 추천해줄 수 있는가에 달려있다고 할 수 있습니다.

 

이렇게 영화 추천 알고리즘이 비교적 잘 발달한 것은 충분한 양의 데이터가 쌓여있기 때문일 것입니다. 사실 영화는 이렇게 수동적으로 추천받지 않아도 조금만 관심을 기울이면 많은 정보를 얻을 수 있습니다. 감독, 배우, 관람객과 전문가의 영화 평점, 각종 영화제 수상 영화, 상세한 리뷰... 그래서 비교적 세분화된 취향을 가지고 있어도 좋아하는 스타일의 영화를 금방 찾을 수 있죠. 가령, 예술 영화를 좋아한다면 각종 독립 영화제 수상작이나 전문가 평점을 다른 특성에 비해 더 상세히 참조할 수도 있을 것입니다.

 

저의 문제의식은 바로 이러한 영화 추천 서비스를 접하고 다른 분야로 시선을 돌렸을 떄 비로소 시작됩니다. 인터넷 서점에서 책을 구매하신 적이 있으신가요? 그렇다면 구매하실 때, 온라인 서점에서 추천하는 책을 구매하는 경우는 얼마나 되나요? 저같은 경우는 거의 없었던 것 같습니다. 보통 발품을 팔아서 필요한 책을 찾거나 주변에서 추천해주는 책을 구매하는 경우가 대부분이죠. 

 

이런 상황은 시집을 구매할 때 더욱 심각해집니다. 비교적 전문화되어 있고, 소수의 독자층이 찾는 장르인 시는 리뷰나 평점도 적은데다가, 알라딘이나 교보문고에서는 보통 '한국시'라는 커다란 카테고리만 있을 뿐 세분화된 시대/장르 구분을 제공하지도 않습니다.

 

이렇기 때문에 (물론 다른 많은 이유들도 있겠지만) 진입 장벽이 높아져서,  막상 시에 흥미를 갖게 된 사람이 본격적으로 시에 입문하려고 했을 때에도 무엇을 읽어야할지 몰라 결국 시에 대한 관심을 거두는 상황이 많이 발생할 거라 추측할 수 있습니다.

2. 한국 출판시장과 시집 출간

취향에 맞는 적절한 시를 추천해주는 것의 장점은 단순히 사람들의 취미 생활의 가짓수를 하나 더 늘리는 데에만 국한되지 않습니다. 2021년 문예연감의 자료에 따르면, 단행본 기준으로 시집의 출판 비중은 전체의 약 1/4 이상을 차지하고 있으며, 비수도권을 기준으로 본다면 비수도권 단행본 출판의 과반 이상을 시집이 차지하고 있는 것으로 나타납니다. 

 

그런데 당장 구글에 '시집 출판'이라고 검색해보면, 시집을 출판하는 것은 이익이 되지 않은, 지속되기 힘든 사업이라는 내용의 기사를 쉽사리 찾아볼 수 있을 겁니다. 즉, 시집은 한국 단행본 출판에서 꽤나 큰 비중을 차지하는 주변부라 할 수 있습니다. 따라서 조금이라도 시에 대한 진입장벽을 완화하고 친숙하게 만들면 출판 시장의 많은 부분(특히 비수도권 출판시장의 많은 부분)에 조금이나마 기여를 할 수 있지 않을까요?


데이터 가져오기

이제 시집을 분류하는 모델을 구성하기 위해 알라딘에서 시집과 관련된 데이터를 가져오는 작업을 진행하였습니다. 그리고 이렇게 얻은 데이터를 바탕으로 영화의 전문가 평점같은 역할을 도입하기 위해 학술지/문학 발행물 제공 서비스인 DBPia에서 추가적으로 데이터를 가져왔습니다.  사실 영화처럼 비교적 잘 작동하는 시집 추천 서비스를 제공하기 위해서는 구매나 리뷰 데이터가 필요하지만(알라딘 연락 주십쇼...!!!!), 현실적으로 구하기 어려워 일단 분류한 후 카테고리별 추천 시집을 제시하는 정도의 서비스를 구현하는 것으로 만족하였습니다.

1. 알라딘 데이터 가져오기

알라딘에서 '한국시'로 카테고리를 설정하면 42,681개의 책이 검색됩니다. 이 책들의 제목과 이후 작업을 위한 책의 알라딘 id를 가져오는 작업을 수행하였습니다. 출간일을 기준으로 가져오기 위해, 조회 기준을 출간순으로 하고, 50개씩 보기로 설정하여 200페이지까지의 정보를 가져왔습니다. (10,000 개의 샘플)

 

BeautifulSoup을 이용해서 데이터를 가져와서 SQLite db에 저장하는 작업입니다. 먼저, `sqlite3` 라이브러리를 가져온 후, 'poems.db'라는 이름의 데이터베이스와 연결시켜줍니다. (해당하는 이름의 데이터베이스가 없는 경우, 해당 이름으로 데이터베이스가 생성됩니다.) 이후 객체를 생성한 후, 'execute'를 실행하여 'Book_Title', 'Book_id' 두 개의 칼럼을 가진 데이터 테이블을 생성합니다.

 

import sqlite3

# sqlite3 연결
conn = sqlite3.connect('poems.db', isolation_level=None)

cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS Book1_table")
cur.execute("""CREATE TABLE Book1_table (
    id INTEGER NOT NULL PRIMARY KEY,
    Book_Title TEXT,
    BOOK_id INTEGER
    )
""")

이제 이렇게 생성한 데이터 테이블에 알라딘에서 가져온 데이터를 저장합니다.

import requests
from bs4 import BeautifulSoup

URL_Aladin = 'https://www.aladin.co.kr/'

# 50개씩 보기로 하면 약 850여개의 페이지가 나오는데, 그 중 발매일 순으로 내림차순하여 200개만 사용 (10,000개)
for num in range(200):
    page_num = num + 1
    URL_List = URL_Aladin + f'shop/wbrowse.aspx?BrowseTarget=List&ViewRowsCount=50&ViewType=Detail&PublishMonth=0&SortOrder=5&page={page_num}&Stockstatus=1&PublishDay=84&CID=51167&SearchOption='

    poem_title = requests.get(URL_List)
    poem_title.raise_for_status()
    title_soup = BeautifulSoup(poem_title.content, 'html.parser')
    
    # 0~49까지 모두 가져온다.
    for i in range(50):
        
        # 책 제목
        titles = title_soup.select('li > a > b')[i].text
        
        # 알라딘 url에 부여된 책 id
        bookcode = int(title_soup.find_all(class_='bo3')[i]['href'].split('=')[-1])
        
        insert_data = "INSERT INTO Book1_table(Book_Title, BOOK_id) VALUES(?, ?)"
        cur.execute(insert_data, [titles, bookcode])
     
conn.close()

이렇게 얻은 알라딘 id 리스트를 활용하여 해당하는 id의 저자, 출판사, 출판일, 가격, 알라딘에서 제공하는 salespoint, 평점, 100자평 수, 리뷰 수, 페이지 수, 무게에 해당하는 값들을 별도의 테이블에 저장하였습니다. 

2. DBPia 데이터 가져오기

국내 학술정보 포털 DBPia는 국내의 다양한 학술지/간행물을 제공합니다. 알라딘에서 가져온 저자명을 DBPia로 설정해 입력하면 나오는 검색 결과를 가져오는 작업을 수행하였습니다. 

 

이렇게 학술정보포털에서 데이터를 가져온 이유는 영화의 전문가 평점과 비슷한 역할을 할 수 있을만한 특성을 도입하기 위해서입니다. 인용 연구에서는 인용 수와 인용 경로 등을 중요한 변수로 고려하고 있으며, 예술사회학 연구들은 평론가가 어떤 예술가를 '언급'한다는 건 그 언급의 성격(우호/적대)와 상관없이 그 예술가의 존재를 인정하는 것을 의미한다는 것을 보여주었습니다. (가령, SNS 시로 베스트셀러 작가가 된 하상욱 시인은 문학 평론가의 글에서 잘 다뤄지지 않습니다. 오히려 이런 작가들은 사회 트랜드로서 사회과학자의 연구 대상으로 다뤄지곤 합니다.)

 

이러한 특성을 고려하여, 작가를 대상으로 한 '인문학' 논문의 개수만을 파악하여 데이터베이스에 가져오기로 결정하였습니다. (물론, 이러한 결정에는 많은 결점이 있습니다. 대표적인 문제로는 동명이인을 구분하지 못하거나, 이름과 단어를 구분하지 못하는 것이 있습니다. 가령, '이상'을 검색하면 '이상x'이라는 이름이나 '~~ 측정의 이상치'와 같은 검색 결과도 포함되어 나타납니다. 여기서는 일단 이런 문제들을 무시하고, 일종의 프로포절처럼 프로토타입을 제작하는 것을 목표로 하겠습니다.)

 

검색어를 입력하고 '인문학' 카테고리를 설정하는 작업이 필요하기 때문에 해당 데이터는 selenium을 통한 독적 스크레이핑으로 진행하였습니다. 

from selenium import webdriver
import time
import requests

URL_DBPia = 'https://www.dbpia.co.kr/'
browser = webdriver.Chrome()
browser.get(URL_DBPia)

for author in author_list['Author']: # 알라딘에서 가져온 Author리스트에 대해
    time.sleep(2) 

	# 검색창을 클릭 후 입력된 글자가 있으면 지우고(clear), 작가 이름을 입력(send_keys)
    elem = browser.find_element_by_id('keyword')
    elem.clear()
    elem.click()
    elem.send_keys(author)
        
    # 논문/기사명에만 이름이 포함될 수 있게 설정 (작가가 저자로 발표한 글 제외)
    elem = browser.find_element_by_id('dev_srchOpt')
    elem.click()
    elem = browser.find_element_by_xpath('//*[@id="dev_srchOpt"]/option[2]')
    elem.click()
    elem = browser.find_element_by_class_name('btnSearch')
    elem.click()
        
    # 검색 로딩 시간을 기다리기 위해 1초간 sleep
    time.sleep(1)
        
    elem = browser.find_element_by_xpath('//*[@id="dev_category"]/li[1]/span/label')
    if elem.is_selected() == False:
        elem.click()

    time.sleep(1)
        
    elem = browser.find_element_by_xpath('//*[@id="contents"]/div[2]/div[2]/div[3]/div[1]/div[2]/div[2]/div[2]')
    elem.click()
        
    # 검색 결과 개수
    html = browser.page_source
    dbpia_soup = BeautifulSoup(html, 'html.parser')
        
    try:
        search_result = int(re.sub(r'[^0-9]', '', dbpia_soup.find(class_='searchCount').text))
    except:
        search_result = None
    
    insert_date3 = "INSERT INTO Dbpia_table(DBPia_rev) VALUES(?)"
    cur.execute(insert_date3, [search_result])
    
browser.quit()