diff --git a/scripts/tender-fetcher/fetch-xkgroup.py b/scripts/tender-fetcher/fetch-xkgroup.py index d990fdd..ae7bc4b 100755 --- a/scripts/tender-fetcher/fetch-xkgroup.py +++ b/scripts/tender-fetcher/fetch-xkgroup.py @@ -1,23 +1,35 @@ #!/usr/bin/env python3 """ 新矿集团供应链平台 公告抓取工具 -数据源:https://xkgroup.kdcloud.com/isv/q5m9/dist/srmportal.html -API:getPortalNoticeData (REST, 无需登录即可访问 guest 数据) +数据源:http://120.70.100.128:8096/(新站,2026-10 切换) + - 纯服务端渲染(layui),无 JSON API,抓 HTML 列表页解析 + - 列表页:/front/notice/cgxx?category=bid|procure|auction(GET 表单分页,pageNo 从 0 起,每页 10 条) + - 详情页:/front/index/v_notice_view?id=<32hex>(含正文 + 附件 PDF 直链) -抓取类型: - C - 寻源公告(核心采购公告) - D - 流标公告 - B - 结果公示 +板块类型标签全集: + bid 招标公告 / 变更&澄清公告 / 资格预审公告 / 中标候选人公示 / 中标公告 / 废标公告 + procure 采购公告 / 变更公告 / 结果公示 / 其他公告 + auction 竞价公告 / 竞价流拍 + +默认抓取"商机类 + 流标类"(对应旧站 C 寻源 + D 流标语义): + 商机类 = 招标公告/采购公告/竞价公告/资格预审公告 + 流标类 = 废标公告/竞价流拍 +--all-types 额外抓结果/其他类(中标候选人公示/中标公告/变更&澄清/结果公示/其他公告) 用法: - python3 fetch-xkgroup.py # 差量更新(默认抓取昨天的寻源公告) + python3 fetch-xkgroup.py # 差量更新(默认回溯到上次运行/7 天内) python3 fetch-xkgroup.py --all-types # 抓取所有公告类型 - python3 fetch-xkgroup.py --from 2026-05-01 # 指定起始日期 + python3 fetch-xkgroup.py --from 2026-10-01 # 指定起始日期 python3 fetch-xkgroup.py --force # 强制重新抓取(不跳过已下载) - python3 fetch-xkgroup.py --keyword 水泥 # 按关键词过滤 + python3 fetch-xkgroup.py --keyword 水泥 # 按关键词过滤(标题/发布单位) + python3 fetch-xkgroup.py --max-pages 10 # 每板块最大翻页数(默认 5 页 = 50 条) + +状态文件 xkgroup_downloaded.json 格式与旧版一致,CrawlerService.runGenericScript 依赖字段: + id/title/org/publish_time/detail_url/amount/deadline/region/region_detail/downloaded_at """ import argparse +import html as html_lib import json import os import re @@ -31,17 +43,19 @@ except ImportError: sys.exit(1) # ============ 配置区 ============ -BASE_URL = "https://xkgroup.kdcloud.com/kapi/app/srm/srmwebapi" -ACCOUNT_ID = "2066845847806639104" -PAGE_SIZE = 50 +BASE_URL = "http://120.70.100.128:8096" +LIST_URL = f"{BASE_URL}/front/notice/cgxx" +PAGE_SIZE = 10 # 新站固定每页 10 条 +MAX_PAGES_DEFAULT = 5 # 每板块默认最多翻 5 页(日增量远小于此,历史回填可加大) -# 公告类型映射 -NOTICE_TYPES = { - "C": "寻源公告", - "D": "流标公告", - "B": "结果公示", -} -DEFAULT_TYPES = ["C", "D"] # 抓取寻源公告 + 流标公告 +# 板块与类型标签 +CATEGORIES = ["bid", "procure", "auction"] + +# 商机类 + 流标类(默认抓取,对应旧版 C+D) +BUSINESS_TAGS = {"招标公告", "采购公告", "竞价公告", "资格预审公告"} +FLOWN_TAGS = {"废标公告", "竞价流拍"} +# 结果/其他类(--all-types 才抓) +RESULT_TAGS = {"中标候选人公示", "中标公告", "变更&澄清公告", "变更公告", "结果公示", "其他公告"} # 多次询价标题正则 (匹配 二次/三次/四次/.../九次) RETRY_PATTERN = re.compile(r"[二三四五六七八九]次") @@ -56,77 +70,99 @@ OUTPUT_BASE = os.path.expanduser("~/desktop/招标公告") HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", - "Accept": "application/json, text/plain, */*", - "Referer": "https://xkgroup.kdcloud.com/isv/q5m9/dist/srmportal.html", + "Accept": "text/html,application/xhtml+xml,*/*", + "Referer": f"{BASE_URL}/", } -# ============ 核心函数 ============ +# ============ 抓取层(新站 HTML 解析) ============ -def fetch_notices(notice_type, page=1, size=PAGE_SIZE): - """调用 API 获取公告列表""" - params = { - "page": page, - "size": size, - "type": notice_type, - "apiname": "getPortalNoticeData", - "methodname": "getPortalNoticeData", - "accountId": ACCOUNT_ID, - } - resp = requests.get(BASE_URL, params=params, headers=HEADERS, timeout=30) +def fetch_list_page(category, page_no, timeout=30): + """抓取列表页 HTML(GET 表单,pageNo 从 0 起)""" + resp = requests.get( + LIST_URL, + params={"category": category, "pageNo": page_no}, + headers=HEADERS, + timeout=timeout, + ) resp.raise_for_status() - return resp.json() + return resp.text -def fetch_all_notices(notice_type): - """翻页获取指定类型的所有公告""" +def parse_list_html(html_text): + """解析列表页 tbody#listTbody 中的公告条目 + + 返回 [{"id", "url_path", "title", "tag", "publish_date", "org"}] + """ + items = [] + tbody_m = re.search(r'
([\s\S]*?)', html_text) + rows = re.split(r"