Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Gorzkie żale na temat informacji pasażerskiej w Kolejach Śląskich, kilka słów o tym, jak scrapować dane i przesyłać je innym, czy Telegram jest naprawdę taki zły oraz ile opóźnień występuje na najbardziej punktualnej linii. Zapraszam!

Codzienne problemy dojazdowe

Wyobraźmy sobie następujący obrazek. Jest 5:45, początek grudnia, za oknem deszcz, około 3°C, wiatr mocny z porywami dochodzącymi do 70 km/h. Musimy się spieszyć, ponieważ jak na mieszkańców GZM-u przystało, dojeżdżamy do pracy Kolejami Śląskimi. Wyruszamy w przyjemny spacer i po piętnastu minutach docieramy na dworzec. Razem z pozostałymi nieszczęśnikami pasażerami czekamy na pociąg - nowoczesny Elf2 rodzimej firmy PESA. Mijają kolejne minuty, naszego środka lokomocji niestety nie widać. Co prawda słyszymy jakieś komunikaty z dworcowych głośników, ale zwyczajowo są one kompletnie niezrozumiałe. Po kilkunastu minutach czekania dociera do nas smutna prawda - dziś pociągi nie jeżdżą. Co najwyżej możemy jeszcze chwilkę poczekać na Zastępczą Komunikację Autobusową, w skrócie ZKA. Do pracy na pewno nie dotrzemy na czas, a gdybyśmy wiedzieli o utrudnieniach wcześniej, moglibyśmy wsiąść w auto, autobus lub cokolwiek innego. Ktoś może powiedzieć: “hej kolego, pewnie taka sytuacja zdarza się raz na rok albo może i rzadziej, więc nie ma powodu tak dramatyzować!”. Tutaj muszę rozczarować optymistów. Na linii którą jeżdżę, w okresie od drugiego grudnia do czwartego marca, pojawiły się 74 komunikaty o utrudnieniach w jeździe pociągów. Warto dodać, że linia ta uchodzi za najbardziej punktualną w ofercie Kolei Śląskich. Niestety duża część pasażerów nie musi sobie wyobrażać powyższego obrazka, ponieważ takie sytuacje niejednokrotnie przeżyli. Czy o utrudnieniach można byłoby się dowiedzieć wcześniej? To zależy. Jeśli mieliśmy pecha i to akurat nasz pociąg wypadł, nie jesteśmy w stanie zbyt wiele zrobić. Natomiast, jeśli pociągi na naszej linii nie jeżdżą od kilku godzin, mielibyśmy szansę inaczej zaplanować sobie naszą podróż. Jest jeden warunek - musimy o tym wiedzieć.

Jak Koleje Śląskie informują o utrudnieniach?

Strona Kolei Śląskich udostępnia pasażerom zakładkę “Informacje o utrudnieniach”, w której chronologicznie wypisywane są utrudnienia (opóźnienia lub odwołane połączenia) występujące na liniach obsługiwanych przez przewoźnika. I to tyle. Nie ma żadnego api, żadnych dodatkowych aplikacji, powiadomień. Jest tylko statyczny HTML, w którym co jakiś czas aktualizowane są informacje. Trzeba przyznać, że to rozwiązanie budzi we mnie nutkę nostalgii, którą potęguje fakt, że w stopce strony uhonorowano autora szablonu wraz z linkiem do jego portfolio(?). Brakuje tylko odnośnika do forum dla pasażerów opartego o phpBB by Przemo.

Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Dodatkowo zauważyłem jeszcze parę ciekawostek:

  1. Na stronie wprowadzono podział na kategorie, ale wszystkie komunikaty są w kategorii Informacje.
  2. Choć przewoźnik wykorzystuje oznaczenia linii (S1, S8 itd.) w komunikatach nie są one wykorzystywane.
  3. Komunikaty są pisane w języku polskim, a tłumaczenie na angielski dotyczy wyłącznie fragmentów “odwołany” lub “opóźniony na trasie”.
  4. Jeśli na jakiejś linii trwa remont lub jest zmiana organizacji, codziennie o północy dodawany jest ten sam komunikat, aż do zakończenia utrudnień.
  5. Strona z informacjami o utrudnieniach ma 4178 podstron, dzięki czemu możemy poczytać o utrudnieniach z marca 2016.
  6. Wydaje mi się, że każdy komunikat pisany jest ręcznie, ponieważ zdarzają się komunikaty z literówkami lub brakującymi elementami, np. brak “nr” przed numerem linii.
  7. Jeśli opóźnienie pociągu ulega zmianie to dany komunikat jest edytowany, ale nie jest ustawiany jako najnowszy (na górze strony).

