Centrum Skryptów - Systemy Operacyjne

Jak usunąć zduplikowane wartości z dwóch plików tekstowych?

Udostępnij na: Facebook

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 usunąć zduplikowane wartości z dwóch plików tekstowych?

Cześć Skrypciarze! Mam dwa pliki zawierające liczby. Potrzebny mi skrypt, który może określić, czy dane liczby znajdują się z obydwu plikach tekstowych. Jeżeli tak jest, muszę usunąć dany wiersz z każdego z tych plików. Jak to zrobić?

-- KT

Cześć, KT. Zanim zacznę, muszę załatwić kilka spraw. Po pierwsze, chce podziękować tym, którzy dołączyli do naszej grupy w Facebooku. Zdecydowaliśmy się uruchomić tę witrynę głównie dlatego, że chcieliśmy poeksperymentować z różnymi sposobami kontaktowania się i wymiany informacji z innymi twórcami skryptów administrowania systemem.

A skoro już o tym mówimy, to chodziło nam także o to, żeby zdobyć więcej znajomych (i więcej uczestników grupy) niż TechNet czy MSDN. Cóż, jak dotąd idzie nam całkiem nieźle: w ostatnim raporcie TechNet miał 43 uczestników grupy, MSDN 84, z Skrypciarze – pomimo dość późnego uruchomienia witryny – mieli 90.

Ups. Teraz już 94 uczestników.

Uwaga. Wiecie co? To całkiem dobry pomysł: biorąc pod uwagę wszystkie okoliczności, może to TechNet powinien być jednostką podrzędną Centrum Skryptów, a nie odwrotnie. Poruszymy tę kwestię przy najbliższym spotkaniu kolegów z TechNet. Nie mogę się doczekać, żeby zobaczyć ich miny, kiedy im to zasugerujemy.

 

Nawiasem mówiąc, zdaję sobie sprawę, że do tej pory nie zrobiliśmy wiele w naszej grupie Facebook. To się zmieni już w styczniu. Wtedy, mam nadzieję, uda nam się umieścić tam kilka materiałów wideo; planujemy ponadto używać tej witryny jako centrum informującego na temat najnowszych osiągnięć Skrypciarzy. Na przykład, Olimpiady Skrypciarskiej 2008. (15 luty – 3 marca 2008, fajnie, że pytacie.)

A skoro już mowa o Olimpiadzie Skrypciarskiej, jeżeli uzyskaliście doskonały wynik w jednej z czterech kategorii Olimpiady 2007 (VBScript Beginner; VBScript Advanced; PowerShell Beginner; PowerShell Advanced) macie jeszcze dużo czasu, żeby przesłać swój życiorys (ze zdjęciem) do naszej witryny Profiles in Perfection. Profile będziemy umieszczać od 2 stycznia, więc im szybciej prześlecie nam swoje informacje, tym lepiej.

Właśnie: wystarczy wysłać wiadomość e-mail i kilka słów o sobie (oraz zdjęcia, które bez obaw możemy umieścić w Internecie) na adres scripter@microsoft.com.

I jeszcze jedno: zawody IT Forum Challenge oficjalnie się zakończyły i trwa podliczanie wyników. Mieliśmy nadzieję, że uda nam się na bieżąco podawać wyniki, ale niestety nie zawsze jest na wszystko czas. Mogę tylko powiedzieć, że wynik 701 to pierwsze miejsce. A o ile mi wiadomo na dzień dzisiejszy jest mnóstwo remisów.

Co z tym zrobimy? Zgodnie z naszymi zasadami, remisy są rozstrzygane przez losowy wybór: wrzucamy do kapelusza nazwiska wszystkich zwycięzców i losujemy 10 z nich. Tak wyłaniamy zwycięzców.

Wiecie co? To chyba wcale nie jest zbyt uczciwe. Zatem zrobimy inaczej. Wylosujemy 10 nazwisk i te osoby dostaną laleczki Dra Scripto; tyle tylko laleczek nam zostało. Ale nie rozpaczajcie; pozostali zwycięzcy otrzymają porównywalną nagrodę, ale będą musieli poczekać na jej otrzymanie do Olimpiady Skrypciarskiej. Zaufajcie mi, warto poczekać.

