https://github.com/magicdict/FDDChtml
更新時間 2018年7月16日 By 帶着兔子去旅行python
開發這個工具的起源是天池大數據競賽,FDDC2018金融算法挑戰賽02-A股上市公司公告信息抽取。這個比賽是針對金融公告開展的信息抽取比賽。在參勝過程中,萌生出一個念頭,是否可以開發出一個泛用的信息抽取工具呢?git
信息抽取是NLP裏的一個實用內容。該工具的目標是打造一個泛用的自動信息抽取工具。使得沒有任何基礎的用戶,能夠經過簡單的步驟提取文檔(PDF,HTML,TXT)中的信息。該工具使用C#(.Net Core)開發,因此能夠跨平臺運行。(Python在作大的工程的時候有諸多不便,因此沒有使用python語言)github
工具原理採用的是開放域實體抽取的方法:
使用各類方法儘量抽取實體,而後對於候選內容進行置信度分析打分。正則表達式
ltp工具:哈工大LTP工具(ltp.ai)提供的ltp工具,最新版爲3.3.4.該工具在windows,max,centos上,srl的訓練可能沒法正常完成。(dp,ner階段沒有問題)因此這裏使用了3.3.2版本。ltp工具的SRL結果中包含了DP和NER的內容,可是暫時保留DP和NER中間XML文件。算法
pdfminer:請注意處理中文的時候須要額外的步驟,具體方法再也不贅述。部分PDF可能沒法正確轉換,緣由CaseByCase。windows
結巴分詞:某些地名,例如"大連",會被誤判。這裏使用地名輔助字典的方式作糾正。ltp工具沒有這個問題。ltp工具和結巴分詞功能雖然重複,可是暫時還不能移除結巴分詞。centos
期待文件夾結構微信
訓練結果例:ide
協議書(5.180388%)[56] 協議(11.84089%)[128] 合同(58.55689%)[633] 合同書(2.960222%)[32] 買賣合同(3.792784%)[41] 承包合同(12.0259%)[130] 意向書(0.2775208%)[3] 補充協議(1.110083%)[12] 項目(0.2775208%)[3] 書(0.9250694%)[10] 議案(0.2775208%)[3] )(0.8325624%)[9]
(更多規則持續加入中,同時對於相關度低的規則也會剔除)
這裏暫時使用頻率最高的前5位做爲抽取依據。同時爲了保證正確率,部分特徵的佔比必須超過某個閾值。
如下是中文冒號的一個例子,要求前導詞佔比在40%以上。
(例如前導詞A能夠正確抽取10個關鍵字,前導詞B能夠抽取5個關鍵字,前導詞C能夠抽取15個關鍵字。則前導詞A的佔比爲33%)
e.LeadingColonKeyWordList = ContractTraning.ContractNameLeadingDict .Where((x) => { return x.Value >= 40; }) //閾值40%以上 .Select((x) => { return x.Key + ":"; }).ToArray();
對於大量表格中的關鍵字,工具也提供了表格統計的功能。主要是統計一下該關鍵字的表頭標題信息。
同時因爲表格中的原始數據可能須要經過參照表格標題才能進行比對的狀況,這裏支持變換器。
/// <summary> /// 增發對象訓練 /// </summary> public static void TrainingIncreaseTarget() { var TargetTool = new TableAnlayzeTool(); var IncreaseNumberTool = new TableAnlayzeTool(); IncreaseNumberTool.Transform = NumberUtility.NormalizerStockNumber; var IncreaseMoneyTool = new TableAnlayzeTool(); IncreaseMoneyTool.Transform = MoneyUtility.Format; TraningDataset.InitIncreaseStock(); var PreviewId = String.Empty; var PreviewRoot = new HTMLEngine.MyRootHtmlNode(); foreach (var increase in TraningDataset.IncreaseStockList) { if (!PreviewId.Equals(increase.id)) { var htmlfile = Program.DocBase + @"\FDDC_announcements_round1_train_20180518\定增\html\" + increase.id + ".html"; PreviewRoot = new HTMLEngine().Anlayze(htmlfile, ""); PreviewId = increase.id; } TargetTool.PutTrainingItem(PreviewRoot, increase.PublishTarget); IncreaseNumberTool.PutTrainingItem(PreviewRoot, increase.IncreaseNumber); IncreaseMoneyTool.PutTrainingItem(PreviewRoot, increase.IncreaseMoney); } TargetTool.WriteTop(10); } 增發對象 17% 00237 發行對象 16% 00223 發行對象名稱 11% 00156 股東名稱 09% 00132 認購對象 07% 00096 投資者名稱 06% 00085 名稱 04% 00061 認購對象名稱 04% 00055 獲配投資者名稱 02% 00035 詢價對象名稱 02% 00029 配售對象名稱 增發數量 30% 00370 獲配股數(股) 19% 00234 配售股數(股) 13% 00158 認購股數(股) 10% 00126 持股數量(股) 03% 00045 認購數量(股) 02% 00028 持股總數(股) 02% 00024 配售數量(股) 01% 00019 持股數(股) 01% 00015 獲配數量(股) 00% 00011 總股本比例 00% 00011 獲配股數(萬股) 00% 00011 認購股數(萬股) 增發金額 35% 00257 獲配金額(元) 21% 00155 認購金額(元) 17% 00125 配售金額(元) 08% 00062 配售金額(元) 02% 00018 認購金額(萬元) 02% 00017 認購金額(人民幣元) 01% 00014 發行前 01% 00014 申購金額(萬元) 01% 00011 獲配金額(元) 01% 00008 追加認購金額(元)
除了統計標題以外,還能夠經過某個標題下面出現的內容。
下面的例子是看一下增減持方式有哪些:
/// <summary> /// 增減持訓練 /// </summary> /// <param name="TraningCnt">訓練條數</param> public static void Traning(int TraningCnt = int.MaxValue) { var ChangeMethodTool = new TableAnlayzeTool(); var PreviewId = String.Empty; var PreviewRoot = new HTMLEngine.MyRootHtmlNode(); int Cnt = 0; foreach (var stockchange in TraningDataset.StockChangeList) { if (!PreviewId.Equals(stockchange.id)) { var htmlfile = Program.DocBase + @"\FDDC_announcements_round1_train_20180518\增減持\html\" + stockchange.id + ".html"; PreviewRoot = new HTMLEngine().Anlayze(htmlfile, ""); PreviewId = stockchange.id; Cnt++; if (Cnt == TraningCnt) break; } ChangeMethodTool.PutValueTrainingItem(PreviewRoot, new string[]{"減持方式","增持方式"}.ToList()); } Program.Training.WriteLine("增減持方式"); ChangeMethodTool.WriteTop(10); } 增減持方式 33% 09277 集中競價交易 24% 06771 集中競價 21% 05940 大宗交易 08% 02468 競價交易 01% 00464 集中競價減持 01% 00365 減持方式 01% 00303 <null> 01% 00289 二級市場競價 00% 00258 合計 00% 00196 競價減持
採用各類方法抽取數據,務必使得全部數據都抽取出來。根據訓練結果從候選值裏面得到置信度最大的數據。抽取手段以下:
代碼內置表頭規則系的表抽取工具,對於表格能夠設定以下抽取規則:
/// <summary> /// 表抽取規則(內容系) /// </summary> public struct TableSearchContentRule { /// <summary> /// 匹配內容 /// </summary> public List<String> Content; /// <summary> /// 是否相等模式 /// </summary> public bool IsContentEq; }
下面是一個表格抽取的例子:
var rule = new TableSearchContentRule(); rule.Content = new string[] { "集中競價交易", "競價交易", "大宗交易", "約定式購回" }.ToList(); rule.IsContentEq = true; var result = HTMLTable.GetMultiRowsByContentRule(root,rule);
代碼內置表頭規則系的表抽取工具,對於表格能夠設定以下抽取規則:
/// <summary> /// 表抽取規則 /// </summary> public struct TableSearchTitleRule { public string Name; /// <summary> /// 父標題 /// </summary> public List<String> SuperTitle; /// <summary> /// 是否必須一致 /// </summary> public bool IsSuperTitleEq; /// <summary> /// 標題 /// </summary> public List<String> Title; /// <summary> /// 是否必須一致 /// </summary> public bool IsTitleEq; /// <summary> /// 是否必須 /// </summary> public bool IsRequire; /// <summary> /// 表標題不能包含的文字 /// </summary> public List<String> ExcludeTitle; /// <summary> /// 抽取內容預處理器 /// </summary> public Func<String, String, String> Normalize; }
下面是一個表格抽取的例子:
增持前 | (合併表頭) | 增持後 | (合併表頭) |
---|
持股數 | 持股比例 | 持股數 | 持股比例 |
---|
這裏咱們想抽取持股比例和持股數,可是但願抽取的是增持後的部分,因此須要使用SuperTitle的規則了。
var HoldList = new List<struHoldAfter>(); var StockHolderRule = new TableSearchRule(); StockHolderRule.Name = "股東全稱"; StockHolderRule.Title = new string[] { "股東名稱", "名稱", "增持主體", "增持人", "減持主體", "減持人" }.ToList(); StockHolderRule.IsTitleEq = true; StockHolderRule.IsRequire = true; var HoldNumberAfterChangeRule = new TableSearchRule(); HoldNumberAfterChangeRule.Name = "變更後持股數"; HoldNumberAfterChangeRule.IsRequire = true; HoldNumberAfterChangeRule.SuperTitle = new string[] { "減持後", "增持後" }.ToList(); HoldNumberAfterChangeRule.IsSuperTitleEq = false; HoldNumberAfterChangeRule.Title = new string[] { "持股股數","持股股數", "持股數量","持股數量", "持股總數","持股總數","股數" }.ToList(); HoldNumberAfterChangeRule.IsTitleEq = false; var HoldPercentAfterChangeRule = new TableSearchRule(); HoldPercentAfterChangeRule.Name = "變更後持股數比例"; HoldPercentAfterChangeRule.IsRequire = true; HoldPercentAfterChangeRule.SuperTitle = HoldNumberAfterChangeRule.SuperTitle; HoldPercentAfterChangeRule.IsSuperTitleEq = false; HoldPercentAfterChangeRule.Title = new string[] { "比例" }.ToList(); HoldPercentAfterChangeRule.IsTitleEq = false; var Rules = new List<TableSearchRule>(); Rules.Add(StockHolderRule); Rules.Add(HoldNumberAfterChangeRule); Rules.Add(HoldPercentAfterChangeRule); var result = HTMLTable.GetMultiInfoByTitleRules(root, Rules, false);
EntityProperty對象屬性以下:
/// <summary> /// 得到合同名 /// </summary> /// <returns></returns> string GetContractName() { var e = new EntityProperty(); e.PropertyName = "合同名稱"; e.PropertyType = EntityProperty.enmType.Normal; e.MaxLength = ContractTraning.MaxContractNameLength; e.MinLength = 5; e.LeadingColonKeyWordList = new string[] { "合同名稱:" }; e.QuotationTrailingWordList = new string[] { "協議書", "合同書", "確認書", "合同", "協議" }; e.QuotationTrailingWordList_IsSkipBracket = true; //暫時只能選True var KeyList = new List<ExtractPropertyByDP.DPKeyWord>(); KeyList.Add(new ExtractPropertyByDP.DPKeyWord() { StartWord = new string[] { "簽署", "簽定" }, //經過SRL訓練得到 StartDPValue = new string[] { LTPTrainingDP.核心關係, LTPTrainingDP.定中關係, LTPTrainingDP.並列關係 }, EndWord = new string[] { "補充協議", "合同書", "合同", "協議書", "協議", }, EndDPValue = new string[] { LTPTrainingDP.核心關係, LTPTrainingDP.定中關係, LTPTrainingDP.並列關係, LTPTrainingDP.動賓關係, LTPTrainingDP.主謂關係 } }); e.DpKeyWordList = KeyList; var StartArray = new string[] { "簽署了", "簽定了" }; //經過語境訓練得到 var EndArray = new string[] { "合同" }; e.ExternalStartEndStringFeature = Utility.GetStartEndStringArray(StartArray, EndArray); e.ExternalStartEndStringFeatureCandidatePreprocess = (x) => { return x + "合同"; }; e.MaxLengthCheckPreprocess = str => { return EntityWordAnlayzeTool.TrimEnglish(str); }; //最高級別的置信度,特殊處理器 e.LeadingColonKeyWordCandidatePreprocess = str => { var c = Normalizer.ClearTrailing(TrimJianCheng(str)); return c; }; e.CandidatePreprocess = str => { var c = Normalizer.ClearTrailing(TrimJianCheng(str)); var RightQMarkIdx = c.IndexOf("」"); if (!(RightQMarkIdx != -1 && RightQMarkIdx != c.Length - 1)) { //對於"XXX"合同,有右邊引號,但不是最後的時候,不用作 c = c.TrimStart("「".ToCharArray()); } c = c.TrimStart("《".ToCharArray()); c = c.TrimEnd("》".ToCharArray()).TrimEnd("」".ToCharArray()); return c; }; e.ExcludeContainsWordList = new string[] { "平常經營重大合同" }; //下面這個列表的根據不足 e.ExcludeEqualsWordList = new string[] { "合同", "重大合同", "項目合同", "終止協議", "經營合同", "特別重大合同", "相關項目合同" }; e.Extract(this); //是否全部的候選詞裏面包括(測試集沒法使用) var contractlist = TraningDataset.ContractList.Where((x) => { return x.id == this.Id; }); if (contractlist.Count() > 0) { var contract = contractlist.First(); var contractname = contract.ContractName; if (!String.IsNullOrEmpty(contractname)) { e.CheckIsCandidateContainsTarget(contractname); } } //置信度 e.Confidence = ContractTraning.ContractES.GetStardardCI(); return e.EvaluateCI(); }
對於一些及其簡單的關鍵字抽取,例如,出現"現金認購",則將認購方法標記爲"現金",則可使用KeyWordMap屬性便可。
/// <summary> /// 評估方式 /// </summary> /// <param name="root"></param> /// <returns></returns> string getEvaluateMethod() { var p = new EntityProperty(); foreach (var method in ReOrganizationTraning.EvaluateMethodList) { p.KeyWordMap.Add(method, method); } p.Extract(this); if (!Program.IsMultiThreadMode) Program.Logger.WriteLine("評估方式:" + string.Join("、", p.WordMapResult)); return string.Join("、", p.WordMapResult); }
在尋在實體的時候,儘量的將找到的實體及其位置進行記錄,下面的結構體則是一個實體的記錄。
/// <summary> /// 位置和值 /// </summary> public struct LocAndValue<T> { /// <summary> /// HTML總體位置 /// </summary> public int Loc; /// <summary> /// 開始位置 /// </summary> public int StartIdx; /// <summary> /// 值 /// </summary> public T Value; /// <summary> /// 類型 /// </summary> public string Type; }
下面則是一個實體位置的應用。公司裏面放着全部公司實體的位置,標的則放着百分比 + 「股權」字樣的實體。經過位置信息,則能夠將「公司」和「標的」成對發現。
/// <summary> /// 得到標的 /// </summary> /// <returns></returns> List<(string Target, string Comany)> getTargetList() { var rtn = new List<(string Target, string Comany)>(); var targetRegular = new ExtractProperyBase.struRegularExpressFeature() { RegularExpress = RegularTool.PercentExpress, TrailingWordList = new string[] { "股權" }.ToList() }; var targetLoc = ExtractPropertyByHTML.FindRegularExpressLoc(targetRegular, root); //全部公司名稱 var CompanyList = new List<string>(); foreach (var companyname in companynamelist) { //注意,這裏的companyname.WordIdx是分詞以後的開始位置,不是位置信息! if (!CompanyList.Contains(companyname.secFullName)) { if (!string.IsNullOrEmpty(companyname.secFullName)) CompanyList.Add(companyname.secFullName); } if (!CompanyList.Contains(companyname.secShortName)) { if (!string.IsNullOrEmpty(companyname.secShortName)) CompanyList.Add(companyname.secShortName); } } var targetlist = new List<string>(); foreach (var companyname in CompanyList) { var companyLoc = ExtractPropertyByHTML.FindWordLoc(companyname, root); foreach (var company in companyLoc) { foreach (var target in targetLoc) { var EndIdx = company.StartIdx + company.Value.Length; if (company.Loc == target.Loc && Math.Abs(target.StartIdx - EndIdx) < 2) { if (!targetlist.Contains(target.Value + ":" + company.Value)) { rtn.Add((target.Value, company.Value)); targetlist.Add(target.Value + ":" + company.Value); } } } } } return rtn; }