0%

[DotnetCore]Reader系列-Pdf檔案

前情提要

Reader系列來到pdf格式檔案了,筆者這邊使用Pdfbox移植的C# Library:PdfPig,讀取部份,看github的readme的教學基本上就滿簡單的,只有讀取一個pdf檔案會很無趣,想了一下應用點,筆者公司銀行有規定新人要考過一些金融業基礎的證照考試取得證照,例: 金融市場常識與職業道德,想到一個有趣的應用,基本上都有考古題下載下來做練習,可以利用PdfPig套件讀取pdf檔案後解析題目與選項及答案,一旦取得這些資料,利用擅長的前端架構做一個頁面,後端則利用這些資料做一個隨機出題的API,就可以完成考古題測驗Application了,筆者這篇主要做到讀取及解析的作業及取得有結構化的資料。

內容

定義題目對應的Model

定義Option

1
2
3
4
5
6
public class QuestionOption
{
public string Option { get; set; }
// 從考古題的答案對應的選項壓為True
public bool IsAnswer { get; set; }
}

定義題目含Option

1
2
3
4
5
public class QuestionnaireWithOption
{
public string Question { get; set; }
public List<QuestionOption> Options { get; set; } = new List<QuestionOption>();
}

下載最新考古題

連結於此

撰寫程式及套用

安裝套件

1
dotnet add package PdfPig

撰寫主要解析邏輯

筆者觀察了一下考古題的pdf檔案,規則是滿統一的,

  • 答案會寫在最前面
  • 答案的格式為( 4 )這種形式:左括弧、空白、數字、空白、右括弧

格式統一就好辦事了,使出Regular Expression去做解析,主要解析程式邏輯為

  • 使用答案對應的Regex找到所有題目的答案所在位置Index
  • 利用第一步驟找到的Index List把完整題目(含選項、答案)擷取出來,此時可以順便擷取出答案
  • 第二步驟得到的每一個完整題目(含選項)在進行一次對選項的切割,也是利用選項的Regex Pattern:左括弧、數字、右括弧,可以得到選項所在位置Index List
  • 利用第三步得到的Index List,將各選項擷取出來,使用QuestionOption的結構,選項數字跟第二步驟取得的答案相符則在IsAnswer中設為true
  • 最後將完整題目(含選項、答案)的內容透過上面步驟得到的資訊,將題目部份擷取下來

經過以上那些步驟,大功告成,來看完成程式碼吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public static class PdfHelper
{
public static void Pdf2Json(FileInfo pdffile, FileInfo txtfile)
{
StreamWriter swPdfChange = new StreamWriter(txtfile.FullName, false, Encoding.GetEncoding("utf-8"));
var pdfContentStringBuilder = new StringBuilder();
// Step1: pdf讀取
using (PdfDocument doc = PdfDocument.Open(pdffile.FullName))
{
foreach (Page page in doc.GetPages())
{
string text = page.Text;
pdfContentStringBuilder.Append(text);
}
}
var pdfContent = pdfContentStringBuilder.ToString();
// Step2: 取得題目清單
var questionList = GetQuestionList(pdfContent);
// Step3: 取得有結構化過的題目選項物件清單
var questionWithOptionsList = GetQuestionWithOptionList(questionList);
// Step4: 寫入Json File中
var jsonText = JsonConvert.SerializeObject(questionWithOptionsList);
swPdfChange.Write(jsonText);
swPdfChange.Close();
}

public static List<string> GetQuestionList(string pdfContent)
{
var answerIndexList = new List<int>();
var questionList = new List<string>();
var regex = new Regex(@"\( \d \) ");
// Step1: 蒐集符合答案pattern的Index List
foreach (Match match in regex.Matches(pdfContent))
{
answerIndexList.Add(match.Index);
}
// Step2: 利用Step1產生的Index List將完整題目切割
for (int i = 0; i < answerIndexList.Count; i++)
{
if (i == answerIndexList.Count() - 1)
{
questionList.Add(pdfContent.Substring(answerIndexList[i]));
}
else
{
questionList.Add(pdfContent.Substring(answerIndexList[i], answerIndexList[i + 1] - answerIndexList[i]));
}
}
return questionList;
}

public static List<QuestionnaireWithOption> GetQuestionWithOptionList(List<string> questionList)
{
var questionWithOptionsList = new List<QuestionnaireWithOption>();
foreach (var question in questionList)
{
var questionWithOptions = new QuestionnaireWithOption();
var answer = 0;
Match match = Regex.Match(question, @"\( \d \)");
if (match.Success)
{
answer = int.Parse(match.Value.Trim('(').Trim(')').Trim());
var questionWithoutAnswer = question.Replace(match.Value, "");
questionWithOptions.Question = questionWithoutAnswer;

var optionIndexList = new List<int>();
var optionRegex = new Regex(@"\(\d\)");
foreach (Match optionMatch in optionRegex.Matches(questionWithoutAnswer))
{
optionIndexList.Add(optionMatch.Index);
}

for (int i = 0; i < optionIndexList.Count; i++)
{
var optionStr = questionWithoutAnswer.Substring(optionIndexList[i]);
if (i != optionIndexList.Count() - 1)
{
optionStr = questionWithoutAnswer.Substring(optionIndexList[i], optionIndexList[i + 1] - optionIndexList[i]);
}

Match optionStrMatch = optionRegex.Match(optionStr);
if (optionStrMatch.Success)
{
questionWithOptions.Options.Add(new QuestionOption()
{
Option = optionStr.Replace(optionStrMatch.Value, ""),
IsAnswer = answer == int.Parse(optionStrMatch.Value.Trim('(').Trim(')'))
});
}

if (i == 0)
{
questionWithOptions.Question = questionWithOptions.Question.Substring(0, optionIndexList[i]).Trim();
questionWithOptions.Question = questionWithOptions.Question.Split(' ')[1];
}
}
}
questionWithOptionsList.Add(questionWithOptions);
}
return questionWithOptionsList;
}
}

其中主要要理解的就是答案對應的Regular Expresion@"\( \d \)"選項對應的Regular Expresion@"\(\d\)"。筆者這邊最後輸出為有結構化過的資料轉為Json字串,輸出為Json結構的檔案,以利後續開發。

Client端使用

1
2
3
4
var sourceFilePath = @"{User}\Downloads\金融市場常識-109.pdf";
var targetFilePath = @"{User}\Downloads\金融市場常識-109.json";
PdfHelper.Pdf2Json(new FileInfo(sourceFilePath)
, new FileInfo(targetFilePath));

結果顯示

筆者這邊擷取Linqpad的輸出畫面

結論

筆者透過有趣的應用介紹pdf讀取器,花了筆者一個上午阿,希望有幫助到你,爾後有時間再來開發考古題測驗Application,屆時再發一篇了,敬請期待。