Uwaga. Jaka to nagroda? Przykro mi, ale tego nie mogę powiedzieć. Mogę tylko powiedzieć, że będzie taka sama, jak główna nagroda w Olimpiadzie Skrypciarskiej 2008. I teraz już wiecie, że warto poczekać, nieprawdaż?

 

Uff, wykonałem dzisiaj już więcej pracy niż inni Skrypciarze przez miesiąc. A to nawet nie jest połowa; w końcu jeszcze nawet nie zacząłem odpowiadać na dzisiejsze pytanie. A to oznacza, że chyba czas najwyższy.

Gwoli przedstawienia problemu zakładamy, ze KT ma dwa pliki tekstowe wyglądające mniej więcej tak:

KT potrzebuje usunąć wszystkie liczby znajdujące się w obydwu plikach (1, 9, 15, 19 oraz 20). Innymi słowy, po zakończeniu naszych działań pliki powinny wyglądać tak:

Jak można to zrobić? Jest na to pewien sposób:

Const ForReading = 1

Const ForWriting = 2



Set objFSO = CreateObject("Scripting.FileSystemObject")



Set objFile1 = objFSO.OpenTextFile("C:\Scripts\File1.txt", ForReading)

strText1 = objFile1.ReadAll

objFile1.Close



Set objFile2 = objFSO.OpenTextFile("C:\Scripts\File2.txt", ForReading)

strText2 = objFile2.ReadAll

objFile2.Close



arrItems1 = Split(strText1, vbCrLf)

arrItems2 = Split(strText2, vbCrLf)



Set objDictionary = CreateObject("Scripting.Dictionary")



For Each strItem2 in arrItems2

    objDictionary.Add strItem2, strItem2  

Next



For i = 0 to Ubound(arrItems1)

    If objDictionary.Exists(arrItems1(i)) Then

         objDictionary.Remove(arrItems1(i))

         arrItems1(i) = "Void"

    End If

Next



For Each strItem in arrItems1

    If strItem <> "Void" Then

        strNewText1 = strNewText1 & strItem & vbCrLf

    End If

Next

        

Set objFile1 = objFSO.OpenTextFile("C:\Scripts\File1.txt", ForWriting)

objFile1.Write strNewText1

objFile1.Close



For Each strKey in objDictionary.Keys

    strNewText2 = strNewText2 & strKey & vbCrLf

Next



Set objFile2 = objFSO.OpenTextFile("C:\Scripts\File2.txt", ForWriting)

objFile2.Write strNewText2

objFile2.Close

Jak ten skrypt działa? Zobaczmy, czy uda nam się go rozkminić. Zaczynamy od zdefiniowania pary stałych – jednej o nazwie ForReading (która będzie nam potrzebna do otwarcia o oczytania zawartości obydwu plików tekstowych) oraz drugiej o nazwie ForWriting (którą wykorzystamy do otwarcia i zapisu w obydwu plikach). Po zdefiniowaniu tej pary stałych, stosujemy poniższy wiersz kodu w celu utworzenia wystąpienia obiektu Scripting.FileSystemObject:

Set objFSO = CreateObject("Scripting.FileSystemObject")

Teraz przechodzimy do następujących trzech wierszy kodu:

Set objFile1 = objFSO.OpenTextFile("C:\Scripts\File1.txt", ForReading)

strText1 = objFile1.ReadAll

objFile1.Close

Używamy tutaj obiektu FileSystemObject do otwarcia pliku C:\Scripts\File1.txt do odczytu. Po jego otwarciu stosujemy metodę ReadAll w celu wczytania całej zawartości pliku do pamięci, zachowując te informacje w zmiennej o nazwie strText1. Kiedy już mamy kopię wirtualną pliku zachowaną bezpiecznie w pamięci, wywołujemy metodę Close, aby zamknąć plik File1.txt.

Teraz powtarzamy cały proces dla naszego drugiego pliku (C:\Scripts\File2.txt), zachowując informację w zmiennej o nazwie strText2. Co otrzymamy? Dwie zmienne – strText1 oraz strText2 – z zawartością naszych dwóch plików tekstowych.

Pięknie to wygląda, ale co zrobimy z tymi dwiema zmiennymi? Cóż, najpierw wykonamy poniższy wiersz kodu:

arrItems1 = Split(strText1, vbCrLf)

