Side Projects/공모주 알리미

[공모주 알리미 개발] 2-4. 38커뮤니케이션 크롤링 : 기업 개요, 공모 정보, 청약 일정 가져오기

hzoooo 2021. 11. 23. 00:55

이번 포스팅은 이전 포스팅에서 공모주 알리미로 뿌려줄(청약 전일, 당일, 마감일 / 상장 전일, 당일) 정보들이 담긴 url들을 이용하여,

공모주 투자에 도움이 되는 정보들을 가져오면 된다.

 

<이전 포스팅>

 

[공모주 알리미 개발] 2-3. 38커뮤니케이션 크롤링 : 종목별 url 추출하기(수정)

저번 글 이후, 두 달이 넘어서 다시 공모주 알리미 개발 포스팅을 하게 됐다. 개발은 열심히 해서 약 30명에게 서비스 중이다. 개발할 때 그때그때 글을 썼으면 더 좋았으련만.ㅎㅎ 글 쓰는데 시

hzoo.tistory.com

 

ipostock에서 1차적으로 정보를 가져오지만 ipostock에서 가끔 늦게 업데이트되는 이유로 38 커뮤니케이션을 크롤링하기 때문에,

전체 정보가 아닌, 필요한 정보만 가져오면 된다.

 

청약 일정(특히 상장일), 확정 공모가, 기업 수요예측결과(기관 경쟁률, 의무보유확약 비율)가 늦게 나오기 때문에,

double check를 위한,

  - 종목명

  - 일정 관련 정보(공모 청약 시작, 마감, 환불일, 상장일)

  - 금액 관련 정보(공모가 밴드 하단, 상단, 확정 공모가, 공모 규모)

  - 신주 모집 주식수 및 비율

  - 수요예측 결과 정보(기관 경쟁률, 의무보유확약 비율)

를 크롤링 하려고 한다.

 

오늘을 예로 들면, 대신밸런스스팩11호, 미래에셋글로벌리츠, 신한서부티엔디리츠 세개의 url을 청약 정보로 가져온다.

위에 표기해둔 정보는 한 url에 모두 담겨있으므로(ipostock과 다르게), 먼저 html 코드를 가져온다.

def parsing_html(url):
    response = requests.get(url)
    html = response.content.decode('euc-kr', 'replace')
    return BeautifulSoup(html, 'lxml')

 

 

이후, 차례대로 정보들을 가져오면 된다.

 

 

<1. 종목명 가져오기>

더보기
사진1. [종목명 가져오기] : '기업개요' summary table

 

 

def crawl_company_name(url):
    soup = parsing_html(url)
    
    table = soup.find('table', {'summary': '기업개요'})
    tr = table.find_all('td')[1]
    company_name = tr.text.strip()
    return company_name

 

 

<2. 일정 관련 정보 가져오기>

더보기
사진2. [일정 관련 정보] : '공모청약일정' summary table

 

def crawl_ipo_date(url):
    ipo_date = IpoDate()
    soup = parsing_html(url)
    
    table = soup.find('table', {'summary': '공모청약일정'})
    tr = table.find_all('tr')[1:6]
    del date_tr_list[1:3]
    date_td_list = [tr.find_all('td')[1].text.replace('\xa0', '').replace(' ', '') for tr in date_tr_list]
    
    ipo_date.bidding_start, ipo_date.bidding_finish = date_td_list[0].split('~')
    ipo_date.refund = date_td_list[1]
    ipo_date.go_public = date_td_list[2]
    
    return ipo_date

 

 

<3. 금액 관련 정보 가져오기>

더보기
사진3. [금액 관련 정보 가져오기] : '공모정보' summary table

 

 

