ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从Word使用指南到Python桁架求解器:表格解析与有限元对账

从Word使用指南到Python桁架求解器:表格解析与有限元对账 简介《结构力学求解器使用指南.doc》是一份面向土木工程、工程力学专业学生、教师及结构设计人员的软件操作文档对应清华大学土木系研制的结构力学求解器V1.5。它围绕平面杆件结构的几何组成分析、静定与超静定结构的力与位移计算、影响线绘制、自振频率与振型求解以及弹性稳定分析等课程核心问题提供从装机运行到数据输入的完整说明。压缩包仅1个doc文件约40KB轻量便于随查随用适合在Windows 98/NT环境下配合软件学习。文档详细拆解节点与单元定义、成批生成、支座约束、材料属性、节点与单元荷载以及频率、影响线参数等输入格式并给出上机操作流程。读者可据此掌握建模命令、结果查看与常见参数设置借助软件可视化显示力图、位移图、振型和失稳模态提升结构力学解题与教学演示效率。目前已有81人学习下载。1. 一份「结构力学求解器使用指南.doc」的真正成本在文档之外交接时拿到的往往不是一个可执行程序而是一个 .doc封面写着「结构力学求解器使用指南」后面跟着节点坐标表、单元连接表、支座约束和荷载工况。双击预览页面大片空白换台机器用 WPS 打开表格跨页错位。真正要交付的东西不是这份文档本身而是里面那套能被求解器吃进去的模型数据。所以这里走两条线一条是把 .doc 稳定读成结构化数据另一条是按使用指南的口径复现一个最小结构力学求解器最后用节点位移、支座反力和杆件轴力三个量做对账。做结构设计、力学教学或自研计算脚本的人都能照着落地。2. .doc 无法预览时的转换链路与 python-docx 读表拿到「无法预览doc」的报错第一反应通常是去装阅读器其实更该先判断文件本身的格式。后缀名只代表期望不代表内容。结构力学求解器的使用指南多半是十年前排的版里面混着 OLE 复合文档、被改成 .doc 的 docx甚至是直接存成 HTML 的伪文档。判断错一次后面所有表格解析都会歪。这个章节把判断、转换、解析、自检四步串成一条可重复执行的链路跑通之后再把数据交给第 3 章的建模环节。2.1 用 file 和十六进制头判断 .doc 的真实格式最快的判断不看后缀看字节。OLE2 复合文档以D0 CF 11 E0 A1 B1 1A E1开头新版 docx 是 ZIP以PK\x03\x04开头。文库类站点下载下来的 .doc 有时被重新打包过文件头会说真话。# 1) 看 file 的类型判定-b 只输出结论不打印文件名 file -b 结构力学求解器使用指南.doc # 2) 看前 8 个字节-l 8 限制读取长度避免把大文件整个读进来 xxd -l 8 结构力学求解器使用指南.doc参数说明file依赖 magic 数据库对伪装 docx 的识别很准xxd -l 8只取文件头几十 MB 的文档也是毫秒级返回。如果检出的是PK\x03\x04直接复制成 .docx 交给 python-docx 即可省掉一次格式转换。如果检出纯文本或html说明这份「使用指南」是打印排版产物表格靠制表符或table对齐得换解析分支。前 8 字节判定推荐处理D0 CF 11 E0 A1 B1 1A E1真 OLE2 的 .docLibreOffice 无头模式转 docx50 4B 03 04实为 docx/odt 改名改名后直接用 python-docx3C 21 44 4F 43 54 59 50存成 HTML 的伪文档用 lxml 按 table 解析其他可打印字符纯文本按分隔符切列2.2 命令行批量转 docxLibreOffice 无头模式的参数怎么设确认是真 OLE2 之后用 LibreOffice 的无头模式转换比装一堆阅读器干净。WPS 新建菜单里默认给的是 docx 而不是 doc这反向说明新版本写出的都是 OOXML读旧 doc 时指定输入过滤器能少踩很多坑。# 单个文件转换--infilter 强制按 Word 97 解析避免自动识别成 RTF soffice --headless --norestore \ --infilterMS Word 97 \ --convert-to docx:MS Word 2007 XML \ --outdir ./converted \ 结构力学求解器使用指南.doc # 批量先找到所有 doc再逐个转换-print0 处理文件名里的空格和括号 find ./docs -name *.doc -print0 | \ xargs -0 -I{} soffice --headless --norestore \ --convert-to docx --outdir ./converted {}参数说明--headless不弹界面适合服务器--norestore跳过崩溃恢复对话框否则批处理会卡在弹窗--infilter里的过滤器名必须和本机 LibreOffice 版本支持的名称一致写错会静默回退到默认识别--outdir不存在时会自动创建。注意 antiword 只能导出纯文本表格结构全丢对一份满是参数表的使用指南来说没有意义别在这条路上浪费时间。2.3 用 python-docx 把参数表读成结构化数据转换完成后先用一遍扫描确认段落数、表格数和每张表的形状再决定哪个表是节点表。from docx import Document doc Document(converted/结构力学求解器使用指南.docx) print(段落数:, len(doc.paragraphs), 表格数:, len(doc.tables)) for i, t in enumerate(doc.tables): rows [[c.text.strip() for c in r.cells] for r in t.rows] if not rows: continue # 合并单元格会让同一行出现重复文本用 dict.fromkeys 去重后看列数 head list(dict.fromkeys(rows[0])) print(f[表{i}] {len(rows)}行 x {len(head)}列 首行{head})逻辑说明python-docx 只认 OOXML所以第 2.2 步不可跳过doc.tables按文档中出现的先后顺序返回一般封面之后第一张带「节点」「坐标」字样的表就是节点表。cells里常混着全角空格、换行符和不间断空格\xa0必须 strip。水印文字如果放在文本框里不会出现在paragraphs中需要解压 docx 后直接看word/document.xml。参数上Document()也能接收文件对象做内存流处理时更方便。2.4 转换后必须做的三项自检转换不是无损的。至少要核对三件事表格数量是否与原文一致、每张表的行数是否一致、单元格里的数字是否还被识别成数字。合并单元格在转换后经常被拆成一堆空行跨页表格会被分页符切成两段如果不检查第 3 步就会得到一份缺行少列的节点表求解出的位移全是错的。一个可用的做法是把每次转换的结果表数量、总行数写进日志和上一次对比出现差异就人工看一眼。3. 从使用指南 .doc 里还原节点、单元、约束与荷载四张表求解器不吃散文只吃数表。使用指南里对模型的描述通常散落在三四个位置正文举例、参数表、图注和附录算例。要复现指南里的结果就得先把这些描述收敛成统一字段的数据结构再做几何和拓扑体检。这一章定义最小输入模型给出映射规则和校验代码让第 4 章的求解器能直接吃进去。3.1 结构力学求解器的最小输入模型平面桁架和平面刚架的教学求解器输入基本固定为四张表。自由度编号规则是全局约定节点 n 的 x 方向自由度是2n-1y 方向是2n。这个约定决定了第 3.3 节为什么不能随便重排节点。表名字段示例关键约定节点表编号, x, y3, 1.0, 1.0长度统一到 m禁止混 mm单元表编号, 起点, 终点3, 1, 3只表达连接关系方向不影响刚度约束表节点, ux, uy1, 1, 11 表示该方向被约束荷载表节点, Fx, Fy3, 0, -10与坐标轴同向为正单位 kN使用指南里写成「固定铰支座」「滚动支座」的说法要翻译成上面的 0/1 组合。固定铰支座是两个 1滚动支座按滑动方向取一个 1。这一步含糊后面反力对不上就会怀疑代码其实是约束定义读错了。3.2 用规则把 Word 表格映射成节点与单元单元格里带单位是常态先做数值清洗再做结构映射。import re, json NUM re.compile(r[-]?\d(\.\d)?([eE][-]?\d)?) def to_float(s: str) - float: s s.replace(, ,).replace(, -).replace(\xa0, ) m NUM.search(s) # 只取第一个数字忽略 mm、kN、节点等噪声 if not m: raise ValueError(f无法解析数值: {s!r}) return float(m.group(0)) def parse_nodes(rows): nodes {} for r in rows[1:]: if not r or not r[0].strip(): continue # 跳过被合并单元格撑出来的空行 nodes[int(to_float(r[0]))] (to_float(r[1]), to_float(r[2])) return nodes def parse_elems(rows): out [] for r in rows[1:]: if not r or not r[0].strip(): continue out.append((int(to_float(r[1])), int(to_float(r[2])))) return out data { nodes: parse_nodes(NODE_ROWS), elems: parse_elems(ELEM_ROWS), supports: parse_supports(SUP_ROWS), loads: parse_loads(LOAD_ROWS), } json.dump(data, open(model.json, w, encodingutf-8), ensure_asciiFalse, indent2)逻辑说明NUM.search只取第一个数字是刻意的一个字串里出现「1(0,0)」这种写法时正则不会把括号里的 0 也抓出来。节点字典用节点号做键天然防止重复编号。落盘成 JSON 是为了让求解器和文档解析解耦后面换求解器或换算例都不用再回到 Word 里翻表。参数上ensure_asciiFalse保证中文键名可读indent2方便人工 diff。3.3 自由度编号与约束顺序为什么不能重排自由度的编号由节点号直接推导所以节点表一旦重新排序约束表和荷载表里所有基于节点号的引用就会错位但程序不会报错只会安静地算出一组看似合理的错误结果。常见事故是把节点表按 x 坐标排了一遍约束表没动结果本该固定的支座变成了自由的总刚变成奇异矩阵np.linalg.solve抛 LinAlgError 或者给出天文数字的位移。稳妥做法是解析阶段保持节点号原样需要排序时只在装配前建立nid - 索引的映射所有表统一用这个映射取索引。3.4 建模数据体检零长单元、孤立节点与单位混用在装配刚度矩阵之前先做四项检查能省掉大部分调试时间。零长单元起终点坐标相同L0会导致EA/L除零。检查hypot(dx, dy) 1e-9。重复节点两个编号坐标相同通常是指南里表格跨页重复了表头造成的。孤立节点某节点不属于任何单元自由度全是零行零列总刚奇异。单位混用节点用 mm、荷载用 kN 是最常见的一种量级会差一千倍看位移数量级就能识别。import math def health_check(nodes, elems): issues [] for i, j in elems: if i not in nodes or j not in nodes: issues.append(f单元引用不存在的节点: {i}-{j}) continue L math.dist(nodes[i], nodes[j]) if L 1e-9: issues.append(f零长单元: {i}-{j}) joined {n for e in elems for n in e} for nid in nodes: if nid not in joined: issues.append(f孤立节点: {nid}) return issues逻辑说明先用集合求出被任何单元引用过的节点集合再和节点全集做差。返回值是问题清单而不是抛异常因为在处理一份格式不规范的使用指南时一次性看完所有问题比一个一个修更高效。4. 用 Python 写一个最小结构力学求解器总刚装配与轴力求法数据齐了接下来是指南里那套标准流程单元刚度矩阵、坐标转换、总刚装配、引入边界条件、解方程、回代求内力。平面桁架是最小可验证的载体两百行以内能跑完而且位移和轴力都有手算结果可以对比。刚架的做法只是把每个节点自由度从 2 个扩到 3 个思路完全一样。4.1 平面桁架单元刚度矩阵与坐标转换矩阵局部坐标下杆单元只有轴向刚度k_local EA/L * [[1,-1],[-1,1]]。转换到整体坐标靠方向余弦c (xj-xi)/L、s (yj-yi)/L得到整体坐标下的 4×4 单元刚度矩阵ke EA/L * [ c*c c*s -c*c -c*s c*s s*s -c*s -s*s -c*c -c*s c*c c*s -c*s -s*s c*s s*s ]EA是弹性模量与截面面积的乘积单位要和长度单位配套。节点坐标用 m、荷载用 kN 时EA用 kN 即可。方向余弦只影响矩阵里的符号分布不影响最终位移结果这也解释了第 3.1 节为什么说单元表里的起终点顺序不重要。4.2 总刚装配与边界条件的两种处理方式装配就是按自由度把单元矩阵累加进总刚。边界条件有两种写法置大数法把被约束自由度的对角元乘上 1e12实现简单但会污染条件数划行划列法把自由度分成自由集和约束集只对自由子块求解数值上更干净。规模上千自由度之前划行划列都是更好的选择因为它把病态风险降到了最低后面查错时矩阵的条件数也可控。4.3 求解位移、支座反力与杆件轴力import numpy as np def truss_solve(nodes, elems, supports, loads, EA): nid sorted(nodes) idx {n: i for i, n in enumerate(nid)} ndof 2 * len(nid) K np.zeros((ndof, ndof)) F np.zeros(ndof) for n, (fx, fy) in loads.items(): F[2*idx[n]] fx F[2*idx[n]1] fy axes [] for i, j in elems: xi, yi nodes[i]; xj, yj nodes[j] L float(np.hypot(xj-xi, yj-yi)) c, s (xj-xi)/L, (yj-yi)/L ke (EA/L) * np.array([ [ c*c, c*s, -c*c, -c*s], [ c*s, s*s, -c*s, -s*s], [-c*c, -c*s, c*c, c*s], [-c*s, -s*s, c*s, s*s]]) g [2*idx[i], 2*idx[i]1, 2*idx[j], 2*idx[j]1] K[np.ix_(g, g)] ke # 用 ix_ 做外积式累加避免双重循环 axes.append((i, j, c, s, L)) fixed [] for n, (ux, uy) in supports.items(): if ux: fixed.append(2*idx[n]) if uy: fixed.append(2*idx[n]1) free np.setdiff1d(np.arange(ndof), fixed) U np.zeros(ndof) U[free] np.linalg.solve(K[np.ix_(free, free)], F[free]) R K U - F # 约束自由度上的残余力就是支座反力 axial {} for (i, j, c, s, L) in axes: u U[[2*idx[i], 2*idx[i]1, 2*idx[j], 2*idx[j]1]] axial[(i, j)] (EA/L) * float(np.dot([-c, -s, c, s], u)) return U, R, axial逻辑说明K[np.ix_(g, g)] ke用花式索引一次性完成 4×4 累加比双重 for 快一个量级。R K U - F是关键一步约束自由度上的不平衡力就是支座反力不需要单独推公式。轴力用局部坐标位移点乘方向向量再乘EA/L正值为拉、负值为压。跑一个三节点算例对一下手算节点 1(0,0)、2(2,0)、3(1,1)单元 1-3 与 2-3节点 1、2 全部约束节点 3 施加竖直向下 10 kN取EA 2e5 kN。nodes {1: (0.0, 0.0), 2: (2.0, 0.0), 3: (1.0, 1.0)} elems [(1, 2), (1, 3), (2, 3)] supports {1: (True, True), 2: (True, True)} loads {3: (0.0, -10.0)} U, R, axial truss_solve(nodes, elems, supports, loads, EA2.0e5) print(U3 , U[4], U[5])结果与核对口径量计算值手算/量纲核对U3x0对称结构水平无位移U3y-7.071e-5 m10*√2/EA即 -0.0707 mm单元 1-3 轴力-7.071 kN压力等于10/2/sin45°支座 1、2 竖向反力各 5 kN与总荷载 10 kN 平衡4.4 参数怎么调EA、单位制与矩阵病态EA是唯一需要按工程实际填的量。钢材取E ≈ 2.06e5 MPa截面面积按 mm² 算出来的EA单位是 N用之前先换算到长度单位配套的 kN 或 N。常见误用是节点坐标用 mm、EA用 N两边量纲不统一位移会差出 10³ 以上。另一个坑是只有弹簧单元或约束不足时总刚奇异np.linalg.solve抛LinAlgError: Singular matrix此时先回去数一数被约束的自由度是否足以约束三个刚体位移平面问题两个平动加一个转动结构力学的结构必须是几何不变体系这条在代码里同样成立。5. 拿使用指南里的算例做对账与批量复算指南给的结果通常不是全部节点位移而是几个关键量某个节点的竖向挠度、某几根杆的轴力、支座反力。对账就围绕这三类量做别一上来就比对整条位移向量格式差异会把真正的问题淹没。误差判据用相对误差更稳abs(calc - ref) / max(abs(ref), 1e-9) 1e-6。对于恒载下的静定结构这个量级是轻松的超静定结构如果对不上先怀疑约束定义再怀疑单元连接最后才怀疑算法。批量复算的价值在于回归。把指南里每个算例单独存成一份 JSON写一个循环跑完结果汇总成一张表下次改求解器只要 diff 这份输出。import json, glob from truss_solver import truss_solve # 第 4 章的求解器独立成模块 report [] for path in sorted(glob.glob(cases/*.json)): m json.load(open(path, encodingutf-8)) U, R, axial truss_solve( {int(k): tuple(v) for k, v in m[nodes].items()}, [tuple(e) for e in m[elems]], {int(k): tuple(v) for k, v in m[supports].items()}, {int(k): tuple(v) for k, v in m[loads].items()}, m[EA]) max_u float(abs(U).max()) report.append((path, max_u, dict(axial))) for row in report: print(row)逻辑说明JSON 里所有键都会变成字符串取出来必须转回 int否则idx映射会错位。abs(U).max()只用来快速判断量级是否正常真正对账还是逐项比。把truss_solve拆成独立模块是为了让.doc解析脚本不依赖数值库两边可以分别升级。几个实操上的细节值得留意。第一指南里的算例常带自重而自重往往用「等效节点荷载」表达如果直接按集中力输入会漏掉一半分配正确做法是按单元长度把均布荷载分摊到两端节点。第二温度作用和支座沉降在最小求解器里不体现遇到这两类算例要么扩展荷载向量要么明确说明不在覆盖范围内。第三跨页表格解析出来的行数如果比预期少一行先看是不是页脚那一行被当成了表头。最后一条经验把每个算例的参考值也写进 JSON跑完直接断言assert abs(calc - ref) tol一个断言胜过事后翻日志。本文还有配套的精品资源点击获取
返回列表