-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcrawler.go
306 lines (269 loc) · 10.2 KB
/
crawler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/chromedp/cdproto/browser"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/kb"
"github.com/dadosjusbr/status"
"github.com/xuri/excelize/v2"
)
const (
direitosPessoaisXPATH = "/html/body/div[5]/div/div[31]/div[2]/table/tbody/tr/td"
indenizacoesXPATH = "/html/body/div[5]/div/div[25]/div[2]/table/tbody/tr/td"
verbasXPATH = "/html/body/div[5]/div/div[28]/div[2]/table/tbody/tr/td"
controleXPATH = "/html/body/div[5]/div/div[55]/div[2]/table/tbody/tr/td"
contrachequeXPATH = "/html/body/div[5]/div/div[49]/div[2]/table/tbody/tr/td"
botaoMesAnoXPATH = "/html/body/div[5]/div/div[49]/div[2]/div[1]/div[1]/div[2]/div/div[6]/div[2]/div/div[1]/div"
)
type crawler struct {
downloadTimeout time.Duration
collectionTimeout time.Duration
timeBetweenSteps time.Duration
court string
year string
month string
output string
}
func (c crawler) crawl() ([]string, error) {
// Pegar variáveis de ambiente
// Chromedp setup.
log.SetOutput(os.Stderr) // Enviando logs para o stderr para não afetar a execução do coletor.
alloc, allocCancel := chromedp.NewExecAllocator(
context.Background(),
append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true), // mude para false para executar com navegador visível.
chromedp.NoSandbox,
chromedp.DisableGPU,
)...,
)
defer allocCancel()
ctx, cancel := chromedp.NewContext(
alloc,
chromedp.WithLogf(log.Printf), // remover comentário para depurar
)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, c.collectionTimeout)
defer cancel()
log.Printf("Realizando seleção (%s/%s/%s)...", c.court, c.month, c.year)
if err := c.selectionaOrgaoMesAno(ctx); err != nil {
verificaErro(err)
}
log.Printf("Seleção realizada com sucesso!\n")
// NOTA IMPORTANTE: os prefixos dos nomes dos arquivos tem que ser igual
// ao esperado no parser CNJ.
// Planilha de controle
ceFname := c.downloadFilePath("controle-de-arquivos")
log.Printf("Fazendo download do controle de arquivos (%s)...", ceFname)
if err := c.clicaAba(ctx, controleXPATH); err != nil {
status.ExitFromError(err)
}
if err := c.exportaExcel(ctx, ceFname); err != nil {
status.ExitFromError(err)
}
log.Printf("Download do controle de arquivos realizado com sucesso!\n")
// Verificando se tem dados
if err := validate(c); err != nil {
status.ExitFromError(err)
}
// Contracheque
cqFname := c.downloadFilePath("contracheque")
log.Printf("Fazendo download do contracheque (%s)...", cqFname)
// Selecionando contracheque...
if err := c.clicaAba(ctx, contrachequeXPATH); err != nil {
status.ExitFromError(err)
}
// Clicando no mês/ano de referência - para ter acesso ao cargo e lotação.
if err := c.clicaAba(ctx, botaoMesAnoXPATH); err != nil {
status.ExitFromError(err)
}
if err := c.exportaExcel(ctx, cqFname); err != nil {
status.ExitFromError(err)
}
// Verificando se os dados estão agregados
if err := validateSourceFile(cqFname); err != nil {
status.ExitFromError(err)
}
log.Printf("Download realizado com sucesso!\n")
// Direitos pessoais
dpFname := c.downloadFilePath("direitos-pessoais")
log.Printf("Fazendo download dos direitos pessoais (%s)...", dpFname)
if err := c.clicaAba(ctx, direitosPessoaisXPATH); err != nil {
status.ExitFromError(err)
}
if err := c.exportaExcel(ctx, dpFname); err != nil {
status.ExitFromError(err)
}
log.Printf("Download realizado com sucesso!\n")
// Indenizações
iFname := c.downloadFilePath("indenizacoes")
log.Printf("Fazendo download das indenizações (%s)...", iFname)
if err := c.clicaAba(ctx, indenizacoesXPATH); err != nil {
status.ExitFromError(err)
}
if err := c.exportaExcel(ctx, iFname); err != nil {
status.ExitFromError(err)
}
log.Printf("Download realizado com sucesso!\n")
// Verbas
deFname := c.downloadFilePath("direitos-eventuais")
log.Printf("Fazendo download dos direitos eventuais (%s)...", deFname)
if err := c.clicaAba(ctx, verbasXPATH); err != nil {
status.ExitFromError(err)
}
if err := c.exportaExcel(ctx, deFname); err != nil {
status.ExitFromError(err)
}
log.Printf("Download dos direitos eventuais realizado com sucesso!\n")
// Retorna caminhos completos dos arquivos baixados.
return []string{cqFname, dpFname, deFname, iFname, ceFname}, nil
}
func (c crawler) downloadFilePath(prefix string) string {
return filepath.Join(c.output, fmt.Sprintf("%s-%s-%s-%s.xlsx", prefix, c.court, c.year, c.month))
}
func (c crawler) selectionaOrgaoMesAno(ctx context.Context) error {
const (
pathRoot = "/html/body/div[2]/input"
baseURL = "https://paineis.cnj.jus.br/QvAJAXZfc/opendoc.htm?document=qvw_l%2FPainelCNJ.qvw&host=QVS%40neodimio03&anonymous=true&sheet=shPORT63Relatorios"
)
return chromedp.Run(ctx,
chromedp.Navigate(baseURL),
chromedp.WaitVisible(`//*[@title='Tribunal']`, chromedp.BySearch),
chromedp.Sleep(c.timeBetweenSteps),
// Seleciona o orgão
chromedp.Click(`//*[@title='Tribunal']//*[@title='Pesquisar']`, chromedp.BySearch, chromedp.NodeReady),
chromedp.WaitVisible(pathRoot),
chromedp.SetValue(pathRoot, c.court),
chromedp.SendKeys(pathRoot, kb.Enter, chromedp.NodeSelected),
chromedp.Sleep(c.timeBetweenSteps),
// Seleciona o ano
chromedp.Click(`//*[@title='Ano']//*[@title='Pesquisar']`, chromedp.BySearch, chromedp.NodeVisible),
chromedp.WaitVisible(pathRoot),
chromedp.SetValue(pathRoot, c.year),
chromedp.SendKeys(pathRoot, kb.Enter),
chromedp.Sleep(c.timeBetweenSteps),
// Seleciona o mes
chromedp.Click(`//*[@title='Mês Referencia']//*[@title='Pesquisar']`, chromedp.BySearch, chromedp.NodeVisible),
chromedp.WaitVisible(pathRoot),
chromedp.SetValue(pathRoot, c.month),
chromedp.SendKeys(pathRoot, kb.Enter),
chromedp.Sleep(c.timeBetweenSteps),
// Altera o diretório de download
browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllowAndName).
WithDownloadPath(c.output).
WithEventsEnabled(true),
)
}
// exportaExcel clica no botão correto para exportar para excel, espera um tempo para download renomeia o arquivo.
func (c crawler) exportaExcel(ctx context.Context, fName string) error {
err := chromedp.Run(ctx,
chromedp.Click(`//*[@title='Enviar para Excel']`, chromedp.BySearch, chromedp.NodeVisible),
chromedp.Sleep(c.timeBetweenSteps),
)
if err != nil {
return status.NewError(status.ConnectionError, fmt.Errorf("erro clicando no botão de download: %v", err))
}
time.Sleep(c.downloadTimeout)
if err := nomeiaDownload(c.output, fName); err != nil {
status.ExitFromError(err)
}
if _, err := os.Stat(fName); os.IsNotExist(err) {
return status.NewError(status.DataUnavailable, fmt.Errorf("download do arquivo de %s não realizado", fName))
}
return nil
}
// clicaAba clica na aba referenciada pelo XPATH passado como parâmetro.
// Também espera até o título Tribunal estar visível.
func (c crawler) clicaAba(ctx context.Context, xpath string) error {
return chromedp.Run(ctx,
chromedp.Click(xpath),
chromedp.Sleep(c.timeBetweenSteps),
)
}
// nomeiaDownload dá um nome ao último arquivo modificado dentro do diretório
// passado como parâmetro nomeiaDownload dá pega um arquivo
func nomeiaDownload(output, fName string) error {
// Identifica qual foi o ultimo arquivo
files, err := os.ReadDir(output)
if err != nil {
return status.NewError(status.SystemError, fmt.Errorf("erro lendo diretório %s: %v", output, err))
}
var newestFPath string
var newestTime int64 = 0
for _, f := range files {
fPath := filepath.Join(output, f.Name())
fi, err := os.Stat(fPath)
if err != nil {
return status.NewError(status.SystemError, fmt.Errorf("erro obtendo informações sobre arquivo %s: %v", fPath, err))
}
currTime := fi.ModTime().Unix()
if currTime > newestTime {
newestTime = currTime
newestFPath = fPath
}
}
// Renomeia o ultimo arquivo modificado.
if err := os.Rename(newestFPath, fName); err != nil {
return status.NewError(status.DataUnavailable, fmt.Errorf("sem planilhas baixadas"))
}
return nil
}
func verificaErro(err error) {
// Verificamos se o erro é de conexão ou 'context deadline exceeded',
// caso contrário, será retornado status Unknown (6)
if strings.Contains(err.Error(), "ERR_CONNECTION_CLOSED") || strings.Contains(err.Error(), "ERR_CONNECTION_RESET") {
err = status.NewError(status.ConnectionError, err)
} else if strings.Contains(err.Error(), "context deadline exceeded") {
err = status.NewError(status.DeadlineExceeded, err)
}
status.ExitFromError(err)
}
func validate(c crawler) error {
xlsx := filepath.Join(c.output, fmt.Sprintf("controle-de-arquivos-%s-%s-%s.xlsx", c.court, c.year, c.month))
file, err := excelize.OpenFile(xlsx)
if err != nil {
return status.NewError(status.InvalidFile, fmt.Errorf("erro abrindo planilha: %w", err))
}
rows, err := file.GetRows("Sheet1")
if err != nil {
return status.NewError(status.InvalidFile, fmt.Errorf("erro lendo planilha: %w", err))
}
filenames := []string{
fmt.Sprintf("%s_%s_%s.xls", c.court, c.month, c.year[2:]),
fmt.Sprintf("%s_%s_%s.xls", c.court, strings.TrimLeft(c.month, "0"), c.year[2:]),
}
var ok bool
for _, row := range rows {
f := strings.ToLower(row[2])
if strings.Contains(f, filenames[0]) || strings.Contains(f, filenames[1]) {
ok = true
}
}
if !ok {
return status.NewError(status.DataUnavailable, fmt.Errorf("não há planilhas a serem baixadas"))
}
return nil
}
// Esta função verifica se os dados estão agregados
func validateSourceFile(file string) error {
f, err := excelize.OpenFile(file)
if err != nil {
return status.NewError(status.InvalidFile, fmt.Errorf("erro abrindo planilha: %w", err))
}
rows, err := f.GetRows("Sheet1")
if err != nil {
return status.NewError(status.InvalidFile, fmt.Errorf("erro lendo planilha: %w", err))
}
// Quando os dados estão agregados, a planilha de contracheques possui apenas 2 linhas: o cabeçalho e 1 linha com dados agregados.
// Além disso, quando os dados estão agregados, o órgão tende a colocar '0' como nome do magistrado.
// Aqui verificamos se a planilha possui apenas 1 linha de dados ou se o nome do magistrado é '0'
if len(rows) == 2 || rows[1][1] == "0" {
return status.NewError(status.DataUnavailable, fmt.Errorf("dados agregados"))
}
return nil
}