o
    BSi                     @   s  d Z ddlZejjdd ddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlmZ ddlmZmZ ddlmZmZmZmZ ddlZeejZed Zed	 Zed
 Zed ZdZdddgddddddgddddddgddddddgdddddgddddZdZ g dZ!g d Z"g d!Z#d"Z$d"Z%ddl&Z&e&j'(d#d$Z)e&j'(d%d$Z*d&e+d'e+fd(d)Z,dd*e+d+e-d'e-fd,d-Z.d*e+d'e-fd.d/Z/dd1e+d*e+d2e+fd3d4Z0d'e1fd5d6Z2d7e3d'e+fd8d9Z4d:e+d'e+fd;d<Z5d=e6d>e7d'e+fd?d@Z8dAe+d'e+fdBdCZ9dAe+d'e+fdDdEZ:dFe+dGe7dHe7dIe7d'e+f
dJdKZ;dLe+d'e1fdMdNZ<dOej=dFe+d'e+fdPdQZ>dRdS Z?e? Z@dTdU ZAg dVZBddXe-dYe7d'ee+ fdZd[ZCd\ed]e+d^e+d'ee+ fd_d`ZDdd]e+d^e+dYe7dae-d'eEe+e7f f
dbdcZFd'e1fdddeZGdLe+d:e+d'e-fdfdgZHdhe+d'eee+ef  fdidjZIdLe+d'eee+ef  fdkdlZJdOej=d=eee+ef  d'eEfdmdnZK	ddOej=dFe+dGe7doe7dpe7dqe+dre+fdsdtZLdudv ZMdwdx ZNeOdykrddlPZPePjQdzd{ZReRjSd|d}d~d eRjSdd}dd eRjSdd}dd eRjSdd}dd eRT ZUeUj6rNeVd eVd eW D ]0\ZXZYdZeYd Z[eVdeX deYd   eVde[ deYd   eVdeYd   eV  qdS eUj\rWeN  dS eUj]rpeM Z^eG Z_e`e^rkd dS d dS eM Z^e`e^rzdnd dS dS )u  
MOPS 重大訊息每日收集器
========================
每日自動下載證交所重大訊息 CSV，解析估價師相關公告，存入資料庫。

使用方式：
    python collector.py              # 下載今天的資料
    python collector.py --backfill   # 補抓（如果今天已抓過則跳過）

排程設定（Windows Task Scheduler）：
    每日 18:00 執行（確保當天公告已更新完畢）
    Nutf-8encoding)Path)datetime	timedelta)OptionalListDictAnydatarawzmops_announcements.dblogsz3https://mopsfin.twse.com.tw/opendata/t187ap04_L.csvu   公司基本資料LOZmonthlyu?   公司基本資料（營業項目、資本額、董事長等）)namemarkets	frequencydescriptionu   重大訊息Zdailyu*   重大訊息公告（主要收集對象）u	   月營收u   每月營業收入資料u   內部人持股異動u!   董監事、經理人持股變動u	   季報EPSZ	quarterlyu   每季每股盈餘資料)Zt187ap03Zt187ap04Zt187ap05Zt187ap11Zt187ap14z$https://mopsfin.twse.com.tw/opendata)u   不動產估價師u   估價師事務所u   專業估價者u   估價報告u   鑑價)%u	   不動產u   土地u   廠房u   倉庫u   建物u   房屋u   大樓u   辦公大樓   租賃u   租入u   租出   出租   承租u   租約u   合建u   都更u   都市更新u   權利變換u   危老u   使用權資產u	   地上權u   興建u   新建u   擴建u   改建u   碼頭u   港口u	   停車場u   倉儲u   合作開發u   共同開發u   投資開發u   建案u   工廠u   廠辦u   營業據點u   營運據點)u   有價證券u	   公司債u	   金融債u   基金u   理財u   存款u   授信資產u	   次順位u   現金增資u   專利u   訴訟u   裁罰u   背書保證u   資金貸與u   股票面額u   轉換公司債u   私募u   法人說明會u	   法說會u   薪酬委員TTELEGRAM_BOT_TOKEN TELEGRAM_CHAT_IDtextreturnc                 C   s$   | sdS |  dd dd ddS )u   轉義 HTML 特殊字元r   &z&amp;<z&lt;>z&gt;)replace)r    r!   ]C:\Users\User\Documents\GitHub\Research_zoo\projects\mops-announcement-collector\collector.pyescape_htmlw   s   r#   messageuse_htmlc              
   C   s
  t sdS trtstd dS z)dt d}t| |rdnddd}d	d
 | D }tj||dd}|  W dS  t	y } z@td|  |ryt
d | dddddddddddddd}t|ddW  Y d}~S W Y d}~dS d}~ww )u   發送 Telegram 訊息

    Args:
        message: 訊息內容（支援 HTML 或純文字）
        use_html: 是否使用 HTML 格式（預設 True，更穩定）

    Returns:
        是否發送成功
    FuX   Telegram 設定不完整，請設定環境變數 TELEGRAM_BOT_TOKEN 和 TELEGRAM_CHAT_IDzhttps://api.telegram.org/botz/sendMessageZHTMLNT)Zchat_idr   Z
parse_modeZdisable_web_page_previewc                 S   s   i | ]\}}|d ur||qS Nr!   ).0kvr!   r!   r"   
<dictcomp>   s    z)send_telegram_message.<locals>.<dictcomp>
   )Zjsontimeoutu   Telegram 通知發送失敗: u*   嘗試使用純文字模式重新發送...z<b>r   z</b>z<i>z</i>z	<a href="z"> z</a>r%   )ENABLE_TELEGRAM_NOTIFICATIONr   r   loggingwarningitemsrequestsZpostraise_for_status	Exceptioninfor    send_telegram_message)r$   r%   urlZpayloadresponseeZ
plain_textr!   r!   r"   r7      s:   



r7   c                 C   s   d|  d}t |ddS )u   發送測試用 Telegram 訊息（帶有明顯標記）

    用於開發測試，避免與正式通知混淆。

    Args:
        message: 測試訊息內容

    Returns:
        是否發送成功
    u   🧪 <b>[測試訊息]</b>

u-   

<i>此為測試訊息，非正式通知</i>Tr.   )r7   )r$   Ztest_messager!   r!   r"   send_telegram_test   s   r;   shorttitledurationc              
   C   sx   t sdS zd| d|  d| d}tjdddd	|gd
dd W dS  ty; } ztd|  W Y d}~dS d}~ww )u   發送 Windows Toast 通知

    Args:
        title: 通知標題
        message: 通知內容
        duration: "short" (5秒) 或 "long" (25秒)
    Na  
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null

$template = @"
<toast duration="zL">
    <visual>
        <binding template="ToastGeneric">
            <text>z</text>
            <text>a*  </text>
        </binding>
    </visual>
</toast>
"@

$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("MOPS Collector").Show($toast)
Z
powershellz-ExecutionPolicyZBypassz-CommandTr+   )Zcapture_outputr,   u   Toast 通知發送失敗: )ENABLE_TOAST_NOTIFICATION
subprocessrunr5   r0   r1   )r=   r$   r>   Z	ps_scriptr:   r!   r!   r"   send_windows_toast   s&   	rB   c            
      C   s   t  sdddddS tt } |  }t }|t| d }|	d}|	dd }|
d|f | d }|
d|f | d }|
d|f | d }|
d|f | d }	|   ||||	dS )	u'   取得累計統計（本週、本月）r   )weekmonthweek_amountmonth_amount)days%Y%m%d%Y%mZ01zt
        SELECT COUNT(*) FROM announcements
        WHERE has_appraiser_info = 1
        AND announce_date >= ?
    z
        SELECT COALESCE(SUM(appraisal_amount), 0) FROM appraiser_records r
        JOIN announcements a ON r.announcement_id = a.id
        WHERE a.announce_date >= ?
    )DB_PATHexistssqlite3connectcursorr   nowr   weekdaystrftimeexecutefetchoneclose)
connrN   todayZ
week_startZweek_start_strZmonth_start_strZ
week_countZmonth_countrE   rF   r!   r!   r"   get_cumulative_stats   s<   

rW   amountc                 C   s@   | sdS | dkr| d ddS | dkr| d ddS | dS )u   格式化金額顯示r    .2fu    億i'  z,.0fu    萬r!   )rX   r!   r!   r"   format_amount!  s   r[   subjectc                    sZ   |    t fdddD rdS t fdddD rdS t fddd	D r+d
S dS )u   分類交易類型c                 3       | ]}| v V  qd S r&   r!   r'   r(   Zsubject_lowerr!   r"   	<genexpr>0      z)categorize_transaction.<locals>.<genexpr>)   取得u   購置u   購買r   r   u   承攬rb   c                 3   r]   r&   r!   r^   r_   r!   r"   r`   2  ra   )   處分u   出售r   rc   c                 3   r]   r&   r!   r^   r_   r!   r"   r`   4  ra   )u   續租   續約rd   u   其他)lowerany)r\   r!   r_   r"   categorize_transaction-  s   rg   rowstotal_appraiserc                 C   s   t  d}g d}|t    }t }dd | D }dd | D }g }i }	|D ]I}
t|
d }d}d}|rV|d }|d	dpCd}|d
d}|rV|	|dd |	|< ||
d |
d |
d ||t|
d |rn|d ndd q*|j	dd dd dd |D }dd |D }t
dd |D }d}|d| d| d7 }|d d7 }|d7 }|d t|  d!7 }|d"| d#7 }|dkr|d$t| d%7 }|d&7 }|d'|d(  d#7 }|d) dkr|d*t|d)  d%7 }|d&7 }|d+|d,  d#7 }|d- dkr|d*t|d-  d%7 }|d&7 }|rh|d&d d&7 }|d.7 }|D ]E}t|d }t|d }|d/| d0| d17 }|d2t|d3  d47 }|d5 r\|d6t|d5 dd7  d&7 }|d8|d9  d&7 }q"|r|d&d d&7 }|d:t| d;7 }|D ]c}t|d }t|d }|d/| d0| d17 }|d3 dkr|d<t|d3  d=7 }|d5 r|d6t|d5 dd7  d&7 }|d> r|d> d?rt|d> d? dd@ }|dA| d&7 }q|	r|d&d d&7 }|dB7 }t|	 dCd dd}|ddD D ]\}}t|ddE }|dF| dG| d!7 }q|rr|d&d d&7 }|dHt| d;7 }|ddD D ]'}
t|
d }t|
d }t|
d dd7 }|dI| d0| dG| d&7 }q7t|dDkrr|dJt|dD  d!7 }|d&d d&7 }|dK7 }|S )Lu   建構每日摘要通知%Y-%m-%du   一u   二u   三u   四u   五u   六u   日c                 S   s   g | ]}|d  r|qS )has_appraiser_infor!   r'   rr!   r!   r"   
<listcomp>D  s    z'build_daily_summary.<locals>.<listcomp>c                 S   s"   g | ]}| d r|d s|qS )is_real_estate_relatedrl   )getrm   r!   r!   r"   ro   E  s   " contentr   Nappraisal_amountappraiser_firmr      
stock_codecompany_namer\   )rv   rw   r\   rX   firmcategoryr6   c                 S      | d S )NrX   r!   xr!   r!   r"   <lambda>c      z%build_daily_summary.<locals>.<lambda>T)keyreversec                 S   s   g | ]
}|d  dkr|qS rX   rY   r!   r'   dr!   r!   r"   ro   f      c                 S   s   g | ]
}|d  dk r|qS r   r!   r   r!   r!   r"   ro   g  r   c                 s       | ]}|d  V  qdS )rX   Nr!   r   r!   r!   r"   r`   j  ra   z&build_daily_summary.<locals>.<genexpr>u   📰 <b>MOPS 每日摘要</b>
   📅 u    (週z)
uK   ─────────────────────────z

u   📊 <b>統計總覽</b>
u   今日公告:     筆
   估價相關:     筆u    | 總金額 u   元
u   本週累計: rC   rE   z | u   本月累計: rD   rF   u2   🔥 <b>重大交易亮點</b> (金額 &gt; 1億)

<b>[] </b>
   💰 <b>rX      元</b>
rx   u   📍 (      📋 ry   u#   🏛 <b>估價師相關公告</b> (u    筆)
u   💰 u   元
r6   notes2      📝 u   🏢 <b>今日事務所</b>
c                 S   rz   )Nru   r!   r{   r!   r!   r"   r}     r~         u   • : u   🏢 <b>其他不動產</b> (   • [
   ...還有 u>   🔗 <a href="https://mops.twse.com.tw/">MOPS 公開資訊</a>)r   rO   rQ   rP   rW   extract_appraiser_inforq   appendrg   sortsumlenr[   r#   sortedr2   )rh   ri   Z	today_strweekday_namesrP   statsZappraiser_rowsZreal_estate_onlyZdealsfirmsrowZappraiser_inforX   rx   r6   Z	big_dealsZnormal_dealsZtoday_totalmsgZdealZstockZcompanyr   Zsorted_firmscountZ
firm_shortsubject_shortr!   r!   r"   build_daily_summary:  s   


 
  r   roc_datec                 C   sT   | rt | dkr
| S t| dd d }| dd }| dd }| d| d| S )u7   民國日期轉西元顯示（1141226 → 2025/12/26）   N   w  r   /)r   int)r   yearrD   dayr!   r!   r"   roc_to_date_str  s   r   c                 C   sx   | rt | dkr
dS t| dd d }t| dd }t| dd }g d}zt|||}||  W S    Y dS )u   取得星期幾r   r   Nr   r   r   rk   )r   r   r   rP   )r   r   rD   r   r   r   r!   r!   r"   get_weekday_str  s   r   csv_datetotalappraiser_count	new_countc                 C   s   t | }t| }t d}d}|d| d| d7 }|d| d7 }|d7 }|d	| d
7 }|d| d
7 }||k rD|d| d
7 }|dkrL|d7 }|S )u'   建構收集狀態通知（簡短版）z%H:%Mu   ✅ <b>MOPS 收集完成</b>
u   📅 資料日期：   （週u   ）
u   ⏰ 執行時間：r   u(   ─────────────
u   📊 總公告：r   u   🏛 估價相關：u   💾 新增入庫：r   u/   
<i>詳細報告將於下一則訊息發送</i>)r   r   r   rO   rQ   )r   r   r   r   date_strrP   rO   r   r!   r!   r"   build_status_notification  s   r   rr   c                 C   s   i }t d| }|r|d dd |d< t d| }|r,|d dd |d< t d	| }|r=|d |d
< d| v sEd| v rId|d< t d| }|r^|d dd |d< t d| }|rzd|dvrz|d dd |d< |S )u!   從公告內容提取關鍵資訊u    交易總金額[：:]\s*([^\n]+)ru   N<   total_amountu4   標的物之名稱及性質[^:：]*[：:]\s*([^\n]+)P   targetuA   預計參與投入之金額[：:]\s*([0-9,.]+\s*(?:億|萬)?元?)
investmentu"   本次交易為關係人交易:是u$   本次交易為關係人交易：是Tis_related_partyu    每單位價格[：:]\s*([^\n]+)r   
unit_priceu   估價結果[：:]\s*([^\n]+)	   不適用Zappraisal_result)researchgroupstrip)rr   r6   matchr!   r!   r"   extract_key_info_from_content  s&   r   rU   c              	   C   s  |   }t|}t|}|d|f | }|sd| dS d}|d| d| dt| d7 }|d	7 }g }g }d
}	|D ]#}
|
\}}}}}}}}|sO|sO|r[||
 |rZ|	|7 }	q=||
 q=|r6|d7 }|D ]}
|
\}}}}}}}}t|}|dt| dt| d7 }t|dkr|dd d n|}|dt| d7 }|r|dt	| d7 }n|
dr|dt|d dd  d7 }|r|dd
  }tdd|}td|}|r|d}|dd }|dt| d7 }|r|dd
  }td d|}|dd! }|r|d"t| d7 }|
d#r$|d$7 }|r5|d%t|dd&  d7 }qj|r|d't| d(7 }|dd) D ]Z}
|
\}}}}}}}}t|}t|d*krh|dd* d n|}|d+t| dt| 7 }|
d,rtd-|d, }|r|d.|d d/7 }|
d#r|d07 }|d7 }qIt|d)kr|d1t|d)  d7 }|d27 }|	d
kr|d3t	|	 d7 }|d47 }|S )5u   建構詳細報告a  
        SELECT
            a.stock_code,
            a.company_name,
            a.subject,
            a.content,
            r.appraiser_firm,
            r.appraiser_names,
            r.appraisal_amount,
            r.notes
        FROM announcements a
        LEFT JOIN appraiser_records r ON a.id = r.announcement_id
        WHERE a.has_appraiser_info = 1 AND a.announce_date = ?
        ORDER BY r.appraisal_amount DESC NULLS LAST
    u   📭 u    無估價相關公告u%   📊 <b>估價案件詳細報告</b>
r   r   u   ）共 r   uL   ─────────────────────────
r   u   
🏛 <b>有委託估價</b>
r   r   r   r   Nz...r   r   r   r   r   u   💰 投入：   ;u   ^估價機構[：:]\s*r   u&   (.+?(?:事務所|估價公司|公司))ru      u   🏢 u#   ^.*?(?:事務所|公司)[-:：]?\s*r   u   👤 r   u   ⚠️ 關係人交易
r   -   u   
📄 <b>未委託估價</b>（u    筆）
r      r   r   u   (\d[\d,]*)\s*元u    💵u   元/坪u
    ⚠️關r   uM   
─────────────────────────
u    📈 有金額案件合計：<b>u1   🔗 <a href="https://mops.twse.com.tw/">MOPS</a>)rN   r   r   rR   fetchallr   r   r   r#   r[   rq   splitr   r   subr   r   )rU   r   rN   r   rP   rh   r   Zreal_appraisalZno_appraisalr   r   rv   rw   r\   rr   rx   namesrX   r   Zkey_infor   Z
firm_cleanr   Znames_cleanZprice_matchr!   r!   r"   build_detail_report  s    
 
 
"
r   c                  C   sZ   t jddd t dt d d } tjtjdtj| ddt	t
jgd	 ttS )
u   設定日誌Tparentsexist_okZ
collector_rI   z.logz'%(asctime)s [%(levelname)s] %(message)sr   r   )levelformathandlers)LOG_DIRmkdirr   rO   rQ   r0   ZbasicConfigZINFOZFileHandlerZStreamHandlersysstdoutZ	getLogger__name__)Zlog_filer!   r!   r"   setup_logging  s   

r   c                  C   s   t jjddd tt } |  }|d z|d W n
 tjy'   Y nw |d z|d W n
 tjy>   Y nw |d |d |d	 |d
 |d |   | S )u   初始化 SQLite 資料庫Tr   u(  
        CREATE TABLE IF NOT EXISTS announcements (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            publish_date TEXT NOT NULL,           -- 出表日期（民國）
            announce_date TEXT NOT NULL,          -- 發言日期（民國）
            announce_time TEXT,                   -- 發言時間
            stock_code TEXT NOT NULL,             -- 公司代號
            company_name TEXT NOT NULL,           -- 公司名稱
            subject TEXT NOT NULL,                -- 主旨
            regulation TEXT,                      -- 符合條款
            event_date TEXT,                      -- 事實發生日
            content TEXT,                         -- 說明（完整內容）
            has_appraiser_info INTEGER DEFAULT 0, -- 是否包含估價師資訊
            is_real_estate_related INTEGER DEFAULT 0, -- 是否與不動產相關（都更、合建、廠房等）
            created_at TEXT DEFAULT CURRENT_TIMESTAMP,
            UNIQUE(stock_code, announce_date, announce_time, subject)
        )
    zMALTER TABLE announcements ADD COLUMN is_real_estate_related INTEGER DEFAULT 0u  
        CREATE TABLE IF NOT EXISTS appraiser_records (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            announcement_id INTEGER REFERENCES announcements(id),
            appraiser_firm TEXT,                  -- 估價師事務所
            appraiser_names TEXT,                 -- 估價師姓名（逗號分隔）
            appraiser_licenses TEXT,              -- 開業證書字號（逗號分隔）
            appraisal_amount BIGINT,              -- 估價金額
            notes TEXT,                           -- 備註（外幣金額、分配比例等）
            raw_text TEXT,                        -- 原始文字（用於驗證）
            created_at TEXT DEFAULT CURRENT_TIMESTAMP
        )
    z3ALTER TABLE appraiser_records ADD COLUMN notes TEXTu  
        CREATE TABLE IF NOT EXISTS collection_log (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            collect_date TEXT UNIQUE NOT NULL,    -- 收集日期（西元）
            csv_date TEXT,                        -- CSV 中的日期（民國）
            total_count INTEGER,                  -- 總公告數
            appraiser_count INTEGER,              -- 估價相關公告數
            file_size INTEGER,                    -- 檔案大小（bytes）
            status TEXT,                          -- success / failed
            error_message TEXT,
            created_at TEXT DEFAULT CURRENT_TIMESTAMP
        )
    zQCREATE INDEX IF NOT EXISTS idx_announcements_date ON announcements(announce_date)zOCREATE INDEX IF NOT EXISTS idx_announcements_stock ON announcements(stock_code)z[CREATE INDEX IF NOT EXISTS idx_announcements_appraiser ON announcements(has_appraiser_info)zaCREATE INDEX IF NOT EXISTS idx_announcements_real_estate ON announcements(is_real_estate_related))	rJ   parentr   rL   rM   rN   rR   OperationalErrorcommit)rU   rN   r!   r!   r"   init_database  s,   







r   )   i  i0*  r   save_rawmax_retriesc                 C   sj  t dt  td|d D ]}zXtjtdd}|  |j}| rbtj	ddd t
 d}td| d	 }t|d
}|| W d   n1 sNw   Y  t d| dt|dd |dW   S  tjy } z<||k rt|d  }	|	d }
t d| d| d|  t d|
 d t|	 nt d| d|  W Y d}~qd}~ww dS )u  下載 CSV 並回傳內容，支援自動重試（指數退避）

    重試策略：
        - 第 1 次失敗：等待 3 分鐘後重試
        - 第 2 次失敗：等待 30 分鐘後重試
        - 第 3 次失敗：等待 3 小時後重試

    Args:
        save_raw: 是否儲存原始檔案
        max_retries: 最大重試次數（預設 3 次）

    Returns:
        CSV 內容字串，失敗時回傳 None
       正在下載: ru   r   r,   Tr   rH   Zt187ap04_L_.csv.gzwbNu   原始檔案已儲存:  (, bytes)z	utf-8-sig   下載失敗 (第 r       次):    等待     分鐘後重試...u   下載失敗 (已重試 u    次，放棄): )loggerr6   CSV_URLranger3   rq   r4   rr   RAW_DIRr   r   rO   rQ   gzipopenwriter   decodeRequestExceptionRETRY_DELAYSr1   timesleeperror)r   r   attemptr9   rr   rV   raw_filefr:   delay	delay_minr!   r!   r"   download_csv  s4   
r  dataset_dirdataset_codemarketc                 C   st   | d| d}t | |dd}|sdS t|d d}| }t| W  d   S 1 s3w   Y  dS )u   取得該資料集最新檔案的 MD5 hash

    Args:
        dataset_dir: 資料集目錄
        dataset_code: 資料集代碼
        market: 市場別

    Returns:
        MD5 hash 字串，無檔案時回傳 None
    _z	_*.csv.gzT)r   Nr   Zrb)r   Zglobr   r   readhashlibmd5	hexdigest)r  r  r  patternfilesr   rr   r!   r!   r"   get_latest_file_hash.  s   $r  skip_unchangedc                 C   s  |  d| d}t  d| }td|  td|d D ]}ztj|dd}|  |j}t|  }	|	j	ddd	 |r`t
