.pl .en .de .ru
Interdyscyplinarny blog badawczy pracowników Instytutu Językoznawstwa i Pracowni Systemów Informacyjnych UAM

Anatomia pliku tekstowego — część II (piekło końca wiersza)

Filip Graliński
01/28

(Zobacz I odcinek cyklu).

Przetwarzasz prosty plik tekstowy i dzieje się coś dziwnego. Wiersze zdają się mieć inną długość niż to, co widzisz na ekranie. Albo duży plik tekstowy jest traktowany tak, jakby wszystko było zapisane w jednej linii.

Witamy w piekle końca wiersza. W kotle diabły gotują Billa Gatesa.

wiersze

Plik tekstowy, dla nas, ludzi, nie jest tylko ciągiem znaków…

I am weary of days and hours, Blown buds of barren flowers, Desires and dreams and powers And everything but sleep.

…zazwyczaj widać w nim wiersze (linie):

I am weary of days and hours,
Blown buds of barren flowers,
Desires and dreams and powers
And everything but sleep.

(Nie twierdzę, że my, Polacy, mamy gorszych poetów, po prostu na razie chcę uniknąć polskich znaków diakrytycznych, o tym — w następnym odcinku!)

Z drugiej strony z odcinka I wiemy, że dla komputera wszystko jest ciągiem zer i jedynek. Czyżby komputer miał jakiś magiczny, łamiący tę zasadę, sposób, żeby układać znaki w wiersze? Nie, nie ma żadnej magii, żadnego wyjątku — koniec wiersza ma swój kod tak jak zwyczajne znaki, litery czy cyfry. Innymi słowy, koniec wiersza oznaczany jest za pomocą specjalnego znaku, znaku końca wiersza. Znak ten jest dla nas niewidoczny, ale widoczny jest jego efekt — zakończenie wiersza i rozpoczęcie nowego. Można powiedzieć, że na klawiaturze odpowiada temu znakowi klawisz „Enter”.

Zobaczmy oczami komputera wyżej podany fragment wiersza Algernona Charlesa Swinburne’a (przypominam, polecenie hexdump -C w Linuksie):

00000000  49 20 61 6d 20 77 65 61  72 79 20 6f 66 20 64 61  |I am weary of da|
00000010  79 73 20 61 6e 64 20 68  6f 75 72 73 2c 0a 42 6c  |ys and hours,.Bl|
00000020  6f 77 6e 20 62 75 64 73  20 6f 66 20 62 61 72 72  |own buds of barr|
00000030  65 6e 20 66 6c 6f 77 65  72 73 2c 0a 44 65 73 69  |en flowers,.Desi|
00000040  72 65 73 20 61 6e 64 20  64 72 65 61 6d 73 20 61  |res and dreams a|
00000050  6e 64 20 70 6f 77 65 72  73 0a 41 6e 64 20 65 76  |nd powers.And ev|
00000060  65 72 79 74 68 69 6e 67  20 62 75 74 20 73 6c 65  |erything but sle|
00000070  65 70 2e 0a                                       |ep..|
00000074

Widać, jaki kod ma znak końca wiersza? 10, prawda? Szesnastkowo ten bajt to 0a, binarnie — 00001010. Tak przyjęto w ASCII (zajrzyjcie do tabelki w poprzednim odcinku).

Jak na razie wygląda to prosto, gdzie tu problem?

przekleństwo CP/M-a

W każdym porządnym systemie operacyjnym znak końca wiersza to po prostu jeden bajt 0a.

W Windowsie jest inaczej, a jak. W Windowsie przejście do nowego wiersza oznaczane jest sekwencją złożoną z dwóch bajtów: 0d 0a (dziesiętnie: 13 i 10).

Że co?? Czemu tak?? Bill Gates tak złośliwie wymyślił, żeby namieszać?

Jakiś tam powód był… Twórcy systemu Windows chcieli zachować zgodność z systemem DOS, gdzie obowiązywała przeklęta dwubajtowa sekwencja, z kolei w DOS-ie wynikło to z potrzeby zachowania wstecznej kompatybilności z systemem CP/M, o którym ludzkość już dawno zapomniała i w którym to nie było porządnych sterowników do drukarek, więc trzeba było ręcznie prosić drukarkę: „przestaw karetkę na początek wiersza (kod 0d), zejdź niżej do nowego wiersza (kod 0a)”.

