
    i:                    V   d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZ ddlmZmZ ddlmZmZmZ  ej&                  d      Z eh d	      Z G d
 de      Z G d de      Z G d de      Z G d de      Ze G d d             ZddZddZddZddZ  G d d      Z!y)u  Lightpanda 크롤링 래퍼 — 텍스트 기반 대량 크롤링 전용

사용법:
    from tools.lightpanda_crawler import LightpandaCrawler

    async with LightpandaCrawler() as crawler:
        result = await crawler.fetch("https://example.com")
        # result.title, result.text, result.html, result.links

        results = await crawler.fetch_many(urls, concurrency=25)

스크린샷이 필요하면 Playwright+Chrome을 사용하세요 (이 모듈은 텍스트 전용).
    )annotationsN)	dataclassfield)Any)urljoinurlparse)BrowserPageasync_playwrightlightpanda_crawler>   colsrowstagsurlshrefsitemslinkslistsnamesemailsimagesphonespricesauthorscolumnsentriesresultsreviewsarticlescommentsproductssections
categories
paragraphsc                  &     e Zd ZdZdd fdZ xZS )
CrawlErroru(   크롤링 관련 오류 기본 클래스c                @    t         |   |       || _        || _        y N)super__init__urlcause)selfmessager+   r,   	__class__s       I/home/jay/workspace/.worktrees/task-2116-dev1/tools/lightpanda_crawler.pyr*   zCrawlError.__init__B   s    !
    ) N)r.   strr+   r3   r,   zException | NonereturnNone)__name__
__module____qualname____doc__r*   __classcell__)r/   s   @r0   r&   r&   ?   s    2 r1   r&   c                      e Zd ZdZy)CrawlTimeoutErroru   타임아웃 오류Nr6   r7   r8   r9    r1   r0   r<   r<   H   s    r1   r<   c                      e Zd ZdZy)CrawlConnectionErroru   CDP 연결 오류Nr=   r>   r1   r0   r@   r@   L   s    r1   r@   c                      e Zd ZdZy)CrawlJSErroru   JavaScript 실행 오류Nr=   r>   r1   r0   rB   rB   P   s    "r1   rB   c                  l    e Zd ZU dZded<   ded<   ded<   ded<   ded<   d	ed
<   ded<   ded<   ded<   y)CrawlResultu!   단일 페이지 크롤링 결과r3   r+   titletexthtml	list[str]r   dict[str, str]metaintstatusfloat
elapsed_msengineN)r6   r7   r8   r9   __annotations__r>   r1   r0   rD   rD   Y   s5    +	HJ
I
I
KKr1   rD   c                   t        j                  d| t         j                        }g }|D ]]  }|j                         }|r|j	                  d      r't        ||      }t        |      }|j                  dv sM|j                  |       _ |S )uO   HTML 에서 모든 <a href="..."> 링크를 절대 URL 로 변환하여 반환.z<a[^>]+href=["\']([^"\']+)["\'])#zjavascript:zmailto:ztel:)httphttps)	refindall
IGNORECASEstrip
startswithr   r   schemeappend)rG   base_urlr   resulthrefabsoluteparseds          r0   _extract_linksra   m   s    JJ94OEF $zz|t'NO8T*(#==--MM(#$ Mr1   c                F   i }ddg}|D ]  }t        j                  || t         j                        D ]k  }|j                  d      r#|j	                  d      |j	                  d      }}n"|j	                  d      |j	                  d      }}|||j                         <   m  |S )u;   HTML 에서 <meta name="..."> 태그의 content 를 추출.z><meta\s+name=["\']([^"\']+)["\']\s+content=["\']([^"\']*)["\']z@<meta\s+content=["\']([^"\']*)["\'][^>]+name=["\']([^"\']+)["\']z<meta\s+name      )rU   finditerrW   rY   grouplower)rG   rJ   patternspatternmatchnamecontents          r0   _extract_metarm   |   s    DIKH  )[[$> 	)E!!/2 %AAg %AA!(D	)) Kr1   c                    t        j                  dd| t         j                  t         j                  z        }t        j                  dd|      }t        j                  dd|      j	                         }|S )u8   HTML 태그 제거 후 텍스트 추출 (간이 구현).z)<(script|style)[^>]*>.*?</(script|style)>r2   )flagsz<[^>]+> z\s+)rU   subDOTALLrW   rX   )rG   rF   s     r0   _html_to_textrs      s^     66>DPRPYPY\^\i\iPijD66*c4(D66&#t$**,DKr1   c                X    | j                         }|t        v xs |j                  d      S )u(   셀렉터 키가 복수형인지 확인.s)rg   _PLURAL_KEYSendswith)keyrg   s     r0   _is_plural_keyry      s&    IIKEL 7ENN3$77r1   c                      e Zd ZdZ	 	 	 d	 	 	 	 	 	 	 ddZddZddZddZddZddZ	ddd	Z
	 	 d	 	 	 	 	 	 	 dd
