Python'da bir dosyanın ikili (metin dışı) olup olmadığını nasıl anlarım?


105

Python'da bir dosyanın ikili (metin dışı) olup olmadığını nasıl anlarım?

Python'da büyük bir dosya kümesinde arama yapıyorum ve ikili dosyalarda eşleşmeler almaya devam ediyorum. Bu, çıktının inanılmaz derecede dağınık görünmesine neden olur.

Kullanabileceğimi biliyorum grep -I, ancak verilerle grep'in izin verdiğinden daha fazlasını yapıyorum.

Geçmişte, bundan daha büyük karakterleri arardım 0x7f, ama utf8ve benzeri, modern sistemlerde bunu imkansız kılıyor. İdeal olarak çözüm hızlı olacaktır, ancak herhangi bir çözüm işe yarar.


EĞER "geçmişte sadece 0x7f'den büyük karakterleri arardım" SONRA düz ASCII metni ile çalışıyordunuz SONRA UTF-8 olarak kodlanan ASCII metni ASCII olarak kaldığından (yani bayt> 127 yok) hala sorun yok.
tzot

@ ΤΖΩΤΖΙΟΥ: Doğru, ama uğraştığım bazı dosyaların utf8 olduğunu biliyorum. Bu dosyaların özel anlamıyla değil, genel anlamda alışık olduğumu söyledim. :)
keder

1
Sadece olasılıkla. Aşağıdakileri kontrol edebilirsiniz: 1) dosya \ n 2) \ n'ler arasındaki bayt miktarı nispeten küçüktür (bu güvenilir DEĞİLDİR) l 3) dosya ASCCI "boşluk" karakterinin ('' değerinden daha küçük bir değere sahip bayt içermiyor mu? ) - "\ n" "\ r" "\ t" ve sıfırlar HARİÇ.
SigTerm

3
grepİkili dosyaları tanımlamak için kullandığı strateji, aşağıdaki Jorge Orpinel tarafından yayınlanan stratejiye benzer . -zSeçeneği belirlemediğiniz sürece "\000", dosyada yalnızca bir boş karakter ( ) tarayacaktır . İle -ztarar "\200". İlgilenenler ve / veya şüpheciler 1126 numaralı satıra bakabilirler grep.c. Maalesef kaynak kodunu içeren bir web sayfası bulamadım, ama tabii ki onu gnu.org'dan veya bir dağıtımdan edinebilirsiniz .
intuited

3
PS Jorge'nin gönderisinin yorum dizisinde belirtildiği gibi, bu strateji örneğin UTF-16 metni içeren dosyalar için yanlış pozitifler verecektir. Bununla birlikte, hem git diffGNU hem diffde aynı stratejiyi kullanır. Bu kadar yaygın olup olmadığından emin değilim çünkü alternatifinden çok daha hızlı ve daha kolay mı yoksa sadece bu araçların kurulu olma eğiliminde olan sistemlerdeki UTF-16 dosyalarının göreceli nadirliği nedeniyle mi?
intuited

Yanıtlar:


42

Ayrıca mimeypes modülünü de kullanabilirsiniz :

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

İkili mime türlerinin bir listesini derlemek oldukça kolaydır. Örneğin Apache, bir liste, ikili ve metin kümesine ayrıştırabileceğiniz ve ardından mime'nin metin veya ikili listenizde olup olmadığını kontrol edebileceğiniz bir mime.types dosyasıyla dağıtır.


16
mimetypesBir dosyanın sadece adı yerine içeriğini kullanmanın bir yolu var mı ?
intuited

4
@intuited Hayır, ancak libmagic bunu yapar. Python-magic ile kullanın .
Bengt

Burada bazı iyi yanıtları olan benzer bir soru var: stackoverflow.com/questions/1446549/… Bir etkin durum reçetesine dayanan yanıt bana iyi görünüyor, yazdırılamayan karakterlerin küçük bir kısmına izin veriyor (ancak bazıları için \ 0 yok nedeni).
Sam Watkins

