Comment puis-je détecter si un fichier est binaire (non textuel) en python?

105

Comment puis-je savoir si un fichier est binaire (non textuel) en python?

Je recherche dans un grand nombre de fichiers en python et continue à obtenir des correspondances dans des fichiers binaires. Cela rend la sortie incroyablement désordonnée.

Je sais que je pourrais utiliser grep -I, mais je fais plus avec les données que ce que permet grep.

Dans le passé, j'aurais simplement cherché des personnages plus grands que 0x7f, mais utf8et autres, cela rendait cela impossible sur les systèmes modernes. Idéalement, la solution serait rapide, mais n'importe quelle solution fera l'affaire.

pleurer
la source
SI "dans le passé, j'aurais juste cherché des caractères supérieurs à 0x7f" ALORS vous aviez l'habitude de travailler avec du texte ASCII brut ALORS toujours pas de problème puisque le texte ASCII encodé en UTF-8 reste ASCII (c'est-à-dire pas d'octets> 127).
tzot
@ ΤΖΩΤΖΙΟΥ: C'est vrai, mais je sais que certains des fichiers que je traite sont utf8. Je voulais dire utilisé dans le sens général, pas dans le sens spécifique de ces fichiers. :)
pleurer
1
Seulement avec probabilité. Vous pouvez vérifier si: 1) le fichier contient \ n 2) La quantité d'octets entre \ n est relativement petite (ce n'est PAS fiable) l 3) le fichier ne contient pas d'octets avec une valeur inférieure à la valeur du caractère ASCCI "espace" ('' ) - SAUF "\ n" "\ r" "\ t" et des zéros.
SigTerm
3
La stratégie greputilisée pour identifier les fichiers binaires est similaire à celle publiée par Jorge Orpinel ci-dessous . Sauf si vous définissez l' -zoption, il recherchera simplement un caractère nul ( "\000") dans le fichier. Avec -z, il recherche "\200". Les personnes intéressées et / ou sceptiques peuvent consulter la ligne 1126 de grep.c. Désolé, je n'ai pas trouvé de page Web avec le code source, mais vous pouvez bien sûr l'obtenir à partir de gnu.org ou via une distribution .
intuité
3
PS Comme mentionné dans le fil de commentaires de l'article de Jorge, cette stratégie donnera des faux positifs pour les fichiers contenant, par exemple, du texte UTF-16. Néanmoins, git diffGNU et GNU diffutilisent également la même stratégie. Je ne sais pas si c'est si répandu parce que c'est tellement plus rapide et plus facile que l'alternative, ou si c'est simplement à cause de la rareté relative des fichiers UTF-16 sur les systèmes qui ont tendance à avoir ces utilitaires installés.
intuité

Réponses:

42

Vous pouvez également utiliser le module mimetypes :

import mimetypes
...
mime = mimetypes.guess_type(file)

Il est assez facile de compiler une liste de types binaires mime. Par exemple, Apache distribue un fichier mime.types que vous pouvez analyser dans un ensemble de listes, binaires et texte, puis vérifier si le mime est dans votre liste de texte ou de binaire.

Gavin M. Roy
la source
16
Existe-t-il un moyen d' mimetypesutiliser le contenu d'un fichier plutôt que simplement son nom?
intuité
4
@intuited Non, mais libmagic le fait. Utilisez-le via python-magic .
Bengt
Il y a une question similaire avec quelques bonnes réponses ici: stackoverflow.com/questions/1446549/… La réponse basée sur une recette d'activation me semble bonne, elle autorise une petite proportion de caractères non imprimables (mais non \ 0, pour certains raison).
Sam Watkins
5
Ce n'est pas une bonne réponse uniquement parce que le module mimetypes n'est pas bon pour tous les fichiers. Je regarde un fichier maintenant que le système filerapporte comme "texte Unicode UTF-8, avec de très longues lignes" mais mimetypes.gest_type () retournera (None, None). De plus, la liste des types MIME d'Apache est une liste blanche / un sous-ensemble. Ce n'est en aucun cas une liste complète des types MIME. Il ne peut pas être utilisé pour classer tous les fichiers en tant que texte ou non-texte.
Purrell
1
guess_types est basé sur l'extension du nom de fichier, pas sur le contenu réel comme le ferait la commande Unix "file".
Eric H.20
61

