personal activity/stock

종목 이름 가져오기]ticker, name, sector 응용 #2

ToolBOX01 2025. 9. 14. 13:30
반응형

BPS(Book-Value Per Share)

BPS(Book-Value Per Share, 주당순자산가치)는 기업의 총자산에서 총부채를 뺀 순자산(자기자본)을 발행된 총주식 수로 나눈 값으로, 기업이 파산하여 모든 자산을 처분하고 부채를 갚은 뒤 주주들에게 돌아갈 몫을 1주당 금액으로 나타낸 지표입니다. 즉, BPS가 높을수록 1주당 장부가치가 높고 재무건전성이 좋다고 해석할 수 있습니다. 

BPS의 의미와 활용

  1. 기업 청산가치:
    BPS는 기업을 청산했을 때 1주당 주주에게 돌아갈 수 있는 가치를 의미합니다. 
  2. 재무건전성 판단:
    BPS가 높은 기업은 자기자본이 많아 재무적으로 안정적이며, 투자가치가 높다고 볼 수 있습니다. 
  3. PBR과 함께 활용:
    BPS 자체만으로는 주가가 자산가치에 비해 얼마나 저평가 또는 고평가되었는지 판단하기 어렵기 때문에, BPS에 현재 주가를 나누어 계산하는 PBR(주가순자산비율)과 함께 사용하여 투자 가치를 판단합니다. 
  4. 예시
    BPS가 1만원인 기업의 주가가 5,000원이라면 PBR은 0.5가 되어 주가가 장부가치보다 낮고, 주가가 20,000원이라면 PBR은 2가 되어 주가가 장부가치보다 높다고 볼 수 있습니다. 

import requests
from bs4 import BeautifulSoup
import pandas as pd
from openpyxl import load_workbook
import re
import time
import os

# 설정
FILE_PATH = "ticker.xlsx"
SHEET_NAME = "tickersheet"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
SAVE_DEBUG_HTML = True   # 못 찾은 종목 HTML 저장 여부

# 숫자 정리 함수
def clean_num(s):
    if not s: 
        return None
    s = str(s).replace('\xa0',' ').strip()
    m = re.search(r'-?\d{1,3}(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?', s)
    if m:
        num_str = m.group(0).replace(',', '')
        # 소수점이 있으면 float, 없으면 int로 변환
        return float(num_str) if '.' in num_str else int(num_str)
    return None

# BPS 추출 함수
def extract_bps_from_soup(soup):
    # 1) id="_pbr" 바로 아래 <em> 태그에서 BPS 추출
    pbr_tag = soup.select_one('em#_pbr')
    if pbr_tag:
        next_em = pbr_tag.find_next('em')
        if next_em:
            v = clean_num(next_em.get_text())
            if v is not None: return v

    # 2) aside_invest_info 블럭
    invest = soup.select_one('.aside_invest_info')
    if invest:
        trs = invest.select('tr')
        for i, tr in enumerate(trs):
            tds = tr.find_all(['th','td'])
            if len(tds) >= 2 and 'PBR' in tds[0].get_text(strip=True):
                # PBR 다음 <em> 태그 추출
                next_em_count = 0
                for j in range(i + 1, len(trs)):
                    em_tags = trs[j].select('em')
                    for em in em_tags:
                        next_em_count += 1
                        if next_em_count == 1:
                            v = clean_num(em.get_text())
                            if v is not None: return v

    # 3) "BPS" 텍스트 주변
    for text_node in soup.find_all(string=re.compile(r'\bBPS\b', re.I)):
        tr = text_node.find_parent('tr')
        if tr:
            tds = tr.find_all(['th','td'])
            for i, td in enumerate(tds):
                if 'BPS' in td.get_text(strip=True):
                    if i + 1 < len(tds):
                        v = clean_num(tds[i + 1].get_text())
                        if v is not None: return v

    # 4) 전체 텍스트에서 탐색
    page_text = soup.get_text(" ", strip=True)
    m = re.search(r'BPS[^0-9\-+]*([-]?\d{1,3}(?:,\d{3})*(?:\.\d+)?)', page_text, re.I)
    if m:
        return clean_num(m.group(1))

    return None

