Telegram bot pentru o selecție personalizată de articole de la Habr

Pentru întrebări precum „de ce?” există un articol mai vechi - Natural Geektimes - face spațiul mai curat.

Sunt o mulțime de articole, din motive subiective unele dintre ele nu-mi plac, iar altele, dimpotrivă, este păcat să sară peste. Aș dori să optimizez acest proces și să economisesc timp.

Articolul de mai sus a sugerat o abordare de scriptare în browser, dar nu mi-a plăcut (chiar dacă l-am folosit înainte) din următoarele motive:

  • Pentru diferite browsere de pe computer/telefon, trebuie să-l configurați din nou, dacă este posibil.
  • Filtrarea strictă de către autori nu este întotdeauna convenabilă.
  • Problema cu autorii ale căror articole nu vrei să le ratezi, chiar dacă sunt publicate o dată pe an, nu a fost rezolvată.

Filtrarea încorporată pe site pe baza evaluărilor articolelor nu este întotdeauna convenabilă, deoarece articolele foarte specializate, în ciuda valorii lor, pot primi o evaluare destul de modestă.

Inițial, am vrut să generez un flux RSS (sau chiar mai multe), lăsând acolo doar lucruri interesante. Dar până la urmă s-a dovedit că citirea RSS nu mi s-a părut foarte convenabilă: în orice caz, pentru a comenta/vota un articol/a-l adăuga la favorite, trebuie să treci prin browser. De aceea am scris un bot telegram care îmi trimite articole interesante într-un mesaj personal. Telegram în sine face previzualizări frumoase din ele, care, combinate cu informații despre autor/evaluare/vizionări, arată destul de informativ.

Telegram bot pentru o selecție personalizată de articole de la Habr

Sub tăietură sunt detalii precum caracteristicile lucrării, procesul de scriere și soluții tehnice.

Pe scurt despre bot

Repertoriu: https://github.com/Kright/habrahabr_reader

Bot în telegramă: https://t.me/HabraFilterBot

Utilizatorul stabilește o evaluare suplimentară pentru etichete și autori. După aceea, se aplică un filtru articolelor - se adună evaluarea articolului pe Habré, evaluarea autorului și media evaluărilor utilizatorilor după etichetă. Dacă suma este mai mare decât un prag specificat de utilizator, atunci articolul trece de filtru.

Un obiectiv secundar al scrierii unui bot a fost să câștigi distracție și experiență. În plus, îmi reaminteam regulat că Nu sunt Google, și de aceea multe lucruri sunt făcute cât mai simplu și chiar primitiv posibil. Cu toate acestea, acest lucru nu a împiedicat procesul de scriere a botului să dureze trei luni.

Afară era vară

Iulie se termina și am decis să scriu un bot. Și nu singur, ci cu un prieten care stăpânia scala și voia să scrie ceva pe ea. Începutul părea promițător - codul va fi tăiat de o echipă, sarcina părea ușoară și m-am gândit că în câteva săptămâni sau o lună bot-ul va fi gata.

În ciuda faptului că eu însumi scriu cod pe stâncă din când în când în ultimii ani, nimeni nu vede sau se uită de obicei la acest cod: proiecte de companie, testarea unor idei, preprocesarea datelor, stăpânirea unor concepte din FP. Eram foarte interesat de cum arată scrierea codului într-o echipă, deoarece codul pe rock poate fi scris în moduri foarte diferite.

Ce s-ar fi putut duce astfel? Totuși, să nu grăbim lucrurile.
Tot ceea ce se întâmplă poate fi urmărit folosind istoricul de comitere.

O cunoștință a creat un depozit pe 27 iulie, dar nu a făcut nimic altceva, așa că am început să scriu cod.

iulie 30

