Przetwarzanie pliku ULIC.xml
Plik ULIC.xml zawiera zbiór identyfikatorów i nazw ulic. Plik ten, został pobrany do bieżącego katalogu bazy danych, do folderu o nazwie TERYT. Na stronie Opis struktury zbioru ULIC możemy zapoznać się ze strukturą tego pliku.
- Opis struktury zbioru ULIC
- WOJ - symbol województwa - 2 zn. C
- POW - symbol powiatu - 2 zn. C
- GMI - symbol gminy - 2 zn. C
- RODZ_GMI (*) - symbol rodzaju jednostki - 1 zn. C
- 1 - gmina miejska,
- 2 - gmina wiejska,
- 3 - gmina miejsko-wiejska,
- 4 - miasto w gminie miejsko-wiejskiej,
- 5 - obszar wiejski w gminie miejsko-wiejskiej,
- 8 - dzielnica w m.st. Warszawa,
- 9 - delegatury w gminach miejskich
- SYM - identyfikator miejscowości - 7 zn. C
- SYM_UL - identyfikator ulicy - 5 zn. C
- CECHA - określenie rodzaju ulicy - 5 zn. C
(ul., al., pl., skwer, bulw., rondo, park, rynek, szosa, droga, os., ogród, wyspa, wyb., inne)
- NAZWA_1 - część nazwy począwszy od słowa, które decyduje o pozycji ulicy w układzie alfabetycznym, - 100 zn. C
- NAZWA_2 - pozostała część nazwy lub pole puste - 100 zn. C (**)
- STAN_NA - data aktualizacji danych w podsystemie ULIC w formacie RRRR-MM-DD - 10 zn. C
(*) - w opisie struktury zbioru TERC element nazwany jest: RODZ.
(**) - w przypadku, gdy pole Nazwa_2 nie jest puste, aby otrzymać nazwę ulicy w pełnym brzmieniu,
człony nazwy należy ułożyć w kolejności: Nazwa_2, Nazwa_1.
Struktura pliku ULIC.xml
Ponieważ struktura pliku ULIC.xml została zmieniona, funkcja przetwarzający plik ULIC.xml
musiała zostać dostosowana do obowiązującej struktury pliku (nazw zmienionych elementów) przetwarzanego pliku ULIC.xml.
Tabele tblULIC_Nazwy i tblULIC_Ulice pozostały bez zmian, ponieważ nie zostały zmienione wielkości elementów.
Plik ULIC.xml zawiera 267 028 elementów <row>, z których każdy z nich zawiera 10 elementów <col>,
co daje 2 670 280 pojedynczych elementów. Za wyjątkiem elementu <"NAZWA_2">, który może być pusty,
wszystkie pozostałe elementy zawierają dane (nie są puste).
Poniżej struktura starego pliku ULIC.xml z dnia 2015-09-25 i struktura aktualnego pliku ULIC.xml,
dla dwóch pierwszych elementów <row> zbioru ULIC.xml.
<?xml version="1.0" encoding="UTF-8"?> <teryt> <catalog name="ULIC" type="all" date="2015-09-25"> <row> <col name="WOJ">02</col> <col name="POW">22</col> <col name="GMI">02</col> <col name="RODZ_GMI">2</col> <col name="SYM">0882939</col> <col name="SYM_UL">26171</col> <col name="CECHA">ul.</col> <col name="NAZWA_1">Zjazdowa</col> <col name="NAZWA_2"/> <col name="STAN_NA">2015-09-25</col> </row> <row> <col name="WOJ">02</col> <col name="POW">22</col> <col name="GMI">03</col> <col name="RODZ_GMI">4</col> <col name="SYM">0987466</col> <col name="SYM_UL">00285</col> <col name="CECHA">ul.</col> <col name="NAZWA_1">Andersa</col> <col name="NAZWA_2">gen. Władysława </col> <col name="STAN_NA">2015-09-25</col> </row> </catalog> </teryt>
<?xml version="1.0" encoding="utf-8"?> <ULIC> <catalog name="ULIC" type="ALL" date="2018-03-01"> <row> <WOJ>02</WOJ> <POW>01</POW> <GMI>01</GMI> <RODZ_GMI>1</RODZ_GMI> <SYM>0935989</SYM> <SYM_UL>00432</SYM_UL> <CECHA>ul.</CECHA> <NAZWA_1>Armii Krajowej</NAZWA_1> <NAZWA_2> </NAZWA_2> <STAN_NA>2018-03-01</STAN_NA> </row> <row> <WOJ>02</WOJ> <POW>01</POW> <GMI>01</GMI> <RODZ_GMI>1</RODZ_GMI> <SYM>0935989</SYM> <SYM_UL>00891</SYM_UL> <CECHA>ul.</CECHA> <NAZWA_1>Stefana Batorego</NAZWA_1> <NAZWA_2> </NAZWA_2> <STAN_NA>2018-03-01</STAN_NA> </row> </catalog> </ULIC>
Struktura tabeli tblULIC_Nazwy.
Znając strukturę zbioru ULIC musimy utworzyć dwie tabelę na przyjęcie danych z pliku ULIC.xml. Struktura tabel przedstawiona jest poniżej:
Tabela tblULIC_Nazwy - struktura | |||||
Nazwa pola*) | Typ | Rozmiar**) | Wymagane | Zerowa długość | Uwagi |
ID_SymUl | Tekst | 5 | Tak | Nie | PrimaryKey |
tCecha | Tekst | 5 | Tak | Nie | Indeks. Duplikaty (OK) |
tNazwa_1 | Tekst | 100 | Tak | Nie | Indeks. Duplikaty (OK) |
tNazwa_2 | Tekst | 100 | Nie | Nie | |
Zawiera zbiór niepowtarzających się nazw ulic(zgodne z brzmieniem uchwał o ich nadaniu) oraz ich identyfikatory, utworzony przez alfabetyczne ułożenie nazw ulic w ramach całego kraju. |
Tworzenie tabeli tblULIC_Nazwy.
Tabelę tblULIC_Nazwy możemy utworzyć korzystając z metod klasy clsTeryt opisanej na stronie: Klasa clsTeryt
Dim clsULIC As clsTeryt Set clsULIC = New clsTeryt With clsULIC .terytDeleteTable "tblULIC_Nazwy" .terytCreateTable "tblULIC_Nazwy" .terytCreateField "ID_SymUl", dbText, 5 .terytCreateField "tCecha", dbText, 5 .terytCreateField "tNazwa_1", dbText, 100 .terytCreateField "tNazwa_2", dbText, 100, False .terytAppendTable ' utwórz Indeks Primary .terytCreateIndex "ID_SymUl", True ' utwórz Indeksy (duplikaty OK) .terytCreateIndex "tCecha" .terytCreateIndex "tNazwa_1" End With Set clsULIC = Nothing
Struktura tabeli tblULIC_Ulice.
Tabela tblULIC_Ulice - struktura | |||||
Nazwa pola*) | Typ | Rozmiar**) | Wymagane | Zerowa długość | Uwagi |
Id_Sym | Tekst | 7 | Tak | Nie | PrimaryKey |
Id_SymUl | Tekst | 5 | Tak | Nie | |
tStan_Na | Tekst | 10 | Tak | Nie | |
Elementy <WOJ>,<POW>,<GMI>,<RODZ_GMI> ze zbioru ULIC nie są potrzebne, ponieważ pole <Id_Sym> jednoznacznie określa miejscowość i jej położenie terytorialne w tabeli tblSIMC_Miejscowosci. |
Tworzenie tabeli tblULIC_Ulice.
Tabelę tblULIC_Ulice możemy utworzyć korzystając z metod klasy clsTeryt opisanej na stronie: Klasa clsTeryt
Dim clsULIC_ulic As clsTeryt Set clsULIC_ulic = New clsTeryt With clsULIC_ulic .terytDeleteTable "tblULIC_Ulice" .terytCreateTable "tblULIC_Ulice" .terytCreateField "Id_Sym", dbText, 7 .terytCreateField "Id_SymUl", dbText, 5 .terytCreateField "tStan_Na", dbText, 10 .terytAppendTable ' utwórz Indeks Primary .terytCreateIndex "Id_Sym", True End With Set clsULIC_ulic = Nothing
Uwagi dodatkowe. Dotyczą wszystkich tabel na tej stronie. |
*) - Dla pola będącego kluczem głównym, stosuję prefiks „ID_”, - Klucz obcy zawsze poprzedzam prefiksem „Id_” - Nazwy pól nie będące kluczem głównym i kluczem obcym poprzedzam prefiksem „t” |
**) Minimalna długość pola. Można użyć większego rozmiaru pola. MS Access nie dopełnia pól tekstowych do zadeklarowanej długości. |
Skoro mamy utworzone dwie nowe tabele, to możemy pobrać dane z pliku ULIC.xml i zapisać je w tabeli
tblULIC_Nazwy oraz tabeli tblULIC_Ulice. Jak to zrobić ? Tym razem nie jest to takie proste.
Dokonując niewielkich zmian w kodzie, funkcję fXmlSimcToTable_DOM (...)
możemy ją dostosować do przetwarzania pliku ULIC.xml.
Public Function fXmlUlicToTable_DOM( _ ByVal sFileXmlPath As String, _ ByVal sTblUlic As String, _ ByVal sTblUlic_Nazwy As String) As Boolean #If pbl_fEarly = True Then Dim xmlDoc As MSXML2.DOMDocument60 Dim oNode As MSXML2.IXMLDOMNode #Else Dim xmlDoc As Object Dim oNode As Object #End If Dim sXPath As String Dim dbs As DAO.Database Dim rstUlice As DAO.Recordset Dim rstNazwy As DAO.Recordset Const cParser_XML As String = "MSXML2.DOMDocument" Const cModule_Name As String = "Function fXmlUlicToTable_DOM (...)" Dim i As Long On Error GoTo Err_Handler ' Utwórz obiekt analizatora składni XML (parsera XML) #If pbl_fEarly = True Then ' Wczesne wiązanie (early binding) Set xmlDoc = New MSXML2.DOMDocument60 #Else ' Późne wiązanie (late binding) Set xmlDoc = CreateObject("MSXML2.DOMDocument") #End If With xmlDoc .Load (sFileXmlPath) ' sprawdź poprawność wczytanego XML'a If (.parseError.errorCode <> 0) Then Err.Raise .parseError.errorCode, cParser_XML, .parseError.reason End If ' utwórz adres do elementów <row> sXPath = "/ULIC/catalog/row" ' utwórz zmienną obiektową odwołującą się do pojedynczego elementu <row> Set oNode = .selectSingleNode(sXPath) End With Set dbs = CurrentDb Set rstUlice = dbs.OpenRecordset(sTblUlic, dbOpenDynaset, dbAppendOnly) Set rstNazwy = dbs.OpenRecordset(sTblUlic_Nazwy, dbOpenDynaset, dbAppendOnly) Do Until oNode Is Nothing With oNode.childNodes ' sprawdź, czy ulica znajduje się w tabeli rstNazwy.FindFirst ("[ID_SymUl]='" & .Item(5).Text & "'") ' jeżeli nie, to zapisz dane ulicy If rstNazwy.NoMatch Then rstNazwy.AddNew rstNazwy!Id_SymUl = .Item(5).Text rstNazwy!tCecha = .Item(6).Text rstNazwy!tNazwa_1 = .Item(7).Text ' jeżeli element "NAZWA_2" nie jest pusty, to go zapisz If Len(.Item(8).Text) > 0 Then rstNazwy!tNazwa_2 = .Item(8).Text End If rstNazwy.Update End If rstUlice.AddNew rstUlice!Id_Sym = .Item(4).Text rstUlice!Id_SymUl = .Item(5).Text rstUlice!tStan_Na = .Item(9).Text rstUlice.Update End With Set oNode = oNode.nextSibling Loop fXmlUlicToTable_DOM = True Exit_Here: ' zniszcz zmienne obiektowe If Not (rstNazwy Is Nothing) Then rstNazwy.Close If Not (rstUlice Is Nothing) Then rstUlice.Close Set rstNazwy = Nothing Set rstUlice = Nothing Set dbs = Nothing Set xmlDoc = Nothing Exit Function Err_Handler: MsgBox "Błąd nr " & Err.Number & vbNewLine & _ Err.Description & vbNewLine & _ "Źródło: Baza Miejscowości TERYT" & vbNewLine & _ "Moduł: " & cModule_Name Resume Exit_Here End Function
Czas przetwarzania przez funkcję fXmlULICToTable_DOM (...) pliku ULIC.xml, wielkości 80 MB,
zawierającego 267 028 elementów <row> z których każdy zawiera 10 elementów <col>
(w sumie 2 670 280 elementarnych danych) wynosi:
-
wczesne wiązanie
wczytanie pliku ULIC.xml do pamięci 22,3 sek. przetwarzanie i zapis do tabeli 10 000 rekordów pliku: 507 sek. co dla 26 000 rekordów daje w przybliżeniu: 507 x 26 = 13 196 (ok. 3,6 godz.) -
późne wiązanie
wczytanie pliku ULIC.xml do pamięci 27,8 sek. przetwarzanie pliku i zapis do tabel: 13 762 sek. (ok. 3,8 godz.)
Trzeba przyznać, że są to makabrycznie długie czasy przetwarzania pliku ULIC.xml. Wyniki mówią same za siebie. Korzystanie z obiektu MSXML2.DOMDocument jest tak przeraźliwie wolne, że wniosek może być tylko jeden. Metoda ta w przypadku dużych plików *.xml nie zdaje egzaminu i należy z niej zrezygnować.
Zobaczmy, jak sprawdzi się metoda mieszana: „późne wiązanie” + VBA. Po wstępnym przekształceniu do naszych potrzeb funkcji fXmlSimcToTable_VBA (...) przetwarzającej plik SIMC.xml, zobaczymy czy metoda mieszana spełni nasze oczekiwania podczas przetwarzania pliku ULIC.xml.
- Przetestujemy dwie pierwsze kluczowe instrukcje:
- • wczytanie pliku ULIC.xml metodą .loadXML obiektu MSXML2.DOMDocument
- • rozdzielenie wczytanego tekstu .xml na elementy (linie) względem znaku końca linii, za pomocą funkcji Split(...)
' Utwórz obiekt analizatora składni XML (parsera XML) Set xmlDoc = CreateObject("MSXML2.DOMDocument") With xmlDoc .Load (sFileXmlPath) ' sprawdź poprawność wczytanego XML'a If (.parseError.errorCode <> 0) Then Err.Raise .parseError.errorCode, cParser_XML, .parseError.reason End If ' rozdziel wczytany tekst .xml na elementy (linie) względem znaku końca linii aLines() = Split(.xml, vbNewLine, , vbBinaryCompare) End With ' obiekt "MSXML2.DOMDocument" jest już nam niepotrzebny Set xmlDoc = Nothing ' dalsze instrukcje
I wszystko by było dobrze, gdyby nie nastąpiła zmiana struktury pliku ULIC.xml i sposób zapisu pustego elementu Pusty element [NAZWA] w pliku ULIC.xml jest zapisany w postaci:
<NAZWA_2>
<NAZWA_2 />
Ale gdy wrócimy do przetwarzanego już wcześniej pliku TERC.xml, to w pliku tym puste elementy zapisywanę są w postaci:
<GMI />
<RODZ />
W wyniku takiego zapisu instrukcja:
aLines = Split(.xml, vbNewLine, , vbBinaryCompare)
dla pustego elementu [NAZWA] zwróci o jeden element więcej w tablicy aLines().
Rozwiązanie niby jest proste. Użyć funkcji Replace(...) by zamienić dwulinijkowy pusty element na jednolinijkowy:
With xmlDoc .Load (sFileXmlPath sStr = Replace(.xml, "<NAZWA_2>" & vbNewLine, "<NAZWA_2>", 1, -1, vbBinaryCompare) aLines = Split(sStr, vbNewLine, , vbBinaryCompare)
Proste jak precelek. Tyle tylko, że zmienna znakowa .xml zawiera dane wielkości ok. 80 MB
i niektóre Accessy się buntują. Access 2007, 2010 i 2016 w 64-bitowym środowisku Windows 7
działają powolutku, ale wszystko jest OK.
Ale w Windows 10 i MS Access 2010 mogą być same niespodzianki.

Windows 10 i funkcja Replace w MS Access 2010 nie lubi dużych plików...
Nie jestem w stanie przeprowadzić testów dla Access 2007, 2010, 2016 w systemach operacyjnych Windows 7, Windows 8,
Windows 10 i w środowisku 32-bit i 64-bit.
U mnie ... SOA#1☺
Dość narzekania. Ad rem
- Dla pliku wielkości 80 MB funkcja Replace zazwyczaj daje sobie radę, ale tylko zazwyczaj. Więc wniosek jest prosty. Dla tak dużych ciągów znaków nie używać funkcji Replace.
-
Dla pliku wielkości 80 MB funkcja Split względem elementu <row> zazwyczaj daje sobie radę.
Po zastosowaniu funkcji Split, dla każdego zwracanego elementu ponownie zastosować funkcję Split
względem elementu "</".
Potem jakieś tam Replace, Replace, Replace i może coś wyjdzie. Nie będzie to piękne, ale może skuteczne. - A za rok może dwa znowu będą walki z .xml-em☺.
Zmodyfikowana metoda rozdzielenia wczytanego tekstu .xml na elementy, za pomocą funkcji Split(...) polega na:
- wczytaniu pliku ULIC.xml metodą .loadXML obiektu "MSXML2.DOMDocument"
- rozdzielenie wczytanego tekstu .xml do tablicy aRows(), względem elementu <row>,
-
w pętli For i = LBound(aRows) + 1 To UBound(aRows)
rozdzielenie każdego elementu tablicy aRows(i) do tablicy aCols(), względem elementu </
i przetwarzanie kolejnych elementów tablicy aCols() za pomocą funkcji getTagValue by zapisać zwracane dane do tabel MS Access.
Skoro mamy utworzone dwie nowe tabele, to możemy pobrać dane z pliku ULIC.xml
i zapisać je w tabeli tblULIC_Nazwy oraz tabeli tblULIC_Ulice.
Jak to zrobić ? Po prostu skorzystać ze zmodyfikowanej starej funkcji:
Public Function fXmlUlicToTable_Split( _ ByVal sFileXmlPath As String, _ ByVal sTblUlic As String, _ ByVal sTblUlic_Nazwy As String) As Boolean
Public Function fXmlUlicToTable_Split( _ ByVal sFileXmlPath As String, _ ByVal sTblUlic As String, _ ByVal sTblUlic_Nazwy As String) As Boolean Dim xmlDoc As Object Dim aRows() As String Dim aCols() As String Dim sID_SymUl As String Dim sNAZWA_2 As String Dim i As Long Dim dbs As DAO.Database Dim rstUlice As DAO.Recordset Dim rstNazwy As DAO.Recordset Const cParser_XML As String = "MSXML2.DOMDocument" Const cModule_Name As String = "Function fXmlUlicToTable_Split (...)" On Error GoTo Err_Handler ' Utwórz obiekt analizatora składni XML (parsera XML) Set xmlDoc = CreateObject("MSXML2.DOMDocument") With xmlDoc .Load (sFileXmlPath) ' sprawdź poprawność wczytanego XML'a If (.parseError.errorCode <> 0) Then Err.Raise .parseError.errorCode, cParser_XML, .parseError.reason End If ' rozdziel wczytany .xml względem oznaczenia początku elementu wiersza <row> aRows = Split(.xml, "<row>", , vbBinaryCompare) End With ' obiekt "MSXML2.DOMDocument" jest już nam niepotrzebny Set xmlDoc = Nothing Set dbs = CurrentDb Set rstUlice = dbs.OpenRecordset(sTblUlic, dbOpenDynaset, dbAppendOnly) Set rstNazwy = dbs.OpenRecordset(sTblUlic_Nazwy, dbOpenDynaset, dbAppendOnly) ' nie przetwarzaj pierwszego elementu For i = LBound(aRows) + 1 To UBound(aRows) ' rozdziel element aRows(i) na poszczególne kolumny do tablicy typu String aCols = Split(aRows(i), "</", , vbBinaryCompare) With rstNazwy sID_SymUl = getTagValue(aCols(5)) ' sprawdź, czy ulica znajduje się w tabeli .FindFirst ("[ID_SymUl]='" & sID_SymUl & "'") ' jeżeli nie, to zapisz dane ulicy If .NoMatch Then .AddNew !Id_SymUl = sID_SymUl !tCecha = getTagValue(aCols(6)) !tNazwa_1 = getTagValue(aCols(7)) sNAZWA_2 = getTagValue(aCols(8)) If Len(sNAZWA_2) > 0 Then !tNazwa_2 = sNAZWA_2 .Update End If End With ' zapisz Id miejscowości i Id ulicy With rstUlice .AddNew !Id_Sym = getTagValue(aCols(4)) !Id_SymUl = sID_SymUl !tStan_Na = getTagValue(aCols(9)) .Update End With Next fXmlUlicToTable_Split = True Exit_Here: ' zniszcz zmienne obiektowe If Not (rstNazwy Is Nothing) Then rstNazwy.Close If Not (rstUlice Is Nothing) Then rstUlice.Close Set rstNazwy = Nothing Set rstUlice = Nothing Set dbs = Nothing Set xmlDoc = Nothing Exit Function Err_Handler: MsgBox "Błąd nr " & Err.Number & vbNewLine & _ Err.Description & vbNewLine & _ "Źródło: Baza Miejscowości TERYT" & vbNewLine & _ "Moduł: " & cModule_Name Resume Exit_Here End Function
Przetworzenie całego 80 MB pliku ULIC.xml zajęło ok. 125 sek. z czego ok. 25 sekund trwało wczytanie pliku ULIC.xml metodą .loadXML obiektu MSXML2.DOMDocument. Całkowity czas obróbki 267 028 rekordów wynosi ok. 2 minuty w porównaniu do 216 minut z użyciem obiektu MSXML2.DOMDocument.
Windows 10 i 64-bitowy MS Access 2010.
NIESPODZIANKA!
Windows 10 i 64-bitowy MS Access 2010.
Wczesne wiązanie.
• Wczytanie pliku ULIC.xml metodą .loadXML obiektu MSXML2.DOMDocument - 2,5 sekundy
• Całkowity czas obróbki 267 028 rekordów wynosi ok. 2 minuty.
Późne wiązanie.
W tym przypadku niespodzianki nie ma. Trzeba cierpliwie czekać 3 - 4 godz.
Podsumowując całe przetwarzanie „Rejestru miejscowości i ulic” (Bazy rejestru TERYT), można stwierdzić,
że opisane sposoby przetwarzania są trochę kapryśne, trochę pokomplikowane i nie da się ładnie napisać kodu przetwarzającego
wszystkie
Pliki pełne rejestru TERYT, czyli pliki WMRODZ.xml, TERC.xml, SIMC.xml oraz ULIC.xml.
Ładny kod da się napisać, ale czekać 3-4 godziny na wykonanie tego ładnego kodu, to troszeczkę za długo.
Obecnie „Pełne pliki rejestru” TERYT zawierają pliki *.xml oraz pliki typu *.csv. Nie jestem pewien,
ale chyba dawniej w pobranych skompresowanych archiwach nie było plików *.csv, ale mogę się mylić.
Pobieżnie przeszukałem internet w roku 2010 na pewno nie były dostępne pliki *.csv. Także w 2015 roku nie były dostępne pliki *.csv
ponieważ z tych lat dostępne były skryptt PHP przetwarzający plik *.xml na plik *.csv.
W najbliższym, bliżej nieokreślonym, czasie postaram się zamieścić przykład przedstawiający pobieranie danych z plików rejestru TERYT o rozszerzeniu *.csv.