5
Bu harika bir cevap değil çünkü mimeypes modülü tüm dosyalar için iyi değil. Şimdi sistemin file"UTF-8 Unicode metni, çok uzun satırlar" olarak raporladığı ancak mimetypes.gest_type () döndüren (Yok, Yok) bir dosyaya bakıyorum . Ayrıca, Apache'nin mime türü listesi bir beyaz liste / alt kümedir. Hiçbir şekilde tam bir mime türü listesi değildir. Tüm dosyaları metin veya metin olmayan olarak sınıflandırmak için kullanılamaz.
Purrell

1
Tahmin_türleri, Unix "dosya" komutunun yapacağı gibi gerçek içeriğe değil, dosya adı uzantısına dayanır.
Eric H.

61

Dosya (1) davranışına dayalı başka bir yöntem daha :

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

Misal:

>>> is_binary_string(open('/usr/bin/python', 'rb').read(1024))
True
>>> is_binary_string(open('/usr/bin/dh_python3', 'rb').read(1024))
False

Hem yanlış pozitif hem de yanlış negatifler alabilir, ancak yine de dosyaların büyük çoğunluğu için çalışan akıllıca bir yaklaşımdır. +1.
spectras

2
Yeterince ilginç bir şekilde, (1) dosyasının kendisi de 0x7f'yi dikkate almıyor, bu nedenle teknik olarak konuşursak, bytearray([7,8,9,10,12,13,27]) + bytearray(range(0x20, 0x7f)) + bytearray(range(0x80, 0x100))bunun yerine kullanmalısınız. Bkz. Python, dosya (1) - Metni ve ikili dosya ve github.com/file/file/ karşılaştırmasını
Martijn Pieters

@MartijnPieters: teşekkür ederim. Yanıtı hariç tutmak için güncelledim 0x7f( DEL).
jfs

1
Setleri kullanarak güzel bir çözüm. :-)
Martijn Pieters

Neden hariç tutuyorsunuz 11veya VT? Tablo 11'de düz ASCII metni olarak kabul edilir ve bu vertical tab.
darksky

15

Python3'ü utf-8 ile kullanıyorsanız, basittir, dosyayı metin modunda açın ve bir UnicodeDecodeError. Python3, dosyaları metin modunda (ve ikili modda bytearray) işlerken unicode kullanacaktır - eğer kodlamanız rastgele dosyaların kodunu çözemezse, elde etme olasılığınız oldukça yüksektir UnicodeDecodeError.

Misal:

try:
    with open(filename, "r") as f:
        for l in f:
             process_line(l)
except UnicodeDecodeError:
    pass # Fond non-text data

neden with open(filename, 'r', encoding='utf-8') as fdoğrudan kullanmıyorsunuz ?
Terry

8

İşe yarayacaksa, birçok ikili tür sihirli bir sayı ile başlar. İşte dosya imzalarının bir listesi .


Libmagic bunun için. Python'da python-magic yoluyla erişilebilir .
Bengt

2
Ne yazık ki, "bilinen bir sihirli sayı ile başlamıyor", "bir metin dosyasıdır" ile eşdeğer değildir.
Purrell

8