# ---- 메인 실행 ----
df = pd.read_excel(FILE_PATH, sheet_name=SHEET_NAME)
bps_list = []

total = len(df)
start_time = time.time()

for idx, row in df.iterrows():
    code = str(row["Ticker"]).zfill(6)
    name = row.get("Name", "")
    print(f"▶ [{idx+1}/{total}] [{code}] {name} BPS 조회 중...")

    url = f"https://finance.naver.com/item/main.naver?code={code}"
    try:
        res = requests.get(url, headers=HEADERS, timeout=10)
        res.encoding = res.apparent_encoding or 'utf-8'
        soup = BeautifulSoup(res.text, "html.parser")
    except Exception as e:
        print(f"   ✖ HTTP/네트워크 오류: {e}")
        bps_list.append(None)
        continue

    bps_value = extract_bps_from_soup(soup)
    if bps_value is None:
        print("   → BPS: None (찾지 못함)")
        if SAVE_DEBUG_HTML:
            debug_name = f"debug_{code}.html"
            with open(debug_name, "w", encoding="utf-8") as f:
                f.write(res.text)
            print(f"     (디버그 HTML 저장: {os.path.abspath(debug_name)})")
    else:
        print(f"   → BPS: {bps_value}")

    bps_list.append(bps_value)
    time.sleep(0.3)  # 서버 부담 완화

elapsed = time.time() - start_time
found = sum(1 for v in bps_list if v is not None)
print(f"\n완료: {found}/{total} 개 항목에서 BPS를 찾음 (소요 {elapsed:.1f}초)")

# ---- F열만 갱신 ----
book = load_workbook(FILE_PATH)
ws = book[SHEET_NAME]

# F2부터 BPS 값 기록
for i, val in enumerate(bps_list, start=2):  
    ws[f"F{i}"] = val

book.save(FILE_PATH)
print("✅ BPS 크롤링 완료 후 F열만 갱신하여 저장되었습니다.")

 

2025년, 09월 15일 종목별 BPS 다운로드

ticker.xlsx
0.15MB


 

 

