Algorytm porządkowania danych z tabel dokumentów Word zawierających scalenia

1 Wstęp

Artykuł powstał na bazie rzeczywistego programu i wykorzystywanego w nim algorytmu.

W artykule opisano sposób porządkowania danych z tabel dokumentu Word, zawierających scalenia komórek. Sposobu można użyć jako jednego z etapów przetwarzania danych.

Aby algorytm był bardziej ogólny, dane umieszczono w sekwencji tabel, których zawartość jest niejako łączona w jedną tabelę.

Układ kolumn we wszystkich tabelach opisuje nagłówek dokumentu, będący jednowierszową tabelą.

Przyjęto że porządkowane są kolejne, pojedyncze wiersze i nie występują różnice w szerokości kolumn między tabelą nagłówkową i pozostałymi tabelami oraz, że nie występują wiersze puste.

Gdyby w wierszach znajdowały kolejne komórki tabeli o układzie zgodnym z układem kolumn w nagłówku dokumentu bez scaleń, tj. o jednakowej ilości z ilością kolumn w nagłówku, wydobycie uporządkowanych danych np. w celu umieszczenia ich w rekordach bazy danych przeznaczonych do dalszego przetwarzania byłoby proste.

Istnieje jednak pewien problem (zaczerpnięty z rzeczywistego programu) – komórki w wierszach często są scalane, przez co zmienia się ich ilość względem nagłówka i ich indeksy. Nie jest to trudne do odczytania dla człowieka, jednak dla programu komputerowego stanowi utrudnienie. Taka sytuacja jest dozwolona.

W algorytmie może zastanawiać operowanie na szerokościach width komórek w celu określenia ich indeksów, o czym dalej, a nie na atrybucie html colspan stosowanym przy połączeniach komórek tabeli w poziomie, jednoznacznie wyznaczającym indeks. Otóż, w rzeczywistym programie, potrzebne są również szerokości komórek, co jest wykorzystywane na dalszych etapach przetwarzania do określania wielkości pól tekstowych w szablonach. W punkcie 1.b podano link do przykładu omawiającego atrybut colspan.

Scalenia sąsiadujących ze sobą komórek wierszy nie są dozwolone. Jest to wykrywane przez szukanie w kodzie HTML tekstu rowspan.

Częściowo użyto techniki referencji do bibliotek COM, a częściowo parsera dokumentu zapisanego jako przefiltrowany HTML na etapie, na którym użycie COM było zbyt czasochłonne, co wykazały pomiary czasu porządkowania danych.

Przyjęto, że system Windows działa przy typowych ustawieniach regionalnych dla języka polskiego.

W artykule zawarto tylko kod klas realizujących porządkowanie.

Przykładowy projekt z pełnym kodem umieszczono w załączniku do artykułu.

1.a Zastosowanie

Algorytm może znaleźć zastosowanie w zadaniach kompresji danych tabelarycznych z uwzględnieniem szerokości pól (komórek tabel), lub np. ich długości liczonej w znakach.

Samo przedstawianie tabel w HTML jest już takim rozwiązaniem, gdzie u podstawy leży szybkość transmisji danych, która jest odwrotnie proporcjonalna – w najprostszym ujęciu – do ilości danych do przesłania, co narzuca sens stosowania kompresji.

1.b Literatura uzupełniająca

Colspan – Łączenie Kolumnami, Kurs Html5, how2html.pl

2 Metoda użyta do uporządkowania danych

Wraz ze scaleniem sąsiadujących komórek wiersza zmienia się ilość komórek w wierszu oraz indeksy komórek następujących za scaleniem, których pierwotne wartości są potrzebne do jednoznacznego i trafnego przypisania tekstów w komórkach do indeksów kolumn tabeli nagłówkowej. Zmienia się też szerokość pierwszej ze scalonych komórek i przyjmuje wartość sumy szerokości pierwotnie pojedynczych komórek.

Po scaleniu tracimy informację o ilości scalonych komórek , jednak mamy inną informację, tj. szerokości i indeksy pojedynczych komórek tabeli nagłówkowej oraz szerokość komórki scalonej. Stąd wyznaczamy pierwotne indeksy, a stąd konieczne przesunięcie (zwiększenie) wartości indeksów komórek za scaleniem.

Dlatego dodatkowo oprócz tekstu w komórce odczytywane są szerokości komórek.

Do wyznaczenia różnicy indeksów przyjęto kryterium ograniczające z góry szerokość scalonej komórki do pewnej granicznej wartości, której przekroczenie powoduje zwiększenie przesunięcia wartości indeksu komórki przy powrocie do pierwotnych indeksów o jeden.

Sposób wyznaczania granicy szerokości komórki scalonej pokazano na rysunku.

limes.png
Rys 1. Granica szerokości komórki scalonej.

3 Klasa VTable – kod C#

Na wszystkich etapach algorytmu wykorzystywana jest klasa, którą nazwano VTable. Klasa operuje na liście tablic jednowymiarowych o jednakowych rozmiarach i stanowi odwzorowanie listy wierszy tabel dokumentu Word.

Umożliwia ona wizualizację działania algorytmu w komponentach DataGridView, aczkolwiek ich użycie nie jest konieczne, choć jest dość przydatną, pewną formą – można to tak nazwać – debuggera lepiej prezentującego pobrane oraz później przetworzone dane i ich układ, niż klasyczny debugger.

Po sprawdzeniu działania algorytmu można zrezygnować z komponentów DataGridView, które domyślnie są parametrami null i mieć gotową część programu.

W artykule wykorzystano prezentację układu danych pobranych z dokumentu Word przed uporządkowaniem i po uporządkowaniu (zrzuty ekranu).

Aby czytelnik nie był zaskoczony nieznanymi obiektami, a także ze względu na – jak się wydaje – ich użyteczność i możliwość wielorakiego zastosowania, klasa VTable zostanie tu omówiona na początku.

Klasa VTable – część główna

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace VTableNamespace
{
    /// <summary>
    /// Klasa VTable używana do przechowywania, przetwarzania 
    /// i prezentacji danych pobranych z tabel dokumentu Word
    /// </summary>
    public partial class VTable
    {
        /// <summary>Pole - lista tablic</summary>
        private List<Array> Rows;

        /// <summary>Pole - komponent DataGridView</summary>
        private DataGridView Grid;

        /// <summary>Pole - rozmiar tablicy</summary>
        private int Size;


        /// <summary>Konstruktor klasy VTable</summary>
        /// <param name="columnCount">Ilość elementów tablicy klasy Array</param>
        /// <param name="grid">Opcjonalny komponent DataGridView do prezentacji danych</param>
        public VTable(int columnCount, DataGridView grid = null)
        {
            Rows = new List<Array>();
            Grid = grid;
            
            if (Grid != null)
            {
                Grid.AllowUserToAddRows = false;
                Grid.AllowUserToDeleteRows = false;
            }
            
            Size = columnCount;
        }
        
        /// <summary>
        /// Własność umożliwiająca zmianę wartości elementu klasy Value
        /// lub jej pobranie, gdzie Value jest elementem tablicy klasy Array
        /// znajdującym się na liście tablic tej klasy, pole Rows
        /// </summary>
        /// <param name="colIdx">Indeks elementu tablicy</param>
        /// <param name="rowIdx">Indeks elementu listy</param>
        /// <returns>Element klasy Value</returns>
        public Value this[int colIdx, int rowIdx]
        {
            set 
            {
                if (rowIdx >= 0 && rowIdx < Rows.Count)
                {
                    Rows[rowIdx][colIdx] = value;
                } 
                else 
                {
                    throw new Exception(GetType().Name + " row index error");
                } 
            }
            
            get 
            {
                if (rowIdx >= 0 && rowIdx < Rows.Count)
                {
                    return Rows[rowIdx][colIdx];
                }
                else
                {
                    throw new Exception(GetType().Name + " row index error");
                }
            }
        }
        
        /// <summary>
        /// Własność umożliwiająca zmianę i pobranie ilości elementów listy
        /// zawierającej elementy klasy Array
        /// </summary>
        /// <returns>Ilość elementów listy</returns>
        public int RowCount
        {
            set
            {
                if (value < Rows.Count)
                {
                    Rows.RemoveRange(value, Rows.Count - value);
                }
                else
                {
                    while (value > Rows.Count)
                    {
                        Rows.Add(new Array(Size));
                    }
                }
            }
            
            get 
            { 
                return Rows.Count; 
            }
        }
        
        /// <summary>
        /// Własność tylko do odczytu umożliwiająca pobranie rozmiaru tablic 
        /// klasy Array, która jest parametrem konstruktora klasy VTable
        /// </summary>
        /// <returns>Ilość elementów tablicy</returns>
        public int ColumnCount
        {
            get 
            { 
                return Size; 
            }
        }

    }
}

Klasa VTable.Array

using System;

namespace VTableNamespace
{
    public partial class VTable
    {
        /// <summary>
        /// Tablica zawierająca elementy klasy Value, której rozmiar jest ustalany w konstruktorze klasy VTable
        /// i jest wspólny dla wszystkich tablic klasy Array używanych przez klasę VTable
        /// </summary>
        class Array
        {
            /// <summary>Pole - rozmiar tablicy</summary>
            private int Size;

            /// <summary>Pole - tablica</summary>
            private Value[] A;

            /// <summary>Konstruktor klasy Array</summary>
            /// <param name="size">Rozmiar tablicy</param>
            public Array(int size)
            {
                Size = size;
                A = new Value[Size];

                for (int colIdx = 0; colIdx < Size; colIdx++)
                {
                    A[colIdx] = new Value();
                }
            }

            /// <summary>Własność umożliwiająca zmianę i pobranie elementu tablicy</summary>
            /// <param name="colIdx">Indeks elementu klasy Value w tablicy</param>
            /// <returns>Element tablicy</returns>
            public Value this[int colIdx]
            {
                set
                {
                    if (colIdx >= 0 && colIdx < Size)
                    {
                        A[colIdx] = value;
                    }
                    else
                    {
                        throw new Exception(GetType().DeclaringType.Name + " column index error");
                    }
                }

                get
                {
                    if (colIdx >= 0 && colIdx < Size)
                    {
                        return A[colIdx];
                    }
                    else
                    {
                        throw new Exception(GetType().DeclaringType.Name + " column index error");
                    }
                }
            }
        }
    }
}

Klasa VTable.Value

namespace VTableNamespace
{
    public partial class VTable
    {
        /// <summary>Struktura przechowująca dane pobrane z tabeli dokumentu Word</summary>
        public struct Record
        {
            /// <summary>Pole typu string o dostępie przez AsStr</summary>
            public string s;

            /// <summary>Pole typu int o dostępie przez AsInt</summary>
            public int i;
        }

        /// <summary>Klasa Value, podstawowy element używany przez klasę VTable</summary>
        public class Value
        {
            /// <summary>Pole - struktura typu Record</summary>
            private Record record;

            /// <summary>Konstruktor klasy Value</summary>
            public Value()
            {
                record.s = "";
                record.i = 0;
            }

            /// <summary>Własność do zmiany i pobierania pola struktury Record</summary>
            /// <returns>Pole s typu string</returns>
            public string AsStr
            {
                set { record.s = value; }
                get { return record.s; }
            }

            /// <summary>Własność do zmiany i pobierania pola struktury Record</summary>
            /// <returns>Pole i typu int</returns>
            public int AsInt
            {
                set { record.i = value; }
                get { return record.i; }
            }
        }
    }
}

Metody specyficzne dla algorytmu

using System.Collections.Generic;

namespace VTableNamespace
{
    public partial class VTable
    {
        /// <summary>Metoda zmieniająca położenie elementu Value w tablicy Array</summary>
        /// <param name="colFrom">Pierwotny indeks elementu w tablicy</param>
        /// <param name="colTo">Nowy indeks elementu w tablicy</param>
        /// <param name="row">Indeks tablicy na liście</param>
        public void ShiftFromTo(int colFrom, int colTo, int row)
        {
            this[colTo, row].AsStr = this[colFrom, row].AsStr;
            this[colFrom, row].AsStr = "";
            this[colTo, row].AsInt = this[colFrom, row].AsInt;
            this[colFrom, row].AsInt = 0;
        }

        /// <summary>
        /// Metoda przesuwająca elementy tablicy klasy Array
        /// najpierw tworząca listę indeksów ostatnich w tablicy elementów klasy Value
        /// które są typu string o długości zero,
        /// a następnie przesuwająca je począwszy od ostatnich indeksów w stronę indeksów wyższych
        /// </summary>
        /// <param name="widthsMatrix">
        /// Macierz pozycjonująca, zawierająca sumy szerokości dla poszczególnych indeksów elementów Value
        /// oraz ilości scalonych komórek tabeli Word wyliczone przez metodę SumOfWidths klasy TableHeader
        /// </param>
        public void ShiftCells(VTable widthsMatrix)
        {
            List<int> EmptyLast = new List<int>();

            for (int rowIdx = 0; rowIdx < this.RowCount; rowIdx++)
            {
                if (this[ColumnCount - 1, rowIdx].AsStr == "")
                {
                    EmptyLast.Add(rowIdx);
                }
            }

            foreach (int rowIdx in EmptyLast)
            {
                for (int colIdx = 0; colIdx < this.ColumnCount - 2; colIdx++)
                {
                    if (this[colIdx, rowIdx].AsStr != "")
                    {
                        int shiftRight = 0;
                        int matrixRowIdx = 0;

                        while (this[colIdx, rowIdx].AsInt > widthsMatrix[colIdx, colIdx + matrixRowIdx].AsInt)
                        {
                            matrixRowIdx++;
                            shiftRight++;
                        }

                        if (shiftRight > 0)
                        {
                            for (int overWidthColIdx = this.ColumnCount - 2; overWidthColIdx > colIdx; overWidthColIdx--)
                            {
                                int newColIdx = overWidthColIdx + shiftRight;

                                if (newColIdx < this.Size)
                                {
                                    this.ShiftFromTo(overWidthColIdx, newColIdx, rowIdx);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Metoda VTable.Show

using System.Drawing;

namespace VTableNamespace
{
    /// <summary>
    /// Pomocnicze typy określające sposób prezentacji danych 
    /// przez metodę Show w oparciu o strukturę Record i klasę Value
    /// </summary>
    public enum VTypes
    {
        /// <summary>
        /// Prezentuj dane używając pojedynczych wierszy DataGridView
        /// i własności klasy Value AsInt
        /// </summary>
        Int,

        /// <summary>
        /// Prezentuj dane używając podwójnych wierszy DataGridView
        /// i własności klasy Value AsStr, AsInt
        /// </summary>
        Rec
    }

    public partial class VTable
    {
        /// <summary>Metoda prezentująca dane w komponencie DataGridView</summary>
        /// <param name="t">Typ określający sposób prezentacji</param>
        /// <param name="indexBase">
        /// Sposób numeracji wierszy, domyślnie od jeden, w przypadku typu VTypes.Rec co drugi wiersz
        /// </param>
        public void Show(VTypes t, int indexBase = 1)
        {
            if (Grid != null)
            {
                Grid.Rows.Clear();
                
                if (t == VTypes.Rec)
                {
                    Grid.RowCount = RowCount * 2;
                }
                else
                {
                    Grid.RowCount = RowCount;
                }
                
                Grid.ColumnCount = ColumnCount;

                for (int rowIdx = 0; rowIdx < Rows.Count; rowIdx++)
                {
                    for (int colIdx = 0; colIdx < ColumnCount; colIdx++)
                    {
                        if (t == VTypes.Int)
                        {
                            int i = this[colIdx, rowIdx].AsInt;

                            if (i == 0)
                            {
                                Grid[colIdx, rowIdx].Value = "";
                            }
                            else
                            {
                                Grid[colIdx, rowIdx].Value = i;
                            }
                            
                            Grid[colIdx, rowIdx].Style.BackColor = Color.Yellow;
                        }
                        else
                        {
                            Grid[colIdx, rowIdx * 2].Value = this[colIdx, rowIdx].AsStr;
                            
                            int i = this[colIdx, rowIdx].AsInt;

                            if (this[colIdx, rowIdx].AsInt == 0)
                            {
                                Grid[colIdx, rowIdx * 2 + 1].Value = "";
                            }
                            else
                            {
                                Grid[colIdx, rowIdx * 2 + 1].Value = i;
                            }
                            
                            Grid[colIdx, rowIdx * 2 + 1].Style.BackColor = Color.Yellow;
                        }

                        if (Grid.ColumnHeadersVisible)
                        {
                            Grid.Columns[colIdx].HeaderText = (colIdx + indexBase).ToString();
                        }
                    }

                    if (Grid.RowHeadersVisible)
                    {
                        if (t == VTypes.Rec)
                        {
                            Grid.Rows[rowIdx * 2].HeaderCell.Value = (rowIdx + indexBase).ToString();
                        }
                        else
                        {
                            Grid.Rows[rowIdx].HeaderCell.Value = (rowIdx + indexBase).ToString();
                        }
                    }
                }
            }
        }
    }
}

4 Klasa TableHeader – kod C#

Statyczna metoda klasy TableHeader (GetHeader) wczytuje nagłówek dokumentu Word z użyciem referencji COM w celu utworzenia macierzy pozycjonującej, służącej do określania przesunięć indeksów komórek następujących po scaleniu. Pozycjonowanie używa kryterium opisanego w punkcie 2. i pokazanego na rysunku 1. Metoda GetHeader jest używana na początku algorytmu

using System;
using System.Windows.Forms;
using VTableNamespace;
using Word = Microsoft.Office.Interop.Word;

namespace TableStructurer
{
    /// <summary>
    /// Klasa do pobierania nagłówka dokumentu Word, oraz wartości i szerokości komórek tego nagłówka,
    /// a także tworzenia macierzy pozycjonującej z wykorzystaniem metody SumOfWidths
    /// <remarks>
    /// Klasa wykorzystuje bibliotekę COM: Microsoft Word 14.0 Object Library,
    /// którą należy dodać do referencji projektu (w przypadku programów MS Word innych niż wersja 2010
    /// numer wersji biblioteki może się różnić)
    /// </remarks>
    /// </summary>
    class TableHeader
    {
        private const char Tab = (char)7;
        private const char Space = (char)32;
        
        /// <summary>Pole - aplikacja MS Word</summary>
        private static Word.Application word;

        /// <summary>Pole - dokument MS Word</summary>
        private static Word.Document doc;

        /// <summary>Pole - nagłówek dokumentu</summary>
        private static VTable THeader;

        /// <summary>Pole - macierz pozycjonująca</summary>
        private static VTable TMatrix;

        /// <summary>Pole - ilość kolumn tabeli w nagłówku</summary>
        private static int ColumnCount;


        /// <summary>Metoda pobierająca nagłówek dokumentu Word
        /// <remarks>
        /// Przyjęto ze nagłówek to jednowierszowa tablica zawierająca minimum trzy komórki
        /// gdyż dla mniejszej ilości komórek nawet w przypadku scaleń w tabelach
        /// nie ma potrzeby zmiany ich indeksów
        /// przy porządkowaniu danych w celu dalszego przetwarzania
        /// </remarks>
        /// </summary>
        /// <param name="aWord">Aplikacja MS Word powiązana przez referencje COM</param>
        /// <param name="aDoc">Dokument MS Word otwierany przez aplikację</param>
        /// <param name="tHeader">
        /// Tworzony przez metodę obiekt klasy VTable zawierający
        /// wartości i szerokości komórek nagłówka dokumentu Word
        /// </param>
        /// <param name="tMatrix">
        /// Tworzona przez metodę macierz pozycjonująca klasy VTable
        /// zawierająca granice dla szerokości komórek dokumentu tabeli Word
        /// pozwalające na ustalenie, czy i o ile należy zmienić indeksy komórek wierszy
        /// przy porządkowaniu danych
        /// </param>
        /// <param name="gridHeader">
        /// Opcjonalny komponent DataGridView prezentujący nagłówek dokumentu
        /// </param>
        /// <param name="gridMatrix">
        /// Opcjonalny komponent DataGridView prezentujący macierz pozycjonującą
        /// </param>
        /// <returns>True w przypadku powodzenia, False w przypadku niepowodzenia metody</returns>
        public static bool GetHeader(
            Word.Application aWord,
            Word.Document aDoc,
            ref VTable tHeader,
            ref VTable tMatrix,
            DataGridView gridHeader = null,
            DataGridView gridMatrix = null
            )
        {
            word = aWord;
            doc = aDoc;
            THeader = tHeader;
            TMatrix = tMatrix;

            Word.HeaderFooter header = doc.Sections[1].Headers[Word.WdHeaderFooterIndex.wdHeaderFooterPrimary];
            
            if (header.Range.Tables.Count != 1)
            {
                MessageBox.Show("Nagłówek musi zawierać jedną tabelę");
                return false;
            }

            Word.Table headerTable = header.Range.Tables[1];
            
            ColumnCount = headerTable.Columns.Count;

            if (ColumnCount < 3)
            {
                MessageBox.Show("Tabela w nagłówku musi zawierać co najmniej 3 kolumny");
                return false;
            }

            if (headerTable.Rows.Count != 1)
            {
                MessageBox.Show("Tabela w nagłówku musi zawierać jeden wiersz");
                return false;
            }

            THeader = new VTable(ColumnCount, gridHeader);
            TMatrix = new VTable(ColumnCount, gridMatrix);
            THeader.RowCount = 1;
            TMatrix.RowCount = ColumnCount;

            // Uwaga indeksowanie w Word liczy się od 1

            for (int c = 1; c <= ColumnCount; c++)
            {
                THeader[c - 1, 0].AsStr = headerTable.Cell(1, c).Range.Text.Replace(Tab, Space).Trim(); ;
                THeader[c - 1, 0].AsInt = (int)Math.Round(headerTable.Cell(1, c).Width, 0);
            }

            for (int c = 0; c < THeader.ColumnCount; c++)
            {
                for (int count = c + 1; count < THeader.ColumnCount; count++)
                {
                    TMatrix[c, count - 1].AsInt = SumOfWidths(c, count);
                }
            }

            for (int c = 0; c < THeader.ColumnCount; c++)
            {
                int i1 = TMatrix[c, TMatrix.RowCount - 2].AsInt;
                int i2 = THeader[THeader.ColumnCount - 1, 0].AsInt;
  
                TMatrix[c, TMatrix.RowCount - 1].AsInt = i1 + i2;
            }
            
            TMatrix[TMatrix.ColumnCount - 1, TMatrix.RowCount - 1].AsInt 
                = 3 * THeader[THeader.ColumnCount - 1, 0].AsInt / 2;
            tHeader = THeader;
            tMatrix = TMatrix;
            return true;
        }

        /// <summary>Metoda używana przy wypełnianiu tMatrix danymi</summary>
        /// <param name="startIdx">Indeks początkowej komórki</param>
        /// <param name="count">Ilość komórek scalonych</param>
        /// <returns>
        /// Suma szerokości komórek tabeli Word dla indeksu startIdx oraz ilości scalonych komórek count
        /// <remarks>
        /// Suma jest powiększona o połowę szerokości ostatniej komórki dla danego indeksu
        /// i danej ilości scalonych komórek, a dla ostatniej komórki w wierszu tabeli Word
        /// jest to 3/2 jej szerokości
        /// </remarks>
        ///</returns>
        private static int SumOfWidths(int startIdx, int count)
        {
            int sum = 0;
            
            for (int i = startIdx; i < count; i++)
            {
                sum += THeader[i, 0].AsInt;
            }
            
            sum += THeader[count, 0].AsInt / 2;
            return sum;
        }
    }
}

5 Zrzuty ekranu przykładowej aplikacji

naglowek.png
Zrzut 1. Nagłówek dokumentu

macierz.png
Zrzut 2. Macierz pozycjonująca

dokument_nieuporzadkowany.png
Zrzut 3. Tabela nieuporządkowana

dokument_uporzadkowany.png
Zrzut 4. Tabela uporządkowana

6 Załącznik – przykładowy projekt z pełnym kodem C# i przykładem dokumentu

Projekt powstał w MS C# Express 2010. Należy go otworzyć i skompilować.
Przykładowy dokument jest w formacie .docx MS Word 2010. Należy go umieścić w ścieżce z plikiem .exe
Załącznik nie zawiera plików .obj oraz .exe

table_structurer.zip

6.a Przykładowy dokument w formatach .docx oraz .doc

dokument.zip

0 komentarzy