Como posso detectar se um arquivo é binário (não texto) em python?

105

Como posso saber se um arquivo é binário (não texto) em python?

Estou pesquisando um grande conjunto de arquivos em python e continuo obtendo correspondências em arquivos binários. Isso faz com que a saída pareça incrivelmente confusa.

Eu sei que poderia usar grep -I, mas estou fazendo mais com os dados do que o grep permite.

No passado, eu teria apenas procurado por caracteres maiores do que 0x7f, mas utf8e assim por diante, tornam isso impossível em sistemas modernos. Idealmente, a solução seria rápida, mas qualquer solução serve.

lamentar
fonte
IF "no passado, eu teria apenas pesquisado por caracteres maiores que 0x7f" ENTÃO você costumava trabalhar com texto ASCII simples ENTÃO ainda não há problema, pois o texto ASCII codificado como UTF-8 permanece ASCII (ou seja, sem bytes> 127).
tzot
@ ΤΖΩΤΖΙΟΥ: Verdade, mas sei que alguns dos arquivos com os quais estou lidando são utf8. Eu quis dizer costumava no sentido geral, não no sentido específico desses arquivos. :)
luto
1
Apenas com probabilidade. Você pode verificar se: 1) o arquivo contém \ n 2) A quantidade de bytes entre \ n's é relativamente pequena (isto NÃO é confiável) l 3) o arquivo não contém bytes com valor menor que o valor do caractere "espaço" ASCCI ('' ) - EXCETO "\ n" "\ r" "\ t" e zeros.
SigTerm
3
A estratégia que grepse utiliza para identificar arquivos binários é semelhante à postada por Jorge Orpinel abaixo . A menos que você defina a -zopção, ele irá apenas procurar um caractere nulo ( "\000") no arquivo. Com -z, ele procura "\200". Os interessados ​​e / ou céticos podem verificar a linha 1126 de grep.c. Não consegui encontrar uma página da web com o código-fonte, mas é claro que você pode obtê-la em gnu.org ou em uma distro .
intuído em
3
PS Conforme mencionado no tópico de comentários da postagem de Jorge, essa estratégia dará falsos positivos para arquivos que contenham, por exemplo, texto UTF-16. No entanto, ambos git diffe o GNU difftambém usam a mesma estratégia. Não tenho certeza se é tão predominante porque é muito mais rápido e fácil do que a alternativa, ou se é apenas por causa da relativa raridade de arquivos UTF-16 em sistemas que tendem a ter esses utilitários instalados.
intuído em

Respostas:

42

Você também pode usar o módulo mimetypes :

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

É bastante fácil compilar uma lista de tipos MIME binários. Por exemplo, o Apache distribui com um arquivo mime.types que você pode analisar em um conjunto de listas, binários e texto e, em seguida, verificar se o mime está em sua lista de texto ou binária.

Gavin M. Roy
fonte
16
Existe uma maneira de começar mimetypesa usar o conteúdo de um arquivo em vez de apenas seu nome?
intuído em
4
@intuited Não, mas libmagic faz isso. Use-o via magia python .
Bengt
Há uma pergunta semelhante com algumas boas respostas aqui: stackoverflow.com/questions/1446549/… A resposta baseada em uma receita de estado ativo parece boa para mim, permite uma pequena proporção de caracteres não imprimíveis (mas não \ 0, para alguns razão).
Sam Watkins
5
Esta não é uma boa resposta apenas porque o módulo mimetypes não é bom para todos os arquivos. Estou vendo um arquivo agora que o sistema filerelata como "texto Unicode UTF-8, com linhas muito longas", mas mimetypes.gest_type () retornará (nenhum, nenhum). Além disso, a lista de tipos MIME do Apache é uma lista de permissões / subconjunto. Não é de forma alguma uma lista completa de tipos MIME. Não pode ser usado para classificar todos os arquivos como texto ou não texto.
Purrell de
1
guess_types é baseado na extensão do nome do arquivo, não no conteúdo real como o comando "arquivo" do Unix faria.
Eric H.
61

Ainda outro método baseado no comportamento do arquivo (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))

Exemplo:

