ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

半结构化 .doc 到 FTS5:铁路规程条款切分与差异比对

半结构化 .doc 到 FTS5:铁路规程条款切分与差异比对 简介这份资料是2021—2022年专题整理的《铁路技术管理规程》普速铁路部分文档面向铁路运输、铁道工程、机车车辆等专业的学生、备考人员及一线技术人员用于系统熟悉普速铁路的法规性技术要求也可作为课程学习与岗位培训的对照依据。压缩包共收录1个doc文件约6.58MB正文按总则、技术设备、线路桥梁及隧道等编章组织条理清晰、便于检索。其中技术设备部分涵盖基建制造与验收交接、限界与安全保护区、养护维修及检查、救援设备、灾害防护、行车安全监测设备等模块线路部分则细化到线路平面及纵断面、路基、桥隧建构筑物、轨道以及道口交叉与线路接轨、安全线及避难线、防护栅栏、声屏障等内容并延伸至信号、通信、联锁等章节。借助这些规范条文读者可掌握从规划设计、施工验收到日常运维的安全标准与作业要求理解限界、路基稳定性、轨道结构、监测设备配置等关键知识点。目前已有188人学习。1. 《铁路技术管理规程》普速铁路部分 .doc一份被当读物下载的半结构化数据集很多人从资源站把这份 2021—2022 年的专题资料拖进硬盘第一反应是这是铁路从业者的教育资料翻两页就放下了。真正卡人的场景在后面想在几千条条款里定位线路安全保护区的数值CtrlF 搜保护区能中但把内容复制进笔记、或喂给检索脚本时就散架——目录页的点线页码混进正文轨 道限 界这类标题被全角空格拆成两半附图里的限界尺寸是图不是字条款编号还是中文数字。这份正文沿用 2014 年 7 月版本结构是三编十九章第一编技术设备管到第九章铁路用地第二编行车组织从第十章排到第十四章第三编信号显示收在第十五至十九章后面还挂着两张附图和八份附件。它更适合按数据源对待先建模层级再清洗入库最后才谈检索与比对。下面几步走完新手能照命令复现熟手能直接看到参数边界和踩坑点。2. 三编十九章的层级建模编、章、节、条的编号规则与切分锚点2.1 编与章的映射要写死不能靠猜三编的边界并不等长第一编技术设备覆盖第 1 至第 9 章从基本要求一路到铁路用地第二编行车组织是第 10 至第 14 章第三编信号显示是第 15 至第 19 章。这个映射关系建议直接写成常量表别用出现第X编就切一刀的规则去推。原因很实在正文里编字会以编组列车编挂车流编组等形态大量出现按字面切分会把第十一章切得七零八落。检索侧同理搜编组应该落到第十一章编组列车搜编则命中的是一堆噪声这本身就是分词粒度问题。层级原文形态示例可用切分锚点编第X编 名称第二编 行车组织行首第[一二三]编\s章第X章 名称第十四章 列车运行第[一二三四五六七八九十]章\s且行内无点线节仅名称无编号接车与发车 / 手信号无锚点需目录白名单条第X条第 233 条第[零一二三四五六七八九十百]条项1. 2. 3. 或 一行首编号行首\d[.、]或[一二三四五六七八九十]节这一层是最难认的。它在排版上靠居中、字号和上下文区分一旦转成纯文本就只剩接车与发车这样一个孤零零的词。常见做法是从目录页把节名抽出来做成白名单集合正文里逐行做精确匹配命中后才升级为节节点。这套办法对付本规程够用因为节名重复率极低。import re CH_HEAD re.compile(r^第([一二三四五六七八九十])章\s*(.)$) # 章行必须行首出现且不能带目录点线点线在 2.2 里处理 def parse_chapters(lines): chapters [] for i, line in enumerate(lines): m CH_HEAD.match(line.strip()) if m and .... not in line: chapters.append({no: m.group(1), title: m.group(2).strip(), line: i}) return chapters这段代码的关键参数是^和strip()章节标题在原文里前后各有一个全角空格strip()之后第X章才能落在行首否则正则永远不匹配。第二个参数是.... not in line它专门用来过滤目录页——目录里的章标题行尾必然跟着一串点和页码。2.2 目录页与正文的重复命中目录部分长得很有规律总 则......................1、第一章 基本要求..................2、第十四章 列车运行...............91。如果直接按第X章切分会先切出一批几乎没有正文的空块紧接着又切出真正的章。判别特征很稳定目录行末尾一定有连续点线加页码。DOT_LEADER re.compile(r[.·]{3,}\s*\d\s*$) def toc_end(lines): 返回目录区最后一行下标找不到则返回 -1 idx [i for i, l in enumerate(lines) if DOT_LEADER.search(l)] if not idx: return -1 # 保险目录区里章标题至少出现 19 次否则判定为误判 cand idx[-1] hits sum(1 for l in lines[:cand] if l.strip().startswith(第) and 章 in l[:8]) return cand if hits 19 else -1[.·]{3,}同时兼容半角点和中点两种点线写法\s*\d\s*$把页码锁在行尾。那个hits 19是回退保护万一正文某张表格也用了点线对齐最后一个目录行可能落在正文深处用章标题计数能把这个错误挡掉。目录区判定失败时退而求其次去找总 则第二次出现的行号因为它在正文里必然再出现一次。2.3 条款切分与中文数字归一化条款是这份规程的最小可引用单元所有检索、比对、题库生成都建立在它上面。用捕获组切分可以保留分隔符切完再判断每一段是条款头还是条款体。CN {零:0,一:1,二:2,三:3,四:4,五:5,六:6,七:7,八:8,九:9} ART re.compile(r(第[零一二三四五六七八九十百]条)) def cn2int(s): s s.strip(第条 ) if s in CN: return CN[s] if s 十: return 10 if 十 in s: a, _, b s.partition(十) return (CN.get(a, 1) * 10 (CN.get(b, 0) if b else 0)) if 百 in s: a, _, b s.partition(百) base CN.get(a, 1) * 100 tail cn2int(第 b 条) if b else 0 return base tail return None def split_articles(text): parts ART.split(text) out, cur [], None for seg in parts: m ART.fullmatch(seg) if m: if cur: out.append(cur) cur {article_no: cn2int(seg), body: } elif cur is not None: cur[body] seg if cur: out.append(cur) return outcn2int要把第一百二十三这类数字转成整数才能做后续的断号检查和按条号排序。百位以上用递归处理尾部比写死映射表干净。切分完成后务必跑一次连续性自检article_no排序后如果出现跳号多半是某个条款头被正文里的引用比如按第 233 条规定误识别或者某条被跨页截断需要用上下文长度做二次筛除——引用型出现的第X条后面通常紧跟规定要求这类词而真正的条款头后面是完整句子。3. .doc 到可检索文本格式转换、清洗与 FTS5 索引3.1 转换路线对比与选择依据.doc是 OLE 复合文档不是 zip 包python-docx这类库只吃.docx直接读会抛异常。所以第一步几乎一定是格式转换。工具输入格式表格保留段落顺序适用场景LibreOffice headlessdoc / docx保留为表格结构保留首选一次转 docx 再解析antiworddoc退化为空格对齐保留无桌面环境、只要纯文本pandocdocx保留保留需要转 Markdown 时顺手python-docxdocx保留保留需要读样式判断节标题catdocdoc丢基本保留应急中文编码需额外指定我一般的做法是先把.doc转成.docx再用python-docx读段落和样式因为节标题的层级信息只存在于样式里纯文本转换会把这个线索彻底丢掉。# --headless 无界面运行--convert-to 指定目标格式 # -env:UserInstallation 给独立配置目录避免批量并发时互相抢锁 soffice --headless \ -env:UserInstallationfile:///tmp/lo_profile \ --convert-to docx --outdir ./out \ 专题资料《铁路技术管理规程》普速铁路部分.doc批量处理时给每个进程分配不同的UserInstallation目录能避免另一个实例正在运行导致的静默退出。转换完成后建议立刻校验输出文件大小如果转出来的 docx 不到原文件的十分之一多半是编码识别失败需要换antiword -m UTF-8.txt兜底再比对行数。3.2 段落级清洗规则转换只是开始真正决定检索质量的是清洗。这一步的原则是原文保留一份归一化字段另存一份检索走归一化字段展示走原文字段。这样既能搜到轨道又不会破坏原文里轨 道的排版形态。噪声类型原文示例处理方式全角空格轨 道、限 界短标题去空格后写入title_norm罗马数字页码信号通信章节页脚的I、II、III按行长度 ≤ 3 且匹配^[IVX]$删除页眉文字每页首行重复的规程名统计高频行首出现次数 50 的直接剔除全半角括号混用一与(一)unicodedata.normalize(NFKC, s)跨页断行条款被页边界切成两段若下一段不以条款头/项号开头则拼接import re, unicodedata ROMAN re.compile(r^[IVX]{1,5}$) def normalize(s: str) - str: s unicodedata.normalize(NFKC, s) # 全角转半角统一括号与数字 s s.replace(\u3000, ).replace(\xa0, ) # 全角空格与不换行空格 s re.sub(r\s, , s).strip() return s def clean_line(s: str) - str: if ROMAN.match(s.strip()): # 罗马数字页码行 return return normalize(s)NFKC这一步别省它会把统一成(、全角数字统一成半角后续正则只写一套就够。注意NFKC也会把某些特殊符号改形如果原文里有限界示意图用的特殊符号务必在清洗前后各存一份快照做抽样比对别一次性覆盖原文件。3.3 条款表与 FTS5 全文索引落库用 SQLite 就够了单文件、可离线、能直接丢进任何脚本。表结构按第 2 章的层级来clauses存正片clauses_fts存索引。CREATE TABLE clauses ( id INTEGER PRIMARY KEY, part TEXT, -- 编如 第二编 行车组织 chapter TEXT, -- 章如 第十四章 列车运行 section TEXT, -- 节无则为空 article_no INTEGER, -- 条款号已归一化为整数 body TEXT, -- 原文 body_norm TEXT -- 归一化文本供检索 ); CREATE INDEX idx_art ON clauses(article_no); CREATE VIRTUAL TABLE clauses_fts USING fts5( body_norm, contentclauses, content_rowidid, tokenizetrigram -- 要求 SQLite 3.34 ); INSERT INTO clauses_fts(rowid, body_norm) SELECT id, body_norm FROM clauses;先跑sqlite3 --version确认版本。trigram分词器支持任意 3 个字符以上的子串匹配对安全保护区列车运行监控装置这类术语效果很好代价是索引体积大约是原文的 3 倍。它有个必须提前知道的限制少于 3 个字符的查询词匹配不到像闭塞限界这种两字词会直接失配。提示检索词普遍在 2 个字左右时把tokenize换成默认unicode61并在入库前用分词工具把body_norm切成空格分隔的词串。unicode61 会把整段连续汉字当成一个 token不预分词的话搜什么都搜不到。3.4 切分质量的自检查询入库后先别急着做检索界面跑几条自检 SQL 看数据是否可信。-- 每章条款数第十六、十七章条款密度明显高于第十九章偏离太多说明切分有问题 SELECT chapter, COUNT(*) AS n, MIN(article_no) AS lo, MAX(article_no) AS hi FROM clauses GROUP BY chapter ORDER BY lo; -- 断号检查用窗口函数找出条款号不连续的位置 SELECT article_no, LEAD(article_no) OVER (ORDER BY article_no) - article_no AS gap FROM clauses WHERE gap 1;第一条查询里lo和hi能帮你在几秒内确认章与条号区间是否对得上比如第十四章列车运行的条号区间和第十一章编组列车的区间必然是紧邻的中间出现大段空白就说明有条款被漏切。第二条用窗口函数算相邻条号差值gap 1的行就是缺口的起点人工回去核对这几处原文即可。4. 参数检索实战限界数值、闭塞方式与信号显示的定位写法4.1 限界数值藏在附图里还是正文里附图 1客货共线铁路建筑限界分了两档一档是v≤160 km/h另一档是v160 km/h还有一张是双层集装箱运输的装载限界与建筑限界。这些图在转换后只剩一个图题尺寸数字全部丢失所以别指望从文本里抠出限界值。可行的做法是把附图当成独立实体入库并建立引用关系。import re # 正文里凡是提到按附图 1见附图 2的条款建立关联 FIG_REF re.compile(r(附\s*图\s*([12]))) ATT_REF re.compile(r(附\s*件\s*([1-8]))) def extract_refs(body: str): figs {m.group(2) for m in FIG_REF.finditer(body)} atts {m.group(2) for m in ATT_REF.finditer(body)} return {figures: sorted(figs), attachments: sorted(atts)}正则里的\s*是必需的原文中附 图中间夹着全角空格只在清洗后的body_norm上跑会更稳。抽出来的关系存成clause_refs(clause_id, ref_type, ref_no)检索限界相关条款时就能一次性把引用附图 1 的所有条款捞出来再回到原图核对数值。如果确实需要把数字变成可计算的数据只能走 OCR而且必须人工复核因为限界图上的标注线密集自动识别错一位数后果很实在。4.2 闭塞方式检索的误召回第十三章行车闭塞里并列了四种方式自动闭塞、自动站间闭塞、半自动闭塞、电话闭塞另有电话中断时的行车。这里有个很典型的子串陷阱半自动闭塞里完整包含自动闭塞这四个字用模糊匹配搜自动闭塞半自动闭塞的条款会全部混进来。检索词期望命中容易误召回判别写法自动闭塞第十三章自动闭塞节半自动闭塞、自动站间闭塞排除含半自动闭塞的行电话闭塞电话闭塞节电话中断时的行车按 section 字段过滤而非全文搜列车调度指挥第三章相关条款调度集中系统用完整术语同义表-- 先按全文索引捞再用 NOT LIKE 排掉子串污染 SELECT id, chapter, section, article_no FROM clauses WHERE id IN (SELECT rowid FROM clauses_fts WHERE clauses_fts MATCH 自动闭塞) AND body_norm NOT LIKE %半自动闭塞%;这条 SQL 的执行顺序很关键FTS 先出候选集NOT LIKE只作用在候选集上成本可控。如果把NOT LIKE单独用在大表上等于全表扫描。同义词这一层建议单独维护一张synonyms(term, alias)表比如列控系统对CTCS-2 级列控系统查询前先做一次词条扩展命中率会明显好过手工拼 OR。4.3 信号显示与附件的编号关联第三编信号显示覆盖第十五到十九章从固定信号、移动信号及手信号一路排到听觉信号附录里还挂着路票、绿色许可证、红色许可证、调度命令、出站/跟踪调车通知书、轻型车辆使用书、调度命令登记簿、书面通知这八份附件。做培训题库时最常见的需求是把信号显示和对应凭证配对这时候靠正文关键词搜很容易漏。# 附件 1 路票、附件 2 绿色许可证……建立凭证字典 ATTACHMENTS { 1: 路票, 2: 绿色许可证, 3: 红色许可证, 4: 调度命令, 5: 出站/跟踪调车通知书, 6: 轻型车辆使用书, 7: 调度命令登记簿, 8: 书面通知, } def link_attachments(conn): cur conn.cursor() for cid, body in cur.execute(SELECT id, body_norm FROM clauses).fetchall(): for m in ATT_REF.finditer(body): cur.execute( INSERT OR IGNORE INTO clause_refs VALUES (?,?,?), (cid, attachment, m.group(2))) conn.commit()附件号到名称的映射写死在这里是因为原文里附件 1和路票并不总在同一段落出现只靠共现统计会漏掉相当一部分。把映射固定下来之后用户搜路票能直接反查到引用它的条款搜附件 5也能拿到名称。5. 条款级差异比对专题资料与 2014 版的增量定位这份 2021—2022 年专题资料的正文主体沿用 2014 年 7 月版本但整理过程中难免有增删。做版本比对时最容易犯的错是拿行号做 key——一旦中间新增一段后面所有行号全部漂移diff 结果会变成一片红。正确做法是以article_no为 key行号只用来定位。import difflib def diff_by_article(old: dict, new: dict): old/new: {article_no: body_norm} added sorted(set(new) - set(old)) removed sorted(set(old) - set(new)) changed [] for no in sorted(set(old) set(new)): if old[no] ! new[no]: ratio difflib.SequenceMatcher(None, old[no], new[no]).ratio() changed.append((no, round(ratio, 3))) return added, removed, changedSequenceMatcher.ratio()返回 0 到 1 的相似度实践中低于 0.85 的多半是实质性修改0.85 到 0.98 之间通常是标点、全半角或分词的差异可以直接过滤掉剩下的再人工看。这个阈值不是定死的标点规范化做得越干净阈值可以压得越低。几类高频排错点值得单独记一笔。第一是跨页表格原文在页边界处会断成两半第二页开头往往有续表之类的标记比对前要按标记把两段拼回去否则会被判成条款被删。第二是 OCR 或转换引入的形近字符混淆典型的是〇与0、l与1、一与—在条款编号和限界数值上破坏性最大建议在归一化函数里把这些字符统一映射。第三是系列条款的重排条号没变但顺序调了diff_by_article看不出来需要额外跑一次顺序校验。-- 顺序校验找出条号顺序与文档出现顺序不一致的位置 SELECT id, article_no, article_no - LAG(article_no) OVER (ORDER BY id) AS delta FROM clauses WHERE delta 0;这条查询按插入顺序即原文档顺序看条号是否递减或持平返回非空结果就说明有条款被移动过位置delta 0的那几行就是切入点。跑完这条如果结果为空、条号连续性检查也干净那么比对结果的可信度就足够了可以直接把变更清单导出给业务方核对不必再逐条人工通读。本文还有配套的精品资源点击获取
返回列表