| }
t|	| |}|r`|
|kr`td
|  dt|fW   S t d}|	|  d| d| d }t|d}|| W d   n1 sw   Y  td|j dt|dd dt|fW   S  tjy } zH||k r|d ttk rt|d  ntd }|d }td| d| d|  td| d t| ntd| d|  W Y d}~qd}~ww dS )uy  下載指定資料集並儲存（支援重複檢查）

    Args:
        dataset_code: 資料集代碼（如 t187ap03）
        market: 市場別（L=上市, O=上櫃）
        max_retries: 最大重試次數
        skip_unchanged: 是否跳過內容未變更的檔案

    Returns:
        (狀態, 檔案大小 bytes)
        狀態: 'saved' | 'unchanged' | 'failed'
    r  z.csvr   r   ru   r   r   Tr   u   內容未變更，跳過: 	unchangedrH   r   r   Nu   已儲存: r   r   r   savedr   r   r   r   u   下載失敗 (): )failedr   )OPENDATA_BASE_URLr   r6   r   r3   rq   r4   rr   r   r   r  r  r	  r  r   r   rO   rQ   r   r   r   r   r   r   r1   r   r   r   )r  r  r   r  filenamer8   r   r9   rr   r  Znew_hashZold_hashrV   r   r   r:   r   r   r!   r!   r"   download_datasetE  sD    $
