工作上的小需求,監控系統定期發送 PDF 報表,回報像下面的主機監控數據。

覺得每天重複收信開 PDF 檔用肉眼檢查很蠢,但監控系統不在轄區沒法要求其他格式,怎麼辦?

幸好,我們可是會寫程式的人呢,不怕不怕。

先寫一隻小程式檢查 Outlook 收件匣依主旨特徵挑出監控系統報表,從中取出 PDF 檔,再取出文字比對。

大家常用的 .NET PDF 元件,iTextSharp,前陣子聽說似乎有授權疑慮,特別查了,狀況應是 iTextSharp 4.1.6/iText 4.2.0 之前原本採用 MPL 和 LGPL 授權(註:GPL 的感染力直逼僵屍病毒,專案只要沾到 GPL 程式庫就必須跟著 GPL 開源, 讓開發者退避三舍,因此有些元件會採用較寬鬆的 Lesser General Public License/LGPL,免除單純呼叫函式也必須公開原始碼的限制。參考: 避免無謂的商業授權費,搞懂LGPL與GPL的不同), 但在 5.0 版後,iTextSharp 改為 AGPL,於非開源專案使用必須購買商業許可,開發商並主張 4.x 包含非 LGPL 程式碼,使用可能侵犯著作權。參考

研究了一下,一時沒找到好用的 iTextSharp 替代品。但由於我只需要純文字,想到另一條路 - 用 IFilter 把 PDF 轉成純文字 (延伸閱讀:為 PDF、Office 檔案產生文字索引)。 我找到一個 NuGet 套件 - IFilterTextReader:

可以一行搞定 PDF 轉字串:

var text = 
  new IFilterTextReader.FilterReader("test.pdf").ReadToEnd();

FilterReader 會將文章開頭圖例的表格轉成如下文字:

192.168.1.100 Number of Changes: Number of Objects Created: Number of Objects Updated: Number of Objects Deleted: Number of Objects Renamed: 11 2 1 0 8 192.168.1.101 Number of Changes: Number of Objects Created: Number of Objects Updated: Number of Objects Deleted: Number of Objects Renamed: 23 9 5 2 7

而我的目標是要轉成以下格式:

對我來說這是一次 RegularExpression 隨堂測驗,我花了半個小時交卷,學會一個知道但沒實際用過的進階樣式 Positive Lookahead (右合樣)。(延伸閱讀:使用 Regular Expression 驗證密碼複雜度 by 保哥)

範例程式如下:

class Program
{
    static string source = 
        "192.168.1.100 Number of Changes: Number of Objects Created: Number of Objects Updated: Number of Objects Deleted: Number of Objects Renamed: 11 2 1 0 8 192.168.1.101 Number of Changes: Number of Objects Created: Number of Objects Updated: Number of Objects Deleted: Number of Objects Renamed: 23 9 5 2 7";

    static void Main(string[] args)
    {
        foreach (Match hostData in Regex.Matches(source, @"(?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?<data>.+?(?=\d{1,3}\.\d{1,3}\.|$))"))
        {
            var ip = hostData.Groups["ip"].Value;
            var data = hostData.Groups["data"].Value;
            var labels = Regex.Matches(data, @"(?<h>Number of .+?:)+").Cast<Match>()
                            .Select(o => o.Groups["h"].Value).ToArray();
            var values = Regex.Matches(data, @"(?<n>[0-9]+)").Cast<Match>()
                            .Select(o => o.Groups["n"].Value).ToArray();
            Console.WriteLine(ip);
            for (var i = 0; i < labels.Length; i++)
            {
                Console.WriteLine($"  {labels[i]} {values[i]}");
            }
        }
        Console.ReadLine();
    }
}

程式執行原理是先用 (?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?<data>.+?(?=\d{1,3}\.\d{1,3}\.|$)) 拆出以 ddd.ddd.ddd.ddd (d 代表 0-9 數字)起首到下一段 ddd.ddd. 或字串結尾的一段文字。 (?=X|$) 即所謂 Positive Lookahead,限制該段文字後要接 X 或是字串結尾才吻合。比對結果跑迴圈取出 IP 及 IP 後方的 Number of ***: 及數字等內容,再分別各用 (?<h>Number of .+?:)+(?<n>[0-9]+) 取出欄位標題及欄位數值。 這裡有用到先前介紹過讓 MatchCollection 支援 LINQ 的小技巧,接著爽爽寫 LINQ 轉成字串陣列,理論上欄位標題及數值的數目要相同,用迴圈輸出結果,大功告成。

My practice of using regular expression positive lookahead to extract info from text which is extracted from PDF by IFilter.


Comments

# by Huang

這個試試 http://www.squarepdf.net/pdfbox-in-net

# by Jeffrey

to Huang, 感謝補充,已筆記。

Post a comment