Lire un fichier généré par le (vieux !) logiciel MS Works
Résolumamiemando Messages postés 33081 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 27 avril 2024 - 28 sept. 2023 à 17:49
- Uptobox
- Lire le coran en français pdf - Télécharger - Histoire & Religion
- Fichier rar - Guide
- Lire fichier epub - Guide
- Fichier host - Guide
- Fichier iso - Guide
13 réponses
22 sept. 2023 à 16:00
Bonjour,
Peux-tu partager quelques fichier wps afin que l'on comprenne mieux et puisse tester ton code ? Je suspecte que les informations sont organisées comme suit :
- un header de taille variable, contenant la taille dudit header et les meta données
- le segment de donnée (ce que tu cherches à afficher) et qui visiblement contient des caractères spéciaux servant pour la mise en forme (ceux que tu filtres dans ton code)
- un footer -- je ne vois pas trop à quoi il sert, mais d'après ce que tu dis il existe ; quoi qu'il en soit, cela laisse penser que la taille du segment de données est spécifiées dans le header.
Bref, ce ne sont que des conjectures, sans voir dans le détail un tel fichier, difficile d'en dire plus puisque comme tu l'indique (ainsi que ce lien), les spécifications du format wps ne sont pas publiques.
Bonne chance
22 sept. 2023 à 16:09
Bonjour,
Comme à l'école polytechnique, pour la Patrie, les Sciences et la Gloire?
https://archive.org/details/works-8_fr
22 sept. 2023 à 16:45
Bonjour Brucine,
Bah, ça m'amuse de faire ce genre de trucs ...
Bonjour Mamiemando,
Comment faire pour partager des fichiers ?
22 sept. 2023 à 17:37
Je n'y comprends rien (pas à l'anglais, à Python), un peu de lecture en anglais si ça t'inspire, mais à part le blabla, le code n'est de toute façon pas en français.
http://blog.digitally-disturbed.co.uk/2012/04/ive-started-work-on-pulling-text-from.html
Modifié le 22 sept. 2023 à 18:23
En passant par un site tiers comme par exemple cjoint.com. Ceci dit le lien de Brucine semble être intéressant, peux-tu le tester et nous indiquer s'il ne résout pas d'ores et déjà ton problème ?
De ce que je comprends du code
- Un document WPS est un document OLE, ce qui signifie qu'il faut traiter tout le header OLE avant de traiter la partie WPS a proprement parler. C'est le rôle de la classe OleDocument qui organise le fichier binaire en dossier (self.directories). Cet attribut est peuplé après avoir parsé les données OLE (méthode parse_contents). Une fois les dossiers déterminés, on cherche les données WPS dans le dossier CONTENTS.
- Le format WPS est une composé d'un header composé de plusieurs entries (voir la méthode _process_entries de la classe WpsReader). Celle qui nous intéresse vérifie text_size > 0. Elle permet aussi de localiser dans le fichier le segment de données qui stocke le texte (text_header_offset).
- Comme aucun traitement n'est fait sur le texte ainsi extrait, le traitement proposé dans le message initial reste nécessaire.
Bonne chance
Modifié le 22 sept. 2023 à 19:05
J'ai testé le code sur un de mes .wps
erreur: Not a valid Ole Storage Document
Forcément, je pense que c'est une ancienne version de Python
(il y a un print sans () à la fin), et il faut écrire
if sig != b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1': raise ReaderError("Not a valid Ole Storage Document")
au lieu de
if sig != "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" raise ReaderError("Not a valid Ole Storage Document")
avec fd.read() on obtient un format byte
Après correction, évidemment, on a une autre erreur
buff += fd.read(self.sector_size)
TypeError: can only concatenate str (not "bytes") to str
puisqu'on ne manipule plus des strings
Après, il faut examiner le code en détails pour essayer de comprendre
ce qu'il fait et corriger les erreurs au fur et à mesure : vaste tache
J'ai même du corriger ça:
for i in range(int(self.sector_size / 4)):
au lieu de :
for i in range(self.sector_size / 4):
Modifié le 25 sept. 2023 à 14:26
Bonjour,
Effectivement le code a été prévu pour python2 (cela se voit au niveau du print) et du coup il y a plein de petites adaptations à apporter, donc celles que tu as reportées.
J'ai même du corriger ça:
for i in range(int(self.sector_size / 4))
au lieu de
for i in range(self.sector_size / 4):
Oui c'est normal : en python 2, / se comporte comme en C : vu que le type de gauche est entier, c'est donc implicitement la division entière (qui s'écrit // en python3). Tu aurais donc pu traduire cette instruction par :
for i in range(self.sector_size // 4):
Après, je ne pense pas qu'il y ait tant que ça à corriger. Je regarderai un peu plus en détail quand j'aurai plus de temps.
Vous n’avez pas trouvé la réponse que vous recherchez ?
Posez votre question23 sept. 2023 à 17:28
Bonjour Mamiemando,
Ci-dessous 2 liens vers 2 fichiers .wps, comme tu me le demandais
hier
https://www.cjoint.com/c/MIxpyFYDoTr
https://www.cjoint.com/c/MIxpzZkhkEr
Dis-moi ce que l'on peut en tirer ....
Modifié le 27 sept. 2023 à 17:18
Hello,
J'ai commencé à retaper le script (en incluant certaines des modifications dont tu as parlé), mais ça n'est pas encore fonctionnel.
#!/usr/bin/env python3 import os import sys import struct import re from collections import namedtuple WPSSTRIPPATTERN = re.compile(r"\r") def unicode(s: str, fmt: str) -> str: return s.decode(fmt, "strict") class ReaderError(Exception): pass class OleDocument(object): def __init__(self, file_name): self.file_name = file_name self.sectors = [] self.directories = {} self._parse_contents() def _read_fat_sector(self, fat_sector, fd): fd.seek(self.sector_size * (fat_sector + 1), os.SEEK_SET) for i in range(self.sector_size // 4): sector = struct.unpack("<I", fd.read(4))[0] yield sector def _parse_contents(self): with open(self.file_name, "rb") as fd: data = fd.read() with open(self.file_name, "rb") as fd: sig = fd.read(8) expected_sig = bytes("\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", "iso-8859-1") if sig != expected_sig: raise ReaderError(f"Not a valid Ole Storage Document:\nobtained: {sig}\nexpected: {expected_sig}") header = fd.read(68) ( sector_shift, mini_sector_shift, fat_sector_count, first_dir_sector, first_mini_sector, mini_sector_count, ) = struct.unpack("<22xHH10xII8xII8x", header) self.sector_size = 1 << sector_shift self.mini_sector_size = 1 << mini_sector_shift fat_sectors = [] for i in range(fat_sector_count): fat_sectors.append(struct.unpack("<I", fd.read(4))[0]) for fat_sector in fat_sectors: for sector in self._read_fat_sector(fat_sector, fd): self.sectors.append(sector) # Now read the directories buff = bytes() for count, dir_sector in enumerate(self._get_sectors(first_dir_sector)): fd.seek( self.sector_size * dir_sector + self.sector_size, os.SEEK_SET ) buff += fd.read(self.sector_size) for i in range((count + 1) * 4): print(f"{i * 128 }:{(i + 1) * 128} -- {len(buff)}") print(buff[i * 128 : (i + 1) * 128]) name, sector, size = struct.unpack( "<64s52xII4x", buff[i * 128 : (i + 1) * 128] ) name = re.sub( "\x00", "", unicode(name, "UTF16") ) self.directories[name] = (sector, size) print(f"{name} -> {sector, size}") def _get_sectors(self, sector): while True: if sector == 0xFFFFFFFE: #Last directory break yield sector sector = self.sectors[sector] def read_stream(self, name): #name = unicode(name) if name not in self.directories: raise ReaderError(f"No stream called {name}: known streams are {list(self.directories.keys())}") start, size = self.directories[name] buff = bytes() with open(self.file_name, "rb") as fd: for sector in self._get_sectors(start): fd.seek(self.sector_size * sector + self.sector_size, os.SEEK_SET) buff += fd.read(self.sector_size) size -= self.sector_size if size <= 0: break return buff class WPSReader(object): def __init__(self, file_name): self.document = OleDocument(file_name) self.strip_pattern = WPSSTRIPPATTERN def _process_entries(self, entry_buff): magic, local, next_offset = struct.unpack("<HHI", entry_buff[:8]) if magic != 0x01F8: raise ReaderError("Invalid format - Entry magic tag incorrect") entry_pos = 0x08 #2 WORDS & 1 DWORD for i in range(local): size = struct.unpack("<H", entry_buff[entry_pos:entry_pos+0x2])[0] name, offset, entry_size = struct.unpack("<2x4s10xII", entry_buff[entry_pos:entry_pos+size]) if name == "TEXT": #Success! return (local, 0x00, offset, entry_size) entry_pos += size return (local, next_offset, 0x00, 0x00) #Needs to be run again def extract_text(self): buff = self.document.read_stream("CONTENTS") total_entries = struct.unpack("<12xH", buff[:14])[0] entries_pos = 24 while True: entries, next_offset, text_header_offset, text_size = \ self._process_entries(buff[entries_pos:]) if text_size: #TEXT found break total_entries -= entries if total_entries and next_offset: entries_pos = next_offset #Move to next block else: raise ReaderError("Unable to find TEXT secion. File corrupt?") text = buff[text_header_offset:text_header_offset+text_size] return self.strip_pattern.sub("\r\n", unicode(text, "UTF16")) if __name__ == '__main__': reader = WPSReader(sys.argv[1]) print(reader.extract_text())
Le problème c'est qu'il n'y a pas de dossier "CONTENTS". Et j'avoue que quand on regarde le contenu hexadécimal de ton fichier WPS (premier lien), je n'en ai pas vu.
Le segment de données qui nous intéresse figure entre les deux "balises" MN0 et CompObj dans le fichier, et il ne semble pas y avoir de balises qui précède les données texte plus proche que MN0. Or dans le code que je propose, j'affiche les buffers associés à chaque dossier, mais aucun ne couvre le texte.
Il faudrait passer un peu plus de temps et regarder les différentes valeurs (secteurs, tailles, etc.).
À ce stade j'ai des soupçons sur la fonction _read_fat_sector qui retourne des valeurs étranges... et sur l'absence de la chaîne CONTENTS (au format unicode) dans le fichier WPS.
Toujours sur ce fichier d'exemple, on peut repérer de manière empirique le segment de données qui contient du texte. Pour obtenir ces valeurs, j'ai regardé le fichier WPS dans vim et repérer le premier mot (Judo) et le dernier mot (DURIAUX) et j'en ai déduis que la plage qui nous intéressait se situait entre les octets 2368 et 3121.
with open(sys.argv[1], "rb") as fd: data = fd.read() start = data.find(bytes("Judo", "iso-8859-1")) end = data.find(bytes("DURIAUX", "iso-8859-1")) + len("DURIAUX") + 4 print(data[start:end].decode("iso-8859-1"))
En admettant qu'on parvienne à trouver start et end de manière automatique, on pourrait complètement ignorer l'analyse du header.
Bonne chance
Modifié le 27 sept. 2023 à 17:55
Salut Phil_1857,
En dehors du défi de réaliser cela en Python, si tu cherches juste à lire le contenu avec un logiciel actuel et libre supportant ce format, tu peux télécharger LibreOffice et lire les fichiers .wps avec LibreOffice Writer.
(testé avec succès sur les 2 fichiers que tu as mis à disposition)
27 sept. 2023 à 18:22
Merci Dal
En fait, avec mon code, on lit parfaitement le contenu des fichiers
Il y a simplement des caractères bizarres en début et en fin de fichier
mais ça n'est pas gênant, je voulais juste m'amuser avec la lecture
de fichiers binaires et améliorer ça...
Le résultat avec interface Tkinter
Modifié le 27 sept. 2023 à 17:51
En poursuivant la piste amorcée dans #9, on aboutit à :
#!/usr/bin/env python3 import sys import string def main(filename): with open(filename, "rb") as fd: data = fd.read() specs = str([ 'â', 'à', 'Â', 'À', 'ç', 'Ç', 'é', 'ê', 'è', 'É', 'Ê', 'È', 'î', 'Î', 'ô', 'Ô', 'û', 'ù', 'Û', 'Ù', ]) valids = set(specs + string.digits + string.ascii_letters + string.punctuation) i = j = None segments = list() for (k, byte) in enumerate(data): if chr(byte) in valids or byte in {9, 10, 13}: if i is None: i = j = k j += 1 else: if (i, j) != (None, None): segments.append((i, j)) i = j = None assert segments (start, end) = sorted( segments, key=lambda segment: segment[1] - segment[0], reverse=True )[0] return data[start:end].decode("iso-8859-1") if __name__ == '__main__': print(main(sys.argv[1]))
L'idée est de reconstruire les segments contenant des séquences de caractères ASCII valides et de prendre le segment le plus long (les autres correspondent par exemple aux noms des polices impliquées dans le document). Puis on décode les octets associés à ce segment de données...
On peut probablement améliorer la manière dont sont définis les caractères valides, notamment dans certaines langues, d'autres caractères spéciaux peuvent apparaître.
Bonne chance
27 sept. 2023 à 18:28
Bonjour Mamiemando,
Merci d'avoir passé un peu de temps là-dessus
Ca à l'air de marcher sur plusieurs fichiers testés !
Je vais maintenant analyser ton code en détails ...
Modifié le 27 sept. 2023 à 20:20
Hello,
Bon j'ai trouvé 10 minutes pour peaufiner le code proposé dans #11, et qui maintenant est agnostique sur les caractères (latins) impliqués dans le document.
#!/usr/bin/env python3 import sys def find_longest_segment(data: bytes) -> tuple: i = j = None segments = list() for (k, byte) in enumerate(data): a = chr(byte) if a.isprintable() or a.isspace(): if i is None: i = j = k j += 1 else: if (i, j) != (None, None): segments.append((i, j)) i = j = None if (i, j) != (None, None): segments.append((i, j)) i = j = None assert segments return max( segments, key=lambda segment: segment[1] - segment[0] ) def decode_wps_data( data: bytes, encoding: str = "iso-8859-1" ) -> str: (start, end) = find_longest_segment(data) return data[start:end].decode(encoding) def decode_wps_file( filename: str, encoding: str = "iso-8859-1" ) -> str: with open(filename, "rb") as fd: data = fd.read() return decode_wps_data(data, encoding) if __name__ == "__main__": text = decode_wps_file(sys.argv[1], "iso-8859-1") print(text)
Note que si tu as des documents qui reposent sur un autre alphabet (genre du cyrillique, des caractères chinois, etc...), ceux-ci ne sont pas supportés par un encodage iso-8859-1 (qui est l'encodage traditionnellement utilisé sous Windows pour un document écrit en français). Bref, si on veut être perfectionniste, il faudrait sans doute permettre à l'utilisateur de préciser l'encodage et envisager d'utiliser argparse.
Bonne continuation
28 sept. 2023 à 10:17
Bonjour Mamiemando,
Ah, effectivement, je vois que l'on n'a plus besoin des listes
specs et valids, par contre l'ensemble des 62 fichiers ne concerne que
des courriers ou des comptes rendus en français, donc pas de cyrillique
ou de chinois
Merci encore
Modifié le 28 sept. 2023 à 00:38
Avec un éditeur hexadécimal on peut voir, sur les deux fichiers que Phil_1857 a mis en ligne, que les données afférentes au texte commencent au déplacement 940 en hexa (2368 en décimal) et que les données de texte se terminent par une série de 00.
Du coup, le programme suivant utilisant les regex Python pour capturer les données à partir du byte 2368 et jusqu'à rencontrer le premier caractère null fonctionne chez moi sur les deux fichiers d'exemple, la regex faisant le travail de capture en une ligne (ligne 11 ci-dessous) :
#!/usr/bin/env python3 import sys import re def decode_wps_file( filename: str, encoding: str = "iso-8859-1" ) -> str: with open(filename, "rb") as fd: data = fd.read() result = re.search(b"([^\x00]+)", data[2368:]) return result.group(1).decode(encoding) if __name__ == "__main__": text = decode_wps_file(sys.argv[1], "iso-8859-1") print(text)
Cela devrait fonctionner à condition que l'entête ait une taille fixe, comme sur les deux exemples que Phil_1857 a donné et quelle que soit la taille du texte, même s'il ne fait que quelques caractères, à condition que le format supposé soit exact et que le texte se termine bien par le caractère null.
28 sept. 2023 à 11:29
Merci Dal,
mais les en-têtes n'ont pas tous la même longueur ...
28 sept. 2023 à 11:32
il me faut surement une autre paire de lunettes, je ne vois pas de bouton
"marquer comme résolu" :-)
28 sept. 2023 à 11:55
C'est en bas de ton premier message :
28 sept. 2023 à 12:02
?????????????????????????
28 sept. 2023 à 13:05
Je ne sais pourquoi cela ne s'affiche pas chez toi.
En tant que "contributeur", je suppose que je dispose du droit de marquer une discussion comme étant résolue.
J'ai marqué la discussion résolue.
28 sept. 2023 à 16:48
Normalement, comme Phil_1857 est créateur du message il aurait dû voir le bouton "Résolu", comme l'indique le tutoriel officiel. Ça ressemble à un bug, je remonte le problème et on verra ce qu'il en est.
Modifié le 28 sept. 2023 à 17:01
Je ne sais pas si il faut bien continuer sur ce fil de discussion, mais en
fait de bug, je trouve bizarre que je n'aie pas à me connecter lorsque
j'ouvre le site, la preuve: je peux répondre directement à un message
autre preuve, le menu déroulant en haut de page affiche bien
"se déconnecter": (donc je suis connecté)
Autre chose:
je me suis inscrit en mars 2019, mais, depuis peu, l'infobulle sur mon pseudo
dit que je me suis inscrit le 23 mars 2020 ....
Modifié le 28 sept. 2023 à 17:57
Bizarre, essaye de vider tes cookies. Concernant la date d'inscription, je ne peux pas te dire, mais j'ai remonté le problème aussi.