r  c                  C   s.  t d t d t d i } t D ]\}}|d }|d }t d| d| d d}d}d}d}|D ].}	d	d
ddd|	|	}
t||	\}}|dkrX|d7 }||7 }q7|dkra|d7 }q7|d7 }q7|||||d| |< |dkrwd}n	|dkr~d}nd}g }|dkr|| d |dkr|| d |dkr|| d d|}t | d| d| d|dd qtdd | 	 D }tdd | 	 D }td d | 	 D }td!d | 	 D }t d" t d#| d$| d%| d t d&|dd'|d( d( d)d* t d | S )+u   收集所有資料集（支援重複檢查）

    Returns:
        收集結果摘要 {dataset_code: {'saved': int, 'unchanged': int, 'failed': int, 'size': int}}
    2==================================================u   開始收集所有資料集r   r   z
--- r   z) ---r   u   上市u   上櫃u   公開發行u   興櫃)r   r   PRr  ru   r  )r   r  r  r  sizeu   ✗u   ✓u   ○u    新增u
    未變更u    失敗, r-   r   r   z bytesc                 s   r   )r  Nr!   rm   r!   r!   r"   r`     ra   z'collect_all_datasets.<locals>.<genexpr>c                 s   r   )r  Nr!   rm   r!   r!   r"   r`     ra   c                 s   r   )r  Nr!   rm   r!   r!   r"   r`     ra   c                 s   r   )r  Nr!   rm   r!   r!   r"   r`     ra   3
