¿Cómo puedo detectar si un archivo es binario (sin texto) en Python?

105

¿Cómo puedo saber si un archivo es binario (sin texto) en Python?

Estoy buscando en un gran conjunto de archivos en Python y sigo obteniendo coincidencias en archivos binarios. Esto hace que la salida se vea increíblemente desordenada.

Sé que podría usar grep -I, pero estoy haciendo más con los datos de lo que permite grep.

En el pasado, solo hubiera buscado caracteres más grandes que 0x7f, pero utf8y similares, lo hacen imposible en los sistemas modernos. Idealmente, la solución sería rápida, pero cualquier solución servirá.

afligirse
fuente
SI "en el pasado solo habría buscado caracteres mayores que 0x7f" ENTONCES solía trabajar con texto ASCII plano ENTONCES todavía no hay problema ya que el texto ASCII codificado como UTF-8 sigue siendo ASCII (es decir, sin bytes> 127).
tzot
@ ΤΖΩΤΖΙΟΥ: Cierto, pero sé que algunos de los archivos con los que estoy tratando son utf8. Quise decir acostumbrado en el sentido general, no en el sentido específico de estos archivos. :)
llorar
1
Solo con probabilidad. Puede verificar si: 1) el archivo contiene \ n 2) La cantidad de bytes entre \ n es relativamente pequeña (esto NO es confiable) l 3) el archivo no contiene bytes con un valor menor que el valor del carácter de "espacio" ASCCI ('' ) - EXCEPTO "\ n" "\ r" "\ t" y ceros.
SigTerm
3
La estrategia que en grepsí misma utiliza para identificar archivos binarios es similar a la publicada por Jorge Orpinel a continuación . A menos que establezca la -zopción, solo buscará un carácter nulo ( "\000") en el archivo. Con -z, busca "\200". Los interesados ​​y / o escépticos pueden consultar la línea 1126 de grep.c. Lo siento, no pude encontrar una página web con el código fuente, pero, por supuesto, puede obtenerlo en gnu.org o mediante una distribución .
intuido
3
PD Como se mencionó en el hilo de comentarios de la publicación de Jorge, esta estrategia dará falsos positivos para archivos que contengan, por ejemplo, texto UTF-16. No obstante, tanto git diffGNU como difftambién utilizan la misma estrategia. No estoy seguro de si es tan frecuente porque es mucho más rápido y más fácil que la alternativa, o si es simplemente por la relativa rareza de los archivos UTF-16 en los sistemas que tienden a tener estas utilidades instaladas.
intuido

Respuestas:

42

También puede utilizar el módulo Mimetypes :

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

Es bastante fácil compilar una lista de tipos de mime binarios. Por ejemplo, Apache se distribuye con un archivo mime.types que puede analizar en un conjunto de listas, binario y texto y luego verificar si el mime está en su lista de texto o binaria.

Gavin M. Roy
fuente
16
¿Hay alguna forma de llegar mimetypesa usar el contenido de un archivo en lugar de solo su nombre?
intuido el
4
@intuited No, pero libmagic hace eso. Úselo a través de python-magic .
Bengt
Hay una pregunta similar con algunas buenas respuestas aquí: stackoverflow.com/questions/1446549/… La respuesta basada en una receta de estado activo me parece bien, permite una pequeña proporción de caracteres no imprimibles (pero no \ 0, para algunos razón).
Sam Watkins
5
Esta no es una gran respuesta solo porque el módulo mimetypes no es bueno para todos los archivos. Estoy viendo un archivo ahora que el sistema fileinforma como "texto Unicode UTF-8, con líneas muy largas" pero mimetypes.gest_type () devolverá (None, None). Además, la lista de tipos MIME de Apache es una lista blanca / subconjunto. De ninguna manera es una lista completa de tipos mime. No se puede usar para clasificar todos los archivos como texto o no texto.
Purrell
1
guess_types se basa en la extensión del nombre del archivo, no en el contenido real como lo haría el comando de Unix "archivo".
Eric H.
61

Otro método más basado en el comportamiento del archivo (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))

Ejemplo:

>>> is_binary_string(open('/usr/bin/python', 'rb').read(1024))
True
>>> is_binary_string(open('/usr/bin/dh_python3', 'rb').read(1024))
False
jfs
fuente
Puede obtener falsos positivos y falsos negativos, pero sigue siendo un enfoque inteligente que funciona para la gran mayoría de archivos. +1.
espectros
2
Curiosamente, el archivo (1) en sí mismo también excluye 0x7f de la consideración, por lo que, técnicamente hablando, debería usarlo en su bytearray([7,8,9,10,12,13,27]) + bytearray(range(0x20, 0x7f)) + bytearray(range(0x80, 0x100))lugar. Ver Python, archivo (1) - ¿Por qué se utilizan los números [7,8,9,10,12,13,27] y el rango (0x20, 0x100) para determinar texto frente a archivo binario y github.com/file/file/ blob /…
Martijn Pieters
@MartijnPieters: gracias. Actualicé la respuesta para exclude 0x7f( DEL).
jfs
1
Buena solución usando conjuntos. :-)
Martijn Pieters
¿Por qué excluye 11o VT? En la tabla 11 se considera texto ASCII simple, y este es el vertical tab.
darksky
15

Si está utilizando python3 con utf-8, es sencillo, simplemente abra el archivo en modo texto y detenga el procesamiento si obtiene un archivo UnicodeDecodeError. Python3 usará unicode cuando maneje archivos en modo texto (y bytearray en modo binario) - si su codificación no puede decodificar archivos arbitrarios, es muy probable que lo obtenga UnicodeDecodeError.

Ejemplo:

try:
    with open(filename, "r") as f:
        for l in f:
             process_line(l)
except UnicodeDecodeError:
    pass # Fond non-text data
Rey del cielo
fuente
¿Por qué no usarlo with open(filename, 'r', encoding='utf-8') as fdirectamente?
Terry
8

Si ayuda, muchos tipos binarios comienzan con números mágicos. Aquí hay una lista de firmas de archivos.

Shane C. Mason
fuente
Para eso está libmagic. Se puede acceder en python a través de python-magic .
Bengt,
2
Desafortunadamente, "no comienza con un número mágico conocido" no es equivalente a "es un archivo de texto".
Purrell
8

Prueba esto:

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
fuente
9
-1 define "binario" como que contiene un byte cero. Clasificará los archivos de texto codificados en UTF-16 como "binarios".
John Machin
5
@John Machin: Curiosamente, en git diffrealidad funciona de esta manera y , efectivamente , detecta archivos UTF-16 como binarios.
intuido el
Hunh .. GNU difftambién funciona de esta manera. Tiene problemas similares con los archivos UTF-16. filedetecta correctamente los mismos archivos que el texto UTF-16. No he comprobado grepel código, pero también detecta archivos UTF-16 como binarios.
intuido el
1
+1 @John Machin: utf-16 es un dato de carácter file(1)que no es seguro para imprimir sin conversión, por lo que este método es apropiado en este caso.
jfs
2
-1 - No creo que 'contiene un byte cero' sea una prueba adecuada para binario vs texto, por ejemplo, puedo crear un archivo que contenga todos los bytes 0x01 o repetir 0xDEADBEEF, pero no es un archivo de texto. La respuesta basada en el archivo (1) es mejor.
Sam Watkins
6

Aquí hay una sugerencia que usa el comando de archivo 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)

Uso de ejemplo:

>>> istext ('/ etc / motd') 
Cierto
>>> istext ('/ vmlinuz') 
Falso
>>> abrir ('/ tmp / japonés'). leer ()
'\ 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') # funciona en UTF-8
Cierto

Tiene las desventajas de no ser portátil para Windows (a menos que tenga algo como el filecomando allí) y tener que generar un proceso externo para cada archivo, que puede no ser aceptable.