Encore une autre méthode basée sur le comportement de file (1) :

>>> textchars = bytearray({7,8,9,10,12,13,27} | set(range(0x20, 0x100)) - {0x7f})
>>> is_binary_string = lambda bytes: bool(bytes.translate(None, textchars))

Exemple:

>>> is_binary_string(open('/usr/bin/python', 'rb').read(1024))
True
>>> is_binary_string(open('/usr/bin/dh_python3', 'rb').read(1024))
False
jfs
la source
Peut obtenir à la fois des faux positifs et des faux négatifs, mais reste une approche intelligente qui fonctionne pour la grande majorité des fichiers. +1.
spectres
2
Il est intéressant de noter que le fichier (1) lui-même exclut également 0x7f de la considération, donc techniquement parlant, vous devriez l'utiliser à la bytearray([7,8,9,10,12,13,27]) + bytearray(range(0x20, 0x7f)) + bytearray(range(0x80, 0x100))place. Voir Python, fichier (1) - Pourquoi les nombres [7,8,9,10,12,13,27] et la plage (0x20, 0x100) sont-ils utilisés pour déterminer le texte par rapport au fichier binaire et github.com/file/file/ blob /…
Martijn Pieters
@MartijnPieters: merci. J'ai mis à jour la réponse pour exclure 0x7f( DEL).
jfs
1
Belle solution utilisant des ensembles. :-)
Martijn Pieters
Pourquoi excluez-vous 11ou VT? Dans le tableau 11 est considéré comme du texte ASCII brut, et c'est le vertical tab.
darksky le
15

Si vous utilisez python3 avec utf-8, c'est simple, ouvrez simplement le fichier en mode texte et arrêtez le traitement si vous obtenez un fichier UnicodeDecodeError. Python3 utilisera unicode lors de la gestion des fichiers en mode texte (et bytearray en mode binaire) - si votre encodage ne peut pas décoder des fichiers arbitraires, il est fort probable que vous obteniez UnicodeDecodeError.

Exemple:

try:
    with open(filename, "r") as f:
        for l in f:
             process_line(l)
except UnicodeDecodeError:
    pass # Fond non-text data
skier
la source
pourquoi ne pas utiliser with open(filename, 'r', encoding='utf-8') as fdirectement?
Terry
8

Si cela aide, de nombreux types binaires commencent par des nombres magiques. Voici une liste de signatures de fichiers.

Shane C. Mason
la source
C'est à cela que sert libmagic. Il est accessible en python via python-magic .
Bengt le
2
Malheureusement, "ne commence pas par un nombre magique connu" n'est pas équivalent à "est un fichier texte".
Purrell
8

Essaye ça:

def is_binary(filename):
    """Return true if the given filename is binary.
    @raise EnvironmentError: if the file does not exist or cannot be accessed.
    @attention: found @ http://bytes.com/topic/python/answers/21222-determine-file-type-binary-text on 6/08/2010
    @author: Trent Mick <[email protected]>
    @author: Jorge Orpinel <[email protected]>"""
    fin = open(filename, 'rb')
    try:
        CHUNKSIZE = 1024
        while 1:
            chunk = fin.read(CHUNKSIZE)
            if '\0' in chunk: # found null byte
                return True
            if len(chunk) < CHUNKSIZE:
                break # done
    # A-wooo! Mira, python no necesita el "except:". Achis... Que listo es.
    finally:
        fin.close()

    return False
Jorge Orpinel
la source
9
-1 définit «binaire» comme contenant un octet nul. Classifiera les fichiers texte encodés en UTF-16 comme "binaires".
John Machin
5
@John Machin: Fait intéressant, fonctionne engit diff fait de cette façon , et bien sûr, il détecte les fichiers UTF-16 comme binaires.
intuité
Hunh .. GNU difffonctionne également de cette façon. Il a des problèmes similaires avec les fichiers UTF-16. filedétecte correctement les mêmes fichiers que le texte UTF-16. Je n'ai pas extrait greple code de, mais il détecte aussi les fichiers UTF-16 comme binaires.
intuité
1
+1 @John Machin: utf-16 est une donnée de caractère selon file(1)laquelle il n'est pas sûr d'imprimer sans conversion, donc cette méthode est appropriée dans ce cas.
jfs le
2
-1 - Je ne pense pas que «contient un octet zéro» soit un test adéquat pour le binaire vs le texte, par exemple je peux créer un fichier contenant tous les octets 0x01 ou répéter 0xDEADBEEF, mais ce n'est pas un fichier texte. La réponse basée sur le fichier (1) est meilleure.
Sam Watkins
6

