Парсим Google Analytics на GO #1
GO это уже даже не "стильно модно молодежно", а просто норма. Он постепенно становится стандартом для написания демонов и других мелких и больших системных утилит. На нем пишут апи, которые обрабатывают кучу запросов и многое другое.
Ссылки на базовые туториалы и другие классные статьи "как начать писать" проще найти в гугле. Здесь же опишу задачу и решение.
Задача
Написать парсер данных из Google Analytics. В первом приближении вытащить только просмотры (pageViews) у заданного массива ссылок.
Прототип
- Авторизуемся через OAuth2 в Google
- Запрашиваем список доступных View из аналитики
- Выбираем нужный View
- Получаем от пользователя список ссылок и даты начала и конца поиска
Проблемы по пути
- Никогда ничего не писал на GO
- Уродливая документация Google Analytics
- Библиотеки от Google без нормальных примеров использования
Сразу предупреждаю, код не претендует на красоту и гениальность, это просто пробный заход, накиданный за пару вечеров.
Начало
Установить GO. Ставил через Homebrew версию 1.6.2 – последняя доступная на момент написания статьи.
Далее нужно создать приложение в Google Developers Console. Включить для него Analytics API и Analytics Reporting API V4. Для тестов callback url можно указать на localhost. У меня было указано localhost:3000.
Сохранить куда-то ClientID и ClientSecret. Их можно найти в учетных данных во вкладке слева.
Подготовительные действия закончены, можно приступать к написанию кода.
Для авторизации надо поднять простенький http сервер и сделать несколько страниц. Начать можно со статьи на golang.org – Writing Web Applications. Приятно удивила обширность функционала в стандартных пакетах. Для большинства задач можно не импортировать сторонние библиотеки, а использовать только базовые пакеты и утилиты в комплекте с языком.
Отдельная боль – пакетный менеджер. Его просто нет. На Github можно найти список пакетных менеджеров сторонних разработчиков и выбрать на свой вкус. В статье не буду подробно на нем останавливаться, потому что не использовал и не могу толком сказать какие плюсы и минусы у каждого. С кем я общался – советуют gvt.
К коду. Простой http сервер:
package main
import (
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Frintf(w, "Hello from Index")
}
func main() {
http.HandleFunc("/", indexHandler)
http.HandleAndServe(":3000", nil)
}
Слушаем порт 3000 и по адресу http://localhost:3000/ – отдаем текстом Hello from Index
.
Для OAuth 2.0 нам нужно сделать редирект на страницу в Google API, чтобы пользователь разрешил приложению делать разные манипуляции с данными и получить обратно в запросе access_token, который будем использовать в запросах к API.
func authHandler(w http.ResponseWriter, r *http.Request) {
}
func authHandler(w http.ResponseWriter, r *http.Request) {
}
func main() {
http.HandleFunc("/authorize", authHandler)
http.HandleFunc("/connect", connectHandler)
http.HandleFunc("/", indexHandler)
http.HandleAndServe(":3000", nil)
}
На главной странице выведем ссылку на /authorize, откуда сделаем редирект в Google, чтобы пользователь выдал нам права. На /connect будем ждать ответа от Google с кодом авторизации, по которому получим access_token.
После получения токена нужно его куда-то сохранить, для этого вполне сойдет сессия. В production такое делать не стоит и надо использовать базу, но я накидывал прототип, поэтому сессии подойдут.
Для работы с токеном будем пользоваться библиотекой от Google oauth2, а для работы с сессиями библиотекой sessions из GorillaToolkit.
Напомню, что в этой статье не описана начальная настройка окружения GO, предполагаю, что с этим можно справиться самостоятельно после прочтения нескольких статей. Поэтому укачиваем библиотеки себе в workspace с помощью команд:
go get github.com/gorilla/sessions
go get golang.org/x/oauth2
Импортируем их в проекте.
import(
"net/http"
"github.com/gorilla/sessions"
"golang.rog/x/oauth2"
"golang.rog/x/oauth2/google"
)
...
В библиотеках есть методы для получения конфига из json файла, но для прототипа можно забить это все руками.
import(
...
)
const (
clientID = "your_client_id"
clientSecret = "your_client_secret"
applicationName = "your_application_name"
)
var store = sessions.NewCookieStore([]byte("gaIntegrationStore"))
var config = &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{"https://www.googleapis.com/auth/analytics", "https://www.googleapis.com/auth/analytics.readonly"},
Endpoint: google.Endpoint,
RedirectUrl: "http://localhost:3000/connect",
}
oauth2 – это набор методов для работы с OAuth 2.0 авторизацией, а в папках репозитория лежат в основном Endpoints для разных провайдеров – Google, Slack и т.д..
Сессии мы делаем через cookie без сохранения где-либо еще. При желании их можно хранить в memcache, redis и т.д..
Далее разберемся с шаблонами и отрендерим простую страницу с ссылкой для авторизации. Для этого положим в папку с проектом файл index.html
:
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css" integrity="sha384-y3tfxAZXuh4HwSYylfB+J125MxIs6mR5FOHamPBG064zB+AFeWH94NdvaCBm8qnd" crossorigin="anonymous">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-4">
<a href="/authorize" class="btn btn-primary"> Authorize GA </a>
</div>
<div class="col-sm-4">
</div>
<div class="col-sm-4">
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js" integrity="sha384-vZ2WRJMwsjRMW/8U7i6PWi6AlO1L79snBrmgiDpgIWJ82z8eA5lenwvxbMV1PAh7" crossorigin="anonymous"></script>
</body>
</html>
Естественно подключили вездесущий Bootstrap. Далее отрендерим шаблон, обратимся к сессии и сделаем логирование:
import(
"log"
"html/template"
...
)
...
func indexHandler(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "GASession")
if err != nil {
log.Println("IH.Error while retrieving session: ", err)
}
log.Println("Session: ", session)
t, _ := template.ParseFiles("index.html")
t.Execute(w, struct{}{})
}
Что мы сделали – получили сессию, считали index.html и отрендерили страницу. С сессией пока ничего не делаем, понадобится дальше.
Теперь надо отправить пользователя на страницу сервиса Google, где он даст нашему приложения необходимые права.
func authorizeHandler(w http.ResponseWriter, r *http.Request) {
url := config.AuthCodeURL("state")
http.Redirect(w, r, url, http.StatusFound)
}
После того как пользователь нажмет кнопку "Allow", сервис переправит его на урл, который мы указали в настройках /connect, поэтому напишу реализацию обработчика этого действия.
func connectHandler(w http.ResponseWriter, r *http.Request) {
code := r.FormValue("code")
log.Println("AH.Code: ", code)
tok, err := config.Exchange(oauth2.NoContext, code)
if err != nil {
log.Fatalln("AH.Error: ", err)
}
log.Println("AH.AccessToken: ", tok)
session, err := store.Get(r, "GASession")
if err != nil {
log.Println("AH.Error :", err)
}
session.Values["access_token"] = tok
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusFound)
}
Разберу подробнее.
code := r.FormValue("code")
Когда пользователя средиректит обратно к нам, в параметрах запроса в ссылке будет поле code
– проверочный код, по которому мы получим access_token с помощью метода Exchange
из библиотеки oauth2
.
tok, err := config.Exchange(oauth2.NoContext, code)
К сожалению складывать все подряд в объект сессии у нас не получиться. Чтобы сохранить там что-то отличное от стандартных типов нужно зарегистрировать тип, который мы хотим сохранить. Регистрируется он в функции init()
. Она вызывается после всех определений констант и переменных и до вызова функции main()
.
func init() {
gob.Register(&oauth2.Token{})
}
Далее сохраним объект сессии до отправки ответа:
func connectHandler(w http.ResponseWriter, r *http.Request) {
...
session.Values["access_token"] = tok
session.Save(r, w)
log.Println("AH.Session: ", session)
http.Redirect(w, r, "/", http.StatusFound)
}
Теперь в indexHandler
мы можем обратиться к сессии и вытащить access_token
для дальнейшей работы. В других обработчиках мы также можем обращаться к access_token
и использовать его для инициализации клиента для запросов в API Google Analytics.
На этом закончу первую часть. Во второй расскажу как использовать библиотеку google.golang.org/api/
для запросов в API Google.