Tak więc mamy dwa standardy zaznaczania końca wiersza: znak 0a (systemy Unix, w tym Linux), podwójny kod 0d-0a (Microsoft DOS i Windows), kod 0d (wcześniejsze wersje systemów firmy Apple). Trzy standardy. Arrghhh.

Przez to zamieszanie ludzkość straciła już miliardy dolarów (nie przesadzam — każdy programista zmarnował ileś godzin swojego życia na diagnozowanie i rozwiązywanie problemów z tym związanych). Billu Gatesie, trzeba było wcześniej uciąć ten idiotyzm!

No dobrze, zastanówmy się teraz na spokojnie, co z tego wynika. Oto nasz przykładowy tekst zapisany na dwa sposoby (pomińmy już standard ze starszych wersji systemu MacOS):

Unix/Linux

DOS/Windows

Pobierzcie proszę oba pliki i porównajcie, jak wyglądają w systemie operacyjnym i w edytorze tekstu, których używacie.

(Pliki są skompresowane, więc najpierw trzeba je rozpakować. Poddałem je kompresji, nie dlatego, żeby zaoszczędzić parę bajtów, lecz aby uniknąć zniekształceń, jakim może ulec plik tekstowy przy pobieraniu przez przeglądarkę. Dobra rada: nigdy nie wystawiajcie w Internecie bezpośrednio plików tekstowych ani nie przesyłajcie ich jako załączników mejlem — zawsze je kompresujcie; skompresowany plik jest traktowany jako „binarny”, to znaczy przeglądarki i klienty poczty elektronicznej nic nie będą próbowały w nim „mieszać”.)

Co się może stać złego?

  • jeśli plik z linuksowymi znakami końca wiersza otworzymy w systemie Windows (np. w Notatniku), możemy zobaczyć jeden bardzo długi wiersz (wiersze nie są złamane tam, gdzie powinny być!),
  • jeśli plik z windowsowym kodem końca wiersza otworzymy w systemie Linux, na końcu wierszy mogą się pojawić dziwne znaki, np. ^M.

Trzeba zaznaczyć, że bez względu na system operacyjny niektóre edytory są bardziej rozgarnięte i potrafią się automatycznie zorientować, że otwierany plik zawiera inne kodowanie. Na przykład w Windowsie, jeśli plik tekstowy z Linuksa otworzymy w WordPadzie, wszystko będzie OK, a jeśli w Notatniku — zobaczymy nieprzerwaną ścianę tekstu. Emacs również automatycznie rozpoznaje kodowanie i jeśli użyto znaków końca wiersza z DOS-a/Windowsa, informuje o tym na dole w pasku statusu:

Inna sprawa, że może to być mylące, bo plik cały czas ma nieoczekiwane kodowanie i inne programy mogą to źle zinterpretować. Po proste bajty 0d w pliku i żadne „oszukiwanie” edytora tego nie zmieni. Możemy się w tym też upewnić, używając polecenia o wdzięcznej nazwie wc (wc zlicza bajty, wyrazy i wiersze):

$ wc newlines-linux.txt
4  21 116 newlines-linux.txt

$ wc newlines-windows.txt
4  21 120 newlines-windows.txt

Liczba wierszy (pierwsza kolumna) i słów (druga kolumna) jest taka sama, natomiast plik windowsowy ma 4 bajty więcej — wszystko się zgadza!

Jeszcze jedno: dlaczego w Linuksie możemy (w edytorach „głupszych” niż Emacs) zobaczyć ^M? Ano dlatego że jest to jeden ze sposobów reprezentowania (zasadniczo niedrukowalnego) znaku o kodzie 0d (pierwszego bajtu z dwuznaku kodującego koniec wiersza pod Windowsem). Czasami możecie też spotkać się z zapisem \r.

nawracanie plików tekstowych

