Zatrzymanie planety

Niedawno napisałem, że planeta weszła na dach. Jest to nawiązanie do pewnego dowcipu, który w jednej z późniejszych odpowiedzi zacytowałem. Dowcip najbardziej kojarzę z ostatniego odcinka drugiego sezonu serialu Przystanek Alaska pod tytułem Slow dance i jest tam genialnie podany.

Jednak do rzeczy. Planeta Joggera została uruchomiona prawie równo dziewięć lat temu. Już wtedy Planet Venus, czyli oprogramowanie, o które jest oparta, wyglądało na nierozwijane. Ale znałem je i były pakiety w Debianie, więc uznałem, że jest to jakoś utrzymywane i łatwo dostępne. Liczyłem, że w tak zwanym międzyczasie znajdę jakąś rozwijaną alternatywę. Albo sam wprowadzę zmiany.

W międzyczasie zauważyłem, że są problemy z kodowaniem, które zwalałem na obsługę UTF-8 w Pythonie 2. Nawet robiłem jakieś podejście do naprawy, bez sukcesu. A na przepisywanie na Pythona 3 nie miałem czasu.

Fast forward. Zgłoszenie błędu pewnie jest bliższe prawdy. Problem nie jest z UTF-8, a z emoji. Jednak nie w tym rzecz. Silnik działa na Pythonie 2, który jest nierozwijany od lat. Debian, na którym jest to aktualnie uruchomiony, jest prehistoryczny, bez wsparcia bezpieczeństwa. Zaraz wychodzi kolejna wersja i będzie mnie to uwierać jeszcze bardziej. Nie jest to coś, co chcę mieć uruchomione na serwerze, nawet w kontenerze.

Próbowałem napisać własny prosty parser feedów w Pythonie. W końcu robiłem to parę razy. Nie jest to niby trudne, ale… Formatów feedów jest wiele. I czym innym jest pobranie informacji z feedu, co robiłem dotychczas, a czym innym pobranie HTML. W grę wchodzą błędy bezpieczeństwa (XSSy i podobne atrakcje), konieczność poprawy linków na bezwzględne. Format niby jest standardowy i jest do tego feedparser, ale różni się znacznie między feedami blogów z których składa się planeta. Last but not least, jeden feed to nie zbiór feedów, jakiś cache by tu się przydał.

Wspominałem, że popularna i polecana biblioteka do tzw. sanityzacji HTML w Pythonie bleach, nie jest już rozwijana? Nadal działa, ale… No i nie korzysta się z tego tak po prostu. Co z tego, że zrobię sanityzację linków do obrazków, jeśli zamiast zdjęcia wyświetli się kod HTML. Wygląda to fatalnie. Mogę usunąć te tagi i wtedy po prostu nie będzie zdjęć. Też średnio.

Kolejna sprawa: ruch. O ile planeta ma stały ruch – i przyznaję, że sam często korzystam w ten sposób – to jest on niewielki.

Ostatnia sprawa: czas. Po tym, jak znowu spędziłem z godzinę na szukaniu alternatyw i kolejną na próbach napisania własnego parsera stwierdzam, że nie mam czasu. OK, nauczyłem się nieco o parsowaniu feedów i sanityzacji w Pythonie. Znalazłem alternatywny soft w Ruby, nierozwijany od raptem 5 lat, czyli w porównaniu – nówka. No ale nie jestem przekonany do niego. I nie mam teraz czasu na zabawę.

Co wchodzi w grę dalej:

  • Uruchomienie planety na innej domenie, niezależnie od wybranego silnika. Rozwiązuje – w sumie tylko mój – problem z XSS itp. Nawet mam domenę i serwer. Mógłbym luźniej podejść do sanityzacji. Tylko nadal, to będzie słaby, niebezpieczny soft. I trzeba go napisać.
  • Prosta planeta, gdzie będą tylko tytuły i daty wpisów. Może tekstowy fragment opisu, bez formatowania HTML. Przyznaję, że ma to swoje zalety, jeśli chodzi o pisanie kodu i jest mi blisko do tego rozwiązania. Nadal, trzeba napisać, przetestować, uruchomić.
  • Ktoś z większym zapałem przejmuje planetę. W sumie oczywista oczywistość, cała konfiguracja i wszystkie potrzebne pliki są dostępna na GitHub.

Tymczasem w najbliższym czasie Planeta przestanie aktualizować wpisy. Nie wyłączam zupełnie, bo jest tam trochę linków do blogów. Nie podjąłem decyzji o wyłączeniu (w końcu finalnie to statyczny plik HTML, więc co tu wyłączać). Jako ołtarzyk – zostaje.

OR-tools

Niedawno xpil wrzucił zagadkę dotyczącą rozmieszczenia liczb na wierzchołkach dwunastościanu foremnego. Nieco rozochocony zeszłorocznym Advent of Code (który w znacznym stopniu odpuściłem, za wiele srok) stwierdziłem, że „to się zaprogramuje”.

