본문 바로가기
프로그래밍

애니메이션 검색 웹 애플리케이션 2

by 기이한날개 2022. 3. 15.

이전 글 보기

Streamlit으로 검색 결과 표시

앞서 필요한 정보를 모두 df.csv라는 파일에 저장을 했고, 이미지 파일도 laftel_thumbnail이라는 폴더 안에 인덱스 이름으로 저장을 해주었다. 

 

다음으로 streamlit에서 검색할 조건을 입력값으로 받은 후에 조건에 따라 검색을 하고, 그 결과값을 streamlit 상에서 보여주는 코드를 짰다. 결과값은 모든 정보를 보여주기는 힘들어서 번호, 이미지, 제목, 매체 순으로 보여주기로 했다. 

 

import streamlit as st
from PIL import Image
import pandas as pd
import numpy as np

st.set_page_config(layout="wide")

# 데이터 불러오기
@st.cache
def get_data():
    path = 'df.csv'
    df = pd.read_csv(path)

    # make df['genres'] to list
    def make_genres_to_list(n):
        if type(n) is list:
            return n
        if n is np.nan or n == '':
            return []
        if n.startswith('[') is True:
            n = n.strip('][').split(', ')
            n = [w.replace("'", "") for w in n]
            return n
    df['genres'] = df['genres'].apply(lambda x: make_genres_to_list(x))
    res = df.copy()
    return res
df = get_data()

# multiselect option 만들기
def make_multiselect_option(df, text):
    dic = dict()
    for n in df[text]:
        if n is np.nan or n == '':
            continue
        if type(n) is list:
            for i in n:
                dic[i] = dic.get(i, 0) + 1
        else:
            dic[n] = dic.get(n, 0) + 1
    res = list(dic.keys())
    if '' in res:
        res.remove('')
    return res

# 작품 수 5개 이상 제작사 리스트 생성
def make_production_select(df, text):
    dic = dict()
    for n in df[text]:
        if n is np.nan or n == '':
            continue
        if type(n) is list:
            for i in n:
                dic[i] = dic.get(i, 0) + 1
        else:
            dic[n] = dic.get(n, 0) + 1

    over_5_dict = dict(filter(lambda elem:elem[1]>=5, dic.items()))
    sorted_keys = sorted(over_5_dict, key=over_5_dict.get, reverse=True)
    return sorted_keys

# 검색
@st.cache
def search_result(df, search_text, search_genre, search_medium, search_onair, search_production, search_all):
    # df: and로 결합한 series 하나로 filter
    mask0 = df['name'].str.contains(search_text)
    mask1 = df.genres.map(set(search_genre).issubset)
    mask2 = df.medium.map(lambda x: not search_medium or x in search_medium)
    mask3 = df.onair_year.str.contains("|".join(search_onair))
    mask4 = df.production.map(lambda x: not search_production or x in search_production)
    mask5 = df['content'].str.contains(search_all) | df['name'].str.contains(search_all)
    
    df_filtered = df[mask0 & mask1 & mask2 & mask3 & mask4 & mask5]
    df_filtered2 = df_filtered.sort_values(by=['view_male', 'id', 'avg_rating'], ascending=False)
    df_filtered3 = df_filtered2.reset_index()
    return df_filtered3

def show_result(df_filtered, show_per_page=200, page=1):
    df_filtered = df_filtered.iloc[:show_per_page]
    if df_filtered is None or len(df_filtered)<1:
        st.write('검색 조건에 맞는 작품이 없어요')
    else:
        for index, row in df_filtered.iterrows():
            no = row['no']
            name = row['name']
            medium = row['medium']

            col_num = 5
            col_idx = index%col_num
            if col_idx==0:
                cols = st.columns(col_num)
            with cols[col_idx]:
                st.write(index+1)
                try:
                    img = Image.open(f'laftel_thumbnail/{no}.jpg')
                except Exception as e:
                    print(e)
                    img = Image.open(f'laftel_thumbnail/noimage.jpg')
                
                # 이미지 사이즈를 240, 330으로 통일
                ratio = (240, 330)
                def crop_resize(image, ratio):
                    width = ratio[0]
                    height = ratio[1]
                    # crop to ratio, center
                    w, h = image.size
                    if w > (width / height) * h: # width is larger then necessary -> resize height to 300
                        hpercent = (height/float(image.size[1]))
                        wsize = int((float(image.size[0])*float(hpercent)))
                        image = image.resize((wsize,height), Image.ANTIALIAS)
                        w, h = image.size
                        x, y = (w - (width / height) * h) // 2, 0
                    else: # height is larger than necessary -> resize width to 216
                        wpercent = (width/float(image.size[0]))
                        hsize = int((float(image.size[1])*float(wpercent)))
                        image = image.resize((width,hsize), Image.ANTIALIAS)
                        w, h = image.size
                        x, y = 0, (h - w / (width / height)) // 2
                    image = image.crop((x, y, w - x, h - y))
                    return image

                st.image(crop_resize(img, ratio))
                
            with cols[col_idx]:
                st.write(name)
                st.write(medium)
                st.write('')

# 본문
def main():
    # sidebar size css
    st.markdown(
        """
        <style>
        [data-testid="stSidebar"][aria-expanded="true"] > div:first-child {
            width: 400px;
        }
        [data-testid="stSidebar"][aria-expanded="false"] > div:first-child {
            width: 400px;
            margin-left: -400px;
        }
        </style>
        """,
        unsafe_allow_html=True,
    )

    # sidebar content
    st.sidebar.title("애니메이션 검색")
    with st.sidebar.form("search_form"):
        search_text = st.text_input('제목으로 검색')
        search_genre = st.multiselect("장르 선택", make_multiselect_option(df2, 'genres'))
        search_medium = st.multiselect("TVA / 극장판", ['TVA', '극장판', 'OVA', '기타'])
        search_onair = st.multiselect('방영 연도', [str(x) for x in range(2022, 2000, -1)])
        
        # 제작사
        search_production = st.multiselect('제작사 선택', make_production_select(df2, 'production'))

        # 아무거나 검색
        search_all = st.text_input('무작위 검색')

        # Form submit button
        submitted = st.form_submit_button("Submit")

    # body        
    df_filtered = search_result(df, search_text, search_genre, search_medium, search_onair, search_production, search_all)
    show_result(df_filtered)

if __name__ == '__main__':
    main()

 

 

코드가 길어졌지만 간단히 설명을 하자면 다음과 같다

1. st.sidebar.form에서 여러 검색 조건들을 받는다(제목, 장르, 매체, 방영 연도, 제작사, 텍스트)

2. search_result 함수를 통해 조건에 맞는 데이터만 골라낸다

3. show_result 함수를 통해 streamlit 웹 애플리케이션 상에 결과를 표시한다

 

 

이런 식으로 자유롭게 검색 조건을 달아서 애니메이션 검색을 할 수 있게 되었다.

사실 조건에 맞는 애니메이션의 이미지와 제목만 보여주고 있기 때문에 누르면 상세정보를 볼 수 있다던가 링크를 타고 관련 페이지로 갈 수 있으면 더 좋을 것 같다는 생각을 했다. 하지만 이것은 웹페이지를 통째로 만들어야 하는 수준이 되기 때문에 이번 프로젝트에서는 라이브러리의 특성 상 검색 결과만을 보여주기로 했다. 사실 streamlit 상에서도 css를 만져 이미지에 링크를 걸어두는 등 구현을 할 수는 있지만 웹사이트를 만들려면 다른 프로그래밍 언어로 만드는 것이 더 적절하다는 생각이 들었다.

 

다음 글에서 계속