diff --git a/stock-lab/app/scraper.py b/stock-lab/app/scraper.py index c84c60b..fd49a01 100644 --- a/stock-lab/app/scraper.py +++ b/stock-lab/app/scraper.py @@ -5,8 +5,9 @@ import time # 네이버 파이낸스 주요 뉴스 NAVER_FINANCE_NEWS_URL = "https://finance.naver.com/news/mainnews.naver" -# 해외증시 뉴스 -NAVER_FINANCE_WORLD_NEWS_URL = "https://finance.naver.com/news/news_list.naver?mode=LSS3D§ion_id=101§ion_id2=258" +# 해외증시 뉴스 (모바일 API 사용) +# NAVER_FINANCE_WORLD_NEWS_URL 사용 안함. + # 해외증시 메인 (지수용) NAVER_FINANCE_WORLD_URL = "https://finance.naver.com/world/" @@ -77,55 +78,36 @@ def fetch_market_news() -> List[Dict[str, str]]: def fetch_overseas_news() -> List[Dict[str, str]]: """ - 네이버 금융 해외증시 뉴스 크롤링 + 네이버 금융 해외증시 뉴스 크롤링 (모바일 API 사용) """ + api_url = "https://m.stock.naver.com/api/news/list/global?pageSize=20&page=1" try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" } - resp = requests.get(NAVER_FINANCE_WORLD_NEWS_URL, headers=headers, timeout=10) + resp = requests.get(api_url, headers=headers, timeout=10) resp.raise_for_status() - - soup = BeautifulSoup(resp.content, "html.parser", from_encoding="cp949") - # 구조: div.newsList > ul > li - # 구조가 mainnews와 비슷하지만 약간 다름. dl > dt/dd + data = resp.json() + items = data.get("result", []) + articles = [] - news_list = soup.select(".newsList ul li") - - for li in news_list: - dl = li.select_one("dl") - if not dl: continue + for item in items: + title = item.get("subject", "") + summary = item.get("summary", "") + press = item.get("officeName", "") - # 썸네일 있을 경우 dt.thumb가 있고, 제목은 dt.articleSubject 또는 dd.articleSubject - subject_tag = dl.select_one(".articleSubject a") - if not subject_tag: - # 썸네일 없는 경우 dt가 제목일 수 있음 - subject_tag = dl.select_one("dt a") - # 근데 dt가 thumb일 수도 있어서 클래스 확인 필요 - if subject_tag and subject_tag.find("img"): - # 이건 썸네일. 다음 형제나 dd를 찾아야 함 - subject_tag = dl.select_one("dd.articleSubject a") - - if not subject_tag: continue + # 날짜 포맷팅 (20260126123000 -> 2026-01-26 12:30:00) + raw_dt = str(item.get("dt", "")) + if len(raw_dt) == 14: + date = f"{raw_dt[:4]}-{raw_dt[4:6]}-{raw_dt[6:8]} {raw_dt[8:10]}:{raw_dt[10:12]}:{raw_dt[12:]}" + else: + date = raw_dt - title = subject_tag.get_text(strip=True) - link = "https://finance.naver.com" + subject_tag["href"] - - summary_tag = dl.select_one(".articleSummary") - summary = "" - press = "" - date = "" - - if summary_tag: - # 불필요한 태그 제거 - for child in summary_tag.select(".press, .wdate"): - if "press" in child.get("class", []): - press = child.get_text(strip=True) - if "wdate" in child.get("class", []): - date = child.get_text(strip=True) - child.extract() - summary = summary_tag.get_text(strip=True) + # 링크 생성 + aid = item.get("articleId") + oid = item.get("officeId") + link = f"https://m.stock.naver.com/worldstock/news/read/{oid}/{aid}" articles.append({ "title": title, @@ -164,10 +146,6 @@ def fetch_major_indices() -> Dict[str, Any]: indices = [] - # 1. 국내 - # (기존 로직 유지하되 targets 루프 안에서 처리) - # 하지만 해외 지수 크롤링 코드가 복잡해지므로, 아래에서 별도로 호출 - # --- 국내 --- resp_kr = requests.get("https://finance.naver.com/", headers=headers, timeout=5) soup_kr = BeautifulSoup(resp_kr.content, "html.parser", from_encoding="cp949") @@ -203,10 +181,6 @@ def fetch_major_indices() -> Dict[str, Any]: resp_world = requests.get(NAVER_FINANCE_WORLD_URL, headers=headers, timeout=5) soup_world = BeautifulSoup(resp_world.content, "html.parser", from_encoding="cp949") - # 구조: div.market_include > div.market_data > ul.data_list > li - # 하지만 world 메인에는 주요 지수가 상단에 있음: .sise_major - # DJI, NAS, SPI - world_targets = [ {"key": "DJI", "selector": ".sise_major .data_list li:nth-child(1)"}, {"key": "NAS", "selector": ".sise_major .data_list li:nth-child(2)"}, @@ -217,45 +191,25 @@ def fetch_major_indices() -> Dict[str, Any]: li = soup_world.select_one(wt["selector"]) if not li: continue - # 이름: dt - # name = li.select_one("dt").get_text(strip=True) (보통 '다우산업' 등) - # 값: dd.point_status strong val_tag = li.select_one("dd.point_status strong") value = val_tag.get_text(strip=True) if val_tag else "" # 등락: dd.point_status em - # 여기는 값과 퍼센트가 em 안에 같이 있거나 분리됨 - # 구조: 상승 123.45 상승 +1.2% - # 파싱이 까다로우니 텍스트 전체 가져와서 분리 시도 - + direction = "" status_dd = li.select_one("dd.point_status") if status_dd: - # em 태그들 제거하면서 텍스트 추출? 아니면 em 안을 분석 em = status_dd.select_one("em") if em: - # class="red" / "blue" - direction = "" if "red" in em.get("class", []): direction = "red" elif "blue" in em.get("class", []): direction = "blue" - - txt = em.get_text(" ", strip=True) # "상승 123.45 상승 +1.2%" - # 숫자만 추출하거나 단순 처리. - # 네이버 해외 증시 메인 구조가 복잡하므로, - # 단순히 리스트 페이지(.w_major_list) 등 다른 곳을 보는 게 나을 수 있음 - # 하지만 일단 간단히 처리: value 밑에 .point_status > em 이 등락폭 - pass - - # 대안: 주요 3대 지수는 aside에 .sise_major 말고 데이터 테이블이나 리스트가 더 명확함 - # 여기서는 aside .sise_major > ul > li 구조를 쓴다고 가정하고, - # 만약 파싱이 어려우면 값만이라도 가져옴. indices.append({ "name": wt["key"], "value": value, - "change_value": "", # 파싱 복잡도 때문에 일단 생략 (추후 보완) + "change_value": "", "change_percent": "", - "direction": "", # direction은 위에서 red/blue 잡으면 됨 + "direction": direction, "type": "overseas" })