Jacob Gabrielson
fuente
Esto rompió mi secuencia de comandos :( Investigando, descubrí que algunos archivos de configuración se describen filecomo "Configuración congelada de Sendmail - versión m"; observe la ausencia de la cadena "texto". ¿Quizás usar file -i?
melissa_boiko
1
TypeError: no se puede usar un patrón de cadena en un objeto de tipo bytes
abg
5

Utilice la biblioteca binaryornot ( GitHub ).

Es muy simple y se basa en el código que se encuentra en esta pregunta de stackoverflow.

De hecho, puede escribir esto en 2 líneas de código, sin embargo, este paquete le evita tener que escribir y probar a fondo esas 2 líneas de código con todo tipo de tipos de archivos extraños, multiplataforma.

guettli
fuente
4

Normalmente tienes que adivinar.

Puede ver las extensiones como una pista, si los archivos las tienen.

También puede reconocer formatos binarios conocidos e ignorarlos.

De lo contrario, vea qué proporción de bytes ASCII no imprimibles tiene y adivine a partir de eso.

También puede intentar decodificar desde UTF-8 y ver si eso produce una salida sensible.

Douglas Leeder
fuente
4

Una solución más corta, con una advertencia 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
fuente
nota: for line in filepuede consumir una cantidad ilimitada de memoria hasta que b'\n'se encuentre
jfs
a @Community: ".read()"devuelve aquí una cadena de bytes que es iterable (produce bytes individuales).
jfs
4

Podemos usar Python para verificar si un archivo es binario, porque falla si intentamos abrir un archivo binario en modo texto.

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
fuente
Esto falla para una gran cantidad de archivos `.avi '(video).
Anmol Singh Jaggi
3

Si no está en Windows, puede usar Python Magic para determinar el tipo de archivo. Luego puede verificar si es un tipo de texto / mímica.

Kamil Kisiel
fuente
2

Aquí hay una función que primero verifica si el archivo comienza con una lista de materiales y si no, busca un byte cero dentro de los 8192 bytes iniciales:

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

Técnicamente, la verificación de la lista de materiales de UTF-8 es innecesaria porque no debe contener cero bytes para fines prácticos. Pero como es una codificación muy común, es más rápido verificar la lista de materiales al principio en lugar de escanear todos los 8192 bytes en busca de 0.

roskakori
fuente
2

Intente usar python-magic actualmente mantenido , que no es el mismo módulo en la respuesta de @Kami Kisiel. Esto es compatible con todas las plataformas, incluido Windows, sin embargo, necesitará los libmagicarchivos binarios. Esto se explica en el archivo README.

A diferencia del módulo mimetypes , no usa la extensión del archivo y en su lugar inspecciona el contenido del archivo.

>>> 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'
Comer en Joes
fuente
1

Vine aquí buscando exactamente lo mismo: una solución integral proporcionada por la biblioteca estándar para detectar binarios o texto. Después de revisar las opciones sugeridas por la gente, el comando nix file parece ser la mejor opción (solo estoy desarrollando para linux boxen). Algunos otros publicaron soluciones usando archivos, pero son innecesariamente complicadas en mi opinión, así que esto es lo que se me ocurrió:

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

No hace falta decirlo, pero su código que llama a esta función debe asegurarse de que pueda leer un archivo antes de probarlo, de lo contrario, se detectará por error el archivo como binario.

rsaw
fuente
1

Supongo que la mejor solución es usar la función guess_type. Contiene una lista con varios tipos MIME y también puede incluir sus propios tipos. Aquí viene el script que hice para solucionar mi problema:

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

Está dentro de una clase, como puede ver en base a la estructura de uso del código. Pero prácticamente puede cambiar las cosas que desea implementar dentro de su aplicación. Es bastante simple de usar. El método getTextFiles devuelve un objeto de lista con todos los archivos de texto que residen en el directorio que pasa en la variable de ruta.

Leonardo
fuente
1

en * NIX:

Si tiene acceso al filecomando de shell, shlex puede ayudar a que el módulo de subproceso sea más utilizable:

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()))

O también puede pegarlo en un bucle for para obtener la salida de todos los archivos en el directorio actual usando:

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()))

o para todos los subdirectorios:

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
fuente
1

La mayoría de los programas consideran que el archivo es binario (que es cualquier archivo que no esté "orientado a líneas") si contiene un carácter NULL .

Aquí está la versión de perl de pp_fttext()( pp_sys.c) implementada 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

Tenga en cuenta también que este código se escribió para ejecutarse tanto en Python 2 como en Python 3 sin cambios.

Fuente: "Adivina si el archivo es de texto o binario" de Perl implementado en Python

Kenorb
fuente
0

estas en unix? si es así, intente:

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

Los valores de retorno del shell están invertidos (0 está bien, así que si encuentra "texto", devolverá un 0, y en Python esa es una expresión falsa).

fortran
fuente
Como referencia, el comando de archivo adivina un tipo según el contenido del archivo. No estoy seguro de si presta atención a la extensión del archivo.
David Z
Estoy casi seguro de que se ve tanto en el contenido como en la extensión.
fortran
Esto se rompe si la ruta contiene "texto", aunque. Asegúrese de rsplit en el último ':' (siempre que no haya dos puntos en la descripción del tipo de archivo).
Alan Plum
3
Usar filecon el -binterruptor; imprimirá solo el tipo de archivo sin la ruta.
dubek
2
una versión un poco mejor:is_binary_file = lambda filename: "text" in subprocess.check_output(["file", "-b", filename])
jfs
0

Una forma más sencilla es comprobar si el archivo consta de un carácter NULL ( \x00) utilizando el inoperador, por ejemplo:

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

Vea a continuación el ejemplo completo:

#!/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!')

Uso de muestra:

$ ./is_binary.py /etc/hosts
The file is not binary!
$ ./is_binary.py `which which`
The file is binary!
Kenorb
fuente