ZddZddZddZddZy)LightpandaCrawleru   Lightpanda CDP 기반 비동기 크롤러.

    Context manager 로 사용:
        async with LightpandaCrawler() as crawler:
            result = await crawler.fetch("https://example.com")
    c                X    || _         || _        || _        d | _        d| _        d | _        y )N
lightpanda)cdp_endpointchrome_endpoint
timeout_ms_browser_engine_pw_context)r-   r~   r   r   s       r0   r*   zLightpandaCrawler.__init__   s1     ).$(,( $r1   c                   K   t               | _        | j                  j                          d {   | _        | j	                          d {    | S 7 #7 wr(   )r   r   
__aenter___pw_connectr-   s    r0   r   zLightpandaCrawler.__aenter__   sG     +-))4466mmo 7s!   -AAAAAAc                (  K   | j                   *	 | j                   j                          d {    d | _         | j                  *	  | j                  j                  |  d {    d | _        y y 7 B# t        $ r Y Kw xY w7 # t        $ r Y 'w xY wwr(   )r   close	Exceptionr   	__aexit__)r-   argss     r0   r   zLightpandaCrawler.__aexit__   s     ==$mm))+++ !DM'0d&&00$777  $D (	 , 
 8 sm   BA2 A0A2 BB "B#B '	B0A2 2	A>;B=A>>BB 	BBBBc                  K   	 | j                   j                  j                  | j                         d{   | _        d| _        t        j                  d| j                         y7 1# t        $ r }t        j                  d|       Y d}~nd}~ww xY w	 | j                   j                  j                  | j                         d{  7  | _        d| _        t        j                  d| j                         y# t        $ rD}t        j                  d|       t        d| j                   d	| j                   d
|      |d}~ww xY ww)uA   CDP 연결 시도. Lightpanda 우선, 실패 시 Chrome fallback.Nr}   zConnected to Lightpanda at %su8   Lightpanda 연결 실패 (%s), Chrome fallback 시도...chromez$Connected to Chrome (fallback) at %su!   Chrome fallback 연결 실패: %szLightpanda(u   )와 Chrome(u   ) 모두 연결 실패)r,   )r   chromiumconnect_over_cdpr~   r   r   loggerinfor   warningr   errorr@   )r-   lp_err
chrome_errs      r0   r   zLightpandaCrawler._connect   s    	_"&(("3"3"D"DTEVEV"WWDM'DLKK79J9JK X  	_NNUW]^^	_
	"&(("3"3"D"DTEYEY"ZZZDM#DLKK>@T@TU 	LL<jI&d//0T=Q=Q<RRhi  	sn   E2A* A(0A* 'E(A* *	B3B	EBE2C< 	C
1C< ;E<	E	?EE		Ec                   K   | j                   *	 | j                   j                          d{    d| _         | j                          d{    y7 $# t        $ r Y -w xY w7 w)u*   브라우저 재연결 (자동 복구용).N)r   r   r   r   r   s    r0   
_reconnectzLightpandaCrawler._reconnect   s_     ==$mm))+++ !DMmmo	 ,  	sC   A%A AA A%A#A%A 	A A%A  A%c                   K    j                   t        d      d fd}	  |        d{   S 7 # t        $ r}t        j	                  d|       	  j                          d{  7    |        d{  7  cY d}~S # t        $ rI}t        |      j                         }d|v rt        d |      |t        d	 d
| |      |d}~ww xY wd}~ww xY ww)uP   새 페이지를 열고 URL 을 로드. 실패 시 1회 재연결 후 재시도.Nu-   브라우저가 연결되지 않았습니다.)r+   c                    K   j                   J j                   j                          d {   } 	 | j                         d {    | S 7 "7 # t        $ r | j	                          d {  7    w xY ww)N)timeout)r   new_pagegotor   r   )pager-   r   r+   s    r0   
_open_pagez2LightpandaCrawler._ensure_page.<locals>._open_page   sw     ==,,,//11DiiZi888 K 28 jjl""sD   ,A=AA=A AA A=A A:2A53A::A=u1   페이지 로드 실패 (%s), 재연결 시도...r   u   타임아웃: r+   r,   u   페이지 로드 실패: u    — )r4   r
   )
