776 lines
35 KiB
TeX
776 lines
35 KiB
TeX
\chapter{Podstawy teoretyczne i/lub spis pojęć}
|
||
\chapter{Ekstrakcja godzin rozpoczęcia mszy świętych}
|
||
\section{Ogólny zarys systemu}
|
||
Architektura systemu ekstrakcji godzin rozpoczęcia mszy świętych została
|
||
przedstawiona na rysunku \ref{pic:architektura}. W tym podrozdziale zostanie ona
|
||
krótko opisana. Szczegółowy opis poszczególnych komponentów znajduje się w
|
||
kolejnych podrozdziałach.
|
||
|
||
System zaczyna działanie od zebrania jak największej ilości danych (nazwa parafii, adres, diecezja
|
||
itd.) o polskich parafiach ze strony deon.pl. Następnie wysyła zapytania do interfejsu API
|
||
Google w
|
||
celu znalezienia adresów internetowych parafii.
|
||
Dla każdej parafii, dla której udało się znaleźć adres URL, pobierane są wszystkie
|
||
podstrony w odległości\footnote{Zakładamy, że sieć jest grafem, zatem odległość
|
||
definiujemy tak jak w teorii grafów.} co najwyżej 3 od strony startowej.
|
||
|
||
Z dużej liczby stron parafialnych, za pomocą reguł wyodrębnione zostają
|
||
te, na których z dużym prawdopodobieństwem znajdują się godziny mszy świętych.
|
||
Następnie godziny zostają wydobyte ekstraktorem
|
||
o bardzo niskiej precyzji i bardzo wysokiej wartości pokrycia.
|
||
Każda godzina wraz z kontekstem, w jakim się znajduje, trafia do anotatora.
|
||
Tam jest oznaczana jako poprawna lub niepoprawna godzina mszy
|
||
świętej\footnote{\label{hour_note}Przez „niepoprawne
|
||
godziny mszy świętych” rozumiemy godziny, które nie są
|
||
godzinami rozpoczęcia mszy świętych.}.
|
||
Regułowy ekstraktor mszy świętych o bardzo wysokiej precyzji znajduje poprawne
|
||
godziny mszy świętych i dołącza je do zanotowanych danych.
|
||
Dodatkowo w celu wyrównania
|
||
klas z nieodfiltrowanego zbioru stron parafialnych wylosowane zostają niepoprawne godziny mszy świętych.
|
||
Zebrane dane zostają użyte do wytrenowania klasyfikatora godzin opartego na
|
||
płytkich sieciach neuronowych.
|
||
|
||
Klasyfikator używany jest do przyporządkowania godzin
|
||
znalezionych przez ekstraktor godzin do następujących klas:
|
||
\begin{itemize}
|
||
\item poprawne godziny mszy świętych,
|
||
\item niepoprawne godziny mszy świętych.
|
||
\end{itemize}
|
||
|
||
\noindent Docelowe godziny rozpoczęcia mszy świętych otrzymujemy z:
|
||
\begin{itemize}
|
||
\item ekstraktora godzin mszy świętych,
|
||
\item klasyfikatora godzin.
|
||
\end{itemize}
|
||
|
||
\begin{figure}[!htb]
|
||
\center
|
||
\includegraphics[width=1\hsize]{struktura_programu.png}
|
||
\caption{Architektura systemu do ekstrakcji godzin mszy świętych.}
|
||
\label{pic:architektura}
|
||
\end{figure}
|
||
|
||
\clearpage
|
||
|
||
\section{Zbieranie informacji o parafiach}
|
||
|
||
\begin{figure}[htb]
|
||
\center
|
||
\includegraphics[width=0.7\hsize]{crawler_adresow_trans.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny za zbieranie informacji o parafiach.}
|
||
\label{pic:pajak_nazw_parafii}
|
||
\end{figure}
|
||
Na rysunku \ref{pic:pajak_nazw_parafii} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
Dane zostały zebrane z serwisu internetowego deon.pl, który zawiera 10130 parafii.
|
||
Jest to większość polskich parafii, ponieważ według
|
||
danych statystycznych GUS\cite{gus} z 2016 roku w Polsce było
|
||
10255 parafii.
|
||
|
||
Dla każdej parafii zebrano:
|
||
\begin{itemize}
|
||
\item nazwę parafii,
|
||
\item miejscowość, w której się znajduje,
|
||
\item dokładny adres,
|
||
\item nazwę dekanatu, do którego należy,
|
||
\item nazwę diecezji, do której przynależy,
|
||
\item województwo, w którym się znajduje.
|
||
\end{itemize}
|
||
|
||
\noindent Fragment zebranych danych został przedstawiony w tabeli \ref{dane_tab}.
|
||
|
||
\begin{table}[H]
|
||
\centering
|
||
\def\arraystretch{1.1}
|
||
\begin{tabular}{ l l l l l l }
|
||
\textbf{Parafia} & \textbf{Miejscowość} & \textbf{Adres} & \textbf{Diecezja} & \textbf{Dekanat} & \textbf{Województwo} \\
|
||
\hline \\ [-2ex]
|
||
Bożego Ciała & Hel & ul. Gdań... & gdańska & Morski & pomorskie \\
|
||
Ducha Św. & Śrem & ul. Prym... & poznańska & Śrem & wielkopolskie\\
|
||
Św. Trójcy & Paszowice & Paszowic... & legnicka & Jawor & dolnośląskie\\
|
||
\\ [-1.5ex]
|
||
\end{tabular}
|
||
\caption{Fragment zebranych danych.}
|
||
\label{dane_tab}
|
||
\end{table}
|
||
|
||
Do wydobycia danych użyto skryptu w języku Python, który korzystał z parsera
|
||
HTML z biblioteki \textit{Beautiful Soup}\cite{beautiful_soup}. Przy wysyłaniu zapytań do serwisu deon.pl zastosowano
|
||
algorytm \textit{Expotential Backoff}\cite{expotential_backoff}, który
|
||
przedstawia się następująco:
|
||
|
||
\begin{algorithm}
|
||
\setstretch{1.2}
|
||
\SetAlgorithmName{Algorytm}{algorithm}{List of Algorithms}
|
||
\caption{\textit{Expotential Backoff}}
|
||
\label{alg:backoff}
|
||
\SetAlgoLined
|
||
\SetKwFunction{Request}{send\_request}
|
||
\SetKwData{MaxWait}{max\_wait\_time}
|
||
\SetKwData{MaxRepeat}{repeat\_limit}
|
||
\SetKwInput{kwInput}{Stałe}
|
||
\SetKwInput{kwAlg}{Algorytm}
|
||
\SetKwInput{kwWhere}{gdzie}
|
||
\kwInput{\\
|
||
\Indp{\makebox[2.8cm][l]{\MaxWait }} $-$ maksymalny czas oczekiwania.\\
|
||
{\makebox[2.8cm][l]{\MaxRepeat }} $-$ limit liczby powtórnych zapytań pod rząd.
|
||
}
|
||
\bigskip
|
||
\kwAlg{
|
||
\begin{enumerate}[rightmargin=5mm]
|
||
\item Wyślij zapytanie do serwisu;
|
||
\item Jeśli zapytanie się nie powiodło, poczekaj 2s i wyślij kolejne zapytanie,
|
||
\item Jeśli zapytanie się nie powiodło, poczekaj 4s i wyślij kolejne zapytanie,
|
||
\item Jeśli zapytanie się nie powiodło, poczekaj 8s i wyślij kolejne zapytanie,
|
||
\item Powtarzaj do czasu aż zapytanie się powiedzie lub liczba ponownych
|
||
zapytań pod rząd wyniesie \MaxRepeat.
|
||
\end{enumerate}
|
||
}
|
||
\kwWhere{
|
||
\begin{itemize}
|
||
\setlength\itemsep{0.3em}
|
||
\item Czas oczekiwania to $2^t$, gdzie $t$ to liczba nieudanych zapytań.
|
||
\item Czas oczekiwania nie może być większy niż \MaxWait.
|
||
\end{itemize}
|
||
}
|
||
\end{algorithm}
|
||
|
||
Algorytm \ref{alg:backoff} uodparnia skrypt na przejściowe problemy z połączeniem i
|
||
zapobiega zbyt częstemu wysyłaniu zapytań do serwisu. Dla przykładu załóżmy,
|
||
że dany serwis jest obciążony i nie daje sobie rady z przetwarzaniem zapytań.
|
||
Wtedy algorytm \textit{Expotential Backoff} przy każdym kolejnym niepowodzeniu
|
||
będzie czekał coraz dłużej, zanim wyśle kolejne zapytanie. W ten sposób nie
|
||
będzie niepotrzebnie obciążał serwisu.
|
||
|
||
\section{Wyszukiwanie adresów URL parafii}
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.7\hsize]{crawler_url_trans.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za wyszukiwanie adresów URL parafii.}
|
||
\label{pic:pajak_url}
|
||
\end{figure}
|
||
|
||
Na rysunku \ref{pic:pajak_url} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
\subsubsection{Pierwsze próby}
|
||
Do wyszukiwania adresów URL parafii próbowano wykorzystać wyszukiwarki takie jak
|
||
Google i DuckDuckGo. Automatycznie wysyłano zapytanie złożone z konkatenacji
|
||
nazwy parafii, jej miejscowości i ulicy, na której się znajduje. Wyszukiwarka Google dawała
|
||
zadowalające wyniki, jednak po kilkunastu zapytaniach blokowała adres IP. W
|
||
dodatku w warunkach użytkowania serwisu i w pliku \textit{robots.txt} Google zabrania
|
||
korzystania z pająków na ich wyszukiwarce.
|
||
Wyszukiwarka DuckDuckGo nie blokowała adresu IP, ale zabraniała indeksowania w pliku \textit{robots.txt} i słabo radziła sobie z polskimi
|
||
zapytaniami. W obu przypadkach powyższa metoda stwarzała kolejny problem do
|
||
rozwiązania -- z wielu wyników wyszukiwania trzeba było wybrać ten, który zawierał
|
||
adres URL parafii.
|
||
\subsubsection{Rozwiązanie}
|
||
Po wielokrotnych próbach poszukiwań znaleziono klucz do rozwiązania problemu
|
||
wyszukiwania adresów URL, jakim jest
|
||
\textit{Google Places API}\cite{google_api}. Serwis \textit{Text Search}\cite{text_search} pozwala na wyszukanie miejsca
|
||
danego obiektu na
|
||
podstawie jego nazwy. Ponadto mając już wyszukany dany obiekt i jego
|
||
identyfikator można odpytać serwis \textit{Place Detail}\cite{place_detail}, aby wyciągnąć więcej
|
||
szczegółów o danym miejscu. Między innymi można otrzymać adres URL danego obiektu.
|
||
|
||
Jedynym minusem jest ograniczenie liczby zapytań do 1000 na 24 godziny. W
|
||
dodatku każde zapytanie do serwisu \textit{Text Search} traktowane jest jak 10
|
||
zapytań. Podając swoją kartę płatniczą, można zwiększyć limit
|
||
zapytań do 150 000 na 24 godziny. Karta płatnicza jest potrzebna Google do
|
||
identyfikacji osoby. Żadna opłata nie jest pobierana za korzystanie z interfejsu
|
||
API.
|
||
|
||
Dla każdej parafii wykonywane jest zapytanie do serwisu \textit{Text Search}.
|
||
Składa się ono z konkatenacji nazwy parafii, jej miejscowości i ulicy, na której
|
||
się znajduje. Jeśli nie zostanie znaleziony żaden obiekt, wysyłane jest powtórne
|
||
zapytanie, lecz tym razem składające się tylko z nazwy parafii i jej
|
||
miejscowości.
|
||
|
||
Zdarza się, że serwis \textit{Text Search} zwraca kilka obiektów. W takim
|
||
przypadku brany jest adres URL pierwszego obiektu z listy wyników.
|
||
Najczęściej jednak oba obiekty należą do tej samej parafii, więc mają taki sam
|
||
adres internetowy. Taki przypadek przedstawia rysunek \ref{pic:text_search}.
|
||
Serwis \textit{Text Search} zwraca dużo danych w formacie JSON, które
|
||
ciężko przedstawić w przejrzystej postaci.
|
||
Dla
|
||
czytelności na rysunku \ref{pic:text_search} pokazano zrzuty ekranu z wyszukiwarki \textit{Google Maps},
|
||
które odpowiadają rezultatowi, jaki otrzymano by, korzystając z serwisu
|
||
\textit{Text Search}.
|
||
|
||
\noindent Powyższą metodą udało się zebrać adresy URL dla ok. 5600 parafii.
|
||
|
||
\begin{figure}[tbh]
|
||
\center
|
||
\includegraphics[width=1\hsize]{amb_text_search.png}
|
||
\caption{Przykład dwóch obiektów zwróconych przez wyszukiwarkę Google, które
|
||
mają ten sam adres internetowy.}
|
||
\label{pic:text_search}
|
||
\end{figure}
|
||
|
||
|
||
\section{Indeksowanie stron parafialnych}
|
||
\enlargethispage{1\baselineskip}
|
||
\begin{figure}[htb!]
|
||
\center
|
||
\includegraphics[width=0.7\hsize]{crawler_parafii_general_trans.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za indeksowanie stron parafialnych.}
|
||
\label{pic:pajak_parafii_general}
|
||
\end{figure}
|
||
|
||
|
||
Na rysunku \ref{pic:pajak_parafii_general} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
Pająk został napisany przy użyciu biblioteki \textit{Scrapy}\cite{scrapy}.
|
||
Punktem startowym jest pojedynczy adres URL parafii podawany na wejście
|
||
programu. Z początkowego adresu URL wydobywana jest domena, w której obrębie
|
||
porusza się pająk. Oznacza to, że jedna instancja pająka zajmuje się pobieraniem
|
||
tylko jednej parafii. W ramach jednej parafii pająk jest w stanie
|
||
asynchronicznie wysłać wiele zapytań do serwera i odbierać wiele odpowiedzi od serwera.
|
||
|
||
\subsection{Komponenty pająka}
|
||
Pająk składa się z następujących komponentów:
|
||
\begin{description}
|
||
\item [Silnik] -- odpowiada za kontrolę przepływu danych i komunikację między komponentami.
|
||
\item [Dyspozytor] -- otrzymuje żądania od silnika, kolejkuje je i na
|
||
prośbę silnika odsyła z powrotem.
|
||
\item [Downloader] -- odpowiada za pobieranie stron parafialnych i
|
||
przekazywanie ich silnikowi.
|
||
\item [Procesor danych] -- zajmuje się końcową obróbką i zapisem danych.
|
||
\item [Spider]\footnote{Użyto angielskiej nazwy, aby rozróżnić
|
||
\textit{spider'a} (komponent pająka), od pająka (cały program
|
||
odpowiedzialny za indeksowanie stron parafialnych).}
|
||
- definiuje sposób, w jaki pobierać dane, między innymi jak parsować stronę i za jakimi
|
||
linkami podążać.
|
||
\item [Spider middleware] -- programy pośredniczące między silnikiem, a
|
||
\textit{spider'em}. Odpowiedzialne są za dodatkowe przetwarzanie danych wyjściowych
|
||
(dane parafialne i żądania) i wejściowych (odpowiedzi) \textit{spider'a}.
|
||
\item [Downloader middleware] -- programy pośredniczące między silnikiem, a
|
||
\textit{downloader'em}. Zajmują się dodatkowym przetwarzaniem danych wejściowych
|
||
(żądań) i wyjściowych (odpowiedzi) \textit{downloader'a}.
|
||
|
||
\end{description}
|
||
\subsection{Przepływ danych}
|
||
|
||
Przepływ danych kontrolowany jest przez
|
||
silnik i wygląda następująco\footnote{Diagram i opis wzorowany jest na
|
||
dokumentacji znajdującej się pod linkiem https://doc.scrapy.org/en/latest/topics/architecture.html.}:
|
||
|
||
\begin{figure}[tbh]
|
||
\center
|
||
\includegraphics[width=0.85\hsize]{scrapy_data_flow.png}
|
||
\caption{Silnik kontrolujący przepływ danych przez komponenty pająka.}
|
||
\label{pic:scrapy_data_flow}
|
||
\end{figure}
|
||
|
||
\begin{enumerate}
|
||
\item Silnik otrzymuje od \textit{spider'a} żądanie pobrania początkowej strony danej
|
||
parafii (najczęściej jest to strona główna parafii).
|
||
\item Silnik oddaje żądania dyspozytorowi, który kolejkuje je do dalszego
|
||
przetwarzania oraz pyta dyspozytora o żądania gotowe do przekazania \textit{downloader'owi}.
|
||
\item Dyspozytor zwraca silnikowi następne żądania.
|
||
\item Silnik wysyła żądania do \textit{downloader'a}. Zanim żądania dotrą do
|
||
\textit{downloader'a}, przetwarzane są przez \textit{downloader middleware}.
|
||
\item \textit{Downloader} pobiera stronę parafialną i umieszcza ją w odpowiedzi, którą
|
||
przesyła silnikowi. Zanim odpowiedź dotrze do silnika, przetwarzana jest przez
|
||
\textit{downloader middleware}.
|
||
\item Silnik otrzymuje odpowiedź od \textit{downloader'a} i przekazuje ją \textit{spider'owi} do
|
||
dalszego przetwarzania. Zanim odpowiedź trafi, do \textit{spider'a} przetwarzana jest
|
||
przez \textit{spider middleware}.
|
||
\item \textit{Spider} przerabia odpowiedź i zwraca dane strony parafialnej silnikowi. Zanim dane
|
||
trafią, do silnika przechodzą przez \textit{spider middleware}. Dodatkowo \textit{spider}
|
||
wysyła żądania z nowymi stronami parafialnymi do pobrania.
|
||
\item Silnik wysyła zebrane dane do procesora danych, który zapisuje je do
|
||
pliku. Następnie przekazuje nowe żądania do zakolejkowania
|
||
dyspozytorowi.
|
||
\end{enumerate}
|
||
% \vspace*{-20mm}
|
||
Cały proces trwa dopóty, dopóki są nowe żądania do przetworzenia.
|
||
|
||
\subsection{Sprawdzanie typu odpowiedzi}
|
||
Podczas indeksowania ważne jest rozpoznawanie typu pobieranych danych. W przypadku
|
||
indeksowania stron parafialnych interesują nas wyłącznie dane tekstowe. Należy
|
||
zatem zadbać o to, aby nie pobierać danych binarnych takich jak np. video, audio
|
||
i obrazy.
|
||
|
||
Biblioteka \textit{Scrapy} obsługuje rozpoznawanie typu zawartości odpowiedzi, bazując na
|
||
następujących kryteriach:
|
||
\begin{itemize}
|
||
\item wartości \mintinline{bash}{Content-type}\cite{RFC2045}, \mintinline{bash}{Content-Encoding}\cite{RFC7231} i \mintinline{bash}{Content-Disposition}\cite{RFC6266} w nagłówku odpowiedzi;
|
||
\item nazwie pliku lub adresie URL (jeśli nie udało się rozpoznać po nagłówku);
|
||
\item obecności znaków kontrolnych w pierwszych 5000 bajtów odpowiedzi
|
||
(jeśli nie udało się
|
||
rozpoznać po nazwie pliku lub adresie URL).
|
||
\end{itemize}
|
||
Powyższy schemat postępowania jest skuteczny, jeśli serwisy internetowe zwracają
|
||
poprawne odpowiedzi. Niestety niektóre strony parafialne potrafią zwracać
|
||
odpowiedzi, które nie są zgodne z rozdziałem 3.1 z dokumentu RFC7231\cite{RFC7231}.
|
||
Dla przykładu strona potrafi zwrócić \mintinline{bash}{Content-Type: text/html}
|
||
w nagłówku, a w ciele binarną
|
||
zawartość np. film. Tego rodzaju anomalie są wykrywane i eliminowane.
|
||
\enlargethispage{2\baselineskip}
|
||
Stosując algorytm \ref{alg:binaryornot}, można określić typ zawartości
|
||
ciała odpowiedzi.
|
||
|
||
\begin{algorithm}[tbh!]
|
||
\setstretch{1.2}
|
||
\SetAlgorithmName{Algorytm}{algorithm}{List of Algorithms}
|
||
\caption{Rozpoznawanie plików binarnych}
|
||
\label{alg:binaryornot}
|
||
\SetKwIF{IfM}{ElseIfM}{ElseM}{if~(\endgraf}{\endgraf\nl)~then}{else
|
||
if}{else}{\nl end if}%
|
||
\SetKwIF{If}{ElseIf}{Else}{if}{then}{else if}{else}{\nl end if}%
|
||
\SetAlgoLined
|
||
\SetKwData{File}{bytes}
|
||
\SetKwData{True}{True}
|
||
\SetKwData{False}{False}
|
||
\SetKwInput{kwInput}{Wejście}
|
||
\SetKwInput{kwOutput}{Wyjście}
|
||
\kwInput{\File $\leftarrow$ 1024 pierwsze bajty pliku}
|
||
\kwOutput{\True jeśli plik binarny, \False jeśli plik tekstowy}
|
||
|
||
\SetKwData{ControlCount}{control\_char\_count}
|
||
\SetKwData{ControlRatio}{control\_char\_ratio}
|
||
\SetKwData{AsciiCount}{high\_ascii\_count}
|
||
\SetKwData{AsciiRatio}{high\_ascii\_ratio}
|
||
\SetKwData{Null}{null}
|
||
\nl\uIf{\File puste {\bf or} \File dekodowalne jako unikod}{
|
||
\nl \Return{\False}\;
|
||
}\ElseIf{znak \Null \bf in \File}{
|
||
\nl \Return{\True}\;
|
||
}
|
||
\nl \tcc{Za znaki kontrolne uznajemy znaki o kodach EASCII od 0 do 7, 11, od 14
|
||
do 32 i od 127 do 159.}
|
||
\nl\ControlCount $\leftarrow$ liczba znaków kontrolnych w \File\;
|
||
\nl\AsciiCount $\leftarrow$ liczba znaków EASCII o kodach od 160 do 255 w \File\;
|
||
\nl\ControlRatio $\leftarrow \frac{\ControlCount}{1024}$\;
|
||
\nl\AsciiRatio $\leftarrow \frac{\AsciiCount}{1024}$\;
|
||
\nl \If{$($\ControlRatio $> 0.3$ {\bf or} \AsciiRatio $> 0.7)$}{
|
||
\nl \Return{\True}\;
|
||
}
|
||
\nl \Return{\False}\;
|
||
\end{algorithm}
|
||
|
||
Algorytm \ref{alg:binaryornot} analizuje zawartość ciała odpowiedzi w celu
|
||
stwierdzenia czy jest ona binarna, czy nie.
|
||
W linii 6 za znaki kontrolne uznano
|
||
wszystkie znaki kontrolne ze zbioru C0\footnote{C0 to znaki kontrolne z kodu
|
||
ASCII o kodach od 0 do 32 i o kodzie 127.} i C1\footnote{C1 to znaki kontrolne
|
||
o kodach od 128 do 159. Zostały zdefiniowane w standardzie ISO/IEC 2022.
|
||
Wiele
|
||
innych systemów kodowań rezerwuje sobie kody od 128 do 159 na znaki kontrolne.} z wyłączeniem
|
||
następujących znaków:
|
||
\begin{itemize}
|
||
\item znak nowej linii (oznaczany przez \mintinline{python}{\n}),
|
||
\item znak powrotu karetki (oznaczany przez \mintinline{python}{\r}),
|
||
\item znak tab (oznaczany przez \mintinline{python}{\t}),
|
||
\item znak backspace (oznaczany przez \mintinline{python}{\b}),
|
||
\item znak nowej linii (oznaczany przez \mintinline{python}{\n}),
|
||
\item znak końca strony (oznaczany przez \mintinline{python}{\f}),
|
||
\end{itemize}
|
||
Powyższe znaki zostały wykluczone, ponieważ często występują w plikach tekstowych.
|
||
|
||
Warto zwrócić uwagę na linię 10. Współczynnik
|
||
\mintinline{python}{control_char_ratio}
|
||
oznacza procent znaków kontrolnych w pierwszych 1024 bajtach pliku.
|
||
Jeśli współczynnik \mintinline{python}{control_char_ratio} jest większy niż
|
||
$0,3$, to plik jest uznawany za binarny. Wartość $0,3$ została wzięta z
|
||
kodu\footnote{Kod znajduje się pod linkiem \url{https://github.com/Perl/perl5/blob/v5.27.11/pp\_sys.c\#L3605-L3665}. Wartość 0,3 występuje w linii 3661.}
|
||
źródłowego języka Perl, który między innymi zajmuje się rozpoznawaniem plików
|
||
binarnych. Natomiast współczynnik \mintinline{python}{high_ascii_ratio} oznacza
|
||
procent znaków EASCII\footnote{EASCII
|
||
oznacza rozszerzone kodowanie ASCII. Przykładowe rozszerzenia to systemy
|
||
kodowania ISO 8859 lub UTF-8.}
|
||
o kodach od 160 do 255.
|
||
Reprezentacja tych znaków zależy od rozszerzenia ASCII. Najczęściej jednak są to
|
||
znaki drukowalne, które rzadko występują w tekście.
|
||
Jeśli współczynnik \mintinline{python}{high_ascii_ratio} jest większy niż $0,7$,
|
||
to plik jest uznawany za binarny.
|
||
Wartość $0,7$ została dobrana na podstawie następujących obserwacji:
|
||
\begin{enumerate}
|
||
\item Zdarzają się pliki binarne, które mają dużo znaków
|
||
\mintinline{python}{high_ascii}. Przykładem jest plik z katalogu
|
||
\mintinline{text}{data.tar.gz/spec/resources/pixelstream.rgb}
|
||
z archiwum \url{https://rubygems.org/downloads/chunky\_png-1.2.8.gem}. Plik
|
||
zawiera bardzo dużo znaków o kodzie 255 w początkowych bajtach.
|
||
\item Mało prawdopodobne jest, aby plik tekstowy miał w pierwszych 1024 bajtach
|
||
więcej niż 70\% znaków \mintinline{python}{high_ascii}. Nawet jeśli pająk
|
||
natrafiłby na taki plik, to z dużym prawdopodobieństwem nie zawierałby on
|
||
informacji parafialnych.
|
||
\end{enumerate}
|
||
|
||
\subsection{Automatyczna regulacja częstości zapytań}
|
||
Biblioteka \textit{Scrapy} zawiera przydatne rozszerzenie, które potrafi automatycznie
|
||
regulować częstość zapytań w zależności od obciążenia pająka i serwera.
|
||
|
||
Algorytm \ref{alg:throttling} przedstawia sposób postępowania, w jaki pająk
|
||
automatycznie reguluje częstość zapytań. Idea algorytmu jest następująca.
|
||
Załóżmy, że serwer potrzebuje
|
||
\mintinline{python}{latency}\footnote{Zmienna \mintinline{python}{latency}
|
||
została
|
||
zdefiniowana w algorytmie \ref{alg:throttling}.} sekund, aby odpowiedzieć pająkowi. Jeśli pająk chce
|
||
mieć przetworzone równolegle
|
||
\mintinline{python}{target_concurrency}\footnote{Stała
|
||
\mintinline{python}{target_concurrency} została zdefiniowana w algorytmie \ref{alg:throttling}.}zapytań to powinien
|
||
wysyłać każde zapytanie co \mintinline{python}{latency/target_concurrency}
|
||
sekund.
|
||
|
||
\begin{algorithm}[tbh!]
|
||
\setstretch{1.2}
|
||
\SetAlgorithmName{Algorytm}{algorithm}{List of Algorithms}
|
||
\caption{Algorytm regulacji częstości zapytań}
|
||
\label{alg:throttling}
|
||
\SetAlgoLined
|
||
\SetKwFunction{Request}{send\_request}
|
||
\SetKwData{Delay}{download\_delay}
|
||
\SetKwData{tDelay}{target\_download\_delay}
|
||
\SetKwData{iDelay}{init\_download\_delay}
|
||
\SetKwData{minDelay}{min\_download\_delay}
|
||
\SetKwData{maxDelay}{max\_download\_delay}
|
||
\SetKwData{latency}{latency}
|
||
\SetKwData{targetConcurrency}{target\_concurrency}
|
||
\SetKwInput{kwConst}{Stałe}
|
||
\SetKwInput{kwVar}{Zmienne}
|
||
\SetKwInput{kwWhere}{gdzie}
|
||
\SetKwInput{kwAlg}{Algorytm}
|
||
\kwConst{\\
|
||
\Indp{\makebox[4.1cm][l]{\iDelay}} $-$ początkowe opóźnienie wysłania zapytania. \\
|
||
{\makebox[4.1cm][l]{\minDelay}} $-$ minimalne opóźnienie wysłania zapytania. \\
|
||
{\makebox[4.1cm][l]{\maxDelay}} $-$ maksymalne opóźnienie wysłania zapytania.\\
|
||
{\makebox[4.1cm][l]{\targetConcurrency}} $-$ średnia wartość równoległych
|
||
zapytań do
|
||
\phantom{{\makebox[4.1cm][l]{\targetConcurrency}} $-$} wysłania.
|
||
}
|
||
\kwVar{\\
|
||
\Indp{\makebox[4.1cm][l]{\tDelay}} $-$ docelowe opóźnienie wysyłania zapytania.\\
|
||
{\makebox[4.1cm][l]{\Delay}} $-$ opóźnienie wysłania zapytania. \\
|
||
{\makebox[4.1cm][l]{\latency}} $-$ czas od ustanowienia połączenia do
|
||
\phantom{{\makebox[4.1cm][l]{\latency}} $-$} otrzymania nagłówków odpowiedzi.
|
||
}
|
||
\bigskip
|
||
\kwAlg{
|
||
\begin{enumerate}[rightmargin=5mm]
|
||
\item Wyślij zapytanie do serwisu;
|
||
\item Ustaw $\Delay \leftarrow \iDelay$.
|
||
\item Gdy odebrano odpowiedź, ustaw
|
||
$\tDelay \leftarrow \frac{\latency}{\targetConcurrency}$.
|
||
\item Ustaw $\Delay \leftarrow \frac{\Delay\ +\ \tDelay}{2}$
|
||
\item Czekaj \Delay sekund.
|
||
\item Wyślij kolejne zapytanie do serwisu;
|
||
\item Wróć do kroku nr 3.
|
||
\end{enumerate}
|
||
}
|
||
\kwWhere{
|
||
\begin{itemize}
|
||
\item Opóźnienia liczone są w sekundach.
|
||
\item \Delay nie może być mniejszy niż \minDelay i większy niż \maxDelay.
|
||
\item Czasy oczekiwania na odpowiedzi z kodem http różnym od 2xx nie są brane
|
||
pod uwagę.
|
||
\item Algorytm kończy się, gdy nie ma więcej zapytań do wysłania.
|
||
\end{itemize}
|
||
}
|
||
\end{algorithm}
|
||
|
||
\clearpage
|
||
\noindent W pająku stron parafialnych stałe z algorytmu \ref{alg:throttling} ustawiono następująco:
|
||
|
||
\begin{itemize}
|
||
\item \mintinline{python}{min_download_delay = 0}
|
||
\item \mintinline{python}{max_download_delay = 300}
|
||
\item \mintinline{python}{init_download_delay = 5}
|
||
\item \mintinline{python}{target_concurrency = 1}
|
||
\end{itemize}
|
||
Stałe \mintinline{python}{min_download_delay} i
|
||
\mintinline{python}{max_download_delay} zostały ustawione w taki sposób, aby nie
|
||
ograniczać zbyt mocno
|
||
pająka co do doboru wartości \mintinline{python}{download_delay}. Celem jest
|
||
przecież automatyczna regulacja wartości \mintinline{python}{download_delay}.
|
||
Niska wartość stałej \mintinline{python}{target_concurrency} umotywowana jest
|
||
dużą liczbą równolegle pracujących pająków (patrz podrozdział \ref{subsec:multiprocess}).
|
||
|
||
\subsection{Indeksowanie wieloprocesorowe}
|
||
\label{subsec:multiprocess}
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.72\hsize]{crawler.png}
|
||
\caption{100 pająków pracujących jednocześnie.}
|
||
\label{pic:pajaki}
|
||
\end{figure}
|
||
Pająk został zaprojektowany w ten sposób, aby bardzo łatwo można było
|
||
urównoleglić pobieranie stron parafialnych.
|
||
Z pomocą programu \textit{GNU parallel}\cite{parallel} indeksowane jest
|
||
jednocześnie 100 parafii (patrz rys. \ref{pic:pajaki}). Gdy jedna ze stu
|
||
parafii zostanie pobrana, zastępuje ją kolejna parafia. Tym sposobem przez
|
||
prawie cały czas równolegle pracuje 100 pająków. Takie podejście pozwoliło
|
||
maksymalnie wykorzystać łącze internetowe, które było wąskim gardłem w procesie
|
||
indeksowania stron parafialnych.
|
||
|
||
\subsection{Organizacja danych}
|
||
|
||
\enlargethispage{1\baselineskip}
|
||
Dane zbierane przez pająka zapisywane są do pliku w formacie JSONL. Format JSONL charakteryzuje się tym, że w każdej linii pliku
|
||
znajduje się poprawny obiekt JSON.
|
||
Dla
|
||
każdej parafii pobrane dane zapisywane są w oddzielnym pliku. W każdej linii
|
||
pliku znajduje się strona parafialna zapisana w formacie JSON.
|
||
Taki sposób organizacji danych przynosi szereg korzyści takich jak:
|
||
\begin{enumerate}
|
||
\item wygodne przetwarzanie równoległe,
|
||
\item łatwa obróbka danych za pomocą narzędzi Uniksowych,
|
||
\item mniejszy rozmiar pliku w porównaniu do zwykłego formatu \textit{JSON}.
|
||
\end{enumerate}
|
||
|
||
\noindent Dla każdej strony parafialnej zapisywane są następujące informacje:
|
||
\begin{enumerate}
|
||
\item adres URL strony,
|
||
\item adres URL poprzedniej strony,
|
||
\item adres URL strony początkowej,
|
||
\item domena parafii,
|
||
\item strona parafii w formacie HTML,
|
||
\item tekst z odnośnika (linku), który doprowadził do bieżącej strony.
|
||
\end{enumerate}
|
||
|
||
\section{Konwersja HTML na tekst.}
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.53\hsize]{html2text_trans.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za konwersje HTML na tekst.}
|
||
\label{pic:html2text}
|
||
\end{figure}
|
||
|
||
Na rysunku \ref{pic:html2text} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
Do konwersji z formatu HTML na format tekstowy wykorzystano bibliotekę \mintinline{bash}{html2text}\cite{html2text}
|
||
pierwotnie rozwijaną przez Aarona Schwartza.
|
||
\mintinline{bash}{html2text} konwertuje HTML na czysty, czytelny tekst w
|
||
formacie \textit{Markdown}\cite{markdown}. Biblioteka oferuje wiele opcji do kontroli
|
||
konwersji i jest bardzo łatwa w modyfikacji.
|
||
|
||
\smallskip
|
||
\noindent Zastosowano następujące opcje i modyfikacje przy konwersji:
|
||
\vspace{-2mm}
|
||
\begin{itemize}
|
||
% \setlength{\itemsep}{1pt}
|
||
\item ignorowanie linków, tabel i obrazków,
|
||
\item usuwanie znaków odpowiedzialne za pogrubienie i kursywę tekstu,
|
||
\item usuwanie znaków odpowiedzialne za tworzenie list.
|
||
\end{itemize}
|
||
|
||
\section{Ekstrakcja godzin}
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.65\hsize]{general_ekstraktor.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za ekstrakcję godzin ze stron parafialnych.}
|
||
\label{pic:general_ekstraktor}
|
||
\end{figure}
|
||
Na rysunku \ref{pic:html2text} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
Ekstraktor godzin służy do znajdowania bardzo ogólnych ciągów znaków mogących
|
||
być godzinami rozpoczęcia mszy świętych. Został napisany z myślą, aby miał
|
||
bardzo wysoką wartość pokrycia, ale już niekoniecznie wysoką precyzję.
|
||
Celem jest,
|
||
aby w zbiorze wyekstrahowanych godzin znalazło się jak najwięcej godzin
|
||
rozpoczęcia mszy, bez względu na to, jak duży jest ten zbiór.
|
||
|
||
Do osiągnięcia tego celu zastosowano następujące reguły.
|
||
Ciąg znaków oznaczony jako \mintinline{bash}{hour} zostanie wyekstrahowany, jeśli
|
||
zajdzie każdy z poniższych warunków:
|
||
\begin{enumerate}
|
||
\item \mintinline{bash}{hour} pasuje do wyrażenia regularnego \\ \mintinline{text}{(0?[6-9]|1\d|2[0-2])[:.](oo|[0-5]\d)|6|7|8|9|1\d|2[0-2]};
|
||
\item Znak przed \mintinline{bash}{hour} zawiera się w
|
||
\mintinline{python}{{',', '('}};
|
||
\item Znak po \mintinline{bash}{hour} zawiera się w
|
||
\mintinline{python}{{',', ')', ';'}};
|
||
\item Jeśli znak przed \mintinline{bash}{hour} równa się
|
||
\mintinline{python}{'('}, to znak po \mintinline{bash}{hour} jest różny od \mintinline{bash}{')'}.
|
||
\end{enumerate}
|
||
Ekstraktor wraz
|
||
ze znalezioną godziną zapisuje kontekst, w jakim ta godzina się znalazła.
|
||
|
||
\section{Filtrowanie stron}
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.5\hsize]{filtrowanie.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za filtrowanie stron parafialnych.}
|
||
\label{pic:filtrowanie}
|
||
\end{figure}
|
||
Na rysunku \ref{pic:filtrowanie} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
Filtr stron parafialnych ma za zadanie odnaleźć strony parafialne, na których z
|
||
dużym prawdopodobieństwem znajdują się godziny mszy świętych. Taki zabieg jest
|
||
potrzebny, aby ograniczyć liczbę godzin, które trafią do anotatora. Gdyby nie zastosowano
|
||
filtru stron parafialnych, ekstraktor godzin wśród wszystkich stron parafialnych
|
||
znalazłby ponad 3 miliony godzin. Po zaaplikowaniu filtru stron i przetworzeniu
|
||
ich przez ekstraktor godzin otrzymano 10920 godzin. Później godziny wraz z
|
||
kontekstami poddawane są anotacji. Etap ten będzie dokładniej opisany w
|
||
podrozdziale \ref{sec:anotator}.
|
||
|
||
Reguły zastosowane do zadecydowania czy dana strona zawiera godziny mszy
|
||
świętych, zostały przedstawione w
|
||
algorytmie \ref{alg:has_mass}.
|
||
\begin{algorithm}
|
||
\setstretch{1.2}
|
||
\SetAlgorithmName{Algorytm}{algorithm}{List of Algorithms}
|
||
\caption{Rozpoznawanie stron z godzinami mszy świętych.}
|
||
\label{alg:has_mass}
|
||
\SetKwIF{IfM}{ElseIfM}{ElseM}{if~(\endgraf}{\endgraf\nl)~then}{else
|
||
if}{\nl else}{\nl end if}%
|
||
\SetKwIF{If}{ElseIf}{Else}{if}{then}{else if}{else}{\nl end if}%
|
||
\SetAlgoLined
|
||
\SetKwData{link}{url}
|
||
\SetKwData{bText}{btn\_text}
|
||
\SetKwData{False}{False}
|
||
\SetKwData{True}{True}
|
||
\SetKwInput{kwInput}{Wejście}
|
||
\SetKwInput{kwOutput}{Wyjście}
|
||
\kwInput{\\
|
||
\vspace{-0.5mm}
|
||
\Indp\link $\leftarrow$ adres internetowy analizowanej strony,\\
|
||
\bText $\leftarrow$ tekst na przycisku, który doprowadził do
|
||
analizowanej \phantom{\bText $\leftarrow$} strony.
|
||
}
|
||
|
||
\kwOutput{
|
||
\vspace{-2mm}
|
||
\begin{itemize}[rightmargin=5mm]
|
||
\setlength\itemsep{-1mm}
|
||
\item \True jeśli jest wysokie prawdopodbieństwo, że strona zawiera godziny mszy
|
||
\item \False jeśli jest niskie prawdopodbieństwo, że strona zawiera godziny mszy
|
||
\end{itemize}
|
||
}
|
||
|
||
\SetKwData{suf}{url\_suf}
|
||
\SetKwData{slash}{'/'}
|
||
\SetKwData{goodreg}{ok\_regex}
|
||
\SetKwData{badreg}{bad\_regex}
|
||
|
||
\nl\tcc{Wyrażenia regularne \goodreg i \badreg ignorują wielkość liter.}
|
||
\nl $\goodreg \leftarrow$
|
||
\mintinline[breaklines]{python}{'msze|nabo[żz]e[ńn]stw(a|(?=\W\d)|$)|
|
||
porz[ąa]dek($|\.htm)|porz[aą]dek.(liturgi|mszy)| (rozk[lł]ad|plan|godziny|uk[lł]ad|harmonogram|grafik|rozpiska).mszy'}
|
||
|
||
\medskip \vspace{-1mm}
|
||
\nl $\badreg \leftarrow$
|
||
\mintinline[breaklines]{python}{'nabo[zż]e[nń]stwa.(majowe|wielk|czerwcowe |maryjne|pasyjne|pokutne|fatimskie|do|ro[żz]a|czterdzie|w.wielk)'}
|
||
|
||
\nl$\suf \leftarrow$ ciąg znaków w \link po ostatnim wystąpieniu \slash\;
|
||
|
||
\nl\uIfM{\begin{tabular}{@{\hspace*{-3.8pt}}l@{}}
|
||
\nl \hspace*{1.2em}
|
||
$($ \suf pasuje do $\goodreg$ {\bf and} \suf nie pasuje do \badreg $)$ {\bf or}\\
|
||
\nl \hspace*{1.2em}
|
||
$($ \bText pasuje do $\goodreg$ {\bf and} \bText nie pasuje do \badreg $)$
|
||
\end{tabular}}{
|
||
\nl \Return{\True};\
|
||
}\ElseM{
|
||
\Return{\False};\
|
||
}
|
||
\end{algorithm}
|
||
|
||
W algorytmie \ref{alg:has_mass} warto zwrócić uwagę na wyrażenia regularne
|
||
\mintinline{text}{ok_regex} i \mintinline{text}{bad_regex}. Wyrażenie
|
||
regularne \mintinline{text}{ok_regex} ma za zadanie dopasować się do słów,
|
||
które są powiązane z porządkiem mszy świętych. Stąd też pojawiają się tam
|
||
wyrażenia takie jak „rozkład mszy” lub „porządek liturgii”.
|
||
|
||
Wyrażenie regularne \mintinline{text}{bad_regex} ma za zadanie dopasować się do
|
||
słów, które są powiązane z innymi nabożeństwami niż msze święte. Stąd pojawiają
|
||
się tam wyrażenia takie jak „nabożeństwa czerwcowe” czy „nabożeństwa maryjne”.
|
||
|
||
\section{Anotator danych}
|
||
|
||
|
||
\begin{figure}[tbh!]
|
||
\center
|
||
\includegraphics[width=0.6\hsize]{annotator.png}
|
||
\caption{Fragment architektury systemu przedstawiający komponent odpowiedzialny
|
||
za anotację danych.}
|
||
\label{pic:anotator}
|
||
\end{figure}
|
||
|
||
Na rysunku \ref{pic:anotator} został przedstawiony fragment architektury
|
||
systemu z rysunku \ref{pic:architektura}, który zostanie omówiony w tym podrozdziale.
|
||
|
||
\subsection{Ogólny zarys}
|
||
Anotator danych został stworzony w celu zebrania jak największej ilości
|
||
danych dla klasyfikatora przewidującego czy zaznaczony fragment jest godziną
|
||
rozpoczęcia mszy świętej, czy nie.
|
||
Aby osiągnąć zamierzony cel anotator został zaprojektowany w ten sposób aby:
|
||
\begin{itemize}
|
||
\item był szybki,
|
||
\item był dostępny na urządzeniach mobilnych i stacjonarnych,
|
||
\item był prosty i wygodny w użyciu,
|
||
\item umożliwiał wykrywanie oszustów (osób intencjonalnie źle anotujących).
|
||
\end{itemize}
|
||
|
||
\noindent Anotator jest dostępny jako aplikacja internetowa pod adresem \url{msze.nsupdate.info}. Aplikacja jest
|
||
responsywna, więc można z niej wygodnie korzystać na każdym urządzeniu
|
||
wyposażonym w co najmniej 4 calowy wyświetlacz. Interfejs jest przejrzysty i
|
||
został pokazany na rysunku X. Jedyne akcje jakie może wykonać użytkownik to:
|
||
\begin{itemize}
|
||
\item kliknąć „Tak” jeśli zaznaczono godzinę rozpoczęcia mszy,
|
||
\item kliknąć „Nie” jeśli zaznaczono inną godzinę,
|
||
\item cofnąć się do poprzedniej anotacji,
|
||
\item wyświetlić instrukcję obsługi.
|
||
\end{itemize}
|
||
Po naciśnięciu przycisku „Tak” lub „Nie” ekran jest automatycznie przewijany na
|
||
sam dół. Taka operacja zapewnia łatwy dostęp do przycisków odpowiedzialnych za anotację. Dzięki
|
||
temu znajdują się one również zawsze w tym samym miejscu co ułatwia szybką
|
||
anotację.
|
||
Po naciśnięciu przycisku „Cofnij” ekran nie jest już przewijany na sam dół. W
|
||
ten sposób zapewniono wygodny dostęp do przycisku „Cofnij”. Jest to szczególnie
|
||
istotne w przypadku gdy
|
||
użytkownik zamierza cofać się wiele razy.
|
||
|
||
Aby zapewnić odpowiednią jakość anotacji przy pierwszym urchomieniu wyświetlana
|
||
jest instrukcja obsługi. Opisuje ona sposób w jaki należy anotować godziny oraz
|
||
przedstawia przykłady poprawnie zanotowanych godzin. Intrukcję można zamknąć
|
||
dopiero po przewinięciu jej na sam dół.
|
||
|
||
Aplikacja nie wymaga logowania. Taka
|
||
decyzja została podjęta ze względu na fakt, że anotatorami są wolontariusze.
|
||
Wymóg rejestracji i logowania spowodowałby zmniejszenie liczby osób chętnych do
|
||
anotacji. Takie podejście wiąże się jednak z problemem identyfikacji
|
||
użytkowników. Identyfikacja jest niezbędna do prawidłowego funkcjonawania
|
||
antotora. Chcielibyśmy wiedzieć, które godziny zostały zanotowane przez danego
|
||
użytkownika, aby między innymi nie dać mu tych samych godzin do anotacji.
|
||
|
||
\subsection{Identyfikacja urządzeń}
|
||
Skuteczną identyfikację można przeprowadzić używając
|
||
|
||
|
||
\subsection{Architektura anotatora}
|
||
Architektura
|
||
|
||
\subsection{Podsumowanie}
|
||
\section{Regułowa ekstrakcja godzin mszy}
|
||
Do napisania
|
||
\section{Klasyfikacja godzin}
|
||
Do napisania
|
||
\subsection{Model teoretyczny}
|
||
\subsection{FastText}
|
||
|
||
|
||
\chapter{Rezultaty}
|
||
Do napisania
|
||
% \section{Organizacja danych} % może zbyt inżynierskieby
|
||
|
||
\chapter{Podsumowanie}
|
||
Do napisania
|
||
|
||
% \subsection{Ewaluacja wewnętrzna} %F1 score
|
||
% \subsection{Ewaluacja zewnętrzna} % w systemie webowym, użytkownicy
|
||
% \chapter{Wnioski}
|
||
%%% Local Variables:
|
||
%%% LaTeX-command: "latex -shell-escape"
|
||
%%% End:
|