構(gòu)化抽取實(shí)戰(zhàn):從文本解析到字段校驗(yàn)的完整流程)
簡(jiǎn)介這份PDF是科沃斯機(jī)器人股份有限公司首次公開(kāi)發(fā)行股票A股的招股說(shuō)明書面向關(guān)注智能家電行業(yè)、擬研究新股發(fā)行案例的投資者、投行從業(yè)者及財(cái)務(wù)研究人員。資源包共1個(gè)文件為PDF格式大小約17.85MB完整收錄了發(fā)行概況、股份鎖定承諾、股東承諾等核心章節(jié)便于按條款檢索與引用。發(fā)行概況部分明確發(fā)行股數(shù)4,010萬(wàn)股、每股面值1.00元、發(fā)行價(jià)20.02元、擬登陸上海證券交易所、發(fā)行后總股本40,010萬(wàn)股等關(guān)鍵數(shù)據(jù)股份鎖定承諾部分則梳理了控股股東創(chuàng)領(lǐng)投資、Ever Group、創(chuàng)袖投資、Sky Sure及實(shí)際控制人錢東奇、David Cheng Qian等主體36個(gè)月鎖定、其他股東12個(gè)月鎖定以及股價(jià)破發(fā)自動(dòng)延長(zhǎng)6個(gè)月等差異化安排。目前已有1171人學(xué)習(xí)下載適合作為IPO案例復(fù)盤、招股書條款比對(duì)與合規(guī)研究的參考材料。1. 從一份 2018 年的招股書 PDF 說(shuō)起為什么值得逐頁(yè)拆很多人拿到「科沃斯機(jī)器人股份有限公司首次公開(kāi)發(fā)行股票招股說(shuō)明書.PDF」這類文件第一反應(yīng)是當(dāng)資料存著真要用的時(shí)候才發(fā)現(xiàn)幾百頁(yè)翻不動(dòng)。這份 2018 年 5 月簽署的招股書其實(shí)是個(gè)很典型的樣本發(fā)行概況、股份鎖定承諾、穩(wěn)定股價(jià)預(yù)案、募集資金投向四塊內(nèi)容幾乎覆蓋了 A 股 IPO 招股書里最常被反復(fù)引用的部分。做行業(yè)咨詢、寫競(jìng)品分析、做投研底稿的人真正需要的不是通讀而是把結(jié)構(gòu)化字段抽出來(lái)變成能檢索、能比對(duì)、能復(fù)用的數(shù)據(jù)。這篇就按「先看懂結(jié)構(gòu)、再動(dòng)手抽取、最后落到校驗(yàn)」的順序把這份 PDF 拆成一套可復(fù)現(xiàn)的處理流程涉及的工具和參數(shù)都給出具體寫法。2. 招股書 PDF 的文本結(jié)構(gòu)與字段抽取思路2.1 為什么不能直接全文正則匹配招股書 PDF 的排版有幾個(gè)固定特征頁(yè)眉帶公司全稱和「招股說(shuō)明書」字樣頁(yè)碼形如「1-1-2」正文里大量使用全角括號(hào)和中文頓號(hào)承諾條款經(jīng)常跨頁(yè)斷開(kāi)。直接對(duì)整份文本跑正則最容易踩的坑是頁(yè)眉頁(yè)腳混進(jìn)正文導(dǎo)致「發(fā)行股數(shù)」這類字段被匹配到目錄頁(yè)而不是正文頁(yè)。常見(jiàn)做法是先把 PDF 按頁(yè)轉(zhuǎn)成文本再按頁(yè)做頁(yè)眉頁(yè)腳清洗最后才做字段抽取。2.2 用 pdfplumber 做分頁(yè)文本提取import pdfplumber import re def extract_pages(pdf_path): pages [] with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): # layoutTrue 保留原始空格便于后續(xù)按列切分 text page.extract_text(layoutTrue) or pages.append({page_no: i 1, text: text}) return pages def clean_header_footer(text): # 去掉頁(yè)眉公司全稱 招股說(shuō)明書 text re.sub(r科沃斯機(jī)器人股份有限公司\s首次公開(kāi)發(fā)行股票招股說(shuō)明書, , text) # 去掉形如 1-1-12 的頁(yè)碼 text re.sub(r^\s*1-\d-\d\s*$, , text, flagsre.MULTILINE) return text.strip() pages extract_pages(科沃斯招股說(shuō)明書.pdf) cleaned [{page_no: p[page_no], text: clean_header_footer(p[text])} for p in pages]extract_text(layoutTrue)會(huì)盡量保留原始排版空格這對(duì)后面按「字段名 值」切分很關(guān)鍵如果不開(kāi)這個(gè)參數(shù)冒號(hào)前后的空格會(huì)被壓掉正則里就得寫得更寬松。clean_header_footer里兩條正則分別處理頁(yè)眉和頁(yè)碼頁(yè)碼那條用了MULTILINE因?yàn)轫?yè)碼通常單獨(dú)占一行。清洗完的cleaned列表保留了頁(yè)碼后面定位字段來(lái)源頁(yè)時(shí)直接可用。2.3 發(fā)行概況字段的抽取規(guī)則發(fā)行概況部分字段名和值之間用全角冒號(hào)分隔且字段名固定??梢越ㄒ粡堊侄斡成浔碇饤l匹配。字段名正則模式示例值發(fā)行股票類型發(fā)行股票類型[:]\s*(.)人民幣普通股A 股發(fā)行股數(shù)發(fā)行股數(shù)[:]\s*([\d,])\s*萬(wàn)股4,010每股面值每股面值[:]\s*([\d.])\s*元1.00每股發(fā)行價(jià)格每股發(fā)行價(jià)格[:]\s*([\d.])\s*元20.02預(yù)計(jì)發(fā)行日期預(yù)計(jì)發(fā)行日期[:]\s*(\d{4}\s*年\s*\d\s*月\s*\d\s*日)2018 年 5 月 16 日擬上市交易所擬上市的證券交易所[:]\s*(.)上海證券交易所發(fā)行后總股本發(fā)行后總股本[:]\s*([\d,])\s*萬(wàn)股40,010FIELD_PATTERNS { 發(fā)行股數(shù): r發(fā)行股數(shù)[:]\s*([\d,])\s*萬(wàn)股, 每股發(fā)行價(jià)格: r每股發(fā)行價(jià)格[:]\s*([\d.])\s*元, 發(fā)行后總股本: r發(fā)行后總股本[:]\s*([\d,])\s*萬(wàn)股, } def parse_issue_profile(pages): result {} for p in pages: for field, pattern in FIELD_PATTERNS.items(): if field in result: continue m re.search(pattern, p[text]) if m: result[field] {value: m.group(1), page: p[page_no]} return result profile parse_issue_profile(cleaned)這里用if field in result: continue保證每個(gè)字段只取第一次命中避免目錄頁(yè)和正文頁(yè)重復(fù)匹配時(shí)被覆蓋。返回結(jié)構(gòu)里帶了page方便回溯核對(duì)。數(shù)值字段統(tǒng)一去掉千分位逗號(hào)后再轉(zhuǎn)int或float不要直接拿字符串去算。注意招股書里「發(fā)行股數(shù)」和「發(fā)行后總股本」單位都是萬(wàn)股做比例計(jì)算時(shí)先統(tǒng)一量綱否則 4010 和 40010 直接相除會(huì)得到錯(cuò)誤結(jié)論。3. 股份鎖定承諾的條款解析與結(jié)構(gòu)化3.1 承諾主體的分類邏輯這份招股書里的鎖定承諾按主體分成幾類鎖定期和附加條件各不相同??毓晒蓶|創(chuàng)領(lǐng)投資、股東 Ever Group、創(chuàng)袖投資、Sky Sure 以及實(shí)際控制人錢東奇、David Cheng Qian、親屬錢嵐鎖定期都是 36 個(gè)月其他股東是 12 個(gè)月持股的董監(jiān)高是 12 個(gè)月。36 個(gè)月那批還帶一條「上市后 6 個(gè)月內(nèi)連續(xù) 20 個(gè)交易日收盤價(jià)低于發(fā)行價(jià)鎖定期自動(dòng)延長(zhǎng) 6 個(gè)月」的條款。做結(jié)構(gòu)化時(shí)主體、鎖定期、是否含延長(zhǎng)條款、減持價(jià)格約束這四個(gè)維度要分開(kāi)存。3.2 用規(guī)則切分承諾段落承諾條款在原文里以「承諾」引出后面跟帶編號(hào)的條款??梢园础赋兄Z人 承諾內(nèi)容」切塊再在塊內(nèi)抽鎖定期。def split_commitments(text): # 以「承諾」為切分點(diǎn)保留前一段作為主體描述 blocks re.split(r(?承諾[:]), text) results [] for b in blocks: if 個(gè)月 not in b: continue lock re.findall(r(\d)\s*個(gè)月, b) results.append({ raw: b[:200], lock_months: sorted(set(int(x) for x in lock)), has_extension: 自動(dòng)延長(zhǎng) in b, has_price_constraint: 不低于 in b and 發(fā)行價(jià)格 in b, }) return resultsre.split用零寬斷言(?承諾[:])切分點(diǎn)保留在下一段開(kāi)頭這樣每塊都從「承諾」開(kāi)始主體描述落在上一塊尾部實(shí)際使用時(shí)可以把上一塊最后一行拼回來(lái)。lock_months去重排序后能直接看出這段承諾涉及 12 還是 36 個(gè)月。has_extension和has_price_constraint兩個(gè)布爾字段是后面做承諾對(duì)比時(shí)最常用的篩選條件。3.3 承諾條款的比對(duì)表把抽出來(lái)的結(jié)果整理成表比逐頁(yè)翻原文快得多。承諾主體鎖定期月自動(dòng)延長(zhǎng)條款減持價(jià)格約束控股股東創(chuàng)領(lǐng)投資36有有Ever Group / 創(chuàng)袖投資 / Sky Sure36有有實(shí)際控制人錢東奇、David Cheng Qian36有有實(shí)際控制人親屬錢嵐36有有其他股東12無(wú)無(wú)董監(jiān)高莊建華等12有有監(jiān)事王煒等12無(wú)無(wú)這張表里最容易被忽略的是董監(jiān)高那行鎖定期只有 12 個(gè)月但同樣帶自動(dòng)延長(zhǎng)條款和控股股東的 36 個(gè)月不是一回事。做行業(yè)咨詢時(shí)如果只按「36 個(gè)月 / 12 個(gè)月」兩檔歸類會(huì)把董監(jiān)高和普通股東混在一起結(jié)論就偏了。提示承諾條款里「每年轉(zhuǎn)讓不超過(guò) 25%」和「離職后半年內(nèi)不轉(zhuǎn)讓」是任職相關(guān)的附加約束和鎖定期是并列關(guān)系抽取時(shí)建議單獨(dú)建字段不要塞進(jìn)鎖定期里。4. 穩(wěn)定股價(jià)預(yù)案的觸發(fā)條件與參數(shù)化4.1 觸發(fā)與停止條件的數(shù)值邊界穩(wěn)定股價(jià)預(yù)案的核心是兩組數(shù)值啟動(dòng)條件是連續(xù) 20 個(gè)交易日收盤價(jià)低于上一年度末經(jīng)審計(jì)每股凈資產(chǎn)停止條件是連續(xù) 20 個(gè)交易日收盤價(jià)高于同一基準(zhǔn)。中間還夾著時(shí)間窗口10 日內(nèi)開(kāi)董事會(huì)、25 日內(nèi)開(kāi)股東大會(huì)、方案通過(guò)后 10 個(gè)交易日內(nèi)啟動(dòng)。這些數(shù)字是預(yù)案可執(zhí)行性的關(guān)鍵做參數(shù)化時(shí)全部要抽成獨(dú)立字段。4.2 把預(yù)案條件寫成可校驗(yàn)的配置STABILIZE_RULES { trigger: { consecutive_days: 20, compare: below, benchmark: 上一年度末經(jīng)審計(jì)每股凈資產(chǎn), }, stop: { consecutive_days: 20, compare: above, benchmark: 上一年度末經(jīng)審計(jì)每股凈資產(chǎn), }, timeline: { board_meeting_days: 10, shareholder_meeting_days: 25, launch_after_approval_days: 10, }, caps: { company_buyback: 不高于上一年度歸母凈利潤(rùn)的 30%, controlling_shareholder: 不高于上市后累計(jì)現(xiàn)金分紅的 30%, directors_and_senior: 不高于上年度初至董事會(huì)日稅后薪酬及現(xiàn)金分紅總額的 30%, }, } def check_trigger(closing_prices, net_asset_per_share): # closing_prices: 最近 N 個(gè)交易日收盤價(jià)列表 window closing_prices[-STABILIZE_RULES[trigger][consecutive_days]:] return all(p net_asset_per_share for p in window)check_trigger只做最樸素的判斷取最近 20 個(gè)交易日收盤價(jià)全部低于每股凈資產(chǎn)才返回True。實(shí)際用的時(shí)候要注意兩點(diǎn)一是每股凈資產(chǎn)用的是「上一年度末經(jīng)審計(jì)」的值不是最新一期二是如果期間發(fā)生派息、送股、資本公積轉(zhuǎn)增價(jià)格要按除權(quán)除息調(diào)整后再比。caps里三個(gè) 30% 的基數(shù)各不相同公司是凈利潤(rùn)控股股東是累計(jì)現(xiàn)金分紅董監(jiān)高是薪酬加分紅混用會(huì)算錯(cuò)。4.3 回購(gòu)與增持的約束差異公司回購(gòu)用的是「集中競(jìng)價(jià)、要約或監(jiān)管認(rèn)可的其他方式」控股股東和董監(jiān)高增持限定在「交易所集中競(jìng)價(jià)交易方式」。這個(gè)差異在原文里寫得很明確做合規(guī)分析時(shí)不能一概而論。另外控股股東和董監(jiān)高在方案實(shí)施期間不得轉(zhuǎn)讓持股除非是繼承、被強(qiáng)制執(zhí)行或重組等情形這條排除項(xiàng)也要單獨(dú)記。主體方式金額上限基數(shù)實(shí)施期間轉(zhuǎn)讓限制公司集中競(jìng)價(jià)、要約等上一年度歸母凈利潤(rùn) 30%不適用控股股東集中競(jìng)價(jià)上市后累計(jì)現(xiàn)金分紅 30%不得轉(zhuǎn)讓董監(jiān)高集中競(jìng)價(jià)稅后薪酬及分紅 30%不得轉(zhuǎn)讓5. 抽取結(jié)果的校驗(yàn)與批量處理技巧5.1 用交叉字段做一致性校驗(yàn)抽完字段后最實(shí)用的校驗(yàn)是拿發(fā)行前總股本、發(fā)行股數(shù)、發(fā)行后總股本三個(gè)數(shù)對(duì)一下36000 4010 40010對(duì)得上說(shuō)明抽取沒(méi)錯(cuò)。這類恒等式校驗(yàn)比逐字段肉眼核對(duì)高效得多。def validate_share_counts(pre_total, issue_count, post_total): pre int(pre_total.replace(,, )) issue int(issue_count.replace(,, )) post int(post_total.replace(,, )) assert pre issue post, f股本校驗(yàn)失敗: {pre} {issue} ! {post} return True validate_share_counts(36,000, 4,010, 40,010)validate_share_counts先把千分位逗號(hào)去掉再轉(zhuǎn)整數(shù)斷言不成立直接拋異常。這種校驗(yàn)適合放進(jìn)批處理流程處理多份招股書時(shí)一旦某份數(shù)據(jù)對(duì)不上能立刻定位到是哪份文件的問(wèn)題。5.2 批量處理多份招股書的目錄約定如果手上不止這一份 PDF建議按「公司簡(jiǎn)稱_年份」建目錄抽取結(jié)果統(tǒng)一落成 JSON字段名保持一致。這樣后面做橫向?qū)Ρ葧r(shí)直接讀 JSON 拼表就行不用再碰 PDF。# 目錄結(jié)構(gòu)示例 # data/ # ecovacs_2018/ # source.pdf # pages.json # profile.json # commitments.json python extract.py --input data/ecovacs_2018/source.pdf --outdir data/ecovacs_2018--input指向原始 PDF--outdir指定輸出目錄腳本內(nèi)部按pages.json、profile.json、commitments.json三個(gè)文件分別落盤。這樣即使后面換了解析庫(kù)只要輸出結(jié)構(gòu)不變下游的比對(duì)腳本不用改。5.3 一個(gè)容易忽略的細(xì)節(jié)簽署日期與發(fā)行日期招股說(shuō)明書簽署日期是 2018 年 5 月 15 日預(yù)計(jì)發(fā)行日期是 2018 年 5 月 16 日兩者只差一天。做時(shí)間線分析時(shí)這兩個(gè)字段經(jīng)常被當(dāng)成同一個(gè)實(shí)際含義不同簽署日期是文件定稿日發(fā)行日期是預(yù)計(jì)的發(fā)行日。抽取時(shí)字段名要寫清楚別都叫「日期」。注意招股書里「預(yù)計(jì)發(fā)行日期」帶「預(yù)計(jì)」二字說(shuō)明是計(jì)劃值后續(xù)實(shí)際發(fā)行日期可能調(diào)整做數(shù)據(jù)比對(duì)時(shí)不要把它當(dāng)確定值用。最后一章說(shuō)一個(gè)具體技巧把上面抽出來(lái)的字段拼成一張寬表后用pandas做多份招股書的橫向?qū)Ρ戎攸c(diǎn)看三個(gè)指標(biāo)——發(fā)行市盈率發(fā)行價(jià)除以每股收益、鎖定期結(jié)構(gòu)、穩(wěn)定股價(jià)預(yù)案的觸發(fā)閾值。這三個(gè)指標(biāo)在不同年份、不同行業(yè)的招股書里差異最明顯也最能反映當(dāng)時(shí)的發(fā)行環(huán)境。拼表時(shí)把page字段一起帶上任何異常值都能一鍵回溯到原文頁(yè)碼省掉重新翻 PDF 的時(shí)間。本文還有配套的精品資源點(diǎn)擊獲取