Pe scurt: am scris o analiză a fluxului rss al lui Habr.

  • com.github.pureconfig pentru citirea configărilor typesafe direct în clasele de cazuri (s-a dovedit a fi foarte convenabil)
  • scala-xml pentru citirea xml: deoarece inițial am vrut să scriu propria mea implementare pentru fluxul rss, iar fluxul rss este în format xml, am folosit această bibliotecă pentru parsare. De fapt, a apărut și analiza RSS.
  • scalatest pentru teste. Chiar și pentru proiecte mici, scrierea testelor economisește timp - de exemplu, atunci când depanați analiza XML, este mult mai ușor să îl descărcați într-un fișier, să scrieți teste și să corectați erorile. Când a apărut mai târziu o eroare cu analizarea unor coduri html ciudate cu caractere utf-8 invalide, sa dovedit a fi mai convenabil să îl puneți într-un fișier și să adăugați un test.
  • actori din Akka. Obiectiv, nu era nevoie de ele deloc, dar proiectul a fost scris pentru distracție, am vrut să le încerc. Drept urmare, sunt gata să spun că mi-a plăcut. Ideea de OOP poate fi privită din cealaltă parte - există actori care fac schimb de mesaje. Ceea ce este mai interesant este că puteți (și ar trebui) să scrieți cod în așa fel încât mesajul să nu ajungă sau să nu fie procesat (în general, când contul rulează pe un singur computer, mesajele nu ar trebui să se piardă). La început mă zgâriam în cap și era gunoi în cod cu actori abonați unul la altul, dar până la urmă am reușit să vin cu o arhitectură destul de simplă și elegantă. Codul din interiorul fiecărui actor poate fi considerat cu un singur thread; atunci când un actor se blochează, acca îl repornește - rezultatul este un sistem destul de tolerant la erori.

9 august

Am adăugat la proiect scala-scrapper pentru analizarea paginilor html din Habr (pentru a extrage informații precum evaluarea articolului, numărul de marcaje etc.).

Și Pisicile. Cei din stâncă.

Telegram bot pentru o selecție personalizată de articole de la Habr

Am citit apoi o carte despre bazele de date distribuite, mi-a plăcut ideea de CRDT (Conflict-free replicated data type, https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type, habr), așa că am postat o clasă de tip a unui semigrup comutativ pentru informații despre articolul despre Habré.

De fapt, ideea este foarte simplă – avem contoare care se schimbă monoton. Numărul de promoții crește treptat, la fel și numărul de plusuri (precum și numărul de minusuri). Dacă am două versiuni ale informațiilor despre un articol, atunci le pot „imbina într-una” - starea contorului care este mai mare este considerată mai relevantă.

Un semigrup înseamnă că două obiecte cu informații despre un articol pot fi îmbinate într-unul singur. Comutativ înseamnă că poți îmbina atât A + B, cât și B + A, rezultatul nu depinde de ordine, iar în final va rămâne cea mai nouă versiune. Apropo, aici există și asociativitate.

De exemplu, așa cum era planificat, rss după analizare a furnizat informații ușor slăbite despre articol - fără valori precum numărul de vizualizări. Un actor special a luat apoi informații despre articole și a alergat către paginile html pentru a le actualiza și a le îmbina cu versiunea veche.

În general, ca și în akka, nu era nevoie de acest lucru, puteai pur și simplu să stochezi updateDate pentru articol și să iei unul mai nou fără îmbinări, dar drumul aventurii m-a condus.

12 august

Am început să mă simt mai liber și, doar pentru distracție, am făcut din fiecare chat un actor separat. Teoretic, un actor în sine cântărește aproximativ 300 de octeți și pot fi creați în milioane, așa că aceasta este o abordare complet normală. Mi se pare că soluția s-a dovedit a fi destul de interesantă:

Un actor a fost o punte între serverul de telegrame și sistemul de mesaje din Akka. Pur și simplu a primit mesaje și le-a trimis actorului de chat dorit. Actorul de chat ar putea trimite ceva înapoi ca răspuns - și ar fi trimis înapoi la telegramă. Ceea ce a fost foarte convenabil este că acest actor s-a dovedit a fi cât se poate de simplu și conținea doar logica pentru a răspunde la mesaje. Apropo, informații despre articole noi au venit la fiecare chat, dar din nou nu văd nicio problemă în asta.

