ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

上海国企名录PDF解析与主数据清洗:pdfplumber+pandas去重

上海国企名录PDF解析与主数据清洗:pdfplumber+pandas去重 简介这份资料是面向研究机构人员、经济史与区域产业研究者、以及需要梳理上海国资背景与产业链脉络的从业者整理的国企名录类文档用以快速定位上海各行业国有企业的归属层级与业务方向解决零散检索、口径不一的问题也可作为行业调研、招商与供应链排查的基础索引。资源共1个PDF文件压缩包约35KB内容按中央管理企业上海部分、国家试点企业集团、上海市市管企业等层级分块编排逐条列出企业全称涉及石油化工、造船、医药、航运、电气、汽车工业、计算机、轻工、纺织、仪电、物资、有色金属、光通信、机械设备、建筑材料、投资、航空、农工商、供销合作、外经、展览、机场、银行、证券、保险等领域兼顾集团、股份公司、有限责任公司、总公司与研究所等多种组织形式。名录中企业多以“上海”冠名便于按行业与归属快速检索可用于观察产业结构与整合脉络。目前已有123人学习下载适合需要一份可直接查阅、可作交叉核对的上海国企清单的读者。1. 一份上海国企名录PDF卡住你的往往不是识别文字拿到《上海国有企业名录整理.pdf》的人多数不是要读它而是要把里面 76 条市管企业、4 条页首未标名单、中央管理上海部分 7 条、国家试点企业集团上海部分 14 条合计 101 条企业名塞进供应商库、客户主数据或一张能做透视的 Excel。真动手才发现OCR 不是瓶颈PDF 里这三类名单是连排的上一家企业的名称、下一个节标题、下一条企业的名称会挤在同一行页码数字会被吸附到上一条名称的尾部比如「上海市锦江航运有限公司111」同一家企业在两张名单里写法还不一样「上海广电集团有限公司」和「上海广电集团公司」指的是同一主体。这份资源适合做数据治理、主数据清洗、产业地图的人拿来练手也适合第一次处理中文名录类 PDF 的人照着走一遍。2. 用 pdfplumber 切出企业条目名单分区与噪声行过滤2.1 为什么不是一句 pdftotext 就完事名录类 PDF 排的是单栏纯文本行序基本可靠问题出在行内混排。按原文顺序看页首 4 条之后紧跟着「中央管理的企业名单上海部分」再紧跟着「1、上海医药工业研究院」中间没有任何换行标记能区分「上一条的结尾」和「节标题」和「下一条的开头」。所以按行解析必然错位必须先用标记正则把文档切成若干分区再在每个分区里按序号切条目。另一个常被忽略的点是页码。提取出来的文本里会出现「……有限公司111 27、锦江国际集团」这样的片段111 是页码27 是下一条的序号而「27、锦江国际集团」本身还是个被截断的名称缺了「有限公司」。页码和被截断的名称要分两步处理不能一把梭全删。2.2 分区先认节标题再认无标题的页首块节标题只有三个固定串直接正则匹配即可。匹配到位置后用相邻标题之间的区间做切分前一个标题结束到下一个标题开始之间的内容就是该分区的正文。import re import pdfplumber SECTION_PAT re.compile(r(中央管理的企业名单|国家试点企业集团公司名单|上海市市管企业名单)) def load_text(path): with pdfplumber.open(path) as pdf: # 名录是单栏排版x_tolerance 保持默认即可调大反而会把相邻字符粘连 return \n.join((page.extract_text() or ) for page in pdf.pages) def split_sections(text): hits [(m.start(), m.group(1)) for m in SECTION_PAT.finditer(text)] sections, order {}, [] # 第一个节标题之前的内容单独成区否则页首那 4 条会被整段丢掉 sections[未标注名单] text[: hits[0][0]] if hits else text order.append(未标注名单) for i, (start, name) in enumerate(hits): end hits[i 1][0] if i 1 len(hits) else len(text) body text[start len(name): end] sections[name] sections.get(name, ) body order.append(name) return sections, order逻辑上只有两件事SECTION_PAT.finditer按出现顺序返回所有节标题位置循环内用「下一个标题的起点」当当前分区的终点。参数上x_tolerance控制字符横向间距的合并阈值默认值对单栏中文名录足够如果你拿到的版本是双栏需要改成按page.crop()分左右两栏分别提取再拼回同一分区。提示节标题匹配用的是短串如果后续版本改成「市管企业名单第一批」这类带后缀的写法正则要跟着放宽否则整块内容会落进上一个分区。2.3 条目切分与页码噪声清理分区切好之后条目边界靠序号识别。原文用的是「1、」「27、」这种全角顿号序号最大两位数正则写成\d{1,3}\s*[、.]就能覆盖同时用前瞻断言只切不删。ITEM_PAT re.compile(r(?\d{1,3}\s*[、.])) def parse_items(body): items [] for chunk in ITEM_PAT.split(body): m re.match(r\s*(\d{1,3})\s*[、.]\s*(.), chunk, re.S) if not m: continue seq, name int(m.group(1)), m.group(2) name re.sub(r\s, , name) # 名称内部不留空格、换行 name re.sub(r\s*\d{1,4}\s*$, , name) # 行尾数字即页码如 …有限公司111 items.append({序号: seq, 名称: name.strip(、。;,)}) return items关键在最后一条清理规则。页码 111 被合并进了上一条名称的末尾所以它一定出现在 chunk 的尾部用$锚定行尾数字最安全如果图省事写成全局re.sub(r\d, , name)遇到名称里带数字的新版本例如「上海某 2050 研究院」就会误伤。切分函数的返回是字典列表序号保留下来做自检名称做去空格和去尾标点。2.4 提取结果的自检跑完之后不要急着往下做归一化先对数分区条目数和序号连续性是两个最灵敏的信号。分区预期条目数需要留意的点未标注名单页首块4无节标题靠「第一个节标题之前」兜住中央管理的企业名单上海部分7与页首块之间有节标题切分点要准国家试点企业集团公司名单上海部分14含「长江经济联合发展集团股份有限公司」等长名上海市市管企业名单76序号连续到 76中间夹着页码 111合计101这是原始条目数不是主体数序号连续性用一行代码就能验assert [i[序号] for i in items] list(range(1, 77))。市管名单这一区最值得跑这个断言因为它是唯一超过 10 条、且被页码打断过的分区一旦切分正则写错先崩的就是这里。3. 企业名称归一化与跨名单去重pandas 主体键方案3.1 名称噪声的六种形态把 101 条名称摆在一起看噪声集中在六类全角括号与半角括号混用名称中间有多余空格或换行残留组织形式后缀不统一公司 / 总公司 / 有限集团公司同一主体在不同名单里一个是「集团公司」一个是「集团总公司」名称被页码或分页截断以及序号、顿号残留。前五类可以用规则吃掉第六类必须靠后缀检查兜住。3.2 全半角统一与括号折叠第一步做字符级归一。中文名录里括号是最容易出分歧的地方和()在不同来源里随手混用偶发还会出现〔〕、【】统一成半角之后再按需转回全角展示。import re import pandas as pd # 依次是 〔 〕 【 】 全角空格 - 半角对应字符 FULL2HALF str.maketrans(〔〕【】 , ()()[] ) def norm_name(s: str) - str: s s.translate(FULL2HALF) s re.sub(r\s, , s) s s.replace((, ).replace(), ) # 统一回全角展示更符合中文习惯 return s.strip(、。;,)str.maketrans做的是逐字符映射比连着写五个replace快也更好维护映射表的两个字符串长度必须相等写的时候数一遍字符数不相等会直接抛ValueError这也是个不错的前置校验。3.3 主体键剥掉组织形式词再做比对如果直接拿归一化后的名称去重「长江计算机集团公司」和「长江计算机集团总公司」会被当成两家。正确做法是额外算一个只用于比对的「主体键」把括号去掉再顺序剥掉组织形式词。# 顺序要紧长词先剥否则「有限公司」会被「公司」截断 ORG_TOKENS [股份有限公司, 有限责任公司, 有限公司, 集团公司, 集团, 总公司, 公司, 研究所, 联合社, 总社] def subject_key(s: str) - str: k norm_name(s).replace(, ).replace(, ) for t in ORG_TOKENS: k k.replace(t, ) return k df[主体键] df[名称].map(subject_key)这里有一个取舍要讲清楚主体键越激进误合并的概率越高。「银行」「证券」「保险」这类词虽然位置在后缀区但带业务含义剥掉后「上海浦东发展银行」会变成「上海浦东发展」和别的名称撞车的风险变大所以我一般把它们留在键里只剥纯组织形式词。同理「研究所」「联合社」「总社」在名录里都是主体身份的组成部分保留或剥掉都可以但不能两套规则混着用。3.4 去重与别名保留按主体键去重的同时一定要把同一主体的多个原始写法收集起来否则后续检索时用户输入「宝钢集团」会查不到只存了主名称的记录。alias df.groupby(主体键)[名称].apply(lambda x: | .join(sorted(set(x)))).rename(别名) merged df.sort_values([主体键, 名单分区]).drop_duplicates(主体键, keepfirst).join(alias, on主体键)sort_values的作用是决定保留哪一条作为主记录。实践中喜欢按名单层级排把更高层级的写法留作主名称drop_duplicates(keepfirst)顺着排序取第一条即可。按这份名录实际跑一遍能对上的重复大体是这些主体键出现过的写法出现位置上海宝钢上海宝钢集团公司中央管理 国家试点中国海运中国海运集团总公司中央管理 国家试点上海电气上海电气集团总公司国家试点 市管上海汽车工业上海汽车工业集团总公司国家试点 市管长江计算机长江计算机集团公司 / 长江计算机集团总公司国家试点 市管上海广电上海广电集团有限公司国家试点 市管上海建工上海建工集团总公司国家试点 市管东方国际东方国际集团有限公司国家试点 市管上海市农工商上海市农工商集团总公司国家试点 市管长江经济联合发展长江经济联合发展集团股份有限公司国家试点 市管101 条原始条目减掉这 10 组对内重复落到主体维度大约 91 条。这个数字不要写死进代码用print(len(df) - len(merged))打出来核对更稳。4. 行业标签与组织形式字段把名录变成可筛选的结构化表4.1 关键词表驱动的行业标签名录里没有行业字段只能从名称里推。最实用的做法不是训模型而是一张有序的关键词规则表前匹配先命中命中即停。INDUSTRY_RULES [ (船舶与海洋工程, [造船, 船舶, 沪东中华]), (石油化工, [石油, 石化, 化工, 化学工业区, 华谊]), (医药, [医药, 药业]), (航运港口物流, [海运, 航道, 航运, 港口, 港务, 洋山, 交运]), (航空机场, [航空, 机场]), (电气装备, [电气, 仪电, 电器科学, 机械设备成套]), (汽车, [汽车]), (计算机与信息, [计算机, 信息, 光通信, 贝尔, 通信]), (建筑与建材, [建工, 建筑, 建材, 建设, 设计, 城建]), (开发区与园区运营, [开发区, 保税区, 工业区, 临港, 张江, 金桥, 陆家嘴, 漕河泾]), (金融, [银行, 证券, 保险, 信用合作社]), (投资与资产管理, [投资, 资产经营, 国资, 久事]), (商贸流通, [物资, 供销, 贸易, 兰生, 百联, 华联, 烟酒, 蔬菜, 水产, 外经, 展览]), (轻工纺织, [纺织, 轻工]), (冶金与材料, [有色金属]), ] def tag_industry(name: str) - str: for label, keys in INDUSTRY_RULES: if any(k in name for k in keys): return label return 未分类每一条规则的本质是「名称里出现某个业务词就归到某个行业」any()保证同一行业下的多个关键词是或的关系for ... return保证先命中的规则优先。4.2 规则的顺序就是优先级上面这张表的顺序不能随手调。举几个真实冲突 「上海市化学工业区发展有限公司」既含「化学工业区」也含「工业区」如果园区规则排在化工前面它会被打成园区运营而按主体身份它更应该归化工「上海电器科学研究所集团有限公司」含「电器」但不含「电气」需要单独在电气装备规则里补「电器科学」这个关键词「上海市机械设备成套集团有限公司」严格说是贸易加装备我一般按装备归类但在备注里留一条说明。判断顺序的通用原则是越具体的业务词放前面越泛的放后面。园区、贸易、投资这三类天然最泛永远垫底。跑完可以用分布表看看有没有明显异常行业标签规则命中示例命中特点石油化工中国石化上海石油化工股份有限公司靠「石油」「石化」双命中船舶与海洋工程沪东中华造船集团有限公司靠「造船」命中开发区与园区运营上海外高桥保税区开发股份有限公司靠「保税区」命中投资与资产管理上海国有资产经营有限公司靠「资产经营」命中规则要够长未分类上海国际集团有限公司、上海上实集团有限公司名称里完全无业务词4.3 组织形式字段的独立抽取行业和主体键之外组织形式是第三个高频筛选维度。它不能复用行业规则因为「公司」这个词几乎人人都有必须按从长到短匹配。ORG_FORM_RULES [ (股份有限公司, [股份有限公司]), (有限责任公司, [有限责任公司]), (有限公司, [有限公司]), (集团公司, [集团公司, 集团有限公司, 集团公司, 集团]), (总公司, [总公司]), (研究所, [研究所]), (银行, [银行]), (证券, [证券]), (保险, [保险]), ] def tag_org_form(name: str) - str: for label, keys in ORG_FORM_RULES: if any(k in name for k in keys): return label return 其他顺序同样是关键「股份有限公司」必须排在「有限公司」前面否则「申银万国证券股份有限公司」会被打成「有限公司」「集团有限公司」必须排在「有限公司」前面否则「上海医药集团有限公司」会丢掉集团属性。这个函数返回的是描述性字段不参与去重所以允许「有限公司 集团公司」这类信息在行业字段里共存。4.4 兜底白名单与产出落地规则跑完必然有漏网的像「上海国际集团有限公司」「上海上实集团有限公司」「上海久事公司」「上海盛勤集团有限公司」这些名称里没有可用的业务词。这类不要硬凑规则直接维护一张人工白名单优先级高于关键词规则。MANUAL_TAGS { 上海国际集团有限公司: 投资与资产管理, 上海上实集团有限公司: 投资与资产管理, 上海久事公司: 投资与资产管理, } df[行业] df.apply( lambda r: MANUAL_TAGS.get(r[名称], tag_industry(r[名称])), axis1 )apply(axis1)逐行走先查白名单再落规则白名单字典的键必须是归一化之后的名称否则会因为括号半全角不一致匹配不上。产出阶段建议同时落两份一份 UTF-8 带 BOM 的 CSV 给 Excel 用户一份 Parquet 给后续入库编码这一步不做Excel 打开中文会乱码这个坑每年都要踩一次。5. 建 SQLite 库做校验幂等写入与截断名抽查名录清洗完的最后一公里是入库和验收。用 SQLite 做个单文件库最省事关键是写入要幂等否则每跑一次多一批重复。import sqlite3 conn sqlite3.connect(sh_soe.db) conn.execute( CREATE TABLE IF NOT EXISTS enterprise( id INTEGER PRIMARY KEY, name TEXT NOT NULL, subject_key TEXT NOT NULL UNIQUE, -- UNIQUE 顺带建了索引也是幂等的保证 industry TEXT, org_form TEXT, list_scope TEXT, aliases TEXT )) merged.to_sql(tmp_import, conn, if_existsreplace, indexFalse) conn.execute( INSERT OR IGNORE INTO enterprise(name, subject_key, industry, org_form, list_scope, aliases) SELECT 名称, 主体键, 行业, 组织形式, 名单分区, 别名 FROM tmp_import ) conn.execute(CREATE INDEX IF NOT EXISTS idx_industry ON enterprise(industry)) conn.commit()subject_key上的 UNIQUE 约束是整套幂等方案的核心配合INSERT OR IGNORE同一主体重复导入时会被静默跳过而不是报错中断。落库前先写临时表再SELECT过去是为了让列名映射这一层保持在 SQL 里Python 侧不用做字段改名。入库之后跑三条校验查询。第一条看分布SELECT industry, COUNT(*) FROM enterprise GROUP BY industry ORDER BY 2 DESC;如果「未分类」占比超过两成就说明规则表该补词了。第二条对分区做交叉核对注意它统计的是主体表跨名单重复已被合并所以合计不再是 101拿 101 去断言会误报。第三条专门抓截断名称把不以下列后缀结尾的记录挑出来TAIL (公司, 集团, 银行, 研究所, 总社, 联合社, 中心) suspect merged[~merged[名称].str.endswith(TAIL)]「锦江国际集团」这类被分页切掉的名称会在这里露头因为它的结尾是右括号而不是「集团」。再把它的别名列打出来对着原始 PDF 回填一次即可。把这条后缀检查固定成每次导入后的第一道校验比事后翻 PDF 一条条比对省事得多。本文还有配套的精品资源点击获取
返回列表