Go & Crawler

Jednoduchy maly crawler pro jednu domenu v jazyce Go

Jiz pred par lety jsem presel na Go jazyk pri tvorbe ruznych nastroju co pouzivam. Je to celkem srozumitelny, jednoduchy ale vykonny jazyk. Drive jsem delal veci hodne v PHP, protoze s nim mam nejvice zkusenosti – ale kdyz proste potrebujete zpracovat velke mnozstvi dat, idealne paralelne pri plnym vykonu serveru, moc to s nim nejde.

Go lang ma skvelou knihovnu Colly ktera resi 99% veci a proto sepsani crawlera je vlastne uplne jednoduche. Vytvorime si tedy soubor main.go s nasledujicim kodem:

package main

import (
 "github.com/gocolly/colly"
 "encoding/json"
 "fmt"
 "log"
 "strings"
 "net/url"
 "golang.org/x/net/publicsuffix"
 "flag"
 )

type Link struct {
 From		string	`json:"from"`
 FromURL		string	`json:"from_url"`
 To		string	`json:"to"`
 ToDomain	string	`json:"to_domain"`
 ToURL		string	`json:"to_url"`
}

var domain string

func main() {
 c := colly.NewCollector(
  colly.Async(true),
 )

 c.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: 24})

 // Find and visit all links
 c.OnHTML("a[href]", func(e *colly.HTMLElement) {

  link := e.Request.AbsoluteURL(e.Attr("href"))
  u, err := url.Parse(link)

  if (err == nil ) {

   var visit string

   if u.Host == "" {
    visit = ""
   } else {
    visit = fmt.Sprintf("%s://%s%s",u.Scheme,u.Host,u.Path)
   }

   if (strings.HasSuffix(u.Host, domain)) {
    e.Request.Visit(visit)
   }

   if visit != "" {

    tlddom, err := publicsuffix.EffectiveTLDPlusOne(u.Host)

    if err != nil {
        tlddom = u.Host
    }

    jsondata := &Link{domain,e.Request.URL.String(),u.Host,tlddom,link}

    b, err := json.Marshal(jsondata)
    				if err != nil {
        				return
    }

    fmt.Println(string(b))

   }
  }

 })

 d := flag.String("d", "", "domain name")
 flag.Parse()

 domain = *d

 if domain == "" {
     flag.PrintDefaults()
 }

 start := fmt.Sprintf("http://%s",domain);
 c.Visit(start)

 c.Wait()

}

Kompilace a spusteni:

user@hdd:~# go build
user@hdd:~# ./crawler_cz
  -d string
    	domain name
user@hdd:~# ./crawler_cz -d flags.cz > result.json

Pri kompilaci muze vyhodit chyby o chybejicich knihovnach, ty se instaluji take jednoduse:

user@hdd:~# go get github.com/gocolly/colly
user@hdd:~# go get golang.org/x/net/publicsuffix

A vysledny soubor po spusteni pak obsahuje .json k dalsimu zpracovani:

{"from":"flags.cz","from_url":"http://flags.cz","to":"flag-icon-css.lip.is","to_domain":"lip.is","to_url":"http://flag-icon-css.lip.is/"}
{"from":"flags.cz","from_url":"http://flags.cz","to":"regtons.com","to_domain":"regtons.com","to_url":"https://regtons.com"}
{"from":"flags.cz","from_url":"http://flags.cz","to":"domain.mr","to_domain":"domain.mr","to_url":"https://domain.mr"}
{"from":"flags.cz","from_url":"http://flags.cz","to":"tlds.africa","to_domain":"tlds.africa","to_url":"https://tlds.africa"}
{"from":"flags.cz","from_url":"http://flags.cz","to":"php7.hosting","to_domain":"php7.hosting","to_url":"https://php7.hosting"}

A jak script tedy presne funguje ?

1, nejprve vydefinujeme ktere knihovny potrebujem naimportovat:

import (
 "github.com/gocolly/colly"
 "encoding/json"
 "fmt"
 "log"
 "strings"
 "net/url"
 "golang.org/x/net/publicsuffix"
 "flag"
 )

2, pote si vytvorime strukturu pro budouci JSON vystup:

type Link struct {
 From		string	`json:"from"`
 FromURL		string	`json:"from_url"`
 To		string	`json:"to"`
 ToDomain	string	`json:"to_domain"`
 ToURL		string	`json:"to_url"`
}

