PDF Library for Delphi có thể khớp văn bản theo tính tương đương chuẩn tắc thay vì theo đơn vị mã, nên một truy vấn được gõ dưới dạng ký tự đã ghép sẵn tìm thấy nội dung được lưu dưới dạng chữ cái gốc cộng dấu kết hợp, và ngược lại. Hai tùy chọn tìm kiếm điều khiển việc này: soCanonicalEquivalent bật chuẩn hóa Unicode trong quá trình khớp, và soGraphemeClusters giới hạn mọi kết quả khớp và mọi bước ký tự đại diện vào trọn vẹn cụm grapheme
Lỗi mà tính năng này khắc phục là một trong những lỗi được báo cáo nhiều nhất và ít được hiểu nhất trong tìm kiếm tài liệu. Người dùng tìm kiếm một tên, không thấy kết quả nào, sao chép tên đó ra khỏi tài liệu, dán vào ô tìm kiếm, và tìm thấy nó. Không có gì hỏng theo cách rõ ràng: hai chuỗi trông giống hệt nhau, in ra giống hệt nhau, và so sánh không bằng nhau, vì một chuỗi là U+00E9 còn chuỗi kia là U+0065 theo sau bởi U+0301
Vì sao cùng một từ lại so sánh không bằng nhau?
Unicode cho phép nhiều cách mã hóa cho cùng một ký tự trừu tượng. Các chữ cái Latin có dấu phụ tồn tại dưới dạng điểm mã đã ghép sẵn và dưới dạng chuỗi chữ cái gốc cộng dấu kết hợp. Âm tiết Hangul tồn tại dưới dạng âm tiết đã ghép sẵn và dưới dạng jamo đã tách rời. Một PDF chứa dạng nào phụ thuộc vào nhà sản xuất, nền tảng, và đôi khi cả font, và không điều nào trong số đó hiển thị cho người thực hiện tìm kiếm
Lý do việc gấp chữ hoa/thường đơn giản không giải quyết được điều này mang tính cấu trúc chứ không phải ngẫu nhiên. Gấp chữ hoa/thường và gấp dấu phụ là ánh xạ một-một ở cấp đơn vị mã: chuỗi đã gấp có cùng độ dài với chuỗi gốc, nên một vị trí khớp trong văn bản đã gấp cũng là một vị trí khớp trong văn bản gốc. Chuẩn hóa không phải một-một. Một ký tự đã ghép sẵn trở thành hai hoặc ba đơn vị mã, một chuỗi đã tách rời gộp lại thành một, và sau phép biến đổi đó, vị trí không còn khớp với văn bản mà bạn đã trích xuất
Giữ tọa độ kết quả khớp trỏ về đúng văn bản gốc
Đây là phần quyết định liệu tìm kiếm đã chuẩn hóa có dùng được hay chỉ đơn thuần đúng về mặt lý thuyết. Mỗi đơn vị mã do chuẩn hóa tạo ra ghi lại vị trí bắt đầu và kết thúc của văn bản UTF-16 gốc đã tạo ra nó. Các phép phân giải đệ quy kế thừa phạm vi nguồn của phần tử cha, các phép ghép hợp gộp phạm vi của các đầu vào của chúng, và khi tìm thấy một kết quả khớp, thư viện quét khoảng ánh xạ để tìm điểm bắt đầu nhỏ nhất và điểm kết thúc lớn nhất
Hiệu ứng là MatchStart, MatchLength, các chuỗi ngữ cảnh và cả hai điểm vào thay thế đều tiếp tục trỏ vào văn bản đã trích xuất gốc, không phải vào bản trung gian đã chuẩn hóa. Nếu không có ánh xạ đó, một tìm kiếm đã chuẩn hóa có thể cho bạn biết một kết quả khớp tồn tại nhưng không thể cho biết đáng tin cậy nó nằm ở đâu, khiến việc tô sáng sai và việc kiểm duyệt (redaction) trở nên nguy hiểm
Bản thân bộ chuẩn hóa hoàn toàn tự chứa: bảng nhỏ gọn cho phân giải chuẩn tắc, ghép hợp và lớp kết hợp chuẩn tắc từ Unicode 15.1, với Hangul được xử lý bằng các quy tắc thuật toán thay vì bằng mục bảng. Không có gì được tải từ một file dữ liệu bên ngoài và không có API chuẩn hóa nền tảng nào được gọi, nên một service Windows, một daemon Linux và một bản build FPC đều cho ra kết quả giống hệt nhau trên cùng đầu vào
Tìm kiếm với tính tương đương chuẩn tắc
Các tùy chọn là một tập hợp, nên tính tương đương chuẩn tắc kết hợp được với các hành vi hiện có như khớp toàn từ, ký tự đại diện và gấp dấu phụ không phân biệt:
uses
PDFlibrary;
var
Lib: TPDFlib;
Hits: array of TPDFlibSearchHit;
Found, I: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('contracts.pdf', '');
SetLength(Hits, 500);
Found := Lib.SearchText('Bäcker', [soCanonicalEquivalent, soWholeWord],
'', Hits); // phạm vi trang rỗng = toàn tài liệu
for I := 0 to Found - 1 do
Log(Format('page %d: "%s" at %d (%d chars)',
[Hits[I].Page, Hits[I].MatchText, Hits[I].MatchStart,
Hits[I].MatchLength]));
finally
Lib.Free;
end;
end;
Chuẩn hóa là tùy chọn bật thêm (opt-in) vì một lý do. Xây dựng văn bản NFD và ánh xạ vị trí của nó tốn công sức, và phần lớn tìm kiếm trên tài liệu chỉ dùng ASCII không bao giờ cần đến nó. Khi tùy chọn được dùng, mỗi khối văn bản cache hai dạng đã biến đổi, một dạng đã loại bỏ dấu kết hợp và một dạng chưa, nên một loạt truy vấn trên cùng một khối chỉ chuẩn hóa một lần thay vì một lần cho mỗi truy vấn. Việc gấp chữ hoa/thường vẫn đi theo con đường một-một rẻ hơn, không đổi
Điều gì hỏng nếu không có ranh giới cụm grapheme?
Đơn vị mã không phải ký tự, và ký tự không phải điều mà người dùng cảm nhận. Một emoji cờ là hai điểm mã chỉ báo vùng (regional indicator). Một emoji gia đình là nhiều điểm mã nối bằng bộ nối độ rộng không (zero-width joiner). Một liên kết phụ âm (conjunct) trong chữ viết Ấn Độ là một phụ âm, một virama và một phụ âm khác. Một chữ cái có hai dấu chồng là ba điểm mã. Khớp hoặc cắt ở giữa bất kỳ chuỗi nào trong số này tạo ra một mảnh vỡ render ra rác
soGraphemeClusters giới hạn cả hai đầu của mọi kết quả khớp, dù là chữ nghĩa hay ký tự đại diện, vào trọn vẹn ranh giới cụm grapheme mở rộng. Việc phân đoạn triển khai các quy tắc mở rộng: ghép cặp CR và LF, ký tự điều khiển, các lớp âm tiết Hangul, Extend và SpacingMark, Prepend, chuỗi ZWJ emoji, ghép cặp chỉ báo vùng và ngắt liên kết phụ âm Ấn Độ. Một ranh giới không bao giờ được tạo ra bên trong một cặp surrogate, riêng điều này đã loại bỏ cả một nhóm kết quả bị hỏng trên bất kỳ nội dung nào vượt ra ngoài mặt phẳng đa ngôn ngữ cơ bản
Tùy chọn này cũng chi phối cách tiêu thụ ký tự đại diện, đây chính là nơi một triển khai ngây thơ vẫn sẽ cắt sai. Ký tự đại diện đơn tiến đúng một cụm hoàn chỉnh, và việc quay lui cho ký tự đại diện chuỗi chỉ di chuyển giữa các ranh giới cụm:
// Không có soGraphemeClusters, "?" có thể tiêu thụ nửa cụm và
// trả về một kết quả khớp có văn bản kết thúc bằng một dấu kết
// hợp lơ lửng
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Cùng ranh giới đó bảo vệ việc thay thế, nên kiểm duyệt và viết
// lại nội dung không bao giờ cắt đôi một emoji hay một chữ cái có dấu
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Chọn tùy chọn cho một khối lượng công việc thực tế
Ba tổ hợp bao phủ phần lớn trường hợp. Với một ô tìm kiếm tài liệu nội bộ, soCanonicalEquivalent cộng soDiacriticInsensitive mang lại hành vi dễ dãi mà người dùng mong đợi, khớp cả hai dạng mã hóa lẫn cả cách viết có dấu và không dấu. Với tìm kiếm pháp lý hoặc tuân thủ, nơi một kết quả khớp sai có cái giá phải trả, hãy dùng soCanonicalEquivalent với soCaseSensitive và soWholeWord và tắt việc gấp dấu phụ, để tính tương đương chính xác và độc lập với mã hóa
Với bất cứ thứ gì chỉnh sửa tài liệu, hãy thêm soGraphemeClusters không có ngoại lệ. Một tìm kiếm trả về phạm vi hơi sai chỉ gây hiểu lầm cho người đọc; một thao tác thay thế hoặc kiểm duyệt dùng cùng phạm vi sai sẽ ghi lỗi đó thẳng vào file. Hệ quả của việc lấy sai phạm vi loại bỏ được đề cập trong kiểm duyệt thực sự và loại bỏ nội dung
Khi thông lượng quan trọng, hãy ưu tiên các điểm vào theo lô. SearchTextBatch chạy mọi truy vấn không rỗng trong khi các khối văn bản của mỗi trang vẫn còn trong bộ nhớ, tránh phải trích xuất lại một trang cho mỗi truy vấn và tái sử dụng chuẩn hóa đã cache, và các biến thể streaming phát ra kết quả khớp mà không cần một bộ đệm kích thước do bên gọi định. Mô hình trích xuất bên dưới được mô tả trong tìm kiếm văn bản và liệt kê phần tử trang
Các chữ viết mà điều này không phải tùy chọn
Đối với tiếng Hàn, tính tương đương chuẩn tắc là sự khác biệt giữa việc tìm thấy một cái tên và không tìm thấy, vì âm tiết đã ghép sẵn và jamo đã tách rời đều phổ biến trong các tài liệu thực tế. Đối với tiếng Việt, các dấu phụ chồng lên nhau khiến dạng ghép hoàn toàn phụ thuộc vào nhà sản xuất. Đối với các chữ viết Ấn Độ, cách xử lý liên kết phụ âm quyết định liệu ranh giới kết quả khớp có nằm ở một chỗ dễ đọc hay không. Đối với tiếng Nhật và tiếng Trung, phía tìm kiếm tương đối đơn giản, dù phía bố cục thì không, như được mô tả trong viết dọc cho tiếng Nhật và tiếng Trung
Quy tắc ngón tay cái ở đây ngắn gọn: nếu kho dữ liệu chứa bất kỳ ngôn ngữ nào ngoài tiếng Anh, hãy bật tính tương đương chuẩn tắc và đo chi phí trước khi kết luận nó quá đắt. Trong phần lớn bộ dữ liệu thì không, và giải pháp thay thế là một tính năng tìm kiếm âm thầm thất bại đúng vào những cái tên mà người dùng của bạn quan tâm nhất để tìm thấy
Tìm kiếm nhận biết Unicode, trích xuất, kiểm duyệt và viết lại văn bản dùng chung một bộ máy cho Delphi, C++Builder và Free Pascal; danh sách tính năng đầy đủ có trên trang PDF Library for Delphi