==================================================u   收集完成: u	    新增, u    未變更, u   新增檔案大小: z bytes (i   rZ   z MB))
r   r6   DATASETSr2   rq   r  r   joinr   values)resultsr  configZdataset_namer   Zsaved_countZunchanged_countZfailed_countZ
total_sizer  Zmarket_namestatusr  ZiconZstatus_partsZ
status_strZtotal_savedZtotal_unchangedZtotal_failedr!   r!   r"   collect_all_datasets  sd   






	
&
$
r"  c                    s>   | d|   t  fddtD rdS t  fddtD S )u   判斷公告是否與不動產相關

    Args:
        content: 公告說明內容
        subject: 公告主旨

    Returns:
        是否與不動產相關
    r-   c                 3   r]   r&   r!   r'   kwZ	full_textr!   r"   r`     ra   z)is_real_estate_related.<locals>.<genexpr>Fc                 3   r]   r&   r!   r#  r%  r!   r"   r`     ra   )rf   REAL_ESTATE_EXCLUDE_KEYWORDSREAL_ESTATE_KEYWORDS)rr   r\   r!   r%  r"   rp     s   
rp   csv_contentc                    s   g }t |  }|D ]S}|dd |dd}t fddtD }t |}||dd|dd|dd|d	d|d
d||dd|dd |rTdnd|rYdndd q|S )u   解析 CSV 內容u   說明r   u   主旨c                 3   r]   r&   r!   r#  rr   r!   r"   r`     ra   zparse_csv.<locals>.<genexpr>u   出表日期u   發言日期u   發言時間u   公司代號u   公司名稱u   符合條款u   事實發生日ru   r   )publish_dateannounce_dateannounce_timerv   rw   r\   
regulation
event_daterr   rl   rp   )csvZ
DictReader
splitlinesrq   rf   APPRAISER_KEYWORDSrp   r   )r(  rh   readerr   r\   Zhas_appraiserZreal_estate_relatedr!   r)  r"   	parse_csv  s*   