În general, botul deja funcționa, răspundea la mesaje, stoca o listă de articole trimise utilizatorului și deja mă gândeam că botul era aproape gata. Am adăugat încet caracteristici mici, cum ar fi normalizarea numelor și etichetelor de autor (înlocuirea „sd f” cu „s_d_f”).

A mai rămas un singur lucru mic dar — statul nu a fost salvat nicăieri.

Totul a mers greșit

Poate ați observat că am scris botul în mare parte singur. Deci, al doilea participant s-a implicat în dezvoltare, iar următoarele modificări au apărut în cod:

  • MongoDB părea să stocheze starea. În același timp, jurnalele din proiect au fost rupte, deoarece din anumite motive Monga a început să le trimită spam și unii oameni pur și simplu le-au dezactivat la nivel global.
  • Actorul de bridge din Telegram a fost transformat dincolo de recunoaștere și a început el însuși să analizeze mesajele.
  • Actorii pentru chat-uri au fost tăiați fără milă și, în schimb, au fost înlocuiți de un actor care a ascuns toate informațiile despre toate chat-urile simultan. Pentru fiecare strănut, acest actor a avut probleme. Ei bine, da, ca atunci când actualizați informații despre un articol, trimiterea acestora către toți actorii de chat este dificil (suntem ca Google, milioane de utilizatori așteaptă un milion de articole în chat pentru fiecare), dar de fiecare dată când chatul este actualizat, e normal să intri în Monga. După cum mi-am dat seama mult mai târziu, logica de lucru a chat-urilor a fost de asemenea complet tăiată și în locul ei a apărut ceva care nu funcționa.
  • Nu a mai rămas nicio urmă din clasele de tip.
  • O oarecare logică nesănătoasă a apărut în actorii cu abonamentele lor unul la altul, ducând la o condiție de rasă.
  • Structuri de date cu câmpuri de tip Option[Int] transformat în Int cu valori implicite magice precum -1. Mai târziu mi-am dat seama că mongoDB stochează json și nu este nimic în neregulă în a-l stoca acolo Option bine, sau cel puțin analizați -1 ca Nimic, dar la acel moment nu știam acest lucru și mă credeam pe cuvânt că „așa ar trebui să fie”. Nu am scris acel cod și nu m-am obosit să-l schimb deocamdată.
  • Am aflat că adresa mea IP publică tinde să se schimbe și de fiecare dată a trebuit să o adaug în lista albă a Mongo. Am lansat bot-ul local, Monga era undeva pe serverele Monga ca companie.
  • Dintr-o dată, normalizarea etichetelor și a formatării mesajelor pentru telegrame a dispărut. (Hmm, de ce ar fi asta?)
  • Mi-a plăcut că starea botului este stocată într-o bază de date externă, iar când este repornit, continuă să funcționeze ca și cum nimic nu s-ar fi întâmplat. Cu toate acestea, acesta a fost singurul plus.

A doua persoană nu s-a grăbit în mod special și toate aceste schimbări au apărut într-o grămadă mare deja la începutul lunii septembrie. Nu am apreciat imediat amploarea distrugerii rezultate și am început să înțeleg activitatea bazei de date, deoarece... Nu am mai avut de-a face cu ei până acum. Abia mai târziu mi-am dat seama cât de mult cod de lucru a fost tăiat și câte erori au fost adăugate în locul lui.

Septembrie

