Perayap web (atau laba-laba web) adalah bagian penting dari mesin pencari untuk merayapi halaman web untuk memasukkan informasi tentang mereka ke dalam basis data, terutama untuk pengindeksan lebih lanjut. Mesin pencari (Google, Yandex, Bing), serta produk SEO (SEMrush, MOZ, ahrefs) dan tidak hanya memiliki hal seperti itu. Dan hal ini cukup menarik: baik dari segi kasus potensial dan penggunaan, dan untuk implementasi teknis.

Dengan artikel ini, kita akan mulai membuat
sepeda crawler Anda
secara iteratif , menganalisis banyak fitur dan menemui jebakan rapat. Dari fungsi rekursif sederhana hingga layanan yang skalabel dan dapat diperluas. Pasti menarik!
Intro
Iteratif - itu berarti bahwa pada akhir setiap rilis, versi "siap pakai" produk yang siap pakai diharapkan dengan batasan, fitur, dan antarmuka yang disepakati.
Node.js dan
JavaScript dipilih sebagai platform dan bahasa, karena sederhana dan tidak sinkron. Tentu saja, untuk pengembangan industri, pilihan basis teknologi harus didasarkan pada persyaratan bisnis, harapan, dan sumber daya. Sebagai demonstrasi dan prototipe, platform ini sama sekali bukan apa-apa (IMHO).
Ini adalah crawler saya. Ada banyak perayap seperti itu, tetapi ini adalah milikku.
Perayap saya adalah sahabat saya.
Implementasi crawler adalah tugas yang cukup populer dan dapat ditemukan bahkan pada wawancara teknis. Sebenarnya ada banyak solusi siap pakai (
Apache Nutch ) dan tulisan sendiri untuk kondisi yang berbeda dan dalam banyak bahasa. Karena itu, setiap komentar dari pengalaman pribadi dalam pengembangan atau penggunaan dipersilahkan dan akan menarik.
Pernyataan masalah
Tugas untuk implementasi pertama (awal) dari crawler
tyap-blooper kami adalah sebagai berikut:
One-Two Crawler 1.0
Tulis skrip perayap yang melewati tautan <a href /> internal situs kecil (hingga 100 halaman). Akibatnya, berikan daftar URL halaman dengan kode yang diterima dan peta tautannya. Aturan robots.txt dan atribut tautan rel = nofollow diabaikan.
Perhatian! Mengabaikan aturan
robots.txt adalah ide yang buruk karena alasan yang jelas. Kami akan menebus kelalaian ini di masa depan. Sementara itu, tambahkan parameter batas yang membatasi jumlah halaman yang akan dirayapi sehingga tidak menghentikan DoS dan coba situs eksperimental (lebih baik menggunakan "situs hamster" pribadi Anda untuk eksperimen).
Implementasi
Untuk yang tidak sabar,
berikut adalah sumber solusi ini.
- Klien HTTP (S)
- Opsi jawaban
- Ekstraksi Tautan
- Persiapan tautan dan penyaringan
- Normalisasi URL
- Algoritma fungsi utama
- Hasil kembali
1. Klien HTTP (S)
Hal pertama yang perlu kita lakukan adalah mengirimkan permintaan dan menerima tanggapan melalui HTTP dan HTTPS. Di node.js ada dua klien yang cocok untuk ini. Tentu saja, Anda dapat mengambil
permintaan klien yang sudah jadi , tetapi untuk tugas kami itu sangat berlebihan: kita hanya perlu mengirim permintaan GET dan mendapatkan respons dengan tubuh dan header.
API kedua klien yang kami butuhkan identik, kami akan membuat peta:
const clients = { 'http:': require('http'), 'https:': require('https') };
Kami menyatakan
pengambilan fungsi sederhana, satu-satunya parameter yang akan menjadi URL
absolut dari string sumber daya web yang diinginkan. Menggunakan
modul url, kami akan mengurai string yang dihasilkan menjadi objek URL. Objek ini memiliki bidang dengan protokol (dengan titik dua), dimana kami akan memilih klien yang sesuai:
const url = require('url'); function fetch(dst) { let dstURL = new URL(dst); let client = clients[dstURL.protocol]; if (!client) { throw new Error('Could not select a client for ' + dstURL.protocol); }
Selanjutnya, gunakan klien yang dipilih dan bungkus hasil dari fungsi
ambil dalam janji:
function fetch(dst) { return new Promise((resolve, reject) => {
Sekarang kita dapat menerima respons secara tidak sinkron, tetapi untuk saat ini kita tidak melakukan apa-apa dengannya.
2. Opsi Jawaban
Untuk menjelajah situs, cukup memproses 3 opsi jawaban:
- OK - Kode Status 2xx diterima. Diperlukan untuk menyimpan badan tanggapan sebagai hasil untuk pemrosesan lebih lanjut - mengekstraksi tautan baru.
- REDIRECT - Kode Status 3xx diterima. Ini adalah pengalihan ke halaman lain. Dalam hal ini, kita akan membutuhkan tajuk respons lokasi , dari mana kita akan mengambil satu tautan "keluar".
- NO_DATA - Semua kasus lainnya: 4xx / 5xx dan 3xx tanpa header Lokasi . Tidak ada tempat untuk melangkah lebih jauh ke perayap kami.
Fungsi
ambil akan menyelesaikan respons yang diproses yang menunjukkan jenisnya:
const ft = { 'OK': 1,
Implementasi strategi menghasilkan hasil dalam tradisi terbaik
jika-yang lain :
let code = res.statusCode; let codeGroup = Math.floor(code / 100);
Fungsi
ambil siap digunakan:
seluruh kode fungsi .
3. Ekstraksi tautan
Sekarang, tergantung pada varian jawaban yang diterima, Anda harus dapat mengekstrak tautan dari data hasil
pengambilan untuk perayapan lebih lanjut. Untuk melakukan ini, kita mendefinisikan fungsi
ekstrak , yang mengambil objek hasil sebagai input dan mengembalikan array tautan baru.
Jika tipe hasil adalah REDIRECT, maka fungsi akan mengembalikan array dengan satu referensi tunggal dari bidang
lokasi . Jika NO_DATA, maka array kosong. Jika OK, maka kita perlu menghubungkan parser untuk
konten teks yang disajikan untuk pencarian.
Untuk tugas pencarian,
<a href />, Anda juga dapat menulis ekspresi reguler. Tetapi solusi ini tidak berskala sama sekali, karena di masa depan kita setidaknya akan memperhatikan atribut (
rel ) tautan lainnya, sebagai maksimum, kita akan memikirkan
img ,
tautan ,
skrip ,
audio / video (
sumber ) dan sumber daya lainnya. Jauh lebih menjanjikan dan lebih mudah untuk mem-parsing teks dokumen dan membangun pohon node-nya untuk mem-bypass penyeleksi yang biasa.
Kami akan menggunakan pustaka
JSDOM populer untuk bekerja dengan DOM di node.js:
const { JSDOM } = require('jsdom'); let document = new JSDOM(fetched.content).window.document; let elements = document.getElementsByTagName('A'); return Array.from(elements) .map(el => el.getAttribute('href')) .filter(href => typeof href === 'string') .map(href => href.trim()) .filter(Boolean);
Kami mendapatkan semua elemen
A dari dokumen, dan kemudian semua nilai yang disaring dari atribut
href , jika tidak baris kosong.
4. Persiapan dan penyaringan tautan
Sebagai hasil dari ekstraktor, kami memiliki serangkaian tautan (URL) dan dua masalah: 1) URL dapat bersifat relatif dan 2) URL dapat mengarah ke sumber daya eksternal (kami hanya membutuhkan yang internal sekarang).
Masalah pertama akan dibantu oleh fungsi
url.resolve , yang menyelesaikan URL halaman arahan relatif terhadap URL halaman sumber.
Untuk mengatasi masalah kedua, kami menulis fungsi utilitas sederhana
diScope yang memeriksa host halaman arahan terhadap host URL dasar dari perayapan saat ini:
function getLowerHost(dst) { return (new URL(dst)).hostname.toLowerCase(); } function inScope(dst, base) { let dstHost = getLowerHost(dst); let baseHost = getLowerHost(base); let i = dstHost.indexOf(baseHost);
Fungsi mencari substring (
baseHost ) dengan karakter sebelumnya diperiksa jika substring ditemukan: karena
wwwexample.com dan
example.com adalah domain yang berbeda. Akibatnya, kami tidak meninggalkan domain yang diberikan, tetapi memotong subdomainnya.
Kami memperbaiki fungsi
ekstrak dengan menambahkan "absolutisasi" dan memfilter tautan yang dihasilkan:
function extract(fetched, src, base) { return extractRaw(fetched) .map(href => url.resolve(src, href)) .filter(dst => /^https?\:\/\
Di sini
diambil adalah hasil dari fungsi
pengambilan ,
src adalah URL dari halaman sumber,
basis adalah URL dasar dari perayapan. Di bagian keluaran, kami mendapatkan daftar tautan internal (URL) yang sudah absolut untuk diproses lebih lanjut. Seluruh kode fungsi dapat
dilihat di sini .
5. Normalisasi URL
Setelah bertemu dengan URL apa pun lagi, tidak perlu mengirim permintaan lain untuk sumber daya, karena data telah diterima (atau koneksi lain masih terbuka dan menunggu jawaban). Tetapi tidak selalu cukup untuk membandingkan string dari dua URL untuk memahami hal ini. Normalisasi adalah prosedur yang diperlukan untuk menentukan kesetaraan dari URL yang berbeda secara sintaksis.
Proses
normalisasi adalah serangkaian transformasi yang diterapkan pada URL sumber dan komponennya. Berikut ini beberapa di antaranya:
- Skema dan host tidak peka terhadap huruf besar-kecil, sehingga harus dikonversi menjadi lebih rendah.
- Semua persentase (seperti "% 3A") harus dalam huruf kapital.
- Port default (80 untuk HTTP) dapat dihapus.
- Fragmen ( # ) tidak pernah terlihat oleh server dan juga dapat dihapus.
Anda selalu dapat mengambil sesuatu yang sudah jadi (misalnya,
normalize-url ) atau menulis fungsi sederhana Anda sendiri yang mencakup kasus paling penting dan umum:
function normalize(dst) { let dstUrl = new URL(dst);
Untuk jaga-jaga, format objek URL Ya, tidak ada penyortiran parameter kueri, mengabaikan tag utm, memproses
_escaped_fragment_ dan hal-hal lain, yang kami (benar-benar) tidak perlu sama sekali.
Selanjutnya, kita akan membuat cache lokal dari URL yang dinormalisasi yang diminta oleh kerangka kerja Crawl. Sebelum mengirim permintaan berikutnya, kami menormalkan URL yang diterima, dan jika tidak ada dalam cache, tambahkan dan baru kemudian kirimkan permintaan baru.
6. Algoritma fungsi utama
Komponen utama (primitif) dari solusi sudah siap, sekarang saatnya untuk mulai mengumpulkan semuanya. Untuk memulainya, mari kita tentukan tanda tangan dari fungsi
crawl : pada input, URL awal dan batas halaman. Fungsi mengembalikan janji yang resolusinya memberikan hasil akumulasi; tulis ke file
output :
crawl(start, limit).then(result => { fs.writeFile(output, JSON.stringify(result), 'utf8', err => { if (err) throw err; }); });
Alur kerja rekursif paling sederhana dari fungsi crawl dapat dijelaskan dalam langkah-langkah:
1. Inisialisasi cache dan objek hasil
2. JIKA URL halaman arahan (via normalisasi ) tidak ada dalam cache, MAKA
- 2.1. JIKA batas tercapai, END (tunggu hasil)
- 2.2. Tambahkan URL ke Cache
- 2.3. Simpan tautan antara sumber dan halaman arahan di hasil
- 2.4. Kirim permintaan tidak sinkron per halaman ( ambil )
- 2.5. JIKA permintaan berhasil, MAKA
- - 2.5.1. Ekstrak tautan baru dari hasil ( ekstrak )
- - 2.5.2. Untuk setiap tautan baru, jalankan algoritma 2-3
- 2.6. ELSE menandai halaman sebagai kesalahan
- 2.7. Simpan data halaman untuk dihasilkan
- 2.8. JIKA ini adalah halaman terakhir, Bawa hasilnya
3. ELSE menyimpan tautan antara sumber dan halaman arahan dalam hasil
Ya, algoritma ini akan mengalami perubahan besar di masa depan. Sekarang, solusi rekursif digunakan dengan sengaja di dahi, sehingga nanti lebih baik untuk "merasakan" perbedaan dalam implementasi. Benda kerja untuk implementasi fungsi terlihat seperti ini:
function crawl(start, limit = 100) {
Pencapaian batas halaman diperiksa oleh penghitung permintaan sederhana. Penghitung kedua - jumlah permintaan aktif pada suatu waktu - akan berfungsi sebagai ujian kesiapan untuk memberikan hasilnya (ketika nilainya berubah menjadi nol). Jika fungsi
ambil tidak bisa mendapatkan halaman berikutnya, maka atur Kode Status untuk itu sebagai nol.
Anda dapat (secara opsional)
membiasakan diri dengan kode implementasi di
sini , tetapi sebelum itu Anda harus mempertimbangkan format hasil yang dikembalikan.
7. Hasil yang dikembalikan
Kami akan memperkenalkan pengidentifikasi
id unik dengan kenaikan sederhana untuk halaman yang disurvei:
let id = 0; let cache = {};
Untuk hasilnya, kita akan membuat array
halaman yang akan kita tambahkan objek dengan data pada halaman:
id {number},
url {string} dan
kode {number | null} (ini sekarang cukup). Kami juga membuat larik
tautan untuk tautan antar halaman dalam bentuk objek:
dari (
id halaman sumber),
ke (
id halaman arahan).
Untuk tujuan informasi, sebelum menyelesaikan hasilnya, kami mengurutkan daftar halaman dalam urutan
id yang naik (setelah semua, jawaban akan datang dalam urutan apa pun), kami melengkapi hasilnya dengan jumlah halaman
hitungan yang dipindai dan bendera untuk mencapai batas
sirip yang ditentukan:
resolve({ pages: pages.sort((p1, p2) => p1.id - p2.id), links: links.sort((l1, l2) => l1.from - l2.from || l1.to - l2.to), count, fin: count < limit });
Contoh penggunaan
Script perayap yang selesai memiliki sinopsis berikut:
node crawl-cli.js --start="<URL>" [--output="<filename>"] [--limit=<int>]
Melengkapi pencatatan poin-poin utama dari proses, kita akan melihat gambar seperti itu pada saat startup:
$ node crawl-cli.js --start="https://google.com" --limit=20 [2019-02-26T19:32:10.087Z] Start crawl "https://google.com" with limit 20 [2019-02-26T19:32:10.089Z] Request (#1) "https://google.com/" [2019-02-26T19:32:10.721Z] Fetched (#1) "https://google.com/" with code 301 [2019-02-26T19:32:10.727Z] Request (#2) "https://www.google.com/" [2019-02-26T19:32:11.583Z] Fetched (#2) "https://www.google.com/" with code 200 [2019-02-26T19:32:11.720Z] Request (#3) "https://play.google.com/?hl=ru&tab=w8" [2019-02-26T19:32:11.721Z] Request (#4) "https://mail.google.com/mail/?tab=wm" [2019-02-26T19:32:11.721Z] Request (#5) "https://drive.google.com/?tab=wo" ... [2019-02-26T19:32:12.929Z] Fetched (#11) "https://www.google.com/advanced_search?hl=ru&authuser=0" with code 200 [2019-02-26T19:32:13.382Z] Fetched (#19) "https://translate.google.com/" with code 200 [2019-02-26T19:32:13.782Z] Fetched (#14) "https://plus.google.com/108954345031389568444" with code 200 [2019-02-26T19:32:14.087Z] Finish crawl "https://google.com" on count 20 [2019-02-26T19:32:14.087Z] Save the result in "result.json"
Dan inilah hasilnya dalam format JSON:
{ "pages": [ { "id": 1, "url": "https://google.com/", "code": 301 }, { "id": 2, "url": "https://www.google.com/", "code": 200 }, { "id": 3, "url": "https://play.google.com/?hl=ru&tab=w8", "code": 302 }, { "id": 4, "url": "https://mail.google.com/mail/?tab=wm", "code": 302 }, { "id": 5, "url": "https://drive.google.com/?tab=wo", "code": 302 }, // ... { "id": 19, "url": "https://translate.google.com/", "code": 200 }, { "id": 20, "url": "https://calendar.google.com/calendar?tab=wc", "code": 302 } ], "links": [ { "from": 1, "to": 2 }, { "from": 2, "to": 3 }, { "from": 2, "to": 4 }, { "from": 2, "to": 5 }, // ... { "from": 12, "to": 19 }, { "from": 19, "to": 8 } ], "count": 20, "fin": false }
Apa yang bisa dilakukan dengan ini? Minimal, daftar halaman Anda dapat menemukan semua halaman situs yang rusak. Dan memiliki informasi tentang tautan internal, Anda dapat mendeteksi rantai panjang (dan loop tertutup) pengalihan atau menemukan halaman yang paling penting dengan massa referensi.
Pengumuman 2.0
Kami telah memperoleh varian crawler crawler paling sederhana, yang mem-bypass halaman satu situs. Kode sumber
ada di sini . Ada juga contoh dan
unit test untuk beberapa fungsi.
Sekarang ini adalah pengirim permintaan tanpa basa-basi dan langkah selanjutnya yang masuk akal adalah mengajarinya sopan santun. Ini akan tentang header
User-agent , aturan
robots.txt , arahan
keterlambatan perayapan , dan banyak lagi. Dari sudut pandang implementasi, ini pertama-tama mengantri pesan dan kemudian melayani beban yang lebih besar.
Jika, tentu saja, materi ini akan menarik!