Jak mogę wykryć, czy plik jest binarny (nietekstowy) w Pythonie?

105

Jak mogę sprawdzić, czy plik jest binarny (nietekstowy) w Pythonie?

Przeszukuję duży zestaw plików w Pythonie i ciągle otrzymuję dopasowania w plikach binarnych. To sprawia, że ​​wydruk wygląda niesamowicie niechlujnie.

Wiem, że mógłbym użyć grep -I, ale robię więcej z danymi, niż pozwala na to grep.

W przeszłości szukałbym po prostu znaków większych niż 0x7f, ale utf8i tym podobnych, uniemożliwiających to w nowoczesnych systemach. Idealnie rozwiązanie byłoby szybkie, ale każde rozwiązanie wystarczy.

smucić
źródło
JEŻELI "w przeszłości szukałbym po prostu znaków większych niż 0x7f" WTEDY pracowałeś ze zwykłym tekstem ASCII WTEDY nadal nie ma problemu, ponieważ tekst ASCII zakodowany jako UTF-8 pozostaje ASCII (tj. Bez bajtów> 127).
tzot
@ ΤΖΩΤΖΙΟΥ: To prawda, ale tak się składa, że ​​wiem, że niektóre z plików, z którymi mam do czynienia, to utf8. Miałem na myśli w ogólnym sensie, a nie w konkretnym znaczeniu tych plików. :)
smuć się
1
Tylko z prawdopodobieństwem. Możesz sprawdzić, czy: 1) plik zawiera \ n 2) Ilość bajtów między \ n jest stosunkowo mała (to NIE jest wiarygodne) l 3) plik nie zawiera bajtów z wartością mniejszą niż wartość znaku spacji ASCCI ('' ) - Z WYJĄTKIEM "\ n" "\ r" "\ t" i zer.
SigTerm
3
Strategia, której grepużywa się do identyfikacji plików binarnych, jest podobna do tej opublikowanej przez Jorge Orpinela poniżej . O ile nie ustawisz tej -zopcji, po prostu skanuje w poszukiwaniu znaku null ( "\000") w pliku. Dzięki -ztemu skanuje w poszukiwaniu plików "\200". Zainteresowani i / lub sceptyczni mogą sprawdzić linię 1126 of grep.c. Przepraszamy, nie mogłem znaleźć strony internetowej z kodem źródłowym, ale oczywiście możesz ją pobrać z gnu.org lub za pośrednictwem dystrybucji .
intuicyjny
3
PS Jak wspomniano w wątku komentarzy do posta Jorge, strategia ta da fałszywe alarmy w przypadku plików zawierających na przykład tekst UTF-16. Niemniej jednak zarówno, jak git diffi GNU diffrównież używają tej samej strategii. Nie jestem pewien, czy jest tak powszechny, ponieważ jest o wiele szybszy i łatwiejszy niż alternatywa, czy też jest to spowodowane względną rzadkością plików UTF-16 w systemach, które mają zwykle zainstalowane te narzędzia.
intuicja

Odpowiedzi:

42

Możesz także skorzystać z modułu MIME :

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

Utworzenie listy binarnych typów MIME jest dość łatwe. Na przykład Apache dystrybuuje plik mime.types, który można przeanalizować w zestaw list, plików binarnych i tekstowych, a następnie sprawdzić, czy mime znajduje się na liście tekstowej lub binarnej.

Gavin M. Roy
źródło
16
Czy istnieje sposób, aby mimetypesużyć zawartości pliku, a nie tylko jego nazwy?
intuicja
4
@intuited Nie, ale libmagic to robi. Użyj go za pomocą magii Pythona .
Bengt
Jest podobne pytanie z kilkoma dobrymi odpowiedziami: stackoverflow.com/questions/1446549/… Odpowiedź oparta na przepisie na status aktywny wygląda dla mnie dobrze, dopuszcza niewielki odsetek niedrukowalnych znaków (ale nie \ 0, dla niektórych powód).
Sam Watkins,
5
To nie jest dobra odpowiedź tylko dlatego, że moduł MIME nie jest dobry dla wszystkich plików. Patrzę teraz na plik, który system filezgłasza jako „tekst UTF-8 Unicode, z bardzo długimi liniami”, ale zwróci mimetypes.gest_type () (Brak, Brak). Ponadto lista typów MIME Apache jest białą listą / podzbiorem. Nie jest to bynajmniej pełna lista typów MIME. Nie można go użyć do sklasyfikowania wszystkich plików jako tekstowych lub nietekstowych.
Purrell
1
guess_types jest oparte na rozszerzeniu nazwy pliku, a nie na rzeczywistej zawartości, jak zrobiłoby to polecenie "plik" systemu Unix.
Eric H.
61