La început m-am gândit că ar fi util să-l stăpânesc pe Monga și să o fac bine. Apoi am început încet-încet să înțeleg că organizarea comunicării cu baza de date este și o artă în care poți face o mulțime de curse și doar să faci greșeli. De exemplu, dacă utilizatorul primește două mesaje precum /subscribe - si ca raspuns la fiecare vom crea o intrare in tabel, deoarece in momentul procesarii respectivelor mesaje utilizatorul nu este abonat. Am o bănuială că comunicarea cu Monga în forma sa actuală nu este scrisă în cel mai bun mod. De exemplu, setările utilizatorului au fost create în momentul în care acesta s-a înscris. Dacă a încercat să le schimbe înainte de abonamentul... botul nu a răspuns nimic, pentru că codul din actor a intrat în baza de date pentru setări, nu l-a găsit și s-a prăbușit. Când am întrebat de ce să nu creez setări după cum este necesar, am aflat că nu este nevoie să le schimb dacă utilizatorul nu s-a abonat... Sistemul de filtrare a mesajelor a fost făcut cumva neevident, și chiar și după o privire atentă la cod am putut nu înțeleg dacă inițial a fost intenționat în acest fel sau există o eroare acolo.

Nu a existat o listă de articole trimise pe chat; în schimb, mi s-a sugerat să le scriu eu. Acest lucru m-a surprins - în general, nu eram împotriva trăgării de tot felul de lucruri în proiect, dar ar fi logic pentru cel care a adus aceste lucruri și le-a dat jos. Dar nu, cel de-al doilea participant a părut să renunțe la tot, dar a spus că lista din chat ar fi fost o soluție proastă și că a fost necesar să se facă un semn cu evenimente de genul „un articol y a fost trimis utilizatorului x”. Apoi, dacă utilizatorul solicita trimiterea de articole noi, era necesar să se trimită o solicitare către baza de date, care să selecteze din evenimente evenimente legate de utilizator, să obțină și o listă de articole noi, să le filtreze, să le trimită utilizatorului. și aruncați evenimente despre asta înapoi în baza de date.

Al doilea participant a fost dus undeva spre abstracții, când botul va primi nu numai articole de la Habr și va fi trimis nu doar pe telegramă.

Am implementat cumva evenimente sub forma unui semn separat pentru a doua jumătate a lunii septembrie. Nu este optim, dar cel puțin botul a început să funcționeze și a început să-mi trimită din nou articole și mi-am dat seama încet ce se întâmplă în cod.

Acum vă puteți întoarce la început și vă puteți aminti că depozitul nu a fost creat inițial de mine. Ce ar fi putut să meargă așa? Solicitarea mea de tragere a fost respinsă. S-a dovedit că aveam cod redneck, că nu știam cum să lucrez într-o echipă și trebuia să repar erorile din curba actuală de implementare și să nu o rafinam la o stare utilizabilă.

M-am supărat și m-am uitat la istoricul de comitere și la cantitatea de cod scrisă. M-am uitat la momente care au fost inițial scrise bine, apoi au fost sparte înapoi...

La naiba

Mi-am adus aminte de articol Nu ești Google.

M-am gândit că nimeni nu are nevoie cu adevărat de o idee fără implementare. M-am gândit că vreau să am un bot funcțional, care să funcționeze într-o singură copie pe un singur computer ca un simplu program java. Știu că botul meu va funcționa luni de zile fără reporniri, deoarece am scris deja astfel de roboți în trecut. Dacă cade brusc și nu trimite utilizatorului un alt articol, cerul nu va cădea la pământ și nu se va întâmpla nimic catastrofal.

De ce am nevoie de Docker, mongoDB și alt cult cargo al software-ului „serios” dacă codul pur și simplu nu funcționează sau funcționează greșit?

Am renunțat la proiect și am făcut totul cum am vrut.

Telegram bot pentru o selecție personalizată de articole de la Habr

Cam în același timp, mi-am schimbat locul de muncă și timpul liber a lipsit foarte mult. Dimineata m-am trezit chiar in tren, seara m-am intors tarziu si nu am mai vrut sa fac nimic. Nu am făcut nimic o vreme, apoi m-a copleșit dorința de a termina botul și am început să rescriu încet codul în timp ce mă duceam la serviciu dimineața. Nu voi spune că a fost productiv: să stai într-un tren tremurând cu un laptop în poală și să te uiți la preaplinul stivei de pe telefon nu este foarte convenabil. Cu toate acestea, timpul petrecut pentru scrierea codului a zburat complet neobservat, iar proiectul a început să se îndrepte încet spre o stare de funcționare.

