어제의 다짐에 이어, 오늘은 OOP 능력 향상을 위해 유명 파이썬 모듈의 소스를 분석해보기로 했다.

 

'아만보'라는 과거 유행어가 있다.

 

"아는만큼 보인다"의 준말인데, 수학과 과학이 연관되어 있지 않는 오픈 소스 중에 그나마 보일 게 많은

 

"Requests"라는 유명 파이썬 오픈소스를 GitHub에서 Clone하여 읽어보기로 했다.

 

출처는 다음과 같다.

 

 

https://github.com/psf/requests

 

GitHub - psf/requests: A simple, yet elegant, HTTP library.

A simple, yet elegant, HTTP library. Contribute to psf/requests development by creating an account on GitHub.

github.com

소스 파일, 테스트 패키지들이 깔끔하다.

 

※ 관전포인트 : 사실 왜 __init__.py가 있는지 몰랐는데, 오늘 이해했다.

(디렉토리 밑에 __init__.py 파일이 있으면 pycharm에서 패키지로 인식함. 패키지를 쓰면, 서로 다른 namespace를 사용하여 변수명이 겹치지 않고, code 재사용성, 공동작업 및 배포등이 편해지는 장점이 있음)

 

메인 패키지의 init을 보면 import만 한 페이지 이상이다. 또한, try, except문을 활용하여 import 의 예외처리를 하는 것이 인상적이다.

 

import warnings

import urllib3

from .exceptions import RequestsDependencyWarning

try:
    from charset_normalizer import __version__ as charset_normalizer_version
except ImportError:
    charset_normalizer_version = None

try:
    from chardet import __version__ as chardet_version
except ImportError:
    chardet_version = None


...

※ 관전포인트 : import error를 처리하는 방법도 우아하다.

 

 

SOCKSProxyManager 라는 함수는 본문에 사용되는 함수인데, imort할 때 error가 발생할 경우 오버라이딩되게 하였다. 그러니, 해당 함수가 동작할 때만 에러를 일으키게 되어 코드 내부에서 매번 except처리를 하지 않아도 되었다.

 

※ 관전포인트 : 추상 클래스를 쓸 필요가 없다. NotImplementedError로 처리

 

 
※ 관전포인트 : attribute를 미리 정리 해놓기

 

 

for ~ in 구문을 통해 __dict__ 보다 우아하게 표현할 수 있다.

 

※ 관전포인트 : init의 파라미터를 멀티라인으로..

 

 

※ 관전포인트 : 같은 패키지 내에서는 from . import [ClassName]

 

 

※ 관전포인트 : 다(多)기능의 함수를 편리하게 세분화? 시킬 수 있다.

 

※ 관전포인트 : all()의 활용방법

 

※ 관전포인트 : 동적인 attr 추가 (request header에 추가하는 것)

 

※ 관전포인트 : 파라미터에서 인자 뽑아쓰기

※ 관전포인트 : iterable 삭제하는 방법

 

※ 관전포인트 : 동적인 인스턴스 생성

※ 관전포인트 : 파이썬 에러 클래스 만들기 + 상속

 

※ 관전포인트 : 함수 하나로 여러 동작 시키기

 

 

더 분석할 게 많지만 시간관계상 오늘은 여기까지 하고 다음번에 또 계속 분석하겠습니다.

 

감사합니다.

 

 

교육과정 시퀀스 중에 발생한 어떤 이벤트로 인해 학습일지 업로드 시간이 변경되었다.

 

아침에 매번 체크하는 것이 지치기 때문에 크롤링 프로그램을 구동시켜 자동으로 체크하게끔 설계해본다.

 

그동안 제작을 미루고 있었는데, 교육 기간이 짧아지면서 늦게 만들수록 효용가치는 더 떨어진다.

 

selenium 을 통해 네이버 로그인을 통과하기엔 현재 내 기술력으로 불가능하기 때문에 업로드는 아직 무리라 생각되지만, 업로드 체크정도는 만들 수 있다.

 

그리하여 오전시간 틈틈히 제작했고 전에 실습해본 것이 있어 그다지 오래 걸리지 않았다.

 

# checker_daily.py
import datetime
import os
import time

from selenium import webdriver
from selenium.webdriver.common.by import By

from class_student import StudentBoard

driver = webdriver.Chrome()
driver.get("https://cafe.naver.com/startdev")

class_div = driver.find_element(by=By.ID, value="group579")

boards = list()

banner_tags = class_div.find_elements(by=By.TAG_NAME, value='li')
for li in banner_tags:
    a_href_link_WebElement = li.find_element(by=By.TAG_NAME, value='a')
    name = a_href_link_WebElement.text
    link = a_href_link_WebElement.get_attribute('href')

    if len(name) == 3 and name not in ['김진영']:
        boards.append(StudentBoard(name, link, driver))

result_list = list()
head = ["작성자", "마지막 글제목", "작성시간", "금일 업로드 여부"]

for board in boards:
    result_list.append(board.has_uploaded_daily())
    # time.sleep(3)

today_date = datetime.datetime.now().strftime("%m-%d")
with open(f'check_result/daily_check_{today_date}.csv', "w", encoding='utf-8') as file:
    for row in result_list:
        file.writelines(row + '\n')
# class_student.py
import re

import selenium.webdriver.chrome.webdriver
from selenium.webdriver.common.by import By


class StudentBoard:
    def __init__(self, name, link, driver):
        assert isinstance(driver, selenium.webdriver.chrome.webdriver.WebDriver)
        self.name = name
        self.link = link
        self.driver = driver
        self.is_uploaded_today = None

    def has_uploaded_daily(self):
        self.driver.get(self.link)
        self.driver.implicitly_wait(3)

        inner_frame = self.driver.find_element(by=By.ID, value='cafe_main')
        self.driver.switch_to.frame(inner_frame)

        board = self.driver.find_element(by=By.CSS_SELECTOR, value="#main-area > div:nth-child(4)")
        top_row = board.find_element(by=By.CSS_SELECTOR,
                                     value='#main-area > div:nth-child(4) > table > tbody > tr:nth-child(1)')
        post_title = top_row.find_element(by=By.CLASS_NAME, value='article').text
        if ',' in post_title:
            post_title = "\"" + post_title + "\""
        post_time = top_row.find_element(by=By.CLASS_NAME, value='td_date').text

        if len(post_time) == 5:
            self.is_uploaded_today = True
        else:
            self.is_uploaded_today = False

        return f"{self.name},{post_title},{post_time},{self.is_uploaded_today}"

파일은 csv로 저장하게끔 만들었고 결과물은 다음과 같다.

 

 

 

 

 

그다음 exe 파일로 변환하고..

 

 

 

그다음 윈도우 배치 프로그램을 통해 09:00AM에 자동으로 켜지게끔 작업스케줄러 설정

 

 

다음과 같이 윈도우 bat 파일을 만들어준다

 

 
 

 

 

그리고 컴퓨터 관리에 들어가서..

 

 
 

 

 
 

 

아까 만든 bat 파일을 맵핑한다

그러면 다음과 같이 스케줄러에 등록된 것을 확인할 수 있다.

 

내일부터는 변경된 폼으로 찾아뵙겠습니다.

 

감사합니다.

 

오늘은 잠시 요구분석을 내려놓고 시간내어 진득하게 데이터 분석을 공부 해보려한다.

 

주제는 건강검진을 시행한 직장가입자와 지역가입자의 검진 정보를 이리저리 분석 및 시각화해보는 것이다.

 

https://www.data.go.kr/data/15007122/fileData.do#/layer_data_infomation

 

국민건강보험공단_건강검진정보_20211229

건강검진정보란 국민건강보험의 직장가입자와 40세 이상의 피부양자, 세대주인 지역가입자와 40세 이상의 지역가입자의 일반건강검진 결과와 이들 일반건강검진 대상자 중에 만40세와 만66세에

www.data.go.kr

 

 
 

최근 공개된 3개년 데이터를 합치려는 중에 발생한 문제

데이터 칼럼의 텍스트가 미묘하게 다르고, 순서도 조금씩 다르다.

또한 2018 ~ 2019 년도에는 결손치유무 치아마모증유무 제3대구치(사랑니)이상 칼럼이 존재했지만

20년도부터는 없어졌다.

 

데이터 정합성을 맞추려면 어떻게 해야할까 고민하는 와중에

 

Pandas 내장 함수 중, column 제거, column 순서 바꾸기, column 이름 바꾸기를 시도한다.

 

1. 칼럼 제거

def delete_column(df):
    df: pd.DataFrame
    # print('before: ', df.columns.values)
    to_delete_column_name = ['결손치', '치아마모증유무', '제3대구치(사랑니)']
    bad_column_list = list()
    column_list = df.columns.values.tolist()
    for column_name in column_list:
        for bad_column_name in to_delete_column_name:
            if bad_column_name in column_name:
                bad_column_list.append(column_name)
                break
    df.drop(bad_column_list, axis='columns', inplace=True)

    # print('after: ', df.columns.values)

    return df
 

이제 모든 칼럼이 31개로 맞다. 순서를 맞춰주어야한다.

 

2. 순서 바꾸기

def main():
    config_pd_option()
    year_list = [2018, 2019, 2020]
    df = load_as_pickle_dataframe(2018)
    df = change_column_order(df)
    # save_as_pickle_from_data_frame_(df, 2018)

def change_column_order(df):
    # 기존 칼럼 순서 list
    df:pd.DataFrame
    # column_list = df.columns.values.tolist()
    column_list = ['기준년도', '가입자일련번호', '시도코드', '성별코드', '연령대코드(5세단위)', '신장(5Cm단위)', '체중(5Kg단위)', '허리둘레', '시력(좌)', '시력(우)', '청력(좌)', '청력(우)', '수축기혈압', '이완기혈압', '식전혈당(공복혈당)', '총콜레스테롤', '트리글리세라이드', 'HDL콜레스테롤', 'LDL콜레스테롤', '혈색소', '요단백', '혈청크레아티닌', '(혈청지오티)AST', '(혈청지오티)ALT', '감마지티피', '흡연상태', '음주여부', '구강검진수검여부', '치아우식증유무', '치석', '데이터공개일자']
    print('before:', df.columns.values)
    df = df[column_list]
    print('after:', df.columns.values)
    print(df)
    return df
 

