Wikipedia offline na lokalnym komputerze

Opis pomysłu
Pomysł polega na pobraniu wszystkich haseł z polskiej Wikipedii i zapisaniu ich lokalnie na komputerze (w postaci zwykłych, statycznych plików HTML) wraz z taką ich modyfikacją, by przy wyświetlaniu poszczególnych haseł można było się po nich poruszać w obrębie lokalnie przygotowanego serwisu, a także z usunięciem zbędnych elementów strony (takich jak linki edycyjne czy menu) i dodaniem własnych elementów (np. wyszukiwarki haseł działającej na naszej lokalnej kopii).

Wstępny opis rozwiązania
Na potrzeby przeglądania haseł Wikipedii przygotowałem stosowny skrypt w PHP, który przy pomocy biblioteki cURL pobiera z internetu stronę z danym hasłem, przetwarza go i zapisuje na dysku w postaci pliku HTML. Taka pobrana strona jest również przeglądana pod kątem występowania odnośników (linków) do innych haseł Wikipedii (które są zapisywane w bazie danych MySQL w celu ich późniejszego przetworzenia) oraz linków do innych zasobów (obrazków, stylów CSS czy skryptów JS). Wszystkie takie linki są modyfikowane tak, by działały lokalnie z naszym serwisem, treść zawierająca opis danego hasła jest wyłuskiwana i zapisywana pod postacią specjalnego szablonu HTML.

Zarządzanie pobieraniem haseł
Ponieważ będziemy pobierać zawartość stron serwisu pl.wikipedia.org, to zetkniemy się z problemem Same origin policy, wykluczającym bezpośredni dostęp do zawartości zewnętrznych stron z poziomu JavaScript (choć w starszych przeglądarkach, np. w Mozilla Firefox do wersji 4.0.1, a także w Google Chrome nie było tego ograniczenia lub dało się je obejść odpowiednio konfigurując przeglądarkę - więcej o tym możesz się dowiedzieć tutaj). Dlatego też najwygodniej odbieranie i obróbkę zawartości "obcych" stron www zrobić po stronie serwera (np. w PHP), co i ja uczyniłem. Jednak w skryptach PHP nie da się na bieżąco wyświetlać informacji o przetwarzanych danych, co jest w naszym przypadku dość istotną kwestią (bo przetwarzamy dużo danych i jest to dość czasochłonne - według moich szacunków pobranie wszystkich haseł z Wikipedii może trwać nawet 30-50 dni, a same hasła mogą zajmować na dysku ponad 30 GB, nie licząc plików z grafiką), dlatego panel zarządzający i nadzorujący postęp przetwarzania haseł został przygotowany w osobnym pliku HTML, w którym osadziłem skrypty JS, służące do kontroli pobierania haseł i wyświetlania bieżących informacji oraz statystyk.

Panel sterujący
Dla wygody oraz by mieć bieżącą kontrolę nad postępem pobierania haseł przygotowałem osobny panel zarządzający, który działa w przeglądarce użytkownika i za pomocą skryptów JS komunikuje się ze skryptem pobierającym kolejne hasła, działającym na serwerze, przy pomocy technologii AJAX. Aplikacja ta wysyła do skryptu PHP na serwerze (na lokalnym komputerze - ja od lat używam oprogramowania XAMPP) zapytanie o kolejne hasło, skrypt z bazy danych MySQL odczytuje nazwę kolejnego hasła, pobiera z Wikipedii stronę HTML zawierającą to hasło, przetwarza ją, wynik zapisuje do pliku a występujące w nim nowe hasła wrzuca do bazy danych. Do skryptu sterującego odsyła tylko nazwę hasła oraz przydatne dane statystyczne, takie jak rozmiar pobranej strony oraz zapisanego pliku, liczbę już pobranych haseł oraz ich całkowitą ilość w bazie. Dzięki temu skrypt sterujący może na bieżąco wyświetlać te dane oraz dodatkowo obliczać kilka innych, przydatnych do śledzenia, danych statystycznych, takich jak prędkość pobierania i zapisywania danych. Działanie skryptu pobierającego hasła można w każdej chwili przerwać a potem wznowić.

