
    i2                         d Z ddlZddlZddlmZ ddlmZmZmZm	Z	m
Z
 ddlmZ ddlmZ ddlmZmZ ddlmZ dd	lmZ  ej,                  e      Z G d
 d      Z G d de      Zy)un  insurance_spider.py — 보험사 공개 데이터 Spider 구현.

Scrapling Spider ABC를 상속한 InsuranceSpider 클래스와
Response 이력 추적을 위한 ResponseHistory 유틸리티.

목적:
    보험사 공시 페이지 등 합법적 공개 데이터를 주기적으로 수집하는
    Spider를 TDD로 구현한다. InsuranceCrawler의 추출 로직을 재활용하며,
    Scrapling Spider 라이프사이클(on_start/on_close/on_error/on_scraped_item)에
    따라 출력 디렉토리 관리, 이력 저장, 에러 로깅 등을 처리한다.

주의사항:
    - 합법적 공개 데이터(보험사 공시 페이지 등)만을 대상으로 합니다.
    - 실제 크롤링 전 반드시 대상 사이트의 robots.txt를 확인하고 준수해야 합니다.
    - download_delay=1.0, concurrent_requests_per_domain=2로 서버 부하를 최소화합니다.
    N)Path)AnyAsyncGeneratorDictOptionalSet)InsuranceCrawler)FetcherSession)RequestSessionManager)Spider)CrawlResultc                       e Zd ZdZddZ	 	 ddededeee      dee	eef      ddf
d	Z
dee	eef      fd
Zdedee	eef      fdZddZddZy)ResponseHistoryu   Response 이력 추적: 리다이렉트 체인 보존.

    크롤링 중 방문한 URL의 상태 코드, 헤더, 리다이렉트 경로를 기록하고
    JSON 파일로 내보낼 수 있다.
    returnNc                     g | _         y N)_historyselfs    I/home/jay/workspace/.worktrees/task-2116-dev1/scripts/insurance_spider.py__init__zResponseHistory.__init__'   s	    .0    urlstatus	redirectsheadersc                 `    ||d}|||d<   |||d<   | j                   j                  |       y)u  URL 방문 기록을 추가한다.

        Args:
            url: 최종 응답 URL
            status: HTTP 상태 코드
            redirects: 리다이렉트 경로 (None이면 저장하지 않음)
            headers: 응답 헤더 딕셔너리
        )r   r   Nr   r   )r   append)r   r   r   r   r   entrys         r   recordzResponseHistory.record*   s@     ),v > !*E+&E)U#r   c                 ,    t        | j                        S )u(   전체 이력 리스트를 반환한다.)listr   r   s    r   get_historyzResponseHistory.get_history@   s    DMM""r   c                     g }| j                   D ]D  }|d   |k(  r|j                  |       |j                  dg       }||v s4|j                  |       F |S )u)  특정 URL에 연결된 이력 항목을 반환한다.

        해당 URL이 최종 URL이거나 리다이렉트 체인에 포함된 항목을 모두 반환한다.

        Args:
            url: 조회할 URL

        Returns:
            해당 URL과 관련된 이력 항목 리스트
        r   r   )r   r   get)r   r   resultr    r   s        r   	get_chainzResponseHistory.get_chainD   sb     ]] 	%EU|s"e$		+r2Iie$	% r   c                     t        |      }|j                  j                  dd       |j                  t	        j
                  | j                  dd      d       y)	u   이력을 JSON 파일로 저장한다.

        Args:
            path: 저장할 파일 경로 (부모 디렉토리가 없으면 생성)
        Tparentsexist_okF   )ensure_asciiindentzutf-8)encodingN)r   parentmkdir
write_textjsondumpsr   )r   path	file_paths      r   savezResponseHistory.saveY   sP     J	td;JJt}}5C 	 	
r   c                 8    | j                   j                          y)u   이력을 초기화한다.N)r   clearr   s    r   r:   zResponseHistory.clearf   s    r   r   N)NN)r6   
str | Pathr   N)__name__
__module____qualname____doc__r   strintr   r#   dictr!   r   r$   r(   r8   r:    r   r   r   r       s    1 *.,0$$ $ DI&	$
 $sCx.)$ 
