This repository has been archived by the owner on Jul 26, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.js
260 lines (219 loc) · 8.74 KB
/
main.js
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
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const downloadPath = path.join(__dirname, 'ifdata');
const fileDados = path.join(downloadPath, 'dados.csv');
// timeout que a rotina espera o site do Bacen carregar, realmente precisa de
// um valor grande, por que durante o dia é comum ter que esperar minutos para
// carregar os dados.
const defaultTimeout = 60000 * 5;
// por padrão o programa verifica somente a quantidade de datas que estão
// configuradas nesta constante, se quiser baixar tudo do site, só colocar
// um valor bem elevado aqui!
const maxDatasVerificadas = 8;
(async () => {
if (!dirExist(downloadPath)) {
fs.mkdirSync(downloadPath);
}
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto('https://www3.bcb.gov.br/ifdata/index.html');
// Hack: configura o local de download dos dados, puppeteer não tem API para lidar com isso
// então acessamos objetos internos e mandamos um comando para interface do chrome para
// definir o download behavior.
await page._client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: downloadPath,
});
var dataOpen = false; // flags que indicam se os combos estão abertos na página
var tipoOpen = false;
var relaOpen = false;
// carrega as datas disponíveis, primeira interação com a pagina o site do Bacen carrega os dados...
await page.click(`button#btnDataBase`);
dataOpen = true;
// espera carregar os dados
await page.waitForSelector('#ulDataBase > li', { visible: true, timeout: defaultTimeout });
var datas = [];
{
const nodes = await page.$$(`#ulDataBase > li`);
for (let i = 0; i < nodes.length; i++) {
const text = await nodes[i].evaluate(e => e.innerText);
datas.push([i + 1, text]);
}
}
var datasVerificadas = maxDatasVerificadas;
while (datas.length && --datasVerificadas >= 0) {
const data = datas.shift();
if (!dataOpen) {
await page.click('button#btnDataBase');
dataOpen = true;
}
await page.click(`#ulDataBase > li:nth-child(${data[0]})`, {
visible: true
});
dataOpen = false;
await page.waitForTimeout(300);
// tipos
var tipos = [];
{
await page.click('button#btnTipoInst');
tipoOpen = true;
const nodes = await page.$$(`#ulTipoInst > li`);
for (let i = 0; i < nodes.length; i++) {
const text = await nodes[i].evaluate(e => e.innerText);
tipos.push([i + 1, text]);
}
}
while (tipos.length) {
const tipo = tipos.shift();
if (!tipoOpen) {
await page.click('button#btnTipoInst');
tipoOpen = true;
}
await page.click(`#ulTipoInst > li:nth-child(${tipo[0]})`, {
visible: true
});
tipoOpen = false;
await page.waitForTimeout(300);
// relatórios
var relatorios = [];
{
await page.click('button#btnRelatorio');
relaOpen = true;
const nodes = await page.$$(`#ulRelatorio > li`);
for (let i = 0; i < nodes.length; i++) {
const text = await nodes[i].evaluate(e => e.innerText);
relatorios.push([i + 1, text]);
}
}
while (relatorios.length) {
const relatorio = relatorios.shift();
if (!relaOpen) {
await page.click('button#btnRelatorio');
relaOpen = true;
}
const yyyymm = data[1].substr(3, 4) + data[1].substr(0, 2);
const fileName = path.join(downloadPath, yyyymm + '_' + normalizaTexto(tipo[1]) + '_' + normalizaTexto(relatorio[1]) + '.csv');
if (!fileExist(fileName)) {
try {
fs.unlinkSync(fileDados);
} catch { }
// seleciona o relatório no combo
await page.click(`#ulRelatorio > li:nth-child(${relatorio[0]})`, {
visible: true
});
relaOpen = false;
// espera o export aparecer na rela e clica nele..
const csv = await page.waitForSelector("a#aExportCsv", {
visible: true,
timeout: defaultTimeout
});
await csv.click();
// como eles salvam o arquivo via blob, é praticamente instantâneo, mas coloquei
// 1 segundo para "garantir" que o "download" tenha completado.
await page.waitForTimeout(1000);
if (fileExist(fileDados)) {
processaDownload(fileName);
} else {
console.error(`Não foi possível localizar o arquivo: ${fileDados}`);
}
}
}
}
}
console.log("Done");
await browser.close();
})();
function processaDownload(outputFile) {
try {
const csv = fs.readFileSync(fileDados, { 'encoding': 'utf8' });
const lines = csv.split('\r\n');
var count = 0;
if (lines.length > 6) {
var header = lines[0].split(';');
// Hack: Além dos programadores que fizeram o site do Bacen não seguirem o formato padrão de CSV,
// eles tiveram a brilhante ideia de colocar agrupamentos de headers dentro do CSV, o que faz
// com que o título da coluna no CSV estar na primeira linha OU na terceira, dependendo da
// quantidade de agrupamentos que os cabeçalhos possuem... aqui lido com essa aberração! :/
var start = 1;
for (var r = 1; r < 6; r++) {
const c = lines[r].split(';');
if (c[0] == '') {
start++;
} else {
break
}
}
if (start > 1) {
for (var c = 0; c < header.length - 1; c++) {
if (header[c] == '' || header[c + 1] == '') {
for (let r = start - 1; r > 0; r--) {
const cols = lines[r].split(';');
if (cols.length > c && cols[c] != '') {
header[c] = cols[c];
break
}
}
}
}
}
if (header[header.length-1] == '') {
header.pop();
}
// paramos de lidar com a aberração...
const file = fs.createWriteStream(outputFile, { encoding: 'utf8' });
file.write(`${header.join(';')}\r\n`);
try {
for (let line = start; line < lines.length; line++) {
if (lines[line].split(';').length != header.length) {
// quando a quantidade de colunas dentro da linha é diferente dos headers,
// é por que chegamos nos rodapés do CSV, onde eles colocam informações de
// agrupamentos, fugindo do padrão, então ignoramos o resto de "lixo"
break;
}
// remove os NI/NA/NA% dos campos numéricos
const data = lines[line].replace(/;N[IA][%]*(?=[;|\n])/g, ';');
file.write(`${data}\r\n`);
count++;
}
} catch (e) {
console.error(`write file error: ${e}`);
} finally {
// console.log(`Write ${count} rows in ${outputFile}`);
file.end()
}
} else {
console.error('O arquivo baixado não contem registros');
}
} catch (e) {
console.log(`erro no tratamento do arquivo: ${e}`);
} finally {
try {
fs.unlinkSync(fileDados);
} catch { }
}
}
function dirExist(fileName) {
try {
return fs.statSync(fileName).isDirectory();
} catch {
return false;
}
}
function fileExist(fileName) {
try {
return fs.statSync(fileName).isFile();
} catch {
return false;
}
}
function normalizaTexto(str) {
return str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, '')
.replace(/[\s+]/g, '_')
.toLowerCase()
;
}