Jak przesortować zawartość pliku tekstowego w porządku alfabetycznym?
Skrypciarze odpowiadają na Wasze pytania
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! Mam plik tekstowy, którego każdy wiersz zaczyna się od numeru. Jak przesortować zawartość tego pliku na podstawie numeracji?
-- KT
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 |