PER, PBR, ROE란? 투자의 나침반이예요 [요즘 금융용어 #금융보카]

다음 중 주가와 기업 가치를 평가하는데 자주 쓰이는 용어로 개념이 틀린 것은 무엇일까요? ① PER : 기...

blog.naver.com

 

▣ Price to Book Ratio, P/B Ratio

PBR은 맥락에 따라 여러 의미로 사용되지만,가장 흔히 가리키는 것은 주가순자산비율(Price to Book Ratio, P/B Ratio)입니다. 이는 주식 투자와 기업 가치 평가에서 중요한 재무비율로, 주식의 시장 가격이 기업의 순자산 가치(장부가치) 대비 얼마나 높은지(또는 낮은지)를 나타냅니다. 다른 의미(예: 네트워킹의 Policy-Based Routing이나 컴퓨터 그래픽스의 Physically Based Rendering)도 있지만, 금융 분야에서 압도적으로 자주 언급됩니다.

예시: 주가가 2,000원이고 BPS가 1,500원이라면, PBR = 2,000 / 1,500 = 1.33배입니다.
         이는 주가가 순자산 가치의 1.33배로 거래되고 있음을 의미합니다.

PBR로 알 수 있는 것

PBR은 기업의 내재 가치 대비 주가의 적정성을 판단하는 데 유용합니다. 주요 인사이트는 다음과 같습니다:

  • PBR < 1: 주가가 순자산 가치보다 낮아 '저평가'된 상태로 보입니다.
    자산 가치가 주가에 제대로 반영되지 않았을 수 있어, 가치 투자자들이 매수 기회로 삼습니다.
    (예: 자산 중심 산업처럼 제조업에서 자주 관찰됨)
  • PBR = 1: 주가가 순자산 가치와 비슷해 적정 수준으로 평가됩니다.
  • PBR > 1: 주가가 순자산 가치보다 높아 '고평가'된 상태입니다. 
    성장 잠재력(예: 기술주)이 반영된 경우일 수 있지만, 과열 가능성을 경고합니다.

추가 활용:

  • 업종 비교: 같은 산업 내 기업들을 비교해 상대적 가치를 평가합니다. (예: 은행주는 평균 PBR이 낮음)
  • 투자 전략: 저PBR 종목은 '가치주'로 분류되어 장기 투자에 적합하며, ROE(자기자본이익률)와 함께 사용하면 기업의 자산 효율성을 더 깊이 분석할 수 있습니다.
  • 한계: 무형자산(브랜드, 특허)이 많은 기업(예: IT 기업)에서는 PBR이 과소평가될 수 있으니, PER(주가수익비율) 등 다른 지표와 함께 봐야 합니다.

※ 네이버 증권 사이트에서 PBR 값을 가져오는 파이썬 코드

import requests
from bs4 import BeautifulSoup
import pandas as pd
from openpyxl import load_workbook
import re
import time
import os

# 설정
FILE_PATH = "ticker.xlsx"
SHEET_NAME = "tickersheet"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
SAVE_DEBUG_HTML = True   # 못 찾은 종목 HTML 저장 여부

# 숫자 정리 함수
def clean_num(s):
    if not s: 
        return None
    s = str(s).replace('\xa0',' ').strip()
    m = re.search(r'-?\d{1,3}(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?', s)
    if m:
        num_str = m.group(0).replace(',', '')
        return float(num_str) if '.' in num_str else int(num_str)
    return None

# PBR 추출 함수
def extract_pbr_from_soup(soup):
    # 1) id="_pbr" 바로 아래 <em> 태그에서 PBR 추출
    pbr_tag = soup.select_one('em#_pbr')
    if pbr_tag:
        v = clean_num(pbr_tag.get_text())
        if v is not None: return v

    # 2) aside_invest_info 블럭
    invest = soup.select_one('.aside_invest_info')
    if invest:
        trs = invest.select('tr')
        for tr in trs:
            tds = tr.find_all(['th','td'])
            if len(tds) >= 2 and 'PBR' in tds[0].get_text(strip=True):
                v = clean_num(tds[1].get_text())
                if v is not None: return v

    # 3) "PBR" 텍스트 주변
    for text_node in soup.find_all(string=re.compile(r'\bPBR\b', re.I)):
        tr = text_node.find_parent('tr')
        if tr:
            tds = tr.find_all(['th','td'])
            for i, td in enumerate(tds):
                if 'PBR' in td.get_text(strip=True):
                    if i + 1 < len(tds):
                        v = clean_num(tds[i + 1].get_text())
                        if v is not None: return v

    # 4) 전체 텍스트에서 탐색
    page_text = soup.get_text(" ", strip=True)
    m = re.search(r'PBR[^0-9\-+]*([-]?\d{1,3}(?:,\d{3})*(?:\.\d+)?)', page_text, re.I)
    if m:
        return clean_num(m.group(1))

    return None

# ---- 메인 실행 ----
df = pd.read_excel(FILE_PATH, sheet_name=SHEET_NAME)
pbr_list = []

total = len(df)
start_time = time.time()

for idx, row in df.iterrows():
    code = str(row["Ticker"]).zfill(6)
    name = row.get("Name", "")
    print(f"▶ [{idx+1}/{total}] [{code}] {name} PBR 조회 중...")

    url = f"https://finance.naver.com/item/main.naver?code={code}"
    try:
        res = requests.get(url, headers=HEADERS, timeout=10)
        res.encoding = res.apparent_encoding or 'utf-8'
        soup = BeautifulSoup(res.text, "html.parser")
    except Exception as e:
        print(f"   ✖ HTTP/네트워크 오류: {e}")
        pbr_list.append(None)
        continue

    pbr_value = extract_pbr_from_soup(soup)
    if pbr_value is None:
        print("   → PBR: None (찾지 못함)")
        if SAVE_DEBUG_HTML:
            debug_name = f"debug_{code}.html"
            with open(debug_name, "w", encoding="utf-8") as f:
                f.write(res.text)
            print(f"     (디버그 HTML 저장: {os.path.abspath(debug_name)})")
    else:
        print(f"   → PBR: {pbr_value}")

    pbr_list.append(pbr_value)
    time.sleep(0.3)  # 서버 부담 완화

elapsed = time.time() - start_time
found = sum(1 for v in pbr_list if v is not None)
print(f"\n완료: {found}/{total} 개 항목에서 PBR를 찾음 (소요 {elapsed:.1f}초)")

# ---- G열만 갱신 ----
book = load_workbook(FILE_PATH)
ws = book[SHEET_NAME]

# G2부터 PBR 값 기록
for i, val in enumerate(pbr_list, start=2):  
    ws[f"G{i}"] = val

book.save(FILE_PATH)
print("✅ PBR 크롤링 완료 후 G열만 갱신하여 저장되었습니다.")

 

※ 2025년 09월 15일 네이버 증권 사이트에서 종목별 PBR 다운로드

ticker.xlsx
0.17MB

 

▣  ROE (Return on Equity -자기자본이익률

기업이 주주가 투자한 자본(자기자본)을 얼마나 효율적으로 활용해 수익을 창출했는지를 나타내는 재무 지표입니다. 주로 기업의 수익성과 운영 효율성을 평가할 때 사용됩니다.

예시: 당기순이익이 100억 원이고, 평균 자기자본이 500억 원이라면, ROE = (100억 / 500억) × 100 = 20%입니다. 이는 기업이 자기자본 1원당 20원의 수익을 창출했음을 의미합니다.

ROE로 알 수 있는 것

ROE는 기업의 자본 효율성과 수익 창출 능력을 평가하는 데 유용합니다. 주요 인사이트는 다음과 같습니다:

  • 높은 ROE (예: 15% 이상):
    기업이 자본을 효율적으로 활용해 높은 수익을 내고 있음을 나타냅니다.
    투자자들에게 매력적인 기업으로 보일 가능성이 큽니다.
  • 낮은 ROE (예: 5% 미만): 
    자본 대비 수익성이 낮아 비효율적이거나 성장성이 떨어질 수 있음을 시사합니다.
  • 업종 비교: 
    ROE는 산업별로 평균값이 다릅니다. 
    예를 들어, 기술주나 성장주는 높은 ROE를 보이는 반면, 자본집약적 산업(예: 제조업)은 상대적으로 낮을 수 있습니다.
  • PBR과의 연계: ROE는 
    PBR(주가순자산비율)과 함께 분석하면 기업의 가치와 수익성을 종합적으로 판단할 수 있습니다. 
    높은 ROE와 낮은 PBR은 저평가된 우량주일 가능성을 높입니다

주의점

  • 한계:
    ROE가 높다고 무조건 좋은 기업은 아닙니다.
    과도한 부채(레버리지)로 ROE가 부풀려질 수 있으므로, 부채비율(D/E Ratio)과 함께 봐야 합니다.
  • 단기 왜곡: 일회성 수익(예: 자산 매각)으로 ROE가 높아질 수 있으니, 장기 추세를 확인하는 것이 중요합니다.
  • 산업 특성:
    무형자산(특허, 브랜드 등)이 중요한 기업에서는 자기자본이 낮게 평가될 수 있어 ROE가 과대 표시될 가능성이 있습니다.

※ 네이버 증권 사이트에서 ROE 값을 가져오는 파이썬 코드

import requests
from bs4 import BeautifulSoup
import pandas as pd
from openpyxl import load_workbook
import re
import time
import os

# 설정
FILE_PATH = "ticker.xlsx"
SHEET_NAME = "tickersheet"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
SAVE_DEBUG_HTML = True  # 못 찾은 종목 HTML 저장 여부

# 숫자 정리 함수
def clean_num(s):
    if not s: 
        return None
    s = str(s).replace('\xa0', '').strip()
    m = re.search(r'-?\d{1,3}(?:,\d{3})*(?:\.\d+)?', s)
    if m:
        num_str = m.group(0).replace(',', '')
        return float(num_str)
    return None

# ROE 추출 함수 (최종 수정)
def extract_roe_from_soup(soup, target_code):
    """
    네이버 금융 페이지에서 ROE를 추출합니다.
    우선순위 1: 종목 메인 재무정보 테이블에서 ROE 찾기
    우선순위 2: 동일업종 비교 테이블에서 해당 종목의 ROE 찾기
    """
    # 1) 종목 메인 재무정보 테이블에서 ROE 찾기
    # 이 테이블은 종목에 따라 다른 클래스를 가질 수 있으므로 여러 클래스를 시도합니다.
    main_tables = soup.select('.section.invest_info, .tb_type1.tb_num, table.per_table, .tb_ty_summary')
    for table in main_tables:
        trs = table.find_all('tr')
        for tr in trs:
            tds = tr.find_all(['th', 'td'])
            if len(tds) >= 2 and 'ROE(%)' in tds[0].get_text(strip=True):
                v = clean_num(tds[1].get_text())
                if v is not None:
                    return v

    # 2) '동일업종 비교' 테이블에서 해당 종목의 ROE 값을 찾아내는 로직
    related_table = soup.select_one('.related_section.main_tab table')
    if related_table:
        # 1. 헤더(th)에서 종목 코드(target_code)가 포함된 열의 인덱스 찾기
        headers = [th.get_text(strip=True) for th in related_table.find_all('th')]
        target_col_idx = -1
        for i, header in enumerate(headers):
            # 헤더 텍스트에 종목 코드 포함 여부 확인
            if target_code in header:
                target_col_idx = i
                break
        
        # 해당 종목 열을 찾았다면
        if target_col_idx != -1:
            # 2. 'ROE(%)' 행을 찾아 해당 열의 값 가져오기
            rows = related_table.find_all('tr')
            for row in rows:
                if 'ROE(%)' in row.get_text(strip=True):
                    tds = row.find_all('td')
                    if target_col_idx < len(tds):
                        v = clean_num(tds[target_col_idx].get_text())
                        return v

    return None

# --- 메인 실행 ---
df = pd.read_excel(FILE_PATH, sheet_name=SHEET_NAME)
roe_list = []

total = len(df)
start_time = time.time()

for idx, row in df.iterrows():
    code = str(row["Ticker"]).zfill(6)
    name = row.get("Name", "")
    print(f"▶ [{idx+1}/{total}] [{code}] {name} ROE 조회 중...")

    url = f"https://finance.naver.com/item/main.naver?code={code}"
    try:
        res = requests.get(url, headers=HEADERS, timeout=10)
        res.encoding = res.apparent_encoding or 'utf-8'
        soup = BeautifulSoup(res.text, "html.parser")
    except Exception as e:
        print(f"   ✖ HTTP/네트워크 오류: {e}")
        roe_list.append(None)
        continue

    # 함수 호출 시 종목 코드(code)를 인자로 전달
    roe_value = extract_roe_from_soup(soup, code)
    if roe_value is None:
        print("   → ROE: None (찾지 못함)")
        if SAVE_DEBUG_HTML:
            debug_name = f"debug_{code}.html"
            with open(debug_name, "w", encoding="utf-8") as f:
                f.write(res.text)
            print(f"     (디버그 HTML 저장: {os.path.abspath(debug_name)})")
        roe_list.append(None)
    else:
        print(f"   → ROE: {roe_value}")
        roe_list.append(roe_value)

    time.sleep(0.3)  # 서버 부담 완화

elapsed = time.time() - start_time
found = sum(1 for v in roe_list if v is not None)
print(f"\n완료: {found}/{total} 개 항목에서 ROE를 찾음 (소요 {elapsed:.1f}초)")

# --- H열만 갱신 ---
book = load_workbook(FILE_PATH)
ws = book[SHEET_NAME]

# H2부터 ROE 값 기록, 데이터가 없는 경우 빈 셀 유지
for i, val in enumerate(roe_list, start=2):  
    if val is not None:
        ws[f"H{i}"] = val

book.save(FILE_PATH)
print("✅ ROE 크롤링 완료 후 H열만 갱신하여 저장되었습니다.")

 

※ 2025년 09월 15일 네이버 증권 사이트에서 종목별 ROE 다운로드

 

ticker.xlsx
0.16MB


반응형