Jeszcze inna metoda oparta na zachowaniu 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))

Przykład:

>>> is_binary_string(open('/usr/bin/python', 'rb').read(1024))
True
>>> is_binary_string(open('/usr/bin/dh_python3', 'rb').read(1024))
False
jfs
źródło
Może uzyskać zarówno fałszywie pozytywne, jak i fałszywie negatywne wartości, ale nadal jest to sprytne podejście, które działa w przypadku większości plików. +1.
widma
2
Co ciekawe, sam plik (1) wyklucza 0x7f z rozważań, więc technicznie rzecz biorąc, powinieneś używać bytearray([7,8,9,10,12,13,27]) + bytearray(range(0x20, 0x7f)) + bytearray(range(0x80, 0x100))zamiast niego. Zobacz Python, plik (1) - Dlaczego liczby [7,8,9,10,12,13,27] i zakres (0x20, 0x100) są używane do określania tekstu względem pliku binarnego i github.com/file/file/ blob /…
Martijn Pieters
@MartijnPieters: dziękuję. Zaktualizowałem odpowiedź, aby wykluczyć 0x7f( DEL).
jfs
1
Niezłe rozwiązanie przy użyciu zestawów. :-)
Martijn Pieters
Dlaczego wykluczasz 11lub VT? W tabeli 11 jest uważany za zwykły tekst ASCII, a to jest vertical tab.
darksky
15

Jeśli używasz python3 z utf-8, jest to proste, po prostu otwórz plik w trybie tekstowym i zatrzymaj przetwarzanie, jeśli pojawi się plik UnicodeDecodeError. Python3 użyje Unicode podczas obsługi plików w trybie tekstowym (i bytearray w trybie binarnym) - jeśli twoje kodowanie nie może zdekodować dowolnych plików, jest całkiem prawdopodobne, że otrzymaszUnicodeDecodeError .

Przykład:

try:
    with open(filename, "r") as f:
        for l in f:
             process_line(l)
except UnicodeDecodeError:
    pass # Fond non-text data
Król nieba
źródło
dlaczego nie używać with open(filename, 'r', encoding='utf-8') as fbezpośrednio?
Terry
8

Jeśli to pomoże, wiele typów binarnych zaczyna się od magicznych liczb. Oto lista podpisów plików.

Shane C. Mason
źródło
Po to jest libmagic. Dostęp do niego można uzyskać w pythonie za pośrednictwem python-magic .
Bengt
2
Niestety, „nie zaczyna się od znanej magicznej liczby” nie jest równoznaczne z „jest plikiem tekstowym”.
Purrell
8

Spróbuj tego:

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
źródło
9
-1 definiuje „binarny” jako zawierający bajt zerowy. Zaklasyfikuje pliki tekstowe zakodowane w UTF-16 jako „binarne”.
John Machin
5
@John Machin: Co ciekawe, git difffaktycznie działa w ten sposób i na pewno wykrywa pliki UTF-16 jako binarne.
intuicja
Hunh .. GNU diffteż działa w ten sposób. Ma podobne problemy z plikami UTF-16. filepoprawnie wykrywa te same pliki, co tekst UTF-16. Nie pobrałem grepkodu, ale on również wykrywa pliki UTF-16 jako binarne.
intuicyjny
1
+1 @John Machin: utf-16 to dane znakowe, zgodnie z file(1)którymi drukowanie bez konwersji nie jest bezpieczne, więc ta metoda jest odpowiednia w tym przypadku.
jfs
2
-1 - nie sądzę, że „zawiera bajt zerowy” jest odpowiednim testem dla binarnego vs tekstowego, na przykład mogę utworzyć plik zawierający wszystkie 0x01 bajtów lub powtórzyć 0xDEADBEEF, ale nie jest to plik tekstowy. Odpowiedź oparta na pliku (1) jest lepsza.
Sam Watkins,
6

