Ζητήστε από μια ροή επεξεργασίας να δρομολογήσει δέκα χιλιάδες υπολογιστικά φύλλα κατά συγγραφέα, εταιρεία ή ημερομηνία τελευταίας τροποποίησης, και το χειρότερο που μπορεί να κάνει είναι να ανοίξει πλήρως κάθε βιβλίο εργασίας. Οι απαντήσεις ταξιδεύουν μέσα στις ιδιότητες εγγράφου του αρχείου, αυτό που ο κόσμος του Office ονομάζει Document Summary Information: το στρώμα μεταδεδομένων που ευρετηριάζει η Αναζήτηση των Windows, βάσει του οποίου ταξινομεί το SharePoint και το οποίο δείχνει το Excel στον διάλογο Properties. Αυτό το στρώμα είναι το πολύ μερικά kilobytes, και βρίσκεται σε καλά τεκμηριωμένη θέση και στις δύο μορφές του Excel. Το κόλπο είναι να το φτάσετε από τη Delphi χωρίς να πληρώσετε για το εκατομμύριο κελιά που δεν χρειάζεστε
Υπάρχουν τρεις πραγματικοί δρόμοι, και διαφέρουν λιγότερο ως προς το τι επιστρέφουν παρά ως προς το τι απαιτούν από το μηχάνημα που τους τρέχει. Το COM automation οδηγεί το ίδιο το Excel και διαβάζει τα πάντα, σε τιμές επιτραπέζιου υπολογιστή. Η μορφή .xls κρατά τις ιδιότητές της σε OLE property-set streams που τα Windows θα αναλύσουν για λογαριασμό σας. Η μορφή .xlsx τις κρατά σε δύο μικρά μέρη XML μέσα σε ένα zip που το RTL της Delphi μπορεί να ανοίξει μόνο του. Ακολουθεί λειτουργικός κώδικας για τον καθένα, με το κόστος δηλωμένο απερίφραστα
Δρόμος 1: το COM automation διαβάζει τα πάντα, σε τιμές επιτραπέζιου υπολογιστή
Το automation είναι ο μόνος δρόμος με ολική κάλυψη μέσα από ένα μοντέλο αντικειμένων: το τυπικό σύνολο σύνοψης, το εκτεταμένο σύνολο με Company και Manager, και τις ιδιότητες που ορίζει ο χρήστης, όλα προσβάσιμα μέσω των BuiltinDocumentProperties και CustomDocumentProperties. Τα πάντα φτάνουν ως OleVariant, και το API έχει μια συνήθεια που αξίζει να ξέρετε πριν σας δαγκώσει: μια ενσωματωμένη ιδιότητα που δεν ανατέθηκε ποτέ δεν επιστρέφει κενή, εγείρει ένα EOleException τη στιγμή που αγγίζετε το Value. Ο βοηθός παρακάτω το αντιμετωπίζει ως «δεν έχει οριστεί» και όχι ως αποτυχία
uses
System.SysUtils, System.Variants, System.Win.ComObj;
procedure ReadPropertiesViaCom(const FileName: string);
var
Excel, Book, Builtin, Custom: OleVariant;
I: Integer;
function BuiltinProp(const Name: string): string;
begin
try
Result := VarToStr(Builtin.Item(Name).Value);
except
on EOleError do
Result := ''; // η ιδιότητα υπάρχει αλλά δεν ανατέθηκε ποτέ
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // μόνο για ανάγνωση
try
Builtin := Book.BuiltinDocumentProperties;
Writeln('Author : ', BuiltinProp('Author'));
Writeln('Title : ', BuiltinProp('Title'));
Writeln('Subject: ', BuiltinProp('Subject'));
Writeln('Company: ', BuiltinProp('Company'));
Writeln('Manager: ', BuiltinProp('Manager'));
Custom := Book.CustomDocumentProperties;
for I := 1 to Custom.Count do
Writeln(VarToStr(Custom.Item(I).Name), ' = ',
VarToStr(Custom.Item(I).Value));
finally
Book.Close(False);
end;
finally
Excel.Quit; // φτάστε εδώ σε κάθε διαδρομή, αλλιώς το EXCEL.EXE μένει πίσω
Excel := Unassigned;
end;
end;
Τώρα ο λογαριασμός. Το Excel πρέπει να είναι εγκατεστημένο σε κάθε μηχάνημα όπου τρέχει αυτός ο κώδικας, κάτι που από μόνο του αποκλείει τους περισσότερους διακομιστές, και η πολιτική υποστήριξης της Microsoft λέει ρητά ότι το Office ούτε σχεδιάστηκε ούτε αδειοδοτείται για automation χωρίς επιτήρηση από την πλευρά του διακομιστή. Το CreateOleObject εκκινεί ένα πλήρες EXCEL.EXE και το Workbooks.Open αναλύει ολόκληρο το βιβλίο εργασίας, οπότε περιμένετε περίπου δύο έως τέσσερα δευτερόλεπτα ανά αρχείο πριν επιστρέψει η πρώτη ιδιότητα. Και το try..finally γύρω από το Quit δεν είναι διακοσμητικό: μια εξαίρεση που διαφεύγει ανάμεσα στο CreateOleObject και το Quit αφήνει ένα ορφανό EXCEL.EXE να κρατά κλείδωμα στο αρχείο, αόρατο μέχρι να αποτύχει πάνω του η επόμενη εκτέλεση. Η επαναχρησιμοποίηση μίας παρουσίας του Excel σε μια παρτίδα αποσβένει το κόστος εκκίνησης αλλά συγκεντρώνει τον κίνδυνο, επειδή ένας αδέσποτος διάλογος στην κρυφή επιφάνεια εργασίας κολλάει κάθε αρχείο που περιμένει πίσω του
Δρόμος 2: το .xls αποθηκεύει ιδιότητες σε OLE property-set streams
Ένα βιβλίο εργασίας BIFF8 είναι ένα OLE compound file, ένα μινιατούρα σύστημα αρχείων από storages και streams. Τα δεδομένα των κελιών ζουν στο stream Workbook· τα μεταδεδομένα ζουν δίπλα τους σε δύο property-set streams των οποίων τα ονόματα αρχίζουν με τον χαρακτήρα ελέγχου #5: το \005SummaryInformation για τα κλασικά πεδία και το \005DocumentSummaryInformation για τα εκτεταμένα και τα προσαρμοσμένα. Μέσα στο καθένα κάθεται ένα δυαδικό property set στη διάταξη MS-OLEPS, με ενότητες κλειδωμένες σε ένα αναγνωριστικό μορφής (FMTID) και ιδιότητες κλειδωμένες σε ένα ακέραιο property ID. Η ενότητα σύνοψης έχει FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, όπου το PIDSI_TITLE είναι $02 και το PIDSI_AUTHOR είναι $04· τα Company ($0F) και Manager ($0E) ζουν στην ενότητα document-summary, και οι προσαρμοσμένες ιδιότητες σε μια δεύτερη ενότητα πίσω από ένα λεξικό ονομάτων
Τα καλά νέα είναι ότι στα Windows δεν αναλύετε ποτέ μόνοι σας αυτά τα bytes. Το structured storage εκθέτει τα streams μέσω του IPropertySetStorage, και το παρακάτω μεταγλωττίζεται όπως φαίνεται με τις τυπικές μονάδες του RTL
uses
System.SysUtils, Winapi.Windows, Winapi.ActiveX, System.Win.ComObj;
const
FMTID_SummaryInfo: TGUID = '{F29F85E0-4FF9-1068-AB91-08002B27B3D9}';
PIDSI_TITLE = $02;
PIDSI_AUTHOR = $04;
STGFMT_STORAGE = 0;
function ReadXlsSummaryString(const FileName: string; PropId: TPropID): string;
var
Unk: IUnknown;
Stg: IStorage;
PropSetStg: IPropertySetStorage;
PropStg: IPropertyStorage;
Spec: TPropSpec;
Value: TPropVariant;
begin
Result := '';
OleCheck(StgOpenStorageEx(PWideChar(FileName),
STGM_READ or STGM_SHARE_DENY_WRITE, STGFMT_STORAGE, 0, nil, nil,
@IID_IStorage, Unk));
Stg := Unk as IStorage;
PropSetStg := Stg as IPropertySetStorage;
OleCheck(PropSetStg.Open(FMTID_SummaryInfo,
STGM_READ or STGM_SHARE_EXCLUSIVE, PropStg));
Spec.ulKind := PRSPEC_PROPID;
Spec.propid := PropId;
if PropStg.ReadMultiple(1, @Spec, @Value) = S_OK then // S_FALSE: δεν υπάρχει
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// χρήση: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Μια ειλικρινής κουβέντα για όσα κρύβει το απόσπασμα. Οι συμβολοσειρές μπορούν να φτάσουν ως VT_LPWSTR ή ως VT_LPSTR, και στην περίπτωση ANSI τα bytes είναι κωδικοποιημένα στην κωδικοσελίδα του ίδιου του property set, η οποία αποθηκεύεται ως ιδιότητα 1 της ενότητας, οπότε η παραπάνω μετατροπή τύπου είναι ακριβής μόνο όταν αυτή η κωδικοσελίδα ταιριάζει με του συστήματος. Οι χρονοσφραγίδες επιστρέφουν ως VT_FILETIME σε UTC. Οι προσαρμοσμένες ιδιότητες σημαίνουν άνοιγμα της ενότητας που ορίζει ο χρήστης, με FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, και διάσχιση του λεξικού ονομάτων της. Το IPropertyStorage απορροφά όλα αυτά στα Windows· το να γράψετε τον δικό σας αναλυτή MS-OLEPS για ένα περιβάλλον χωρίς structured storage είναι πραγματικό έργο, όχι δουλειά ενός απογεύματος
Δρόμος 3: το .xlsx κρατά τα docProps ως XML μέσα στο zip
Αυτός είναι ο δρόμος που χρειάζονται πραγματικά οι περισσότερες ροές επεξεργασίας, αφού τα νέα αρχεία είναι .xlsx εδώ και σχεδόν δύο δεκαετίες. Ένα βιβλίο εργασίας OOXML είναι ένα πακέτο zip, και οι ιδιότητές του είναι μοιρασμένες σε μικρά μέρη ανά σκοπό: το docProps/core.xml κρατά τα πεδία Dublin Core, τα dc:title, dc:creator, cp:lastModifiedBy, συν τα dcterms:created και dcterms:modified ως χρονοσφραγίδες W3CDTF σε UTC, ενώ το docProps/app.xml κρατά πεδία σε επίπεδο εφαρμογής όπως τα Company και AppVersion, και το docProps/custom.xml κρατά τις προσαρμοσμένες ιδιότητες. Επειδή ο κεντρικός κατάλογος του zip εντοπίζει κάθε μέρος απευθείας, η ανάγνωσή τους κοστίζει μερικά kilobytes ανεξάρτητα από το πόσο μεγάλο είναι το βιβλίο εργασίας. Τα TZipFile και IXMLDocument, και τα δύο μέσα στο RTL που έρχεται μαζί, κάνουν όλη τη δουλειά
uses
System.SysUtils, System.Classes, System.Zip, Xml.XMLDoc, Xml.XMLIntf;
const
NsDC = 'http://purl.org/dc/elements/1.1/';
NsTerms = 'http://purl.org/dc/terms/';
NsCore = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
NsApp = 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties';
function PartToXml(Zip: TZipFile; const PartName: string): IXMLDocument;
var
Bytes: TBytes;
begin
Zip.Read(PartName, Bytes);
Result := LoadXMLData(TEncoding.UTF8.GetString(Bytes));
end;
function Field(const Doc: IXMLDocument; const LocalName, Ns: string): string;
var
Node: IXMLNode;
begin
Node := Doc.DocumentElement.ChildNodes.FindNode(LocalName, Ns);
if Node <> nil then
Result := Node.Text
else
Result := '';
end;
procedure ReadXlsxProperties(const FileName: string);
var
Zip: TZipFile;
Doc: IXMLDocument;
begin
Zip := TZipFile.Create;
try
Zip.Open(FileName, zmRead);
if Zip.IndexOf('docProps/core.xml') >= 0 then
begin
Doc := PartToXml(Zip, 'docProps/core.xml');
Writeln('Title : ', Field(Doc, 'title', NsDC));
Writeln('Creator : ', Field(Doc, 'creator', NsDC));
Writeln('Modifier: ', Field(Doc, 'lastModifiedBy', NsCore));
Writeln('Modified: ', Field(Doc, 'modified', NsTerms)); // W3CDTF, UTC
end;
if Zip.IndexOf('docProps/app.xml') >= 0 then
begin
Doc := PartToXml(Zip, 'docProps/app.xml');
Writeln('Company : ', Field(Doc, 'Company', NsApp));
Writeln('App : ', Field(Doc, 'Application', NsApp), ' ',
Field(Doc, 'AppVersion', NsApp));
end;
finally
Zip.Free;
end;
end;
Δύο λεπτομέρειες κρατούν αυτό το πράγμα ανθεκτικό στην παραγωγή. Πρώτον, τα μέρη είναι προαιρετικά: ένα ελάχιστο πακέτο χωρίς καθόλου docProps είναι απολύτως έγκυρο κατά το ECMA-376, γι αυτό ο κώδικας ελέγχει με IndexOf αντί να υποθέτει. Δεύτερον, αντιστοιχίστε τα στοιχεία κατά τοπικό όνομα και URI namespace, όπως κάνει παραπάνω το FindNode, ποτέ κατά κυριολεκτικό πρόθεμα· τα dc: και cp: είναι συμβάσεις του writer του Excel, και τα αρχεία που παράγουν άλλες γεννήτριες είναι ελεύθερα να διαλέξουν διαφορετικά προθέματα. Μια σημείωση για το περιβάλλον: ο προεπιλεγμένος πάροχος του IXMLDocument είναι το MSXML, οπότε μια εφαρμογή κονσόλας ή ένα νήμα εργασίας πρέπει να καλέσει CoInitialize πριν από το LoadXMLData, αλλιώς η πρώτη ανάλυση πεθαίνει με σφάλμα COM
Ο πίνακας κόστους, και πότε μια βιβλιοθήκη νικά και τους δύο αναλυτές
Μετρημένος σε ένα συνηθισμένο μηχάνημα προγραμματιστή, ο δρόμος του COM προσγειώνεται περίπου στα δύο έως τέσσερα δευτερόλεπτα ανά αρχείο όταν η συνεδρία automation δημιουργείται ανά αρχείο, σχεδόν όλο αυτό εκκίνηση του EXCEL.EXE συν πλήρης ανάλυση του βιβλίου εργασίας, και απαιτεί εγκατεστημένο, αδειοδοτημένο Excel όπου κι αν τρέχει. Οι δύο άμεσοι δρόμοι διαβάζουν μόνο τους περιέκτες μεταδεδομένων, τελειώνουν σε μονοψήφια χιλιοστά του δευτερολέπτου ανά αρχείο, και δεν χρειάζονται τίποτα εγκατεστημένο πέρα από όσα ήδη συνδέει ένα εκτελέσιμο της Delphi. Σε ένα κοινόχρηστο φάκελο δέκα χιλιάδων αρχείων, αυτή είναι η διαφορά ανάμεσα στο μεγαλύτερο μέρος μιας εργάσιμης ημέρας και σε λιγότερο από ένα λεπτό, χωρίς κανένα ζήτημα ανάπτυξης του Office
Η παγίδα με τους άμεσους δρόμους είναι ότι είναι δύο. Μια ροή επεξεργασίας που δέχεται και τις δύο μορφές συντηρεί δύο αναλυτές με δύο ξένους μεταξύ τους τρόπους αστοχίας, κωδικοσελίδες και τύπους PROPVARIANT από τη μία πλευρά, namespaces και προαιρετικά μέρη από την άλλη, και κανένας δεν διαβάζει τη μορφή του άλλου. Αυτό το φορτίο συντήρησης είναι το επιχείρημα υπέρ μιας εγγενούς βιβλιοθήκης: το HotXLS, η βιβλιοθήκη υπολογιστικών φύλλων της losLab σε Object Pascal για Delphi και C++Builder στα Windows, εκθέτει τα ίδια πεδία ως απλές ιδιότητες βιβλίου εργασίας, Title, Author, Company, Created και τα υπόλοιπα, συμπληρωμένα από το Open τόσο για .xls όσο και για .xlsx, χωρίς εγκατάσταση Excel και χωρίς τίποτα από τις παραπάνω σωληνώσεις περιεκτών. Διαβάζει ιδιότητες ως μέρος ενός πλήρους ανοίγματος βιβλίου εργασίας και όχι ως έλεγχο μόνο μεταδεδομένων, οπότε ταιριάζει σε ροές που ούτως ή άλλως θα αγγίξουν στη συνέχεια τα δεδομένα των κελιών· η πλήρης επιφάνεια ιδιοτήτων και στις δύο όψεις, συμπεριλαμβανομένης της πλευράς εγγραφής, καλύπτεται στο άρθρο μας για τον ορισμό ιδιοτήτων εγγράφου Excel με το HotXLS
Σημείωση: πλήρη εργαλεία ανάλυσης Excel και εξαγωγής μεταδεδομένων είναι διαθέσιμα στο HotXLS Delphi VCL Component