从数据安全法PPT到控制项:python-pptx抽取与分类分级
发布时间:2026/9/18 1:08:57来源:尧图网络
简介这份《数据安全法解读》课件以PPT形式系统梳理《中华人民共和国数据安全法》的立法脉络与制度要点适合企业法务、合规与信息安全从业者以及需要开展内部普法培训的团队使用。压缩包内共1个pptx文件大小约2.84MB结构围绕立法背景及进程、数安法全面解读、落地应对三部分展开。内容从数据作为生产要素、国内外违法事件频发、“清洁网络”计划与GDPR等外部挑战讲起梳理2018年列入立法规划、2020年草案审议、2021年6月通过并于9月施行的关键节点随后逐章拆解总则、数据安全与发展、数据安全制度、保护义务、政务数据安全与开放及法律责任重点说明数据分类分级、重要数据目录、出口管制、安全审查、风险评估、出境管理与各方主体责任并给出“我们应该怎么做”的实践提示。目前已有157人学习下载可作合规宣贯与自学参考。1. 一份 60 多页的 PPT为什么值得工程师拆开看去年帮一家做车联网的团队做数据资产梳理业务方抛来第一个问题不是怎么加密而是我们这些数据算不算重要数据。安全团队答不上来合规团队只能翻法条翻完还是落不到字段上。后来他们拿这份《数据安全法解读.pptx》当内部培训底稿我把它拆了一遍——它的价值不在于告诉你要合规而在于给出了七章结构的骨架总则、数据安全与发展、数据安全制度、数据安全保护义务、政务数据安全与开放、法律责任、附则每一章对应一类可拆解的义务。对 IT 从业者来说这份资源的真实用法有两层。表层是培训课件它把条文、立法进程、分类分级的行业标准清单、政务数据开放要求整理成了一张张幻灯片直接可以拿去开课。深层是一个可被程序处理的数据源——它本身就是 .pptx里面每一页文本框、表格、备注都是结构化文本可以被抽取、数据库化再映射成企业的控制项清单和自检问卷。做数据治理、等保、密评、数据平台建设的同学把这个映射过程跑一遍比读十遍条文有用。2. 拆开 .pptxOOXML 结构与 python-pptx 文本抽取2.1 一个 pptx 就是一个改了后缀的 zip很多人第一反应是用 Office 打开另存为 txt结果是版式全乱、表格丢失。更靠谱的做法是把它当压缩包处理。.pptx遵循 OOXML 规范内容散在若干 XML 与媒体文件里改后缀为.zip就能解开。不同部分承担不同职责抽之前先认清结构能省掉后面大量返工。路径存什么抽取方式ppt/slides/slideN.xml第 N 页所有形状的文本与坐标python-pptx 或 lxmlppt/notesSlides/notesSlideN.xml演讲者备注通常是讲稿slide.notes_slideppt/slideLayouts/、ppt/slideMasters/版式与母版的固定文字一般要过滤避免重复计数ppt/media/图片、图标、音视频直接解压取用ppt/charts/、ppt/embeddings/图表数据、嵌入对象按需解析docProps/app.xml页数、应用、公司等元信息zipfile 读 XML先用几行命令确认目录里到底有什么再决定抽取策略# 列出包内全部条目并统计各类文件数量确认是否为纯文本型课件 unzip -l 数据安全法解读.pptx | awk {print $4} | grep -v ^$ \ | sed s#.*\.## | sort | uniq -c | sort -rn # 只看幻灯片与备注页判断讲稿是否写进了备注 unzip -l 数据安全法解读.pptx | grep -E slides/|notesSlides/第一条命令按扩展名聚合输出里xml数量应约等于幻灯片数乘以形状数png/jpeg数量反映图表和配图规模。第二条用来判断备注页是否存在如果notesSlides条目为 0说明讲稿没写进备注抽取时就不必调用备注接口省掉一轮异常处理。2.2 用 python-pptx 一次抽全正文、表格与备注直接遍历shapes只能拿到顶层形状遇到组合形状或表格就会漏。下面这个函数做了三件事递归展开组合形状、把表格按行列拼成文本、把备注单独存一列方便后续区分正文和讲稿。from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def walk(shapes, depth0): 递归展开形状组合形状里的文本框是最容易被漏掉的一类 for sh in shapes: if sh.shape_type MSO_SHAPE_TYPE.GROUP: yield from walk(sh.shapes, depth 1) else: yield sh def extract(pptx_path): prs Presentation(pptx_path) rows [] for idx, slide in enumerate(prs.slides, start1): body, tables [], [] for sh in walk(slide.shapes): if sh.has_text_frame: # \x0b 是 PowerPoint 的软换行统一替换成普通换行 txt sh.text_frame.text.replace(\x0b, \n).strip() if txt: body.append(txt) if getattr(sh, has_table, False) and sh.has_table: for r in sh.table.rows: # 单元格内的软换行同样要处理否则入库后会串行 cells [c.text.replace(\x0b, \n).strip() for c in r.cells] tables.append( | .join(cells)) notes if slide.has_notes_slide: notes slide.notes_slide.notes_text_frame.text.strip() rows.append({ slide_no: idx, body: \n.join(body), tables: \n.join(tables), notes: notes, }) return rowswalk的depth参数只用于递归计数实际不需要限制层数slide.has_notes_slide属性在无备注页时返回 False比直接访问notes_slide更安全tables用|拼接行内单元格入库后按这个分隔符切分就能还原成二维表。抽取出来的结果是每页一条记录slide_no就是天然的页码主键。2.3 抽取时最容易踩的四个坑第一是组合形状。课件里为了排版整齐作者常把标题正文编号打成一个组slide.shapes只会返回那个组对象不解开的后果是整页正文全空。第二是表格。表格形状没有text_frame只能走shape.table所以判断条件必须写成has_table而不是has_text_frame。第三是母版文字。页码、目录 CONTENTS这类固定文字存在于版式中如果从slide_master里取文本会在每一页重复出现建议只从slide.shapes取母版单独处理或者直接丢弃。第四是符号与空白。中文课件里常见全角空格、不间断空格\xa0和软换行\x0b入库前做一次统一清洗把\xa0换成普通空格把\x0b换成\n再对连续空行做折叠。这一步不做后面按关键词匹配条款时会出现明明原文有、就是匹配不上的诡异问题。注意抽完先抽查两页把body和 Office 里看到的文字逐字比对一遍确认没有丢段落再往下做入库和映射。3. 把七章条款映射成可执行控制项3.1 条文不等于控制项条文写的是义务比如国家建立数据分类分级保护制度。控制项写的是可验证的动作比如在元数据平台为每张表登记分级标签标签缺失时阻断上线。两者之间差一层翻译义务 → 控制目标 → 技术落点 → 证据物。四个字段凑齐审计时才说得清我做了什么、凭什么证明。映射关系可以按条款域整理成下面这张表它同时是控制项台账的字段设计依据条款域控制目标技术落点证据物数据分类分级明确重要数据目录元数据平台、打标服务分级台账、评审记录风险评估与监测预警风险可发现可上报日志审计、告警平台风险评估报告、告警工单应急处置事件可处置可追溯预案编排、工单系统演练记录、事件报告数据出境管理出境活动可管可查流量审计、审批流出境清单、审批单数据交易中介服务来源合法可追溯数据血缘、来源登记来源凭证、合同安全教育培训人员具备基本意识培训与考核系统签到记录、考试成绩政务数据开放开放目录与脱敏目录服务、脱敏网关开放目录、脱敏规则3.2 三张表把映射固化下来把条款、控制项、证据拆成三张表比塞在一张宽表里更好维护——一条条款可能对应多个控制项一个控制项也可能同时满足多条条款。-- 条款表从 PPT 抽取的正文按章节落库 CREATE TABLE dsl_clause ( clause_id VARCHAR(16) PRIMARY KEY, -- 条款序号如 21 clause_domain VARCHAR(64) NOT NULL, -- 条款域分类分级 / 风险评估 / 应急处置 obligation TEXT NOT NULL, -- 义务原文摘要 chapter VARCHAR(32) -- 所属章节 ); -- 控制项表一条条款可拆出多个控制项用外键回指 CREATE TABLE dsl_control ( control_id VARCHAR(32) PRIMARY KEY, clause_id VARCHAR(16) REFERENCES dsl_clause(clause_id), target VARCHAR(128) NOT NULL, -- 控制目标 tech_point VARCHAR(128), -- 技术落点 owner_dept VARCHAR(64), -- 责任部门对应各地区各部门负责 status SMALLINT DEFAULT 0 -- 0 未建设 1 建设中 2 已上线 ); -- 证据表审计时按控制项拉证据避免临时补材料 CREATE TABLE dsl_evidence ( evidence_id BIGSERIAL PRIMARY KEY, control_id VARCHAR(32) REFERENCES dsl_control(control_id), ev_type VARCHAR(32), -- 台账 / 报告 / 工单 / 日志 uri TEXT, -- 文件或系统链接 produced_at DATE );clause_id用字符串而非自增整数是为了保留条文编号的可读性讨论时直接说第 21 条就能对上。owner_dept这个字段很关键法律责任一章把责任落到具体主体台账里没有责任人控制项就永远停在未建设。用一条查询就能看出哪些条款还是空转-- 找出有条款、无控制项的覆盖缺口 SELECT c.clause_id, c.clause_domain, c.obligation FROM dsl_clause c LEFT JOIN dsl_control ct ON ct.clause_id c.clause_id WHERE ct.control_id IS NULL ORDER BY c.clause_id; -- 按责任部门统计建设进度输出给管理层看的口径 SELECT owner_dept, COUNT(*) FILTER (WHERE status 2) AS done, COUNT(*) AS total, ROUND(100.0 * COUNT(*) FILTER (WHERE status 2) / COUNT(*), 1) AS pct FROM dsl_control GROUP BY owner_dept ORDER BY pct;3.3 从抽取结果自动生成待办清单手工把几十页幻灯片拆成控制项太慢可以先用关键词做一轮粗筛把命中的条款推进人工评审队列。from collections import defaultdict # 条款域 - 关键词。命中即归入该域一个条款可命中多个域 DOMAIN_KEYWORDS { 分类分级: [分类分级, 重要数据目录, 核心数据], 风险评估: [风险评估, 监测预警, 信息共享], 应急处置: [应急处置, 应急预案], 出境管理: [出境, 出口管制], 交易中介: [交易中介, 数据交易], 教育培训: [教育培训, 人才培养], 政务数据: [政务数据, 开放目录], } def tag_slides(rows, min_len8): hits defaultdict(list) for r in rows: text f{r[body]}\n{r[tables]} # 过滤过短段落页眉页脚和编号不参与匹配 for line in (l.strip() for l in text.split(\n)): if len(line) min_len: continue for domain, kws in DOMAIN_KEYWORDS.items(): if any(k in line for k in kws): hits[domain].append({slide_no: r[slide_no], text: line}) return hitsmin_len用来过滤页码、章节编号这类短文本设成 8 个字符能挡掉绝大多数噪声关键词表里保留核心数据这类专有名词而不是拆成核心数据是为了降低误召回如果一个条款被多个域同时命中说明它跨域人工评审时优先处理。跑完之后hits的结构就是一张以条款域为索引的待办清单页号可以直接跳回原幻灯片核对上下文。4. 数据分类分级与风险评估的工程化实现4.1 分级不是拍脑袋是算出来的课件里列了一串行业标准从政务数据到金融、工业、电信各有各的分级指南。落到自己公司不能直接抄任何一份因为维度权重不同。通用做法是取两个轴影响对象和影响程度交叉得出级别。影响对象按个人和组织权益 → 公共利益 → 国家安全递增影响程度按轻微 → 一般 → 严重递增。维度取值权重说明影响对象个人组织权益 / 公共利益 / 国家安全1 / 2 / 3对象越宏观权重越高影响程度轻微 / 一般 / 严重1 / 2 / 3后果越重权重越高数据规模记录数分档0.5 ~ 1.5大规模泄露加重后果可识别性直接标识 / 可关联 / 已匿名1.5 / 1.0 / 0.5匿名化程度越高风险越低OBJ {个人组织权益: 1, 公共利益: 2, 国家安全: 3} DEG {轻微: 1, 一般: 2, 严重: 3} IDENT {直接标识: 1.5, 可关联: 1.0, 已匿名: 0.5} def scale_factor(records): 按记录数给出规模系数分档而非线性避免百万行把分值拉爆 if records 1_000_000: return 1.5 if records 100_000: return 1.2 if records 10_000: return 1.0 return 0.5 def grade(obj, deg, records, ident): score OBJ[obj] * DEG[deg] * scale_factor(records) * IDENT[ident] # 阈值是经验值上线前用历史数据回测再调 if score 13.5: return 4 # 对应核心数据最严 if score 9.0: return 3 # 重要数据 if score 4.5: return 2 return 1 # 示例单个数据集打分 print(grade(公共利益, 严重, 200_000, 可关联)) # - 3scale_factor故意做成阶梯函数因为记录数增长对风险的贡献是递减的线性放大会让所有大表都冲到最高级分级就失去区分度。阈值 13.5、9.0、4.5 是初始经验值正确用法是拿历史上已定级的几十个数据集回测看分级结果和人工判定的一致率再微调阈值——这一步不做模型就是摆设。4.2 字段级识别正则先跑一遍人工只做复核分级要落到字段上才有意义。身份证、手机号、银行卡号这类强标识字段有固定格式先用正则扫一遍全量元数据。import re PATTERNS { id_card: re.compile(r^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$), mobile: re.compile(r^1[3-9]\d{9}$), bank_card:re.compile(r^\d{16,19}$), email: re.compile(r^[^\s][^\s]\.[A-Za-z]{2,}$), } def scan(sample_values, hit_ratio0.8, sample_size50): 对字段采样 50 条命中率超过阈值才判定为敏感字段 vals [str(v).strip() for v in sample_values if v not in (None, )] if len(vals) sample_size // 2: return None, 0.0 # 样本太少不下结论转人工 hit {k: 0 for k in PATTERNS} for v in vals[:sample_size]: for k, p in PATTERNS.items(): if p.match(v): hit[k] 1 best max(hit, keyhit.get) ratio hit[best] / min(len(vals), sample_size) return (best, round(ratio, 2)) if ratio hit_ratio else (None, round(ratio, 2))hit_ratio设 0.8 而不是 1.0是因为字段里常混有空值和测试数据sample_size取 50 是在准确率和扫描耗时之间折中宽表几百个字段全量扫描会明显拖慢元数据采集。返回的ratio要一起存下来复核时优先看 0.8 到 0.95 这段——它们最可能是误判也最可能是格式不规范的脏数据。4.3 风险评估结果入库与统计口径评估打分要落库才能做趋势对比。最少要记录评估时间、数据集、分值、定级和目标级别方便出缺口清单。CREATE TABLE data_risk ( id BIGSERIAL PRIMARY KEY, dataset VARCHAR(128) NOT NULL, assessed_at DATE NOT NULL, obj VARCHAR(16), -- 影响对象 deg VARCHAR(8), -- 影响程度 score NUMERIC(6,2), grade SMALLINT, -- 模型定级 1~4 target SMALLINT -- 制度要求级别用于比对缺口 ); -- 找出定级低于要求的数据集按差距倒序就是整改优先级 SELECT dataset, score, grade, target, (target - grade) AS gap FROM data_risk WHERE assessed_at (SELECT MAX(assessed_at) FROM data_risk) AND grade target ORDER BY gap DESC, score DESC;target字段是这套表的关键模型算出来的是现状制度或行业指南要求的是应然两者相减才是整改工作量。没有这一列评估报告只能给出某数据集是 3 级说不出还差几级、先改谁。5. 让这份课件真正跑起来版本比对与自检复用课件里保留了立法过程的多个节点和两次审议稿的修改要点这本身是一份现成的版本比对素材。把不同版本的文本抽出来做差异比对能快速定位制度演进的方向也能反过来检查自己的控制项是否漏掉了新增要求。用 Python 的difflib对齐段落即可粒度选到句子而不是整页否则改动小的段落会被判成整段重写import difflib, re def split_sentences(text): # 按中文句号、分号、换行切句保留标点避免句子被切碎 parts re.split(r(?[。\n]), text) return [p.strip() for p in parts if len(p.strip()) 6] def diff_drafts(old_text, new_text, outdraft_diff.md): a, b split_sentences(old_text), split_sentences(new_text) d difflib.unified_diff(a, b, fromfile初稿, tofile通过稿, lineterm, n1) with open(out, w, encodingutf-8) as f: f.write(\n.join(d)) return outn1控制上下文行数只在变更句前后各留一句输出文件更适合人工读len(p.strip()) 6过滤掉第一章这类过短的碎片它们在两个版本里位置不同会造成大量假差异。跑完把结果和控制项台账对一遍新增段落里出现的关键词如果没有对应控制项就是需要补的建设点。把课件转成内部自检问卷也值得做一次。PPT 里的表格结构已经比较规整抽取出来的行可以直接变成是/否/部分满足三态题目再用一个覆盖率统计收口-- 按条款域统计自检覆盖率低于 80% 的域列为下一轮重点 SELECT c.clause_domain, COUNT(DISTINCT ct.control_id) AS controls, COUNT(DISTINCT e.control_id) AS with_evidence, ROUND(100.0 * COUNT(DISTINCT e.control_id) / NULLIF(COUNT(DISTINCT ct.control_id), 0), 1) AS cover_pct FROM dsl_clause c LEFT JOIN dsl_control ct ON ct.clause_id c.clause_id LEFT JOIN dsl_evidence e ON e.control_id ct.control_id GROUP BY c.clause_domain ORDER BY cover_pct NULLS FIRST;NULLS FIRST让完全没有证据的条款域排在最前面——这些是从未被检查过的部分风险往往比已经建了一半的更大。课件备注页里的讲稿也别浪费抽出来按页号切成 Markdown 小标题一次培训的讲稿和大纲就同时有了讲完一轮把学员提问回填到对应的控制项备注里下一轮开课就是现成的答疑材料。本文还有配套的精品资源点击获取
网站建设高端定制企业官网