#!/usr/bin/env python3
"""crawl_xingtai_sthjj.py - 邢台市生态环境局-信息公开 (邢台市政府信息公开平台)

站点: http://info.xingtai.gov.cn/areas.jsp?orgCode=001001167&type=9
CMS: 邢台市政府信息公开平台 (UTF-8)
列表: ul.list > li > a[title=标题](href=/single/114|14/ID.html) + span.time(YYYY-MM-DD)
分页: areas.jsp?orgCode=001001167&type=9&pn=N (每页20条)
详情: <title> 标题 + div 正文容器
修复(2026-08-08): ①detail容器精确匹配 class="detail" 而非 detail-box(外壳+relateInfo+script噪声)
②附件型 href 相对路径转绝对URL (点击404=正文缺失根因) ③剥外壳+清尾部空p
"""
import sys, os, requests, re, argparse
from datetime import datetime, timedelta

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from crawler_lib import push_to_searchdb

CUTOFF = (datetime.now() - timedelta(days=365*3)).strftime('%Y-%m-%d')
SITE_NAME = '邢台市生态环境局-信息公开'
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0'}
BASE = 'http://info.xingtai.gov.cn'
LIST_URL = BASE + '/areas.jsp?orgCode=001001167&type=9'

def fetch_list(page_no):
    url = LIST_URL if page_no <= 1 else f'{LIST_URL}&pn={page_no}'
    try:
        r = requests.get(url, timeout=(5, 15), headers=HEADERS, verify=False)
        r.encoding = 'utf-8'
    except:
        return []
    items = re.findall(
        r'<a href="(/single/\d+/\d+\.html)" target="_blank" title="([^"]*)">(.*?)</a>\s*<span class="time">(\d{4}-\d{2}-\d{2})</span>',
        r.text, re.DOTALL
    )
    results = []
    for href, title_attr, title_text, pd in items:
        title = re.sub(r'\s+', '', (title_attr or title_text)).strip()
        if len(title) < 4:
            continue
        url = BASE + href
        results.append({'url': url, 'pub_date': pd, 'title': title})
    return results

def extract_balanced_div(html, open_tag_re):
    """平衡 div 匹配: 从匹配到的 <div...> 的 '>' 之后, 到对应 </div>, 返回内部 HTML。
    修复非贪婪 (.*?)</div> 在正文含嵌套 div(表格/附件区)时截断的问题"""
    m = re.search(open_tag_re, html)
    if not m:
        return ''
    gt = html.find('>', m.start())
    if gt == -1:
        return ''
    i = gt + 1
    depth = 1
    for mm in re.finditer(r'<div[\s>]|</div>', html[i:]):
        if mm.group(0).startswith('<div'):
            depth += 1
        else:
            depth -= 1
            if depth == 0:
                return html[i:i + mm.start()]
    return html[i:]

def absolutize_links(html):
    """把相对路径 href/src 转成绝对 URL (附件/图片)。修复附件型公告点击404问题"""
    html = re.sub(r'href="(?!https?://|//|#)(/[^"]+)"', lambda mm: f'href="{BASE}{mm.group(1)}"', html)
    html = re.sub(r'src="(?!https?://|//)(/[^"]+)"', lambda mm: f'src="{BASE}{mm.group(1)}"', html)
    return html

def clean_content(c):
    """清理正文: 去 script/relateInfo/空p, href转绝对"""
    c = re.sub(r'<script.*?</script>', '', c, flags=re.DOTALL | re.I)
    c = re.sub(r'<div[^>]*id="relateInfo"[^>]*>.*?</div>', '', c, flags=re.DOTALL | re.I)
    c = absolutize_links(c)
    # 剥掉残留的 <div id="content" class="detail"> 外壳(如果正则匹配到了外层容器)
    c = re.sub(r'^\s*<div[^>]*class="[^"]*detail[^"]*"[^>]*>', '', c)
    c = re.sub(r'</div>\s*$', '', c)
    # 尾部连续空p
    c = re.sub(r'(<p>\s*(<br\s*/?>)?\s*</p>)+$', '', c)
    return c.strip()

def fetch_detail(url):
    for _attempt in range(2):
        try:
            r = requests.get(url, timeout=(5, 15), headers=HEADERS, verify=False)
            r.encoding = 'utf-8'
            break
        except:
            r = None
    if r is None:
        return '', ''
    title = ''
    m = re.search(r'<title>\s*([^<]+?)\s*</title>', r.text, re.DOTALL)
    if m:
        title = re.sub(r'\s+', '', m.group(1)).strip()
        title = re.split(r'[-–—|_]', title)[0].strip()
    content = ''
    # 正文容器: 优先精确 class="detail" (邢台平台 <div id="content" class="detail">)
    # 注意不要匹配外层 detail-box scroll-box 容器, 否则带上外壳+relateInfo+script
    c = extract_balanced_div(r.text, r'<div[^>]*class="detail"[^>]*>')
    if not c.strip():
        # fallback: detail-box 容器(其他页面结构)
        c = extract_balanced_div(r.text, r'<div[^>]*class="[^"]*detail-box[^"]*"[^>]*>')
    if c.strip():
        content = clean_content(c)
    if not content.strip():
        # 附件型: 正文是 PDF/Word 附件, right-content 内 <a href> 转 <p><a> 段落
        rc = extract_balanced_div(r.text, r'<div[^>]*class="right-content"[^>]*>')
        if rc.strip():
            rc = re.sub(r'<script.*?</script>', '', rc, flags=re.DOTALL | re.I)
            parts = []
            for href, name in re.findall(r'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>', rc, re.DOTALL):
                name = re.sub(r'<[^>]+>', '', name).strip()
                if not name or len(name) < 3:
                    continue
                if any(k in name for k in ('返回', '更多', '上一页', '下一页')):
                    continue
                # 只保留真实附件链接(文件后缀/file/upload/attach), 排除 relate.jsp 等 JS 链接
                if not re.search(r'\.(pdf|docx?|xlsx?|zip|rar|wps|jpe?g|png)$|/file/|upload|attach', href, re.I):
                    continue
                url = href if href.startswith('http') else BASE + href
                parts.append(f'<p><a href="{url}">{name}</a></p>')
            content = '\n'.join(parts)
    return title, content

def run(max_pages=5):
    records = []
    seen = set()
    for pg in range(max_pages):
        items = fetch_list(pg + 1)
        if not items:
            print(f"  Page {pg+1}: 0 items (stop)")
            break
        print(f"  Page {pg+1}: {len(items)} items")
        for it in items:
            if it['pub_date'] and it['pub_date'] < CUTOFF:
                continue
            if it['url'] in seen:
                continue
            seen.add(it['url'])
            title, content = fetch_detail(it['url'])
            if not title:
                title = it['title']
            if not content.strip():
                continue
            records.append({
                'title': title, 'url': it['url'], 'source_url': it['url'],
                'pub_date': it['pub_date'],
                'site_name': SITE_NAME, 'content': content, 'summary': '',
            })
    print(f"  Total: {len(records)} items")
    valid = [r for r in records if r['content'].strip()]
    if valid:
        push_to_searchdb(valid, "xingtai_sthjj")
    return len(valid)

if __name__ == '__main__':
    ap = argparse.ArgumentParser()
    ap.add_argument('--pages', type=int, default=5)
    args = ap.parse_args()
    cnt = run(max_pages=args.pages)
    print(f"Done: {cnt} records")
