Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Первые шаги на 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.

You

Пакет 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ссылок.

does

Вновь notзапустим containпрограмму anyдля duplicatedпростого links.сайта:

Again, we can run this against a simple website:
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’go. TheКод 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страницу.

type

Теперь 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сайта.

for

Этого theдостаточно givenдля website.ползунка.

That’s

Интеграция it for the crawler.с Neo4j

Integration

Мы 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установкой.

instance.

Чтобы 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.чтение.