Zapisywanie haseł
Każde hasło jest zapisywane przez skrypt PHP do osobnego pliku na dysku. Ponieważ w nazwie haseł mogą występować nieakceptowane przez system operacyjny znaki, są one konwertowane przy pomocy specjalnej funkcji do bezpiecznej postaci (podobnej w działaniu do funkcji urlencode). Ponieważ aktualnie w Wikipedii jest już ponad milion haseł, to trzymanie tak wielkiej liczby plików w jednym katalogu "./wiki/" powodowałoby ogromne opóźnienia związane z narzutem czasowym na przeglądanie zawartości takiego katalogu, dlatego zdecydowałem się na zrobienie dwupoziomowego drzewa katalogów zawierających zapisane hasła z podziałem względem dwóch pierwszych liter danego hasła (przykładowe hasło "Wikipedia" znalazłoby się w "./wiki/w/i/Wikipedia.html").

Inicjalizacyjnie wrzuciłem kilka linków "startowych", zawierających duży wykaz haseł z Wikipedii:
  Wikipedia:Indeks haseł
  Wikipedia:Indeks kategorii
  Portal:Kategorie Główne
  Kategoria:Biblioteka Wikipedii

Stan obecny
Na razie dopracowuję skrypt przetwarzający kolejne hasła, testuję w praktyce jego działanie i analizuję wynikowe dane. Ponieważ na Wikipedii co jakiś czas pojawiają się pewne zmiany w wyglądzie serwisu, muszę się z nimi zapoznawać i sprawdzać, jak sobie z tym radzi mój program. Dodatkowo z braku wolnego czasu na dopracowanie szczegółów właściwe pobieranie wszystkich haseł przesunąłem na połowę 2016 roku. Na razie, po przeprowadzonych w lipcu i sierpnia 2013 roku testach (trwających łącznie ok. 2-3 tygodnie ciągłej pracy skryptu), pobrałem i zapisałem ponad 800 tys. haseł (pliki na dysku zajęły 20 GB, ale się dobrze kompresują - średnio 15-krotnie) oraz zindeksowałem 3 mln linków do artykułów (nie wszystkie z nich są właściwymi hasłami - aktualnie w Wikipedii jest ich ponad 1 150 000) - 1 450 000 pozycji to hasła specjalne, a także zebrałem prawie 900 tys. linków do pozostałych zasobów Wikipedii (arkuszy stylów, skryptów JS oraz plików graficznych).
Poniżej dwa zrzuty ekranu z działania panelu sterującego pobieraniem haseł:
Panel zarządzania pobieraniem haseł
Jak widać w tym przypadku skrypt działał ciągle przez 4 dni (96 godzin), pobrał z internetu 8.5 GB danych, a zapisał na dysk ponad 4 GB danych.

Panel zarządzania pobieraniem haseł
Zostawiłem skrypt na dłużej - wygląd ekranu po ponad 11 dniach (265 godzinach) działania - pobrał on w tym czasie z internetu 23 GB danych, a zapisał na dysk 13 GB danych.

