Первые шаги на Go — Построение простого веб приложения с Neo4j
Цель:
Цель этого поста создать простое веб приложение которое снимает структуру вашего сайта получая все ссылки которые содержит и сохраняет их в neo4j базу данных. Идея проста - и в ней следующие шаги:
- Делаем запрос на URL
- парсим ответ
- Извлекаем ссылки из ответа
- Сохраняем извлеченные ссылки в neo4j
- Повторяем 1 шаг с полученными ссылками пока не исследуем весь сайт
- Наконец используем Neo4j веб интерфейс чтобы посмотреть на структуру.
Требования:
Эта статья подойдет начинающим. Будут приведены ссылки, каждый раз, когда будет преставленна новая идея. Для Neo4j, базовое знание графово ориентированной базы данных будет к месту. Предполагается что Go и Neo4j уже установленны на машинет.
Создаем ползунок:
Теперь, когда у нас все есть. Начнем.
Получение одной страницы из интеренета:
Время написать сожный код, которы поможет нам получить определенную страницу из интернета.
package main
import (
"fmt"
"io"
"net/http"
"os"
)
type responseWriter struct{}
func main() {
resp, err := http.Get("http://www.sfeir.com")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
rw := responseWriter{}
io.Copy(rw, resp.Body)
}
func (responseWriter) Write(bs []byte) (int, error) {
fmt.Printf(string(bs))
return len(bs), nil
}
Мы начали с объявления главного пакета и импорта требуемых пакетов. Дальше, мы объявили структуру которая будет реализовывать Writer
интерфейс. В main
функции, мы собираемся присвоить множествую переменных значения. В основном. http.Get
будет возвращать значения с ответом и некоторой ошибкой, если что-то пойдет не так. Это общий способ обработки ошибок в Go программах.
Если вы посмотрите на документацию, вы найдете Writer
интерфейс с одной функцией. Для того, чтобы реализовать этот интерфейс, нам нужно добавить получателя функции к нашей responseWriter
структуре которая совпадает с функцией Writer
. Если вы пришли из Java вы должны ожидать синтаксис типа реалзиация Writer
. Ну чтож, это не тот случай для Go, так как тут реализация происходит неявно.
Наконец, мы используем io.Copy
для записи тела ответа в нашу переменную ответа. Следующий шаг это модификация нашего кода для извлечения ссылок из данного адреса страницы. После некоторого рефакторинга, у нас будет два фала.
Это main.go
:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Web site url is missing")
os.Exit(1)
}
url := os.Args[1]
retreive(url)
}
И этот retreiver.go
:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
type responseWriter struct{}
func (responseWriter) Write(bs []byte) (int, error) {
fmt.Printf(string(bs))
return len(bs), nil
}
func retreive(uri string) {
resp, err := http.Get(uri)
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
rw := responseWriter{}
io.Copy(rw, resp.Body)
}
Мы можем запустить это для небольшого вебсайта:
go run main.go retreiver.go http://www.sfeir.com
Теперь мы сделали наш первый шаг для создание ползунка. Есть возможность загрузить и спарсить данный адрес, открыть подключение прям к удлённому хосту, и получить html содержание.
GettingСоздадим allвсе hyperlinksгиперссыылки forдля aодной single pageстраницы
Now,Теперь thisначинается isчасть theгде partнам whereнужно weизвлечь needвсе toссылки extractиз allhtml linksдокумента. fromК theсожалению, нет доступного для этого помошника для обработки HTML document. Unfortunately, there’s no available helpers to manipulate HTML in theв Go API. So,Поэтому weмы mustдолжны lookпосмотреть forна 3rd partyстороннюю API. Let’sДавайте considerрассмотрим ‘goquery’goquery
. AsКак youвы mightможете guess,догадаться, it’sона similarпохоже toна ‘jquery’jquery
butтолько withдля Go.
Пакет cangoquery
easilyлегко getполучается the ‘goquery’ package by running the following command:командой:
go get github.com/PuerkitoBio/goquery
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Web site url is missing")
os.Exit(1)
}
url := os.Args[1]
links, err := retrieve(url)
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
for _, link := range links {
fmt.Println(link)
}
}
IЯ changedизменил ourнашу retrieve
functionфункцию, toтаким returnобразом, aчтобы listона ofвозвращала linksссылки ofданные aна given web page.странице.
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
)
func retrieve(uri string) ([]string, error) {
resp, err := http.Get(uri)
if err != nil {
fmt.Println("Error:", err)
return nil, err
}
doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
if readerErr != nil {
fmt.Println("Error:", readerErr)
return nil, readerErr
}
u, parseErr := url.Parse(uri)
if parseErr != nil {
fmt.Println("Error:", parseErr)
return nil, parseErr
}
host := u.Host
links := []string{}
doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
href, _ := item.Attr("href")
lu, err := url.Parse(href)
if err != nil {
fmt.Println("Error:", err)
return
}
if isInternalURL(host, lu) {
links = append(links, u.ResolveReference(lu).String())
}
})
return unique(links), nil
}
// insures that the link is internal
func isInternalURL(host string, lu *url.URL) bool {
if lu.IsAbs() {
return strings.EqualFold(host, lu.Host)
}
return len(lu.Host) == 0
}
// insures that there is no repetition
func unique(s []string) []string {
keys := make(map[string]bool)
list := []string{}
for _, entry := range s {
if _, value := keys[entry]; !value {
keys[entry] = true
list = append(list, entry)
}
}
return list
}
AsКак youможно canувидеть, see,наша ourretrieve
‘retrieve’функция functionстала hasсущественно significantlyулучшена. improved.Я Iубрал removedresponseWriter
theстуктуру ‘responseWriter’так structкак becauseона it’sбольше noне longerнужна neededиз-за sincegoqeury
theимеет ‘goquery’свою hasреализацию itsинтерфейса ownWriter
.
implementationЯ ofтак ‘Writer’же interface.добавил Iдве alsoфункции addedпомошников. twoПервая helper- functions. The first one, detect whether theопределяет URL isуказывающий pointingна toвнутреннюю anстраницу. internalВторая page.- Theпроверяет, secondчто one,список ensureне thatсодержит theдубликатов listссылок.
Вновь notзапустим containпрограмму anyдля duplicatedпростого links.сайта:
go run main.go retreiver.go http://www.sfeir.com
GettingПолучаем allвсе hyperlinksгиперссылки forдля theвсего entire siteсайта.
Yeah!Ура! WeМы madeсделали aбольшую hugeработу. progress.Следующее, Theмы nextсобираемся thingпосмотре, we’reкак goingулучшить toretrieve
seeфункцию isдля howтого, toчтобы improveполучить theссылки ‘retrieve’с functionдругих inстраниц, orderв toтом getчисле. linksЯ inпредлагю otherрассмотреть pagesиспользование too.рекурсии. So,Мы I’mсоздадим consideringдругую theфункцию recursiveпод approach.названием We’llcrawl
createи anotherэта functionфункция calledбудет ‘crawlвызывать ’себя andрекурсивно, thisс functionкаждой willследующей callполученой itссылкой. selfТак recursivelyже, withнам eachнужно linkотслеживать givenпосещенные byстраницы, theчтоби ‘retrieve’избежать function.повторных Also,переходов we’llпо needссылкам.
to keep track of the visited pages to avoid visiting the same page multiple times.
Let’s check this Прроверим:
// part of retreiver.go
var visited = make(map[string]bool)
func crawl(uri string) {
links, _ := retrieve(uri)
for _, l := range links {
if !visited[l] {
fmt.Println("Fetching", l)
visited[uri] = true
crawl(l)
}
}
}
NowТеперь weможно canвызывать callcrawl
theвместо ‘crawl’retrieve
insteadфункции ofв the ‘retrieve’ function in the ‘main.
. go’goTheКод codeбудет will be the following :следующим.
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Web site url is missing")
os.Exit(1)
}
url := os.Args[1]
crawl(url)
}
Let’sЗапустим runнашу our program:программу:
go run main.go retreiver.go http://www.sfeir.com
ImplementingРеализуем eventsслушателя listenersсобытий throughчерез Channelsканалы.
InВ theпрошлой previousчасти, sectionмы weувидели sawкак that the fetchedполученные URL isотображаются beingвнутри displayedcrawl
insideфункции. theЭто ‘crawl’не function.лучшее Thisрешение isособенно notкогда theвам bestнужно solutionделать especiallyбольеш whenчем youпросто needвывод toна doэкране. moreЧтобы thanэто justисправить, printingв onосновном, theнам screen.нужно Toреализовать fixслушателя this,событий basically,для we’llполучения needURL toчерез implementканалы.
anДавайте eventпосмотрим listenerна for fetching URLs through Channels.
Let’s have a look at this это:
// same imports
type link struct {
source string
target string
}
type retriever struct {
events map[string][]chan link
visited map[string]bool
}
func (b *retriever) addEvent(e string, ch chan link) {
if b.events == nil {
b.events = make(map[string][]chan link)
}
if _, ok := b.events[e]; ok {
b.events[e] = append(b.events[e], ch)
} else {
b.events[e] = []chan link{ch}
}
}
func (b *retriever) removeEvent(e string, ch chan link) {
if _, ok := b.events[e]; ok {
for i := range b.events[e] {
if b.events[e][i] == ch {
b.events[e] = append(b.events[e][:i], b.events[e][i+1:]...)
break
}
}
}
}
func (b *retriever) emit(e string, response link) {
if _, ok := b.events[e]; ok {
for _, handler := range b.events[e] {
go func(handler chan link) {
handler <- response
}(handler)
}
}
}
func (b *retriever) crawl(uri string) {
links, _ := b.retrieve(uri)
for _, l := range links {
if !b.visited[l] {
b.emit("newLink", link{
source: uri,
target: l,
})
b.visited[uri] = true
b.crawl(l)
}
}
}
func (b *retriever) retrieve(uri string) ([]string, error) {
resp, err := http.Get(uri)
if err != nil {
fmt.Println("Error:", err)
return nil, err
}
doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
if readerErr != nil {
fmt.Println("Error:", readerErr)
return nil, readerErr
}
u, parseErr := url.Parse(uri)
if parseErr != nil {
fmt.Println("Error:", parseErr)
return nil, parseErr
}
host := u.Host
links := []string{}
doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
href, _ := item.Attr("href")
lu, err := url.Parse(href)
if err != nil {
fmt.Println("Error:", err)
return
}
if isInternalURL(host, lu) {
links = append(links, u.ResolveReference(lu).String())
}
})
return unique(links), nil
}
// same helper functions
AsКак youможно canувидеть, see,у weнас haveесть threeдополнительные additionalфункции, functionsдля toуправлеия helpсобытиями usдля manageданного the events for a given ‘retriever’retriever
. ForДля thisэтого codeкода Iя usedиспользовал thego
‘go’ключевое keyword.слово. Basically,В writingосновном, ‘написание go foo()
’willзапустит makeфункцию thefoo
‘foo’запуститься functionасинхронно. runВ asynchronously.нашем Inслучае ourмы case,использовали we’rego
usingкоторая aявляется ‘go’анонимной withфункцией, anчтобы anonymousпослать functionпарамметр toсобытия(ссылку) sendвсем theслушателям eventчерез parameterканал.
(theЯ link)указал forтип allканала listenersданных throughкак channelslink
, Note:который I’veсодержит setисточник theи channelцелевую dataстраницу.
Теперь toдавайте ‘link’взглянем thatна containsmain
the source and target page.
Now let’s have a look on the ‘main’ function функцию:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Web site url is missing")
os.Exit(1)
}
url := os.Args[1]
ev := make(chan link)
r := retriever{visited: make(map[string]bool)}
r.addEvent("newLink", ev)
go func() {
for {
l := <-ev
fmt.Println(l.source + " -> " + l.target)
}
}()
r.crawl(url)
}
AgainВновь Iиспользуя usedgo
theключевое ‘go’слово, keyword,в thisэтот timeраз forчтобы receivingполучать theпараметры eventсобытия parameterотправленные sentcrawl
byфункцией. theЕсли ‘crawl’мы function.запустим Ifнашу weпрограмму, runтеперь ourмы programдолжны nowувидеть weвсе shouldвнутренние seeссылки allдля internalданного linksсайта.
Этого theдостаточно givenдля website.ползунка.
Интеграция it for the crawler.с Neo4j
Мы Nowзакончили thatс we’reползунком, doneдавайте withперейдем theк crawler,части let’sс getNeo4j. toПервая theвещь, Neo4jкоторую part.мы Theсобираемся firstсделать thingэто we’reустановить going to do is to install the driver.драйвер.
go get github.com/neo4j/neo4j-go-driver/neo4j
AfterПосле installingустановки theдрайвера, driver,нам weнужно needсоздать toнекую createбазовую someфункцию basicкоторая functionsпозволит thatнам willработать allow us to work withс Neo4j.
Let’sСоздадим createфайл aпод newназванием file called ‘neo4j.
:go’ go
package main
import (
"github.com/neo4j/neo4j-go-driver/neo4j"
)
func connectToNeo4j() (neo4j.Driver, neo4j.Session, error) {
configForNeo4j40 := func(conf *neo4j.Config) { conf.Encrypted = false }
driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth(
"neo4j", "alice!in!wonderland", ""), configForNeo4j40)
if err != nil {
return nil, nil, err
}
sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
session, err := driver.NewSession(sessionConfig)
if err != nil {
return nil, nil, err
}
return driver, session, nil
}
func createNode(session *neo4j.Session, l *link) (neo4j.Result, error) {
r, err := (*session).Run("CREATE (:WebLink{source: $source, target: $target}) ", map[string]interface{}{
"source": l.source,
"target": l.target,
})
if err != nil {
return nil, err
}
return r, err
}
func createNodesRelationship(session *neo4j.Session) (neo4j.Result, error) {
r, err := (*session).Run("MATCH (a:WebLink),(b:WebLink) WHERE a.target = b.source CREATE (a)-[r:point_to]->(b)", map[string]interface{}{})
if err != nil {
return nil, err
}
return r, err
}
Basically,В weоснове, haveмы threeимеет functionsтри responsibleфункции forотвечающие initiatingза connectionинициализацию toподключения Neo4j withс basicбазовым querying.запросом.
Note:Вам Youнужно might need to change theпоменять Neo4j configurationконфигурацию toдля workработы withс yourлокальной localустановкой.
Чтобы Toсоздать createWebLink
aноду ‘WebLink’нам nodeпросто weнужно simplyзапустить needследующий to run the following query:запрос:
CREATE (:WebLink{source: "http://www.sfeir.com/", target: "http://www.sfeir.com/en/services"})
OnceКак theтолько nodesнода areбудет created,создана, weнам needнужно toсоздать createотношение relationshipмежду betweenними themзапустив byследующий running the following query запрос:
MATCH (a:WebLink),(b:WebLink)
WHERE a.target = b.source
CREATE (a)-[r:point_to]->(b)
Now,Давайте let’sобновим updateнашу ourmain
‘main’ function.функцию.
package main
import (
"fmt"
"os"
"github.com/neo4j/neo4j-go-driver/neo4j"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Web site url is missing")
os.Exit(1)
}
driver, session, connErr := connectToNeo4j()
if connErr != nil {
fmt.Println("Error connecting to Database:", connErr)
os.Exit(1)
}
defer driver.Close()
defer session.Close()
url := os.Args[1]
ev := make(chan link)
r := retriever{visited: make(map[string]bool)}
r.addEvent("newLink", ev)
go func(session *neo4j.Session) {
for {
l := <-ev
fmt.Println(l.source + " -> " + l.target)
_, err := createNode(session, &l)
if err != nil {
fmt.Println("Failed to create node:", err)
}
}
}(&session)
r.crawl(url)
fmt.Println("Creation of relationship between nodes.. ")
_, qErr := createNodesRelationship(&session)
if qErr == nil {
fmt.Println("Nodes updated")
} else {
fmt.Println("Error while updating nodes:", qErr)
}
}
WithИспользую theтри usageфункциюю ofобъявленные threeв functionsneo4j.go
declaredнаша inпрограмма theсоздаст ‘neo4j.go’подключение our program will initiate a connection toк neo4j, subscribeподпишется toна ‘newLink’newLink
eventсобытие toдля insertвставки nodesнод andи finallyнаконец updateобновит nodesсвязи relationship.нод.
IЯ usedиспользовал thedefer
theключево ‘defer’слово, keywordчтобы toсослаться defersна theисполнение executionфункции ofдо aтех functionпор untilпока theне surroundingзавершиштся ‘main’main
functionфункция.
returns.Давайте Let’sзапустим runв thisпоследний for the last time раз:
go run main.go retreiver.go neo4j.go http://www.sfeir.com
ToЧтобы checkпроверить inрезультат the result on Neo4j, you can run the following query on yourв Neo4j Browser:вы можете запустить следующий запрос в вашем Neo4j браузере.
MATCH (n:WebLink) RETURN count(n) AS count
OrИли thisэтот queryзапрос toотобразит displayвсе all nodes:ноды:
MATCH (n:WebLink) RETURN n
Et Voilà! The result after running the last query :
It’s pretty, isn’t it?
ConclusionВыводы
ThroughВ thisэтом postпосте, weмы exploredизучили aмножество lotсвойств ofязыка featuresGolang ofвключая theмножественные Goприсвоения programmingпеременным, languageреализацию includingинтерфейсов multipleи variableканалов assignment,и implementationгорутин. interfacesТак andже channelsмы andиспользовали goroutines.стандартную Also,библиотеку. weСпасибо usedза the standard library as well as some 3rd party libraries. Thank you for reading it. The code source is available on my GitHub.чтение.