Suma liczb na każdym boku była dość spora, ale istniało ograniczenie w postaci wymogu, że muszą być liczbami pierwszymi, więc może nie będzie tak źle? No bo na ile sposobów można wybrać pięć liczb z nieco ponad trzystu tak, by suma dawała określoną wartość? Otóż niestety na wiele i po wstępnej przymiarce wiedziałem, że brnę w ślepą uliczkę.

Przypomniałem sobie o Z3 solver, które bywa wykorzystywane w CTFach do rozwiązywania zadań i wyglądało trochę na szwajcarski scyzoryk. Tyle, że nie znam tego rozwiązania – nigdy nie znalazłem czasu, by się nauczyć. Ale od czego mamy AI? Porozmawiam z chatem, na pewno pomoże.

Rozmowę zacząłem jednak od problemu ogólnego, trochę licząc, że jest jakiś wyjątkowa właściwość lub algorytm dla tego dwunastościanu. Gdy poprosiłem o kod w Pythonie, ku mojemu zdziwieniu zaproponował rozwiązanie z użyciem nie Z3, tylko OR-tools. Zerknąłem i okazuje się, że Microsoft zrobił Z3, a Google zrobiło coś może mniej uniwersalnego, ale podobno szybszego, przeznaczonego do optymalizacji.

Przyznaję, że OR-tools robi dobre wrażenie. Podobnie jak Z3 nie jest proste i intuicyjne, ale po krótkiej chwili walki z chatGPT udało się złożyć program, który znalazł rozwiązanie. W bardzo krótkim czasie, rzędu kilkunastu sekund. Co ciekawe, algorytm jest niedeterministyczny. Rozwiązania nie podaję, bo jest na stronie z rozwiązaniem zagadki – na oko bardzo podobne. Jeśli komuś zależy to znajdę to co chatGPT zaproponował.

To teraz wypadałoby nauczyć się obu narzędzi, ale raczej nie znajdę na to czasu. Za to przynajmniej będę wiedział, że istnieją i co mniej więcej potrafią.

I ciekawostka. Wiecie co to jest „LUB-przykładowe narzędzia”? Jest to odpowiednik „OR-Tools Examples” w tłumaczeniu na oficjalnej stronie Google. To tak dla ustalenia, gdzie jesteśmy z automatycznymi tłumaczeniami. Chciałem napisać, „z AI”, ale chyba nie było tam wykorzystane – Gemini tłumaczy znacznie lepiej i całkiem sensownie.

Dict, set, list!

Przy okazji niedawnego code review dostałem pytanie, czemu w skrypcie napisanym w Pythonie nie korzystam z obiektu typu set, tylko z dict. Chodziło o cache na kilka tysięcy elementów, odpytywany kilkaset tysięcy razy. Z przyzerowym hit ratio. Zdziwiłem się, bo kojarzyłem, że czytelna konstrukcja wykorzystująca in dla obiektu typu list

data = [x for x in range(1000)]
y = 1001
if y in data:
    print("Hit")

jest raczej wolna. Zwłaszcza w porównaniu z nieco mniej czytelnym wariantem z użyciem dict:

data = {x: True for x in range(1000)}
y = 1001
if data.get(y):
    print("Hit")

Odpisałem o co chodziło i usłyszałem, że przecież set jest szybki. Coś mi zaświtało. Bo niby set jest bardziej podobny w użyciu do listy, ale pod spodem ma parę ciekawych właściwości. Zresztą, gdy poprosimy LLM o optymalizację pod kątem szybkości, otrzymamy coś w stylu[1]:

data = {x for x in range(1000)} # Converted 'data' to a set
y = 1001
if y in data:  # Checking membership still works the same way
    print("Hit")

Różnice w czasie wykonania możemy zgrubnie i niezbyt elegancko sprawdzić w następujący sposób:

Jak już jesteśmy przy tego typu ciekawostkach. A co jeśli mamy w cache ciągi znaków i chcemy sprawdzić, czy nasz ciąg znaków nie kończy się jednym z ciągów z cache? LLM poproszony o optymalizację znowu podpowiada, że lepszy jest set. Ale nieoczekiwanie sugeruje też wykorzystanie regexpów. I co? Ku mojemu zaskoczeniu regexp w stylu

pattern = re.compile("("+"|".join(our_set)+")"$")
return bool(pattern.search(tested_value))

okazuje się najszybszym rozwiązaniem! Nie to, że uważam regexpy za szczególnie wolne, co to, to nie. Oczywiście pattern wykonujemy raz, nie dla każdej testowanej wartości.

Dalsza lektura:
O’reily High performance Python Dictionaries and Sets

[1] Tak, różnica niezbyt rzuca się w oczy przy takim zapisie. Mało widać różnicę między dict a set. Kiedyś już o tym wspominałem. Czytelniejszy zapis – którego chyba nikt nie używa – tamże.