Jak dokonać konwersji pliku z jednego kodowania do drugiego? W Linuksie jest zazwyczaj dostępne bardzo wygodne polecenie dos2unix. Program ten może pobierać dane ze standardowego wejścia i zapisywać na standardowe wyjście:

$ dos2unix < newlines-windows.txt > newlines.txt
$ wc newlines.txt
4  21 116 newlines-linux.txt

albo konwertować plik w miejscu (co jest bardzo wygodne):

$ dos2unix newlines-windows.txt
$ wc newlines-windows.txt
4  21 116 newlines-linux.txt

Łatwo odgadnąć nazwę polecenia, które dokonuje odwrotnej konwersji — to unix2dos.

A w Windowsie? Można użyć bardziej „zaawansowanego” edytora zamiast Notatnika, np. Notepad++, znajdziemy w nim polecenie Edit / EOL conversion:

higiena końca wiersza

Kwestia dziwacznego kodowania w Windowsie nie wyczerpuje tematu końca wiersza. Pracując z plikami tekstowymi, warto trzymać się następujących zaleceń:

  • unikajmy niepotrzebnych spacji przed końcem wiersza,
  • unikajmy niepotrzebnych pustych wierszy na końcu pliku,
  • ostatni wiersz powinien również kończyć się znakiem końca wiersza (innymi słowy, ostatnim znakiem w pliku tekstowym zawsze powinien być znak końca wiersza).

Łatwo niechcący złamać te zalecenia (a konsekwencje mogą być odłożone w czasie, acz bolesne — kiedy plik tekstowy będzie przetwarzany automatycznie). Najprościej tak zmienić ustawienia edytora, by było to naprawiane automatycznie. W Emacsie czynimy to poprzez dodanie do pliku konfiguracyjnego (.emacs) następujących poleceń:

(add-hook 'write-file-hooks 'delete-trailing-whitespace)


(defun delete-trailing-blank-lines ()
  "Deletes all blank lines at the end of the file, even the last one"
  (interactive)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-max))
      (delete-blank-lines))))

(add-hook 'write-file-hooks 'delete-trailing-blank-lines)

programiści mają gorzej

Nie dość na tym, programiści zmagają się z kolejną warstwą mętliku związanego ze znakami końca wiersza. (Jeśli nie programujesz, możesz pominąć dalszą część).

Napiszmy w języku C++ prosty program, który wczytuje z pliku wiersze i dla każdego wiersza wypisuje jego długość:

#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::ifstream plik("newlines-windows.txt", std::ios_base::binary);

    std::string wiersz;

    while (getline(plik, wiersz))
    {
       std::cout << wiersz.size() << std::endl;
    }
}

Będzie interesowała nas tutaj szczególnie druga opcja konstruktora obiektu std::ifstream, powyżej podałem jako argument std::ios_base::binary, alternatywnie można go pominąć (w pierwszym przypadku otwieramy plik w trybie binarnym, w drugim — tekstowym). W systemie Linux to, czy podamy std::ios_base::binary, czy nie, nie ma żadnego znaczenia, na wyjściu zobaczymy:

30
30
30
26

(Uwaga: zasadniczo funkcja getline nie zapisuje do napisu wiersz znaku końca wiersza, trafi tam jednak znak o kodzie 0d, który dla systemu Linux nie ma specjalnego znaczenia).

Jeśli jednak program skompilujemy i uruchomimy pod Windowsem, sytuacja się zmieni. Otóż jeśli w Windowsie otworzymy plik w trybie tekstowym (tj. bez std::ios_base::binary) wszystkie windowsowe znaki (tj. dwu-znaki) końca wiersza będą „automagicznie”, w locie, zamieniane na znak o kodzie 0a! Czyli powyższy przykładowy program pod Windowsem da inny wynik:

29
29
29
25

Inny niż pod Linuksem! Aj, musimy uważać nie tylko, pod jakim systemem działamy, ale też, w jakim trybie otwieramy plik… Gdybyśmy bowiem otworzyli plik w trybie binarnym, wynik byłby taki jak pod Linuksem.


W następnym odcinku: Unicode.

Tagi