$,#T$sCx.1 #S T$sCx.%9 *
r   r   c                       e Zd ZU dZdZg Zee   ed<    e	       Z
ee   ed<   dZeed<   dZeed<   d	Zeed
<   	 	 	 	 	 	 d$deee      deee      dddddedeeeef      ddf fdZdeddfdZdedeeeef   ez  dz  df   fdZd%deddfdZd&dZdededdfdZdeeef   deeeef      fdZ d'd ede!fd!Z"e#	 d(d"edee   dedeeeef      deeef   f
d#       Z$ xZ%S ))InsuranceSpideruC  보험사 공시 데이터 수집 Spider.

    InsuranceCrawler의 추출 로직(extract_with_selector / extract_similar /
    extract_table)을 Scrapling Spider 라이프사이클에 통합한다.

    extraction_config 딕셔너리로 추출 모드와 파라미터를 제어한다:
        - mode="css"     : extract_with_selector() (기본값)
        - mode="table"   : extract_table()
        - mode="similar" : extract_similar()

    예의 바른 크롤링(SP-5):
        - concurrent_requests=4
        - concurrent_requests_per_domain=2
        - download_delay=1.0
    insurance_spider
start_urlsallowed_domains   concurrent_requestsr-   concurrent_requests_per_domaing      ?download_delayN
output_dirr<   crawldirzstr | Path | Noneintervalextraction_configr   c                     |t        |      | _        |t        |      | _        t	        |      | _        || _        t        d      | _        t               | _
        t        | 1  ||       y)u  InsuranceSpider 초기화.

        Args:
            start_urls: 크롤링 시작 URL 목록
            allowed_domains: 허용 도메인 집합
            output_dir: 결과 파일 저장 디렉토리 (기본: "crawl_output")
            crawldir: 체크포인트 디렉토리 (SP-3). None이면 체크포인트 비활성화.
            interval: 체크포인트 저장 주기(초, 기본: 300.0)
            extraction_config: 추출 설정 딕셔너리
        NF)adaptive)rO   rP   )r#   rH   setrI   r   rN   rQ   r	   _crawlerr   response_historysuperr   )r   rH   rI   rN   rO   rP   rQ   	__class__s          r   r   zInsuranceSpider.__init__   si    ( !":.DO&#&#7D  $Z 0;L(%8 / 1 	(X>r   managerc                 <    |j                  dt               d       y)u3   FetcherSession을 기본 세션으로 등록한다.defaultT)r[   N)addr
   )r   rY   s     r   configure_sessionsz"InsuranceSpider.configure_sessions   s    I~/>r   responsec                  K   | j                   y| j                   }|j                  dd      }t        |dd      xs d}t        |dd      xs d}| j                  j	                  ||       g }|dk(  r0|j                  d	d      }| j
                  j                  ||
      }n|dk(  r^|j                  dd      }|j                  d      }	t        |j                  dd            }
| j
                  j                  |||	|
      }nT|j                  dd      }|j                  d      }	|j                  dd      }| j
                  j                  ||||	      }|D ]  }||d<   |  |j                  d      }|r|j                  |      }|ru|j                  r%|j                  j                  j                  d      nd}|r?|j                  d      r|}nddlm}  |||      }t!        |d| j"                         yyyyw)u=  보험 데이터 추출 + 다음 페이지 follow.

        extraction_config에 따라 추출 모드 선택:
            - mode="table"   : extract_table()
            - mode="similar" : extract_similar()
            - mode="css"     : extract_with_selector() (기본값)

        각 아이템에 _source_url 메타데이터를 추가한다.
        next_page_selector가 있으면 다음 페이지 Request를 yield한다.

        Args:
            response: Scrapling Response 객체 (Selector 상속)

        Yields:
            dict 아이템 또는 Request
        Nmodecssr    r   r   tabletable_selector)rd   similarreference_selectorfields	thresholdg?)rf   rg   rh   css_selector
identifier)ri   rj   rg   _source_urlnext_page_selectorhrefhttp)urljoinr[   )sidcallback)rQ   r&   getattrrV   r!   rU   extract_tablefloatextract_similarextract_with_selectorra   firstattrib
startswithurllib.parsero   r   parse)r   r^   configr`   
source_urlr   itemsrd   rf   rg   rh   ri   rj   itemrl   	next_linkrm   next_urlro   s                      r   r{   zInsuranceSpider.parse   s    " !!)''zz&%(!(E26<"
 h!49$$Z8&(7?"(**-=w"GNMM///XEY&,jj1Er&J/5zz(/CF$VZZS%ABIMM11#5#	 2 E !'

>2 >LZZ)F$jjr:JMM77)%	 8 E  	D",DJ	
 -3JJ7K,L %78ILUOOioo&<&<&@&@&Haev.#'8#*:t#<!(	DJJOO   s   HHresumingc                    K   | j                   j                  dd       |r'| j                  j                  d| j                          y| j                  j                  d| j                          yw)uz   output_dir 생성 및 시작 로깅.

        Args:
            resuming: True이면 체크포인트에서 재개
        Tr*   z/Resuming spider from checkpoint (output_dir=%s)z(Starting InsuranceSpider (output_dir=%s)N)rN   r2   loggerinfo)r   r   s     r   on_startzInsuranceSpider.on_start   sW      	dT:KKNPTP_P_`KKGYs   A.A0c                 4  K   | j                   dz  }	 | j                  j                  |       | j                  j	                  d|       | j                  j	                  d       y# t
        $ r&}| j                  j                  d|       Y d}~Fd}~ww xY ww)u8   ResponseHistory를 파일로 저장하고 종료 로깅.zresponse_history.jsonzSaved response history to %sz#Failed to save response history: %sNzInsuranceSpider closed)rN   rV   r8   r   r   	Exceptionwarning)r   history_pathexcs      r   on_closezInsuranceSpider.on_close  s     )@@	L!!&&|4KK;\J 	12  	LKK EsKK	Ls.   B7A& 