Powyższe informacje mówią nam jedno. Jeśli chcemy być na bieżąco z utrudnieniami, musimy odświeżać stronę przed przejazdami i sprawdzać czy nie pojawiły się komunikaty dotyczące naszego połączenia. Inne rozwiązanie nie istnieje albo nie potrafiłem go znależć. W związku z tym, że parę razy doświadczyłem sytuacji opisanej w rozdziale pierwszym, postanowiłem coś podziałać.

Czas scrapowania

Web scraping to technika pozyskiwania danych z witryn internetowych. Dzięki niej można automatycznie gromadzić informacje dostępne w sieci, sortować je i wykorzystywać. Dzięki temu programiści i analitycy mają możliwość śledzenia trendów, monitorowania cen czy zbierania danych do analizy konkurencji. Choć web scraping jest niezwykle użyteczny, wymaga stosowania się do pewnych zasad etycznych i prawnych. Źródło.

Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Pomysł był prosty. Stworzyć program, który co określony czas (niekoniecznie bardzo często) czyta wszystkie komunikaty ze strony, sortuje je i przesyła zainteresowanym pasażerom. Pojawia się pytanie, jak dotrzeć do pasażerów. Naturalną odpowiedzią wydaje się stworzenie aplikacji na Androida i iOSa, które będą wysyłały powiadomienia w razie utrudnień. Choć uważam to za najlepszy pomysł, zrezygnowałem z niego z kilku powodów:

  1. Brak czasu.
  2. Brak chęci utrzymywania aplikacji w przyszłości.
  3. Koszty.

W mojej głowie pojawił się inny pomysł. Wykorzystajmy ogólnodostępne komunikatory, którymi będą przekazywane komunikaty. Zdefiniowałem następujące wymagania:

  1. Darmowe API.
  2. Możliwość tworzenia grup odbiorczych dla poszczególnych linii. Tak, żeby pasażerowie dostawali jedynie interesujące ich komunikaty.
  3. API z dobrą dokumentacją, proste w obsłudze.
  4. Duża popularność komunikatora - im więcej osób ma go na swoim smartfonie, tym lepiej.

Porównanie wygląda następująco.

Komunikator Darmowe API Możliwość tworzenia grup odbiorczych Dobra dokumentacja Popularność komunikatora
SMS Nie Teoretycznie tak, ale niewygodny sposób dołączania Tak Wysoka
Messanger Wersja darmowa jest bardzo ograniczona Tak Tak Wysoka
Whatsapp Wersja darmowa jest bardzo ograniczona Tak Tak Wysoka
Signal Tak Tak Tak, ale brak oficjalnego wsparcia dla chatbotów. Można skorzystać np. z SignalBot Niska
Telegram Tak, z niewielkimi ograniczeniami Tak Tak Średnia

Niech będzie Telegram

Początkowo, podjąłem próby z Signalem, ale szybko stwierdziłem, że nie jest to najlepsze rozwiązanie do tworzenia chatbotów. Pomimo dużych niechęci wybór padł na Telegram. W związku z tym, należało wykonać następujące kroki:

  1. Założyłem konto na Telegramie.
  2. Wyszukałem użytkownika @BotFather, wysłałem wiadomość o treści: “/newbot” i postępowałem wg kolejnych instrukcji.
  3. Otrzymany token zapisałem w zmiennych środowiskowych, które będą wykorzystywane przez mój skrypt Pythonowy.
  4. W Telegramie utworzyłem kanały dedykowane dla każdej z linii i dodałem do nich mojego bota jako administratora. Uwaga: darmowa wersja Telegrama umożliwia utworzenie maksymalnie 10 publicznych kanałów.
  5. Za pomocą @JsonBOT pobrałem ID każdego z kanałów i również umieściłem je w zmiennych środowiskowych.
Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Niech będzie Python