>>> is_binary_string(open('/usr/bin/python', 'rb').read(1024))
True
>>> is_binary_string(open('/usr/bin/dh_python3', 'rb').read(1024))
False
jfs
fonte
Pode obter falsos positivos e falsos negativos, mas ainda é uma abordagem inteligente que funciona para a grande maioria dos arquivos. +1.
spectras
2
Curiosamente, o próprio arquivo (1) exclui 0x7f da consideração também, portanto, tecnicamente falando, você deve usá-lo bytearray([7,8,9,10,12,13,27]) + bytearray(range(0x20, 0x7f)) + bytearray(range(0x80, 0x100)). Consulte Python, arquivo (1) - Por que os números [7,8,9,10,12,13,27] e o intervalo (0x20, 0x100) são usados ​​para determinar texto x arquivo binário e github.com/file/file/ blob /…
Martijn Pieters
@MartijnPieters: obrigado. Eu atualizei a resposta para excluir 0x7f( DEL).
jfs
1
Boa solução usando conjuntos. :-)
Martijn Pieters
Por que você exclui 11ou VT? Na tabela 11 é considerado texto ASCII simples, e este é o vertical tab.
darksky
15

Se você estiver usando python3 com utf-8, é simples, basta abrir o arquivo no modo de texto e parar o processamento se obtiver um UnicodeDecodeError. Python3 usará unicode ao lidar com arquivos em modo texto (e bytearray em modo binário) - se sua codificação não puder decodificar arquivos arbitrários, é bem provável que você os obtenha UnicodeDecodeError.

Exemplo:

try:
    with open(filename, "r") as f:
        for l in f:
             process_line(l)
except UnicodeDecodeError:
    pass # Fond non-text data
skyking
fonte
por que não usar with open(filename, 'r', encoding='utf-8') as fdiretamente?
Terry
8

Se ajudar, muitos tipos binários começam com números mágicos. Aqui está uma lista de assinaturas de arquivo.

Shane C. Mason
fonte
É para isso que serve o libmagic. Ele pode ser acessado em python via python-magic .
Bengt
2
Infelizmente, "não começa com um número mágico conhecido" não é equivalente a "é um arquivo de texto".
Purrell
8

Experimente isto:

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
fonte
9
-1 define "binário" como contendo um byte zero. Classificará os arquivos de texto codificados em UTF-16 como "binários".
John Machin
5
@John Machin: Curiosamente, git diffna verdade funciona dessa maneira e, com certeza, detecta arquivos UTF-16 como binários.
intuído em
Hunh .. GNU difftambém funciona dessa maneira. Ele tem problemas semelhantes com arquivos UTF-16. filedetecta corretamente os mesmos arquivos de texto UTF-16. Não verifiquei o grepcódigo de, mas ele também detecta arquivos UTF-16 como binários.
intuído em
1
+1 @John Machin: utf-16 é um dado de caractere de acordo com file(1)que não é seguro imprimir sem conversão, portanto, este método é apropriado neste caso.
jfs
2
-1 - Não acho que 'contém um byte zero' seja um teste adequado para binário vs texto, por exemplo, posso criar um arquivo contendo todos os bytes 0x01 ou repetir 0xDEADBEEF, mas não é um arquivo de texto. A resposta baseada no arquivo (1) é melhor.
Sam Watkins
6

Aqui está uma sugestão que usa o comando de arquivo 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)

Exemplo de uso:

>>> istext ('/ etc / motd') 
Verdade
>>> istext ('/ vmlinuz') 
Falso
>>> 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') # funciona em UTF-8
Verdade

Ele tem o lado ruim de não ser portátil para o Windows (a menos que você tenha algo como o filecomando lá) e ter que gerar um processo externo para cada arquivo, que pode não ser palatável.