def crawl_ipo_price(url):
    ipo_price = IpoPrice()
    soup = parsing_html(url)
    
    table = soup.find('table', {'summary': '공모정보'})
    price_tr_list = table.find_all('tr')[2:4]

    band_price_range_td = price_tr_list[0].find_all('td')[1].text
    band_price_range = band_price_range_td.replace('\xa0', '').replace(' ', '').replace('원', '').replace(',', '')

    band_price_low, band_price_high = band_price_range.split('~')

    offering_price_td = price_tr_list[1].find_all('td')[1].text.replace('-', '0')
    offering_price = offering_price_td.replace('\xa0', '').replace(' ', '').replace('원', '').replace(',', '')

    offering_amount_td = price_tr_list[1].find_all('td')[3].text
    offering_amount = offering_amount_td.replace('\xa0', '').replace(' ', '').replace('(백만원)', '').replace(',', '')

    ipo_price.band_price_low = band_price_low
    ipo_price.band_price_high = band_price_high
    ipo_price.offering_price = offering_price
    ipo_price.offering_amount = str(int(offering_amount) // 100)

    return ipo_price

 

 

 

<4. 신주 모집 주식수 및 비율 가져오기>

더보기
사진4. [신주 모집 주식수 및 비율 가져오기] : '공모정보' summary table

 

 

def crawl_ipo_new_stocks_info(url):
    ipo_new_stocks_info = IpoNewStocksInfo()
    soup = parsing_html(url)
    
    table = soup.find('table', {'summary': '공모정보'})
    new_stock_info_tr_list = table.find_all('tr')[:2]

    total_num_of_new_stocks_td = new_stock_info_tr_list[0].find_all('td')[1].text
    total_num_of_new_stocks = total_num_of_new_stocks_td.replace('주', '').replace(' ', '').replace(',', '').strip()

    temp_td = new_stock_info_tr_list[1].find_all('td')[1].text.split('/')[0]
    ratio_of_new_stocks = temp_td.split('(')[1]
    ratio_of_new_stocks = ratio_of_new_stocks.replace(' ', '').replace('%)', '').strip()

    ipo_new_stocks_info.total_num_of_new_stocks = total_num_of_new_stocks
    ipo_new_stocks_info.ratio_of_new_stocks = ratio_of_new_stocks

    return ipo_new_stocks_info

 

 

<5. 수요예측 결과 정보 가져오기>

더보기
사진5. [수요예측 결과 정보 가져오기] : '공모청약일정' summary table

 

 

def crawl_ipo_demand_forecast(url):
    ipo_demand_forecast = IpoDemandForecast()
    soup = parsing_html(url)
    
    table = soup.find('table', {'summary': '공모청약일정'})
    demand_forecast_rows_tr = table.select('table > tr')[19].find_all('td')[1::2]
    competition_ratio = demand_forecast_rows_tr[0].text.replace(':1', '').strip()
    commitment_ratio = demand_forecast_rows_tr[1].text.replace('0.00%', '').replace('%', '').strip()

    ipo_demand_forecast.competition_ratio = competition_ratio if any(competition_ratio) else 0
    ipo_demand_forecast.commitment_ratio = commitment_ratio if any(commitment_ratio) else 0

    return ipo_demand_forecast

 

위 소스 코드들에 여러 클래스들을 정의해뒀는데, 클래스 소스코드는 바로 아래줄 더보기에 첨부하였다.

더보기
class IpoDate:
    def __init__(self):
        self.bidding_start = None
        self.bidding_finish = None
        self.refund = None
        self.go_public = None


class IpoPrice:
    def __init__(self):
        self.band_price_low = None
        self.band_price_high = None
        self.offering_price = None
        self.offering_amount = None


class IpoNewStocksInfo:
    def __init__(self):
        self.total_num_of_new_stocks = None
        self.ratio_of_new_stocks = None


class IpoStockConditions:
    def __init__(self):
        self.total_num_of_stock_after_ipo = None
        self.num_of_stock_lockup = None
        self.num_of_stock_sale_available = None
        self.ratio_of_lockup = None
        self.ratio_of_sale_available = None


class IpoUnderwriter:
    def __init__(self):
        self.underwriter = None
        self.num_of_stock_allocated = None


class IpoDemandForecast:
    def __init__(self):
        self.competition_ratio = None
        self.commitment_ratio = None


class IpoData(IpoDate, IpoPrice, IpoNewStocksInfo, IpoStockConditions, IpoUnderwriter, IpoDemandForecast):
    def __init__(self, company_name):
        self.company_name = company_name
        self.public_offering_page_url = None
        self.stock_holder_page_url = None
        self.demand_forecast_page_url = None
        self.ref_url_ipo_stock = None
        self.ref_url_38com = None
        self.is_from_KONEX = False
        IpoDate.__init__(self)
        IpoPrice.__init__(self)
        IpoNewStocksInfo.__init__(self)
        IpoStockConditions.__init__(self)
        IpoUnderwriter.__init__(self)
        IpoDemandForecast.__init__(self)

    def set_public_offering_page_url(self, url):
        self.public_offering_page_url = url
        self.stock_holder_page_url = self.public_offering_page_url.replace('_04', f'_02')
        self.demand_forecast_page_url = self.public_offering_page_url.replace('_04', f'_05')

    def set_ipo_date(self, ipo_date: IpoDate):
        self.bidding_start = ipo_date.bidding_start
        self.bidding_finish = ipo_date.bidding_finish
        self.refund = ipo_date.refund
        self.go_public = ipo_date.go_public

    def set_ipo_price(self, ipo_price: IpoPrice):
        self.band_price_low = ipo_price.band_price_low
        self.band_price_high = ipo_price.band_price_high
        self.offering_price = ipo_price.offering_price
        self.offering_amount = ipo_price.offering_amount

    def set_ipo_new_stocks_info(self, ipo_new_stocks_info: IpoNewStocksInfo):
        self.total_num_of_new_stocks = ipo_new_stocks_info.total_num_of_new_stocks
        self.ratio_of_new_stocks = ipo_new_stocks_info.ratio_of_new_stocks

    def set_ipo_stock_conditions(self, ipo_stock_conditions: IpoStockConditions):
        self.total_num_of_stock_after_ipo = ipo_stock_conditions.total_num_of_stock_after_ipo
        self.num_of_stock_lockup = ipo_stock_conditions.num_of_stock_lockup
        self.num_of_stock_sale_available = ipo_stock_conditions.num_of_stock_sale_available
        self.ratio_of_lockup = ipo_stock_conditions.ratio_of_lockup
        self.ratio_of_sale_available = ipo_stock_conditions.ratio_of_sale_available

    def set_ipo_underwriter(self, ipo_underwriter: IpoUnderwriter):
        self.underwriter = ipo_underwriter.underwriter
        self.num_of_stock_allocated = ipo_underwriter.num_of_stock_allocated

    def set_ipo_demand_forecast(self, ipo_demand_forecast: IpoDemandForecast):
        self.competition_ratio = ipo_demand_forecast.competition_ratio
        self.commitment_ratio = ipo_demand_forecast.commitment_ratio

    def get_ipo_price(self):
        ipo_price = IpoPrice()

        ipo_price.band_price_low = self.band_price_low
        ipo_price.band_price_high = self.band_price_high
        ipo_price.offering_price = self.offering_price
        ipo_price.offering_amount = self.offering_amount

        return ipo_price

    def get_ipo_date(self):
        ipo_date = IpoDate()

        ipo_date.bidding_start = self.bidding_start
        ipo_date.bidding_finish = self.bidding_finish
        ipo_date.refund = self.refund
        ipo_date.go_public = self.go_public

        return ipo_date

    def get_ipo_new_stocks_info(self):
        ipo_new_stocks_info = IpoNewStocksInfo()

        ipo_new_stocks_info.total_num_of_new_stocks = self.total_num_of_new_stocks
        ipo_new_stocks_info.ratio_of_new_stocks = self.ratio_of_new_stocks

        return ipo_new_stocks_info

    def get_ipo_stock_conditions(self):
        ipo_stock_conditions = IpoStockConditions()

        ipo_stock_conditions.total_num_of_stock_after_ipo = self.total_num_of_stock_after_ipo
        ipo_stock_conditions.num_of_stock_lockup = self.num_of_stock_lockup
        ipo_stock_conditions.num_of_stock_sale_available = self.num_of_stock_sale_available
        ipo_stock_conditions.ratio_of_lockup = self.ratio_of_lockup
        ipo_stock_conditions.ratio_of_sale_available = self.ratio_of_sale_available

        return ipo_stock_conditions

    def get_ipo_underwriter(self):
        ipo_underwriter = IpoUnderwriter()

        ipo_underwriter.underwriter = self.underwriter
        ipo_underwriter.num_of_stock_allocated = self.num_of_stock_allocated

        return ipo_underwriter

    def get_ipo_demand_forecast(self):
        ipo_demand_forecast = IpoDemandForecast()

        ipo_demand_forecast.competition_ratio = self.competition_ratio
        ipo_demand_forecast.commitment_ratio = self.commitment_ratio

        return ipo_demand_forecast

이렇게 실제로 코드를 다중상속해서 써본적이 없어서, 사실 맞는 코드인가 싶기도 하다..

 

IpoData는 IpoDate, IpoNewStocksInfo, IpoPrice, IpoStockConditions, IpoUnderwriter, IpoDemandForecast를 포함하고 있으며,

아래 블로그를 참고하여 내 IpoData 클래스는 집합 관계>집약 관계임을 알게 되었다.

https://gmlwjd9405.github.io/2018/07/04/class-diagram.html

 

[UML] 클래스 다이어그램 작성법 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

굳이 private으로 짜야될 메서드가 없을 것 같아서, 편리함을 위해 public으로 모두 구현했다.

현업으로 가면 절대 안 그렇겠지만..