posts - 644, comments - 2003, trackbacks - 137

My Links

News

Raffaele Rialdi website

Su questo sito si trovano i miei articoli, esempi, snippet, tools, etc.

Archives

Post Categories

Image Galleries

Blogs

Links

Leggere docx (Office OpenXML) via Linq

Tra le maggiori critiche che sono circolate sul formato ISO DIS29500, conosciuto meglio come Office OpenXML, sono quelle della complessità.

È assolutamente vero che le specifiche sono molto corpose (il reference supera le 5200 pagine) ma questo non implica automaticamente il concetto di complessità.

I formati di Office OpenXML (docx, pptx, xlsx, ...) sono dei file zip (basta rinominarli per verificarlo) e la struttura interna dei dati segue il formato OPC (Open Packaging Conventions) che è usato anche dal formato XPS. Internamente allo zip ci sono una serie di file che determinano il contenuto del documento, dei file di "relazioni" che stabiliscono le dipendenze di un file da un altro (per esempio le immagini in un documento), e altri file come le proprietà del documento (autore, titolo, etc.) e quello che specifica i tipi mime contenuti nei file.

A seconda se il documento è di tipo word processing o spreadsheet, il tipo di file contenuti nello zip varia. Parlo di file in formato xml che seguono lo schema indicato nelle specifiche Office OpenXML.

Il primo dato di fatto è che qualsiasi sistema di sviluppo ha le librerie per leggere il contenuto di uno zip e fare il parsing di XML, quindi non ci sono requisiti particolari. Il .NET Framework però fornisce un supporto nativo per OPC e quindi è possibile ottenere uno stream al file xml contenuto dentro un Package grazie alla classe System.IO.Packaging.ZipPackage.

La versione 1.0 dell'SDK di Office OpenXML semplifica ulteriormente la gestione del formato OPC fornendo dei wrapper specializzati sulle parti che sono utilizzate da questo formato. In futuro questo SDK dovrebbe fornire sempre maggiore supporto ma al momento si limita solo alla gestione (lettura e scrittura) del formato OPC e delle parti di Office OpenXML.

Referenziando la "DocumentFormat.OpenXML.dll" possiamo leggere un wordprocessing document in questo modo:

   1: using (WordprocessingDocument doc = WordprocessingDocument.Open(_FileName, true))
   2: {
   3:     MainDocumentPart mainPart = doc.MainDocumentPart;
   4:  
   5:     using (StreamReader streamReader = new StreamReader(mainPart.GetStream()))
   6:     {
   7:         using (XmlReader stream = XmlReader.Create(streamReader))
   8:         {
   9:             _MainDocument = XElement.Load(stream);
  10:         }
  11:     }
  12: }

In questo snippet, _FileName è il nome del file docx e _MainDocument è un XElement in cui viene caricato tutto il documento in memoria. Il grosso vantaggio di questo approccio è che non è necessario estrarre i file dallo zip ma con GetStream si ottiene direttamente uno stream all'xml contenuto nel Package.

XElement è una preziosa classe del Framework 3.5 che semplifica in modo disarmante la gestione di documenti XML. Un passo avanti enorme comparato al classico DOM, a XmlReader o al vecchio Sax.

A questo punto dentro _MainDocument c'è il documento principale in memoria. Gli header e footer sono custoditi in un file xml separato e quindi volendo fare operazioni anche su di essi, dovremo caricarlo in un altro XElement.

Ora leggiamo il documento docx grazie a Linq.

Supponendo a questo punto di voler recuperare le parole formattate in Bold in un documento, possiamo farlo con una classica query XPath oppure con una molto più semplice (e soprattutto strong-typed) query Linq:

   1: public IEnumerable<string> GetBoldWords()
   2: {
   3:     IEnumerable<string> words =
   4:         from b in _MainDocument.Descendants(NS_w + "body")
   5:         from p in b.Elements(NS_w + "p")
   6:         from r in p.Elements(NS_w + "r")
   7:  
   8:         where r.Descendants(NS_w + "rPr").Descendants(NS_w + "b").Count() == 1
   9:  
  10:         select (string)r.Descendants(NS_w + "t").First();
  11:  
  12:     return words;
  13: }

NS_w è il namespace in cui sono definiti i tag body, p, r, rPr, b, t:

   1: public static XNamespace NS_w = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");

Se guardiamo il file xml del documento wordprocessing:

   1: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
   2: <w:document
   3:     xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006"
   4:     xmlns:o="urn:schemas-microsoft-com:office:office"
   5:     xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
   6:     xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
   7:     xmlns:v="urn:schemas-microsoft-com:vml"
   8:     xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
   9:     xmlns:w10="urn:schemas-microsoft-com:office:word"
  10:     xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  11:     xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
  12:   <w:body>
  13:     <w:p w:rsidR="00BC037B" w:rsidRPr="00410511" w:rsidRDefault="00410511">
  14:       <w:r>
  15:         <w:t>Esempio di formattazione</w:t>
  16:       </w:r>
  17:       <w:r>
  18:         <w:t xml:space="preserve">: </w:t>
  19:       </w:r>
  20:       <w:r>
  21:         <w:rPr>
  22:           <w:b/>
  23:         </w:rPr>
  24:         <w:t>Bold</w:t>
  25:       </w:r>
  26: ...

Quello che otteniamo è una collection di stringhe (words) prese dentro il tag <w:t> in cui esista il tag <w:b/> dentro <w:rPr>

   1: WordHelper wh = new WordHelper(WordFileName);
   2: var words = wh.GetBoldWords();            // IEnumerable<string>
   3: foreach(string word in words)
   4:     Console.WriteLine(word);

Supponendo di avere scritto il codice precedente in una classe di nome WordHelper, una volta ottenute le stringhe dal metodo GetBoldWords non faccio altro che enumerarle tramite un semplice foreach.

Due parole sull'elaborazione parallela

Come ho potuto mostrare ai Community Days, il Parallel FX permette, tra le tante cose, di parallelizzare le query Linq aggiungendo "AsParallel" alle query.
Dalle prove che ho fatto, con tutti i limiti della CTP di Giugno che ho usato, il vero guadagno nella parallelizzazione non è tanto sull'operazione di filtro (where) ma sull'eventuale elaborazione della select (assente nel metodo GetBoldWords in quanto non necessaria).

Perciò la regola è sempre la stessa, prima di mettere AsParallel come fosse prezzemolo, misurate, misurate, misurate.

In conclusione

La quantità di codice è veramente minimale. Senza neppure estrarre lo zip su disco si legge il documento wordprocessing in memoria e si esegue la query sul documento.

Spazio quindi alla fantasia perché Linq non pone alcun limite a quello che possiamo leggere sul documento XML.

Per lo spreadsheet la cosa è leggermente più complessa ma se interessa ho già pronto un esempio analogo a quello che ho scritto per il wordprocessing.

Print | posted on sabato 12 luglio 2008 16:45 |

Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET