Récemment, j'ai dû faire face à la nécessité d'obtenir du texte à partir de documents de bureau ( docx, xlsx, rtf , doc, xls, odt et ods ). La tùche a été compliquée par l'exigence de présenter le texte au format xml sans ordures avec la structure la plus pratique pour une analyse ultérieure.
La dĂ©cision d'utiliser Interop est immĂ©diatement tombĂ©e en raison de son encombrement, de la redondance Ă bien des Ă©gards et de la nĂ©cessitĂ© d'installer MS Office sur le serveur . En consĂ©quence, une solution a Ă©tĂ© trouvĂ©e et mise en Ćuvre sur un projet interne. Cependant, la recherche s'est avĂ©rĂ©e si compliquĂ©e et non triviale en raison de l'absence de manuels gĂ©nĂ©ralement accessibles que j'ai dĂ©cidĂ© d'Ă©crire une bibliothĂšque pendant mon temps libre qui rĂ©soudrait la tĂąche spĂ©cifiĂ©e, et de crĂ©er Ă©galement une sorte d'instruction Ă Ă©crire pour que les dĂ©veloppeurs lisent elle a pu, au moins superficiellement, comprendre le problĂšme.
Avant de passer à la description de la solution trouvée, je vous suggÚre de vous familiariser avec certaines des conclusions qui ont été tirées de mes recherches:
- Pour la plate-forme .Net, il n'y a pas de solution toute faite pour travailler avec tous les formats répertoriés, ce qui nous obligera à castrer notre solution à certains endroits.
- N'essayez pas de trouver un bon manuel pour travailler avec Microsoft OpenXML sur le réseau: pour gérer cette bibliothÚque, vous devrez avoir les yeux rouges, fumer StackOverflow et jouer avec le débogueur.
- Oui, j'ai quand mĂȘme rĂ©ussi Ă apprivoiser le dragon.
Je ferai immĂ©diatement une rĂ©servation pour le moment, la bibliothĂšque n'est pas encore prĂȘte, mais elle est en cours d'Ă©criture (autant que le temps libre le permet). Il est supposĂ© que des articles sĂ©parĂ©s pour chaque format seront Ă©crits et en parallĂšle, avec leur publication, le rĂ©fĂ©rentiel sur le github sera mis Ă jour, d'oĂč il sera possible d'obtenir les sources.
Travailler avec xlsx et docx
.xlsx
, , , docx xlsx zip-, xml. , : zip . , : \xl\worksheets
.
excel , , - , :

, , , ( <f>
) ( <v>
). , shared
sharedStrings.xml, \xl
.
: .
, -, IConvertable:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ConverterToXml.Converters
{
interface IConvertable
{
string Convert(Stream stream);
string ConvertByFile(String path);
}
}
, : string Convert(Stream stream)
( , - ), string ConvertByFile(String path)
.
XlsxToXml
, IConvertable
Nuget DocumentFormat.OpenXml ( , 2.10.0).
string SpreadsheetProcess(Stream memStream)
, string Convert(Stream stream)
.
public string Convert(Stream memStream)
{
return SpreadsheetProcess(memStream);
}
, *string SpreadsheetProcess(Stream memStream)*
:
string SpreadsheetProcess(Stream memStream)
{
using (SpreadsheetDocument doc = SpreadsheetDocument.Open(memStream, false))
{
memStream.Position = 0;
StringBuilder sb = new StringBuilder(1000);
sb.Append("<?xml version=\"1.0\"?><documents><document>");
SharedStringTable sharedStringTable = doc.WorkbookPart.SharedStringTablePart.SharedStringTable;
int sheetIndex = 0;
foreach (WorksheetPart worksheetPart in doc.WorkbookPart.WorksheetParts)
{
WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);
sheetIndex++;
}
sb.Append(@"</document></documents>");
return sb.ToString();
}
}
, string SpreadsheetProcess(Stream memStream)
:
using
excel . xlsx DocumentFormat.OpenXml SpreadsheetDocument.
StringBuilder sb ( 1000 . StringBuilder , . , , .
shared ( ). , SpreadsheetDocument :
SharedStringTable sharedStringTable = doc.WorkbookPart.SharedStringTablePart.SharedStringTable
.
,
foreach (WorksheetPart worksheetPart in doc.WorkbookPart.WorksheetParts)
{
WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);
sheetIndex++;
}
WorkSheetProcess(sb, sharedStringTable, worksheetPart, doc, sheetIndex);
:
private void WorkSheetProcess(StringBuilder sb, SharedStringTable sharedStringTable, WorksheetPart worksheetPart, SpreadsheetDocument doc,
int sheetIndex)
{
string sheetName = doc.WorkbookPart.Workbook.Descendants<Sheet>().ElementAt(sheetIndex).Name.ToString();
sb.Append($"<sheet name=\"{sheetName}\">");
foreach (SheetData sheetData in worksheetPart.Worksheet.Elements<SheetData>())
{
if (sheetData.HasChildren)
{
foreach (Row row in sheetData.Elements<Row>())
{
RowProcess(row, sb, sharedStringTable);
}
}
}
sb.Append($"</sheet>");
}
, :
string sheetName = doc.WorkbookPart.Workbook.Descendants<Sheet>().ElementAt(sheetIndex).Name.ToString();
, , . , . , , shift+F9( ), doc
( )->WorkbookPart->Workbook Descendants(), Sheet
. , ( ). :
foreach
, . sheetData
- , , RowProcess
:
foreach (SheetData sheetData in worksheetPart.Worksheet.Elements<SheetData>())
{
if (sheetData.HasChildren)
{
foreach (Row row in sheetData.Elements<Row>())
{
RowProcess(row, sb, sharedStringTable);
}
}
}
void RowProcess(Row row, StringBuilder sb, SharedStringTable sharedStringTable)
:
void RowProcess(Row row, StringBuilder sb, SharedStringTable sharedStringTable)
{
sb.Append("<row>");
foreach (Cell cell in row.Elements<Cell>())
{
string cellValue = string.Empty;
sb.Append("<cell>");
if (cell.CellFormula != null)
{
cellValue = cell.CellValue.InnerText;
sb.Append(cellValue);
sb.Append("</cell>");
continue;
}
cellValue = cell.InnerText;
if (cell.DataType != null && cell.DataType == CellValues.SharedString)
{
sb.Append(sharedStringTable.ElementAt(Int32.Parse(cellValue)).InnerText);
}
else
{
sb.Append(cellValue);
}
sb.Append("</cell>");
}
sb.Append("</row>");
}
foreach (Cell cell in row.Elements<Cell>())
:
if (cell.CellFormula != null)
{
cellValue = cell.CellValue.InnerText;
sb.Append(cellValue);
sb.Append("</cell>");
continue;
}
, , (cellValue = cell.CellValue.InnerText;
) .
, , shared: , :
if (cell.DataType != null && cell.DataType == CellValues.SharedString)
{
sb.Append(sharedStringTable.ElementAt(Int32.Parse(cellValue)).InnerText);
}
, .
.docx
, word excel-.
, , , , , , . , , .., , , , - , , .
, . zip . . word document. , , , , . , : - .
, w:t, w:r, w:p. , docx, . : , w:numPr, (w:ilvl) id , (w:numId).

, , , , ( , ), , id , , .
, , :

, . w:tr () w:tc().

Avant de commencer Ă coder, je veux faire attention Ă une nuance trĂšs importante (oui, comme dans la blague sur Petka et Vasily Ivanovich). Lors de l'analyse de listes, en particulier lorsqu'il s'agit de listes imbriquĂ©es, une situation peut se produire lorsque les Ă©lĂ©ments de liste sont sĂ©parĂ©s par une sorte d'insertion de texte, d'image ou de toute autre chose. Alors la question se pose, quand met-on la balise de fermeture de la liste? Ma suggestion, qui sent la bĂ©quille et la construction de vĂ©los, revient Ă ajouter un dictionnaire, dont les clĂ©s seront l'identifiant des listes, et la valeur correspondra Ă l'identifiant du paragraphe (oui, il s'avĂšre que chaque paragraphe du document a son propre identifiant unique), qui est Ă©galement le dernier d'une liste. C'est peut-ĂȘtre Ă©crit assez difficile, mais je pense que lorsque vous regardez la mise en Ćuvre, cela deviendra un peu plus clair:public string Convert(Stream memStream)
{
Dictionary<int, string> listEl = new Dictionary<int, string>();
string xml = string.Empty;
memStream.Position = 0;
using (WordprocessingDocument doc = WordprocessingDocument.Open(memStream, false))
{
StringBuilder sb = new StringBuilder(1000);
sb.Append("<?xml version=\"1.0\"?><documents><document>");
Body docBody = doc.MainDocumentPart.Document.Body;
CreateDictList(listEl, docBody);
foreach (var element in docBody.ChildElements)
{
string type = element.GetType().ToString();
try
{
switch (type)
{
case "DocumentFormat.OpenXml.Wordprocessing.Paragraph":
if (element.GetFirstChild<ParagraphProperties>() != null)
{
if (element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val != CurrentListID)
{
CurrentListID = element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
sb.Append($"<li id=\"{CurrentListID}\">");
InList = true;
ListParagraph(sb, (Paragraph)element);
}
else
{
ListParagraph(sb, (Paragraph)element);
}
if (listEl.ContainsValue(((Paragraph)element).ParagraphId.Value))
{
sb.Append($"</li id=\"{element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val}\">");
}
continue;
}
else
{
SimpleParagraph(sb, (Paragraph)element);
continue;
}
case "DocumentFormat.OpenXml.Wordprocessing.Table":
Table(sb, (Table)element);
continue;
}
}
catch (Exception e)
{
continue;
}
}
sb.Append(@"</document></documents>");
xml = sb.ToString();
}
return xml;
}
Dictionary<int, string> listEl = new Dictionary<int, string>();
â .
using (WordprocessingDocument doc = WordprocessingDocument.Open(memStream, false))
â doc
WordprocessingDocument, word, ( , OpenXML) .
StringBuilder sb = new StringBuilder(1000);
â xml.
Body docBody = doc.MainDocumentPart.Document.Body;
â ,
CreateDictList(listEl, docBody);
, foreach , :
void CreateDictList(Dictionary<int, string> listEl, Body docBody)
{
foreach(var el in docBody.ChildElements)
{
if(el.GetFirstChild<ParagraphProperties>() != null)
{
int key = el.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
listEl[key] = ((DocumentFormat.OpenXml.Wordprocessing.Paragraph)el).ParagraphId.Value;
}
}
}
GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
â ( https://docs.microsoft.com/ru-ru/office/open-xml/open-xml-sdk ), . , , , )
, , foreach . : . , , . , (, ) , . , . :
string type = element.GetType().ToString();
try
{
switch (type)
{
case "DocumentFormat.OpenXml.Wordprocessing.Paragraph":
if (element.GetFirstChild<ParagraphProperties>() != null)
{
if (element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val != CurrentListID)
{
CurrentListID = element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
sb.Append($"<li id=\"{CurrentListID}\">");
InList = true;
ListParagraph(sb, (Paragraph)element);
}
else
{
ListParagraph(sb, (Paragraph)element);
}
if (listEl.ContainsValue(((Paragraph)element).ParagraphId.Value))
{
sb.Append($"</li id=\"{element.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val}\">");
}
continue;
}
else
{
SimpleParagraph(sb, (Paragraph)element);
continue;
}
case "DocumentFormat.OpenXml.Wordprocessing.Table":
Table(sb, (Table)element);
continue;
}
}
try-catch
, - , switch-case
( , , ). , - , .
, ListParagraph(sb, (Paragraph)element);
:
void ListParagraph(StringBuilder sb, Paragraph p)
{
var level = p.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingLevelReference>().Val;
var id = p.GetFirstChild<ParagraphProperties>().GetFirstChild<NumberingProperties>().GetFirstChild<NumberingId>().Val;
sb.Append($"<ul id=\"{id}\" level=\"{level}\"><p>{p.InnerText}</p></ul id=\"{id}\" level=\"{level}\">");
}
<ul>
, id .
, , SimpleParagraph(sb, (Paragraph)element);
:
void SimpleParagraph(StringBuilder sb, Paragraph p)
{
sb.Append($"<p>{p.InnerText}</p>");
}
, <p>
Le tableau est traité dans la méthode Table(sb, (Table)element);
:
void Table(StringBuilder sb, Table table)
{
sb.Append("<table>");
foreach (var row in table.Elements<TableRow>())
{
sb.Append("<row>");
foreach (var cell in row.Elements<TableCell>())
{
sb.Append($"<cell>{cell.InnerText}</cell>");
}
sb.Append("</row>");
}
sb.Append("</table>");}
Le traitement d'un tel élément est assez trivial: nous lisons les lignes, les divisons en cellules, prenons des valeurs dans les cellules, les enveloppons dans des balises <cell>
, que nous emballons dans des balises <row>
et mettons tout cela à l'intérieur <table>
.
Sur ce point, je propose de considérer la tùche comme résolue pour les documents au format docx et xlsx.
Le code source peut ĂȘtre consultĂ© dans le rĂ©fĂ©rentiel sur le lien
Article de conversion RTF