이제 모든 3개년 정보의 칼럼 순서도 맞추었다. 미묘하게 다른 이름을 통일시켜주자.

 

3. 칼럼 이름 통일하기

 

def change_column_name(df):
    df:pd.DataFrame
    column_list = ['기준년도', '가입자일련번호', '시도코드', '성별코드', '연령대코드(5세단위)', '신장(5Cm단위)', '체중(5Kg단위)', '허리둘레', '시력(좌)', '시력(우)',
                   '청력(좌)', '청력(우)', '수축기혈압', '이완기혈압', '식전혈당(공복혈당)', '총콜레스테롤', '트리글리세라이드', 'HDL콜레스테롤', 'LDL콜레스테롤',
                   '혈색소', '요단백', '혈청크레아티닌', '(혈청지오티)AST', '(혈청지오티)ALT', '감마지티피', '흡연상태', '음주여부', '구강검진수검여부', '치아우식증유무',
                   '치석', '데이터공개일자']
    df.columns = column_list
    return df
 

4. 이제 하나의 dataframe으로 합치고 싶다.

그러나 pandas는 1.4 버전 이후부터 (현재 2.0.1) dataframe 끼리 append 하는 것을 없앴다.(deprecated)

 

현재는 pd.concat([df1, df2]) 방식을 사용한다.

 