B&	B/BBBBrequesterrorc                    K   | j                   j                  d|j                  t        |      j                  |       yw)up   에러 로깅.

        Args:
            request: 실패한 요청
            error: 발생한 예외
        zError fetching %s: %s: %sN)r   r   r   typer=   )r   r   r   s      r   on_errorzInsuranceSpider.on_error  s4      	'KKK  		
s   =?r   c                    K   |j                         D cg c]  \  }}|dk7  s| }}}t        d |D              }|sy|S c c}}w w)u.  빈 아이템 필터링.

        모든 값이 None이나 빈 문자열이면 None을 반환하여 아이템을 드롭한다.

        Args:
            item: 스크래핑된 아이템

        Returns:
            유효한 아이템이면 그대로 반환, 빈 아이템이면 None (드롭)
        rk   c              3   `   K   | ]&  }|d uxr t        |      j                         dk7   ( y w)Nrb   )rA   strip).0vs     r   	<genexpr>z2InsuranceSpider.on_scraped_item.<locals>.<genexpr>+  s+     YQ!4-@CFLLNb,@@Ys   ,.N)r~   any)r   r   kr   content_valueshas_contents         r   on_scraped_itemzInsuranceSpider.on_scraped_item  sO      )-

K1]8J!KK Y.YY Ls   AAA Aoutput_formatc                 `   | j                         }| j                  j                  dd       | j                  d| z  }|dk(  r|j                  j	                  |d       n|j                  j                  |       | j                  j                  dt        |j                        |       |S )u   Spider 실행 + 결과 내보내기.

        Args:
            output_format: "json" 또는 "jsonl" (기본값: "jsonl")

        Returns:
            CrawlResult (stats + items)
        Tr*   zitems.r4   )r/   zExported %d items to %s)	startrN   r2   r~   to_jsonto_jsonlr   r   len)r   r   r'   output_paths       r   runzInsuranceSpider.run2  s     #jjl 	dT:oo&(@@F"LL  T :LL!!+.%	
 r   schedulec                     d| |||d}|S )u  cokacdir --cron 연동을 위한 설정 딕셔너리 생성.

        Args:
            schedule: cron 표현식 (예: "0 6 * * *")
            start_urls: 크롤링 시작 URL 목록
            output_dir: 결과 저장 디렉토리
            extraction_config: 추출 설정 딕셔너리

        Returns:
            cokacdir --cron 연동에 필요한 설정 딕셔너리
        rG   )spiderr   rH   rN   rQ   rD   )r   rH   rN   rQ   r|   s        r   create_cron_configz"InsuranceSpider.create_cron_configO  s!    & ) $$!2"
 r   )NNcrawl_outputNg     r@N)Fr;   )jsonlr   )&r=   r>   r?   r@   namerH   r#   rA   __annotations__rT   rI   r   rK   rB   rL   rM   rt   r   rC   r   r   r   r]   r   r   r   r{   boolr   r   r   r   r   r   r   staticmethodr   __classcell__)rX   s   @r   rF   rF   k   s     DJS	 #OSX%  ! *+"C+NE +/.2#1(,6:?T#Y'? "#c(+? !	?
 &? ? $DcN3? 
?B?. ?T ?LPC LPN4S>G;SVZ;Z\`;`,a LP\
Zt 
Z 
Z3
g 
i 
D 
$sCx. Xd3PS8n=U , ; : 
 7;	I  $DcN3	
 
c3h r   rF   )r@   r4   loggingpathlibr   typingr   r   r   r   r   insurance_crawlerr	   scrapling.engines.staticr
   scrapling.spidersr   r   r   _Spiderscrapling.spiders.resultr   	getLoggerr=   logr   rF   rD   r   r   <module>r      sW   "    ; ; . 3 5 / 0g!H HV}g }r   