Centrum skryptów - Systemy operacyjne

Jak przesortować zawartość pliku tekstowego w porządku alfabetycznym?

Udostępnij na: Facebook

Skrypciarze odpowiadają na Wasze pytania

Cześć Skrypciarze!

Witamy w rubryce TechNet, w której Skrypciarze z firmy Microsoft odpowiadają na częste pytania dotyczące używania skryptów w administracji systemu. Jeśli macie jakieś pytania z tej dziedziny, zachęcamy do wysłania e-maila na adres: scripter@microsoft.com. Nie możemy zagwarantować odpowiedzi na każde otrzymane pytanie, ale staramy się jak możemy.

Jak przesortować zawartość pliku tekstowego w porządku alfabetycznym?

Cześć Skrypciarze! Pytanie

Cześć, Skrypciarze! Mam plik tekstowy, którego każdy wiersz zaczyna się od numeru. Jak przesortować zawartość tego pliku na podstawie numeracji?

-- KT

Cześć Skrypciarze! Pytanie

Cześć, KT! Nie uwierzysz, ale dziś będzie bez ciekawej historii na początku. Dlaczego? Po prostu nic ciekawego się w dniu dzisiejszym nie zdarzyło, a pisanie po raz tysięczny na temat: „pamiętajcie, że od 15. lutego do 3. marca trwa nasza Zimowa Olimpiada Skrypciarska” nie ma chyba sensu, bo Skrypciarska Pani Redaktor da nam zaraz odczuć swoje niezadowolenie z powodu ciągłego powielania tematu. Moglibyśmy też napisać o „Ziomowej Olimpiadzie Skrypciarskiej”, ale to już za daleko posunięte skojarzenie. Zaczniemy więc może wyjątkowo od skryptu. Dziś serwujemy skrypt, który sortuje wiersze pliku tekstowego na podstawie numeru zaczynającego każdy wiersz:

Const ForReading = 1

Const ForWriting =2



Const adVarChar = 200

Const MaxCharacters = 255

Const adFldIsNullable = 32

Const adInteger = 3



Set DataList = CreateObject("ADOR.Recordset")

DataList.Fields.Append "TextLine", adVarChar, MaxCharacters, adFldIsNullable

DataList.Fields.Append "NumberLine", adInteger, , adFldIsNullable

DataList.Open



Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)



Do Until objFile.AtEndOfStream

    strLine = objFile.ReadLine

    intLength = Len(strLine) 

    strValue = ""

    For i = 1 to intLength

        If IsNumeric(Mid(strLine, i, 1)) Then

            strValue = strValue & Mid(strLine, i, 1)

        Else

            Exit For

        End If

    Next

    DataList.AddNew

    DataList("TextLine") = strLine

    DataList("NumberLine") = strValue

    DataList.Update

Loop



objFile.Close



Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)



DataList.Sort = "NumberLine ASC"



DataList.MoveFirst



Do Until DataList.EOF

    objFile.WriteLine DataList.Fields.Item("TextLine") 

    DataList.MoveNext

Loop



objFile.Close

Zanim omówimy szczegółowo nasz skrypt, zwróćcie uwagę, że jest on trochę bardziej skomplikowany niż ten, o który prosił nas KT. Dlaczego? Ponieważ VBScript nie ma wbudowanego mechanizmu sortującego informacje. Nie zawsze rozróżnia nawet czy Wasz plik składa się z liter, czy cyfr. Dla przykładu, dajmy na to, że mamy następujące wartości:

2 BBB

1 AAA

10 DDD

3 CCC

Jeżeli skrypt VBScript uzna, że te wartości są liczbami, to posortuje je w następujący sposób:

1 AAA

2 BBB

3 CCC

10 DDD

Jeżeli jednak uzna, że ma do czynienia z wartościami ciągu, to zwróci nam je uporządkowane w następujący sposób:

1 AAA

10 DDD

2 BBB

3 CCC

W przypadku KT sprawa jest o tyle prosta, że najwyższym numerem wiersza w jego pliku jest 9. Skąd to wiemy? Z magicznego stwierdzenia, że skrypt ma sortować plik „na podstawie wartości pierwszego znaku danego wiersza, który zawsze jest liczbą”. Czy Wasz tok myślenia również prowadzi Was do podobnych wniosków?

Mamy jednak wrażenie, że wielu z Was ma pliki tekstowe, których wiersze zaczynają się numerem większym niż 9. W takim wypadku, jeśli będziemy mieli do czynienia np. z wierszem 145, sortowanie na podstawie pierwszej cyfry niczego dobrego Wam nie przyniesie. Dlatego właśnie nasz skrypt jest trochę bardziej skomplikowany, ale o niebo bardziej wydajny. Działa w każdej sytuacji, dla wszystkich liczb.

Bez obaw jednak, KT, na końcu artykułu znajdziesz skrypt, który odpowiada tylko na Twoje pytanie i nie bierze pod uwagę żadnych innych przypadków.

