-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathVerleihnixCrawler.js
302 lines (253 loc) · 11.2 KB
/
VerleihnixCrawler.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
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
const net = require( 'net' );
const Crawler = require( 'crawler' );
const Parser = require( './Parser' );
const Writer = require( './Writer' );
class VerleihnixCrawler {
/**
* Hier initiieren wir den Crawler und setzen die Konfiguration als lokale
* Variablen in der Instanz.
*
* @param {Record<string, any>} config
* @param {Parser|null} parser
* @param {Writer|null} writer
*/
constructor(
config,
parser = null,
writer = null,
) {
// Wir mergen die Konfiguration mit unseren Standardwerten. Dadurch muss
// nicht jede Option in der Konfigurationsdatei angegeben werden, wenn
// sie wahrscheinlich sowieso nicht geändert werden muss.
this._config = {
connectionTimeout: 10_000,
targetScheme: 'https',
startUrl: '/',
crawlerOptions: {},
...config,
};
// Wir übernehmen die Instanzen die übergeben wurden, oder erstellen
// neue mit unserer Konfiguration.
this._parser = parser || new Parser( this._config );
this._writer = writer || new Writer( this._config );
}
/**
* Die start-Methode ist die einzige, die von außerhalb des Moduls
* aufgerufen werden soll. Sie prüft alle Ausgangsbedingungen und startet
* anschließend den Crawler.
*
* @return {Promise<void>}
*/
async start() {
this._writeLog( 'Starting Crawler', '================' );
// Wir bereiten den Writer vor. Dabei wird geprüft, ob er die Ausgabe
// überhaupt schreiben kann.
await this._writer.initialize();
// Dann prüfen wir, ob der Server überhaupt erreichbar ist
await this._checkTargetHostIsUp();
// Wir erstellen einen Crawler, und übergeben ihm unser Handler-Callback
// mit dem wir die Antworten bearbeiten.
await this._createCrawler( this._handle.bind( this ) );
this._writeLog( 'Stopping Crawler', '================' );
}
/**
* Die Handle-Methode arbeitet Crawler-Antworten ab. Dabei muss immer eine
* oder mehrere neue URL in die Warteschlange eingereiht werden, damit der
* Crawler weiterläuft - er braucht quasi weiter Treibstoff zum verfeuern.
*
* @param {Crawler} crawler
* @param {CrawlerRequestResponse} response
* @return {Promise<void>}
* @private
*/
async _handle( crawler, response ) {
this._writeLog( 'Passing response to parser' );
// Wir starten den Parser, der die Antwort auswertet
const { result, nextUrls } = await this._parser.parse(
response,
);
// Wir haben ein Ergebnis, also schreiben wir es in die Ausgabe.
if ( result ) {
this._writeLog( 'Passing result to writer' );
await this._writer.write( result );
}
// Wir haben alle Seiten gecrawlt! Es gibt also nichts mehr zu holen
// und wir können die Crawler-Warteschlange beenden. Dazu rufen wir
// das done-Callback auf, und beenden die Ausführung.
if ( !nextUrls || nextUrls.length === 0 ) {
this._writeLog( 'No further URLs in response' );
return;
}
this._writeLog( 'Adding next URL to queue' );
// Wir haben weitere URLs vom Parser erhalten, also legen wir sie in die
// Warteschlange; der Crawler bearbeitet sie dann im nächsten Durchlauf.
nextUrls.forEach( url => crawler.queue( url ) );
}
/**
* Prüft, ob der Zielhost erreichbar ist.
*
* @return {Promise<void>}
* @private
*/
async _checkTargetHostIsUp() {
if ( !this._config.targetHost ) {
throw new Error(
'Target host not configured: There is no "targetHost" key in ' +
'the configuration file. Add the target host to your config ' +
'file and try running the crawler again.\nThe target host ' +
'should be the host part of the website URL, without scheme ' +
'and path: For "https://www.example.com/foo", it is ' +
'"www.example.com".',
);
}
return new Promise( ( resolve, reject ) => {
this._writeLog(
`Attempting to connect to ${ this._config.targetHost }:443`,
);
// Wir stellen einen Timer: Sobald er abgelaufen ist und wir noch
// keine Antwort vom Zielserver erhalten haben, brechen wir ab: Der
// Server ist nicht erreichbar.
const timer = setTimeout(
// Erst schließen wir den Netzwerk-Socket, dann markieren wir
// das Promise als fehlgeschlagen.
() => socket.end() && reject( 'Connection Timeout' ),
this._config.connectionTimeout,
);
// Ab hier stoppen wir die Zeit für den Verbindungsaufbau.
const startTime = new Date();
// Wir öffnen einen Netzwerk-Socket zum Zielserver auf Port 443: Das
// ist standardmäßig der HTTPS-Port für Webserver.
const socket = net.createConnection(
443,
this._config.targetHost,
() => {
this._writeLog(
`Received response from target host ` +
`after ${ ( new Date() ) - startTime }ms`,
);
// Wir löschen den Timeout, damit er die Verbindung nicht
// nachträglich abbrechen kann
clearTimeout( timer );
// Wir markieren das Promise als erfolgreich
resolve();
// Wir schließen den Socket
socket.end();
} );
// Wenn auf dem Socket ein Fehler auftritt, brechen wir die
// Verbindung ab und markieren das Promise als fehlgeschlagen: Wir
// können nichts mehr tun.
socket.on(
'error',
error => {
this._writeLog( `Could not connect: ${ error }` );
clearTimeout( timer );
reject( error );
},
);
} );
}
/**
* Erstellt eine Crawler-Instanz und startet das Crawling.
*
* @param {function(crawler: Crawler, response: CrawlerRequestResponse )} callback
* @return {Promise<void>}
* @private
*/
_createCrawler( callback ) {
// Hier verpacken wir den kompletten Crawling-Prozess in ein Promise.
// Dadurch können wir auf die vollständige Abarbeitung aller Links
// warten und Code ausführen, wenn wir fertig sind.
return new Promise( ( resolve, reject ) => {
this._writeLog( 'Creating crawler instance' );
// Wir zeichnen auf, wie viele URLs wir schon gecrawlt haben. Das
// ist aber nur als interessante Information gedacht.
let counter = 0;
// Wir erstellen eine Crawler-Instanz. Damit können wir den Prozess
// flexibel steuern, auch während er schon gestartet ist.
const crawler = new Crawler( {
// Wir übernehmen die Optionen aus der Konfigurationsdatei. Mit
// dem spread-Operator (...) mergen wir die Optionen in das
// aktuelle Objekt, überschreiben aber den Wert von "callback",
// wenn er angegeben ist.
...this._config.crawlerOptions,
// Anstatt das Callback von unserem Parameter direkt zu
// übergeben, verpacken wir es in unser eigenes, übergeordnetes
// Callback, um das Handling etwas zu vereinfachen.
// Diese Funktion wird für jede gecrawlte URL aufgerufen, wenn
// wir eine Antwort vom Server erhalten haben.
callback: async ( error, response, done ) => {
this._writeLog(
`Received response for "${ response.request.uri.href }"`,
);
// Wenn ein Fehler aufgetreten ist, bringt es erst gar
// nichts unser callback aufzurufen - wir brechen direkt ab.
if ( error ) {
this._writeLog(
`An error occurred during crawling: ${ error }`,
);
return reject( error );
}
// Wir zählen den Zähler hoch: Die Antwort muss erfolgreich
// gewesen sein, sonst wären wir im Error-Handler angekommen
counter++;
this._writeLog(
`+++++++++++++++++++++ Request ${ counter } +++++++++++++++++++++++++++`,
);
// Wenn kein Fehler aufgetreten ist, rufen wir das
// ursprüngliche Callback auf und übergeben ihm die
// Crawler-Instanz und die Antwort vom Server.
await callback( crawler, response );
// Wir melden uns beim Crawler: Wir haben die aktuelle
// Antwort bearbeitet und sind fertig.
done();
// Wenn der Crawler keine weiteren Links mehr hat, können
// wir das Promise erfüllen: Wir sind fertig.
if ( crawler.queueSize === 0 ) {
return resolve();
}
},
} );
// Der Crawler funktioniert, indem er eine Warteschlange von URLs
// abarbeitet. In unserer Implementierung reihen wir immer neue
// Seiten in die Warteschlange ein, wenn wir sie auf den
// Ergebnisseiten finden. Um diesen Prozess zu starten, übergeben
// wir dem Crawler hier die Start-URL, auf der wir mit dem Crawling
// anfangen, und von der wir alle folgenden URLs ableiten.
const startUrl = this._createUrlForPath(
this._config.startUrl,
);
// Indem wir dem Crawler die Stat-URL übergeben, wird die
// Warteschlange gestartet. Go!
crawler.queue( startUrl.toString() );
this._writeLog( `Queued start URL "${ startUrl }"` );
} );
}
/**
* Baut eine URL-Instanz zum Zielserver aus einem relativen Pfad.
*
* @param {string} path
* @return {URL}
* @private
*/
_createUrlForPath( path ) {
const hostname = this._config.targetHost;
const scheme = this._config.targetScheme;
return new URL( path, `${ scheme }://${ hostname }` );
}
/**
* Schreibt eine Zeile auf die Konsole.
*
* @param lines
* @private
*/
_writeLog( ...lines ) {
const date = new Date();
const timestamp = date.toDateString() + ' ' +
date.getUTCHours() + ':' +
date.getUTCMinutes() + ':' +
date.getUTCSeconds() + '.' +
date.getUTCMilliseconds();
lines.map( line => console.log( `${ timestamp }\t${ line }` ) );
}
}
module.exports = VerleihnixCrawler;