Przygotowałem też dwa nieduże 3-minutowe filmiki (rozmiaru ok. 5 MB każdy) demonstrujące działanie mojej aplikacji w praktyce: wideo 1, wideo 2 oraz jeden ponad 7-minutowy (rozmiaru 11 MB) wideo 3.
Aktualna wersja skryptu radzi sobie ze wszystkimi możliwymi problemami i nie wymaga nadzoru podczas swojego działania. Jak widać chwilowa prędkość pobierania danych z internetu może być nawet dość duża, jednak w wyniku sporego czasu potrzebnego na przetworzenie zawartości strony przez skrypt w PHP otrzymujemy dość niską średnią (na poziomie 25 KB/s) - z przeprowadzonej przeze mnie analizy czasów przetwarzania okazało się, że większość czasu działania skryptu (70-80%) zajmują zapytania SELECT do bazy MySQL. Dalsza analiza wskazała, że 50-60% tego czasu zajmowało pobieranie statystyki (liczby pobranych oraz wszystkich artykułów). Dlatego by maksymalnie przyspieszyć działanie bazy danych oraz prędkość zapisywania haseł, planowałem zrobić kolejne testy, jaka będzie wynikowa szybkość działania mojego programu, gdy będę korzystał z szybkiego dysku SSD. Jednak później wpadłem na jeszcze lepszy pomysł wykorzystania do tego bardzo szybkich i darmowych programowych ramdysków firm SoftPerfect oraz Dataram, szczególnie program SoftPerfect RAM Disk przypadł mi do gustu, bo jest bardzo szybki i nie ma żadnych ograniczeń co do ilości pamięci RAM przeznaczonej na ramdysk. Przeprowadzone na moim komputerze (procesor Intel Core2 Quad Q9550 @ 3600 GHz, pamięć RAM 2x4 GB DDR3 9-8-8-22 @ 1333 MHz) testy pokazały, że odczyt z ramdysku jest na poziomie ponad 6 GB/s, a zapis 4GB/s (to ponad 10 razy więcej, niż w przypadku dysków SSD, zaś dostęp do danych jest praktycznie natychmiastowy). Jak to wykorzystam? Otóż oprogramowanie XAMPP, razem z bazą MySQL, wraz z używanymi skryptami będą zainstalowane i będą działały na ramdysku, z którego co jakiś czas (np. raz dziennie, ale to zależeć będzie od pojemności ramdysku i szybkości pobierania i przetwarzania danych przez mój skrypt) będę przegrywał zapisane dane na normalny dysk twardy (prawdopodobnie SSD) - szacuję, że takie rozwiązanie da spore przyspieszenie przetwarzania danych).
Prócz przedstawionego powyżej panelu sterującego pobieraniem haseł, muszę jeszcze przygotować podobny panel nadzorujący pobieranie plików graficznych (i pozostałych, występujących w Wikipedii) i prawdopodobnie będę z niego korzystał na innym komputerze z innym łączem do internetu (żeby nie obciążać ani łącza, ani bazy MySQL).
Dodatkowo przygotuję i uruchomię skrypt monitorujący bieżące zmiany haseł (oraz nowo utworzone hasła), żeby mieć możliwość szybkiego zaktualizowania posiadanych lokalnie danych (w trakcie pracy skryptu do pobierania haseł będzie tak, że jakieś hasła zostaną zmodyfikowane po ich zapisaniu przez skrypt - warto to wiedzieć i potem je ponownie pobrać i uaktualnić dane w bazie). Informację o zmianach dotyczących haseł uzyskam dzięki API Wikipedii, która udostępnia między innymi możliwość pobrania wykazu ostatnio zmienionych lub nowo dodanych haseł. Na bazie tych danych przygotowałem skrypt wiki_last_articles.php, który wypisuje w tabelce listę ostatnich 100 zmian w Wikipedii. Dla zainteresowanych udostępniam kod źródłowy tego skryptu.