r   r@   r   r   r   r   r3   rg   r<   r&   )r-   r+   r   r   	first_err
second_errerr_msgs   ```    r0   _ensure_pagezLightpandaCrawler._ensure_page   s     == &'V\_``		@#%%% 		@NNNPYZ@oo''''\))) @j///1'+nSE,BS]^dnn #<SEzl!SY\dnou	@		@st   "C$
9 79 C$9 	C!CB,A/-B<A?=BC!C$	CACCCC!!C$Nc                  K   ||n| j                   }t        j                         }| j                  ||       d{   }	 |j	                          d{   }|j                          d{   }|j                          d{    t        j                         |z
  dz  }t        |      }	t        ||      }
t        |      }t        |||	||
|d|| j                  	      S 7 7 7 z7 d# |j                          d{  7   w xY ww)u   단일 페이지 크롤링.

        Args:
            url: 크롤링할 URL
            timeout_ms: 요청 타임아웃 (None 이면 self.timeout_ms 사용)

        Returns:
            CrawlResult 인스턴스
        Ni     )	r+   rE   rF   rG   r   rJ   rL   rN   rO   )r   time	monotonicr   rE   rl   r   rs   ra   rm   rD   r   )r-   r+   r   effective_timeoutstartr   rE   rG   elapsedrF   r   rJ   s               r0   fetchzLightpandaCrawler.fetch  s      +5*@Jdoo &&s,=>>	**,&E'D**,>>#e+t3T"tS)T"<<

 
	
 ?&'$**,sj   :D
C$D
C, C&C, -C(.C, 2D
C*AD
&C, (C, *D
,D DDD
c                    K   |sg S t        j                  |      d fd}|D cg c]  }t        j                   ||             }}t        j                  |ddi d{   }t	        |      S c c}w 7 w)u\  대량 병렬 크롤링.

        asyncio.Semaphore 로 동시 요청 수를 제어합니다.

        Args:
            urls: 크롤링할 URL 목록
            concurrency: 최대 동시 요청 수 (기본 25)
            timeout_ms: 개별 요청 타임아웃

        Returns:
            CrawlResult 목록 (입력 URL 순서 유지)
        c                
  K   4 d {    	 j                  |        d {   cd d d       d {    S 7 17 7 	# t        $ r  t        $ r}t        d|  | |      |d }~ww xY w# 1 d {  7  sw Y   y xY ww)N)r   u   크롤링 실패: r   )r   r&   r   )r+   er-   semr   s     r0   
_fetch_onez0LightpandaCrawler.fetch_many.<locals>._fetch_oneT  s      Z ZZ!%CJ!GGZ Z ZGZ "   Z$'9#%?SPQRXYYZZ Z Zsk   B<BA.A>ABA BA BA+A&&A++A..B 4A75B <Breturn_exceptionsFN)r+   r3   r4   rD   )asyncio	Semaphorecreate_taskgatherlist)	r-   r   concurrencyr   r   utasksr   r   s	   `  `    @r0   
fetch_manyzLightpandaCrawler.fetch_many=  su     $ I,	Z >BB$$Z]3BBGGGG} CGs   &A>"A7A>'A<(A>c                L  K   | j                  || j                         d{   }	 	 |j                  |       d{   }	 |j                          d{    |S 7 :7 !# t        $ r}t	        d| ||      |d}~ww xY w7 -# |j                          d{  7   w xY ww)u   페이지에서 JavaScript 를 실행하고 결과를 반환.

        Args:
            url: 로드할 URL
            js_code: 실행할 JS 표현식 또는 함수

        Returns:
            JS 실행 결과 (JSON 직렬화 가능한 값)
        Nu   JS 실행 오류: r   )r   r   evaluater   rB   r   )r-   r+   js_coder   r]   r   s         r0   r   zLightpandaCrawler.evaluatea  s      &&sDOO<<	V#}}W55 **, = 6 V"%7s#;ANTUUV $**,sl    B$AB$A! AA! B$BB$A! !	B*A<<BB B$B!BB!!B$c                P  K   | j                  || j                         d{   }	 i }|j                         D ]  \  }}t        j                  d|      }|r%|j                  d      }|d|j                          }	nd}|}	t        |      rp|j                  |	       d{   }
g }|
D ]J  }|r|j                  |       d{   }n|j                          d{   }|:|j                  |       L |||<   |j                  |	       d{   }|d||<   |r|j                  |       d{   ||<   |j                          d{   ||<   ! 	 |j                          d{    |S 7 U7 7 7 7 n7 M7 27 # |j                          d{  7   w xY ww)u  CSS 셀렉터 기반 구조화 데이터 추출.

        셀렉터 문법:
          - 기본: "h1" → 첫 번째 매칭 요소의 텍스트
          - ::attr(name): 지정 속성 값 (예: "a::attr(href)")
          - 복수형 키 (links, items 등): 모든 매칭 요소의 리스트

        Args:
            url: 크롤링할 URL
            selectors: {"결과키": "CSS셀렉터"} 매핑

        Returns:
            {"결과키": 추출값} 딕셔너리
        Nz::attr\(([^)]+)\)$rc   )r   r   r   rU   searchrf   r   ry   query_selector_allget_attribute
inner_textr[   query_selectorr   )r-   r+   	selectorsr   r]   rx   selector
attr_match	attr_namepure_selectorelementsvalueselvals                 r0   extract_structuredz$LightpandaCrawler.extract_structuredu  s     &&sDOO<<"	%'F!*!2 <XYY'<hG
 * 0 0 3I$,-Az/?/?/A$BM $I$,M!#&%)%<%<]%KKH(*F& /$(*(8(8(C"CC(*"7C?"MM#./ #)F3K  $22=AABz&*s",.,<,<Y,G&Gs,.MMO&;s=<@ **,K =  L #D"7 B 'H&;$**,s    F&E7F&A9F !E:"!F E<F E>F $-F F "F 4F5F F
F F&0F1F&:F <F >F  F F F F&F#FF##F&c                    K   t        d      w)uM   미구현: 스크린샷이 필요하면 Playwright+Chrome을 사용하세요.u~   screenshot()은 이 모듈에서 지원하지 않습니다. 스크린샷이 필요하면 Playwright+Chrome을 사용하세요.NotImplementedErrorr-   r+   kwargss      r0   
screenshotzLightpandaCrawler.screenshot  s     ! P
 	
   c                    K   t        d      w)uK   미구현: PDF 생성이 필요하면 Playwright+Chrome을 사용하세요.uu   pdf()는 이 모듈에서 지원하지 않습니다. PDF 생성이 필요하면 Playwright+Chrome을 사용하세요.r   r   s      r0   pdfzLightpandaCrawler.pdf  s     ! G
 	
r   )zws://127.0.0.1:9333zws://127.0.0.1:9222i0u  )r~   r3   r   r3   r   rK   r4   r5   )r4   z'LightpandaCrawler')r   r   r4   r5   )r4   r5   )r+   r3   r   rK   r4   r
   r(   )r+   r3   r   
int | Noner4   rD   )   N)r   rH   r   rK   r   r   r4   zlist[CrawlResult])r+   r3   r   r3   r4   r   )r+   r3   r   rI   r4   zdict[str, Any])r+   r3   r   r   r4   bytes)r6   r7   r8   r9   r*   r   r   r   r   r   r   r   r   r   r   r   r>   r1   r0   r{   r{      s     24	%% % 	%
 
%$$$0@@$
R !%	"" " 	"
 
"H(4t

r1   r{   )rG   r3   r\   r3   r4   rH   )rG   r3   r4   rI   )rG   r3   r4   r3   )rx   r3   r4   bool)"r9   
__future__r   r   loggingrU   r   dataclassesr   r   typingr   urllib.parser   r   playwright.async_apir	   r
   r   	getLoggerr   	frozensetrv   r   r&   r<   r@   rB   rD   ra   rm   rs   ry   r{   r>   r1   r0   <module>r      s    #   	  (  * @ @			/	0 < 
 : #: #   &"8V
 V
r1   