ABOUT ME

-

Total
-
  • Python: 간단한 streamlit 앱 만들면서 배운 점
    컴퓨터/파이썬 2021. 9. 19. 14:34
    728x90
    반응형

    streamlit

     

    만든 것

    데이터 앱을 쉽고 예쁘게 만들 수 있는 streamlit,

    mongoDB 필터링 기능도 익힐 겸 아래의 값들에 따라 필터링을 해서 테이블을 업데이트한다.

    (자동으로 공지들을 db에 저장하고 있다.)

    구조

    처음에 st.slider, st.multiselect 와 같은 위젯들 on_change에 callback 함수를 추가해서

    그 함수에서 값들을 받고, 필터를 만들고, global 테이블을 업데이트했다.

    st.title("title")
    my_table = st.empty()
    
    def update_notice():
        global my_table
        global categories
    
        my_table.empty()
    
        filter = {}
    
        try:  # whenever I change category, it is changed too
            if categories:
                filter = {"$or": [{"category": category} for category in categories]}
        except NameError:
            pass
    
        sort = list({"id": -1}.items())
    
        # I tried same method like categories for count, but the count value never changes
        result = tuple(
            client["db"]["collection"].find(filter=filter, sort=sort, limit=count)
        )
    
        # print(result)
    
        data = {
            "title": tuple(article["title"] for article in result),
        }
    
        df = pd.DataFrame(data)
        df.index = [f"{i}th" for i in range(1, len(result) + 1)]
        my_table.table(df)
    
    categories = st.multiselect(
        "label",
        categories,  # like ["a", "b"...]
        [],
        on_change=update_notice(),  # I should pass the function not return value, but the table disappears when I use  lambda: f() or f...
        help="help",
    )
    
    count = st.slider(
        "label",
        min_value=1,
        max_value=100,
        value=10,
        on_change=update_notice(),  # I should pass the function not return value, but the table disappears...
        help="help",
    )
    
    # print(count), if I print it in here, it prints updated value..

    하지만 이런 방식은 테이블이 무조건 사라지고 값들이 추가되어 뚝뚝 끊기는 업데이트 느낌과

    2~3번 중복으로 업데이트되었고, 위젯들 값들도 받기가 어려웠다.

    streamlit 커뮤니티에 질문했더니 streamlit은 위젯들 값이 바뀔 때 코드를 처음부터 끝까지 다시 실행한다고 한다.

    그래서 떠올린 방법은 각 위젯들에게 key를 부여하고 st.session_state.some_key 를 확인하여

    처음 실행할 때 필터 오류를 방지했다. (그러면 on_change는 중간 인터셉터 역할?)

     

    처음: 각 위젯 on_change에 callback 함수 추가 -> callback에서 위젯 값들 받아오고 table 업데이트 시도

    이후: 처음에 state 확인하고 몽고DB 필터 만들기 -> table 다시 생성


    mongoDB 필터

    filter = {"$and": [{"$or": []}]}
    
    sort_id = list({"id": -1}.items())
    
    client["db"]["collection"].find(
        filter=filter, sort=sort_id, limit=10
    )
    
    # range 필터
    # db 각 struct 마다 date가 "21.07.21"처럼 있음
    # greater equal, less than equal (inclusive)
    # 아래는 오늘 ~ 3일전
    filter["$and"].append(
        {
            "date": {
                "$gte": (datetime.now() - timedelta(days=3)).strftime("%y.%m.%d"),
                "$lte": datetime.now().strftime("%y.%m.%d"),
            }
        }
    )
    
    # regex
    filter["$and"].append({"title": {"$regex": "hello"}})
    
    # or
    filter["$and"][0]["$or"].append({"category": cate})

     

    이미지 centering

    st.markdown(
        f"""
    <div style="display: flex; justify-content: center">
    <p>
        <a href="link" target="_blank" rel="noopener"><img style="width: 100px" src="https://user-images.githubusercontent.com/2356749/133914037-2b5e515a-3ac6-4228-b7ea-294d8fa735f1.png"></a>
    </p>
    """,
        unsafe_allow_html=True,
    )

     

    메뉴 + Made with streamlit 없애기 (바꾸기)

    # 버거 메뉴 + footer 수정
    hide_streamlit_style = """
                <style>
                #MainMenu {visibility: hidden; }
                footer {visibility: hidden;}
                footer:after {visibility: visible; content:"footer!";}
                </style>
                """
    st.markdown(hide_streamlit_style, unsafe_allow_html=True)

     

    코드

    from datetime import datetime, timedelta
    from os import linesep
    
    import pandas as pd
    import streamlit as st
    from pymongo import MongoClient
    
    MONGODB = "mongodb+srv://DB주소"
    client = MongoClient(MONGODB, serverSelectionTimeoutMS=5000)
    
    
    filter = {"$and": [{"$or": []}]}
    sort_id = list({"id": -1}.items())
    result = []
    
    min_date, max_date = datetime.now() - timedelta(days=3), datetime.now()
    
    ajou_categories = [
        "학사",
        "비교과",
        "장학",
        "학술",
        "입학",
        "취업",
        "사무",
        "기타",
        "행사",
        "파란학기제",
        "학사일정",
    ]
    
    
    # 필터링 값 만들기
    def collect_selections():
        global filter, result, min_date, max_date
    
        try:
            min_date, max_date = st.session_state.my_date
        except Exception:
            pass
    
        try:
            if st.session_state.my_category:
                for category in st.session_state.my_category:
                    filter["$and"][0]["$or"].append({"category": category})
                # filter = {"$or": [{"category": category} for category in categories]}
        except Exception:
            pass
    
        try:
            filter["$and"].append(
                {
                    "date": {
                        "$gte": st.session_state.my_date[0].strftime("%y.%m.%d"),
                        "$lte": st.session_state.my_date[1].strftime("%y.%m.%d"),
                    }
                }
            )  # pyright: reportGeneralTypeIssues=false
        except Exception:
            filter["$and"].append(
                {
                    "date": {
                        "$gte": (datetime.now() - timedelta(days=3)).strftime("%y.%m.%d"),
                        "$lte": datetime.now().strftime("%y.%m.%d"),
                    }
                }
            )
    
        try:
            filter["$and"].append({"title": {"$regex": st.session_state.my_keyword}})
        except Exception:
            pass
    
        if not filter["$and"][0]["$or"]:
            filter["$and"][0]["$or"].append({})
    
        try:
            result = tuple(
                client["ajou"]["notice"].find(
                    filter=filter, sort=sort_id, limit=st.session_state.my_count
                )
            )
        except AttributeError:
            result = tuple(
                client["ajou"]["notice"].find(filter=filter, sort=sort_id, limit=10)
            )
    
    
    collect_selections()
    
    # 로고 img 센터링
    st.markdown(
        f"""
    <div style="display: flex; justify-content: center">
    <p>
        <a href="https://www.ajou.ac.kr/kr/ajou/notice.do" target="_blank" rel="noopener"><img style="width: 100px" src="https://user-images.githubusercontent.com/2356749/133914037-2b5e515a-3ac6-4228-b7ea-294d8fa735f1.png"></a>
    </p>
    """,
        unsafe_allow_html=True,
    )
    
    data = {
        "제목": tuple(notice["title"] for notice in result),
        "날짜": tuple(notice["date"] for notice in result),
        "글쓴이": tuple(notice["writer"] for notice in result),
        "종류": tuple(notice["category"] for notice in result),
        "링크": tuple(notice["link"] for notice in result),
    }
    
    # 마크다운 테이블에 쓸 text 생성
    markdown_txt = []
    for i in range(len(result)):
        markdown_txt.append(
            f'|[{data["제목"][i]}]({data["링크"][i]})|{data["날짜"][i]}|{data["글쓴이"][i]}|{data["종류"][i]}|'
        )
    
    st.markdown(
        f"""
    |제목|날짜|글쓴이|종류|
    |:---:|:---:|:---:|:---:|
    {linesep.join(markdown_txt)}
    """
    )
    
    st.write(linesep)  # little blank
    
    if len(result) == 0:
        st.error("조건에 맞는 공지가 없습니다!")
    
    keyword = st.text_input(
        "공지 키워드를 입력하세요.",
        key="my_keyword",
        max_chars=30,
        help="공지 제목을 필터링할 수 있습니다.",
    )
    
    categories = st.multiselect(
        "카테고리 선택",
        ajou_categories,
        [],
        key="my_category",
        help="공지 카테고리를 선택할 수 있습니다.",
    )
    
    
    notice_date = st.slider(
        "공지 날짜",
        min_value=datetime.now() - timedelta(days=30),
        max_value=datetime.now(),
        value=(min_date, max_date),
        format="MM/DD",
        key="my_date",
        help="공지 날짜를 지정할 수 있습니다.",
    )
    
    count = st.slider(
        "공지 개수",
        min_value=1,
        max_value=100,
        value=10,
        key="my_count",
        help="공지 개수를 지정할 수 있습니다.",
    )
    
    # 버거 메뉴 + footer 수정
    hide_streamlit_style = """
                <style>
                #MainMenu {visibility: hidden; }
                footer {visibility: hidden;}
                footer:after {visibility: visible; content:"My Footer!";}
                </style>
                """
    st.markdown(hide_streamlit_style, unsafe_allow_html=True)
    728x90

    '컴퓨터 > 파이썬' 카테고리의 다른 글

    Python: 사진 to pdf 파일 변환  (0) 2021.10.23
    FastAPI + discord.py snippet  (0) 2021.08.24
    Python Selenium: 여러가지 팁  (0) 2021.06.07

    댓글