r3  c              
      s  g }g d dt dtf fdddt dtt  ffddd}d	}d
}d}t|| tj}t|| tj}t|| tj}t|| }	dd fdd|D D }
dd fdd|D D }dd fdd|D D }g }td| }|r|dd|dd   g d}|D ]%}t|| }|rd|v rdnd|v rdnd}|| d|	d  qd d!g}|D ]}t|| }|r|d"|	d |	d   nqtd#| }|r|	s|d$|	d d% |std&| }|rd'|	dvr|d(|	ddd)   |
s|rQ||
rd|
nd|r'd|nd|r0d|nd|	r>t
|	d* d+d,nd|rGd|nd| dd- d. |S )/u'   從公告內容中提取估價師資訊)u
   ^不適用u   ^無$u   ^否$z^N/?A$z^-$u   ^。$z^\s*$valuer   c                    sT   | sdS |   }  D ]}t|| tjr dS q
td| r dS t| dk r(dS dS )u   檢查是否為無效值Tu   [:：]\s*不適用   F)r   r   r   Z
IGNORECASEr   r   )r4  r
  )EXCLUDE_PATTERNSr!   r"   
is_invalid  s   z*extract_appraiser_info.<locals>.is_invalidc                    s   | sdS |   } tdd| } tdd| } ddg}|D ]}t|| }|r0| d|    } qtdd| }  | r>dS | S )u*   清理提取的值，過濾掉無效內容Nz	^\d+\.\s*r   uO   ^(?:估價事務所|專業估價者事務所|專業估價師事務所)[:：]\s*u]   \d+\.\s*(?:不動產估價師|專業估價師|估價金額|估價報告|取得|處分|交易)u:   (?:姓名|字號|金額|目的|日期|價格|條件)[:：]u   [。，、；：]+$)r   r   r   r   start)r4  Zstop_patternsr
  r   )r7  r!   r"   clean_value  s"   z+extract_appraiser_info.<locals>.clean_valueu   (?:專業估價者事務所|專業估價師事務所|不動產估價師事務所)[^:：]*[:：]\s*([^,，\n]+?)(?=\s*(?:\d+\.|估價金額|$))uo   (?:專業估價師姓名|不動產估價師姓名)[^:：]*[:：]\s*([^,，\n]+?)(?=\s*(?:\d+\.|開業證書|$))u   (?:專業估價師開業證書字號|不動產估價師開業證書字號)[^:：]*[:：]\s*([^,，\n]+?)(?=\s*(?:\d+\.|估價報告|$))uD   估價金額[^0-9]*?(?:新台幣|新臺幣|NT\$?)?\s*([0-9,]+)\s*元c                 S      g | ]}|r|qS r!   r!   r'   r   r!   r!   r"   ro   P      z*extract_appraiser_info.<locals>.<listcomp>c                       g | ]} |qS r!   r!   r;  r9  r!   r"   ro   P  r<  c                 S   r:  r!   r!   r'   nr!   r!   r"   ro   Q  r<  c                    r=  r!   r!   r?  r>  r!   r"   ro   Q  r<  c                 S   r:  r!   r!   r'   lr!   r!   r"   ro   R  r<  c                    r=  r!   r!   rA  r>  r!   r"   ro   R  r<  uB   (?:日幣|日圓|JPY|美金|美元|USD)\s*[0-9,]+(?:\s*\([^)]+\))?u   外幣: z; Nr5  )u+   地主分配比例[：:]\s*([0-9.%~～\-]+)u+   建方分配比例[：:]\s*([0-9.%~～\-]+)u+   共同負擔比例[約]?\s*([0-9.%~～\-]+)u   地主u   地主比例u   建方u   建方比例u   共同負擔r   ru   uV   預計投入總成本[^0-9]*?(?:約)?(?:新台幣|新臺幣)?\s*([0-9,.]+)\s*(億|萬)uM   投資金額[^0-9]*?(?:約)?(?:新台幣|新臺幣)?\s*([0-9,.]+)\s*(億|萬)u   總成本: u+   (?:月租金?|租金)[^0-9]*([0-9,]+)\s*元u   月租金: u    元u&   估價結果[：:]\s*([^。\n]{10,60})r   u   估價結果: #   r   r   r   i  )rt   appraiser_namesappraiser_licensesrs   r   raw_text)strboolr   r   ZfindallZ	MULTILINEr   r  r   r   r   r    )rr   r  Zpattern1Zpattern2Zpattern3Zpattern4Z	firms_rawZ	names_rawZlicenses_rawZamountsr   r   Zlicensesr   Zforeign_amountsZratio_patternsr
  r   Z