Co zaś tyczy się naszego rozbudowanego skryptu, zaczynamy od zdefiniowania stałych. Pierwsze dwie – ForReading oraz ForWriting – potrzebne nam będą do sczytania tekstu i wpisania danych do pliku tekstowego. Pozostałe cztery pełnią następujące funkcje:

  • adVarChar. Ta stała pozwala na utworzenie pola zbioru rekordów przy pomocy typu danych Variant. To pole przechowa cały tekst każdego wiersza w pliku tekstowym.
  • MaxCharacters. Dzięki tej stałej w polu Variant możemy umieścić nawet 255 znaków.
  • adFldIsNullable. Dzięki tej zmiennej możemy operować wartościami pustymi w danym polu. W dzisiejszy skrypcie nie jest to niezbędne, ale zawsze warto o tym wiedzieć.
  • adInteger. Dzięki tej stałej możemy utworzyć pole zestawu rekordów przy użyciu typu danych z licznika. Tej stałej będziemy używać w celu sprawdzenia cyfr znajdujących się na początku każdego wiersza.

Naszym kolejnym krokiem będzie utworzenie „rozłączonego zestawu rekordów”, czyli tabeli w bazie danych, która istnieje tylko w pamięci (tzn. nie jest w żaden sposób połączona z jakąś fizyczną bazą danych). Nie będziemy tu szczegółowo omawiać rozłączonych zestawów rekordów, jeżeli Was to interesuje, to zapraszamy do odpowiedniej sekcji (j.ang.) naszego przewodnika Microsoft Windows 2000 Scripting Guide. Na razie wystarczy stwierdzić, że następujący fragment kodu tworzy nowy rozłączony zestaw rekordów, dodaje pole Variant o nazwie TextLine oraz pole licznika o nazwie NumberLine, po czym otwiera zestaw rekordów do użytku:

Set DataList = CreateObject("ADOR.Recordset")

DataList.Fields.Append "TextLine", adVarChar, MaxCharacters, adFldIsNullable

DataList.Fields.Append "NumberLine", adInteger, , adFldIsNullable

DataList.Open

Kiedy dysponujemy już zestawem rekordów, możemy otworzyć plik tekstowy C:\Scripts\Test.txt do odczytu; właśnie do tego służą następujące wiersze kodu:

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)

Po otworzeniu pliku ustawiamy pętlę Do, która działa do momentu, kiedy sczyta każdy wiersz w danym pliku (innymi słowy, do momentu kiedy właściwość AtEndOfStream będzie równa True):

Do Until objFile.AtEndOfStream

Całe czary-mary (lub przynajmniej większa ich część) odbywają się właśnie wewnątrz pętli Do. Zaczynamy od skorzystania z ReadLine, która sczytuje pierwszy wiersz pliku i przechowuje je wartość w strLine. Następnie korzystamy z funkcji Len języka VBScript , która ustala długość wiersza, czyli liczbę jego znaków:

intLength = Len(strLine)

Po co nam ta wiedza? Ponieważ wiemy, że pierwszymi znakami x w wierszu są liczby, nie wiemy jednak, ile wynosi nasza niewiadoma. Dlatego właśnie musimy zbadać każdy wiersz znak po znaku i przestać dopiero wtedy, gdy natkniemy się na znak inny niż liczba. Dla przykładu, wyobraźmy sobie, że pierwszy wiersz w naszym pliku tekstowym wygląda w następujący sposób:

124This is a line in the text file.

Pobieramy 1, 2 i 4, i kończymy proces w momencie, kiedy natkniemy się na T, które, choć byśmy nie wiem jak chcieli, nie jest wartością numeryczną.

Jak to wszystko zrobić? Dla początkujących – zmiennej strValue przypisujemy pusty ciąg:

strValue = ""

Następnie uruchamiamy pętlę For Next: 

For i = 1 to intLength

    If IsNumeric(Mid(strLine, i, 1)) Then

        strValue = strValue & Mid(strLine, i, 1)

    Else

        Exit For

    End If

Next

Przechodzimy tu pętlą od 1 przez cały zestaw znaków w wierszu (intLength). Wewnątrz tej właśnie pętli korzystamy z funkcji IsNumeric, która ustala nam czy pierwszy znak jest cyfrą, czy nie. Jeżeli jest, to dodajemy ten znak do zmiennej strValue:

strValue = strValue & Mid(strLine, i, 1)

Podczas przechodzenia pętlą pierwszy raz, nasza strValue będzie równa 1. Kiedy powtórzymy proces dla kolejnego znaku w danym wierszu i okaże się, że to także jakaś cyfra (a tak się w tym wypadku bez wątpienia okaże), dodajemy tę wartość do zmiennej Value. Wartość strValue będzie teraz równa 12. Powtarzamy proces dla kolejnego znaku i tak się dzieje aż do momentu, kiedy pętla natknie się na wartość nienumeryczną. W takim wypadku wywołujemy twierdzenie Exit For i wychodzimy z pętli For Next.