Oto sugestia, która używa polecenia pliku systemu 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)

Przykładowe użycie:

>>> istext („/ etc / motd”) 
Prawdziwe
>>> istext („/ vmlinuz”) 
Fałszywe
>>> 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') # działa na UTF-8
Prawdziwe

Ma wady polegające na tym, że nie jest przenośny do systemu Windows (chyba że masz tam coś takiego, jak filepolecenie) i konieczność tworzenia zewnętrznego procesu dla każdego pliku, który może nie być przyjemny.

Jacob Gabrielson
źródło
To zepsuło mój skrypt :( Po przeprowadzeniu dochodzenia dowiedziałem się, że niektóre pliki konfiguracyjne są opisane filejako „Zamrożona konfiguracja Sendmaila - wersja m” - zauważ brak ciągu znaków „tekst”. Może użyć file -i?
melissa_boiko
1
TypeError: nie można użyć wzorca łańcuchowego na obiekcie podobnym do bajtów
abg
5

Użyj biblioteki binaryornot ( GitHub ).

Jest to bardzo proste i oparte na kodzie znalezionym w tym pytaniu dotyczącym stackoverflow.

W rzeczywistości możesz napisać to w 2 wierszach kodu, jednak ten pakiet oszczędza ci konieczności pisania i dokładnego testowania tych 2 wierszy kodu z różnymi dziwnymi typami plików, na różnych platformach.

guettli
źródło
4

Zwykle musisz zgadywać.

Możesz spojrzeć na rozszerzenia jako jedną wskazówkę, jeśli pliki je mają.

Możesz także rozpoznać znane formaty binarne i zignorować je.

W przeciwnym razie zobacz, jaki procent posiadasz niedrukowalnych bajtów ASCII i zgadnij z tego.

Możesz także spróbować dekodować z UTF-8 i sprawdzić, czy daje to rozsądny wynik.

Douglas Leeder
źródło
4

Krótsze rozwiązanie z ostrzeżeniem 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
źródło
uwaga: for line in filemoże zużywać nieograniczoną ilość pamięci, dopóki nie b'\n'zostanie znaleziony
jfs
to @Community: ".read()"zwraca tutaj bytestring, który jest iterowalny (daje pojedyncze bajty).
jfs
4

Możemy użyć samego Pythona, aby sprawdzić, czy plik jest binarny, ponieważ nie powiedzie się, jeśli spróbujemy otworzyć plik binarny w trybie tekstowym

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
źródło
To kończy się niepowodzeniem dla wielu plików `.avi '(wideo).
Anmol Singh Jaggi
3

Jeśli nie korzystasz z systemu Windows, możesz użyć Python Magic do określenia typu pliku. Następnie możesz sprawdzić, czy jest to typ tekstowy / mime.

Kamil Kisiel
źródło
2

Oto funkcja, która najpierw sprawdza, czy plik zaczyna się od BOM, a jeśli nie, szuka bajtu zerowego w początkowych 8192 bajtach:

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

Z technicznego punktu widzenia sprawdzenie BOM UTF-8 jest niepotrzebne, ponieważ nie powinno zawierać zerowych bajtów ze wszystkich praktycznych powodów. Ale ponieważ jest to bardzo powszechne kodowanie, szybciej jest sprawdzić BOM na początku, zamiast skanować wszystkie 8192 bajty w poszukiwaniu 0.

roskakori
źródło
2

Spróbuj użyć aktualnie utrzymywanej magii Pythona, która nie jest tym samym modułem w odpowiedzi @Kami Kisiel. Obsługuje to wszystkie platformy, w tym Windows, jednak będziesz potrzebować libmagicplików binarnych. Jest to wyjaśnione w README.

W przeciwieństwie do modułu mimetypes , nie używa rozszerzenia pliku i zamiast tego sprawdza zawartość pliku.

>>> 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'
Jedz w Joes
źródło
1

Przyjechałem tutaj, szukając dokładnie tego samego - kompleksowego rozwiązania dostarczanego przez bibliotekę standardową do wykrywania plików binarnych lub tekstowych. Po przejrzeniu opcji sugerowanych przez ludzi, polecenie nix file wydaje się być najlepszym wyborem (rozwijam się tylko dla linux boxen). Niektórzy inni opublikowali rozwiązania wykorzystujące plik, ale moim zdaniem są one niepotrzebnie skomplikowane, więc oto co wymyśliłem:

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

Powinno być oczywiste, ale Twój kod, który wywołuje tę funkcję, powinien upewnić się, że możesz odczytać plik przed jego przetestowaniem, w przeciwnym razie zostanie to omyłkowo wykryte jako binarny.

rsaw
źródło
1

Myślę, że najlepszym rozwiązaniem jest użycie funkcji guess_type. Zawiera listę z kilkoma typami MIME, a także możesz dołączyć własne typy. Oto skrypt, który zrobiłem, aby rozwiązać mój problem:

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

Znajduje się wewnątrz klasy, jak widać na podstawie struktury kodu. Ale możesz prawie zmienić rzeczy, które chcesz zaimplementować w swojej aplikacji. Jest dość prosty w użyciu. Metoda getTextFiles zwraca obiekt listy ze wszystkimi plikami tekstowymi, które znajdują się w katalogu, który przekazujesz w zmiennej path.

Leonardo
źródło
1

na * NIX:

Jeśli masz dostęp do polecenia filepowłoki, shlex może pomóc uczynić moduł podprocesu bardziej użytecznym:

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

Możesz też umieścić to w pętli for, aby uzyskać dane wyjściowe dla wszystkich plików w bieżącym katalogu, używając:

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

lub dla wszystkich podkatalogów:

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
źródło
1

Większość programów uważa, że ​​plik jest binarny (czyli każdy plik, który nie jest „zorientowany liniowo”), jeśli zawiera znak NULL .

Oto wersja perla pp_fttext()( pp_sys.c) zaimplementowana w Pythonie:

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

Zauważ również, że ten kod został napisany tak, aby działał zarówno w Pythonie 2, jak i Pythonie 3 bez zmian.

Źródło: Perl „Zgadnij, czy plik jest tekstowy czy binarny” zaimplementowany w Pythonie

kenorb
źródło
0

jesteś w unixie? jeśli tak, spróbuj:

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

Wartości zwracane przez powłokę są odwracane (0 jest w porządku, więc jeśli znajdzie "tekst", zwróci 0, aw Pythonie jest to fałszywe wyrażenie).

fortran
źródło
Dla porównania, polecenie file zgaduje typ na podstawie zawartości pliku. Nie jestem pewien, czy zwraca uwagę na rozszerzenie pliku.
David Z
Jestem prawie pewien, że wygląda to zarówno w treści, jak i rozszerzeniu.
fortran
To się psuje, jeśli ścieżka zawiera słowo „tekst”, chociaż. Upewnij się, że wykonałeś rsplit na ostatnim ':' (pod warunkiem, że w opisie typu pliku nie ma dwukropka).
Alan Plum
3
Użyj filez -bprzełącznikiem; wypisze tylko typ pliku bez ścieżki.
dubek
2
nieco ładniejsza wersja:is_binary_file = lambda filename: "text" in subprocess.check_output(["file", "-b", filename])
jfs
0

Prostszym sposobem jest sprawdzenie, czy plik zawiera znak NULL ( \x00) za pomocą inoperatora, na przykład:

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

Zobacz poniżej pełny przykład:

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

Przykładowe użycie:

$ ./is_binary.py /etc/hosts
The file is not binary!
$ ./is_binary.py `which which`
The file is binary!
kenorb
źródło
0
from binaryornot.check import is_binary
is_binary('filename')

Dokumentacja

parZval
źródło