├── LICENSE
└── README.md
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Radosław Kućmierowski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pobieranie danych z rejestru TERYT
2 |
3 | GUS udostępnia usługę sieciową [TERYT ws1](https://api.stat.gov.pl/Home/TerytApi).
4 | Dane przesyłane są w formacie XML protokołem [SOAP](https://pl.wikipedia.org/wiki/SOAP).
5 | Potrzebujemy więc klienta, który obsłuży ten protokół, np. [zeep](https://python-zeep.readthedocs.io/en/master/):
6 | ```bash
7 | pip install zeep
8 | ```
9 |
10 | Aby aplikacja mogła pobierać dane z rejestru musimy przejść uwierzytelnienie.
11 | Dane do logowania zapiszemy w słowniku:
12 | ```python
13 | CREDENTIALS = {
14 | 'wsdl': 'https://uslugaterytws1test.stat.gov.pl/wsdl/terytws1.wsdl',
15 | 'username': 'TestPubliczny',
16 | 'password': '1234abcd'
17 | }
18 | ```
19 |
20 | Są to dane do środowiska testowego.
21 | Aby korzystać z usługi na produkcji,
22 | należy wysłać maila do GUSu z prośbą o założenie prywatnego konta.
23 |
24 | Tworzymy nową instancję klienta:
25 | ```python
26 | from zeep import Client
27 | from zeep.wsse.username import UsernameToken
28 |
29 | token = UsernameToken(
30 | username=CREDENTIALS['username'],
31 | password=CREDENTIALS['password']
32 | )
33 | client = Client(wsdl=CREDENTIALS['wsdl'], wsse=token)
34 | ```
35 |
36 | Sprawdzamy czy uda się nawiązać połączenie z usługą:
37 | ```python
38 | print(client.service.CzyZalogowany())
39 | ```
40 |
41 | Jeżeli wszystko jest ok, powinno wypisać ```True```.
42 |
43 | > **Update:** W przypadku wystąpienia błędu:
44 |
45 | > `zeep.exceptions.XMLParseError: The namespace defined on the xsd:import doesn't match the imported targetNamespace located at 'https://uslugaterytws1test.stat.gov.pl/wsdl/xsd1.xsd' (https://uslugaterytws1test.stat.gov.pl/wsdl/terytws1.wsdl:53)
46 | `
47 |
48 | > należy pobrać wskazany w komunikacie plik [terytws1.wsdl](https://uslugaterytws1test.stat.gov.pl/wsdl/terytws1.wsdl) i w linii nr 53 zmienić odwołanie z `xsd1.xsd` na `xsd2.xsd`, a następnie w słowniku `CREDENTIALS` dla klucza `wsdl` podać ścieżkę do zmodyfikowanego pliku terytws1.wsdl.
49 |
50 | Kiedy mamy już do dyspozycji obiekt klienta,
51 | możemy na nim wywoływać metody dostępne w TERYT ws1
52 | (pełna lista metod w [instrukcji](https://api.stat.gov.pl/Content/files/teryt/instrukcja_techniczna_uslugi_teryt_ws1.zip)).
53 |
54 | Wiele z tych metod wymaga podania daty jako argumentu. Zatem:
55 | ```python
56 | from datetime import datetime
57 |
58 | STATE_DATE = datetime.now()
59 | ```
60 |
61 | Spróbujmy pobrać listę województw:
62 | ```python
63 | client.service.PobierzListeWojewodztw(STATE_DATE)
64 | ```
65 |
66 | Wywołanie zwróci listę obiektów słownikopodobnego typu JednostkaTerytorialna.
67 | Oto jeden z nich:
68 | ```python
69 | {
70 | 'GMI': None,
71 | 'NAZWA': 'DOLNOŚLĄSKIE',
72 | 'NAZWA_DOD': 'województwo',
73 | 'POW': None,
74 | 'RODZ': None,
75 | 'STAN_NA': '1/2/2018 12:00:00 AM',
76 | 'WOJ': '02'
77 | }
78 | ```
79 |
80 | Możemy więc, powołując się na klucz ```'NAZWA'```, stworzyć listę złożoną z nazw:
81 | ```python
82 | [e['NAZWA'] for e in client.service.PobierzListeWojewodztw(STATE_DATE)]
83 | ```
84 |
85 | I voilà:
86 | ```python
87 | ['DOLNOŚLĄSKIE', 'KUJAWSKO-POMORSKIE', 'LUBELSKIE', 'LUBUSKIE', 'ŁÓDZKIE',
88 | 'MAŁOPOLSKIE', 'MAZOWIECKIE', 'OPOLSKIE', 'PODKARPACKIE', 'PODLASKIE',
89 | 'POMORSKIE', 'ŚLĄSKIE', 'ŚWIĘTOKRZYSKIE', 'WARMIŃSKO-MAZURSKIE',
90 | 'WIELKOPOLSKIE', 'ZACHODNIOPOMORSKIE']
91 | ```
92 |
93 | ## Wyszukiwanie jednostek
94 |
95 | Jednostki terytorialne możemy wyszukiwać m.in. po nazwie:
96 | ```python
97 | client.service.WyszukajJPT(nazwa='Warszawa')
98 | ```
99 |
100 | ```python
101 | [{
102 | 'GmiNazwa': None,
103 | 'GmiNazwaDodatkowa': 'miasto stołeczne, na prawach powiatu',
104 | 'GmiRodzaj': None,
105 | 'GmiSymbol': None,
106 | 'PowSymbol': '65',
107 | 'Powiat': 'Warszawa',
108 | 'WojSymbol': '14',
109 | 'Wojewodztwo': 'MAZOWIECKIE'
110 | }, {
111 | 'GmiNazwa': 'Warszawa',
112 | 'GmiNazwaDodatkowa': 'gmina miejska, miasto stołeczne',
113 | 'GmiRodzaj': '1',
114 | 'GmiSymbol': '01',
115 | 'PowSymbol': '65',
116 | 'Powiat': 'Warszawa',
117 | 'WojSymbol': '14',
118 | 'Wojewodztwo': 'MAZOWIECKIE'
119 | }]
120 | ```
121 |
122 | Wyszukiwanie z użyciem identyfikatora TERC
123 | wymaga stworzenia specjalnego obiektu klasy identyfikatory.
124 | Posłuży nam do tego fabryka, którą dostarcza klient:
125 | ```python
126 | factory = client.type_factory('ns2')
127 | ```
128 |
129 | Prefix ```'ns2'``` wskazuje na określoną przestrzeń nazw,
130 | w której zadeklarowane zostały klasy obiektów.
131 | Ale skąd wiemy jakiego namespace użyć? Oto ściągawka:
132 | ```python
133 | print(client.wsdl.dump())
134 | ```
135 |
136 | Powyższe polecenie wyświetli wszystkie dostępne prefixy, klasy, metody,
137 | jednym słowem cały schemat usługi.
138 |
139 | Tworzymy nowy identyfikator podając string z numerem TERC:
140 | ```python
141 | identyfikator = factory.identyfikatory(terc='1465011')
142 | ```
143 |
144 | Ale to nie wszystko. Metoda ```WyszukajJednostkeWRejestrze()```
145 | wymaga podania listy identyfikatorów, a nie pojedynczego numeru.
146 |
147 | Uruchamiamy fabrykę ponownie:
148 | ```python
149 | array = factory.ArrayOfidentyfikatory(identyfikator)
150 | ```
151 |
152 | Do pełni szczęścia pozostaje nam wskazać kategorię szukanej jednostki.
153 | Zgodnie z instrukcją "0" oznacza wyszukiwanie wśród wszystkich rodzajów.
154 | ```python
155 | client.service.WyszukajJednostkeWRejestrze(identyfiks=array, kategoria=0, DataStanu=STATE_DATE)
156 | ```
157 |
158 | ```python
159 | [{
160 | 'GmiNazwa': 'Warszawa',
161 | 'GmiNazwaDodatkowa': 'gmina miejska, miasto stołeczne',
162 | 'GmiRodzaj': '1',
163 | 'GmiSymbol': '01',
164 | 'PowSymbol': '65',
165 | 'Powiat': 'Warszawa',
166 | 'WojSymbol': '14',
167 | 'Wojewodztwo': 'MAZOWIECKIE'
168 | }]
169 | ```
170 |
171 | Prościej wygląda sprawa z miejscowościami. Można od razu użyć numeru SIMC:
172 | ```python
173 | client.service.WyszukajMiejscowosc(identyfikatorMiejscowosci='0329898')
174 | ```
175 |
176 | ```python
177 | [{
178 | 'GmiRodzaj': '2',
179 | 'GmiSymbol': '04',
180 | 'Gmina': 'Pcim',
181 | 'Nazwa': 'Pcim',
182 | 'PowSymbol': '1209',
183 | 'Powiat': 'myślenicki',
184 | 'Symbol': '0329898',
185 | 'WojSymbol': '12',
186 | 'Wojewodztwo': 'MAŁOPOLSKIE'
187 | }]
188 | ```
189 |
190 | ## Pobieranie katalogów
191 |
192 | Usługa umożliwia również pobieranie całych zbiorów danych.
193 | W odpowiedzi na żądanie otrzymujemy obiekt klasy PlikKatalog
194 | posiadającej właściwości:
195 | * nazwa_pliku – string z sugerowaną nazwą pliku
196 | * plik_zawartosc – string z zakodowaną w [Base64](https://pl.wikipedia.org/wiki/Base64) treścią pliku zip
197 | * opis – string z opisem pliku.
198 |
199 | A zatem przesłany zostaje plik binarny,
200 | podobnie jak załączniki w poczcie elektronicznej.
201 |
202 | Pobierzemy teraz katalog miejscowości:
203 | ```python
204 | catalog = client.service.PobierzKatalogSIMC(STATE_DATE)
205 | ```
206 |
207 | Jego właściwości zapiszemy do zmiennych:
208 | ```python
209 | filename = catalog['nazwa_pliku']
210 | content = catalog['plik_zawartosc']
211 | ```
212 |
213 | Można oczywiście użyć własnej nazwy pliku, np.: ```filename='katalog.zip'```,
214 | a także zmienić ścieżkę: ```filename=os.path.expanduser('~/Desktop/katalog.zip')```.
215 |
216 | Zawartość pliku odkodujemy używając funkcji ```b64decode()```
217 | z modułu base64 z biblioteki standardowej:
218 | ```python
219 | from base64 import b64decode
220 |
221 | decoded = b64decode(content)
222 | ```
223 |
224 | A tak wygląda treść zakodowana i odkodowana (fragment):
225 | ```python
226 | CONTENT: c0bANbUuAEcAAAAU0lNQ19VcnplZG9
227 | DECODED: b'\x01\x1c\x00\x00\x00SIMC_Urzedowy_2018-11-23.'
228 | ```
229 |
230 | Odkodowaną treść zapiszemy jako plik na dysku pod wskazaną nazwą:
231 | ```python
232 | with open(filename, 'wb') as file:
233 | file.write(decoded)
234 | file.close()
235 | ```
236 |
237 | Plik zip został "zmaterializowany" i można go otworzyć.
238 | Ale nie będziemy przecież otwierać tego ręcznie.
239 | Biblioteka standardowa oferuje odpowiednie narzędzia:
240 | ```python
241 | from zipfile import ZipFile
242 |
243 | zf = ZipFile(filename, 'r')
244 | ```
245 |
246 | Otrzymaliśmy pythonową reprezentację zapisanego wcześniej pliku.
247 | Sprawdźmy co znajduje się wewnątrz:
248 | ```python
249 | print(zf.namelist())
250 | ```
251 |
252 | Widzimy, że znajdują się tam dwa pliki, XML i CSV:
253 | ```python
254 | ['SIMC_Urzedowy_2018-11-23.xml', 'SIMC_Urzedowy_2018-11-23.csv']
255 | ```
256 |
257 | Przeczytajmy teraz pierwszy z nich (ale nie wszystko, kilobajt tylko, stąd n=1024):
258 | ```python
259 | with zf.open(zf.namelist()[0]) as xml_file:
260 | print(xml_file.read(n=1024))
261 | ```
262 |
263 | Oczywiście parsowanie XML to temat na osobny artykuł,
264 | ale wylistujmy sobie chociaż nazwy miejscowości jakimś prostym narzędziem:
265 | ```python
266 | from xml.dom import minidom
267 |
268 | with zf.open(zf.namelist()[0]) as xml_file:
269 | DOMTree = minidom.parse(xml_file)
270 |
271 | children = DOMTree.childNodes
272 | for row in children[0].getElementsByTagName('row'):
273 | print(row.getElementsByTagName('NAZWA')[0].childNodes[0].toxml())
274 | ```
275 |
276 | Konsola zaczyna wyrzucać kolejne nazwy miejscowości:
277 | ```python
278 | Zagórze
279 | Zacisze
280 | Dobrzyków
281 | Dzwonów Dolny
282 | Dzwonów Górny
283 | ...
284 | ```
285 |
286 | Zwróćmy uwagę, że metoda ```ZipFile.open()``` zwraca objekt klasy ZipExtFile,
287 | która dziedziczy po io.BufferedIOBase.
288 | O ile parser XML nie miał problemu z obsługą tego typu danych,
289 | to w przypadku CSV sprawa się komplikuje:
290 | ```python
291 | import csv
292 |
293 | with zf.open(zf.namelist()[1]) as csv_file:
294 | csv_reader = csv.reader(csv_file, delimiter=";")
295 | for row in csv_reader:
296 | print(row)
297 | ```
298 |
299 | Przy wykonywaniu pętli rzuciło wyjątkiem:
300 | ```
301 | _csv.Error: iterator should return strings, not bytes (did you open the file in text mode?)
302 | ```
303 |
304 | Dzieje się tak, ponieważ obiekty klasy io.BufferedIOBase,
305 | a co za tym idzie również ZipExtFile,
306 | reprezentują strumienie binarne, a nie tekstowe.
307 |
308 | W komunikacie jest podpowiedź, aby plik otworzyć w trybie tekstowym.
309 | Jak czytamy w opisie metody ```ZipFile.open()``` w [dokumentacji Pythona](https://docs.python.org/3.5/library/zipfile.html?highlight=#zipfile.ZipFile.open):
310 | >Use io.TextIOWrapper for reading compressed text files in universal newlines mode.
311 |
312 | Nic, tylko zastosować:
313 | ```python
314 | import csv
315 | import io
316 |
317 | with zf.open(zf.namelist()[1]) as csv_file:
318 | text_file = io.TextIOWrapper(csv_file)
319 | csv_reader = csv.reader(text_file, delimiter=";")
320 | for row in csv_reader:
321 | print(row)
322 | ```
323 |
324 | I już możemy cieszyć się widokiem kolejnych rekordów:
325 | ```python
326 | ['\ufeffWOJ', 'POW', 'GMI', 'RODZ_GMI', 'RM', 'MZ', 'NAZWA', 'SYM', 'SYMPOD', 'STAN_NA']
327 | ['02', '16', '01', '5', '03', '1', 'Zagórze', '0363122', '0363100', '2018-01-02']
328 | ['02', '16', '01', '5', '03', '1', 'Zacisze', '0363168', '0363145', '2018-01-02']
329 | ['02', '16', '01', '5', '03', '1', 'Dobrzyków', '0363263', '0363257', '2018-01-02']
330 | ['02', '09', '02', '2', '03', '1', 'Dzwonów Dolny', '0363330', '0363323', '2018-01-02']
331 | ['02', '09', '02', '2', '03', '1', 'Dzwonów Górny', '0363346', '0363323', '2018-01-02']
332 | ```
333 |
334 | Po skończonej pracy nie zapomnijmy zamknąć archiwum:
335 | ```python
336 | zf.close()
337 | ```
338 | Niby taka błaha sprawa jak odpytanie API,
339 | a ile się można nowych rzeczy nauczyć :slightly_smiling_face:.
340 |
--------------------------------------------------------------------------------