자동완성 기능 추가
parent
fc88eedfa2
commit
f29ac29649
|
|
@ -5,7 +5,7 @@ from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
|
from app.utils.nvMapPwScraper import NvMapPwScraper
|
||||||
logger = get_logger("core")
|
logger = get_logger("core")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,6 +24,7 @@ async def lifespan(app: FastAPI):
|
||||||
|
|
||||||
await create_db_tables()
|
await create_db_tables()
|
||||||
logger.info("Database tables created (DEBUG mode)")
|
logger.info("Database tables created (DEBUG mode)")
|
||||||
|
await NvMapPwScraper.initiate_scraper()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.error("Database initialization timed out")
|
logger.error("Database initialization timed out")
|
||||||
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.database.session import get_session, AsyncSessionLocal
|
from app.database.session import get_session, AsyncSessionLocal
|
||||||
from app.home.models import Image
|
from app.home.models import Image
|
||||||
from app.home.schemas.home_schema import (
|
from app.home.schemas.home_schema import (
|
||||||
|
AutoCompleteRequest,
|
||||||
CrawlingRequest,
|
CrawlingRequest,
|
||||||
CrawlingResponse,
|
CrawlingResponse,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
|
|
@ -27,6 +28,7 @@ from app.utils.chatgpt_prompt import ChatgptService
|
||||||
from app.utils.common import generate_task_id
|
from app.utils.common import generate_task_id
|
||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
from app.utils.nvMapScraper import NvMapScraper, GraphQLException
|
from app.utils.nvMapScraper import NvMapScraper, GraphQLException
|
||||||
|
from app.utils.nvMapPwScraper import NvMapPwScraper
|
||||||
from app.utils.prompts.prompts import marketing_prompt
|
from app.utils.prompts.prompts import marketing_prompt
|
||||||
from config import MEDIA_ROOT
|
from config import MEDIA_ROOT
|
||||||
|
|
||||||
|
|
@ -105,17 +107,52 @@ def _extract_region_from_address(road_address: str | None) -> str:
|
||||||
tags=["Crawling"],
|
tags=["Crawling"],
|
||||||
)
|
)
|
||||||
async def crawling(request_body: CrawlingRequest):
|
async def crawling(request_body: CrawlingRequest):
|
||||||
"""네이버 지도 장소 크롤링"""
|
return await _crawling_logic(request_body.url)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/autocomplete",
|
||||||
|
summary="네이버 자동완성 크롤링",
|
||||||
|
description="""
|
||||||
|
네이버 검색 API 정보를 활용하여 Place ID를 추출한 뒤 자동으로 크롤링합니다.
|
||||||
|
|
||||||
|
## 요청 필드
|
||||||
|
- **url**: 네이버 지도 장소 URL (필수)
|
||||||
|
|
||||||
|
## 반환 정보
|
||||||
|
- **image_list**: 장소 이미지 URL 목록
|
||||||
|
- **image_count**: 이미지 개수
|
||||||
|
- **processed_info**: 가공된 장소 정보 (customer_name, region, detail_region_info)
|
||||||
|
""",
|
||||||
|
response_model=CrawlingResponse,
|
||||||
|
response_description="크롤링 결과",
|
||||||
|
responses={
|
||||||
|
200: {"description": "크롤링 성공", "model": CrawlingResponse},
|
||||||
|
400: {
|
||||||
|
"description": "잘못된 URL",
|
||||||
|
"model": ErrorResponse,
|
||||||
|
},
|
||||||
|
502: {
|
||||||
|
"description": "크롤링 실패",
|
||||||
|
"model": ErrorResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags=["Crawling"],
|
||||||
|
)
|
||||||
|
async def autocomplete_crawling(request_body: AutoCompleteRequest):
|
||||||
|
url = _autocomplete_logic(request_body.dict())
|
||||||
|
return await _crawling_logic(url)
|
||||||
|
|
||||||
|
async def _crawling_logic(url:str):
|
||||||
request_start = time.perf_counter()
|
request_start = time.perf_counter()
|
||||||
logger.info("[crawling] ========== START ==========")
|
logger.info("[crawling] ========== START ==========")
|
||||||
logger.info(f"[crawling] URL: {request_body.url[:80]}...")
|
logger.info(f"[crawling] URL: {url[:80]}...")
|
||||||
|
|
||||||
# ========== Step 1: 네이버 지도 크롤링 ==========
|
# ========== Step 1: 네이버 지도 크롤링 ==========
|
||||||
step1_start = time.perf_counter()
|
step1_start = time.perf_counter()
|
||||||
logger.info("[crawling] Step 1: 네이버 지도 크롤링 시작...")
|
logger.info("[crawling] Step 1: 네이버 지도 크롤링 시작...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scraper = NvMapScraper(request_body.url)
|
scraper = NvMapScraper(url)
|
||||||
await scraper.scrap()
|
await scraper.scrap()
|
||||||
except GraphQLException as e:
|
except GraphQLException as e:
|
||||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||||
|
|
@ -288,6 +325,23 @@ async def crawling(request_body: CrawlingRequest):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _autocomplete_logic(autocomplete_item:dict):
|
||||||
|
step1_start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
async with NvMapPwScraper() as pw_scraper:
|
||||||
|
new_url = await pw_scraper.get_place_id_url(autocomplete_item)
|
||||||
|
except Exception as e:
|
||||||
|
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||||
|
logger.error(
|
||||||
|
f"[crawling] Autocomplete FAILED - 자동완성 예기치 않은 오류: {e} ({step1_elapsed:.1f}ms)"
|
||||||
|
)
|
||||||
|
logger.exception("[crawling] Autocomplete 상세 오류:")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail="자동완성 place id 추출 실패",
|
||||||
|
)
|
||||||
|
return new_url
|
||||||
|
|
||||||
def _extract_image_name(url: str, index: int) -> str:
|
def _extract_image_name(url: str, index: int) -> str:
|
||||||
"""URL에서 이미지 이름 추출 또는 기본 이름 생성"""
|
"""URL에서 이미지 이름 추출 또는 기본 이름 생성"""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,22 @@ class CrawlingRequest(BaseModel):
|
||||||
|
|
||||||
url: str = Field(..., description="네이버 지도 장소 URL")
|
url: str = Field(..., description="네이버 지도 장소 URL")
|
||||||
|
|
||||||
|
class AutoCompleteRequest(BaseModel):
|
||||||
|
"""자동완성 요청 스키마"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
'title': '<b>스테이</b>,<b>머뭄</b>',
|
||||||
|
'address': '전북특별자치도 군산시 신흥동 63-18',
|
||||||
|
'roadAddress': '전북특별자치도 군산시 절골길 18',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
title: str = Field(..., description="네이버 검색 place API Title")
|
||||||
|
address: str = Field(..., description="네이버 검색 place API 지번주소")
|
||||||
|
roadAddress: Optional[str] = Field(None, description="네이버 검색 place API 도로명주소")
|
||||||
|
|
||||||
class ProcessedInfo(BaseModel):
|
class ProcessedInfo(BaseModel):
|
||||||
"""가공된 장소 정보 스키마"""
|
"""가공된 장소 정보 스키마"""
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
class nvMapPwScraper():
|
class NvMapPwScraper():
|
||||||
# cls vars
|
# cls vars
|
||||||
is_ready = False
|
is_ready = False
|
||||||
_playwright = None
|
_playwright = None
|
||||||
|
|
@ -107,7 +107,7 @@ patchedGetter.toString();''')
|
||||||
if "/place/" in self.page.url:
|
if "/place/" in self.page.url:
|
||||||
return self.page.url
|
return self.page.url
|
||||||
|
|
||||||
if (count == self._max_retry / 2):
|
# if (count == self._max_retry / 2):
|
||||||
raise Exception("Failed to identify place id. loading timeout")
|
# raise Exception("Failed to identify place id. loading timeout")
|
||||||
else:
|
# else:
|
||||||
raise Exception("Failed to identify place id. item is ambiguous")
|
# raise Exception("Failed to identify place id. item is ambiguous")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue