C# DotNetZip doppeltes speichern sorgt für Fehler beim öffnen bei DOCX

heulendoch

Ensign
Registriert
Feb. 2014
Beiträge
252
Hallo zusammen,

vorweg: Leider gibt es einen Grund weshalb ich direkt innerhalb einer Word-Datei manipulieren muss.

Ich habe eine kleine Hilfsklasse die mir die Ersetzung innerhalb eines DOCX-ZIPs ermöglicht. Dazu verwende ich DotNetZip 1.16.0. Nach dem zweiten mal speichern ist allerdings die DOCX angeblich beschädigt.

Folgendes Minimalbeispiel:
C#:
using Ionic.Zip;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace WordZipChangerTest {
    public class Program {
        static void Main(string[] args) {

            var WordDoc = @"C:\tmp\testreplace.docx";

            var ZipChanger = new WordZipChangerModel();
            ZipChanger.Replace(WordDoc, (content) => { return content.Replace("Hallo!", "Tschüss!"); });

            // Jetzt lässt sich das Dokument noch ohne Probleme öffnen!
            Console.ReadKey();
            Process.Start("explorer.exe", WordDoc); // Speichert man das geöffnete Word-Dokument hier mit Word wieder gibt es keine Probleme
            Console.ReadKey();

            ZipChanger.Replace(WordDoc, (content) => { return content.Replace("Tschüss!", "Wieder Hallo!"); });

            // Jetzt gibt es einen Fehler beim öffnen des Dokuments sich das Dokument noch ohne Probleme öffnen!
            Console.ReadKey();
            Process.Start("explorer.exe", WordDoc);
            Console.ReadKey();

        }
    }

    public class WordZipChangerModel {

        public bool Replace(string wordDocxFile, Func<string, string> replacing, string fileName = "word/document.xml") {

            Console.WriteLine($"Dokument bearbeiten \"{wordDocxFile}\" \"{fileName}\"");

            var Sw = Stopwatch.StartNew();
            var DoneSomething = false;

            using (var WordFile = new ZipFile(wordDocxFile)) {
                var EntryFile = WordFile.Entries.FirstOrDefault(Current => Current.FileName == fileName);
                if (EntryFile != null) {

                    using (var MemStrD = new MemoryStream()) {
                        EntryFile.Extract(MemStrD);
                        var Content = new UTF8Encoding().GetString(MemStrD.GetBuffer(), 0, (int)MemStrD.Length);
                        var ContentNew = replacing(Content);
                        DoneSomething = Content != ContentNew;

                        if (DoneSomething) {
                            var UpdatedEntry = WordFile.UpdateEntry(EntryFile.FileName, ContentNew, Encoding.UTF8);
                        }
                    }
                }

                if (DoneSomething)
                    WordFile.Save();
            }

            Sw.Stop();

            if (DoneSomething)
                Console.WriteLine($"Erledigt in {Sw.ElapsedMilliseconds} ms");
            else
                Console.WriteLine($"Nichts erledigt in {Sw.ElapsedMilliseconds} ms");


            return DoneSomething;
        }

    }
}

Nach dem zweiten Mal speichern erhalte ich folgenden Fehler:

1672157031750.png


Ich kann einfach nicht nachvollziehen weshalb.. hat jemand die Lösung?
 
schreib mal "Wieder Hallo!" ohne Leerzeichen, also "WiederHallo!". Ist schon ewig her das ich mich mit so Krams befassen musste, aber ich meine mich zu entsinnen das einzelne Wörter als eine Art Baum gespeichert werden und nicht einfach als kompletter Textblock.

Schau Dir auch auf jeden Fall den Inhalt von word/document.xml im Notepad an, vielleicht sieht man dann gleich was es zerbröselt hat.

Word schreibt (bzw. schrieb) auch irgendwohin ein Logfile mit ein bisschen höherer Aussagekraft als "geht nicht".
Grundsätzlich ist es eine eher schlechte Idee direkt im XML rum zu schreiben, da zerhaut es sehr schnell was.

damit ärgerte ich mich damals rum: https://learn.microsoft.com/de-de/office/open-xml/word-processing

zum Glück lange vorbei :-)
 
Zuletzt bearbeitet:
Office Open XML ist abgeleitet von zip aber ist, ms typisch, nicht 100% kompatibel.

Wirf nen Blick in die OO XML specs, konkret den ZIP Header betreffend.
 
Kalsarikännit schrieb:
schreib mal "Wieder Hallo!" ohne Leerzeichen, also "WiederHallo!". Ist schon ewig her das ich mich mit so Krams befassen musste, aber ich meine mich zu entsinnen das einzelne Wörter als eine Art Baum gespeichert werden und nicht einfach als kompletter Textblock.
Ohne Leerzeichen auch das gleiche Probleme. Ich mache eigentlich viel komplexere Ersetzungen die funktionieren. Allerdings halt nicht, wenn ich diese Ersetzungen in zwei Läufen mache.
Kalsarikännit schrieb:
Schau Dir auch auf jeden Fall den Inhalt von word/document.xml im Notepad an, vielleicht sieht man dann gleich was es zerbröselt hat.
Ist identsich, bis auf die erwartete Änderung.

1672213629715.png


1672213643196.png

Ergänzung ()

Iqra schrieb:
Wirf nen Blick in die OO XML specs, konkret den ZIP Header betreffend.
Aber warum klappt es dann beim ersten mal und beim zweiten mal nicht mehr? Würde ich die Änderung auf einen Schlag machen, dann macht es ja auch keine Probleme.

So sehen die ZIPs aus. Es kommen also Geändert, Erstellt... dazu und das Verfahren ist "Deflate" anstatt "Deflate:Fastest". Aber das hat sich ja auch schon nach dem ersten mal geändert und da klappt das öffnen noch problemlos.
1672213969643.png
 
Zuletzt bearbeitet:
heulendoch schrieb:
Aber warum klappt es dann beim ersten mal und beim zweiten mal nicht mehr?
Vermutlich räumt das Dispose von ZipFile nicht ausreichend auf, wofür du nichts kannst. Erkennbar daran, wenn nach einem erzwungenen Reset, z.b. die App vor dem 2. Lauf schliessen, der 2. Lauf ohne Fehler funktioniert.
Ich würde statt plötzlich string (in UpdateEntry) zu übergeben, bei stream bleiben ... so wie das beim Einlesen auch der präferierte Weg zu sein scheint. Evtl. tritt der Bug dann nicht auf.
Zusätzlich um die Kompression beizubehalten, würde ich bei ZipEntry schauen, ob es eine write Möglichkeit gibt. Denn WordFile.UpdateEntry wird eher ein neues ZipEntry erstellen, statt es upzudaten.
Wenn das alles nichts hilft, gibt es auch noch andere nuget Pakete.
 
Zuletzt bearbeitet:
Micke schrieb:
Vermutlich räumt das Dispose von ZipFile nicht ausreichend auf, wofür du nichts kannst. Erkennbar daran, wenn nach einem erzwungenen Reset, z.b. die App vor dem 2. Lauf schliessen, der 2. Lauf ohne Fehler funktioniert.
Funktioniert ebenfalls nicht.

Micke schrieb:
Wenn das alles nichts hilft, gibt es auch noch andere nuget Pakete.
Ich habe es vorhin schon mit ICSharpCode.SharpZipLib.Zip probiert und habe dort das selbe verhalten.
C#:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Zip;

namespace WordZipChangerTest2 {
    public class Program {
        static void Main(string[] args) {

            var WordDoc = @"C:\tmp\0\testreplace - Kopie.docx";

            var ZipChanger = new WordZipChangerModel();
            ZipChanger.Replace(WordDoc, (content) => { return content.Replace("Hi!", "Bye!"); });

            // Document is still fine, opening without an issue
            Console.ReadKey();
            Process.Start("explorer.exe", WordDoc);
            // If (!!!) we save the document here with microsoft word, the second opening works without an issue too
            Console.ReadKey();

            ZipChanger.Replace(WordDoc, (content) => { return content.Replace("Bye!", "Ciao!"); });

            // There is now an issue opening the document with microsoft word
            Console.ReadKey();
            Process.Start("explorer.exe", WordDoc);
            Console.ReadKey();

        }
    }

    public class WordZipChangerModel {

        public bool Replace(string wordDocxFile, Func<string, string> replacing, string fileName = "word/document.xml") {

            Console.WriteLine($"Modify document \"{wordDocxFile}\" \"{fileName}\"");

            var Sw = Stopwatch.StartNew();
            var DoneSomething = false;
            var ContentNew = default(string);

            using (var WordFile = new ZipFile(wordDocxFile)) {
                var EntryFile = WordFile.GetEntry(fileName);
                if (EntryFile != null) {

                    using (var MemStrD = new MemoryStream())
                    using (var str = WordFile.GetInputStream(EntryFile)) {
                        str.CopyTo(MemStrD);
                        var Content = new UTF8Encoding().GetString(MemStrD.GetBuffer(), 0, (int)MemStrD.Length);
                        ContentNew = replacing(Content);
                        DoneSomething = Content != ContentNew;

                    }

                    if (DoneSomething) {
                        WordFile.BeginUpdate();

                        // To use the entryStream as a file to be added to the zip,
                        // we need to put it into an implementation of IStaticDataSource.
                        CustomStaticDataSource sds = new CustomStaticDataSource();
                        sds.SetStream(GenerateStreamFromString(ContentNew));

                        // If an entry of the same name already exists, it will be overwritten; otherwise added.
                        WordFile.Add(sds, fileName);

                        // Both CommitUpdate and Close must be called.
                        WordFile.CommitUpdate();
                        // Set this so that Close does not close the memorystream
                        //WordFile.IsStreamOwner = false;
;
                    }

                    WordFile.Close();
                }
            }

            Sw.Stop();

            if (DoneSomething)
                Console.WriteLine($"Done in {Sw.ElapsedMilliseconds} ms");
            else
                Console.WriteLine($"Nothing done in {Sw.ElapsedMilliseconds} ms");


            return DoneSomething;
        }
        public static Stream GenerateStreamFromString(string s) {
            var stream = new MemoryStream();
            var writer = new StreamWriter(stream, Encoding.UTF8);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }
    }

    public class CustomStaticDataSource : IStaticDataSource {
        private Stream _stream;
        // Implement method from IStaticDataSource
        public Stream GetSource() {
            return _stream;
        }

        // Call this to provide the memorystream
        public void SetStream(Stream inputStream) {
            _stream = inputStream;
            _stream.Position = 0;
        }
    }
}

Beim ersten Mal lässt es sich öffnen, beim zweiten Mal nicht mehr.
 
Das Verhalten tritt auch bei unterschiedlichen Dateien auf ? Nicht dass die Datei eine Ausnahme ist.
 
Zuletzt bearbeitet:
@Micke ja unterschiedliche Dateien. Wenn du Zeit hättest es nachzustellen, dann müsste es auch bei dir so passieren.
 
Ist noch offen, habe einen Workaround den ich eigentlich nicht machen möchte (zwischen drin Word öffnen und erneut abspeichern).
 
Ich hab den Code aus dem EinstiegsPost mit DotNetZip 1.16 genommen - damit bekommt die Datei auch bei mir einen Treffer.
Nach dem Überfliegen der ZipFile & ZipEntry Klasse würde ich diese wie folgt nutzen, ich hab allerdings nicht untersucht welche Zeile genau das Problem behebt.
Code:
 public static void Main()
    {
        try
        {
            var wordDoc = "...";

            for (var i = 0; i < 5; i++)
            {
                Replace(wordDoc, (content) => { return content.Replace("Hallo!", "Tschüss!"); });
                Replace(wordDoc, (content) => { return content.Replace("Tschüss!", "Wieder Hallo!"); });
            }

            // Output: Wieder Wieder Wieder Wieder Wieder Hallo!

        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }
    }

    static bool Replace(string wordDocxFile, Func<string, string> replacing
       , string fileName = "word/document.xml")
    {
        using (var wordFile = ZipFile.Read(wordDocxFile))
        // ZipFile.Read suggeriert daß man es für eine existierende Datei nutzen soll
        {
            var entryFile = wordFile[fileName];
            if (entryFile == null)
                return false;

            string content;
            using (var stream = entryFile.OpenReader())
            //es scheint mir sinnvoller entryFile weiter zu nutzen, wenn man es einmal in der Hand hält, statt den Inhalt in weiteren Speicher (MemoryStream, Byte Array etc.) zu kopieren.
            using (var reader = new StreamReader(stream, Encoding.UTF8))
            {
                content = reader.ReadToEnd();
                reader.Close();
            }
            var contentNew = replacing(content);

            if (!content.Equals(contentNew))
            {
                var UpdatedEntry = wordFile.UpdateEntry(entryFile.FileName, contentNew, Encoding.UTF8);
                wordFile.Save();
                return true;
            }
        }
        return false;
    }
 
Danke für den Code, ich verwende den jetzt so, da er sauberer ist.

Warum auch immer bin ich jetzt gerade dadurch im Kopf in die korrekte Richtung abgebogen:
Bei meinem ursprünglichen Code ist beim zweiten mal eine zweite BOM entstanden, was das Problem verursacht.

Warum auch immer das passiert..
 
  • Gefällt mir
Reaktionen: Micke
Zurück
Oben