Bunu dene:

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 <TrentM@ActiveState.com>
    @author: Jorge Orpinel <jorge@orpinel.com>"""
    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

9
-1, "ikili" yi sıfır bayt içeren olarak tanımlar. UTF-16 kodlu metin dosyalarını "ikili" olarak sınıflandırır.
John Machin

5
@John Machin: İlginç bir şekilde, git diffaslında bu şekilde çalışıyor ve kesinlikle UTF-16 dosyalarını ikili olarak algılıyor.
intuited

Hunh .. GNU diffda bu şekilde çalışır. UTF-16 dosyalarıyla benzer sorunları var. fileUTF-16 metniyle aynı dosyaları doğru şekilde algılar. grepKodunu kontrol etmedim , ancak UTF-16 dosyalarını ikili olarak algılıyor.
intuited

1
+1 @John Machin: utf-16 bir karakter verisidir buna göre file(1)dönüştürme olmadan yazdırmak güvenli değildir, bu nedenle bu yöntem bu durumda uygundur.
jfs

2
-1 - 'Sıfır bayt içerir' ifadesinin ikili ve metin için yeterli bir test olduğunu sanmıyorum, örneğin tüm 0x01 baytları içeren bir dosya oluşturabilir veya 0xDEADBEEF'i tekrar edebilirim, ancak bu bir metin dosyası değil. (1) numaralı dosyaya dayalı cevap daha iyidir.
Sam Watkins

6

Unix dosya komutunu kullanan bir öneri :

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)

Örnek kullanım:

>>> istext ('/ etc / motd') 
Doğru
>>> istext ('/ vmlinuz') 
Yanlış
>>> open ('/ tmp / japanese'). oku ()
'\ 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') # UTF-8 üzerinde çalışır
Doğru

Windows için taşınabilir olmamanın ( fileoradaki komuta benzer bir şeye sahip olmadığınız sürece ) ve her dosya için hoş olmayabilecek harici bir işlem oluşturmak zorunda olmanın dezavantajlarına sahiptir .


Bu senaryomu bozdu :( Araştırırken, bazı gizli dosyaların file"Sendmail donmuş yapılandırması - sürüm m" olarak tanımlandığını öğrendim - "metin" dizesinin yokluğuna dikkat edin. Belki file -i?
melissa_boiko

1
TypeError: bayt benzeri bir nesnede dizge kalıbı kullanılamaz
abg

5

Binaryornot kitaplığını ( GitHub ) kullanın .

Bu çok basittir ve bu yığın aşımı sorusunda bulunan koda dayanmaktadır.

Bunu aslında 2 satır kodla yazabilirsiniz, ancak bu paket sizi bu 2 satırlık kodu her türden garip dosya türleriyle, çapraz platformla yazmak ve kapsamlı bir şekilde test etmek zorunda kalmaktan kurtarır.


4

Genellikle tahmin etmen gerekir.

Dosyalarda varsa uzantılara tek bir ipucu olarak bakabilirsiniz.

Ayrıca ikili formatları tanıyabilir ve bunları görmezden gelebilirsiniz.

Aksi takdirde, yazdırılamayan ASCII bayt oranının ne kadar olduğuna bakın ve bundan bir tahminde bulunun.

Ayrıca UTF-8'den kod çözmeyi deneyebilir ve bunun mantıklı bir çıktı üretip üretmediğini görebilirsiniz.


4

UTF-16 uyarısıyla daha kısa bir çözüm:

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

not: for line in filekadar bellek sınırsız miktarda tüketebilir b'\n'bulunursa
jfs

@Community için: ".read()"bir burada bytestring döner bir iterable (tek tek bayt verir).
jfs

4

Bir dosyanın ikili olup olmadığını kontrol etmek için python'u kullanabiliriz, çünkü ikili dosyayı metin modunda açmaya çalışırsak başarısız olur.

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

Bu, birçok ".avi '(video) dosyası için başarısız olur.
Anmol Singh Jaggi

3

Windows'ta değilseniz , dosya türünü belirlemek için Python Magic'i kullanabilirsiniz . Sonra bir metin / mime türü olup olmadığını kontrol edebilirsiniz.


2

İlk olarak dosyanın bir malzeme listesi ile başlayıp başlamadığını ve ilk 8192 bayt içinde sıfır bayt aramadığını kontrol eden bir işlev:

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

Teknik olarak UTF-8 BOM'un kontrolü gereksizdir çünkü tüm pratik amaçlar için sıfır bayt içermemelidir. Ancak çok yaygın bir kodlama olduğu için, tüm 8192 baytı 0 için taramak yerine başlangıçta BOM'u kontrol etmek daha hızlıdır.


2

@Kami Kisiel'in cevabında aynı modül olmayan şu anda korunan python- magic'i kullanmayı deneyin . Bu, Windows dahil tüm platformları destekler, ancaklibmagic ikili dosyalara . Bu BENİOKU'da açıklanmıştır.

Mimeypes modülünden farklı olarak , dosyanın uzantısını kullanmaz ve bunun yerine dosyanın içeriğini inceler.

>>> 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'

1

Buraya tam olarak aynı şeyi aramak için geldim - ikili veya metni algılamak için standart kitaplık tarafından sağlanan kapsamlı bir çözüm. İnsanların önerdiği seçenekleri gözden geçirdikten sonra, nix dosya komutu en iyi seçenek gibi görünüyor (ben sadece linux boxen için geliştiriyorum). Bazıları dosya kullanarak çözümler yayınladılar, ancak bence gereksiz yere karmaşıklar, işte bulduğum şey:

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

Söylemeye gerek yok, ancak bu işlevi çağıran kodunuz, bir dosyayı test etmeden önce okuyabildiğinizden emin olmalıdır, aksi takdirde bu, yanlışlıkla dosyayı ikili olarak algılar.


1

Sanırım en iyi çözüm tahmin_tür işlevini kullanmaktır. Birkaç mime türü içeren bir liste tutar ve kendi türlerinizi de dahil edebilirsiniz. Sorunumu çözmek için yaptığım senaryo geliyor:

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

Kodun yapısına göre görebileceğiniz gibi, bir Sınıfın içindedir. Ancak, uygulamanızda uygulamak istediğiniz şeyleri hemen hemen değiştirebilirsiniz. Kullanımı oldukça basit. GetTextFiles yöntemi, yol değişkeninde ilettiğiniz dizinde bulunan tüm metin dosyalarını içeren bir liste nesnesi döndürür.


1

* NIX üzerinde:

fileKabuk komutuna erişiminiz varsa , shlex, alt işlem modülünü daha kullanışlı hale getirmeye yardımcı olabilir:

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

Veya, şu komutu kullanarak geçerli dizindeki tüm dosyalar için çıktı almak için bunu bir döngü içinde yapıştırabilirsiniz:

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

veya tüm alt dizinler için:

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

1

Programların çoğu, bir NULL karakter içeriyorsa, dosyanın ikili ("satır odaklı" olmayan herhangi bir dosya) olduğunu düşünür .

Python'da uygulanan perl sürümü pp_fttext()( pp_sys.c):

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

Ayrıca bu kodun hem Python 2 hem de Python 3 üzerinde değişiklik yapılmadan çalışacak şekilde yazıldığını unutmayın.

Kaynak: Perl'in Python'da uygulanan "dosyanın metin mi yoksa ikili mi olduğunu tahmin et"


0

unix'te misin? eğer öyleyse, şunu dene:

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

Kabuk dönüş değerleri tersine çevrilir (0 tamam, yani "metin" bulursa 0 döndürür ve Python'da bu bir Yanlış ifade).


Başvuru için, file komutu dosyanın içeriğine bağlı olarak bir tür tahmin eder. Dosya uzantısına dikkat edip etmediğinden emin değilim.
David Z

Hem içerikte hem de uzantıda göründüğünden neredeyse eminim.
fortran

Yol "metin" içeriyorsa bu, tho. Son ':' (dosya türü açıklamasında iki nokta üst üste olmaması koşuluyla) noktadan ayırdığınızdan emin olun.
Alan Plum

3
Kullanım fileile -banahtarı; yol olmadan yalnızca dosya türünü yazdırır.
dubek

2
biraz daha güzel bir versiyon:is_binary_file = lambda filename: "text" in subprocess.check_output(["file", "-b", filename])
jfs

0

Daha basit yol, operatör \x00kullanarak dosyanın NULL karakter ( ) içerip içermediğini kontrol etmektir in, örneğin:

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

Tam örneğe bakın:

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

Örnek kullanım:

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.