装修报价、工程结算最头疼的就是手里一份几十上百页的工程量清单 PDF,想把它变成可筛选、可汇总的结构化数据,却只能靠人工一行行抄。本文用 pdfplumber 把国标清单计价表(带表头:序号/项目编码/项目名称/项目特征/单位/工程量/综合单价/合价...)解析出来,再用正则把"项目编码""综合单价"等关键字段抽干净,最后用 openpyxl 导出成一张能直接发给甲方对账的 Excel。
一、为什么不直接用 pdfplumber.extract_table
pdfplumber 的 page.extract_table() 对规整的表格很好用,但工程清单常有跨页续表、合并单元格、表头重复出现,直接 extract_table 会丢行或把表头当数据。更稳的做法是:先按行拿文本,再用正则识别"这是不是一行清单项"。
python
import pdfplumber, re, os
PDF = "工程量清单.pdf"
def is_item(line):
# 清单项特征:开头是数字序号,且行内含 12 位项目编码(国标)
return bool(re.match(r'^\s*\d+[\.\)]?\s', line)) and bool(re.search(r'\d{12}', line))
rows = []
with pdfplumber.open(PDF) as pdf:
for pi, page in enumerate(pdf.pages):
text = page.extract_text() or ""
for line in text.splitlines():
if is_item(line):
rows.append((pi + 1, line))
print(f"共识别到 {len(rows)} 行疑似清单项")
二、用正则在单行里拆字段
国标清单计价表的每行,项目编码是 12 位数字,综合单价/合价是金额(带小数),单位是短中文(m²、m、项)。我们用几个针对性正则分别抓取:
python
CODE = re.compile(r'(\d{12})') # 12 位项目编码
MONEY = re.compile(r'(\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)') # 千分位/小数金额
UNIT = re.compile(r'([m³㎡m²cm个套项遍遍次处]|平方米|米|项)')
def parse(line):
code = CODE.search(line)
units = UNIT.findall(line)
moneys = [float(m.replace(',', '')) for m in MONEY.findall(line)]
# 金额通常:单价、合价(合价=单价×量,取最大的两个)
moneys.sort()
price = moneys[-2] if len(moneys) >= 2 else (moneys[-1] if moneys else 0.0)
total = moneys[-1] if len(moneys) >= 2 else 0.0
name = line
if code:
name = line.replace(code.group(1), '').strip()
return {
'code': code.group(1) if code else '',
'name': name[:60],
'unit': units[-1] if units else '',
'price': price,
'total': total,
}
注意:
MONEY会同时抓到工程量(如 12.5)、单价、合价。工程清单里"合价"一般是该行最大金额,"单价"次之,所以取排序后最后两个最稳。若你的表列序不同,按列位置取更准------可改用extract_table拿到列后再取值。
三、聚合成结构化列表
python
records = []
for pi, line in rows:
r = parse(line)
r['page'] = pi
records.append(r)
# 按项目编码去重,保留综合单价/合价最高的一条(不同页续表常重复)
best = {}
for r in records:
k = r['code'] or r['name']
if k not in best or r['total'] > best[k]['total']:
best[k] = r
final = list(best.values())
print(f"去重后 {len(final)} 条清单")
四、导出 Excel 对账
中文列名 openpyxl 原生支持,直接写即可:
python
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
wb = Workbook(); ws = wb.active
ws.append(["页码", "项目编码", "项目名称", "单位", "综合单价(元)", "合价(元)"])
for r in final:
ws.append([r['page'], r['code'], r['name'], r['unit'], r['price'], r['total']])
ws.append(["", "", "合计", "", "", round(sum(r['total'] for r in final), 2)])
for c in ws[1]:
c.font = Font(bold=True)
for row in ws.iter_rows():
for c in row:
c.alignment = Alignment(horizontal='left', vertical='center')
ws.column_dimensions['C'].width = 50
wb.save("工程量清单_解析.xlsx")
print("已导出 工程量清单_解析.xlsx")
五、避坑提示
- 跨页续表:清单常一页写不下,下页不带表头继续,务必按页遍历再按行识别,别只取首页。
- 金额列歧义 :若表里有"人工费""机械费"分列,金额正则会多抓,建议改用
extract_table拿列索引精确取值,正则只做兜底。 - 编码缺失行:有些清单项编码为空(如措施项目),去重键要回退到"名称+单位",否则会误并。
- 千分位 :金额里
1,234.56的逗号必须先replace(',','')再float,否则报错。 - 字体 :
pdfplumber解析中文依赖 PDF 内嵌字体,极少数扫描件(图片型 PDF)需要先 OCR,纯文本型 PDF 才能直接抽。
这套脚本把"几十页 PDF → 一张可筛选 Excel"从几小时人工压缩到几秒,正好对应工程报价里最费眼的核对环节。把它和在线报价系统串起来,就是一份现成的清单解析工具雏形。