přihlásit se
k47.cz
Už deset let na vašich monitorech.

Projekt Chanminer - implementace

autor: kaja47 - CC by-nc-sa - publikováno: - sekce obsah » programování #2370
štítky: , a
programování je součástí sbírky Projekt Chanminer «« »»

Chanminer je napsaný ve Scale a je tvořen mnoha aktorovými pipeline. Pro každý board (jako např. /g/) na každém chanu (jako například 4chan) existuje jedna kompletní pipeline, která ústí do globálního úložiště.

Schématicky vypadá tok zpráv v systému následovně:

╭─┤Board├────────╮
│ ╭────────────╮ │
│ │ httpClient │ │
│ ╰─────┬──────╯ │
│       ∇        │
│ ╭────────────╮ │
│ │   parser   │ │
│ ╰─────┬──────╯ │
│       ∇        │
│ ╭────────────╮ │  ╭─────────────────╮
│ │   buffer   ├───>│ ImageDownloader │
│ ╰─────┬──────╯ │  ╰────────┬────────╯
╰───────│────────╯           │
        ∇                    │
  ╭────────────╮             │
  │   storage  │<────────────╯
  ╰────────────╯

HttpClient periodicky stahuje první stranu každého boardu. Využívá při tom HTTP hlavičku Last-Modified If-Modified-Since, aby mohl plánovat požadavky jenom když jsou skutečně potřeba a šetřit tak datové přenosy. Na nejrušnějších boardech přibývá několik postů za vteřinu, u těch nejpomalejších může trvat hodinu něž někdo něco pošle.

Parser je srdce Chanmineru, které ze stažené stránky extrahuje jednotlivé posty. Stažené HTML je nejdřív naprasováno knihovnou tagsoup do DOMu Scala.xml, ze kterého se pak jednotlivé informace extrahují xpath-like výrazy. Napsat každý parser není tak úplně jednoduché: jednak se spoléhá na velice slabě strukturovaná data, která vysloveně nejsou určena pro strojové zpracování a extrakci informací. Formát se navíc kdykoli může změnit a má spoustu drobných nuancí. Navíc standardní Scalovská knihovna pro XML není nic moc a někdy se s ní pracuje vyloženě špatně.

Buffer je přestupní stanice mezi parserem a databází. Posílají se do něj extrahované posty, buffer filtruje duplicity (některé posty extrahované z jedné stránky často byly získány už z minulého požadavku, na pomalých boardech je obvyklé, že stránka obsahuje přes 80% duplicit) a bufferuje unikáty. Když velikost bufferu dosáhne určité meze, pošle svůj obsah do Storage aktoru. Buffer původně vznikl jako optimalizace, aby se do databáze zapisovalo jenom ve velkých dávkách. Až později se ukázalo, že se hodí i pro různé statistiky.

ImageDownloader se stará o stahování obrázků a/nebo jejich náhledů podle různých pravidel (všechno, nic, jenom náhledy, obrázek pokud je menší než 1MB jinak náhled, atd).

Storage je pak poslední článek řetězu do kterého ústí všechny pipeline a který ukládá zprávy do MySQL databáze, se kterou komunikuje pomocí knihovny ScalaQuery


Hlavní úložiště zpráv je jedna gigantické tabulka, která se pro tenhle účel krásně hodí, protože všechna data jsou plochá.

Ale nové zprávy nejdou přímo tam, ale nejdřív se uloží do jiné tabulky, která slouží jako buffer a a odtud se v dávkách po milionu přesypou do hlavní tabulky. Tohle podivné uspořádání vzniklo jako další předčasná optimalizace (buffer nemá žádné indexy, takže by se do něj mělo zapisovat velice rychle), ale překvapivě našlo uplatnění. Manipulace s hlavní tabulkou, která má přes 20 GB obvykle trvá celou věčnost. Změna nebo přidání sloupce zabere několik hodin. Takže, když provádím nějakou takovouhle monster-operaci s hlavní tabulkou, Chanminer může nerušeně plnit buffer, aniž by se nechal znepokojovat metamorfózou hlavního úložiště.


Aplikace využívá dva typicky Scalovké vzory:

  1. Cake Pattern pro depandency injection.
  2. Stackable trait pattern pro výsledné poskládání všech boardů

Základem je trait Boards, který tvoří kontejner celé aplikace: definuje závislosti (storage atd.) využívané všemi boardy a abstraktní vnitřní trait Board představující aktorovou pipeline jednoho boardu.

trait Boards {
  def boards: List[Board] = Nil // každý mixnutý trait do téhle funkce přidá vlastní boardy přes stackable trait pattern
  val storage: Storage // dependency
  val logger: Logger   // dependency
  val imageDownloader: ImageDownloader

  trait Board {
    val httpClient: HttpClient = new HttpClient
    val parser: Parser
    val buffer: Buffer = new Buffer