Undeva în mintea mea era un vierme al îndoielii care dorea să folosească mongoDB, dar m-am gândit că, pe lângă avantajele stocării de stat „fiabile”, existau dezavantaje vizibile:

  • Baza de date devine un alt punct de eșec.
  • Codul devine din ce în ce mai complex și îmi va lua mai mult timp să-l scriu.
  • Codul devine lent și ineficient; în loc să schimbe un obiect din memorie, modificările sunt trimise în baza de date și, dacă este necesar, retrase.
  • Există restricții privind tipul de stocare a evenimentelor într-un tabel separat, care sunt asociate cu particularitățile bazei de date.
  • Versiunea de încercare a Monga are unele limitări, iar dacă dai de ele, va trebui să lansezi și să configurezi Monga pe ceva.

Am tăiat monga, acum starea botului este pur și simplu stocată în memoria programului și din când în când salvată într-un fișier sub formă de json. Poate că în comentarii vor scrie că mă înșel, că aici ar trebui folosită baza de date etc. Dar acesta este proiectul meu, abordarea cu dosarul este cât se poate de simplă și funcționează într-o manieră transparentă.

A aruncat valori magice precum -1 și a returnat cele normale Option, a adăugat stocarea unui tabel hash cu articole trimise înapoi la obiect cu informații de chat. S-a adăugat ștergerea informațiilor despre articolele mai vechi de cinci zile, pentru a nu stoca totul. Am adus jurnalul într-o stare de lucru - jurnalele sunt scrise în cantități rezonabile atât în ​​fișier, cât și în consolă. S-au adăugat mai multe comenzi de administrare, cum ar fi salvarea stării sau obținerea de statistici, cum ar fi numărul de utilizatori și articole.

S-au remediat o grămadă de lucruri mici: de exemplu, pentru articole, acum este indicat numărul de vizualizări, aprecieri, neapreciere și comentarii la momentul trecerii filtrului utilizatorului. În general, este surprinzător câte lucruri mici au trebuit corectate. Am ținut o listă, am notat toate „neregularitățile” de acolo și le-am corectat pe cât posibil.

De exemplu, am adăugat posibilitatea de a seta toate setările direct într-un singur mesaj:

/subscribe
/rating +20
/author a -30
/author s -20
/author p +9000
/tag scala 20
/tag akka 50

Și o altă echipă /settings le afișează exact în această formă, puteți lua textul din acesta și trimite toate setările unui prieten.
Pare un lucru mic, dar există zeci de nuanțe similare.

Filtrarea articolelor implementată sub forma unui model liniar simplu - utilizatorul poate seta o evaluare suplimentară pentru autori și etichete, precum și o valoare de prag. Dacă suma evaluării autorului, evaluarea medie a etichetelor și evaluarea reală a articolului este mai mare decât valoarea de prag, atunci articolul este afișat utilizatorului. Puteți fie să cereți botului articole cu comanda /new, fie să vă abonați la bot și acesta va trimite articole într-un mesaj personal în orice moment al zilei.

În general, am avut o idee ca fiecare articol să scoată mai multe funcții (hub-uri, numărul de comentarii, marcaje, dinamica modificărilor de evaluare, cantitatea de text, imagini și cod din articol, cuvinte cheie) și să arate utilizatorului un ok/ nu ok voteaza sub fiecare articol si antreneaza un model pentru fiecare utilizator, dar mi-a fost prea lene.

În plus, logica lucrării nu va fi atât de evidentă. Acum pot seta manual un rating de +9000 pentru patientZero și cu un prag de rating de +20 voi fi garantat să primesc toate articolele lui (cu excepția cazului în care, desigur, am setat -100500 pentru unele etichete).