ratio_typeZcost_patternsZ
cost_matchZ
rent_matchZ
eval_matchr!   )r6  r9  r7  r"   r     sn   
  
	r   c           
      C   s2  |   }d}d}|D ]}zd|d|d |d |d |d |d |d |d	 |d
 |d |d |ddf |jdkro|d7 }|d ro|d7 }|j}t|d }|D ]}|d||d |d |d |d |d|d f qRW q
 tjy }	 zt	d|d  d|	  W Y d}	~	q
d}	~	ww | 
  ||fS )u8   儲存到資料庫，回傳 (新增數, 估價相關數)r   a8  
                INSERT OR IGNORE INTO announcements
                (publish_date, announce_date, announce_time, stock_code, company_name,
                 subject, regulation, event_date, content, has_appraiser_info, is_real_estate_related)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            r*  r+  r,  rv   rw   r\   r-  r.  rr   rl   rp   ru   a/  
                            INSERT INTO appraiser_records
                            (announcement_id, appraiser_firm, appraiser_names,
                             appraiser_licenses, appraisal_amount, notes, raw_text)
                            VALUES (?, ?, ?, ?, ?, ?, ?)
                        rt   rD  rE  rs   r   rF  u   儲存失敗 (r  N)rN   rR   rq   rowcount	lastrowidr   rL   Errorr   r1   r   )