    class HttpClient extends Actor { ... }
    class Parser extends Actor { ... }
    class Buffer extends Actor { ... }
  }
}

Každý chan si podědí kontejner Boards do vlastního traitu a uvnitř si vytvoří vlastního potomka traitu Board a v něm vlastní Parser, případně přepíše a nastaví všechno, co je potřeba pro ten který chan. Pak vytvoří instance všech boardů a připojí je do metody boards pomocí stackable trait pattern.

Cake Pattern, jak je popsán v původním Martinově článku používá self typy pro vyznačení závislosti na jiné službě: trait 4chanBoards { this: Boards => ... }. Chanminer rovnou dědí, protože self typy by znemožnily použití stackable trait pattern. Z celého složitého Cake patternu tedy vzal za vlastní jenom myšlenku vnějšího kontejneru v němž jsou všechny závislosti deklarovány jako abstract val.

trait `4chanBoards` extends Boards {

  private val _boards = List("3", "a", "_", "c", "cm", "d", "e", ... "x").map(new `4chanBoard`(_))
  abstract override def boards = _boards ::: super.boards // stackable traits

  class `4chanBoard`(board: String) extend Board {
    override val httpClient = new HttpClient { ... }
    val parser = new Parser { ... }
  }

}

trait `7chanBoards` extends Boards {

  private val _boards = List("777", "7ch", "b", "banner", "fl", ... "ss", "unf", "v").map(new `7chanBoard`(_))
  abstract override def boards = _boards ::: super.boards

  class `7chanBoard`(board: String) extend Board { ... }
}

Pak mám připraveny dvě prostředí: testovací a produkční.

trait ProductionBoards extends Boards with ImageDownloaderComponent {
  val storage = new MySQLStorage()
  val logger = new Logger()
  val imageDownloader = new ImageDownloader(directory, acceptRules)
}

trait TestBoards extends Boards with ImageDownloaderComponent {
  val storage = new DummyStorage
  val logger = new PrintlnLogger
  val imageDownloader = new DummyImageDownloader
}

Výsledný program získám tak, že k danému prostředí mixnu všechny potřebné chany:

object App extends ProductionBoards with `4chanBoards` with `7chanBoards`

A díky stackable traits jsou v metodě App.boards naštosované instance všech boardů. Pak už stačí nastartovat aktory a systém jede.


Příště konečně ukážu nějaké zajímavé poznatky získané z té hromady bezcenných dat.

Obsah seriálu Projekt Chanminer

čtěte pokračování »

komentáře RSS

v6ak
26. října 2011 13:13 #936
v6ak píše…

Já poslední dobou taky crawluji jistá data (i když asi poněkud jiná a mám to jednodušší – pro stahování mi stačil bash a pro zrychlení jedné části C++) a určitě jsem nezapomněl na HTTP Keep alive. (V případě wgetu prostě předám více URL zároveň, v mém případě to takto jde.) Používáš to? (Já to původně začal používat, abych se na fakultě vyhnul postihu za podezřelé množství otevíraných a zavíraných socketů.)

Komentář bude formátován pomocí Texy! syntaxe.
Např: **tučný text**, *kurzíva*, "text odkazu":adresa.
Na ostatní komentáře můžete odkazovat pomocí [čísla komentáře].

Napište komentář!

 

o autorovi:

K. Jmenuji se Karel Čížex, v síti také známý jako kaja47 - tak trochu spisovatel, trochu programátor, trochu webař, milovník divné hudby atd atd.
mail:
jabber: kaja47@jabbim.cz

další projekty

wyhledawacz fel.log stalkr vtipy.k47.cz k47.shop Zkracovač adres stripbot

živě z twitteru

N/A

tadá

poslední články

#299
článek | 27. srpna 2014
O čem mluvím, když mluvím o Murakamim
článek | 31. července 2014
Kafe v pět
povídka | 30. června 2014
O bestii
povídka | 30. června 2014
Úvod do zrcadel a labyrintů
článek | 12. května 2014
Slova starého feťáka
článek | 17. dubna 2014
669.350
| 8. dubna 2014
Sny cypherpunků
článek | 17. března 2014
Přinesla si na to dokonce vlastní provaz
článek | 20. února 2014

poslední komentáře

MySQL group by trik
mrsa | 20. března 2014
Vodník
cbvcxy | 22. února 2014
Rozdělení velkých tříd ve Scale
JeLiTo | 11. ledna 2014
Božena Němcová - V zámku a podzámčí
Anonym | 27. prosince 2013
Sedm let post-rocku
Woodbin | 9. prosince 2013

největší kecalové

Anonym Anonym
JeLito JeLito
cbvcxy cbvcxy
Ondřej Levek Ondřej Levek
mrsa mrsa

K47i © 2002 - 2014 K. aka Kaja47