W moim planie pojawił się pewien problem. Komunikaty o utrudnieniach nie zawierają informacji o liniach, których dotyczą. Założeniem było, że wysyłamy wiadomości do użytkowników zainteresowanych daną linią, tak żeby uniknąć spamu na ich Telegramach. Myślałem przez chwilę o posortowaniu komunikatów ze względu na miasta, ale to znowu drastycznie zwiększyłoby ilość kanałów. Wpadłem na pomysł, żeby wyszukiwać linie po numerach pociągów, które są umieszczane w komunikatach. Problem polega na tym, że jedynym miejscem, które przypisuje te numery do linii są rozkłady jazdy w postaci PDFów. Tu pojawia się kilka trudności, dla jednej linii może być nawet kilka rozkładów. Osobno umieszczane są rozkłady na dni robocze, osobno na dni wolne, jeszcze osobno na jakieś specjalne okazje, np. długie weekendy. Dodatkowo z nieznanego mi powodu rozkłady są w różnych formatach.

Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Dodatkowo w przypadku zmian rozkładów (dzieje się to 4 razy w roku), mogą, ale nie muszą, zmieniać się numery pociągów.
W związku z powyższym konieczne było napisanie skryptu do pobierania nr linii z rozkładów jazdy. Wygląda on następująco.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import pdfplumber


class TimeTable:
"""
Representation of single timetable
"""

def __init__(self, file):
"""
Timetable constructor

:param file: name of file which contains timetable
"""
self.file = file

def get_train_numbers(self):
train_numbers = []

with pdfplumber.open(self.file) as pdf:
for page in pdf.pages:
tables = page.extract_tables()

if tables:
for table in tables:
if table:
# Get train number from first row
first_row = table[0]

# Skip first result, it is row description
skip_first = True

for i, cell in enumerate(first_row):
if skip_first:
skip_first = False
continue

# if cell is not empty
if cell:
# Get text in the first line
parts = cell.split('\n')
first_line = parts[0]

# In case of double numbers, eg. '90431/90432'
if first_line.endswith('/'):
if len(parts) > 1:
train_number = first_line + parts[1]
elif i + 1 < len(table):
train_number = first_line + table[1][i]
else:
train_number = first_line
else:
train_number = first_line

train_numbers.append(train_number)

# remove duplicated numbers
train_numbers = list(set(train_numbers))
return train_numbers

def get_line_number(self):
"""
Extract line number from the file name.
Assumes that the file name follows the format: "some-text-LINE_NUMBER-something.pdf"

:return: line number as string
"""
line = self.file.split('/')[-1]
result = line.split('-')[-2]
return result

Jak wygląda przykładowy komunikat?

Początkowo myślałem, że komunikaty są generowane w sposób półautomatyczny, tzn. szablon jest zawsze taki sam, a pracownik uzupełnia informacje takie jak numer pociągu czy długość opóźnienia. Po czasie okazało się jednak, że informacje są (chyba) wprowadzane ręcznie, ponieważ zdarzają się literówki, czasem brakuje niektórych słów lub są one zmienione. Początkowo sprawiało to trochę problemów w czasie scrapowania strony, ponieważ skrypt nie uwzględniał takich okoliczności. Na przykład numer pociągu był początkowo wyszukiwany jako wartość zapisywana po słowach “Pociąg nr”. Okazało się, że czasem tworzone są komunikaty w których brakuje skrótu “nr”. Skrypt był dostosowywany w miarę znajdowanych wyjątków i aktualnie sprawia wrażenie odpornego na różne “zakłócenia”.

