Павел Ерофеев
Я занимаюсь разработкой на прикладных языках. Пишу про интересные мне вещи из мира IT и не только.
4 апреля 2017 Разработка

Парсинг веб страниц на языке Go с помощью goquery

Go - современный язык программирования, призванный решить задачи, связанные с системным программированием. Естественно, если бы он был заточен исключительно под них, то не набрал бы своей популярности.
В данной статье я хочу продемонстрировать как можно легко и просто использовать Go для парсинга веб страниц. В частности, особенности работы с пакетом GoQuery

GoQuery - делает практически то же самое, что и jQuery, только для Go. Мы имеем такую же обёртку над DOM, с которой можем оперировать обычными CSS селекторами. За выборку по CSS отвечает пакет cascadia.

go get github.com/PuerkitoBio/goquery
Так как под рукой нет готового html, как это было с javascript, GoQuery предоставляет несколько способов его получения:
// запросить документ по url
NewDocument(url string) (*Document, error)
// получить из структуры *html.Node пакет golang.org/x/net/html)
NewDocumentFromNode(root *html.Node) *Document 
// из потока чтения
NewDocumentFromReader(r io.Reader) (*Document, error)
// из ответа http запроса
NewDocumentFromResponse(res *http.Response) (*Document, error)
Данный набор методов, на данный момент, покрывает практически все необходимые варианты доставки html.
 
В ответ мы получаем структуру goquery.Document:
type Document struct {
    *Selection
    Url *url.URL
    // contains filtered or unexported fields
}

Полученная структура имеет множество методов, я рассмотрю лишь некоторые из них. Полный список можно найти в официальной документации.

Поиск элементов по css-селектору:
// Получаем документ по url
doc, _ := goquery.NewDocument("http://mozgcorp.ru")
// получаем объект со всеми тегами "a"
items := doc.Find("a")
 
Получить атрибуты узла можно с помощью метода Attr:
func (s *Selection) Attr(attrName string) (val string, exists bool)​
 
Методом Each можно перебрать полученную выборку, с вызовом callback-а для каждого элемента:
func (s *Selection) Each(f func(int, *Selection)) *Selection​
Each на вход принимает функцию, где первый параметр - общее число элементов, а второй - ссылка на элемент:
doc.Find("a").Each(func(_, s *goquery.Selection){
    // получаем ссылку из атрибута 
    if href, ok := s.Attr("href"); ok {
         fmt.Println(href)
    }
})

А так же функции получения данных внутри элемента:

// получить значения всех текстовых нод внутри элемента
func (s *Selection) Text() string 
// получить html содержимое элемента
func (s *Selection) Html() (ret string, e error)​
 
Ниже приведу общий пример работы с библиотекой. Что бы сделать его немного интереснее, сделаем его асинхронным.
package main

import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"os"
	"sync"
)

func _check(err error) {
	if err != nil {
		panic(err)
	}
}

// основная функция обработки
func parseUrl(url string) {
	fmt.Println("request: " + url)

	// заворачиваем источник в goquery документ
	doc, err := goquery.NewDocument(url)
	_check(err)

	// в манере jquery, css селектором получаем все ссылки
	doc.Find("a").Each(func(i int, s *goquery.Selection) {
		link, _ := s.Attr("href")
		fmt.Println(link)
	})
}

func main() {
	var wg sync.WaitGroup

	// получаем список url из входных параметров
	for _, url := range os.Args[1:] {
		// каждый выполним параллельно
		wg.Add(1)

		// закрываем в анонимной функции переменную из цикла,
		// что бы предотвартить её потерю во время обработки
		go func(url string) {
			defer wg.Done()
			parseUrl(url)
		}(url)
	}

	// ждем завершения процессов
	wg.Wait()
}

Это всего лишь краткий пример работы с GoQuery. Пакет умеет не только получать и парсить html, но и создавать, и модифицировать его. Пакет достаточно прост в использовании, в особенности если вы уже знакомы с jQuery или аналогичными библиотеками.