Textové vs binární soubory

Od Petr Zemek, 2015-08-26

Podíváme se krátce na to, v čem se liší zpracování textových a binárních souborů. Vše si budeme ilustrovat v Pythonu. Tento příspěvek volně navazuje na můj předchozí příspěvek o rozdílech mezi znakovou sadou a kódováním.

Na úvod

Programovací jazyky a jejich standardní knihovny typicky při zpracování souborů rozlišují mezi textovým a binárním režimem. Např. v Pythonu je to podobné, jako v jazyce C: při otevírání souboru specifikujete mód, ve kterém se má soubor otevřít. Výchozí je textové zpracování ("r"). Pro binární zpracování se uvádí mód "rb". Je však dobré si uvědomit, že na úrovni souborového systému a disku není zpravidla mezi textovým a binárním souborem žádný rozdíl (obojí jsou jen data). K onomu rozdílu dochází až při zpracování souborů. Každopádně, jako binární se běžně označují ty soubory, které nejsou čitelné v textovém editoru (např. spustitelné programy obsahující strojový kód, hudba či obrázky).

Pro úplnost: všechny ukázky níže jsou v Pythonu verze 3.

Binární zpracování souboru

Začneme binárním zpracováním, které je jednodušší. Mějme soubor test, který obsahuje 10 náhodných bajtů dat:

$ dd if=/dev/urandom of=test bs=1 count=10
 
$ hexdump -C test
00000000  ff 9f 16 c8 a9 c9 a8 6c  75 3b                    |.......lu;|
0000000a

Pokud jej otevřeme v binárním režimu v Pythonu a přečteme, dostaneme posloupnost přesně 10 bajtů:

>>> with open('test', 'rb') as f:
...     data = f.read()
...
>>> len(data)
10
>>> data
b'\xff\x9f\x16\xc8\xa9\xc9\xa8lu;'

Onen prefix b značí, že se jedná o posloupnost bajtů a nikoliv text (u textových řetězců není žádný prefix). Jelikož poslední tři bajty odpovídají znakům l, u a ; z ASCII, tak jsou tyto bajty vytištěny jako znaky (Python to tak dělá). Důležité je, že v proměnné data máme 10 bajtů. To, že tři z nich odpovídají znakům z ASCII je náhoda.

Binární režim se vyznačuje tím, že po přečtení souboru dostaneme přesně ty stejné bajty, jako se nachází v souboru. Nedochází k žádným konverzím. To je velmi žádané chování. Pokud např. chcete zpracovávat formát MP3 (audio), určitě byste nechtěli, abyste o některé bajty přišli či došlo k jejich automatické změně. Místo hudby byste pak slyšeli např. šum.

Textové zpracování souboru

U textového režimu dochází oproti binárnímu režimu obecně ke dvěma zásadním rozdílům:

  1. Dekódování. Znaky každého textového souboru jsou zapsány v nějakém kódování (Windows-1250, ISO 8859-2, UTF-8 atd.). Při čtení souboru dochází k jejich dekódování.
  2. Konverze konců řádků. Pokud si otevřete textový soubor z Windows (konce řádků CRLF alias \r\n) a přečtete jej, tak je každá tato sekvence nahrazena jedním znakem LF (\n). Při zápise je pak každý znak LF nahrazen za CRLF (pokud k zápisu dochází na Windows). To se velmi hodí, protože ve vašem kódu stačí pracovat s ukončovači řádků LF a onu konverzi za vás zajistí programovací jazyk (resp. jeho standardní knihovna).

Samozřejmě, tyto rozdíly se mohou lišit jazyk od jazyka. Později v článku si ukážeme ještě jeden rozdíl.

Pro ilustraci, mějme textový soubor test.txt obsahující text Kočička\r\n zakódovaný jako UTF-8, tedy slovo Kočička a za ním ukončovač řádku používaný na Windows (CRLF). Obsah tohoto souboru si načteme do řetězce pojmenovaném text v textovém režimu:

>>> with open('test.txt', 'r', encoding='utf-8') as f:
...     text = f.read()
...
>>> text
'Kočička\n'
>>> len(text)
8