Czy ten pociąg dziś przyjedzie? O scrapowaniu danych słów kilka

Jak działa scrapowanie poszczególnych informacji?

Kod jest dostępny na moim githubie, a jego sercem jest plik message.py, który scrapuje informacje zapisane w komunikatach:

  1. Numer pociągu

    1
    2
    3
    4
    5
    6
    7
    def __parse_number(self, data):
    temp = re.search(r"Pociąg(?: nr)? (\d+)", data)

    if temp:
    return temp.group(1)
    else:
    self.status = self.status * 0

    Funkcja wykorzystuje wyrażenie regularne, w którym zapisano dosłowne dopasowanie słowa “Pociąg” i opcjonalny “nr”, po których następuje numer złożony z dowolnej liczby cyfr.

  2. Relacja

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def __parse_route(self, data):
    x = self.data.find("relacji") # remove beginning
    if x != -1:
    try:
    temp = self.data[x + len("relacji"):]
    temp = re.findall(r"([\w\s.]+)\s(\d{2}:\d{2})", temp)
    route = {}
    if temp:
    route['departure_city'] = temp[0][0]
    route['departure_time'] = temp[0][1]
    route['arrival_city'] = temp[1][0]
    route['arrival_time'] = temp[1][1]
    return route
    else:
    self.status = self.status * 0
    except IndexError:
    self.status = self.status * 0

    Funkcja wyszukuje w komunikacie słowa “relacji” i zapisuje infomracje po nim następujące w formie słownika. Wykonywane jest to za pomocą re.findall()

  3. Typ utrudnienia.

    1
    2
    3
    4
    5
    6
    7
    8
    def __parse_type(self, data):
    if "opóźniony" in self.data:
    return "delayed"
    elif "odwołany" in self.data:
    return "cancelled"
    else:
    self.status = self.status * 0
    return "not recognized"

Zwraca typ utrudnienia na podstawie słów zawartych w komunikacie.

  1. Opóźnienie.
    1
    2
    3
    4
    5
    6
    def __parse_delay(self, data):
    temp = re.search(r"(\d+)\sminut", self.data)
    if temp:
    return temp.group(1)
    else:
    return 0 # in case of cancelled train

Wyszukuje liczby zapisanej przed spacją i słowem “minut”. Opóźnienie zawsze podawane jest w minutach, więc nie należy przejmować się innymi jednostkami czasu.

Jak to wszystko działa?

Skrypt uruchamiany jest na Raspberry Pi 4b za pomocą CRONa co 10 minut. Interwał może wydawać się dość rzadki, ale same Koleje Śląskie nie spieszą się z dodawaniem komunikatów, więc nie ma to wielkiego znaczenia. Dane są pobierane z serwera, parsowane i zapisywane jako lista obiektów Message. Lista ta jest porównywana z poprzednią zapisaną i jeśli mamy nowe komunikaty, to są one wysyłane w poszczególnych kanałach nadawaczych. W ostatnim kroku nowa lista jest zapisywana za pomocą modułu Pickle, służącego do serializacji obiektów. Będzie ona wykorzystywana do porównania przy kolejnym wywołaniu skryptu. Rekomendowane jest ostrożne używanie modułu Pickle, ponieważ może być wykorzystywany do wykonywania złośliwego kodu, szczególnie w aplikacjach webowych.

Czy to działa?

Aktualnie jako jedyny użytkownik, mogę stwierdzić, że działa i to zadziwiająco dobrze! Z małymi przerwami od 27 listopada i po wprowadzeniu kilku poprawek aplikacja jest prawie bezobsługowa. “Prawie”, ponieważ w razie zmian rozkładu trzeba aktualizować listę numerów pociągów jeżdżących na danej linii. Oczywiście, przy odrobinie chęci można to zautomatyzować, ale aktualnie chęci brak.

python, telegram, raspberry, zbiorkom, scrapping
Dlaczego nie umiesz dodać dwóch identycznych mikrofonów do macOS?
© 2025 crlhz
crlhz@proton.me


Powered by hexo | Theme is blank