rU   rh   rN   r   r   r   Zannouncement_idZappraiser_recordsrecordr:   r!   r!   r"   save_to_database  sV   

&rM  	appraiser	file_sizer!  r   c           	   
   C   s<   |   }t d}|d|||||||f |   dS )u   記錄收集結果rj   z
        INSERT OR REPLACE INTO collection_log
        (collect_date, csv_date, total_count, appraiser_count, file_size, status, error_message)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    N)rN   r   rO   rQ   rR   r   )	rU   r   r   rN  rO  r!  r   rN   rV   r!   r!   r"   log_collection  s   rP  c            
      C   s  t d t d t d t } tdd}|s.t| dddddd td	d
 td dS t|}t dt| d |sMt| dddt|dd dS |rU|d d nd}t	| |\}}t
dd |D }t d| d| d t d| d t| |t||t|d |dkrt d t d |D ]}|d rt d|d  d|d  d|d  dd!   qtd"d#t| d$| d% t|t|||}t| |dkrt| |}	t|	 |   t d& dS )'u   執行今日收集r  u    MOPS 重大訊息收集器啟動T)r   Nr   r  u   下載失敗u   MOPS 收集器 - 失敗u(   CSV 下載失敗，請檢查網路連線uI   ❌ <b>MOPS 收集器失敗</b>