Arhitectura finală s-a dovedit a fi destul de simplă:

  1. Un actor care stochează starea tuturor chat-urilor și articolelor. Își încarcă starea dintr-un fișier de pe disc și o salvează înapoi din când în când, de fiecare dată într-un fișier nou.
  2. Un actor care vizitează fluxul RSS din când în când, află despre articole noi, se uită la linkuri, analizează și trimite aceste articole primului actor. În plus, solicită uneori o listă de articole de la primul actor, le selectează pe cele care nu sunt mai vechi de trei zile, dar nu au fost actualizate de mult timp și le actualizează.
  3. Un actor care comunică cu o telegramă. Am adus în continuare mesajul parsing complet aici. Într-un mod amiabil, aș dori să o împart în două - astfel încât unul să analizeze mesajele primite, iar al doilea să se ocupe de probleme de transport, cum ar fi retrimiterea mesajelor netrimise. Acum nu există nicio retrimitere, iar un mesaj care nu a sosit din cauza unei erori va fi pur și simplu pierdut (cu excepția cazului în care este notat în jurnale), dar până acum acest lucru nu a cauzat probleme. Poate că vor apărea probleme dacă o grămadă de oameni se abonează la bot și eu ajung la limita de trimitere de mesaje).

Ce mi-a plăcut este că datorită akka, căderile actorilor 2 și 3, în general, nu afectează performanța botului. Poate că unele articole nu sunt actualizate la timp sau unele mesaje nu ajung la telegramă, dar contul repornește actorul și totul continuă să funcționeze. Salvez informațiile că articolul este afișat utilizatorului doar atunci când actorul telegramei răspunde că a livrat cu succes mesajul. Cel mai rău lucru care mă amenință este să trimit mesajul de mai multe ori (dacă este livrat, dar confirmarea se pierde cumva). În principiu, dacă primul actor nu a stocat starea în sine, ci a comunicat cu o bază de date, atunci ar putea și el să cadă pe nesimțite și să revină la viață. Aș putea încerca și akka persistență pentru a restabili starea actorilor, dar implementarea actuală mi se potrivește prin simplitatea ei. Nu este că codul meu s-a prăbușit des - dimpotrivă, am depus destul de mult efort pentru a face imposibil. Dar rahat se întâmplă, iar capacitatea de a împărți programul în piese izolate-actori mi s-a părut foarte convenabilă și practică.

Am adăugat circle-ci pentru ca dacă codul se rupe, veți afla imediat despre el. Cel puțin, înseamnă că codul s-a oprit din compilare. Inițial am vrut să adaug travis, dar mi-a arătat doar proiectele fără furculițe. În general, ambele lucruri pot fi folosite liber în depozite deschise.

Rezultatele

Este deja noiembrie. Botul este scris, îl folosesc în ultimele două săptămâni și mi-a plăcut. Dacă aveți idei de îmbunătățire, scrieți. Nu văd rostul monetizării acestuia - lăsați-l să funcționeze și trimiteți articole interesante.

Link pentru bot: https://t.me/HabraFilterBot
Github: https://github.com/Kright/habrahabr_reader

Concluzii mici:

  • Chiar și un proiect mic poate dura mult timp.
  • Nu ești Google. Nu are rost să împuști vrăbii dintr-un tun. O soluție simplă poate funcționa la fel de bine.
  • Proiectele pentru animale de companie sunt foarte bune pentru experimentarea noilor tehnologii.
  • Boții Telegram sunt scrisi destul de simplu. Dacă nu ar fi fost „lucrarea în echipă” și experimentele cu tehnologie, bot-ul ar fi fost scris într-o săptămână sau două.
  • Modelul actorului este un lucru interesant care merge bine cu codul multi-threading și tolerant la erori.
  • Cred că am văzut de ce comunitatea open source iubește furcile.
  • Bazele de date sunt bune deoarece starea aplicației nu mai depinde de blocările/repornirile aplicației, dar lucrul cu o bază de date complică codul și impune restricții asupra structurii datelor.

Sursa: www.habr.com

Adauga un comentariu