def main():
    config_pd_option()
    year_list = [2018, 2019, 2020]
    df = load_as_pickle_dataframe(2018)
    df:pd.DataFrame
    for y in year_list[1:]:
        a_df = load_as_pickle_dataframe(y)
        df = pd.concat([df,a_df])

    print(df
 

근데 문제가 있다..?

 

100만 + 100만(+@) + 100만 row 기 때문에, 300만 row 가 나와야하는데, 100만 row 밖에 안된다.

 

concat을 사용할 때는 index_ignore 옵션을 True로 주어야 인덱스를 덮어쓰지 않고, 그대로 추가된다.

 

{수정 후}

def main():
    config_pd_option()
    year_list = [2018, 2019, 2020]
    df = load_as_pickle_dataframe(2018)
    df:pd.DataFrame
    for y in year_list[1:]:
        a_df = load_as_pickle_dataframe(y)
        df = pd.concat([df,a_df], ignore_index=True)

    print(df)

    with open('healthcare_as_byte_for_last_3_years', 'wb') as file:
        pickle.dump(df, file)
 

옵션을 추가해주었더니, 이제 잘 나온다.

 

※ 참고로 csv -> pickle로 전환해서 사용하는 이유는 load가 빠르기 때문이다.

pickle은 bytes로 저장되는데 반해 csv는 매번 전체 string 그대로 읽어오고 ',' 를 기준으로 구분해서 읽어주기 때문에 로딩속도가 해보면 매우 실감나게 차이난다.

 

이제 데이터베이스로 값을 집어넣는다.

 

단위시간 대비 많은 실습을 하기 위해 SQLite browser를 사용했다.

 

테이블을 짜놓고 보려니, 문제가 발생했다.

뒤쪽 데이터들이 아주 맘대로 저장되어있다. 어떤건 integer, 어떤건 real(float)로 입력되어있고,

흡연상태는 1,2,3 이 무슨 뜻인지, 모르겠고 (공공데이터 메타데이터에도 설명이 없다)

bool 값에 대해서는 Y 또는 N 또는 1.0 또는 0, NaN(Not a Number) 가 들어가있다.

 

그리하여 칼럼마다 어떤 도메인을 갖고 있는지 분석해보았다.

시도코드 도메인 : {'49', '42', '27', '50', '28', '11', '26', '30', '36', '48', '43', '47', '46', '29', '44', '31', '45', '41'}
성별코드 도메인 : {'2', '1'}
연령대코드(5세단위) 도메인 : {'13', '16', '9', '15', '14', '17', '11', '8', '10', '6', '18', '5', '7', '12'}
신장(5Cm단위) 도메인 : {'155', '125', '130', '165', '145', '150', '195', '180', '185', '170', '175', '190', '160', '140', '135'}
체중(5Kg단위) 도메인 : {'65', '140', '120', '130', '95', '60', '70', '115', '125', '100', '145', '30', '110', '75', '55', '85', '80', '50', '90', '40', '105', '35', '135', '45'}
허리둘레 도메인 : {'', '121.8', '88.8', '59.8', '67.0', '72.9', 중략..}
시력(좌) 도메인 : {'', '0.15', '0.4', '1.7', '2.0', '2.5', '0.6', '1.1', '0.9', '1.6', '2.2', '0.7', 'nan', '0.3', '1.9', '0.8', '1.5', '9.9', '1.3', '2', '0.1', '1', '0.5', '0.2', '1.8', '1.4', '1.2', '1.0', '2.1'}
시력(우) 도메인 : {'', '0.15', '0.4', '1.7', '2.0', '2.5', '0.6', '1.1', '0.9', '1.6', '2.2', '0.7', 'nan', '0.3', '2.4', '1.9', '0.8', '1.5', '9.9', '1.3', '2', '0.1', '1', '0.5', '0.2', '1.8', '1.4', '1.2', '1.0', '2.1'}
청력(좌) 도메인 : {'', '2.0', 'nan', '2', '1', '1.0', '3.0'}
청력(우) 도메인 : {'', '2.0', 'nan', '2', '1', '1.0', '3.0'}
수축기혈압 도메인 : {'', '77', '209.0', '174', '136.0', '191.0', '211.0', '188', '109.0', '156.0', '125', '214.0', '153.0', 'nan', 중략..}
이완기혈압 도메인 : {'', '117.0', '69.0', '67.0', '165', '126', '36.0', '47.0', '37.0', '59', '37', '77', '43', '127', 'nan', 중략..}
식전혈당(공복혈당) 도메인 : {'', '392.0', '409', '263.0', '67.0', '547.0', '47.0', '37', '353.0', 'nan', 중략..}
총콜레스테롤 도메인 : {'', '263.0', '409', '392.0', '9', '67.0', '2238', 'nan', 중략..}
트리글리세라이드 도메인 : {'', '642.0', '790.0', '1287.0', '871.0', '209.0', 'nan', 중략..}
HDL콜레스테롤 도메인 : {'', '9', '67.0', '39.5', '47.0' ,'nan', 중략..}
LDL콜레스테롤 도메인 : {'', '263.0', '9', '67.0', '790.0', '2030', 'nan', 중략..}
혈색소 도메인 : {'', '9', '16.5', '14.4', '14.1', '20.2', 'nan', 중략..}
요단백 도메인 : {'', '2.0', 'nan', '2', '4', '3.0', '1', '3', '4.0', '1.0', '6', '6.0', '5', '5.0'}
혈청크레아티닌 도메인 : {'', '2.9', '9', '14.4', '14.1', '15.7', '4.4', '37', '0.4', '77', 'nan', 중략..}
(혈청지오티)AST 도메인 : {'', '209.0', '1006', '31', '23.3', '270', '349.0',  'nan', 중략..}
(혈청지오티)ALT 도메인 : {'', '790.0', '2030', '2380', '209.0', '483.0',  'nan', 중략..}
감마지티피 도메인 : {'', '642.0', '790.0', '871.0', '209.0', 'nan', 중략..}
흡연상태 도메인 : {'', '2.0', 'nan', '2', '1', '3', '1.0', '3.0'}
음주여부 도메인 : {'', 'nan', 'N', '0', 'Y', '1', '0.0', '1.0'}
구강검진수검여부 도메인 : {'Y', 'N', '0', '1'}
치아우식증유무 도메인 : {'', 'nan', '0', '1', '0.0', '1.0'}
치석 도메인 : {'', '2.0', 'nan', '2', 'N', '0', 'Y', '1', '0.0', '1.0'}
데이터공개일자 도메인 : {'2019-12-19', '2019-12-31', '2021-12-29'}
 

이제 이것들을 하나의 데이터 타입으로 통일시켜주어야한다.

소수점 중요한 데이터들은 float로 저장하고 Integer 칼럼은 Integer로 nullable 하게 column 제약을 설정한다

 

요약 : 'nan' or ' ' -> 'Null' 로 변환하여 db 저장함

 

또한, 공공데이터에 주기성 자료에 칼럼 내용에 대한 서술이 정리된 파일이 있었음을 늦게서야 발견했다.

칼럼 하나씩 값을 정리해나가는 과정 중에 만난 에러...

  File "C:\Users\KDT107\Desktop\data_analysis\anaysis_practice.py", line 173, in organize_domain
    temp_list.append(int(row))
                     ^^^^^^^^
ValueError: cannot convert float NaN to integer
 

DataFrame에 들어있는 NaN 값들은 python의 int함수로는 치환할 수 없다.

그렇다고 if row == np.NaN 으로 등호가 비교되지 않았다(시도 해봤다가 실패함)

 

np.isnan(row) 를 했을 때만 Nan인지 아닌지 알 수 있었다.

  File "C:\Users\KDT107\Desktop\data_analysis\venv\Lib\site-packages\pandas\core\dtypes\astype.py", line 150, in _astype_float_to_int_nansafe
    raise IntCastingNaNError(
pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer: Error while type casting for column '청력(좌)'
 

또한 NaN이 들어간 column은 dtype 을 'int32'로 변환시키는 것이 불가능했다.

StackOverFlow(https://stackoverflow.com/questions/12708807/numpy-integer-nan) 에서 검색한 바로는 Float 열에만 NaN을 쓸 수 있고, 어떤 특별한 비트에 대응하는 값에만 NaN을 대응시켜서 그렇다는 것 같다. 따라서 Integer에는 적용할 수 없었는데, 정 필요하다면 -999999 같은 수에 대응시켜서 NaN을 인식하는 쪽으로 가이드해주었다.

 

Numpy integer nan

Is there a way to store NaN in a Numpy array of integers? I get: a=np.array([1],dtype=long) a[0]=np.nan Traceback (most recent call last): File "<stdin>", line 1, in <module> Value...

stackoverflow.com

 

하지만 나는 NaN일 경우 SQLite에 Null값을 입력할 예정이라 그대로 두었다.

 

이후의 내용은 내일 마저 진행 예정이다.

 

 

이전에도 조금씩 시각화 연습을 하고 있었기 때문에, 꾸준히 보니깐 조금씩 익숙해지는 것 같습니다.

 

전체 소스코드입니다.

 

import os
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager
import matplotlib.lines


def main():
    draw_violin_plots()
    set_plot_title('예제19')
    save_fig_png()
    showing_plot()


def showing_plot():
    plt.show()


# 기본 사용
def basic_using_matplotlib():
    plt.plot([1, 2, 3, 4])


def basic_using_matplotlib_2():
    plt.plot([1, 2, 3, 4], [1, 4, 9, 16])


def basic_using_matplotlib_3():
    plt.plot([1, 2, 3, 4], [1, 4, 9, 16], 'ro')
    plt.axis([0, 6, 0, 20])


def basic_using_matplotlib_4():
    # 0.2 씩 간격으로 균일한 등차
    t = np.arange(0., 5., 0.2)
    plt.plot(t, t, 'r--', t, t ** 2, 'bs', t, t ** 3, 'g^')


# 숫자 입력하기
def input_dataset_1():
    plt.plot([1, 2, 3, 4], [2, 3, 5, 10])


def input_dataset_2():
    # dictionary type 사용(레이블이 있는 데이터)
    data_dict = {'X축': [1, 2, 3, 4, 5], 'Y축': [2, 3, 5, 10, 8]}
    plt.plot('X축', 'Y축', data=data_dict)


# 축 레이블 설정
def set_axis_label(x_label_title, y_label_title):
    plt.xlabel(x_label_title)
    plt.ylabel(y_label_title)


def set_axis_label_with_space(x_label_title, y_label_title):
    plt.xlabel(x_label_title, labelpad=10)
    plt.ylabel(y_label_title, labelpad=20)


def set_axis_label_another_position(x_label_title, y_label_title):
    plt.xlabel(x_label_title, loc='right')
    plt.ylabel(y_label_title, loc='top')


# 범례 표시하기
def set_legend_basic():
    fig = plt.figure(1)
    fig: matplotlib.pyplot.Figure
    ax = fig.axes[0]
    ax: matplotlib.pyplot.Axes
    ax.legend(title='범례', labels=['첫번째'])


# stack example
def doing_sample():
    x = np.linspace(0, 6 * np.pi, 100)
    y = np.sin(x)
    plt.ion()

    fig = plt.figure()
    ax = fig.add_subplot(111)
    line1, = ax.plot(x, y, 'r-')  # Returns a tuple of line objects, thus the comma

    for phase in np.linspace(0, 10 * np.pi, 500):
        line1.set_ydata(np.sin(x + phase))
        fig.canvas.draw()
        fig.canvas.flush_events()


# 선 종류 지정하기
def set_line_type():
    plt.plot([1, 2, 3, 4], [1, 4, 9, 16], 'ro')
    plt.plot([2, 3, 4, 5], [1, 2, 3, 4], 'bs')


# 마커 지정하기
def set_markers():
    plt.plot([1, 2, 3, 4], [2, 3, 5, 10], 'bo--')


def set_markers_2():
    plt.plot([4, 5, 6], marker="H")
    plt.plot([3, 4, 5], marker="d")
    plt.plot([2, 3, 4], marker="x")
    plt.plot([1, 2, 3], marker=11)
    plt.plot([0, 1, 2], marker='$Z$')


# 색상 지정하기
def set_color():
    plt.plot([1, 2, 3, 4], [2, 3, 5, 10], color='#e35f62', marker='o', linestyle='--')


# 그래프 영역 채우기
def fill_sector():
    x = [1, 2, 3, 4]
    y = [2, 3, 5, 9]

    plt.plot(x, y)
    # plt.fill_between(x[1:3], y[1:3], alpha=0.5)
    plt.fill_between(x[1:], y[1:], alpha=0.5)


# 축 스케일 지정
def set_axis_log_scale():
    x = np.linspace(-10, 10, 1000)
    y = x ** 3
    plt.plot(x, y)
    plt.yscale('symlog')


# 여러 곡선 그리기
# pass

# 그리드 설정하기
def set_grid_on():
    # plt.grid(True)
    # plt.grid(True, axis='x')
    plt.grid(True, axis='y')


# 눈금 표시하기
def show_ticks():
    x = np.linspace(0., 2., 100, endpoint=True)
    plt.plot(x, x)
    plt.plot(x, x ** 2)
    plt.plot(x, x ** 3)
    plt.xticks([-0.5, 0, 1, 2, 2.5], labels=['하나', '둘', '셋', '넷', '다섯'])
    plt.yticks(np.arange(0, 10.5, 0.5))


# 타이틀 설정하기
def set_plot_title(title_str):
    font_dict = {
        'fontsize': 20,
        'fontweight': 'bold'
    }
    # plt.title(title_str)
    plt.title(title_str, fontdict=font_dict, loc='left', pad=20)  # 이렇게하면 2개가 생김


# 수평선/수직선 그리기
def draw_vertical_or_horizontal_lines():
    x = np.arange(0, 4.5, 0.5)
    plt.plot(x, x + 1, 'bo')
    plt.plot(x, x ** 2, 'g--')
    plt.plot(x, -2 * x + 3, 'r:')

    plt.axhline(3.0, 0.2, 1.0, color='gray', linestyle='-', linewidth=2)
    plt.hlines(6.0, 1.0, 2.5, color='cyan', linestyle='solid', linewidth=3)

    plt.axvline(3.0, 0.2, 1.0, color='lightblue', linestyle='-', linewidth=2)
    plt.vlines(6.0, 1.0, 2.5, color='purple', linestyle='solid', linewidth=3)


# 막대 그래프 그리기
def draw_vertical_bar():
    x = np.arange(4)
    years = ['2020', '2021', '2022', '2023']
    values = [120, 190, 230, 530]

    plt.bar(x, values, color=['r', 'y', 'y', 'g'], align='edge', width=0.2, edgecolor='lightgray', tick_label=years)


# 수평 막대 그리기
def draw_horizontal_bar():
    x = np.arange(5)
    trial_count = ['first', 'second', 'third', 'fourth', 'fifth']
    value = [17, 15, 16, 14, 12]
    color_list = [get_random_color() for x in range(5)]
    plt.barh(x, value, tick_label=trial_count, color=color_list)


# 산점도 그리기
def draw_scatters():
    n = 50
    x = np.random.rand(n)
    y = np.random.rand(n)
    area = (30 * np.random.rand(n)) ** 2
    # colors = np.random.rand(n)
    colors = [get_random_color() for x in range(n)]

    plt.scatter(x, y, s=area, c=colors)


# 3차원 산점도 그리기
def draw_3D_scatters():
    n = 50
    x = np.random.rand(n)
    y = np.random.rand(n)
    z = np.random.rand(n)
    area = (30 * np.random.rand(n)) ** 2
    colors = [get_random_color() for x in range(n)]

    figure = plt.figure(1)
    ax = figure.add_subplot(111, projection='3d')
    ax.scatter(x, y, z, s=area, color=colors, marker='o')


# 히스토그램 그리기
def draw_histogram():
    random_bwt = np.random.randint(40, 101, size=100)
    figure = plt.figure(1)
    ax = figure.axes[0]
    counts, edges, bars = ax.hist(random_bwt, bins=10, histtype='barstacked', color=get_random_color())
    plt.bar_label(bars)  # 바 상단에 숫자 첨부


# 에러바 표시하기
def draw_error_bar():
    x = [1, 2, 3, 4]
    y = [1, 4, 9, 16]
    yerr = [2.3, 3.1, 1.7, 2.5]
    plt.errorbar(x, y, yerr=yerr)


# 파이 차트 그리기
def draw_pie_chart():
    ratio = [34, 32, 16, 18]
    labels = ['Apple', 'Banana', 'Melon', 'Grapes']
    plt.pie(ratio, labels=labels, autopct='%.1f%%', startangle=260, counterclock=False)


# 히트맵 그리기
def draw_heatmap():
    arr = np.random.standard_normal((30, 40))
    plt.matshow(arr)
    plt.colorbar(shrink=0.5, aspect=10)


# 컬러맵 설정하기
def using_colormap():
    arr = np.random.standard_normal((12, 100))
    plt.subplot(2, 2, 1)
    plt.scatter(arr[0], arr[1], c=arr[2])
    plt.spring()
    plt.title('spring')

    plt.subplot(2, 2, 2)
    plt.scatter(arr[3], arr[4], c=arr[5])
    plt.summer()
    plt.title('summer')

    plt.subplot(2, 2, 3)
    plt.scatter(arr[6], arr[7], c=arr[8])
    plt.autumn()
    plt.title('autumn')

    plt.subplot(2, 2, 4)
    plt.scatter(arr[9], arr[10], c=arr[11])
    plt.winter()
    plt.title('winter')


# 이미지 저장하기
def save_fig_png():
    path = r'plot_imgs/'
    dir_list = os.listdir(path)
    plt.savefig(path + f'saved_figure_{len(dir_list) + 1:02d}.png')


# 박스 플롯 그리기
def draw_box_plots():
    data_a = np.random.normal(0, 2.0, 100)
    data_b = np.random.normal(-3.0, 1.5, 500)
    data_c = np.random.normal(1.2, 1.8, 1000)

    fig, ax = plt.subplots()
    ax: plt.Axes
    ax.boxplot([data_a, data_b, data_c], notch=True, whis=2.5, vert=False)
    ax.set_xlim(-10.0, 10.0)
    ax.set_xlabel('Value')
    ax.set_ylabel('Data Type')


# 바이올린 플롯 그리기
def draw_violin_plots():
    data_a = np.random.normal(0, 2.0, 1000)
    data_b = np.random.normal(-1.2, 3, 500)
    data_c = np.random.normal(-4, 3, 2000)

    fig, ax = plt.subplots()
    ax: plt.Axes
    violin = ax.violinplot([data_a, data_b, data_c], positions=[2, 3, 4],
                           showmeans=True, quantiles=[[0.1, 0.9], [], []])
    ax.set_ylim(-20.0, 10.0)
    ax.set_xticks([1, 2, 3, 4, 5])
    ax.set_xlabel('Data type')
    ax.set_ylabel('Value')

    violins = violin['bodies']
    violins[0].set_facecolor('lightblue')
    violins[1].set_facecolor('violet')
    violins[2].set_facecolor('pink')


# 다양한 패턴 채우기
def fill_pattern():
    x = [1, 2, 3]
    y = [1, 2, 3]
    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
    ax1: plt.Axes
    ax2: plt.Axes
    ax3: plt.Axes
    ax4: plt.Axes

    ax1.bar(x, y, color='aquamarine', edgecolor='black', hatch='/')
    ax2.bar(x, y, color='salmon', edgecolor='black', hatch='\\')
    ax3.bar(x, y, color='navajowhite', edgecolor='black', hatch='+')
    ax4.bar(x, y, color='lightskyblue', edgecolor='black', hatch='*')

    plt.tight_layout()


# numpy로 random hex code 만들기
def get_random_color():
    result_color = "#"
    rgb_numbers = np.random.randint(0, 256, size=3)
    result_color = result_color + ''.join('{:02X}'.format(a) for a in rgb_numbers)
    return result_color


if __name__ == '__main__':
    main()

 

그리고 오늘 그려본 것들의 일부입니다.

 

감사합니다.

 

Pycharm에서 다중커서를 지원하는걸 모르시는 분들도 있는데,

 

ctrl을 빠르게 두번 누른 상태에서 키보드 상하로 움직이면 선택이 가능합니다.

 

아니면 Alt를 누른 상태에서 클릭을 하면, 다중 커서를 지원해줍니다.

 

ESC를 누르면 취소가 되구요.

 

그동안 한땀한땀 번호를 매기는게 쉽지 않았는데, 이런 플러그인도 있어 소개해드립니다.

``ctrl`` + ``alt`` + ``s`` 를 누르면 pycharm setting에 들어가지는데, 왼쪽 상단 검색에서 'plugin'이라고 검색하고,

 

[Marketplace] 탭에서 "String Manipulation"을 설치해줍시다.

 

설치가 끝나면 다중 선택에서 마우스 우클릭으로 사용할 수 있습니다.

 

프로젝트 내에서는 보안정책 때문에 사용하기 어렵겠지만, 연습용 개인 프로젝트에서는 유용하게 쓰일 수 있으리라 생각됩니다.

 

감사합니다.

 

 

1. 크롤링이란?

Crawling : to move slowly with the body close to the ground : move on hands and knees. : to move along slowly. the bus crawled along

영미에서는 무릎과 손바닥을 바닥에 붙이고 엉금엉금 기어가는 행위를 크롤링이라 일컫는다. 웹상에서 정보를 싹싹 긁어모으는 행위가 이와 유사하여 명명됐다.

 

 

2. BS4

BeautifulSoup4 는 정적페이지에서 크롤링할 때 쓰이는 툴로, 빠르고 가볍기 때문에, 위키페이지 같은 곳이나, 로그인 권한이 필요 없는 상황에서 빠르고 간편하다.

 

파이썬 request module로 직접 html 소스를 받아와서 분석하는 툴이기 때문에 태그를 잘 걸러 필요한 정보를 정리하는 작업이 필요하다.

 

 

3. Selenium

3-1. 개요

기존에 프론트엔드 테스트 모듈로 개발이 되었으나, 막강하고 유용한 성능들로 인해, 다목적용으로 이용되는 듯하다.

요즘 웹 어플리케이션들은 자바스크립트를 통해 동작하는 경우가 많아 접속하고자하는 버튼들이 숨겨져있을 수 있고, 어떤 액션(ex. login)을 수행해야만 가능한 행동들이 많다. 또한, 사람마다 다르게 보여지는 동적 웹들이 주류를 이루고 있다.

 

Selenium 을 잘 접목하면 자동으로 호텔, 교통수단 booking을 할 수 있을 것으로 보이며, 아무래도 요즘 뜨는 chatGPT 외부 플러그인들이 이런 것들을 이용하지 않을까 싶다.

 

3-2. Selenium 상세 실습

그동안 내가 작성한 글들을 정리하는 소스를 만들어보았다. 이동녘 교수님께서 알려주신 방법을 그대로 따라한 것이지만, 혼자서 다시 쳐보는 연습을 해보고 있다. 사실 자기가 썼던 글 제목만 복사하는 정도는 로그인이 필요 없어서, 그 부분은 생략했다.

 

그리고, CSS-Selector 이거 참 물건이라고 느낀다. 크롤링을 수월하게 배운 것 같아 매우 기분이 좋습니다.

 

(맥 환경에서 테스트한 것이기 때문에, chromedriver에 exe 확장자가 따로 없는게 맞습니다)

 

[소스 코드]

 

# cafe 접속

from selenium import webdriver
from selenium.webdriver import Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

# WEB DRIVER PATH 설정
WEB_DRIVER_PATH = '/Users/gwang/WebDriver/chromedriver/chromedriver'
s = Service(WEB_DRIVER_PATH)

# DRIVER를 통한 브라우저 실행
driver = webdriver.Chrome(service=s)
driver.get('https://www.naver.com')

# 전체 html 불러오기
total_html_page = driver.find_element(by=By.TAG_NAME, value='html')

# css-selector 복사
btn_cafe = driver.find_element(by=By.CSS_SELECTOR, value='#shortcutArea > ul > li:nth-child(2) > a')
btn_cafe.click()

# 새 탭 전환
driver.switch_to.window(driver.window_handles[1])

# 검색창에 start dev 입력
input_area = WebDriverWait(driver, timeout=3).until(
    lambda d: d.find_element(by=By.CSS_SELECTOR,
                             value='#header > div.snb_area > div > div.SearchArea > form > fieldset > div > div > div.FormInputText > input')
)

input_area.send_keys('start developer')
input_area.send_keys(Keys.ENTER)

btn_cafe_start_dev = WebDriverWait(driver, timeout=3).until(
    lambda d: d.find_element(by=By.CSS_SELECTOR,
                             value='#mainContainer > div > div > div.section_search_content > div > div.list_area.cafe_list_area > ul > li > div > div > a')
)
btn_cafe_start_dev.click()

# 새 탭 전환
driver.switch_to.window(driver.window_handles[2])

# 내 게시판 접속
link_my_board = WebDriverWait(driver, timeout=3).until(
    lambda d: d.find_element(by=By.CSS_SELECTOR, value='#menuLink590')
)
link_my_board.click()

# iFrame으로 전환
inner_frame = WebDriverWait(driver, timeout=3).until(
    lambda d: d.find_element(By.ID, 'cafe_main')
)
driver.switch_to.frame(inner_frame)


def print_posts(web_driver):
    # 게시판 리스트로 선택
    board_list = web_driver.find_elements(by=By.CSS_SELECTOR,
                                          value='#main-area > div:nth-child(4) > table > tbody > tr')

    for tr in board_list:
        post_id = tr.find_element(by=By.CLASS_NAME, value='inner_number').text
        post_title = tr.find_element(by=By.CLASS_NAME, value='article').text
        poster = tr.find_element(by=By.CLASS_NAME, value='m-tcol-c').text
        post_date = tr.find_element(by=By.CLASS_NAME, value='td_date').text
        print(f"{post_id:^11}|{post_title:^30}|{poster:^15}|{post_date:^15}")


page_list = driver.find_elements(by=By.CSS_SELECTOR, value='#main-area > div.prev-next > a')
for index in range(len(page_list)):
    print(f'{index} 페이지')
    driver.find_elements(by=By.CSS_SELECTOR, value='#main-area > div.prev-next > a')[index].click()
    print_posts(driver)

while True:
    pass

[출력화면]

0 페이지
   38158   |크롤링, 어디까지 해도 괜찮은걸까? [2023-06-05 학습일지]|   박광현KDT23    |  2023.06.05.  
   38157   |Pandas, SQLite 접목 복습 [2023-06-05 학습일지]|   박광현KDT23    |  2023.06.05.  
   38127   |소프트웨어 요구 명세서(SRS) 제작 일지 3일차 -완성- [2023-06-04]|   박광현KDT23    |  2023.06.04.  
   38115   |          StarUML 소개          |   박광현KDT23    |  2023.06.04.  
   38051   |소프트웨어 요구 명세서(SRS) 제작 일지 1일차 [2023-06-02]|   박광현KDT23    |  2023.06.02.  
   38012   |소프트웨어 요구 사양서(SRS) 제작 일지 0일차 [2023-06-01]|   박광현KDT23    |  2023.06.01.  
   37975   |matplotlib 한글 폰트 적용, 데이터 시각화 [2023-05-31 학습일지]|   박광현KDT23    |  2023.05.31.  
   37946   |SQLite 수업 정리 [2023-05-30 학습일지]|   박광현KDT23    |  2023.05.30.  
   37930   |파이썬으로 실시간 버스 도착 정보 가져오기 with 타요뻐스 [2023-05-29]|   박광현KDT23    |  2023.05.29.  
   37929   |Pandas, SQLite3 공부하기 [2023-05-29 학습일지]|   박광현KDT23    |  2023.05.29.  
   37923   |파이썬 궁금한 것 공부해서 정리하기(프리 토픽) [2023-05-27 학습일지]|   박광현KDT23    |  2023.05.27.  
   37888   |레전드오브복이 개발일지 14일차- 마지막날 - [2023-05-25]|   박광현KDT23    |  2023.05.25.  
   37841   |레전드오브복이 개발일지 13일차 [2023-05-24]|   박광현KDT23    |  2023.05.24.  
   37820   |레전드오브복이 개발일지 12일차 [2023-05-23]|   박광현KDT23    |  2023.05.23.  
   37771   |레전드오브복이 개발일지 11일차 [2023-05-22]|   박광현KDT23    |  2023.05.22.  
1 페이지
   37744   |레전드오브복이 개발일지 10일차 [2023-05-21]|   박광현KDT23    |  2023.05.21.  
   37711   |레전드오브복이 개발일지 9일차 [2023-05-20]|   박광현KDT23    |  2023.05.20.  
   37675   |레전드오브복이 개발일지 8일차 [2023-05-19]|   박광현KDT23    |  2023.05.19.  
   37637   |레전드오브복이 개발일지 7일차 [2023-05-18]|   박광현KDT23    |  2023.05.18.  
   37592   |레전드오브복이 개발일지 6일차 [2023-05-17]|   박광현KDT23    |  2023.05.17.  
   37543   |레전드오브복이 개발일지 5일차 [2023-05-16]|   박광현KDT23    |  2023.05.16.  
   37509   |레전드오브복이 개발일지 4일차 [2023-05-15]|   박광현KDT23    |  2023.05.15.  
   37486   |레전드 오브 복이 개발일지 - 3일차 - [2323-05-14 학습일지]|   박광현KDT23    |  2023.05.14.  
   37360   |변수명, 함수명은 어떻게 써야할까? (PEP8 간단 요약) [2023-05-09 학습일지]|   박광현KDT23    |  2023.05.09.  
   37359   |파이썬으로 유닛 테스트(Unit Test) 하기 [2023-05-09 학습일지]|   박광현KDT23    |  2023.05.09.  
   37328   |Qt Designer 사용법 발표 및 로또 번호 추첨기 [2023-05-08 학습일지]|   박광현KDT23    |  2023.05.08.  
   37317   |처음 시도한 Pair Programming Result [2023-05-07 학습일지]|   박광현KDT23    |  2023.05.07.  
   37269   |PyQt5 리그오브레전드 로그인창 따라하기 [2023-05-04 학습일지]|   박광현KDT23    |  2023.05.04.  
   37234   |PyQt5 모듈 확인, 오늘 공부한 것 [2023-05-03 학습일지]|   박광현KDT23    |  2023.05.03.  
   37196   | PyQt5 공부하기 [2023-05-02 학습일지] |   박광현KDT23    |  2023.05.02.  
2 페이지
   37124   |<파이썬으로 8만대장경 작성하기> 개발 실패 <2023-05-01 최종>|   박광현KDT23    |  2023.05.01.  
   37080   |<파이썬으로 8만 대장경 작성하기> 개발 계획서 [2023-04-27 시작]|   박광현KDT23    |  2023.04.27.  
   37029   |회원가입, 로그인 화면 구축 개발 실패 보고서 [2023-04-26]|   박광현KDT23    |  2023.04.26.  
   36993   |<개복치 키우기> 개발 완료 보고서 [2023-04-25 학습일지]|   박광현KDT23    |  2023.04.25.  
   36952   |<경주마와 아이들> 팀이 내주신 문제 풀이 중간 보고서 <2023-04-24 개발일지>|   박광현KDT23    |  2023.04.24.  
   36887   |for 반복문 사용하여 숫자 스무고개 만들기 [2023-04-21]|   박광현KDT23    |  2023.04.21.  
   36886   |for 반복문 사용하여 가위바위보 게임 만들기 (메시징 처리) [2023-04-21]|   박광현KDT23    |  2023.04.21.  
   36853   |<김주김> 팀이 내주신 문제 풀이 완료 [2023-04-20 학습일지]|   박광현KDT23    |  2023.04.20.  
   36849   |<동녘짱과 아이들> 팀이 내주신 문제 풀이 완료 [2023-04-20 학습일지]|   박광현KDT23    |  2023.04.20.  
   36848   |다시 돌아온 별찍기 파이썬 버전 [2023-04-20]|   박광현KDT23    |  2023.04.20.  
   36805   |<점프투용궁> 팀이 내주신 문제 풀이 완료 [2023-04-19 학습일지]|   박광현KDT23    |  2023.04.19.  
   36804   |<똘똘이> 팀이 내주신 문제 풀이 완료 [2023-04-19 학습일지]|   박광현KDT23    |  2023.04.19.  
   36803   |<이것도 찾어보던가> 팀이 내주신 문제 풀이 완료 [2023-04-19 학습일지]|   박광현KDT23    |  2023.04.19.  
   36801   |월요일 출제된 문제 다 풀어보기 [2023-04-19 학습일지]|   박광현KDT23    |  2023.04.19.  
   36703   |문제 출제 및 학우님들 문제 풀어보기 [2023-04-17 학습일지]|   박광현KDT23    |  2023.04.17.  
3 페이지
   36629   |Bool type 배운 것 정리[2023-04-14] |   박광현KDT23    |  2023.04.14.  
   36569   |Python Tuple, Dictionary, Set 자료형 배운 것, 개인 공부 조금 추가하여 정리 [2023-04-13 학습일지]|   박광현KDT23    |  2023.04.13.  
   36482   |행렬을 쓰는 이유, Numpy 개인 공부 [2023-04-12 학습일지]|   박광현KDT23    |  2023.04.12.  
   36410   |간단 요약하는 AI 역사 [2023-04-11 학습일지]|   박광현KDT23    |  2023.04.11.  
   36359   |      내가 정보처리기사를 공부하는 이유      |   박광현KDT23    |  2023.04.10.  
   36311   |<개인프로젝트> 이세계 간호사 시뮬레이터 만들기 [2023-04-09 학습일지]|   박광현KDT23    |  2023.04.09.  
   36174   |로또 시뮬레이션 프로그램 만들기, 개발 계획서, 개발 완료 보고서, 소스코드 [2023-04-06 학습일지]|   박광현KDT23    |  2023.04.06.  
   36122   |<나무 나무를 심자> 개발 계획서 및 소스, 개발 완료 계획서 [2023-04-05 학습일지]|   박광현KDT23    |  2023.04.05.  
   36100   |  [공유]아스키코드로 텍스트 이미지 만드는 방법   |   박광현KDT23    |  2023.04.04.  
   36063   |가위바위보 턴제 전투 게임 만들기, 개발 계획서, 소스 코드, 개발 완료 보고서 [2023-04-04 학습일지]|   박광현KDT23    |  2023.04.04.  
   36012   |숫자야구 만들기 개발 계획서, 개발 완료 보고서, 소스코드 [2023-04-03 학습일지]|   박광현KDT23    |  2023.04.03.  
   35934   |[과제 제출] <C언어로 김밥천국 POS기 프로그램> 개발 완료 및 보고서|   박광현KDT23    |  2023.04.01.  
   35902   | <C언어로 김밥천국 POS기 프로그램 개발 계획서> |   박광현KDT23    |  2023.03.31.  
   35888   |심폐소생술 순서도 그리기, list 자료구조 C언어로 구현 [2023-03-31 학습일지]|   박광현KDT23    |  2023.03.31.  
   35874   |   각 언어별, 다른 종류 IDE 쓸 때 깨알팁   |   박광현KDT23    |  2023.03.30.  
4 페이지
   35855   |C언어 한글 입력 받기 실패 & 배열 없이 로또 번호 생성, 정렬 [2023-03-30 학습일지]|   박광현KDT23    |  2023.03.30.  
   35830   |C 언어로 한글 파일 출력하기 [2023-03-29 학습일지]|   박광현KDT23    |  2023.03.30.  
   35744   |GitHub 24.6k star를 받은 AI 전문가 로드맵 [2023-03-28 학습일지]|   박광현KDT23    |  2023.03.28.  
   35693   | 가위바위보 게임 만들기 [23.03.27 학습일지] |   박광현KDT23    |  2023.03.27.  
   35649   |   이번 주 풀었던 퀴즈 & 과제 소스 코드들    |   박광현KDT23    |  2023.03.24.  
   35639   |C언어 변수, 그런데 메모리를 곁들인 [2023-03-24 학습일지]|   박광현KDT23    |  2023.03.24.  
   35564   |C 언어 포인터 쉽게 이해해보려 노력하기 [2023-03-22 학습일지]|   박광현KDT23    |  2023.03.22.  
   35522   |SQL 공부하기 for 정보처리기사 실기 [2023-03-21 학습일지]|   박광현KDT23    |  2023.03.21.  
   35482   |C의 탄생 이전엔 어떤 프로그래밍 언어들이 있었을까? [2023-03-20 학습일지]|   박광현KDT23    |  2023.03.20.  
   35460   |        좋은 개발자에 대한 고찰         |   박광현KDT23    |  2023.03.18.

 

감사합니다.

 

오늘은 Python의 BS4 와 Selenium을 사용한 크롤링에 대해서 배웠습니다.

 

필요한 정보를 한꺼번에 끌어올 수 있다는 장점이 있습니다. 하지만 늘 장점만 존재할까요?

 

한편, 역지사지로 생각해봅시다. 당신이 '서버 관리자'이고 회사의 경영과 밀접한 연관성이 있다고 '가정'해봅니다.

 

당신이 소중하게 모은 데이터들, 혹은 개인정보법에 보호되어야할 정보들이 로봇 프로그램에 의해

 

국적불문, 이름도 모르는 사람에게 싸그리 복사당하고 있습니다. 더불어 이들은 일반이용자들보다 단위시간당 훨씬 많은 사이트를 방문하고 정보를 요청합니다.

 

그래서, 서버 크기도 늘려야하고 운용 비용이 증가합니다. 이들이 너무 많은 요청을 한꺼번에 해서, 일반적인 이용자가 사이트 접속이 느려지는 상황입니다. 실제적으로 사이트를 운용하는데 도움이 되는 광고나 매출 증가에는 아무런 영향을 주지 않습니다. 이런 경우엔, 어떻게 해야할까요? 소송...

 

이런 경우도 있습니다. 2008년 구인구직 사이트의 정보를 그대로 크롤링하여 자신의 구인 구직 사이트에 올려 소송을 당한 사례가 있습니다. 부정경쟁행위로 5000만원 보상금과 소송비용을 부담하는 것 2016년까지 가서 최종판결이 내려졌습니다.

 

판례 참조 : 서울중앙지방법원 2016. 2. 17. 선고 2015가합517982 판결

 

2021년도에는 A숙박업소 정보제공 서비스 업체에서 B숙박업소 정보제공 서비스 업체를 데이터베이스 제작업자의 권리를 침해했다는 주장으로 부정경쟁방지법과 이런 크롤링 행위가 정보통신망을 침입행위에 해당하는 일로 정보통신망법 위배, A숙박업소의 서비스 제공에 방해를 주었다는 주장으로 컴퓨터등장애업무방해죄로 소송이 진행된 바 있습니다.

 

최종 판결에서는 A사의 API 접근방식에 대해 통상적인 패킷캡쳐로 알 수 있었고, 이에 대해 적절한 보호조치(권한 제한)을 하지 않은 것, 회사 내규에 명확한 권리를 명문화하지 않은 것의 이유로 정보통신망법 위반을 적용할 수 없다는 판결, 상당 부분 상대 회사에서 제공하는 데이터와 유사한 데이터를 제공하게 될 경우 저작권법 위배에 해당하나, B사가 제공하는 데이터는 A사 데이터베이스의 일부이며, 상당히 알려진 정보이기 때문에 B사의 행위가 A사의 이익에 부당하게 해친다고 보기 어렵다는 이유로 B사가 저작권법을 위배하지 않았다는 판결, B사의 크롤링 행동으로 A사가 정보처리 장애를 입었다는 주장에 대해 B사의 행동이 일부러 A사의 정보통신망에 부정한 명령을 하여 장애를 유발시켰다고 보기 어려워 범죄의 성립을 부정했습니다.

 

판례 참조 : 대법원 2022. 5. 12. 선고 2021도1533 판결

 

요약 및 돌이켜 말하면 크롤링을 하지말라고 회사내규에 명시되어있고, 로그인 같은 권한 보호장치가 있음에도 뚫고 크롤링을 과도하게 유발시키면서 데이터를 상업적으로 쓴다면... 소송입니다!

 

 

 

robots.txt" ??

 

웹로봇관련 국제 규약참고 http://www.robotstxt.org/

 

The Web Robots Pages

The Web Robots Pages Web Robots (also known as Web Wanderers, Crawlers, or Spiders), are programs that traverse the Web automatically. Search engines such as Google use them to index the web content, spammers use them to scan for email addresses, and they

www.robotstxt.org

 

 

 

여기서 Disallow: / 는 전체를 의미합니다. Allow /$ 는 메인페이지만 허용한다는 뜻이니, 메인 페이지 말고는 전체 로봇 접근을 금한다는 의미입니다. 그래서 tistory 같은 경우엔 로그인과 접근권한을 제외하고는 열어두었다고 볼 수 있어 구글에서 검색했을 때 티스토리 관련 블로그들이 많이 게제되는 경우가 많은 반면, 네이버블로그가 검색되는 일은 드문 경우가 이런 이유라고 추측됩니다.

 

모사이트는 크롤링 유저에 대해 선제적으로 IP밴 같은 접속 제한을 부여하기도 한다. 대인배 같이 크롤링을 허용하는 사이트들에 비해 이들의 행위를 너무 부당하다고 생각하지 않도록 하자.

 

[결론]

크롤링 이용자는 해당 서비스 약관을 준수하고, 상식적으로 해당 웹서비스에 무리가 가지 않을 정도로 가능하다는 것이고, 저작권법을 위배하지 않아야 한다는 것, 자신의 행위가 상대방 비즈니스에 위해를 가할 수 있는 행위임을 인지하자는 것.

 

서버 관리자는 크롤링에 대해 적절한 보안과 권한 조치를 취해야하며, 권리, 규약을 명확히 명시, robots.txt를 제공하여 크롤링 이용자들에게 명확한 허용범위를 안내할 수 있어야한다는 것이다.

 

 

감사합니다.

 

matplotlib는 설치 직후 상태에선 한글을 지원해주지 않는다.

 

그래서 우리는 matplotlib의 폰트에 한글 폰트를 추가해주고(1) 기존의 font 캐시를 삭제해주고(2), 기본 폰트를 한글 폰트로 설정해주는 작업(3)을 거쳐야한다.

 

(최종 결과물)

 

1. 우선은 venv 가상환경 matplotlib에 fonts 폴더에 우리가 추가하고자하는 한글폰트를 복사한다.

2. 그다음 user (사용자) 폴더에 있는 .matplotlib 폴더에서 fontlist-(~~).json 파일을 지워준다.

3. 그리고, 아까 가상환경 내의 matplotlib 폴더의 mpl-data 폴더에서 matplotlibrc 를 메모장으로 열어준 뒤, font.family를 검색하고, 사용하고자 하는 폰트 이름을 입력한다.

순서는 상관없다. 3가지 조건을 달성하면 기본으로 이제 한글이 적용된다.

 

다음은 pandas로 겪은 에러상황이다. 미세정보 데이터를 가져오기 위해 에어코리아(환경부 산하 한국환경공단)에서 필요로하는 대기정보를 엑셀로 저장했다.

 

 

 

PM10의 데이터는 ten이라고 prefix를 붙여주었다.

확장자가 xls지만 pandas의 read_excel 함수로 잘 열렸기에 전혀 문제될 것 없는 것처럼 보였다.

 

 

하지만, pandas 로도 열리지 않고, engine을 이것저것 바꾸어봐도 dataframe으로 열리지 않는 문제가 발생했다.

 

 

한참을 찾아보아도 방법이 없던 중에, 이곳을 통해 해결 방법에 대한 실마리를 얻었다.

https://maeng-gun.github.io/excel/excel1/#%EC%A7%81%EC%9E%A5%EC%9D%B8-%EC%BD%94%EB%94%A9%EC%9D%98-%EA%B1%B8%EB%A6%BC%EB%8F%8C-%EB%AC%B8%EC%84%9C%EB%B3%B4%EC%95%88drm 

 

사내망 엑셀파일에 걸린 자물쇠를 뚫고 코딩하기

엑셀 자동화 (1) - xlwings 라이브러리 시작하기

maeng-gun.github.io

 

xlwings도 안먹힘. -> 다른 spreadsheet 프로그램으로 저장한다 -> LibreOffice 설치 -> 새로 xlsx 로 저장해보는 시도 끝에 판다스로 dataframe 객체를 만드는데 성공했다!

 

굳이 libreoffice를 설치할 필요 없이 excel 파일의 metadata로 인한 문제인 것 같았다. 따라서, 필요한 데이터만 웹 스프레드시트로 옮겨서 따로 xlxs 파일로 저장해도 정상적으로 dataframe을 만들었다.

 

그래서 만든 데이터는 다음과 같다.

def get_avg_pollution_from_air_by_date():
    conn = sqlite3.connect("sample_bus_info_2.db")
    c = conn.cursor()
    result_list = c.execute("""
        select 
         avg(time_1) as '1',
         avg(time_2) as '2',
         avg(time_3) as '3',
         avg(time_4) as '4',
         avg(time_5) as '5',
         avg(time_6) as '6',
         avg(time_7) as '7',
         avg(time_8) as '8',
         avg(time_9) as '9',
         avg(time_10) as '10',
         avg(time_11) as '11',
         avg(time_12) as '12',
         avg(time_13) as '13',
         avg(time_14) as '14',
         avg(time_15) as '15',
         avg(time_16) as '16',
         avg(time_17) as '17',
         avg(time_18) as '18',
         avg(time_19) as '19',
         avg(time_20) as '20',
         avg(time_21) as '21',
         avg(time_22) as '22',
         avg(time_23) as '23',
         avg(time_24) as '24'
         from air_pollution where measured_type = 'pm2.5' and date = '20230428' group by date;
        """, ).fetchone()
    conn.commit()
    conn.close()

    return list(range(1,25)), result_list
    
def get_data_by_all_bus_transaction():
    conn = sqlite3.connect("sample_bus_info_2.db")
    c = conn.cursor()
    list_x = list()
    list_y = list()
    result_list = c.execute("""
        select measured_time, sum(transaction_count) from tb_bus where date = '20230428' group by date, measured_time;
        """).fetchall()
    for row in result_list:
        list_x.append(row[0])
        list_y.append(row[1])
    conn.close()

    return list_x, list_y

 

오늘도 데이터 시각화를 만들어보는 연습을 진행했습니다.

 

감사합니다.

 

 

요즘은 많은 공공기관에서 REST API를 제공하고 있다.

 

정식적으로 공공API를 사용하기 위해서는 활용신청 허가를 받아야한다.

 

https://www.data.go.kr/tcs/dss/selectApiDataDetailView.do?publicDataPk=15001106 

 

광주광역시 BIS 도착정보

광주광역시 BIS 도착정보에 대한 데이터를 제공하고 있습니다. 정류소 ID에 해당하는 정류소의 도착 정보 조회가 가능합니다.

www.data.go.kr

 

네이버로도 간편 회원가입을 할 수 있고, 휴대폰 인증을 거치면, 다음과 같이 간단히 신청을 할 수 있다.

 

참고 횟수는 데일리로 100회까지 지원이 되는 것 같다. 아직 정식으로 계속 필요한게 아니라서,

 

이정도로도 만족한다. (인증키가 공유되면 안되므로 가려놓음)

 

 

웹브라우저에서는 다음처럼 잘 불러와진다.

그리고 다음과 같이

import json
import requests
import pickle


def request_bus_info_from_api():
    service_key = 'TOP_SECRET'
    response = requests.get(f"http://api.gwangju.go.kr/json/arriveInfo?serviceKey={service_key}&BUSSTOP_ID=838")
    with open('temp_data.pickle', 'wb') as file:
        pickle.dump(response.content, file)


def load_bus_file():
    with open('temp_data.pickle', 'rb') as file:
        b_bus_stop_data = pickle.load(file)

    s_bus_stop_data = b_bus_stop_data.decode('utf-8')
    bus_stop_data = json.loads(s_bus_stop_data)
    return bus_stop_data


def get_bus_remain_info():
    bus_data = load_bus_file()
    result_list = list()
    for a_bus in bus_data['BUSSTOP_LIST']:
        result_list.append(str(a_bus['SHORT_LINE_NAME']))
        result_list.append(str(a_bus['REMAIN_STOP']))
        result_list.append(str(a_bus['REMAIN_MIN']))
    return result_list

버스 정보를 불러와서 피클로 저장해놓는 함수를 만들고,

import datetime
import sys

import PyQt5
import bus_info
from scratch_for_test import *


class MyBusInfoWindow(PyQt5.QtWidgets.QWidget, bus_info.Ui_MainWidget):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.refresh_count = 0
        self.init_pixmaps()
        self.init_labels_setting()
        self.init_timer()
        self.init_btn_mapping()
        self.label_status.hide()

    def init_btn_mapping(self):
        self.btn_refresh.clicked.connect(lambda state: self._clicked_refresh_btn())

    def init_pixmaps(self):
        self.green_good = PyQt5.QtGui.QPixmap('src/green_bus_good-removebg.png')
        self.green_doubt = PyQt5.QtGui.QPixmap('src/green_bus_doubt-removebg.png')
        self.green_worry = PyQt5.QtGui.QPixmap('src/green_bus_worry-removebg.png')
        self.yellow_good = PyQt5.QtGui.QPixmap('src/yellow_bus_good-removebg.png')
        self.yellow_doubt = PyQt5.QtGui.QPixmap('src/yellow_bus_doubt-removebg.png')
        self.yellow_worry = PyQt5.QtGui.QPixmap('src/yellow_bus_worry-removebg.png')

    def init_timer(self):
        self.inner_timer = PyQt5.QtCore.QTimer()
        self.inner_timer.setInterval(1000)  # 1초
        self.label_now_time.setText(self._get_now_time())
        self.inner_timer.timeout.connect(lambda: self.label_now_time.setText(self._get_now_time()))
        self.inner_timer.start()

        self.bus_refresh_timer = PyQt5.QtCore.QTimer()
        self.bus_refresh_timer.setInterval(1000 * 60)
        self._call_station_info()
        self.bus_refresh_timer.timeout.connect(lambda: self._call_station_info())
        self.bus_refresh_timer.timeout.connect(lambda: self.initialize_refresh_count())
        self.bus_refresh_timer.start()

    def _get_now_time(self):
        return datetime.datetime.now().strftime("%H:%M:%S")

    def initialize_refresh_count(self):
        self.refresh_count = 0
        self.label_status.hide()

    def init_labels_setting(self):
        self.frame_1_labels = list()
        self.frame_2_labels = list()
        self.frame_3_labels = list()
        self.frame_4_labels = list()
        self.frame_1_labels.append(self.frame_1_title)
        self.frame_1_labels.append(self.frame_1_time_left)
        self.frame_1_labels.append(self.frame_1_time_arrival)
        self.frame_1_labels.append(self.frame_1_time_left_title)
        self.frame_1_labels.append(self.frame_1_time_arrival_title)

        self.frame_2_labels.append(self.frame_2_title)
        self.frame_2_labels.append(self.frame_2_time_left)
        self.frame_2_labels.append(self.frame_2_time_arrival)
        self.frame_2_labels.append(self.frame_2_time_left_title)
        self.frame_2_labels.append(self.frame_2_time_arrival_title)

        self.frame_3_labels.append(self.frame_3_title)
        self.frame_3_labels.append(self.frame_3_time_left)
        self.frame_3_labels.append(self.frame_3_time_arrival)
        self.frame_3_labels.append(self.frame_3_time_left_title)
        self.frame_3_labels.append(self.frame_3_time_arrival_title)

        self.frame_4_labels.append(self.frame_4_title)
        self.frame_4_labels.append(self.frame_4_time_left)
        self.frame_4_labels.append(self.frame_4_time_arrival)
        self.frame_4_labels.append(self.frame_4_time_left_title)
        self.frame_4_labels.append(self.frame_4_time_arrival_title)

    def _call_station_info(self):
        request_bus_info_from_api()
        self.refresh_count += 1
        bus_info_list = get_bus_remain_info()
        for i in range(1, 5):
            for label in getattr(self, f'frame_{i}_labels'):
                label: PyQt5.QtWidgets.QLabel
                label.setText('')
        showing_size, temp = divmod(len(bus_info_list), 3)

        for i in range(1, showing_size + 1):
            temp_list = getattr(self, f'frame_{i}_labels')
            temp_list[0].setText(bus_info_list[(i - 1) * 3])
            temp_list[1].setText(bus_info_list[(i - 1) * 3 + 1] + ' 정거장')
            temp_list[2].setText(bus_info_list[(i - 1) * 3 + 2] + ' 분')
            temp_list[3].setText('남은 정류소')
            temp_list[4].setText('남은 도착시간')

        self._update_pixmap()

    def _clicked_refresh_btn(self):
        if self.refresh_count > 1:
            self.label_status.show()
            return
        else:
            self._call_station_info()

    def _update_pixmap(self):
        self.is_yellow_1 = self._assert_is_yellow_bus(1)
        self.is_yellow_2 = self._assert_is_yellow_bus(2)
        self.is_yellow_3 = self._assert_is_yellow_bus(3)
        self.is_yellow_4 = self._assert_is_yellow_bus(4)

        for i in range(1, 5):
            if getattr(self, f"frame_{i}_labels")[0].text() != '':
                if getattr(self, f"is_yellow_{i}") is True:
                    remain_time = int(getattr(self, f"frame_{i}_labels")[2].text()[:2].rstrip())
                    if remain_time > 8:
                        getattr(self, f"frame_{i}_img").setPixmap(self.yellow_good)
                    elif remain_time > 4:
                        getattr(self, f"frame_{i}_img").setPixmap(self.yellow_worry)
                    else:
                        getattr(self, f"frame_{i}_img").setPixmap(self.yellow_doubt)
                else:
                    remain_time = int(getattr(self, f"frame_{i}_labels")[2].text()[:2].rstrip())
                    if remain_time > 8:
                        getattr(self, f"frame_{i}_img").setPixmap(self.green_good)
                    elif remain_time > 4:
                        getattr(self, f"frame_{i}_img").setPixmap(self.green_worry)
                    else:
                        getattr(self, f"frame_{i}_img").setPixmap(self.green_doubt)
            self._assert_is_blank_cell_and_clear(i)

    def _assert_is_yellow_bus(self, index):
        if '봉선' in getattr(self, f'frame_{index}_labels')[0].text():
            return True
        else:
            return False

    def _assert_is_blank_cell_and_clear(self, index):
        if getattr(self, f'frame_{index}_labels')[0].text() == '':
            getattr(self, f'frame_{index}_img').clear()


if __name__ == '__main__':
    app = PyQt5.QtWidgets.QApplication(sys.argv)
    myWindow = MyBusInfoWindow()
    myWindow.show()
    app.exec_()

Qt에 적용시켜주는 로직을 만들어주면?

 

이런식으로 파일이 만들어진다.

 

그리고 간단하게 exe로 실행시킬 수 있게, pyinstaller를 적용해주면,

 

 

매번 pycharm을 실행시킬 필요 없이, 바탕화면에서도 버스 정보를 간단하게 불러올 수 있게 된다.

 

레전드오브복이 프로젝트 전부터 조금씩 진행중이던 개인프로젝트 였는데, 연휴동안 큰 탈 없이 마무리되었다.

 

이제 버스도착정보를 미리 보고 떠날 수 있으니, 시간낭비가 줄어들 것 같다.

 

직접 만든 프로그램을 유용하게 써먹을 수 있게 되어 기분이 좋다.

 

감사합니다.

 

 

 

오늘은 데이터 다루기 연습을 해보았다.

 

목표 : csv 또는 excel 형태의 공공데이터를 pandas로 불러와 sqlite3로 db에 저장한다.

db에 저장해둔 데이터를 query를 이용해 필요한 데이터를 불러온다.

 

타겟 : https://www.data.go.kr/data/15088456/fileData.do

 

광주광역시_시내버스 노선별 승하차 인원 정보_20230430

광주광역시 내 시내버스 승하차 인원정보에 대한 데이터로 일자별, 노선명, 정류장명, 시간별, 승하차별 거래건수를 제공합니다.

www.data.go.kr

나는 필요이상의 데이터를 요하지 않아 4월 27일 ~ 4월 30일 4일간의 데이터만 불러왔다. (그래도 64999 rows)

 

import pickle

import pandas as pd


# test_file = pd.read_excel('bus_info.xlsx')
#
# with open('bus_info_as_byte', 'wb') as file:
#     pickle.dump(test_file, file)

def open_pickle_return_dataframe():
    with open('bus_info_as_byte', 'rb') as file:
        load_bus_info = pickle.load(file)
    load_bus_info: pd.DataFrame
    return load_bus_info
# '일자', '노선명', '정류장명', 'ARS_ID', '시간', '승하차', '거래건수'


if __name__ == '__main__':
    df = open_pickle_return_dataframe()
    print(df.index)

 

우선 pandas로 불러들인 object를 pickle을 이용해 byte로 저장했다.

 

그렇지 않으면 반복적으로 읽을 때 너무 느리다.

 

import sqlite3
import open_xlsx


def create_table():
    conn = sqlite3.connect('bus_info.db')
    c = conn.cursor()
    # '일자', '노선명', '정류장명', 'ARS_ID', '시간', '승하차', '거래건수'
    c.execute("drop table mybus_info")
    c.execute("""
        CREATE TABLE mybus_info (
                  id INTEGER PRIMARY KEY AUTOINCREMENT, 
                  date text, 
                  bus_no text,
                  bus_stop_name text,
                  bus_stop_id int,
                  measured_time text,
                  boarding_type text,
                  transaction_count int
          )
    """)
    conn.commit()
    conn.close()


def fetch_all():
    conn = sqlite3.connect('bus_info.db')
    c = conn.cursor()
    for i in c.execute('select * from mybus_info').fetchall():
        print(i)
    conn.close()


def insert_all():
    conn = sqlite3.connect('bus_info.db')
    c = conn.cursor()
    df = open_xlsx.open_pickle_return_dataframe()
    df_filled = df.fillna(0)
    for idx in df.index:
        temp = (
            str(df_filled.iloc[idx][0]), df_filled.iloc[idx][1], df_filled.iloc[idx][2], int(df_filled.iloc[idx][3]),
            str(df_filled.iloc[idx][4]), df_filled.iloc[idx][5],
            int(df.iloc[idx][6]))
        c.execute("""
            insert into mybus_info 
            (date,bus_no,bus_stop_name,bus_stop_id,measured_time,boarding_type,transaction_count)
            values (?,?,?,?,?,?,?)
        """, temp)
    conn.commit()
    conn.close()


def select_busy_bus_stop(number):
    if not isinstance(number, int):
        raise
    conn = sqlite3.connect('bus_info.db')
    c = conn.cursor()
    for i in c.execute('select * from mybus_info where transaction_count > (?)', (number,)).fetchall():
        print(i)
    conn.close()


if __name__ == '__main__':
    select_busy_bus_stop(100)

 

이후 db를 생성해주는 함수와 dataframe에서 불러들여온 정보를 하나씩 insert 해주는 함수를 만들었다.

 

중간에 NAN으로 승강장이 들어가있지 않는 cell들이 있어 그런 경우엔 '0'으로 채웠다.

 

또한 query를 날려 승하차 건수가 1시간동안 100건이 넘는 정보를 출력하도록 함수를 하나 만들었다.(select_busy_bus_stop)

 

(8360, '20230427', '금남57', '0', 0, '17', '승차', 108)
(8680, '20230427', '금남58', '동림삼익아파트', 4148, '7', '승차', 107)
(25696, '20230427', '마을777', '전남대용봉탑', 4663, '14', '승차', 106)
(25699, '20230427', '마을777', '전남대용봉탑', 4663, '16', '승차', 137)
(36927, '20230427', '문흥18', '전남대공과대학', 4411, '16', '승차', 113)
(48867, '20230427', '봉선37', '호남대', 5265, '15', '승차', 165)
(48868, '20230427', '봉선37', '호남대', 5265, '16', '승차', 137)

 

데이터를 분석해보면, 목요일 14시, 16시 전남대 용봉탑과 공과대학쪽, 호남대는 대학생들이 많이 버스를 이용하고 있다는 것을 알았다.

 

하지만, 이는 옳지 않은 해석이다. 단일 버스 노선의 데이터이기 때문에, 정류장을 기준으로 다시 카운트해줘야하는데..

 

당분간 계속 조작하면서 이것저것 다뤄봐야겠다.

 

또한 깊은 고민 발생, 이것을 어떻게 시각화할 수 있을까..??

 

감사합니다.

 

집에서 홀로 공부하면서 코드리뷰도 하고, 그동안 파이썬 공부하면서 의구심이 생겼던 부분에 대해서 알아보는 시간을 가졌습니다.

 

저는 공부하는 스타일이 "왜 그러지?" 라는 물음에서 시작해서, 다양한 자료 습득을 시행하고, 간단하게 요점정리하여

 

나중에 실제 꺼내보는 일은 드물지만 빠르게 요점을 리마인드 할 때 쓰기 위해 파워포인트로 정리해놓는 것을 선호합니다.

 

그리하여 자세한 설명은 접어두고, 정리된 포인트 몇장 올리고 마무리하겠습니다!

 

다른 분들도 흥미롭게 보실 수 있는 소재라고 생각되니 꼭 읽어보시길 권장합니다.

 

감사합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
감사합니다.

 

 

 

 

'노리끼리-노르스름-연노랑-누런-샛노랑-노랑-노릇노릇-진노랑-쩐노랑'

 

노란색을 부르는 이름도 다양도 하다. 자주 쓰는 색깔도 이렇게 부르는 이름이 다양한데,

 

보이지 않는 변수, 함수의 이름은 어떻게 불러야할까?

 

 

 

 

슬슬 협동 플레이를 해야하는데...

 

내 맘대로 클래스, 함수, 변수명을 지으면 동료들이 알아볼 수 있을까??

 

 

 

(가정을 해본다)

 

로직이 마음대로 풀리지 않는다. 담당 사수에게 내가 짠 코드를 보여주고 왜 안풀리는지 물어보고 싶다.

 

그런데, "얘는 뭘 뜻하는거야?" "이건 뭘 의미하는거야?"...

 

하루종일 스무고개만 하다가 그냥 처음부터 다시 짜오라고 시킨다.

 

 

어디서 잘못된 걸까??

 

문제의 근본은 내 맘대로 짠 코드들이다.

 

https://peps.python.org/pep-0008/

 

PEP 8 – Style Guide for Python Code | peps.python.org

PEP 8 – Style Guide for Python Code Author: Guido van Rossum , Barry Warsaw , Nick Coghlan Status: Active Type: Process Created: 05-Jul-2001 Post-History: 05-Jul-2001, 01-Aug-2013 Table of Contents This document gives coding conventions for the Python co

peps.python.org

 

 

PEP8 Python Enhancement Proposal 8의 줄임말이다.

 

이곳에서는 내맘대로 짠 코드들(=일관성이 떨어지는)은 작은 홉고블린이라고 표현하고 있다.

 

또한, 일관성 있는 코드들에 대한 중요성을 설파하는데 그 이유는 다음과 같다.

 

* 코드가 쓰여지는 것보다 훨씬 더 자주 읽힌다.

* (PEP8을 준수하면) 가독성을 향상시키고 Python 코드의 광범위한 스펙트럼에서 일관성을 유지하도록 의도한다.

* 스타일 가이드는 일관성이다. 여기 가이드를 따라해는 것은 매우 중요하다.

* 그러나 때로는 일관성을 유지하지 않을 때도 있다.

* PEP8을 준수할지, 기존의 스타일을 유지할지 판단은 당신의 몫, 어떤 것이 가장 좋아 보이는지 판단하라.

* 기존에 있는게 더 읽기 쉬울수도 있으니깐, 기존에 이미 있는걸 PEP8 맞춘다고 다 갈아엎진 마라.

 

 

그럼 도대체 어떻게 하라고?

 

대부분은 Pycharm에서는 Ctrl + Alt + L 만 누르면 된다.

 

그래서 오늘 요약 정리하려는 것은 변수명, 함수명, 클래스명 정도이다.

 

변수명은 다음과 같이 정해라.

Variable names should be lowercase, with words separated by underscores.

변수명은 반드시 소문자를 써라. 띄어쓰기는 언더스코어를 써라.

 

If a variable name needs to be composed of multiple words, the recommended style is to use underscores between the words, rather than camelCase or mixedCase.

만약 변수명이 여러단어로 조합해야할 필요가 있다면, 카멜케이스 기법이나 다른 것 보단 언더스코어를 써라.

 

Variable names should be descriptive and indicative of the variable's purpose.

변수명은 변수의 목적을 암시, 내포하게 정해라.

 

Single-character variable names should be avoided except for certain cases like looping variables.

반복용으로 쓰이는 변수아니면, 외자 변수명 쓰지마라.

(a.k.a a=10)

 

Constants should be named using all capital letters, with words separated by underscores.

안변하는 수(상수) 선언할 때는 전부 대문자로 쓰고 띄어쓰기는 언더스코어 써라.

 

 

함수명은 다음과 같이 정해라.

Function names should be lowercase, with words separated by underscores.

함수는 모두 소문자, 띄어쓰기는 언더스코어 써라.

 

Function names should be descriptive and convey the purpose of the function.

함수의 이름은 함수의 목적이 충분히 설명되게 정해라.

 

If a function name consists of multiple words, each word should start with a capital letter except for the first word. This is called CamelCase or PascalCase.

여러 단어로 조합된 단어로 구성됧 경우는 첫번째만 소문자 시작하고 카멜 케이스로 작성해라.

 

Function names should not be too long or too short. A good guideline is to aim for names that are between 3-20 characters in length.

함수 이름 너무 길게도, 짧게도 쓰지 마라.

 

Avoid using single letter names or abbreviations that are not widely known or understood.

한글자 함수나 아무도 모르겠는 축약어로 함수명 쓰지 마라.

 

If a function is intended for internal use only, start the name with an underscore.

함수를 내부적으로만 쓰고 싶으면, 앞에 언더스코어를 붙여라.

 

If a function is intended to be used as a class method, start the name with a lowercase letter.

클래스 매소드로만 쓸꺼면, 맨 앞글자 소문자로 적어라.

 

# Function name in lowercase with words separated by underscores
def calculate_average(numbers_list):
    pass

# Function name in CamelCase
def calculateAverage(numbersList):
    pass

# Function name in PascalCase
def CalculateAverage(NumbersList):
    pass

# Function intended for internal use only
def _helper_function(input_data):
    pass

# Function intended to be used as a class method
def add_item(self, item):
    pass

 

 

클래스명은 다음과 같이 정해라.

 

Class names should normally use the CapWords convention. This means that the first letter of each word in the name is capitalized, and there are no underscores between words.

클래스 네이밍은 캡워드가 근본이다. 띄워쓰기 대신 첫글자 대문자 써라.

 

Exception classes should end in "Error".

에러클래스 이름은 마지막에 에러라고 붙여라.

 

Names of classes that are meant to be used as interfaces (i.e., subclassed but not instantiated directly) should end in "ABC" (which stands for "Abstract Base Class").

인터페이스로 쓸 용도의 클래스는 뒤에 ABC 붙여라.

 

Private class attributes and methods should start with a single underscore. For example, _internal_method().

접근제한 클래스의 속성과 함수는 언더스코어 붙여라.

 

클래스 예제

 

class MyClass:
    pass

class MyExceptionError(Exception):
    pass

class MyInterfaceABC(metaclass=ABCMeta):
    @abstractmethod
    def my_method(self):
        pass

class _MyPrivateClass:
    pass

감사합니다.

+ Recent posts