ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • KOSPI 200 크롤링
    주식 2020. 11. 30. 19:52
    반응형

    네이버 주식에서 KOSPI200 데이터를 크롤링하는 방법을 정리해 보았습니다.

    HTS나 증권 포털에서 볼 수 있지만 좀 더 복잡한 분석을 위해서는 HTS나 증권 포탈에서 제공되는 데이터로는 많이 부족합니다. 매일 KOSPI 상위 200개 종목의 데이터를 클롤링하여 저장해두고 분석하여 장기 투자할 종목을 선정해보려고 합니다.

    크롤링 환경 구성

    - CentOS 8

    - Python 3.6

    - PostgreSQL 12.4

    파이썬 관련 패키지

    - selenium==3.141.0

    - beautifulsoup4==4.9.1

    크롤링한 데이터를 저장할 데이터 테이블을 생성합니다. Schema는 아래와 같습니다.

    DROP TABLE IF EXISTS kospi_stock;
    CREATE TABLE IF NOT EXISTS kospi_stock (
        code varchar(100) not null,
        name varchar(100) not null,
        inserted_date date not null default CURRENT_DATE,
        updated_date date not null default CURRENT_DATE
    );
    
    COMMENT ON COLUMN kospi_stock.code IS '종목코드';
    COMMENT ON COLUMN kospi_stock.name IS '종목명';
    COMMENT ON COLUMN kospi_stock.inserted_date IS '저장날짜';
    COMMENT ON COLUMN kospi_stock.updated_date IS '업데이트날짜';
    
    DROP INDEX IF EXISTS kospi_stock_idx;
    CREATE UNIQUE INDEX IF NOT EXISTS kospi_stock_idx on kospi_stock (
        code,
        name
    );
    
    DROP TABLE IF EXISTS kospi_stock_raw;
    CREATE TABLE IF NOT EXISTS kospi_stock_raw (
        code varchar(100) not null,
        price numeric not null,
        diff numeric not null,
        fractuation_rate numeric not null,
        volume numeric not null,
        trading_value numeric not null,
        market_capitalization numeric not null,
        trade_date date not null default CURRENT_DATE,
        UNIQUE(code, trade_date)
    );
    
    COMMENT ON COLUMN kospi_stock_raw.code IS '종목코드';
    COMMENT ON COLUMN kospi_stock_raw.price IS '현재가';
    COMMENT ON COLUMN kospi_stock_raw.diff IS '전일비';
    COMMENT ON COLUMN kospi_stock_raw.fractuation_rate IS '등락률';
    COMMENT ON COLUMN kospi_stock_raw.volume IS '거래량';
    COMMENT ON COLUMN kospi_stock_raw.trading_value IS '거래대금';
    COMMENT ON COLUMN kospi_stock_raw.market_capitalization IS '시가총액';
    COMMENT ON COLUMN kospi_stock_raw.trade_date IS '거래날짜';
    
    DROP INDEX IF EXISTS kospi_stock_raw_idx;
    CREATE INDEX IF NOT EXISTS kospi_stock_raw_idx on kospi_stock_raw (
        code,
        trade_date
    );

    kospi_stock에는 상위 200개 항목의 기준 정보가 kospi_stock_raw에는 daily로 crawling한 데이터가 저장될 예정입니다. 생성이 완료되었으면 stock.yaml 파일을 생성하고 값을 설정합니다. 이 파일은 DB 접속 정보와 데이터 저장을 위한 UPSERT 쿼리를 포함하고 있습니다.

    stock:
      conn_info:
        host: 'localhost'
        port: 10864
        name: 'stock'
        user: '*********'
        pass: '*********'
      kospi200_stock:
        # Naver Crawling 페이지
        page_url: https://finance.naver.com/sise/entryJongmok.nhn?&page={page}
        # 페이지 갯수
        page_limit: 20
        # 기준정보 저장 쿼리
        upsert_kospi_stock:
          INSERT INTO kospi_stock(code, name)
          VALUES (%(code)s, %(name)s)
            ON CONFLICT(code, name)
              DO UPDATE SET updated_date = CURRENT_DATE
        # Raw Data 저장 쿼리
        upsert_kospi_stock_raw:
          INSERT INTO kospi_stock_raw(code, price, diff, fractuation_rate, volume, trading_value, market_capitalization, trade_date)
          VALUES (%(code)s, %(price)s, %(diff)s, %(fractuation_rate)s, %(volume)s, %(trading_value)s, %(market_capitalization)s, CURRENT_DATE)
            ON CONFLICT(code, trade_date)
              DO UPDATE
                SET code = %(code)s, price = %(price)s,
                  diff = %(diff)s, fractuation_rate = %(fractuation_rate)s, volume = %(volume)s,
                  trading_value = %(trading_value)s, market_capitalization = %(market_capitalization)s

    준비가 완료되었으면 본격적으로 Crawling 코드를 작성해보겠습니다. 사용할 패키지를 임포트합니다.

    import os
    import re
    import yaml
    
    import psycopg2 as pg2
    
    from datetime import date
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys
    from bs4 import BeautifulSoup
    

    yaml 파일에 있는 설정값과 Crawling을 위한 WebDriver를 초기화합니다.

    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    options.add_argument('window-size=1024x768')
    options.add_argument('disable-gpu')
    
    print("\tInit chrome driver")
    driver = webdriver.Chrome('/usr/local/bin/chromedriver', chrome_options=options)
    driver.implicitly_wait(1)
    
    with open(r'config/stock.yaml') as file:
        stock_config = yaml.load(file)['stock']
    
    conn_info = stock_config['conn_info']
    
    print("\tConnect stock_db")
    conn = pg2.connect(host=conn_info['host'], database=conn_info['name'], user=conn_info['user'],
        password=conn_info['pass'], port=conn_info['port'])
    cur = conn.cursor()
    
    kospi200_stock = stock_config['kospi200_stock']
    page_limit = kospi200_stock['page_limit']
    page_url = kospi200_stock['page_url']
    upsert_kospi_stock = kospi200_stock['upsert_kospi_stock']
    upsert_kospi_stock_raw = kospi200_stock['upsert_kospi_stock_raw']

     

    네이버의 주식의 KOSPI200 https://finance.naver.com/sise/sise_index.nhn?code=KPI200 URL에서 확인 가능하고 "편입상위 종목"에서 페이지당 10개의 항목이 20페이지로 구분하여 표시해주는걸 확인 할 수 있습니다.

     

    이제 루프를 돌면서 값을 읽어오도록 합니다. 먼저 테이블의 body를 읽어오는 작업에서 시작합니다. 아래 코드에서 주석이 달려진 테이블 읽어오기에서 불러올수 있으며 헤더는 스킵합니다. 테이블에서 읽어온 Row에서 루프를 돌면서 Column 값을 가져옵니다.. 크게 어려운 부분은 없으며 전일비 값의 경우 text로만 가져올 경우 +, - 구분이 불가능하므로 추가 HTML 코드를 분석하여 ico 이름에서 Up, Down을 분석하여 + 또는 - 값을 처리하였습니다. 그리고 항목 코드도 HTML 코드에 링크로 제공되기 때문에 숫자 6개 값을 불러오는 정규식으로 대체하였습니다.

    Crawling으로 읽어온 데이터는 각 항목별로 변수로 분리하여 최종 upsert_kospi_stock, upsert_kospi_stock_raw에 저장됩니다.

    index = 0
    reg_code = re.compile('[0-9]{6}')
    reg_diff = re.compile('ico_(up|down).gif')
    print("\t\tGetting kospi top 200 data...")
    for i in range(1, page_limit + 1):
        driver.get(page_url.format(page=i))
        tbody = driver.find_elements_by_xpath('/html/body/div/table[1]/tbody') # 테이블 읽어오기
        trs = tbody[0].find_elements_by_xpath('./tr')[2:-2] # 헤더는 스킵한다.
        for _, tr in enumerate(trs):
            tds = tr.find_elements_by_xpath('./td')
            index += 1
            code = reg_code.search(tds[0].get_attribute('innerHTML')).group()
            name = tds[0].text
            price = tds[1].text
            diff = float(tds[2].text.replace(",", ""))
            ico = reg_diff.search(tds[2].get_attribute('innerHTML'))
            if ico is not None and ico.group(1) == "down":
                diff = diff * -1
            fractuation_rate = tds[3].text
            volume = tds[4].text
            trading_value = tds[5].text
            market_capitalization = tds[6].text
            cur.execute(upsert_kospi_stock, {
                'code': code,
                'name': name
            })
            cur.execute(upsert_kospi_stock_raw, {
                'code': code,
                'price': price.replace(",", ""),
                'diff': diff,
                'fractuation_rate': fractuation_rate.replace("%", ""),
                'volume': volume.replace(",", ""),
                'trading_value': trading_value.replace(",", ""),
                'market_capitalization': market_capitalization.replace(",", "")
            })
        conn.commit()

    전체 코드는 아래에서 확인 가능합니다. Github로 제공하려고 하였으나 패스워드 처리를 원활하게 하기 힘들어 코드 전체를 공개하기로 결정하였습니다.

    import os
    import re
    import yaml
    
    import psycopg2 as pg2
    
    from datetime import date
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys
    from bs4 import BeautifulSoup
    
    def kospi200_stock():
    
        driver = None
        cur = None
        conn = None
    
        try:
            options = webdriver.ChromeOptions()
            options.add_argument('headless')
            options.add_argument('window-size=1024x768')
            options.add_argument('disable-gpu')
    
            print("\tInit chrome driver")
            driver = webdriver.Chrome('/usr/local/bin/chromedriver', chrome_options=options)
            driver.implicitly_wait(1)
    
            with open(r'config/stock.yaml') as file:
                stock_config = yaml.load(file)['stock']
    
            conn_info = stock_config['conn_info']
    
            print("\tConnect stock_db")
            conn = pg2.connect(host=conn_info['host'], database=conn_info['name'], user=conn_info['user'],
                password=conn_info['pass'], port=conn_info['port'])
            cur = conn.cursor()
    
            kospi200_stock = stock_config['kospi200_stock']
            page_limit = kospi200_stock['page_limit']
            page_url = kospi200_stock['page_url']
            upsert_kospi_stock = kospi200_stock['upsert_kospi_stock']
            upsert_kospi_stock_raw = kospi200_stock['upsert_kospi_stock_raw']
    
            index = 0
            reg_code = re.compile('[0-9]{6}')
            reg_diff = re.compile('ico_(up|down).gif')
            print("\t\tGetting kospi top 200 data...")
            for i in range(1, page_limit + 1):
                driver.get(page_url.format(page=i))
                tbody = driver.find_elements_by_xpath('/html/body/div/table[1]/tbody')  #  페이지 읽어오기
                trs = tbody[0].find_elements_by_xpath('./tr')[2:-2]
                for _, tr in enumerate(trs):
                    tds = tr.find_elements_by_xpath('./td')
                    index += 1
                    code = reg_code.search(tds[0].get_attribute('innerHTML')).group()
                    name = tds[0].text
                    price = tds[1].text
                    diff = float(tds[2].text.replace(",", ""))
                    ico = reg_diff.search(tds[2].get_attribute('innerHTML'))
                    if ico is not None and ico.group(1) == "down":
                        diff = diff * -1
                    fractuation_rate = tds[3].text
                    volume = tds[4].text
                    trading_value = tds[5].text
                    market_capitalization = tds[6].text
                    cur.execute(upsert_kospi_stock, {
                        'code': code,
                        'name': name
                    })
                    cur.execute(upsert_kospi_stock_raw, {
                        'code': code,
                        'price': price.replace(",", ""),
                        'diff': diff,
                        'fractuation_rate': fractuation_rate.replace("%", ""),
                        'volume': volume.replace(",", ""),
                        'trading_value': trading_value.replace(",", ""),
                        'market_capitalization': market_capitalization.replace(",", "")
                    })
                conn.commit()
        finally:
            if (driver):
                driver.quit()
            if (cur):
                cur.close()
            if (conn):
                conn.close()
    
    def main():
        print("kospi200_stock")
        kospi200_stock()
    
    if __name__ == "__main__":
    	main()

    주식과 파이썬 전문가가 아니라 모호한 변수 또는 코드가 있을 수 있습니다. 참고용으로 사용하시면 됩니다.

    반응형
Designed by Tistory.