Hodnoty jsem zvolil tyto:

From – obsahuje domenu pro kterou odkazy zjistujeme
FromURL – obsahuje konkretni URL ze ktere odkazy zjistujeme
To – obsahuje domenu (hosta) kam odkaz smeruje
ToDomain – obsahuje ocistenyho hosta primo na domenu (www.domena.tld -> domena.tld, news.domena.tld -> domena.tld)
ToURL – obsahuje URL odkazu

Tyto parametry jsem zvolil pro budouci vyuziti v ElasticSearch kam JSON budu importovat.

3, promennou domain chceme pouzivat globalne, proto ji musime vydefinovat jiz nyni:

var domain string

4, inicializace Colly crawleru. Povolujeme asynchronni zpracovani a nastavujeme paralelismus na 24 (tolik mam jader CPU):

c := colly.NewCollector(
 colly.Async(true),
)

c.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: 24})

5, za pomoci knihovny flags vydefinujeme nutnost zadani parametru „-d“ pro domenu kterou chceme zpracovat. V pripade ze neni zadana, vypiseme informaci jak se ma program spoustet:

d := flag.String("d", "", "domain name")
flag.Parse()

domain = *d

if domain == "" {
    flag.PrintDefaults()
}

6, prevedeme domenu do spravneho URL formatu a pustime crawler:

start := fmt.Sprintf("http://%s",domain);
c.Visit(start)

7, a pockame dokud se vse neprovede 🙂

c.Wait()

8, Colly ma pak eventy pri kterych vola urcity fce. V nasem pripade vyuzijeme OnHTML(„a[href]“) ktery se zavola pro kazdy A tag s definovanym HREF ktery na dane strance najde. V teto casti kodu pak zpracovavame vystup:

// Find and visit all links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {

 link := e.Request.AbsoluteURL(e.Attr("href"))
 u, err := url.Parse(link)

 if (err == nil ) {

  var visit string

  if u.Host == "" {
   visit = ""
  } else {
   visit = fmt.Sprintf("%s://%s%s",u.Scheme,u.Host,u.Path)
  }

  if (strings.HasSuffix(u.Host, domain)) {
   e.Request.Visit(visit)
  }

  if visit != "" {

   tlddom, err := publicsuffix.EffectiveTLDPlusOne(u.Host)

   if err != nil {
       tlddom = u.Host
   }

   jsondata := &Link{domain,e.Request.URL.String(),u.Host,tlddom,link}

   b, err := json.Marshal(jsondata)
   				if err != nil {
       				return
   }

   fmt.Println(string(b))

  }
 }

})

Konkretne pak:

  • do promenne link ulozime absolutni URL nalezeneho odkazu (tj Colly nam z odkazu /neco udela rovnou https://domena.tld/neco odkaz)
  • nasledne rozparsujem vzniklou URL na Scheme, Host, Path a dalsi ktere nepouzijem 🙂
  • vydefinujem promennou visit a to pouze v pripade ze Host existuje (neni tedy zadna chyba) a vygenerujeme ji ciste jako [scheme]://[host]/[path] bez query stringu (GET parametru). Pro toto jsem se rozhodl kvuli duplicitam co vznikaji kdyz se napriklad projizdi web eshopu s volbou men, kde pak projizdi kazdou stranku vicekrat (pro kazdou menu) uplne zbytecne. Samozrejme by to chtelo efektivnejsi reseni, nebot jsem timto odrizl vsechny interni odkazy u webu co nepouzivaji rewrite pravidla a hezke url.
  • podminkou HasSuffix zjistime, jestli nalezena URL se nachazi ve stejne domene (podpora i subdomeny, proto overuju konec retezce), a pokud ano, vlozim do Colly k dalsimu zpracovani
  • pokud neni visit prazdna, tak do promenny tlddom si vytahnu cistou domenu (jiz bez subdomeny) a do jsondata si vlozim potrebne hodnoty pro vygenerovani JSON stringu
  • a ten nakonec vypisu 🙂

Kod neni buhvi jak spickovy, nema osetreny nektery problemy co mohou vzniknout ale pro svuj ucel slouzi perfektne 🙂 Jakekoliv dotazy ci pripominky, nebo i vyuziti muzete sdilet v komentarich 🙂 Diky!

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..