CSV 下載失敗，請檢查網路連線Fu   解析完成: u
    筆公告u   CSV 無資料r+  c                 s   s    | ]	}|d  rdV  qdS )rl   ru   Nr!   rm   r!   r!   r"   r`     s    z collect_today.<locals>.<genexpr>u   儲存完成: 新增 u    筆，估價相關 r   u    今日估價相關公告總數: successz2--------------------------------------------------u   今日估價相關公告:rl   z  [rv   r   rw   r   r\   r   u   MOPS 收集器 - 完成u
   已收集 u    筆公告，其中 u    筆與估價相關u   收集完成)r   r6   r   r  rP  rB   r7   r3  r   rM  r   r   r   rT   )
rU   r(  rh   r   r   r   ri   r   Z
status_msgZ
detail_msgr!   r!   r"   collect_today  sV   





0

rR  c                  C   sJ  t  s
td dS tt } |  }|d | d }|d | d }|d | d }|d | }td td	 td
 td|dd td|dd td| d td|d  d|d   |d | }|rtd |D ]}td|d  d|d dd|d  d|d  d	 q| 	  dS )   顯示統計資訊u   資料庫尚未建立Nz"SELECT COUNT(*) FROM announcementsr   z?SELECT COUNT(*) FROM announcements WHERE has_appraiser_info = 1z7SELECT COUNT(DISTINCT announce_date) FROM announcementsz@SELECT MIN(announce_date), MAX(announce_date) FROM announcementsr  u    MOPS 重大訊息資料庫統計r  u   總公告數: r   r   r   u   收集天數: u    天u   日期範圍: z ~ ru   z
        SELECT collect_date, csv_date, total_count, appraiser_count, status
        FROM collection_log ORDER BY collect_date DESC LIMIT 5
    u   
最近收集記錄:  r   r5  u    筆 (估價: r   z) [   ])
rJ   rK   printrL   rM   rN   rR   rS   r   rT   )rU   rN   r   Zappraiser_totalrG   Z
date_ranger   logr!   r!   r"   
show_stats  s6   





6rY  __main__u    MOPS 重大訊息每日收集器)r   z--statsZ
store_truerS  )actionhelpz
--backfillu*   補抓模式（跳過已抓過的日期）z--alluN   收集所有資料集（公司基本資料、月營收、內部人持股等）z--listu   列出所有可用的資料集u   
可用的資料集:z<============================================================r  r   rT  r   r   u              市場: u    | 更新頻率: r   z           r   ru   )T)r<   )Tr   )r   Tr&   )a__doc__r   r   reconfigurer/  rL   r3   r0   r   r   Zshutilr@   r  Zpathlibr   r   r   typingr   r	   r
   r   r   __file__r   ZBASE_DIRZDATA_DIRr   rJ   r   r   r  r  r1  r'  r&  r?   r/   osenvironrq   r   r   rG  r#   rH  r7   r;   rB   dictrW   floatr[   rg   listr   r   r   r   r   r   
Connectionr   r   r   r   r   r  r  tupler  r"  rp   r3  r   rM  rP  rR  rY  r   ZargparseZArgumentParserZparserZadd_argumentZ
parse_argsargsrW  r2   coder   r  r   r   allrQ  r  exitr!   r!   r!   r"   <module>   s   
"	
-+9 
$ P2(;M $;
F
-