Voici une suggestion qui utilise la commande de fichier Unix :

import re
import subprocess

def istext(path):
    return (re.search(r':.* text',
                      subprocess.Popen(["file", '-L', path], 
                                       stdout=subprocess.PIPE).stdout.read())
            is not None)

Exemple d'utilisation:

>>> istext ('/ etc / motd') 
Vrai
>>> istext ('/ vmlinuz') 
Faux
>>> open ('/ tmp / japanese'). read ()
'\ xe3 \ x81 \ x93 \ xe3 \ x82 \ x8c \ xe3 \ x81 \ xaf \ xe3 \ x80 \ x81 \ xe3 \ x81 \ xbf \ xe3 \ x81 \ x9a \ xe3 \ x81 \ x8c \ xe3 \ x82 \ x81 \ xe5 \ xba \ xa7 \ xe3 \ x81 \ xae \ xe6 \ x99 \ x82 \ xe4 \ xbb \ xa3 \ xe3 \ x81 \ xae \ xe5 \ xb9 \ x95 \ xe9 \ x96 \ x8b \ xe3 \ x81 \ x91 \ xe3 \ x80 \ x82 \ n '
>>> istext ('/ tmp / japanese') # fonctionne sur UTF-8
Vrai

Il a les inconvénients de ne pas être portable sur Windows (à moins que vous n'ayez quelque chose comme la filecommande là-bas) et de devoir générer un processus externe pour chaque fichier, ce qui peut ne pas être acceptable.

Jacob Gabrielson
la source
Cela a cassé mon script :( En enquêtant, j'ai découvert que certains conffiles sont décrits par file"Configuration gelée de Sendmail - version m" - notez l'absence de la chaîne "texte". Peut-être utiliser file -i?
melissa_boiko
1
TypeError: impossible d'utiliser un modèle de chaîne sur un objet de type octets
abg
5

Utilisez la bibliothèque binaryornot ( GitHub ).

C'est très simple et basé sur le code trouvé dans cette question stackoverflow.

Vous pouvez en fait l'écrire en 2 lignes de code, mais ce package vous évite d'avoir à écrire et à tester minutieusement ces 2 lignes de code avec toutes sortes de types de fichiers étranges, multiplateformes.

guettli
la source
4

Habituellement, vous devez deviner.

Vous pouvez regarder les extensions comme un indice, si les fichiers en ont.

Vous pouvez également reconnaître les formats binaires connus et les ignorer.

Sinon, voyez quelle proportion d'octets ASCII non imprimables vous avez et faites une estimation à partir de cela.

Vous pouvez également essayer le décodage à partir de UTF-8 et voir si cela produit une sortie sensible.

Douglas Leeder
la source
4

Une solution plus courte, avec un avertissement UTF-16:

def is_binary(filename):
    """ 
    Return true if the given filename appears to be binary.
    File is considered to be binary if it contains a NULL byte.
    FIXME: This approach incorrectly reports UTF-16 as binary.
    """
    with open(filename, 'rb') as f:
        for block in f:
            if b'\0' in block:
                return True
    return False
Tom Kennedy
la source
Remarque: for line in filepeut consommer une quantité illimitée de mémoire jusqu'à ce qu'il b'\n'soit trouvé
jfs
à @Community: ".read()"retourne un bytestring ici qui est itérable (il donne octets individuels).
jfs
4

Nous pouvons utiliser python lui-même pour vérifier si un fichier est binaire, car il échoue si nous essayons d'ouvrir un fichier binaire en mode texte

def is_binary(file_name):
    try:
        with open(file_name, 'tr') as check_file:  # try open file in text mode
            check_file.read()
            return False
    except:  # if fail then file is non-text (binary)
        return True
Serhii
la source
Cela échoue pour beaucoup de fichiers `.avi '(vidéo).
Anmol Singh Jaggi
3

Si vous n'êtes pas sous Windows, vous pouvez utiliser Python Magic pour déterminer le type de fichier. Ensuite, vous pouvez vérifier s'il s'agit d'un type texte / mime.

Kamil Kisiel
la source
2

Voici une fonction qui vérifie d'abord si le fichier commence par une nomenclature et sinon recherche un octet de zéro dans les 8192 octets initiaux:

import codecs


#: BOMs to indicate that a file is a text file even if it contains zero bytes.
_TEXT_BOMS = (
    codecs.BOM_UTF16_BE,
    codecs.BOM_UTF16_LE,
    codecs.BOM_UTF32_BE,
    codecs.BOM_UTF32_LE,
    codecs.BOM_UTF8,
)


def is_binary_file(source_path):
    with open(source_path, 'rb') as source_file:
        initial_bytes = source_file.read(8192)
    return not any(initial_bytes.startswith(bom) for bom in _TEXT_BOMS) \
           and b'\0' in initial_bytes

Techniquement, la vérification de la nomenclature UTF-8 n'est pas nécessaire car elle ne doit pas contenir zéro octet dans tous les cas pratiques. Mais comme il s'agit d'un encodage très courant, il est plus rapide de vérifier la nomenclature au début au lieu de balayer tous les 8192 octets pour 0.

Roskakori
la source
2

Essayez d'utiliser le python-magic actuellement maintenu qui n'est pas le même module dans la réponse de @Kami Kisiel. Cela prend en charge toutes les plates-formes, y compris Windows, mais vous aurez besoin des libmagicfichiers binaires. Ceci est expliqué dans le README.

Contrairement au module mimetypes , il n'utilise pas l'extension du fichier et inspecte à la place le contenu du fichier.

>>> import magic
>>> magic.from_file("testdata/test.pdf", mime=True)
'application/pdf'
>>> magic.from_file("testdata/test.pdf")
'PDF document, version 1.2'
>>> magic.from_buffer(open("testdata/test.pdf").read(1024))
'PDF document, version 1.2'
Manger chez Joes
la source
1

Je suis venu ici à la recherche exactement de la même chose - une solution complète fournie par la bibliothèque standard pour détecter le binaire ou le texte. Après avoir examiné les options suggérées par les gens, la commande de fichier nix semble être le meilleur choix (je ne développe que pour linux boxen). Certains autres ont publié des solutions à l'aide de fichiers, mais elles sont inutilement compliquées à mon avis, alors voici ce que j'ai proposé:

def test_file_isbinary(filename):
    cmd = shlex.split("file -b -e soft '{}'".format(filename))
    if subprocess.check_output(cmd)[:4] in {'ASCI', 'UTF-'}:
        return False
    return True

Cela devrait aller de soi, mais votre code qui appelle cette fonction doit s'assurer que vous pouvez lire un fichier avant de le tester, sinon cela détectera par erreur le fichier comme binaire.

scie
la source
1

Je suppose que la meilleure solution est d'utiliser la fonction guess_type. Il contient une liste avec plusieurs types MIME et vous pouvez également inclure vos propres types. Voici le script que j'ai fait pour résoudre mon problème:

from mimetypes import guess_type
from mimetypes import add_type

def __init__(self):
        self.__addMimeTypes()

def __addMimeTypes(self):
        add_type("text/plain",".properties")

def __listDir(self,path):
        try:
            return listdir(path)
        except IOError:
            print ("The directory {0} could not be accessed".format(path))

def getTextFiles(self, path):
        asciiFiles = []
        for files in self.__listDir(path):
            if guess_type(files)[0].split("/")[0] == "text":
                asciiFiles.append(files)
        try:
            return asciiFiles
        except NameError:
            print ("No text files in directory: {0}".format(path))
        finally:
            del asciiFiles

Il se trouve à l'intérieur d'une classe, comme vous pouvez le voir en fonction de la structure du code. Mais vous pouvez pratiquement changer les choses que vous souhaitez implémenter dans votre application. C'est assez simple à utiliser. La méthode getTextFiles retourne un objet de liste avec tous les fichiers texte qui résident dans le répertoire que vous passez dans la variable de chemin.

Léonard
la source
1

sur * NIX:

Si vous avez accès à la filecommande shell, shlex peut aider à rendre le module de sous-processus plus utilisable:

from os.path import realpath
from subprocess import check_output
from shlex import split

filepath = realpath('rel/or/abs/path/to/file')
assert 'ascii' in check_output(split('file {}'.format(filepth).lower()))

Ou, vous pouvez également coller cela dans une boucle for pour obtenir la sortie de tous les fichiers dans le répertoire actuel en utilisant:

import os
for afile in [x for x in os.listdir('.') if os.path.isfile(x)]:
    assert 'ascii' in check_output(split('file {}'.format(afile).lower()))

ou pour tous les sous-répertoires:

for curdir, filelist in zip(os.walk('.')[0], os.walk('.')[2]):
     for afile in filelist:
         assert 'ascii' in check_output(split('file {}'.format(afile).lower()))
Rob Truxal
la source
1

La plupart des programmes considèrent que le fichier est binaire (c'est-à-dire tout fichier qui n'est pas "orienté ligne") s'il contient un caractère NULL .

Voici la version de perl de pp_fttext()( pp_sys.c) implémentée en Python:

import sys
PY3 = sys.version_info[0] == 3

# A function that takes an integer in the 8-bit range and returns
# a single-character byte object in py3 / a single-character string
# in py2.
#
int2byte = (lambda x: bytes((x,))) if PY3 else chr

_text_characters = (
        b''.join(int2byte(i) for i in range(32, 127)) +
        b'\n\r\t\f\b')

def istextfile(fileobj, blocksize=512):
    """ Uses heuristics to guess whether the given file is text or binary,
        by reading a single block of bytes from the file.
        If more than 30% of the chars in the block are non-text, or there
        are NUL ('\x00') bytes in the block, assume this is a binary file.
    """
    block = fileobj.read(blocksize)
    if b'\x00' in block:
        # Files with null bytes are binary
        return False
    elif not block:
        # An empty file is considered a valid text file
        return True

    # Use translate's 'deletechars' argument to efficiently remove all
    # occurrences of _text_characters from the block
    nontext = block.translate(None, _text_characters)
    return float(len(nontext)) / len(block) <= 0.30

Notez également que ce code a été écrit pour s'exécuter à la fois sur Python 2 et Python 3 sans modifications.

Source: Perl "suppose si le fichier est du texte ou du binaire" implémenté en Python

Kenorb
la source
0

êtes-vous sous Unix? si c'est le cas, essayez:

isBinary = os.system("file -b" + name + " | grep text > /dev/null")

Les valeurs de retour du shell sont inversées (0 est ok, donc s'il trouve "text" alors il retournera un 0, et en Python c'est une expression False).

fortran
la source
Pour référence, la commande de fichier devine un type en fonction du contenu du fichier. Je ne sais pas s'il prête attention à l'extension de fichier.
David Z
Je suis presque sûr que cela ressemble à la fois au contenu et à l'extension.
fortran
Cela casse si le chemin contient du "texte", tho. Assurez-vous de rsplit au dernier ':' (à condition qu'il n'y ait pas de deux-points dans la description du type de fichier).
Alan Plum
3
À utiliser fileavec le -bcommutateur; il n'imprimera que le type de fichier sans le chemin.
dubek
2
une version légèrement plus agréable:is_binary_file = lambda filename: "text" in subprocess.check_output(["file", "-b", filename])
jfs
0

Un moyen plus simple est de vérifier si le fichier est composé de caractères NULL ( \x00) en utilisant l' inopérateur, par exemple:

b'\x00' in open("foo.bar", 'rb').read()

Voir ci-dessous l'exemple complet:

#!/usr/bin/env python3
import argparse
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('file', nargs=1)
    args = parser.parse_args()
    with open(args.file[0], 'rb') as f:
        if b'\x00' in f.read():
            print('The file is binary!')
        else:
            print('The file is not binary!')

Exemple d'utilisation:

$ ./is_binary.py /etc/hosts
The file is not binary!
$ ./is_binary.py `which which`
The file is binary!
Kenorb
la source
0
from binaryornot.check import is_binary
is_binary('filename')

Documentation

parZval
la source