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!