Dzięki temu procesowi możemy pobrać każdy numer początkowy danego wiersza. Teraz, gdy wiemy już jaki to numer, możemy skorzystać z następującego fragmentu kodu w celu dodania nowego rekordu do naszego zestawu rekordów (TextLine) oraz liczbę, która pojawia się na początku wiersza:

DataList.AddNew

DataList("TextLine") = strLine

DataList("NumberLine") = strValue

DataList.Update

Innymi słowy, nasz zestaw rekordów składa się obecnie z następującego zapisu:

TextLine NumberLine
124This is a line in the text file. 124

Następnie przechodzi do następnego wiersza pliku tekstowego.

Po sczytaniu wszystkich wierszy z pliku tekstowego (i dodaniu ich do zestawu rekordów) zamykamy plik, po czym otwieramy go ponownie, tym razem do zapisu. Wystarczy już tylko dodać jeden wiersz kodu, a wszystko nam się przesortuje:

DataList.Sort = "NumberLine ASC"

Sortujemy wszystko oczywiście na podstawie pola NumberLine; właśnie to pole zawiera liczbę, która rozpoczyna dany wiersz pliku. Założyliśmy również, że dane sortować będziemy rosnąco (od 1 do 100), do tego właśnie służy krótkie, ale jakże przydatne ASC. Oczywiście sortować można także malejąco (od 100 do 1) – w takim wypadku zamiast ASC należy użyć DESC:

DataList.Sort = "NumberLine DESC"

Musimy teraz zastąpić pierwotną zawartość pliku nową (przesortowaną) zawartością pliku. W tym celu korzystamy z metody MoveFirst, która przemieszcza nasz pierwszy rekord w zestawie. Następnie, poniższy fragment kodu uruchamia pętlę, która przechodzi przez całą zawartość zestawu rekordów, korzystając z metody WriteLine, która dodaje wartości pola TextLine do pliku:

Do Until DataList.EOF

    objFile.WriteLine DataList.Fields.Item("TextLine") 

    DataList.MoveNext

Loop

Czy zadziała? Przypuśćmy, że nasz plik tekstowy wygląda następująco:

14tttt

2aaaa

8ffff

6vvvv

4cccc

12xxxx

11zzzz

9qqqq

Po uruchomieniu skryptu wygląda już trochę inaczej, nieprawdaż?

2aaaa

4cccc

6vvvv

8ffff

9qqqq

11zzzz

12xxxx

14tttt

Hurra! Zadziałał!!!

To powinno uporać się z Twoim problemem KT, szkoda tylko, że sami nie wpadliśmy na Twoje pytanie, mogłaby to być niezła dyscyplina w naszej Zimowej Olimpiadzie Skrypciarskiej 2009. Tak a propos, czy wspominaliśmy już, że kolejna edycja naszej Zimowej Olimpiady Skrypciarskiej rusza już 15. lutego? Chyba tak...

Tegoroczna edycja jest dopiero trzecią, jest jednak bardzo urozmaicona, i to nie tylko pod względem nagród. Chodzi raczej o konkurencje – będzie Perl, będzie VBScript i będzie Windows. Są trudne, ale wykonalne. Na każdy dzień przewidujemy około 10 konkurencji i nie martwcie się, jeżeli dopiero zaczynacie się bawić w skrypty – mamy dział dla początkujących. Dla zaawansowanych oczywiście też.

Zaznaczcie więc 15. lutego w Waszym kalendarzu, albo jeszcze lepiej, poczekajcie do kolejnego artykułu z tej edycji – zamierzamy w nim pokazać skrypt, który dokonuje zapisów w Waszym kalendarzu.

Zapomnielibyśmy! Oto uproszczona wersja skryptu, taka, która sortuje nam wartości od 0 do 9:

Const ForReading = 1

Const ForWriting =2



Const adVarChar = 200

Const MaxCharacters = 255

Const adFldIsNullable = 32



Set DataList = CreateObject("ADOR.Recordset")

DataList.Fields.Append "TextLine", adVarChar, MaxCharacters, adFldIsNullable

DataList.Open



Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForReading)



Do Until objFile.AtEndOfStream

    strLine = objFile.ReadLine

    DataList.AddNew

    DataList("TextLine") = strLine

    DataList.Update

Loop



objFile.Close



Set objFile = objFSO.OpenTextFile("C:\Scripts\Test.txt", ForWriting)



DataList.Sort = "TextLine ASC"



DataList.MoveFirst



Do Until DataList.EOF

    objFile.WriteLine DataList.Fields.Item("TextLine") 

    DataList.MoveNext

Loop



objFile.Close

Tyle Wam chyba wystarczy na dziś? Do zobaczenia jutro!

 Do początku strony Do początku strony

Centrum skryptów - Systemy operacyjne