Używamy tutaj funkcji Split skryptu VBScript w celu przekonwertowania wartości w zmiennej strText1 na tablicę, tworząc nowy element tablicy za każdym razem, gdy napotkamy znak powrotu karetki (vbCrLf). Do czego jest nam to potrzebne? Cóż, kiedy skończymy, tablica arrItems1 będzie wyglądała tak:

1

2

3

9

15

19

20

Inni słowy, mamy teraz tabelę będącą dokładną repliką pliku File1.txt.

Tak, wiem. To nie jest odpowiedź na pytanie, do czego potrzebna jest nam ta tablica, nieprawdaż? Dojdę to tego za momencik. Przedtem jednak chcę zauważyć, że robimy to samo ze zmienną strText2, tym razem tworząc tablicę o nazwie arrItems2. To nam daje tablicę idealnie replikującą plik tekstowy File2.txt.

Uwaga. Czy nie można było utworzyć tych tablic, wczytując pliki wiersz po wierszu i dodając każdy wiersz do odpowiedniej tablicy podczas odczytu z pliku tekstowego? Pewnie. Pomyślałem tylko, że to podejście jest nieco łatwiejsze, zwłaszcza dla osób mających ograniczone doświadczenie w zakresie pracy z tablicami.

Być może pamiętacie, że naszym dzisiejszym celem jest usunięcie wartości pojawiających się zarówno w pliku File1.txt, jak i pliku File2.txt. To trochę bardziej skomplikowane, niż się wydaje. Dlaczego? No cóż, pierwsza wartość w pliku File1.txt to liczba 1. To może prowadzić do przekonania, że wystarczy wyszukać liczbę 1 w obydwu plikach i usunąć wszelkie wystąpienia tej liczby. Ale to się nie sprawdzi. W końcu, jeżeli po prostu usuniemy wszystkie wystąpienia liczby 1, zamienimy wartość 212 na:

22

Niedobrze. Oczywiście liczba 1 pojawia się w wierszu sama; czyż nie możemy wyszukać liczby 1 z następującym po niej znakiem powrotu karetki? Niestety, odpowiedź jest ta sama: nie. Dlaczego nie? Ponieważ liczna 651 zawiera liczbę 1 z następującym po niej znakiem powrotu karetki. Gdybyśmy wyszukali liczbę 1 z następującym po niej znakiem powrotu karetki, zamienilibyśmy liczbę 651 na:

65

Ech…

Co zatem musimy z tym zrobić?

Cóż, rozwiązanie, które wymyśliłem (a na pewno są jeszcze inne sposoby rozwiązania tego problemu) wymaga utworzenia wystąpienia obiektu Scripting.Dictionary. Kiedy już mamy ten obiekt, stosujemy następujący wiersz kodu w celu uruchomienia pętli, która przejdzie przez wszystkie wartości w znajdujące się w tablicy arrItems2 i doda każdą z tych wartości do obiektu Dictionary:

For Each strItem2 in arrItems2

    objDictionary.Add strItem2, strItem2  

Next
Uwaga. Zakładamy, że wartości są niepowtarzalne w każdym pliku; czyli File1.txt will zawiera tylko jedno wystąpienie liczby 322. Jeżeli wartości nie są niepowtarzalne – cóż. to zdecydowanie komplikuje skrypt. Jeżeli ktoś potrzebuje rozwiązania tego problemu, dajcie nam znać, zobaczymy, co da się zrobić.

Teraz przechodzimy do następującego fragmentu kodu stanowiącego serce i duszę skryptu:

For i = 0 to Ubound(arrItems1)

    If objDictionary.Exists(arrItems1(i)) Then

         objDictionary.Remove(arrItems1(i))

         arrItems1(i) = "Void"

    End If

Next

Wprawdzie jest to tylko sześć wierszy, jednakże w nich są skumulowane wszelkie emocje dotyczące tego skryptu. Jak widzimy, zaczynamy od uruchomienia pętli For Next, która przejdzie przez wszystkie elementy znajdujące się w tablicy arrItems1. (Pętla rozpoczyna się od 0, czyli od numeru indeksu pierwszego elementu w tablicy i działa aż do elementu Ubound, ostatniego elementu w tablicy.) Wewnątrz tej pętli używamy metody Exists w celu określenia czy dany element tablicy (np. liczba 1) znajduje się w obiekcie Dictionary:

If objDictionary.Exists(arrItems1(i)) Then

Przypuśćmy, że metoda Exists przekaże nam wartość False; oznacza to, że dana wartość pojawia się jedynie w pliku File1.txt; a nie pojawia się zarówno w pliku File1.txt, jak i File2.txt. A ponieważ nas interesują tylko wartości pojawiające się w obydwu plikach, po prostu wracamy na początek pętli i powtarzamy proces dla następnego elementu w tablicy.

Jednakże przypuśćmy, że metoda Exists przekaże nam wartość True. To oznacza, że dana wartość naprawdę pojawia się w obydwu plikach. Oznacza to także, że musimy wykonać następujące dwa wiersze kodu:

objDictionary.Remove(arrItems1(i))

arrItems1(i) = "Void"

W wierszu 1 używamy metody Remove w celu usunięcia danego elementu z obiektu Dictionary. Jeżeli dany element to liczba 1 oznacza to, że liczba 1 nie będzie już ujęta w obiekcie Dictionary. Ponadto, liczba 1 nie będzie także ujęta w tablicy arrItems1. Dlaczego nie? Ponieważ w wierszu 2 ustawiamy wartość tego elementu tablicy jako ciąg Void.

Uwaga. Dlaczego po prostu nie usuniemy tego elementu z tablicy? Chcielibyśmy tak zrobić. Ale usuwanie elementów z tablicy wcale nie jest zabawne. A Skrypciarze z reguły nie lubią robić niezabawnych rzeczy. Ustawienie wartości jako ciągu Void jest dużo prostsze i gwarantuje, że w dalszym ciągu będziemy mogli szybko wybierać wartości, które są niepowtarzalne w pliku File1.txt

Jaki jest cel tego wszystkiego? Cóż, po zakończeniu działania pętli na elementach tablicy arrItems1 nasz obiekt Dictionary będzie zawierał następujące elementy:

4

21

29

Tak się składa, ze są to liczby pojawiające się tylko w plikuFile2.txt. Wszelkie liczby pojawiające się zarówno w pliku File1.txt, jak i File2.txt zostały usunięte z obiektu Dictionary.

Tymczasem, tablica arrItems1 wygląda tak:

Void

2

3

Void

Void

Void

Void

Jak widzimy, jedynymi liczbami, które pozostały są wartości pojawiające się tylko w pliku File1.txt. Liczby pojawiające się w obydwu plikach tekstowych zostały zastąpione słowem Void.

Teraz wystarczy tylko wpisać te niepowtarzalne wartości z powrotem do naszych plików tekstowych. W celu utworzenia nowego pliku tekstowego dla File1.txt stosujemy poniższy fragment kodu:

For Each strItem in arrItems1

    If strItem <> "Void" Then

        strNewText1 = strNewText1 & strItem & vbCrLf

    End If

Next

Pobieramy tutaj wszystkie wartości z tablicy arrItems1, które nie są ciągiem Void (w tym przypadku, liczby 2 i 3). W celu utworzenia nowego tekstu dodajemy po prostu każdą z tych wartości plus znak powrotu karetki do zmiennej o nazwie strNewText1:

strNewText1 = strNewText1 & strItem & vbCrLf

Następnie otwieramy ponownie plik C:\Scripts\File1.txt (tym razem do zapisu), a następnie stosujemy metodę Write w celu zapisania wartości zmiennej strNewText1 do pliku:

objFile1.Write strNewText1

Po zastąpieniu starej zawartości pliku File1.txt wartością zmiennej strNewText1 zamykamy plik File1.txt.

Następnie stosujemy poniższy fragment kodu w celu utworzenia podobnego ciągu dla pliku File2.txt:

For Each strKey in objDictionary.Keys

    strNewText2 = strNewText2 & strKey & vbCrLf

Next

Po zakończeniu, wpisujemy wartość zmiennej strNewText2 do pliku File2.txt. I w tym momencie kończymy naszą pracę.

Mam na myśli pracę ze skryptem. Mnie czeka jeszcze całe mnóstwo roboty przed Świętami.

 Do początku strony Do początku strony

Centrum Skryptów - Systemy Operacyjne