Jacob Gabrielson
fonte
Isso quebrou meu script :( Investigando, descobri que alguns conffiles são descritos filecomo "Configuração congelada do Sendmail - versão m" - observe a ausência da string "texto". Talvez use file -i?
melissa_boiko
1
TypeError: não é possível usar um padrão de string em um objeto semelhante a bytes
abg
5

Use a biblioteca binaryornot ( GitHub ).

É muito simples e baseado no código encontrado nesta questão stackoverflow.

Você pode realmente escrever isso em 2 linhas de código, no entanto, este pacote evita que você tenha que escrever e testar completamente essas 2 linhas de código com todos os tipos de arquivos estranhos, multiplataforma.

Guettli
fonte
4

Normalmente você tem que adivinhar.

Você pode ver as extensões como uma pista, se os arquivos as tiverem.

Você também pode reconhecer formatos binários conhecidos e ignorá-los.

Caso contrário, veja que proporção de bytes ASCII não imprimíveis você tem e faça uma estimativa a partir disso.

Você também pode tentar decodificar de UTF-8 e ver se isso produz uma saída razoável.

Douglas Leeder
fonte
4

Uma solução mais curta, com um aviso 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
fonte
observação: for line in filepode consumir uma quantidade ilimitada de memória até b'\n'ser encontrado
jfs
para @Community: ".read()"retorna uma cadeia de bytes aqui que é iterável (produz bytes individuais).
jfs
4

Podemos usar o próprio python para verificar se um arquivo é binário, porque ele falha se tentarmos abrir o arquivo binário em modo de 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
fonte
Isso falha para muitos arquivos `.avi '(vídeo).
Anmol Singh Jaggi
3

Se você não estiver no Windows, pode usar o Python Magic para determinar o tipo de arquivo. Em seguida, você pode verificar se é um tipo de texto / mime.

Kamil Kisiel
fonte
2

Aqui está uma função que primeiro verifica se o arquivo começa com um BOM e, se não, procura um byte zero dentro dos 8192 bytes iniciais:

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

Tecnicamente, a verificação do BOM UTF-8 é desnecessária porque ele não deve conter bytes zero para todos os fins práticos. Mas como é uma codificação muito comum, é mais rápido verificar o BOM no início em vez de verificar todos os 8192 bytes em busca de 0.

roskakori
fonte
2

Tente usar a python-magic mantida atualmente, que não é o mesmo módulo na resposta de @Kami Kisiel. Isso oferece suporte a todas as plataformas, incluindo Windows, no entanto, você precisará dos libmagicarquivos binários. Isso é explicado no README.

Ao contrário do módulo mimetypes , ele não usa a extensão do arquivo e, em vez disso, inspeciona o conteúdo do arquivo.

>>> 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 no Joes
fonte
1

Vim aqui procurando exatamente a mesma coisa - uma solução abrangente fornecida pela biblioteca padrão para detectar binário ou texto. Depois de revisar as opções que as pessoas sugeriram, o comando nix file parece ser a melhor escolha (estou desenvolvendo apenas para linux boxen). Alguns outros postaram soluções usando o arquivo, mas elas são desnecessariamente complicadas na minha opinião, então aqui está o que eu descobri:

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

Não é preciso dizer, mas o código que chama esta função deve garantir que você possa ler um arquivo antes de testá-lo, caso contrário, ele será detectado por engano como binário.

rsaw
fonte
1

Eu acho que a melhor solução é usar a função guess_type. Ele contém uma lista com vários tipos MIME e você também pode incluir seus próprios tipos. Aí vem o script que fiz para resolver meu 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

Ele está dentro de uma classe, como você pode ver com base na estrutura de uso do código. Mas você pode alterar muito as coisas que deseja implementar em seu aplicativo. É muito simples de usar. O método getTextFiles retorna um objeto de lista com todos os arquivos de texto que residem no diretório que você passa na variável de caminho.

Leonardo
fonte
1

em * NIX:

Se você tiver acesso ao filecomando shell, shlex pode ajudar a tornar o módulo de subprocesso mais utilizável:

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 você também pode inserir isso em um loop for para obter a saída de todos os arquivos no diretório atual 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()))

ou para todos os subdiretórios:

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

A maioria dos programas considera o arquivo binário (que é qualquer arquivo que não seja "orientado por linha") se contiver um caractere NULL .

Aqui está a versão de perl de pp_fttext()( pp_sys.c) implementada em 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

Observe também que este código foi escrito para rodar em Python 2 e Python 3 sem alterações.

Fonte: "adivinhe se o arquivo é texto ou binário" do Perl implementado em Python

Kenorb
fonte
0

você está no unix? se sim, tente:

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

Os valores de retorno do shell são invertidos (0 está certo, portanto, se encontrar "texto", ele retornará 0, e em Python essa é uma expressão falsa).

Fortran
fonte
Para referência, o comando de arquivo adivinha um tipo com base no conteúdo do arquivo. Não tenho certeza se ele dá alguma atenção à extensão do arquivo.
David Z
Tenho quase certeza de que parece tanto no conteúdo quanto na extensão.
fortran de
Isso é interrompido se o caminho contiver "texto", embora. Certifique-se de rsplit no último ':' (desde que não haja dois pontos na descrição do tipo de arquivo).
Alan Plum,
3
Use filecom o -bswitch; ele imprimirá apenas o tipo de arquivo sem o caminho.
dubek
2
uma versão um pouco melhor:is_binary_file = lambda filename: "text" in subprocess.check_output(["file", "-b", filename])
jfs
0

A maneira mais simples é verificar se o arquivo consiste no caractere NULL ( \x00) usando o inoperador, por exemplo:

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

Veja abaixo o exemplo 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 amostra:

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

Documentação

parZval
fonte