Jak vidíme, došlo ke konverzi CRLF na LF a korektnímu dekódování slova Kočička. U textových souborů je vždy dobré kódování explicitně specifikovat. Pokud bychom to neudělali, tak se použije systémové kódování. To se může lišit systém od systému. Na Linuxu je to sice většinou UTF-8, ale nedá se na to spoléhat, a už vůbec ne, pokud píšete přenositelný kód.

Pokud bychom si stejný soubor přečetli v binárním režimu, dostaneme následující data:

>>> with open('test.txt', 'rb') as f:
...     data = f.read()
...
>>> data
b'Ko\xc4\x8di\xc4\x8dka\r\n'
>>> len(data)
11

Dle očekávání jsme dostali přesně ty bajty, které se v souboru skutečně nachází: zakódované slovo Kočička v UTF-8 a CRLF. Písmeno č je v UTF-8 zakódováno jako dva bajty 0xc4 a 0x8d.

Příklad výše velmi pěkně ilustruje rozdíl mezi textovými řetězci a posloupností bajtů. Zatímco textové řetězce se používají pro uchování textu, posloupnost bajtů se používá, ehm, pro uložení bajtů :). Kódování je tedy potřeba řešit při čtení a zapisování textových souborů, takže v ostatním kódu programu kódování typicky nemusíte řešit. Samozřejmě, pokud si textový soubor načtete v binárním režimu, tak si konverzi musíte pořešit sami.

Čtení a zápis čísel

Abych rozdíl mezi textovým a binárním zpracováním souborů ještě lépe ilustroval, představte si následující situaci. Máme číslo 25 a chceme jej zapsat do souboru. Pokud jej zapíšeme textově, budou v souboru dva znaky 2 a 5 zapsané textově:

>>> with open('25.txt', 'w') as f:
...     f.write(str(25))
...
2  # zapsány dva bajty
>>> with open('25.txt', 'r') as f:
...     f.read()
...
'25'

Ono číslo jsme museli zapsat jako řetězec, protože write() umí zapsat jen řetězce či posloupnosti bajtů. Pokud jej zapíšeme binárně, bude v souboru binární reprezentace čísla 25:

>>> import struct
>>> with open('25.bin', 'wb') as f:
...     f.write(struct.pack('i', 25))  # zapiš jako 32b int
...
4  # zapsány čtyři bajty
>>> with open('25.bin', 'rb') as f:
...     f.read()
...
b'\x19\x00\x00\x00'  # 0x19 == 25

Zde jsme museli použít standardní modul struct, abychom číslo 25 zkonvertovali na posloupnost bajtů. Když jsme si přečetli obsah souboru 25.bin, tak obsahoval 4 bajty 0x00 0x00 0x00 0x19, což je reprezentace čísla 25 na 4 bajtech. Samozřejmě, do hry v tomto případě přichází endianita. Jelikož jsme při zápisu (resp. konverzi) žádnou neuvedli, použila se nativní endianita v systému.

Třetí rozdíl

Na závěr bych chtěl uvést ještě třetí rozdíl mezi binárním a textovým režimem, na který můžete občas narazit: bufferování. Binární vstup/výstup může být načítán/zapisován po blocích (např. 4 kB), kdežto textový vstup/výstup může být načítán/zapisován po řádcích. Typicky se ale ještě odlišuje čtení z textového souboru a čtení z terminálu (u souboru se pak může číst/zapisovat po blocích, u terminálu po řádcích). To je ale už velmi závislé na jazyce a operačním systému. Pokud by vás zajímalo, jak je to v Pythonu, mrkněte na popis funkce open().

Jiné jazyky a systémy

Ač jsem se snažil v článku psát obecně, tak to, co bylo napsáno výše, platí pro Python. Jiné jazyky (či systémy, ve kterých programy běží) mohou mít různé odlišnosti. Např. v Céčku na Linuxu se u standardní funkce fopen() textový a binární přístup nerozlišuje. Tedy i přesto, že si v Céčku na Linuxu otevřete soubor v módu "r", tak vám při čtení textových souborů vytvořených na Windows nedojde k náhradě CRLF za LF. Ke konverzi konců řádků dochází pouze na Windows. Je na to tedy třeba dávat pozor.

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
1 + 2 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.