@Wizzie - ja nie programuję w Pythonie, więc jakichś cudownych przykładów Ci nie podam, jednak pytaj jak najwięcej tych, którzy ten język znają i potrafią w nim pisać eleganckie kawałki kodu;
Przede wszystkim stwórz klasy dla poszczególnych elementów - osobne klasy dla atrybutów, dla węzłów i linków do plików dołączanych; Z tych klas będzie tworzona struktura elementów w pamięci; Klasy te muszą być uniwersalne, więc klasa atrybutu musi być w stanie przechowywać informacje zarówno o standardowym atrybucie, jak i o atrybucie referencjonowanym (z ciałem wydzielonym z głównego ciała drzewa); Tak samo z węzłami - klasa węzła powinna być jedna, ale mogąca reprezentować węzeł standardowy, węzeł referencjonowany, główny węzeł drzewa (to najważniejsze) oraz węzeł wirtualny, czyli główny węzeł drzewa linkowanego z osobnego pliku; Linki do plików dołączanych mogą posiadać tylko jeden zapis i tylko jedną funkcję, więc one nie muszą być uniwersalne;
Jak widzisz uniwersalizm klas jest bardzo ważny; Następnie stwórz klasę drzewa, która zawierać musi kompletną strukturę ww. elementów; I znów uniwersalizm - klasa drzewa musi być w stanie reprezentować drzewo ładowane z pliku tekstowego, jak i wczytywane z pliku binarnego; Dodatkowo, klasa drzewa musi także potrafić reprezentować drzewo linkowane, aby dowolny system konfiguracji móc budować z tylko jednej klasy-wzorca;
Czyli podsumowując - każdy element drzewa musi opisywać tylko jedna klasa, bez rozróżniania typu takiego elementu (standardowy, referencjonowany, główny czy linkowany); Z kolei każde drzewo także musi być instancją jednej klasy; Piszę że musi celowo, dlatego że musi - kod będzie krótki, zwięzły i uniwersalny; Tak jest zbudowane moje API, które bardzo dobrze radzi sobie z przeróżnymi elementami i różnymi drzewami;
Kolejna ważna rzecz - skoro jedna klasa może reprezentować drzewa ładowane ze źródeł tekstowych i binarnych, musisz mieć dwie klasy parserów (parserów to może zbyt uogólnione - po prostu klas przetwarzających źródła), oraz dwie klasy generujące wyjście - tekstowe bądź binarne; Można te funkcjonalności połączyć i stworzyć tylko dwie klasy - jedna klasa ładuje drzewo ze źródła tekstowego lub binarnego (klasa wczytująca), a druga generująca wyjście - tekstowe lub binarne; To już wola programującego, jak wolałem stworzyć cztery klasy, wspomiane wyżej;
Jednak ile być tych klas nie stworzył, to każda powinna przyjmować np. w konstruktorze instancję klasy drzewa, aby móc ją przetworzyć (przy ładowaniu) bądź na jej podstawie wygenerować wyjście; Zarówno klasy ładujące drzewa z plików (ogólnie ze źródeł), jak i klasy zapisujące, muszą działać rekurencyjnie; Rekursja jest nieodzownym sposobem przetwarzania węzłów drzew, ale także przetwarzania drzew dołączanych o dowolnym typie i dowolnym poziomie linkowania; Nie jest to trudne do zrealizowania pod warunkiem, że rozumie się rekurencję i umie się z niej korzystać;
Nie wiem jak wygląda biblioteka standardowa w Pythonie, więc samo parsowanie musisz zaprogramować sam (lub z pomocą forumowiczów), jednak ja pisałem zestaw dedykowanych metod, analizujących pojedyncze linie i wyodrębniające z nich dane, wymagane do tworzenia nowych elementów drzew; Działają szybko, bo nie korzystają z redundantnych funkcji Pos, PosEx czy Copy, ale zajmują więcej linii; Najgorsze było stworzenia funkcji konwersji daty i czasu na łańcuch i vice versa, bo dość dużo przypadków muszą obsługiwać, ale poszło nadwyraz gładko; W Pythonie może to być łatwiejsze, a nawet może być do tego wbudowana funkcja - oby była, będziesz miał mniej roboty; Jednak w RTL Lazarusa był problem z UTF-8, więc trzeba było ją przepisać;
Parsowanie źródła tekstowego sprowadź do jak najprostszej postaci; U mnie główna metoda parsująca wygląda tak:
procedure TTSInfoTextInputReader.ProcessInput();
var
strLine: AnsiString;
begin
try
try
FTSInfoFile.FCurrentNode := FTSInfoFile.FRootNode;
PrepareSourceLines();
if IsCommentMarkerLine(FInput.Objects[FLineIndex]) then
begin
ExtractComment();
AddTreeComment();
end;
if IsTreeHeaderLine(FInput[FLineIndex]) then
AnalyzeTreeHeader()
else
ThrowException(EM_MISSING_TREE_HEADER, []);
while FLineIndex < FInput.Count do
begin
strLine := FInput[FLineIndex];
if IsCommentMarkerLine(FInput.Objects[FLineIndex]) then
ExtractComment()
else
if IsAttributeLine(strLine) then AddAttribute() else
if IsNodeLine(strLine) then AddNode() else
if IsEndNodeLine(strLine) then CloseNode() else
if IsLinkLine(strLine) then AddLink() else
if IsEndTreeLine(strLine) then Break
else
ThrowException(EM_INVALID_SOURCE_LINE, [strLine]);
end;
except
FTSInfoFile.DamageClear();
end;
finally
FTSInfoFile.FCurrentNode := FTSInfoFile.FRootNode;
FTSInfoFile.FModified := False;
end;
end;
Pole FTSInfoFile
w klasie parsera zawiera referencję do instancji klasy uzupełnianego drzewa;
Najpierw ustawia się wskaźnik aktualnego węzła na główny węzeł drzewa; Następnie przygotowuje się źródło - metoda PrepareSourceLines
usuwa wcięcia i puste linie, szuka frazy zakańczającej główne ciało drzewa i przenosi definicje elementów referencjonowanych w miejsca ich deklaracji; Źródłowe linie są przygotowane, więc w pierwszej kolejności trzeba spawdzić, czy drzewo posiada swój komentarz i jeśli tak - wyodrębnia się go i dodaje do odpowiedniego pola w klasie drzewa; Następnie parsuje się nagłówek i ekstrahuje opcjonalną nazwę drzewa; To tyle z rzeczy opcjonalnych - następnie to tylko pętelka po wszystkich liniach aż do końca głównego ciała drzewa; Każda iteracja wypakowuje opcjonalny komentarz elementu, a następnie w krótkiej drabince if
ów następuje rozpoznanie elementu i jego dodanie do dorzewa; Jak coś jest błędnie zapisane - wyjątek; Linia nierozpoznana - wyjątek; W razie wystąpienia wyjątku drzewo jest czyszczone awaryjnie (metoda DamageClear
) i usuwana jest flaga ffWrite
ze zbioru, aby drzewo nie zostało przypadkowo zapisane do pliku, podczas gdy nie mogło zostać poprawnie wczytane; Na koniec ustawienie wskaźnika na aktualnie otwarty węzeł (pole FCurrentNode
) na główny węzeł drzewa i ustawienie flagi pamiętającej modyfikacje na False; Tyle - drzewo w pamięci gotowe do molestowania;
Tutaj kończą się podstawy formatu - jeżeli Twoje API będzie sobie bezbłędnie radzić z drzewami, wypchanymi elementami standardowymi, przyjdzie czas na referencjonowanie elementów i linkowanie drzew z innych plików; Parsowanie źródła i generowanie wyjścia (tylko tekstowych) z wydzielonymi ciałami elementów referencjonowanych nie jest nie wiadomo jak trudne, jednak z poczatku wydaje się cholernie zagmatwane; Rekurencja w według mnie nie da prostoty implementacji, dlatego skorzystałem z fajnego sposobu znaczenia linii elementów referencjonowanych, które następnie są przesuwane w miejsca ich deklaracji, lub przesuwane poza główne ciało drzewa spod swoich deklaracji, podc zas generowania wyjścia; Stosowane są dwie metody - iteracyjna dla przetwarzania źródła lub tworzenia wyjścia, oraz rekurencyjna, dla przenoszenia definicji przed parsowaniem źródła i przenoszenia definicji elementów po wygenerowaniu wyjścia tekstowego; Obsługujące to metody nie są skomplikowane - dość łatwo je zapisać;
No i tyle, jeśli chodzi o pliki tekstowe; Ich przetwarzanie jest trudniejsze i bardziej czasochłonne, niż pliki binarne; Te drugie z kolei tworzone są tak łatwo, że kod klas ładujących drzewa ze źródeł binarnych i generujących wyjście binarne jest bardzo krótki; Oczywiście bez rekurencji się nie obejdzie, ale brak tam jakiegokolwiek parsowania, więc przy ładowaniu pliku binarnego wystarczy od razu pobierać ze strumienia porcje danych i na ich podstawie tworzyć elementy w pamięci; Nad poprawnością danych w drzewach musi czuwać klasa drzewa, więc podczas ładowania pliku ze źródła binarnego jedyne co trzeba sprawdzić to sygnaturę pliku; Zaś generowanie pliku binarnego na podstawie instancji klasy drzewa jest jeszcze szybsze, bo już w ogóle nic nie trzeba sprawdzać; Dlatego obsługa plików binarnych powinna być o wiele szybsza, niż ich tekstowych odpowiedników; W przypadku drzew bez elementów referencjonowanych powinien być to zysk około 40%, przy dłuższych i bardziej złożonych drzewach (z elementami referencjonowanymi) zysk powinien być jeszcze większy (muszę w końcu zrobić ten benchmark, bo te dane są trochę z d**y - badałem to podczas pisania parserów);
W każdym razie pliki binarne nie powinny dla nikogo stanowić problemu - są beznadziejnie proste w obsłudze, ale właśnie o to chodziło; Dobrze by było, aby zmiana formy drzewa była jak najprostsza; Jeżeli komuś zachce się zmienić formę drzewa (np. z tekstowej na binarną, bo mu konfiguracja mocno spuchła), niech mam możliwość uczynienia tego w jak najprostszy sposób; W moim API wystarczy zmienić zestaw flag drzewa, np. tak:
var
tsiConfig: TSimpleTSInfoFile;
{...}
tsiConfig := TTSInfoFile.Create('C:\File.tsinfo', [ffLoadFile, ffTextFile, ffWrite]);
{..}
{ zmiana formy pliku z tekstowej na binarną }
tsiConfig.FileFlags := [ffLoadFile, ffBinaryFile, ffWrite];
i tyle - w destruktorze klasy drzewa użyty zostanie generator binarny, więc i drzewo zostanie zapisane w postaci binarnej; Przy odczycie drzewa z pliku wystarczy użyć innej flagi;
Jak widzisz całość nie jest trudna do zaprogramowania, jednak dopóki nie zobaczysz rozwiązania - może się to wydawać skomplikowane i bardzo trudne do zaprogramowania; Rozpisałem się, więc na koniec dodam, że jeżeli chcesz coś wiedzieć na temat formatu czy sposobów przetwarzania drzew to pytaj - z chęcią udzielę wskazów, bo swoje API znam na pamięć; Dodatkowo, jeśli będziesz chciał, możesz korzystać z dokumentacji do mojego API; Język nie gra roli, bo każdy element mojej biblioteki jest szczegółowo opisany w dokumentacji, wraz z podanym kodem (jeśli tym elementem jest metoda, funkcja, procedura); Możesz też korzystać z tutoriala do mojego API - wyczaisz jak z niego można korzystać i jak sam możesz ustalić sposoby obsługi klasy drzewa; Obsługa musi być prosta i przyjemna dla użytkownika, więc jest się nad czym zastanawiać.