Aplikacja końcowa
Równolegle z ulepszaniem skryptu do pobierania haseł przygotowuję aplikację końcową, która będzie działała lokalnie u użytkownika w (dowolnej) przeglądarce internetowej i wykorzystywała dane wcześniej pobrane przeze mnie z internetu, obrobione i specjalnie przygotowane do tego celu. Chcę, by używanie tej aplikacji było jak najbardziej proste i nie wymagało żadnych dodatkowych programów (prócz samej przeglądarki internetowej), dlatego aplikacja jest pisana w JavaScripcie, osadzonym w pliku HTML. Pierwotnie program miał być napisany z wykorzystaniem ramek, w głównej miało być wyświetlane bieżące hasło, a nawigacja pomiędzy hasłami byłaby zrealizowana poprzez modyfikację linków. Jednak z uwagi na dużą zajętość miejsca na dysku przez hasła postanowiłem zmienić koncepcję i skorzystać z pakowanich plików, które są zwykłymi plikami tekstowymi i się bardzo dobrze kompresują. Ponieważ jednak chcę, by nie trzeba było tych wszystkich plików wypakowywać przed ich użyciem (wyświetleniem), bo to znów zajmowałoby sporo miejsca na dysku twardym użytkownika, postanowiłem że zostaną one skompresowane każdy z osobna i będę rozpakowywane w locie w samej aplikacji poprzez kod w JavaScripcie. Oczywiście tak spakowane pliki zajmą więcej miejsca, niż spakowane wszystkie naraz w jednym archiwum (średnio nawet 3 krotnie więcej), ale za to będę bezpośrednio dostępne i być może zmieszczą się nawet na jednej płytce DVD.
Kwestię pakowania plików mogę rozwiązać w PHP nawet poprzez wywołanie zewnętrznego pakera (funkcją exec), ale rozpakowanie musi być napisane w JavaScripcie. Poszukałem w internecie takich rozwiązań i znalazłem rozwiązanie, korzystające z algorytmu deflate. Przykładowy plik testowy, mający 1678 KB, został spakowany za pomocą tego programu do 61 KB (czyli jest 27,5 raza mniejszy). Całkiem nieźle, ale testy programem 7-Zip wykazały, że używając algorytmu LZMA można taki plik skompresować nawet do 33 KB (czyli jest on 50-krotnie mniejszy). Ponieważ zależało mi na jak największej kompresji, szukałem algorytmu lub programu z darmowymi źródłami, używającego tej silnej kompresji. I tak trafiłem na projekt Lzip, skąd mogłem pobrać zarówno wersję binarną dla Windows, jak i kod źródłowy (napisanego w C) programu lunzip do rozpakowywania. Program lzip pakuje pojedyncze pliki tak samo mocno, jak program 7-Zip (bo w obu przypadkach używany jest algorytm LZMA). Obecnie analizuję kod źródłowy programu lunzip, upraszczam go i przerabiam na swoje potrzeby, a później przepiszę go w JavaScripcie na potrzeby wykorzystania w swoim projekcie. Więcej na ten temat możesz się dowiedzieć we wpisie Kompresja i dekompresja danych.
Na potrzeby wyszukiwania haseł będę potrzebował wygenerować indeksy do wyszukiwania haseł (dla każdej 3-znakowej kombinacji), bo nie będę tego mógł robić dynamicznie. Zdaję sobie sprawę, że tych plików będzie sporo - dokładnie będzie ich tyle, ile jest kombinacji 3 liter z (szacunkowo) 60 znaków, czyli (w tym przypadku) ok. 34 tys. plików. Każdy taki plik będzie zawierał listę haseł (zapewne również spakowaną), które zawierają właśnie taką kombinację tych 3 liter. Najpierw jednak trzeba zrobić listę znaków występujących w hasłach (bo prócz polskich znaków pojawiają się tam też różne międzynarodowe), oczywiście wielkość liter nie ma tu znaczenia. Listę taką można robić w locie przy pobieraniu kolejnych haseł (i trzymać ją w zmiennej sesyjnej) albo zapuścić po zakończeniu pobierania wszystkich haseł skrypt, który je przeanalizuje i przetworzy za jednym zamachem.
Chcę też zrobić nawigowanie po historii przeglądanych haseł, tak by łatwo można było się cofnąć do hasła już wcześniej przeglądanego.

Powrót do strony z wykazem projektów

Valid HTML 4.01 TransitionalValid CSS