1026 lines
37 KiB
Python
1026 lines
37 KiB
Python
"""
|
||
标准投标技术标常见附件:按章节标题识别类型,生成带固定表头的 [TABLE]/[FIGURE] 块。
|
||
内容结合项目摘要、清单摘要做概括性填充(不照搬示例工程数据)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from datetime import date
|
||
from typing import Any, Optional
|
||
|
||
# 从标题中去掉行首编号(如 十二、附件一、)
|
||
_TITLE_NUM_RE = re.compile(
|
||
r'^[\s\u3000]*(?:[一二三四五六七八九十百零〇两]+|\d+)(?:[、..]\s*)+'
|
||
)
|
||
# 「附表一:」「附件二:」等(与前端 stripAnnexTitlePrefix 一致)
|
||
_PRE_ANNEX_RE = re.compile(
|
||
r'^[\s\u3000]*(?:附表|附件)\s*[一二三四五六七八九十百零〇两\d]+[::、..]\s*'
|
||
)
|
||
|
||
|
||
def _strip_title_prefix(title: str) -> str:
|
||
t = (title or '').strip()
|
||
while True:
|
||
m = _PRE_ANNEX_RE.match(t)
|
||
if m:
|
||
t = t[m.end() :].strip()
|
||
continue
|
||
m = _TITLE_NUM_RE.match(t)
|
||
if not m:
|
||
break
|
||
t = t[m.end() :].strip()
|
||
return t
|
||
|
||
|
||
def classify_standard_appendix(title: str) -> Optional[str]:
|
||
"""
|
||
识别常见附件类型。返回:
|
||
main_equipment | test_instruments | labor_plan | schedule_chart |
|
||
site_layout | temp_land
|
||
"""
|
||
t = _strip_title_prefix(title)
|
||
if not t:
|
||
return None
|
||
# 主要施工设备表
|
||
if '主要施工设备' in t or ('拟投入' in t and '设备' in t and '表' in t):
|
||
if '仪器' not in t and '检测' not in t:
|
||
return 'main_equipment'
|
||
# 试验和检测仪器设备表
|
||
if ('试验' in t or '检测仪器' in t or '检测' in t) and '设备' in t and '表' in t:
|
||
return 'test_instruments'
|
||
# 劳动力计划表
|
||
if '劳动力' in t and ('计划' in t or '表' in t):
|
||
return 'labor_plan'
|
||
# 临时用地表
|
||
if '临时用地' in t:
|
||
return 'temp_land'
|
||
# 进度网络图 / 横道图 / 开完工日期(含「竣工」「开、竣工」等用语)
|
||
if ('进度' in t or '开工' in t or '完工' in t or '竣工' in t) and (
|
||
'网络' in t or '横道' in t or '计划' in t or '日期' in t or '图' in t
|
||
):
|
||
return 'schedule_chart'
|
||
# 施工总平面图
|
||
if '施工总平面' in t or ('总平面' in t and '图' in t):
|
||
return 'site_layout'
|
||
return None
|
||
|
||
|
||
def _ctx_hint(summary: str, boq: str, max_len: int = 400) -> str:
|
||
parts = []
|
||
if (summary or '').strip():
|
||
parts.append((summary or '').strip()[:max_len])
|
||
if (boq or '').strip():
|
||
parts.append(('清单摘要:' + (boq or '').strip())[:max_len])
|
||
return ';'.join(parts) if parts else '按招标文件及施工组织要点配置'
|
||
|
||
|
||
def _project_text(summary: str, boq: str) -> str:
|
||
return f'{summary or ""}\n{boq or ""}'
|
||
|
||
|
||
def _text_deterministic_seed(text: str) -> int:
|
||
"""短文本时仍能得到稳定偏移,便于同项目多次生成一致。"""
|
||
raw = (text or '').strip()[:8000]
|
||
if not raw:
|
||
return 7
|
||
return sum(ord(c) * (i + 1) for i, c in enumerate(raw[:600])) % 997
|
||
|
||
|
||
def _equipment_scale_from_text(summary: str, boq: str) -> float:
|
||
"""设备台数规模系数,随项目信息量与关键词调整。"""
|
||
t = _project_text(summary, boq)
|
||
f = 1.0
|
||
if any(k in t for k in ('大规模', '重点', '总长', '线路', '隧道', '桥梁', '水利枢纽')):
|
||
f += 0.1
|
||
if any(k in t for k in ('小型', '维修', '改造', '零星', '装修')):
|
||
f -= 0.12
|
||
if len(t) > 3000:
|
||
f += 0.06
|
||
elif len(t) < 350:
|
||
f -= 0.06
|
||
return max(0.78, min(f, 1.22))
|
||
|
||
|
||
def _parse_area_sqm_near_keywords(text: str, keywords: tuple[str, ...]) -> Optional[int]:
|
||
"""在含关键词的行中抓取面积数字(㎡、平方米、m² 等)。"""
|
||
if not text or not keywords:
|
||
return None
|
||
for line in text.splitlines():
|
||
if not any(k in line for k in keywords):
|
||
continue
|
||
m = re.search(r'(\d+(?:\.\d+)?)\s*(?:㎡|m2|m²|平方米)', line, re.I)
|
||
if m:
|
||
return max(1, int(round(float(m.group(1)))))
|
||
m2 = re.search(r'(\d+(?:\.\d+)?)\s*平方', line)
|
||
if m2:
|
||
return max(1, int(round(float(m2.group(1)))))
|
||
return None
|
||
|
||
|
||
def _area_m2_with_context(
|
||
text: str,
|
||
keywords: tuple[str, ...],
|
||
base_m2: int,
|
||
scale: float,
|
||
seed: int,
|
||
row_index: int,
|
||
lo: int,
|
||
hi: int,
|
||
) -> int:
|
||
hinted = _parse_area_sqm_near_keywords(text, keywords)
|
||
if hinted is not None:
|
||
return max(lo, min(hinted, hi))
|
||
v = int(round(base_m2 * scale))
|
||
v += ((seed >> (row_index * 4)) & 31) - 15
|
||
return max(lo, min(v, hi))
|
||
|
||
|
||
# 临时用地行定义(与临时用地表、施工总平面图共用)
|
||
TEMP_LAND_SPECS: list[dict] = [
|
||
{
|
||
'use': '项目部办公',
|
||
'kws': ('项目部办公', '办公区', '项目部'),
|
||
'base': 150,
|
||
'lo': 80,
|
||
'hi': 420,
|
||
'loc': '施工用地内近大门侧、与作业区相对独立',
|
||
'time': '全施工期',
|
||
},
|
||
{
|
||
'use': '门卫及保卫',
|
||
'kws': ('门卫', '保卫室', '门岗'),
|
||
'base': 18,
|
||
'lo': 10,
|
||
'hi': 55,
|
||
'loc': '施工现场主出入口内侧',
|
||
'time': '全施工期',
|
||
},
|
||
{
|
||
'use': '工人生活区',
|
||
'kws': ('生活区', '宿舍区', '民工宿舍'),
|
||
'base': 220,
|
||
'lo': 120,
|
||
'hi': 650,
|
||
'loc': '场区一侧,与加工堆场保持安全距离',
|
||
'time': '全施工期',
|
||
},
|
||
{
|
||
'use': '食堂与卫浴',
|
||
'kws': ('食堂', '伙房', '卫浴', '卫生间'),
|
||
'base': 42,
|
||
'lo': 25,
|
||
'hi': 120,
|
||
'loc': '生活区内上风向、排水接入沉淀设施',
|
||
'time': '全施工期',
|
||
},
|
||
{
|
||
'use': '材料堆场',
|
||
'kws': ('堆场', '材料堆场', '仓储'),
|
||
'base': 320,
|
||
'lo': 150,
|
||
'hi': 900,
|
||
'loc': '主作业区侧、靠近施工道路与材料装卸通道',
|
||
'time': '分阶段布设,与进度同步',
|
||
},
|
||
{
|
||
'use': '钢木加工车间',
|
||
'kws': ('加工区', '加工车间', '钢筋加工', '木工棚'),
|
||
'base': 300,
|
||
'lo': 160,
|
||
'hi': 800,
|
||
'loc': '靠近堆场与环场道路,设防噪声围挡',
|
||
'time': '主体施工高峰期为主',
|
||
},
|
||
{
|
||
'use': '机械停放区',
|
||
'kws': ('机械停放', '设备停放', '停车场'),
|
||
'base': 200,
|
||
'lo': 100,
|
||
'hi': 550,
|
||
'loc': '硬化地坪、靠近出入口便于进退场',
|
||
'time': '全施工期',
|
||
},
|
||
{
|
||
'use': '临时道路及硬化场地',
|
||
'kws': ('临时道路', '施工道路', '硬化'),
|
||
'base': 980,
|
||
'lo': 400,
|
||
'hi': 2800,
|
||
'loc': '环场主干道及作业面连通支线',
|
||
'time': '随施工阶段动态调整',
|
||
},
|
||
]
|
||
|
||
|
||
def compute_temp_land_rows(summary: str, boq: str) -> list[dict[str, Any]]:
|
||
"""与临时用地表一致的用途、面积(㎡)、位置、时间(供总平面图文字勾连)。"""
|
||
ctx = _project_text(summary, boq)
|
||
seed = _text_deterministic_seed(ctx)
|
||
scale = _equipment_scale_from_text(summary, boq)
|
||
out: list[dict[str, Any]] = []
|
||
for i, spec in enumerate(TEMP_LAND_SPECS):
|
||
m2 = _area_m2_with_context(
|
||
ctx,
|
||
spec['kws'],
|
||
spec['base'],
|
||
scale,
|
||
seed,
|
||
i,
|
||
spec['lo'],
|
||
spec['hi'],
|
||
)
|
||
out.append(
|
||
{
|
||
'use': spec['use'],
|
||
'area_m2': m2,
|
||
'loc': spec['loc'],
|
||
'time': spec['time'],
|
||
}
|
||
)
|
||
return out
|
||
|
||
|
||
def _build_temp_land_table_md(title: str, summary: str, boq: str) -> str:
|
||
"""
|
||
临时用地表:面积(㎡) 给出具体整数;优先从摘要/清单中解析与用途相关的面积表述。
|
||
"""
|
||
clean = (title or '临时用地表').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
rows = compute_temp_land_rows(summary, boq)
|
||
header = '| 用途 | 面积(㎡) | 位置 | 需用时间 |'
|
||
sep = '|------|----------|------|----------|'
|
||
lines = [header, sep]
|
||
for r in rows:
|
||
lines.append(
|
||
'| {use} | {m2} | {loc} | {time} |'.format(
|
||
use=r['use'],
|
||
m2=r['area_m2'],
|
||
loc=r['loc'],
|
||
time=r['time'],
|
||
)
|
||
)
|
||
body = '\n'.join(lines)
|
||
return (
|
||
f'[TABLE:{clean}]\n'
|
||
f'{body}\n'
|
||
f'[/TABLE]\n'
|
||
f'(各项面积已按项目规模具体填写;若总平面布置或招标文件另有用地指标,以{hint}及审批总平面为准复核。)'
|
||
)
|
||
|
||
|
||
def _parse_qty_near_keywords(text: str, keywords: tuple[str, ...]) -> Optional[int]:
|
||
"""在含关键词的行中抓取「数字+台/套/辆」。"""
|
||
if not text or not keywords:
|
||
return None
|
||
for line in text.splitlines():
|
||
if not any(k in line for k in keywords):
|
||
continue
|
||
for m in re.finditer(r'(\d+)\s*(?:台|套|辆|组|把)', line):
|
||
n = int(m.group(1))
|
||
if 1 <= n <= 999:
|
||
return n
|
||
return None
|
||
|
||
|
||
def _parse_year_from_text(text: str) -> Optional[int]:
|
||
"""摘要/清单中出现的合理制造年份(近年出厂设备)。"""
|
||
years = [int(y) for y in re.findall(r'(20[1-2]\d)', text or '')]
|
||
if not years:
|
||
return None
|
||
y = max(years)
|
||
if 2015 <= y <= 2026:
|
||
return y
|
||
return None
|
||
|
||
|
||
def _pick_year(base_year: Optional[int], row_index: int, seed: int) -> int:
|
||
"""制造年份:优先项目文本;否则在近年区间内按行略作错落。"""
|
||
if base_year is not None:
|
||
return max(2018, min(base_year, 2025))
|
||
pool = [2019, 2020, 2021, 2022, 2023, 2024]
|
||
return pool[(seed + row_index * 17) % len(pool)]
|
||
|
||
|
||
def _qty_with_context(
|
||
text: str,
|
||
keywords: tuple[str, ...],
|
||
base_qty: int,
|
||
scale: float,
|
||
seed: int,
|
||
row_index: int,
|
||
) -> int:
|
||
hinted = _parse_qty_near_keywords(text, keywords)
|
||
if hinted is not None:
|
||
return max(1, min(hinted, 99))
|
||
q = int(round(base_qty * scale))
|
||
jitter = ((seed >> (row_index * 3)) & 3) - 1
|
||
q = max(1, q + jitter)
|
||
return min(q, 99)
|
||
|
||
|
||
# 主要施工设备行定义(设备表用;附图示意中不出现机械,见文生图提示词)
|
||
MAIN_EQUIPMENT_SPECS: list[dict] = [
|
||
{
|
||
'name': '液压挖掘机',
|
||
'kws': ('挖掘机', '液压挖', '反铲', '土方机械'),
|
||
'q0': 4,
|
||
'models': ('SY235C-10', 'PC220-8MO', 'ZX240LC-5A'),
|
||
'powers': (125, 132, 128),
|
||
'cap': ('斗容约1.1m³', '斗容约1.0m³', '斗容约1.2m³'),
|
||
'part': '土方开挖、基坑与回填作业面',
|
||
'rmk': '自有或租赁按进度调配',
|
||
},
|
||
{
|
||
'name': '履带式推土机',
|
||
'kws': ('推土机', '平整'),
|
||
'q0': 2,
|
||
'models': ('SD16标准型', 'D6K LGP', 'TY160'),
|
||
'powers': (120, 104, 121),
|
||
'cap': ('额定功率匹配铲刀作业', '铲刀容量与土质匹配', '平整效率满足工序'),
|
||
'part': '场地平整、路基粗平',
|
||
'rmk': '与挖装设备流水衔接',
|
||
},
|
||
{
|
||
'name': '自卸汽车',
|
||
'kws': ('自卸', '渣土', '运输', '汽车'),
|
||
'q0': 10,
|
||
'models': ('东风天锦 15t', '重汽豪沃 18t', '解放J6P 20t'),
|
||
'powers': (180, 228, 265),
|
||
'cap': ('额定载重15t', '额定载重18t', '额定载重20t'),
|
||
'part': '土方、砂石料及混凝土运输',
|
||
'rmk': '道路与地磅条件满足通行',
|
||
},
|
||
{
|
||
'name': '轮式装载机',
|
||
'kws': ('装载机', '铲车'),
|
||
'q0': 3,
|
||
'models': ('ZL50GN', 'L956F', 'WA380-6'),
|
||
'powers': (162, 175, 142),
|
||
'cap': ('斗容约3.0m³', '斗容约2.8m³', '斗容约2.5m³'),
|
||
'part': '堆场装车、短驳与备料',
|
||
'rmk': '与运输车辆匹配',
|
||
},
|
||
{
|
||
'name': '振动压路机',
|
||
'kws': ('压路机', '碾压', '压实'),
|
||
'q0': 3,
|
||
'models': ('XS223J', 'CA30D', 'SR26M-C5'),
|
||
'powers': (118, 132, 129),
|
||
'cap': ('工作质量约22t', '激振力与遍数受控', '适用于基层与面层'),
|
||
'part': '路基、基层及面层压实',
|
||
'rmk': '试验段确定压实参数',
|
||
},
|
||
{
|
||
'name': '混凝土搅拌运输车',
|
||
'kws': ('搅拌车', '罐车', '商砼'),
|
||
'q0': 6,
|
||
'models': ('12m³ 陕汽底盘', '10m³ 三一重工', '12m³ 中联重科'),
|
||
'powers': (276, 257, 288),
|
||
'cap': ('几何容积12m³', '搅动与卸料满足泵送', 'GPS调度'),
|
||
'part': '商品混凝土水平运输',
|
||
'rmk': '与泵车浇筑节拍匹配',
|
||
},
|
||
{
|
||
'name': '汽车式起重机',
|
||
'kws': ('汽车吊', '起重机', '吊装'),
|
||
'q0': 2,
|
||
'models': ('QY25K5C 25t', 'STC250T4 25t', 'XCT25L5'),
|
||
'powers': (213, 206, 210),
|
||
'cap': ('最大起重量25t', '主臂长度满足构件', '支腿全伸作业'),
|
||
'part': '钢筋、模板及小型构件吊装',
|
||
'rmk': '专项方案与地基承载复核',
|
||
},
|
||
{
|
||
'name': '柴油发电机组',
|
||
'kws': ('发电机', '发电', '备用电源'),
|
||
'q0': 2,
|
||
'models': ('GF-200kW', 'GF-250kW', 'SC9D340D2'),
|
||
'powers': (200, 250, 308),
|
||
'cap': ('连续输出功率200kW', '连续输出功率250kW', '连续输出功率280kW'),
|
||
'part': '基坑降水、塔吊及高峰施工备用电',
|
||
'rmk': '一机一闸一漏一箱',
|
||
},
|
||
]
|
||
|
||
|
||
def _build_main_equipment_table_md(title: str, summary: str, boq: str) -> str:
|
||
"""
|
||
主要施工设备表:型号规格、数量、制造年份、定额功率等尽量给出具体数值;
|
||
优先从项目摘要/清单中解析台数线索,其余按工程规模系数与稳定随机偏移生成合理配置。
|
||
"""
|
||
clean = (title or '主要施工设备表').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
ctx = _project_text(summary, boq)
|
||
seed = _text_deterministic_seed(ctx)
|
||
scale = _equipment_scale_from_text(summary, boq)
|
||
year_hint = _parse_year_from_text(ctx)
|
||
|
||
specs = MAIN_EQUIPMENT_SPECS
|
||
|
||
header = (
|
||
'| 序号 | 机械或设备名称 | 型号规格 | 数量 | 国别产地 | 制造年份 | '
|
||
'定额功率(KW) | 生产能力 | 用于施工部位 | 备注 |'
|
||
)
|
||
sep = (
|
||
'|------|----------------|----------|------|----------|----------|'
|
||
'---------------|----------|--------------|------|'
|
||
)
|
||
lines = [header, sep]
|
||
|
||
for i, spec in enumerate(specs):
|
||
mi = (seed + i * 31) % len(spec['models'])
|
||
model = spec['models'][mi]
|
||
power = spec['powers'][mi]
|
||
cap = spec['cap'][mi]
|
||
qty = _qty_with_context(ctx, spec['kws'], spec['q0'], scale, seed, i)
|
||
qty_str = f'{qty}台'
|
||
if spec['name'] in ('自卸汽车', '混凝土搅拌运输车'):
|
||
qty_str = f'{qty}辆'
|
||
elif spec['name'] in ('柴油发电机组',):
|
||
qty_str = f'{qty}套'
|
||
|
||
year = _pick_year(year_hint, i, seed)
|
||
pwr_str = str(power)
|
||
|
||
lines.append(
|
||
'| {idx} | {name} | {model} | {qtys} | 中国 | {year} | {pwr} | {cap} | {part} | {rmk} |'.format(
|
||
idx=i + 1,
|
||
name=spec['name'],
|
||
model=model,
|
||
qtys=qty_str,
|
||
year=year,
|
||
pwr=pwr_str,
|
||
cap=cap,
|
||
part=spec['part'],
|
||
rmk=spec['rmk'],
|
||
)
|
||
)
|
||
|
||
body = '\n'.join(lines)
|
||
return (
|
||
f'[TABLE:{clean}]\n'
|
||
f'{body}\n'
|
||
f'[/TABLE]\n'
|
||
f'(型号、台数、功率等已按项目资料做具体化填写;若清单或技术条款另有约定,以{hint}为准最终核定。)'
|
||
)
|
||
|
||
|
||
def _instrument_used_hours(
|
||
year: int,
|
||
seed: int,
|
||
row_index: int,
|
||
powered: bool,
|
||
) -> str:
|
||
"""
|
||
已使用台时数:机动/电子类给具体整数(台时);非动力器具为 0。
|
||
随出厂年份与稳定种子变化,避免全表雷同。
|
||
"""
|
||
if not powered:
|
||
return '0'
|
||
ynow = date.today().year
|
||
age = max(0, min(ynow - year, 8))
|
||
base = 380 + (seed % 220) + row_index * 88
|
||
annual = 260 + ((seed >> (row_index + 2)) & 0xFF) % 240
|
||
h = int(base + age * annual)
|
||
h = max(160, min(h, 5600))
|
||
return str(h)
|
||
|
||
|
||
def _instrument_qty(
|
||
text: str,
|
||
keywords: tuple[str, ...],
|
||
base_qty: int,
|
||
scale: float,
|
||
seed: int,
|
||
row_index: int,
|
||
max_qty: int = 12,
|
||
) -> int:
|
||
hinted = _parse_qty_near_keywords(text, keywords)
|
||
if hinted is not None:
|
||
return max(1, min(hinted, max_qty))
|
||
q = int(round(base_qty * scale))
|
||
jitter = ((seed >> (row_index * 4)) & 3) - 1
|
||
q = max(1, q + jitter)
|
||
return min(q, max_qty)
|
||
|
||
|
||
def _build_test_instruments_table_md(title: str, summary: str, boq: str) -> str:
|
||
"""
|
||
试验和检测仪器设备表:型号规格、数量、制造年份、已使用台时数为具体数值;
|
||
台数优先从摘要/清单中含关键词的行解析。
|
||
"""
|
||
clean = (title or '试验和检测仪器设备表').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
ctx = _project_text(summary, boq)
|
||
seed = _text_deterministic_seed(ctx)
|
||
scale = _equipment_scale_from_text(summary, boq)
|
||
year_hint = _parse_year_from_text(ctx)
|
||
|
||
rows_spec: list[dict] = [
|
||
{
|
||
'name': '全站仪',
|
||
'kws': ('全站仪', '全站', '测量仪'),
|
||
'q0': 2,
|
||
'models': ('徕卡 TS09plus 1″', '南方 NTS-362R10', '苏一光 RTS112R10'),
|
||
'part': '施工控制测量、轴线与坐标放样',
|
||
'rmk': '检定合格且在有效期内',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '水准仪',
|
||
'kws': ('水准仪', '水准'),
|
||
'q0': 3,
|
||
'models': ('苏一光 DSZ2', '徕卡 NA730', '天津赛博 DS32'),
|
||
'part': '高程传递、沉降与水准路线观测',
|
||
'rmk': 'i角定期校验',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '钢卷尺',
|
||
'kws': ('钢卷尺', '卷尺'),
|
||
'q0': 8,
|
||
'models': ('5m Ⅰ级', '50m 标准钢卷尺', '30m Ⅰ级'),
|
||
'part': '距离量测、模板与预埋位置复核',
|
||
'rmk': '周期检定',
|
||
'powered': False,
|
||
'unit': '把',
|
||
},
|
||
{
|
||
'name': '游标卡尺',
|
||
'kws': ('游标卡尺', '卡尺'),
|
||
'q0': 6,
|
||
'models': ('0–150mm 0.02mm', '0–200mm 0.02mm', '数显卡尺 0–150mm'),
|
||
'part': '钢筋直径、螺栓与加工件尺寸抽检',
|
||
'rmk': '计量台账管理',
|
||
'powered': False,
|
||
'unit': '把',
|
||
},
|
||
{
|
||
'name': '混凝土回弹仪',
|
||
'kws': ('回弹仪', '回弹'),
|
||
'q0': 2,
|
||
'models': ('ZC3-A 中回', 'HT-225A', '瑞士 Proceq Original Schmidt'),
|
||
'part': '结构混凝土强度现场抽检',
|
||
'rmk': '率定试验按期进行',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '数字万用表',
|
||
'kws': ('万用表',),
|
||
'q0': 4,
|
||
'models': ('Fluke 117', '优利德 UT39C+', '胜利 VC890C+'),
|
||
'part': '临电线路、设备绝缘与电压电流测试',
|
||
'rmk': 'CAT III 安全等级满足现场',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '接地电阻测试仪',
|
||
'kws': ('接地电阻', '接地测试'),
|
||
'q0': 2,
|
||
'models': ('ETCR2000A', 'Fluke 1623-2', '胜利 VC4105A'),
|
||
'part': '接地网、防雷及设备接地电阻测试',
|
||
'rmk': '三极法/钳形法按规范选用',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '绝缘电阻测试仪',
|
||
'kws': ('绝缘电阻', '兆欧表'),
|
||
'q0': 2,
|
||
'models': ('Fluke 1507 1000V', '优利德 UT501A', '胜利 VC60B+'),
|
||
'part': '电缆、电机及配电回路绝缘测试',
|
||
'rmk': '送电前必测项目',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '电液伺服压力试验机',
|
||
'kws': ('压力试验机', '试验机', '万能试验机'),
|
||
'q0': 1,
|
||
'models': ('YAW-3000 微机控制', 'WAW-1000 微机控制', 'YES-2000 数显'),
|
||
'part': '混凝土、砂浆试块抗压强度试验',
|
||
'rmk': '与标养室及见证取样制度配套',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
{
|
||
'name': '超声波测厚仪',
|
||
'kws': ('测厚仪', '超声波测厚'),
|
||
'q0': 2,
|
||
'models': ('TT130 0.1mm', 'Olympus 38DL PLUS', '时代 TIME2130'),
|
||
'part': '钢管、钢板及防腐层厚度抽检',
|
||
'rmk': '耦合剂与探头匹配管材材质',
|
||
'powered': True,
|
||
'unit': '台',
|
||
},
|
||
]
|
||
|
||
header = (
|
||
'| 序号 | 仪器设备名称 | 型号规格 | 数量 | 国别产地 | 制造年份 | '
|
||
'已使用台时数 | 用于施工部位 | 备注 |'
|
||
)
|
||
sep = (
|
||
'|------|--------------|----------|------|----------|----------|'
|
||
'--------------|--------------|------|'
|
||
)
|
||
lines = [header, sep]
|
||
|
||
for i, spec in enumerate(rows_spec):
|
||
mi = (seed + i * 29) % len(spec['models'])
|
||
model = spec['models'][mi]
|
||
qty_n = _instrument_qty(ctx, spec['kws'], spec['q0'], scale, seed, i, max_qty=15)
|
||
qty_str = f'{qty_n}{spec["unit"]}'
|
||
year = _pick_year(year_hint, i, seed)
|
||
hours = _instrument_used_hours(year, seed, i, spec['powered'])
|
||
rmk = spec['rmk']
|
||
lines.append(
|
||
'| {idx} | {name} | {model} | {qty} | 中国 | {year} | {hours} | {part} | {rmk} |'.format(
|
||
idx=i + 1,
|
||
name=spec['name'],
|
||
model=model,
|
||
qty=qty_str,
|
||
year=year,
|
||
hours=hours,
|
||
part=spec['part'],
|
||
rmk=rmk,
|
||
)
|
||
)
|
||
|
||
body = '\n'.join(lines)
|
||
return (
|
||
f'[TABLE:{clean}]\n'
|
||
f'{body}\n'
|
||
f'[/TABLE]\n'
|
||
f'(型号、数量、出厂年份、已使用台时数已按项目资料具体填写;机动器具台时为累计估值,非机动为0;最终以{hint}及设备台账、检定证书为准。)'
|
||
)
|
||
|
||
|
||
def _labor_peak_factor(summary: str, boq: str) -> float:
|
||
"""根据摘要/清单篇幅与关键词略调人数规模,避免所有项目同一套数字。"""
|
||
text = (summary or '') + (boq or '')
|
||
f = 1.0
|
||
if any(k in text for k in ('大规模', '重点工程', '总建筑面积', '线路长度', '合同额')):
|
||
f += 0.12
|
||
if any(k in text for k in ('小型', '维修', '改造', '零星')):
|
||
f -= 0.08
|
||
n = len(text)
|
||
if n > 2500:
|
||
f += 0.08
|
||
elif n < 400:
|
||
f -= 0.05
|
||
return max(0.82, min(f, 1.28))
|
||
|
||
|
||
# 各阶段人数基准(人):准备 / 建筑主体 / 临时工程 / 附属 / 收尾;峰值落在主体阶段
|
||
_LABOR_BASE_ROWS: list[tuple[str, tuple[int, int, int, int, int]]] = [
|
||
('测量工', (4, 8, 4, 6, 3)),
|
||
('挖掘机司机', (2, 8, 6, 2, 1)),
|
||
('装载机司机', (1, 5, 4, 2, 1)),
|
||
('自卸车司机', (2, 10, 6, 4, 2)),
|
||
('木工', (0, 8, 2, 4, 2)),
|
||
('砼工', (0, 20, 4, 8, 6)),
|
||
('钢筋工', (0, 18, 4, 8, 4)),
|
||
('电工', (3, 4, 3, 4, 3)),
|
||
('普工', (10, 30, 20, 20, 10)),
|
||
('试验工', (2, 4, 2, 3, 2)),
|
||
('仓管', (2, 3, 2, 3, 2)),
|
||
]
|
||
|
||
|
||
def _build_labor_plan_table_md(title: str, summary: str, boq: str) -> str:
|
||
clean = (title or '劳动力计划表').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
factor = _labor_peak_factor(summary, boq)
|
||
# 表头五列名称须与「按工程施工阶段投入劳动力情况」下子列一致(导出 Word 时做双层合并表头)
|
||
header = (
|
||
'| 工种 | 施工准备阶段 | 建筑工程施工阶段 | 临时工程施工阶段 | '
|
||
'其他附属相关工程 | 收尾阶段 |'
|
||
)
|
||
sep = '|------|--------------|------------------|------------------|------------------|----------|'
|
||
lines = [header, sep]
|
||
scaled_rows: list[tuple[int, int, int, int, int]] = []
|
||
for trade, nums in _LABOR_BASE_ROWS:
|
||
row = tuple(max(0, int(round(n * factor))) for n in nums)
|
||
scaled_rows.append(row)
|
||
a, b, c, d, e = row
|
||
lines.append(f'| {trade} | {a} | {b} | {c} | {d} | {e} |')
|
||
if scaled_rows:
|
||
tot = tuple(sum(r[j] for r in scaled_rows) for j in range(5))
|
||
lines.append(
|
||
f'| 合计 | {tot[0]} | {tot[1]} | {tot[2]} | {tot[3]} | {tot[4]} |'
|
||
)
|
||
body = '\n'.join(lines)
|
||
return (
|
||
f'[TABLE:{clean}]\n'
|
||
f'{body}\n'
|
||
f'[/TABLE]\n'
|
||
f'(上表为按施工阶段配置的**人数**估算,单位:人;高峰一般集中在建筑工程施工阶段,须结合工期与作业面以{hint}为准复核调整。)'
|
||
)
|
||
|
||
|
||
def _find_date_after_keyword(text: str, keywords: tuple[str, ...], window: int = 56) -> Optional[str]:
|
||
"""在关键词后若干字符内抓取 yyyy年mm月dd日 或 yyyy-mm-dd。"""
|
||
if not text:
|
||
return None
|
||
for kw in keywords:
|
||
start = 0
|
||
while True:
|
||
idx = text.find(kw, start)
|
||
if idx < 0:
|
||
break
|
||
frag = text[idx : idx + len(kw) + window]
|
||
dm = re.search(r'(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日', frag)
|
||
if dm:
|
||
return f'{dm.group(1)}年{dm.group(2)}月{dm.group(3)}日'
|
||
dm2 = re.search(r'(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})', frag)
|
||
if dm2:
|
||
return f'{dm2.group(1)}年{dm2.group(2)}月{dm2.group(3)}日'
|
||
start = idx + len(kw)
|
||
return None
|
||
|
||
|
||
def _parse_duration_calendar_days(text: str) -> Optional[int]:
|
||
if not text:
|
||
return None
|
||
for pat in (
|
||
r'(\d+)\s*日历天',
|
||
r'总工期[::为]?\s*(\d+)\s*天',
|
||
r'工期[((]?\s*含?[^))]*?[))]?\s*[::为]?\s*(\d+)\s*天',
|
||
r'计划工期[::为]?\s*(\d+)\s*天',
|
||
):
|
||
m = re.search(pat, text)
|
||
if m:
|
||
n = int(m.group(1))
|
||
if 1 <= n <= 3650:
|
||
return n
|
||
return None
|
||
|
||
|
||
def _parse_schedule_facts(summary: str, boq: str) -> dict[str, Any]:
|
||
"""从摘要+清单摘取开工、完工、日历天及里程碑行(摘不到则为空,禁止下游杜撰)。"""
|
||
ctx = _project_text(summary, boq)
|
||
start = _find_date_after_keyword(
|
||
ctx,
|
||
('计划开工', '开工日期', '拟定开工', '合同开工', '开工时间', '开工日'),
|
||
)
|
||
end = _find_date_after_keyword(
|
||
ctx,
|
||
('计划完工', '完工日期', '竣工日期', '交工日期', '合同完工', '竣工时间', '完工时间'),
|
||
)
|
||
duration = _parse_duration_calendar_days(ctx)
|
||
milestones: list[str] = []
|
||
for line in ctx.splitlines():
|
||
s = line.strip()
|
||
if any(
|
||
k in s
|
||
for k in (
|
||
'里程碑',
|
||
'节点工期',
|
||
'控制性节点',
|
||
'关键节点',
|
||
'主体封顶',
|
||
'竣工验收',
|
||
'中间验收',
|
||
)
|
||
):
|
||
if 8 < len(s) < 160:
|
||
milestones.append(s)
|
||
if len(milestones) >= 8:
|
||
break
|
||
return {
|
||
'start': start,
|
||
'end': end,
|
||
'duration_days': duration,
|
||
'milestones': milestones,
|
||
}
|
||
|
||
|
||
def _boq_line_cells(line: str) -> list[str]:
|
||
line = line.strip()
|
||
if line.startswith('|') and line.endswith('|'):
|
||
return [c.strip() for c in line[1:-1].split('|')]
|
||
return []
|
||
|
||
|
||
def _extract_boq_work_items(summary: str, boq: str, max_items: int = 14) -> list[str]:
|
||
"""
|
||
从工程量清单摘要/表格行中抽取可作为进度网络节点的分项名称。
|
||
仅使用文本中已出现的短语,不编造清单外工程。
|
||
"""
|
||
primary = (boq or '').strip() or (summary or '').strip()
|
||
secondary = (summary or '').strip() if primary != (summary or '').strip() else ''
|
||
candidates: list[str] = []
|
||
|
||
def _consume(text: str) -> None:
|
||
for raw in text.splitlines():
|
||
line = raw.strip()
|
||
if not line or len(line) < 4:
|
||
continue
|
||
if re.match(r'^\|[\s\-:|]+\|$', line):
|
||
continue
|
||
if line.startswith('|'):
|
||
cells = _boq_line_cells(line)
|
||
for c in cells:
|
||
c = c.replace('**', '').strip()
|
||
if 4 <= len(c) <= 80 and re.search(r'[\u4e00-\u9fff]', c):
|
||
if re.match(r'^(序号|编码|项目编码|名称|单位|工程量|单价|合价|分部|分项)$', c):
|
||
continue
|
||
if re.match(r'^[\d\.%%]+$|^第?[一二三四五六七八九十]+[章节部分项、]', c):
|
||
continue
|
||
candidates.append(c)
|
||
continue
|
||
m = re.match(r'^[\d\.]+\s+(.+)', line)
|
||
if m:
|
||
frag = m.group(1).strip()[:80]
|
||
if re.search(r'[\u4e00-\u9fff]', frag):
|
||
candidates.append(frag)
|
||
continue
|
||
if re.match(r'^[((]?[一二三四五六七八九十百零〇\d]+[))]\s*[、..]?\s*.+', line) and len(line) < 100:
|
||
candidates.append(line[:80])
|
||
|
||
_consume(primary)
|
||
if secondary:
|
||
_consume(secondary)
|
||
|
||
seen: set[str] = set()
|
||
out: list[str] = []
|
||
for c in candidates:
|
||
key = c.strip()
|
||
if key and key not in seen:
|
||
seen.add(key)
|
||
out.append(key)
|
||
if len(out) >= max_items:
|
||
break
|
||
return out
|
||
|
||
|
||
def _schedule_figure_body(
|
||
clean: str,
|
||
hint: str,
|
||
sch: dict[str, Any],
|
||
nodes: list[str],
|
||
) -> str:
|
||
start_s = sch.get('start')
|
||
end_s = sch.get('end')
|
||
nd = sch.get('duration_days')
|
||
milestones = sch.get('milestones') or []
|
||
|
||
lines: list[str] = [
|
||
f'[FIGURE:{clean}]',
|
||
'【编制原则】进度网络/横道须与招标文件计划工期及工程量清单工作范围一致;下列日期、日历天与节点均**仅摘自**已解析的项目摘要与清单摘要,摘要未出现的**不得臆造**。',
|
||
'【计划工期(摘自资料)】',
|
||
]
|
||
if start_s:
|
||
lines.append(f'· 计划开工日期(摘录):{start_s}')
|
||
else:
|
||
lines.append('· 计划开工日期:摘要/清单未摘录到明确日期的,以招标文件、补遗书及合同协议书为准,**本图不填写具体开工日**。')
|
||
if end_s:
|
||
lines.append(f'· 计划完工/竣工日期(摘录):{end_s}')
|
||
else:
|
||
lines.append('· 计划完工/竣工日期:同上,以招标文件明示为准,**本图不填写具体完工日**。')
|
||
if nd:
|
||
lines.append(f'· 总工期(摘录):{nd} 日历天')
|
||
else:
|
||
lines.append('· 总工期:摘要未摘录日历天数的,**不绘制具体日历刻度**,仅表示工序逻辑先后。')
|
||
|
||
if milestones:
|
||
lines.append('【控制性节点/里程碑(摘自资料原文)】')
|
||
for m in milestones[:6]:
|
||
lines.append(f'· {m}')
|
||
else:
|
||
lines.append('【控制性节点】资料未摘录专项里程碑条目的,不强行列举;实施阶段按监理与招标人批复计划执行。')
|
||
|
||
lines.append('【工程量清单工作项与网络节点(图中工序名称仅限下列,禁止增加清单外内容)】')
|
||
if nodes:
|
||
for i, n in enumerate(nodes, 1):
|
||
lines.append(f'{i}. {n}')
|
||
else:
|
||
lines.append(
|
||
'(清单摘要未解析到分项名称时:网络图仅表示「施工准备 → 清单列项施工 → 验收移交」三阶段逻辑关系,'
|
||
'具体分项以招标工程量清单及图纸为准。)'
|
||
)
|
||
|
||
lines.append('【施工进度逻辑示意】')
|
||
if nodes:
|
||
chain = ' → '.join(nodes[:10])
|
||
lines.append(f'逻辑关系(示意):开始 → {chain} → 结束')
|
||
else:
|
||
lines.append('逻辑关系(示意):开始 → 施工准备 → 清单工程施工 → 竣工验收与移交 → 结束')
|
||
|
||
ndays = sch.get('duration_days')
|
||
if ndays and isinstance(ndays, int) and nodes:
|
||
lines.append(
|
||
f'横道比例说明:总控制工期 {ndays} 日历天(摘自资料);下列横道仅为相对比例示意,条形长度不代表未在招标文件中给出的细部作业日历。'
|
||
)
|
||
width = 36
|
||
n_show = min(len(nodes), 8)
|
||
for i, node in enumerate(nodes[:n_show]):
|
||
frac = (i + 1) / (n_show + 1)
|
||
filled = max(1, min(width, int(round(width * frac))))
|
||
bar = '█' * filled + '░' * (width - filled)
|
||
label = node[:18] + ('…' if len(node) > 18 else '')
|
||
lines.append(f'{label:<20} {bar}')
|
||
elif nodes:
|
||
lines.append('(未摘录总日历天:横道仅表示先后顺序,不标注天数刻度。)')
|
||
for i, node in enumerate(nodes[:10], 1):
|
||
lines.append(f' {i}. {node}')
|
||
|
||
lines.append(f'(编制依据摘要:{hint})')
|
||
lines.append('[/FIGURE]')
|
||
return '\n'.join(lines)
|
||
|
||
|
||
def _build_schedule_figure_md(title: str, summary: str, boq: str) -> str:
|
||
clean = (title or '施工进度计划网络图').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
sch = _parse_schedule_facts(summary, boq)
|
||
nodes = _extract_boq_work_items(summary, boq)
|
||
return _schedule_figure_body(clean, hint, sch, nodes)
|
||
|
||
|
||
def _site_layout_ascii(zones: list[str]) -> str:
|
||
"""极简 ASCII,仅标注已给出的用地用途名称。"""
|
||
if not zones:
|
||
return '(临时用地分区见临时用地表)'
|
||
z = zones[:8]
|
||
top = ' — '.join(f'[{a}]' for a in z[:4])
|
||
bot = ''
|
||
if len(z) > 4:
|
||
bot = ' — '.join(f'[{a}]' for a in z[4:])
|
||
parts = [top]
|
||
if bot:
|
||
parts.append(' │')
|
||
parts.append(' ' + bot)
|
||
return '\n'.join(parts)
|
||
|
||
|
||
def _build_site_layout_figure_md(title: str, summary: str, boq: str) -> str:
|
||
clean = (title or '施工总平面图').strip()
|
||
hint = _ctx_hint(summary, boq)
|
||
land_rows = compute_temp_land_rows(summary, boq)
|
||
|
||
lines: list[str] = [
|
||
f'[FIGURE:{clean}]',
|
||
'【编制原则】总平面示意须与《临时用地表》分区及面积一致;**示意图中不得出现塔吊及任何施工机械、'
|
||
'工程车辆的形象或剪影**(含「机械停放区」仅作纯色块或线框,不画车辆轮廓)。',
|
||
'【临时用地分区与面积(与临时用地表同口径)】',
|
||
]
|
||
for r in land_rows:
|
||
lines.append(f'· {r["use"]}:{r["area_m2"]}㎡;位置:{r["loc"]};时间:{r["time"]}')
|
||
|
||
lines.append(
|
||
'【图示范围】仅表达临建分区、环场道路、大门、堆场/加工区与主作业区的相对位置关系;'
|
||
'设备配置见《主要施工设备表》及清单,**本图不表现机械**。'
|
||
)
|
||
|
||
zone_labels = [str(r['use']) for r in land_rows]
|
||
lines.append('【相对位置关系示意(非比例尺;分区名称与上表一致)】')
|
||
lines.append(_site_layout_ascii(zone_labels))
|
||
|
||
lines.append(
|
||
'文字要点:环场道路、门禁、办公生活区与作业区隔离、堆场与加工区相对位置、排水沉淀、消防设施布置等须符合招标文件及用地表;'
|
||
f'具体坐标以审批总平面为准。(依据摘要:{hint})'
|
||
)
|
||
lines.append('[/FIGURE]')
|
||
return '\n'.join(lines)
|
||
|
||
|
||
def build_standard_appendix_markdown(
|
||
kind: str,
|
||
title: str,
|
||
summary: str = '',
|
||
boq_summary: str = '',
|
||
) -> str:
|
||
"""生成完整单块 markdown(含 [TABLE] 或 [FIGURE])。"""
|
||
clean = (title or '附件').strip()
|
||
hint = _ctx_hint(summary, boq_summary)
|
||
|
||
if kind == 'main_equipment':
|
||
return _build_main_equipment_table_md(clean, summary, boq_summary)
|
||
|
||
if kind == 'test_instruments':
|
||
return _build_test_instruments_table_md(clean, summary, boq_summary)
|
||
|
||
if kind == 'labor_plan':
|
||
return _build_labor_plan_table_md(clean, summary, boq_summary)
|
||
|
||
if kind == 'temp_land':
|
||
return _build_temp_land_table_md(clean, summary, boq_summary)
|
||
|
||
if kind == 'schedule_chart':
|
||
return _build_schedule_figure_md(clean, summary, boq_summary)
|
||
|
||
if kind == 'site_layout':
|
||
return _build_site_layout_figure_md(clean, summary, boq_summary)
|
||
|
||
return ''
|
||
|
||
|
||
def is_mandatory_bid_appendix(title: str) -> bool:
|
||
"""是否为应强制输出图/表块的标准附件(与「正文可选图表」开关解耦)。"""
|
||
return classify_standard_appendix(title) is not None
|