尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

i2 Analyst‘s Notebook 8 .doc解析:实体关系提取与导入实战

i2 Analyst‘s Notebook 8 .doc解析:实体关系提取与导入实战 简介这份资源是 i2 Analysts Notebook 8 的中文培训教材面向犯罪侦查、金融调查、反欺诈等领域的分析人员以及需要掌握数据可视化与关联分析工具的初学者。教材以话单、银行交易等真实场景为主线帮助读者从零建立节点、边、时间线、聚类等核心概念并逐步过渡到实战演练。资源包内仅含 1 个 doc 文档压缩包约 25.9MB内容按章节组织涵盖入门、基本绘图、基础功能与功能演练四大模块。其中功能演练部分尤为完整包含话单关系分析、案件串并、人员物品动态关系、银行账户交易分析、话单 ABC 分析、手机串号分析、盗窃案旅业分析、爆炸案话单分析、人员活动轨迹、银行账户五级分析、通讯簿分析及活动轨迹跟踪绘图等十余个专题可帮助读者掌握从数据导入、图表绘制到高级搜索与关联挖掘的完整流程。目前已有 1578 人学习适合作为系统化培训与实操参考。1. 从一份 .doc 说起i2 Analysts Notebook 8 到底在解决什么手里拿到一份 i2 Analysts Notebook 8 导出的.doc报表第一反应往往是「这不就是个 Word 文档吗」。真打开看里面是成排的实体清单、关系描述、时间线摘要还有一堆带编号的图表引用。问题在于这些内容原本是 Analysts Notebook 里一张可交互的关系网络图导出成.doc之后图没了只剩文字。你要做的不是读它而是把它重新变回可分析的结构化数据或者反过来把分析结论按这套格式交付出去。i2 Analysts Notebook 8 是 IBM i2 系列里做链路分析、时间线梳理、实体关系建模的主力工具常见于反欺诈、案件研判、供应链溯源这类场景。它擅长把散落的人、账号、电话、地址、交易记录连成一张图再叠加时间轴看事件演进。而.doc在这里扮演的是「交付物」或「交换格式」的角色——上游分析师在 Notebook 里画完图导出成文档给下游下游拿到文档要么归档要么把里面的实体关系重新导入别的系统。这篇要讲清楚三件事.doc里到底承载了哪些可提取的结构、怎么把它还原成可再分析的实体关系表、以及反过来怎么把外部数据整理成 Notebook 8 能吃的格式。适合手里有这类文档、想把它变成可查询数据的人也适合需要把分析结果按固定格式交付出去的从业者。下面按「先看懂结构、再动手提取、最后避坑」的顺序推。2. 拆解 .doc 里的实体关系结构先搞清楚哪些字段能提2.1 实体、关系、属性三件套在文档里的落点Analysts Notebook 的核心数据模型就三样实体Entity、关系Link、属性Attribute。导出成.doc后这三样通常以表格或分段文本的形式出现。实体一般表现为「类型 名称 标识」的组合比如「人员张三身份证号 110…」。关系则写成「张三 — 持有 — 账户 622…」这种主谓宾结构或者用箭头符号连接。属性散落在实体或关系的描述句里比如「开户时间2023-04-12」。常见做法是先用文本解析把文档切成块再按「实体行」「关系行」「属性行」分类。难点在于导出格式不统一有的版本用制表符对齐有的用全角空格有的干脆把关系写成自然语言句子。我一般先做一次「结构探测」——统计每行的分隔符类型和字段数量判断它属于哪种导出模板。import re from collections import Counter def probe_structure(doc_text): 探测 .doc 导出文本的行结构返回分隔符统计和字段数分布 lines [l.strip() for l in doc_text.splitlines() if l.strip()] sep_counter Counter() field_counts Counter() for line in lines: # 常见分隔符制表符、全角空格、竖线、连续空格 if \t in line: sep_counter[tab] 1 parts line.split(\t) elif in line or | in line: sep_counter[pipe] 1 parts re.split(r[|], line) elif in line: sep_counter[multi_space] 1 parts re.split(r\s{2,}, line) else: sep_counter[none] 1 parts [line] field_counts[len(parts)] 1 return sep_counter, field_counts # 用法把 .doc 转成纯文本后传入 # doc_text open(export.txt, encodingutf-8).read() # print(probe_structure(doc_text))这段代码不直接提取数据而是先告诉你文档长什么样。sep_counter告诉你哪种分隔符占主导field_counts告诉你每行大概几个字段。如果field_counts集中在 3 或 4说明结构规整可以直接按列解析如果分散在 1 到 8 之间说明文档里混了标题、正文、表格多种内容需要先分段。参数上re.split(r\s{2,}, line)里的{2,}是阈值两个及以上空格才算分隔。有些导出用单个空格对齐那就得改成{1,}但误切风险会上升。我的经验是先用{2,}跑一遍看字段数分布是否合理不合理再降阈值。2.2 从关系描述句里抽主谓宾正则和规则怎么配合关系行是最难啃的部分因为它经常写成自然语言。比如「张三于 2023 年 4 月通过尾号 8899 的账户向李四转账 5 万元」。这句话里实体是张三、李四、账户 8899关系是「转账」属性是时间、金额。纯正则很难覆盖所有句式常见做法是「正则抽候选 规则校验」。import re # 预定义实体模式人员、账户、电话、地址 ENTITY_PATTERNS { person: r[\u4e00-\u9fa5]{2,4}(?于|向|从|与|和), account: r尾号\s*(\d{4})|账户\s*(\d{6,}), phone: r1[3-9]\d{9}, amount: r(\d(?:\.\d)?)\s*(?:万)?元, date: r(\d{4})[年\-/](\d{1,2})[月\-/](\d{1,2}), } def extract_relation(sentence): 从单句关系描述中抽取实体和属性 result {raw: sentence, entities: [], attrs: {}} for label, pat in ENTITY_PATTERNS.items(): for m in re.finditer(pat, sentence): if label amount: val float(m.group(1)) if 万 in m.group(0): val * 10000 result[attrs][amount] val elif label date: result[attrs][date] f{m.group(1)}-{m.group(2).zfill(2)}-{m.group(3).zfill(2)} else: result[entities].append({type: label, value: m.group(0)}) return result # 示例 # print(extract_relation(张三于2023年4月通过尾号8899的账户向李四转账5万元))逻辑上ENTITY_PATTERNS是候选池每个模式负责一类字段。person模式用「后接介词」来限定避免把普通名词误判成人名。account同时兼容「尾号 8899」和「账户 622…」两种写法。amount里做了「万」的单位换算这是血泪经验——不换算的话5 万和 50000 会变成两个量级后续聚合全乱。参数说明[\u4e00-\u9fa5]{2,4}限定中文姓名长度 2 到 4 字复姓或少数民族姓名可能超长需要放宽到{2,6}。1[3-9]\d{9}是手机号粗匹配不校验号段真实性因为文档里可能有测试数据。日期模式里zfill(2)保证月份和日期补零方便后续按字符串排序。规则校验这一步不能省。正则抽出来的「张三」可能是「张三丰」被截断也可能是「小张三」被误取。我一般会拿一份已知实体清单做交叉验证命中率低于 80% 就回去调模式。这一步没有银弹只能靠样本迭代。2.3 把提取结果落成可再导入的实体关系表抽完之后要落成两张表实体表和关系表。实体表至少要有entity_id、type、name、attrs关系表要有link_id、source_id、target_id、relation_type、attrs。这样后续无论是导入 Neo4j、NetworkX 还是回灌 Notebook都有统一入口。import uuid import json def build_tables(relations): 把抽取结果整理成实体表和关系表 entities {} links [] for rel in relations: ent_ids [] for ent in rel[entities]: key (ent[type], ent[value]) if key not in entities: entities[key] { entity_id: str(uuid.uuid4())[:8], type: ent[type], name: ent[value], attrs: {} } ent_ids.append(entities[key][entity_id]) if len(ent_ids) 2: links.append({ link_id: str(uuid.uuid4())[:8], source_id: ent_ids[0], target_id: ent_ids[1], relation_type: transfer, # 可由句式分类器推断 attrs: rel[attrs] }) return list(entities.values()), links # 输出可直接写 CSV 或 JSONuuid.uuid4()[:8]取前 8 位做短 ID够用且可读。relation_type这里写死成transfer只是示例实际应该用一个小的句式分类器比如判断句子里有没有「转账」「通话」「同行」等关键词。实体去重靠(type, value)元组同一类型同一值只建一条记录避免图里出现重复节点。落表之后建议先做一次连通性检查如果关系表里大量source_id或target_id在实体表里找不到说明抽取阶段漏了实体得回去补模式。这个检查用一句 SQL 就能做。-- 检查关系表中是否存在孤立引用 SELECT l.link_id, l.source_id, l.target_id FROM links l LEFT JOIN entities e1 ON l.source_id e1.entity_id LEFT JOIN entities e2 ON l.target_id e2.entity_id WHERE e1.entity_id IS NULL OR e2.entity_id IS NULL;返回结果不为空就说明有悬空引用优先排查实体抽取的覆盖度而不是急着导入图数据库。3. 反向操作把外部数据整理成 Notebook 8 能吃的格式3.1 Notebook 8 导入对字段的硬性要求反过来如果你手里有 CSV 或数据库表想导入 Analysts Notebook 8 做可视化得先对齐它的字段约定。常见做法是准备两个文件实体文件和关系文件。实体文件至少要有唯一标识列、类型列、标签列关系文件要有起点标识、终点标识、关系类型。列名不一定要和 Notebook 内部字段完全一致但导入向导里要做映射映射错了图就散了。我一般会先把源数据整理成下面这种最小结构再走导入流程。列名含义是否必填示例entity_id实体唯一标识是E001entity_type实体类型是personlabel显示名称是张三attr_date附加属性-日期否2023-04-12attr_amount附加属性-金额否50000关系表对应结构列名含义是否必填示例link_id关系唯一标识是L001source_id起点实体标识是E001target_id终点实体标识是E002link_type关系类型是transferattr_time关系发生时间否2023-04-12注意source_id和target_id必须能在实体表的entity_id里找到否则导入后会出现孤立节点或直接报错。导入前用上一节的 SQL 做一次引用完整性检查能省掉很多返工。3.2 用脚本做字段映射和类型归一源数据往往列名五花八门类型也不统一。比如日期有的写2023/4/12有的写2023-04-12有的写20230412。金额有的带「元」有的带「万」。直接导入会出各种玄学问题最好在导入前用脚本归一。import pandas as pd from datetime import datetime def normalize_date(val): 把多种日期写法统一成 YYYY-MM-DD if pd.isna(val): return None s str(val).strip() for fmt in (%Y-%m-%d, %Y/%m/%d, %Y%m%d, %Y年%m月%d日): try: return datetime.strptime(s, fmt).strftime(%Y-%m-%d) except ValueError: continue return None def normalize_amount(val): 把金额统一成数值处理万/元单位 if pd.isna(val): return None s str(val).replace(,, ).strip() if 万 in s: return float(s.replace(万, ).replace(元, )) * 10000 return float(s.replace(元, )) # 读取源数据 df pd.read_csv(source_entities.csv) df[attr_date] df[日期].apply(normalize_date) df[attr_amount] df[金额].apply(normalize_amount) df df.rename(columns{编号: entity_id, 类型: entity_type, 名称: label}) df[[entity_id, entity_type, label, attr_date, attr_amount]].to_csv( notebook_entities.csv, indexFalse, encodingutf-8-sig )normalize_date用循环尝试多种格式命中即返回全不中返回None方便后续过滤。normalize_amount先去掉千分位逗号再判断「万」做乘法。encodingutf-8-sig是为了让 Excel 打开不乱码如果导入工具对 BOM 敏感改成utf-8。参数上pd.read_csv的dtype建议显式指定 ID 列为str否则E001这种带字母的没问题但纯数字 ID 会被读成int前导零丢失。这个坑我踩过不止一次导进去发现001变成1关系全对不上。3.3 导入后做一次图完整性自检导入完成不等于万事大吉。Notebook 里图能画出来但可能有重复节点、自环、类型错标。常见做法是导入后先看三个指标节点总数、关系总数、孤立节点数。孤立节点就是没有任何关系连接的实体要么是源数据里本来就没有关系要么是关系表的 ID 映射错了。import networkx as nx def check_graph(entities_df, links_df): 用 NetworkX 做导入前的图完整性自检 G nx.Graph() for _, row in entities_df.iterrows(): G.add_node(row[entity_id], typerow[entity_type], labelrow[label]) for _, row in links_df.iterrows(): if row[source_id] in G and row[target_id] in G: G.add_edge(row[source_id], row[target_id], typerow[link_type]) else: print(f悬空关系: {row[link_id]} - {row[source_id]} / {row[target_id]}) isolated [n for n in G.nodes if G.degree(n) 0] print(f节点数: {G.number_of_nodes()}, 关系数: {G.number_of_edges()}, 孤立节点: {len(isolated)}) return G, isolated # G, iso check_graph(entities_df, links_df)nx.Graph()建无向图如果关系有方向性就换nx.DiGraph()。G.degree(n) 0筛孤立节点。悬空关系在加边前就打印出来避免静默丢弃。孤立节点占比超过 20% 就要回头看源数据是不是关系表覆盖不全。这一步用 NetworkX 而不是直接导 Notebook是因为脚本检查比在 GUI 里肉眼找快得多尤其是节点上千的时候。4. 避坑与排查.doc 解析和导入环节的五个翻车点4.1 现象解析出来全是乱码中文变成问号原因通常是.doc是二进制格式直接按文本读会拿到错误编码。老版本.doc不是纯文本是 OLE 复合文档得先转成.docx或纯文本再处理。用python-docx只能读.docx读.doc会直接报错。解决先用 LibreOffice 或 Word 批量转成.docx再用python-docx提取段落和表格。命令行转换可以写成libreoffice --headless --convert-to docx --outdir ./converted ./raw_docs/*.doc转完再读编码问题基本消失。如果转换后还有乱码检查源文件是不是加密或损坏。4.2 现象实体抽出来一堆「于」「向」「和」这种单字原因person模式里[\u4e00-\u9fa5]{2,4}没有排除停用词介词被当成姓名。正则只认长度和位置不认语义。解决维护一个停用词表抽取后过滤。停用词至少包含「于、向、从、与、和、在、对、为、被、把、将、由、经、通过」这些高频介词和连词。过滤逻辑放在抽取之后、建表之前避免污染实体表。4.3 现象金额字段有的 50000 有的 5聚合结果差三个数量级原因源数据里「5 万」和「50000」混用归一化时只处理了一种。或者「万」字被全角/半角混写万 in s判断失败。解决归一化函数里同时处理全角「万」和半角「万」并且先做一次字符规范化把全角数字和符号转半角。unicodedata.normalize(NFKC, s)能处理大部分全角半角问题放在normalize_amount开头。4.4 现象导入 Notebook 后关系全指向同一个节点原因实体 ID 生成时用了自增整数但源数据里不同实体被分配了相同 ID或者 ID 列在读取时被 pandas 自动转成 float1和1.0被当成同一个。解决ID 列强制dtypestr生成 ID 时用uuid或「类型前缀 序号」保证唯一。导入前用df[entity_id].duplicated().sum()检查重复不为零就先解决重复再导。4.5 现象时间线在 Notebook 里顺序全乱原因日期字段没归一2023-4-5和2023-04-05按字符串排序时顺序不同。或者时区信息丢失跨时区数据混在一起。解决统一成YYYY-MM-DD格式需要精确到秒就统一成YYYY-MM-DD HH:MM:SS。如果源数据带时区先统一转成同一时区再落库。字符串排序在补零之后是可靠的不补零就会出「10 月排在 2 月前面」这种问题。5. 进阶把 .doc 解析做成可复用的流水线单次解析一份.doc用脚本跑跑就行但如果这类文档是持续产生的就得做成流水线。我一般会拆成四段转换、抽取、校验、导出。每段独立可测中间用 JSON 或 CSV 落盘方便断点重跑。import subprocess import json from pathlib import Path def pipeline(doc_path, out_dir): 四段式流水线转换 - 抽取 - 校验 - 导出 out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) # 1. 转换.doc - .docx subprocess.run([ libreoffice, --headless, --convert-to, docx, --outdir, str(out_dir / converted), str(doc_path) ], checkTrue) # 2. 抽取读 docx抽实体关系 from docx import Document docx_path out_dir / converted / (Path(doc_path).stem .docx) doc Document(str(docx_path)) full_text \n.join(p.text for p in doc.paragraphs) relations [extract_relation(line) for line in full_text.splitlines() if line.strip()] # 3. 校验检查悬空引用和重复 ID entities, links build_tables(relations) ent_ids {e[entity_id] for e in entities} dangling [l for l in links if l[source_id] not in ent_ids or l[target_id] not in ent_ids] if dangling: print(f警告{len(dangling)} 条悬空关系已写入日志) (out_dir / dangling.json).write_text(json.dumps(dangling, ensure_asciiFalse, indent2)) # 4. 导出落 CSV pd.DataFrame(entities).to_csv(out_dir / entities.csv, indexFalse, encodingutf-8-sig) pd.DataFrame(links).to_csv(out_dir / links.csv, indexFalse, encodingutf-8-sig) print(f完成{len(entities)} 实体{len(links)} 关系) # pipeline(case_2023.doc, ./output)subprocess.run调 LibreOffice 做转换checkTrue保证转换失败时抛异常而不是静默继续。抽取段复用前面的extract_relation和build_tables。校验段把悬空关系单独落盘不阻断主流程因为有些悬空是源文档本身的问题不是解析错误。导出段用utf-8-sig保证 Excel 兼容。参数上out_dir建议按日期或案件编号分目录避免多次运行互相覆盖。dangling.json是后悔药出了问题可以回查是哪几条关系没对上。如果文档量大转换段可以并行但 LibreOffice 无头模式并发多了会抢资源我一般控制在 4 个并发以内。验证流水线是否可靠我习惯拿一份已知结果的文档跑一遍人工核对实体数和关系数。如果实体数对但关系数少多半是关系句式没覆盖全回去补ENTITY_PATTERNS或加句式分类规则。如果实体数就少了检查probe_structure的分隔符判断是不是把某些行归成了「无分隔符」而跳过。这套东西我前后改了三版第一版直接读.doc全是乱码第二版没做金额归一导致聚合全错第三版才把校验段加上。现在每次拿到新格式的导出文档先跑probe_structure看结构再决定要不要调模式。希望帮到你。本文还有配套的精品资源点击获取
返回列表