PDFium Component ตรวจจับตารางบนหน้า PDF และคืนค่าเป็นตารางกริดของเซลล์พร้อม row span, column span, แถวหัวตาราง และค่าความมั่นใจ ผ่าน ExtractTables สำหรับหนึ่งหน้า และ ExtractDocumentTables สำหรับทั้งเอกสาร แต่ละตารางแปลงเป็น CSV หรือ JSON ได้ด้วยการเรียกครั้งเดียว และตารางที่ต่อเนื่องข้ามการขึ้นหน้าใหม่สามารถเชื่อมโยงเข้าเป็นห่วงโซ่ต่อเนื่องได้
PDF ไม่มีอ็อบเจกต์ตาราง ตารางใน PDF คือชุดของ text run ที่วางตำแหน่งไว้ให้มนุษย์อ่านเป็นกริด บางครั้งมีเส้นวาดล้อมรอบและมักไม่มี การกู้คืนกริดหมายถึงการสร้างเจตนาที่ไฟล์ไม่เคยบันทึกไว้ขึ้นมาใหม่ ซึ่งเป็นเหตุผลที่เครื่องมือดึงข้อมูลทุกตัวให้ผลลัพธ์ต่างกันเล็กน้อย และเป็นเหตุผลที่เครื่องมือที่บอกคุณถึงความมั่นใจของมันมีประโยชน์มากกว่าเครื่องมือที่ไม่บอก
สองโหมดการตรวจจับสำหรับตารางสองแบบ
การตรวจจับแบบมีเส้นใช้เส้นที่วาดไว้ แต่ละส่วนของเส้นทางที่ลากเส้นถูกแปลงเป็นพิกัดหน้าผ่านเมทริกซ์ของ page object เส้นแนวนอนและแนวตั้งถูกตัดกัน และจุดตัดกลายเป็นคอมโพเนนต์ที่เชื่อมต่อกัน แต่ละคอมโพเนนต์กลายเป็นกริดตำแหน่ง X และ Y ที่เรียงลำดับแล้วของตัวเอง ซึ่งเป็นสิ่งที่ป้องกันไม่ให้ตารางสองตารางที่แยกกันบนหน้าเดียวกันถูกรวมเป็นกริดที่ไร้ความหมายเดียว
การตรวจจับแบบช่องว่างจัดการตารางที่วาดด้วยการจัดวางแทนเส้น กล่องคำถูกจัดกลุ่มเป็นแถวที่มองเห็นได้ ช่องว่างภายในแถวแบ่งมันออกเป็นคอลัมน์ผู้เข้าชิง และตารางจะถูกยอมรับเมื่อมีอย่างน้อย MinRows แถวที่ทำซ้ำ anchor ชิดซ้ายอย่างน้อย MinColumns ตำแหน่งภายใน AlignmentTolerance เท่านั้น ตัวคูณช่องว่างระหว่างแถวมีค่าเริ่มต้นที่ 3 ซึ่งครอบคลุมระยะเส้นฐานประมาณ 30 พอยต์ที่พบทั่วไปในข้อความ 12 พอยต์ โดยไม่ปล่อยให้บรรทัดเดียวที่มี text run หลายชิ้นแอบอ้างเป็นตาราง
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'annual-report.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 12; // นับเริ่มที่ 1
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // ค่าเริ่มต้นคือ 0.5
Options.HeaderRowCount := 1;
Tables := Pdf.ExtractTables(Options);
for I := 0 to High(Tables) do
Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
[I, Tables[I].RowCount, Tables[I].ColumnCount,
Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));
if Length(Tables) > 0 then
SaveText('page12-table0.csv', Tables[0].ToCsv);
finally
Pdf.Free;
end;
end;
เซลล์ที่ผสานถูกกู้คืนอย่างไร
นี่คือส่วนที่ตัวดึงข้อมูลแบบไร้เดียงสามักทำผิด เซลล์ที่ผสานไม่สามารถระบุได้จากกริดรวมเพียงอย่างเดียว เพราะกริดถูกอนุมานจากเส้นทั้งหมดบนหน้า และพื้นที่ที่ผสานเพียงแค่ขาดเส้นภายในที่ควรจะแยกมันออกจากกัน
กฎที่ใช้ตรงนี้เป็นแบบท้องถิ่น เซลล์ฐานที่อยู่ติดกันสองเซลล์จะถูกผสานเมื่อไม่มีเส้นขอบครอบคลุมช่วงระหว่างมัน union-find เชื่อมโยงมันเข้าด้วยกัน คอมโพเนนต์สี่เหลี่ยมที่ได้กลายเป็นค่า RowSpan และ ColumnSpan และข้อความถูกกำหนดให้กับเซลล์ฐานตามจุดศูนย์กลางของมัน แล้วตามเซลล์นั้นไปยัง merge root ของมัน การทำแบบนี้ยังทำให้ต้นทุนเป็นเชิงเส้นตามจำนวนคำบวกเซลล์ แทนที่จะเป็นการสแกนแบบกำลังสองที่ได้จากการทดสอบทุกคำกับทุกเซลล์
ผลในทางปฏิบัติคือตารางการเงินที่มีหัวตาราง "รวม" ที่ผสานครอบคลุมสามคอลัมน์จะออกมาเป็นเซลล์เดียวที่มี span เท่ากับสาม แทนที่จะเป็นเซลล์ที่มีข้อมูลหนึ่งเซลล์กับเซลล์ว่างลึกลับสองเซลล์
ความต่อเนื่องข้ามหน้า
ตารางยาวมักขาดตอนข้ามหน้า และการปฏิบัติต่อชิ้นส่วนของแต่ละหน้าเป็นตารางอิสระบังคับให้ผู้เรียกใช้ต้องเย็บมันเข้าด้วยกัน ExtractDocumentTables สามารถเชื่อมโยงมันได้แทน แต่ภายใต้เงื่อนไขที่เข้มงวดเท่านั้น ชิ้นส่วนต้องเป็นตารางล่างสุดบนหน้าก่อนหน้า ชิ้นต่อไปต้องเป็นตารางบนสุดบนหน้าถัดไป หมายเลขหน้าต้องอยู่ติดกัน และขอบเขตคอลัมน์ต้องตรงกัน
เงื่อนไขทั้งสี่ข้อรวมกันคือสิ่งที่ป้องกันข้อผิดพลาดที่ชัดเจน คือการเชื่อมโยงทุกตารางสี่คอลัมน์ในเอกสารเข้าเป็นตารางยักษ์สมมติเดียว เพียงเพราะมันบังเอิญมีจำนวนคอลัมน์เท่ากัน เมื่อเงื่อนไขเป็นจริง ตารางเหล่านั้นจะแชร์ตัวระบุกลุ่มความต่อเนื่องและพกเมทาดาทาความต่อเนื่อง เมื่อไม่เป็นจริง คุณจะได้ตารางแยกและตัดสินใจได้เอง
การดึงข้อมูลระดับเอกสารแชร์งบประมาณ MaxCells และ MaxTables ข้ามหน้าแทนที่จะรีเซ็ตทุกหน้า และมันคืนหน้าที่แอกทีฟกลับใน block finally ดังนั้นการดึงข้อมูลในตัวแสดงผลจึงทิ้งผู้ใช้ไว้ที่หน้าที่เขากำลังดูอยู่
การส่งออกโดยไม่ทำให้ข้อมูลเสียหาย
ตัวส่งออกทั้งสองแบบมีเจตนาชัดเจนเรื่องการ escape CSV จะใส่เครื่องหมายคำพูดรอบทุกฟิลด์เสมอและซ้อนเครื่องหมายคำพูดภายในเป็นสองตัว ซึ่งหลีกเลี่ยงความล้มเหลวคลาสสิกที่เซลล์ที่มีจุลภาคกลายเป็นสองคอลัมน์อย่างเงียบ ๆ สำหรับเซลล์ที่ผสาน เนื้อหาจะถูกปล่อยออกเฉพาะที่ anchor บนซ้ายเท่านั้น ดังนั้นการวนรอบ CSV จึงไม่ทำสำเนาหัวตารางที่คร่อมหลายคอลัมน์ซ้ำ
JSON คงรักษา Unicode แทนที่จะ escape เป็น ASCII escape อักขระควบคุม และรวมเมทาดาทาที่ผู้บริโภคปลายทางต้องใช้ตัดสินคุณภาพ คือโหมดการตรวจจับ ความมั่นใจ ขอบเขต ค่า span แฟล็กหัวตาราง และข้อมูลความต่อเนื่อง หากคุณกำลังป้อนตารางที่ดึงมาเข้าระบบปลายทาง ให้เลือก JSON เพราะแถว CSV ไม่สามารถบอกคุณได้ว่าตารางที่มันมาจากได้คะแนนความมั่นใจ 0.51
// การดึงข้อมูลทั้งเอกสาร เก็บเฉพาะตารางที่น่าเชื่อถือ
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
if Tables[I].Confidence < 0.75 then
begin
Log(Format('page %d table needs review (%.2f)',
[Tables[I].PageNumber, Tables[I].Confidence]));
Continue;
end;
if Tables[I].ContinuationGroup > 0 then
AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
else
EmitStandalone(Tables[I].ToJson);
end;
การปรับจูน และการรู้ว่าเมื่อไหร่ควรหยุด
มีการตั้งค่าสามอย่างที่สำคัญกว่าอันอื่น MinConfidence คือประตูคุณภาพ และค่า 0.5 นั้นตั้งใจให้ผ่อนปรน ยกให้สูงขึ้นสำหรับการนำเข้าอัตโนมัติและลดลงสำหรับ UI สำหรับตรวจสอบที่มีมนุษย์ยืนยันแต่ละผลลัพธ์ MinColumnGap ตัดสินว่าอะไรนับเป็นขอบเขตคอลัมน์ในโหมดช่องว่าง และตารางที่จัดวางแน่นในรายงานหนาแน่นอาจต้องลดค่าลงจากค่าเริ่มต้น 12 พอยต์ MaxRowGapFactor ตัดสินว่าระยะแนวตั้งเมื่อไหร่ที่จบตาราง ซึ่งสำคัญสำหรับตารางที่มีแถวว่างเป็นครั้งคราว
ควรตรงไปตรงมาเรื่องขีดจำกัด ตารางแบบมีเส้นดึงข้อมูลได้เชื่อถือได้ ตารางแบบช่องว่างที่จัดวางเรียบร้อยดึงข้อมูลได้ดี ตารางที่มีข้อความหมุน ตารางซ้อนใน หรือเซลล์ที่เนื้อหาตัดขึ้นเป็นสิ่งที่ดูเหมือนอีกแถวหนึ่ง จะต้องการการตรวจสอบไม่ว่าพารามิเตอร์จะตั้งค่าอย่างไร สำหรับกรณีเหล่านั้น โมเดลข้อความมีโครงสร้างให้วัตถุดิบสำหรับสร้างตัวอ่านเฉพาะโดเมน อธิบายไว้ใน บล็อกข้อความมีโครงสร้างและลำดับการอ่าน
มีการจับคู่ที่มีประโยชน์อยู่หนึ่งอย่าง เมื่อเอกสารที่สแกนไม่มีข้อความเลย การตรวจจับตารางจะไม่มีอะไรให้ทำงานด้วยจนกว่าจะมีเลเยอร์ข้อความ เพิ่มเลเยอร์ก่อนตามที่อธิบายไว้ใน การเพิ่มเลเยอร์ข้อความที่ค้นหาได้ให้ PDF ที่สแกน แล้วค่อยดึงข้อมูล กล่องคำที่ผู้ให้บริการ OCR ส่งคืนคือสิ่งที่การตรวจจับช่องว่างต้องการเป็นอินพุตพอดี
การดึงตาราง ข้อความมีโครงสร้าง และการจัดเรียงใหม่ล้วนอ่านจากโมเดลหน้าเดียวกันใน Delphi, C++Builder และ Lazarus API ฉบับเต็มอธิบายไว้ที่หน้า PDFium Component สำหรับ Delphi