
简介这份PPT课件围绕国际工程承包合同展开面向工程管理、国际商务及土木工程方向的学生与从业者帮助其系统梳理合同类型划分、通用合同条件与国际工程合同管理要点。压缩包内共1个pptx文件约268KB以幻灯片形式呈现核心知识框架便于课堂讲授、自学速览或备考复习时按页查阅。内容将承包合同按形式与业务内容、承包范围、支付方式三条线索分类涵盖设备供给与安装合同、施工合同、设计—建造合同、交钥匙/EPC合同、施工总承包、分包与劳务合同以及总价、单价、成本加酬金等计价方式并对比FIDIC、ICE、JCT、NEC、EDF、AIA等国际通用合同条件的特点与适用场景。同时涉及合同管理在质量、工期、成本三方面的预期目标以及变更索赔处理、风险防范与各方协调等内容有助于读者建立从合同选择到履约管理的整体认识。目前已有95人学习下载适合作为国际工程合同知识入门与要点速查的参考资料。1. 国际工程承包合同PPT为什么不能直接当资料库用投标截止前一个晚上商务经理把「国际工程承包合同1.pptx」丢进群里九十多页条款正文、报价表、付款里程碑、保函格式、合同附件目录全混在文本框和表格里。你想确认误期损害赔偿的费率上限CtrlF 搜「误期」出来三处另外两处写的是 Delay Damages还有一处藏在组合形状里搜索根本命中不了。反直觉的地方在于这类文件的问题不是内容少而是版式结构和条款语义结构根本不是一回事。编号常常单独占一个文本框正文被拆成七八个小段落关键数字被塞进表格单元格澄清说明写在备注页里。人眼能拼回来程序照单抓取只会得到一堆碎片。所以真正要做的不是「读一遍 PPT」而是把它变成有字段、有编号、能检索、能跨版本比对的条款库。做海外工程商务、合同管理、投标报价的人需要它被临时拉来搭「合同知识库」的后端同学更需要它。2. 用 python-pptx 拆解国际工程承包合同PPT的文本、表格与备注「国际工程承包合同1.pptx」这类文件抽取阶段的目标只有一个把每一段可读文字连同它的页码、位置、来源形状类型一起落地成 JSONL。字段留全后面做条款切分、比对、问答都不用回头重抽。2.1 先认清 PPT 里的六类内容容器python-pptx 的对象模型是 Slide → shapes → 具体形状但阅读顺序不等于 shapes 的排列顺序。文本框的 left/top 才是排序依据而且组合形状GROUPshape_type 等于 6内部的坐标是相对父形状的必须逐层累加。PPT 元素访问路径拿不到文字时的退路普通文本框shape.text_frame.paragraphs[].runs[]无组合形状shape.shapes递归累加 left/top 还原绝对坐标表格shape.has_table→shape.table.rows[].cells[]合并单元格时cell.text返回全文SmartArt / 图示无text_frame直接解析 slide XML 里的a:t节点备注页slide.notes_slide.notes_text_frame无嵌入图片、扫描件无OCR 后按页面位置回填SmartArt 这一条最容易被忽略。国际工程合同 PPT 里「合同价格构成」「付款里程碑」经常画成图示python-pptx 读出来是空的需要用slide._element.xml抓a:t。另外left、top对占位符继承的形状可能是 None取坐标时要做兜底。2.2 最小可跑的遍历脚本import json from pptx import Presentation def walk(shapes, slide_no, origin(0, 0)): 递归遍历形状组合形状内的坐标是相对的必须叠加父级 left/top items [] for sh in shapes: left origin[0] (getattr(sh, left, 0) or 0) top origin[1] (getattr(sh, top, 0) or 0) if sh.shape_type 6: # 6 GROUP items walk(sh.shapes, slide_no, (left, top)) continue if sh.has_text_frame: for para in sh.text_frame.paragraphs: text .join(run.text for run in para.runs).strip() if text: items.append({slide: slide_no, top: top, left: left, kind: text, text: text}) if getattr(sh, has_table, False) and sh.has_table: for r_i, row in enumerate(sh.table.rows): for c_i, cell in enumerate(row.cells): if cell.text.strip(): items.append({slide: slide_no, top: top r_i, left: left c_i, kind: table, text: cell.text.strip()}) return items prs Presentation(国际工程承包合同1.pptx) rows [] for idx, slide in enumerate(prs.slides, start1): rows walk(slide.shapes, idx) if slide.has_notes_slide: # 备注页常放澄清口径别丢 note slide.notes_slide.notes_text_frame.text.strip() if note: rows.append({slide: idx, top: 0, left: 0, kind: note, text: note}) rows.sort(keylambda r: (r[slide], r[top], r[left])) # 还原阅读顺序 with open(clauses_raw.jsonl, w, encodingutf-8) as f: for r in rows: f.write(json.dumps(r, ensure_asciiFalse) \n)坐标单位是 EMU1 厘米等于 360000 EMU排序时不用换算只要保证同一页内可比。表格单元格用top 行号、left 列号造一个伪坐标是为了让表格行插在正确位置而不是整块堆到页面末尾。备注页给 top 赋 0会排在该页最前面如果希望备注参与条款合并改成10**9让它落到页尾更符合实际阅读习惯。2.3 用编号正则把碎片拼回条款抽出来的 JSONL 是行条款是块。切分的关键是识别编号样式把同一个编号下的连续片段按 top 升序拼接。国际工程合同里常见的编号有四种编号样式正则片段示例中文条第\s*[一二三四五六七八九十百零\d]\s*条第三条第2款多层点号\d(?:\.\d){1,3}14.2.1FIDIC 子条款(?:Sub-)?Clause\s\d(?:\.\d)?Sub-Clause 8.4附件编号(?:附录附件import re NO re.compile( r(?Pcn第\s*[一二三四五六七八九十百零\d]\s*条(?:\s*第\s*[\d一二三四五六七八九十]\s*款)?) r|(?Pnum\b\d(?:\.\d){1,3}\b) r|(?Pfidic\b(?:Sub-)?Clause\s\d(?:\.\d)?\b) r|(?Pannex\b(?:Annex|Appendix)\s*[A-Z\d]\b|附[录件]\s*[A-Z\d])) def split_clauses(rows): blocks, cur [], None for r in rows: m NO.match(r[text].lstrip()) if m: # 命中编号开新块 if cur: blocks.append(cur) cur {no_raw: m.group(0), slide: r[slide], parts: [r[text]], top: r[top]} elif cur: # 续写并入当前块 cur[parts].append(r[text]) else: # 编号前的封面、目录等 blocks.append({no_raw: None, slide: r[slide], parts: [r[text]], top: r[top]}) if cur: blocks.append(cur) for b in blocks: b[body] .join(b.pop(parts)) return blocksmatch而不是search很重要编号必须出现在片段开头才算新条款否则「本合同第14.2条约定」这种引用会被误判成条款起点把一条拆成两条。中文数字编号要额外过一遍归一化把「第十四条」转成14否则同一份文件里中英文编号混排时比对阶段会对不上。3. 国际工程承包合同条款的结构化字段与抽取规则切成块只是第一步。要让条款能比对、能问答、能出审阅清单必须给它加结构化字段把金额、工期、费率、义务主体这些从正文里提出来。3.1 字段设计先定能落库的最小集合字段不宜过多够用即可多余字段会逼着你写大量永远跑不准的规则。我一般会留下面这些字段类型来源说明docTEXT文件名如 国际工程承包合同1.pptxslideINT抽取页码回答必须能回指clause_noTEXT归一化编号14.2中文条转阿拉伯数字langTEXT启发式zh / en / mixedowner_sideTEXT规则OWNER / CONTRACTOR / BOTHbodyTEXT原文不做改写保留原样risk_tagTEXT规则逗号分隔可多标签amountsTEXT正则JSON 数组含币种与金额body一定要存原文不要存清洗后的文本。清洗只发生在比对和检索的输入侧原文是唯一能拿去跟对方核对的依据。3.2 金额、费率、工期三类数字的抽取与归一国际工程合同的数字有三个难点币种混用USD、EUR、CNY、$、€、量级缩写million、billion、万、亿、费率表达方式不同0.05% per day、万分之五每天。import re from decimal import Decimal MONEY re.compile( r(?PcurUSD|EUR|CNY|RMB|GBP|JPY|\$|€|¥)\s* r(?Pnum\d[\d,]*(?:\.\d)?)\s*(?Punitmillion|billion|万|亿)?, re.IGNORECASE) UNIT {: 1, 万: 10**4, 亿: 10**8, million: 10**6, billion: 10**9} CUR {$: USD, €: EUR, ¥: CNY, RMB: CNY} def parse_money(text): out [] for m in MONEY.finditer(text): cur CUR.get(m[cur], m[cur].upper()) scale UNIT.get((m[unit] or ).lower(), 1) num (Decimal(m[num].replace(,, )) * scale).normalize() out.append({currency: cur, amount: str(num)}) return outnormalize()去掉尾随零方便后续做等值判断。币种符号¥在部分合同里指日元如果文件同时出现 JPY 和¥要在规则里加一条人工确认标记别自动归一。费率统一折算成「日费率」再入库比对时才有共同单位CN_NUM {一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10} RATE re.compile( r(?Pv\d(?:\.\d)?\s*%|万分之[一二三四五六七八九十\d]|千分之[一二三四五六七八九十\d]) r\s*(?:per|/|每)?\s*(?Pwday|week|month|天|日|周|月)) W_DAYS {day: 1, 天: 1, 日: 1, week: 7, 周: 7, month: 30, 月: 30} def daily_rate(text): res [] for m in RATE.finditer(text): v m[v] if v.startswith(万分之): frac Decimal(0.0001) * CN_NUM.get(v[-1], 0) elif v.startswith(千分之): frac Decimal(0.001) * CN_NUM.get(v[-1], 0) else: frac Decimal(v.rstrip(%).strip()) / 100 res.append({daily_rate: str(frac / W_DAYS[m[w]]), raw: m.group(0)}) return res月按 30 天折算是一个约定不是精算。误期损害赔偿的封顶通常写成合同价的百分比和日费率是两套字段别混在一起算。3.3 义务归属与风险标签的规则引擎规则引擎用正则足够关键是标签体系要稳定别每次看到新说法就加一个新标签。RISK_RULES [ (r业主(?:应|须|负责|承担), OWNER_OBLIGATION), (r承包商(?:应|须|负责|承担), CONTRACTOR_OBLIGATION), (r(?:无条件|不可撤销).{0,8}保函, UNCONDITIONAL_GUARANTEE), (r(?:误期损害赔偿|延期赔偿|delay damages), LD), (r(?:单方|自行)(?:决定|变更)|sole discretion, SOLE_DISCRETION), (r(?:视为|deemed).{0,10}(?:批准|接受|认可), DEEMED_APPROVAL), (r(?:间接损失|consequential loss).{0,10}(?:不|no), CARVE_OUT), ] def tag(body): hits [name for pat, name in RISK_RULES if re.search(pat, body, re.IGNORECASE)] return ,.join(hits) if hits else NONEDEEMED_APPROVAL和SOLE_DISCRETION这两类最值得盯它们往往对应「逾期未答复即视为认可」「业主单方决定价格调整」这类不对称条款商务复核时优先级最高。规则跑完记得统计NONE的占比占比过高说明标签体系跟这份合同的措辞不匹配要回去改正则而不是硬上模型。3.4 落库SQLite 表结构与全文索引CREATE TABLE clause ( id INTEGER PRIMARY KEY, doc TEXT NOT NULL, slide INTEGER NOT NULL, clause_no TEXT, lang TEXT, owner_side TEXT, body TEXT NOT NULL, risk_tag TEXT, amounts TEXT, created_at TEXT DEFAULT (datetime(now)) ); CREATE INDEX idx_clause_no ON clause(doc, clause_no); CREATE INDEX idx_slide ON clause(doc, slide); CREATE VIRTUAL TABLE clause_fts USING fts5( body, contentclause, content_rowidid, tokenizeunicode61 );用外部内容表contentclause而不是把正文再拷一份避免原文和索引两份数据不同步。中文用unicode61分不出词如果查询以中文关键词为主检索走后面章节的 BM25 或向量方案SQLite 只当结构化存储和精确查询用。amounts存 JSON 字符串SQLite 的 json1 扩展可以直接json_extract(amounts, $[0].amount)取第一个金额。4. 合同条款跨版本比对定位国际工程承包合同1到后续版本的实质性偏差实务里最常见的场景是手里有「国际工程承包合同1.pptx」对方回了一份改过的版本你要在半天内说清楚哪些条款被动了、动的是不是要害。4.1 先归一化再 diff否则九成差异是噪声直接拿原文 diff 会崩。全角空格、段内换行、编号后多了个点、中文标点变英文都会产生大量无意义差异。归一化的顺序是去零宽字符 → 全角转半角 → 删所有空白 → 统一数字格式。import re, unicodedata def norm(s): s unicodedata.normalize(NFKC, s) # 全角转半角兼容字符归一 s re.sub(r[\u200b-\u200d\ufeff], , s) # 去零宽字符PPT 里极常见 s re.sub(r\s, , s) # 中文合同里空白不承载语义 return sNFKC 会把「」转成「14.2」也会把「㎡」这类兼容字符改形所以归一化只用于比对输入不写回body。4.2 段落级 diff用 SequenceMatcher 出变更明细比对按clause_no对齐而不是按页码对齐——PPT 增删一页会让后面全部错位。import difflib def diff_clause(old, new, no): a, b norm(old), norm(new) if a b: return None sm difflib.SequenceMatcher(None, a, b, autojunkFalse) ops [op for op in sm.get_opcodes() if op[0] ! equal] return {clause_no: no, similarity: round(sm.ratio(), 3), changes: [{op: t, old: a[i1:i2], new: b[j1:j2]} for t, i1, i2, j1, j2 in ops]}autojunkFalse必须显式传。默认值会把高频字符当噪声过滤掉中文短文本上会导致相似度虚高。相似度落在不同区间处置方式完全不同相似度判定处理建议1.0无变化忽略0.95 ~ 0.99措辞微调只记录不看0.60 ~ 0.95实质修改人工复核 0.60条款被替换优先复核按新条款重跑规则单侧缺失新增或删除查是否为附件合并导致4.3 换了说法但意思相同的条款用 BM25 兜住diff 抓的是字面变化。对方把「误期损害赔偿」改成「逾期完工违约金」字面相似度很低但语义上是同一件事。这类要找回来用一个轻量检索做双向召回。import jieba from rank_bm25 import BM25Okapi def tokenize(text): return [w for w in jieba.lcut(text) if len(w.strip()) 1] corpus [tokenize(c[body]) for c in clauses] bm25 BM25Okapi(corpus, k11.5, b0.75) # k1 控词频饱和b 控长度惩罚 def top_match(query, topk5): scores bm25.get_scores(tokenize(query)) order sorted(range(len(scores)), keylambda i: -scores[i])[:topk] return [(clauses[i][clause_no], round(float(scores[i]), 3), clauses[i][slide]) for i in order]k1取 1.5 左右是通用起点取大更容易被重复词带偏b取 0.75 表示长度惩罚接近满分条款长短差异大时这个值比 0.5 更稳。为了让「误期」和「逾期完工」能对上在分词后加一层同义词映射表把两边的词统一到同一个规范词比换更大的模型更省事也更好解释。4.4 变更分级把人工精力压到真正要紧的地方比对结果不能平铺给人看要按「数值变化 / 义务反转 / 责任上限 / 新增删除」四类打标按风险标签优先级排序输出。数值变化指金额、费率、天数出现差异义务反转指owner_side字段从 CONTRACTOR 变成 OWNER 这类角色互换责任上限指封顶条款百分比变化或封顶条款被删。前两类必须逐条看后两类可以只做抽查。5. 条款问答的召回阈值与人工复核边界问答环节最容易翻车的不是答错而是答得头头是道却引错页码。要压住这个问题混合召回比单路召回可靠BM25 出一份排序向量检索出一份排序用 RRF 融合而不是把两边的分数直接相加——BM25 分数和余弦相似度量纲不同相加没有意义。def rrf(rank_lists, k60): 每个 rank_lists 元素是已按相关性排好的 [(clause_id, score), ...] fused {} for ranks in rank_lists: for pos, (cid, _) in enumerate(ranks, start1): fused[cid] fused.get(cid, 0) 1 / (k pos) return sorted(fused.items(), keylambda kv: -kv[1])k取 60 是常用的平滑项作用是压低头部名次的绝对优势避免某一路召回的 top1 直接把结论带偏。融合后取前 3 条进上下文前 8 条留作候选如果 top1 的融合分数低于 0.016大致相当于两路都排在 20 名开外直接判为未召回输出「未在文档中找到对应条款需人工确认」不要交给模型硬答。回答模板里强制带引用块格式固定为[来源: 国际工程承包合同1.pptx P23 第14.2条]。条款号缺失的段落回退到页码加坐标坐标取 JSONL 里的top值人工拿着页码去 PPT 里翻一屏之内就能定位。审阅清单一律导出 CSV字段固定为条款号、页码、风险标签、相似度、变更摘要、复核人、复核结论。最后一件事复核人签字签在 CSV 上不签在模型输出上。模型输出只是把页码和条款号摆到人面前的中间产物任何进入投标文件的结论都必须能沿着页码和条款号回到 PPT 原文的那一行字。本文还有配套的精品资源点击获取