BanditHijo.dev

Membuat Web Scraper dengan Ruby (Output: HTML) Level 2

Created at: 2020-08-20
Author by: BanditHijo

PERHATIAN!

Data yang penulis gunakan adalah data yang bersifat free public data. Sehingga, siapa saja dapat mengakses dan melihat tanpa perlu melalui layer authentikasi.

Penyalahgunaan data, bukan merupakan tanggung jawab dari penulis seutuhnya.

Sekenario Masalah

Blog post ini adalah modifikasi dari post sebelumnya yang berjudul, β€œMembuat Web Scraper dengan Ruby (Output: HTML)”.

Permasalahan dengan script sebelumnya adalah tidak dapat mendapatkan hasil.

Laporan ini saya dapatkan dari seorang teman, yaitu mas Rejka Permana di Telegram.

Ternyata, setelah saya cek website dari target belajar, desain dari website sudah berubah.

Sekarang menjadi seperti ini.

Gambar 1

Tampilan yang sekarang, tentunya tidak dapat difetch menggunakan CSS selector yang sebelumnya. Karena markup dari HTML sudah berubah.

Lantas saya pun mencoba untuk memodifikasi script tersebut.

Pemecahan Masalah

Tidak ada cara lain selain memodifikasi CSS selector.

Namun, kali ini, saya akan memanfaatkan Ruby Class sekaligus membuat script menjadi lebih Object Oriented.

Tujuannya agar apabila terjadi perubahan lagi, dapat lebih mudah untuk dimaintain.

Langkah pertama adalah, saya me-rename file scraper.rb menjadi main.rb.

Kemudian membuat 2 file baru yaitu scaper.rb dan template.rb.

πŸ“‚ ruby-web-scraper-dosen/
β”œβ”€ πŸ“„ daftar_dosen.html
β”œβ”€ πŸ“„ Gemfile
β”œβ”€ πŸ“„ Gemfile.lock
β”œβ”€ πŸ“„ main.rb πŸ‘ˆοΈ
β”œβ”€ πŸ“„ scraper.rb πŸ‘ˆοΈ
└─ πŸ“„ template.rb πŸ‘ˆοΈ

main.rb adalah aktor utama yang akan kita running.

scraper.rb adalah Scraper Class yang akan berisi logic dari proses scraping (backend).

template.rb adalah file yang akan menggenerate template (frontend).

Oke, selanjutnya adalah isi dari ketiga file tersebut.

Ngoding Session

Meskipun sebelumnya sudah pernah dilakukan, saya akan coba menulis kembali dari awal. Agar teman-teman yang baru mengikuti dari blog post ini tidak begitu kebingungan.

Initialisasi Gemfile

Buat file dengan nama Gemfile. dan kita akan memasang gem yang diperlukan di dalam file ini.

Gemfile
1source 'https://rubygems.org'
2
3gem 'httparty', '~> 0.18.1'
4gem 'nokogiri', '~> 1.10', '>= 1.10.9'
5gem 'byebug', '~> 11.1', '>= 11.1.3'

Setelah memasang gem pada Gemfile, kita perlu melakukan instalasi gem-gem tersebut.

$ bundle install

Proses bundle install di atas akan membuat sebuah file baru bernama Gemfile.lock yang berisi daftar dependensi dari gem yang kita butuhkan –daftar requirements–.

main.rb

Selanjutnya adalah si tokoh utama.

main.rb
1require 'httparty'
2require 'nokogiri'
3require 'byebug'
4require_relative './scraper'
5require_relative './template'
6
7def main
8 begin
9 target_url = "http://baak.universitasmulia.ac.id/dosen/"
10 unparsed_page = HTTParty.get(target_url)
11 rescue SocketError
12 puts "ERROR: Target URL tidak dikenal (salah alamat)"
13 exit
14 end
15
16 parsed_page = Nokogiri::HTML(unparsed_page)
17
18 # daftar semua dosen
19 dosens = Scraper.new(parsed_page).fetch_all
20
21 # daftar dosen pria
22 dosens_pria = Scraper.new(parsed_page).fetch_by_gender('pria')
23
24 # daftar dosen wanita
25 dosens_wanita = Scraper.new(parsed_page).fetch_by_gender('wanita')
26
27 # byebug
28
29 # template
30 Template.new(dosens, dosens_pria, dosens_wanita).create_html
31
32 puts "TOTAL SELURUH DOSEN : #{dosens.count} orang"
33 puts "TOTAL DOSEN PRIA : #{dosens_pria.count} orang"
34 puts "TOTAL DOSEN WANITA : #{dosens_wanita.count} orang"
35end
36
37main
38
39# Create index.html from daftar_dosen.html for rendering on netlify & vercel
40%x(cp -f daftar_dosen.html index.html)

scraper.rb

scrapper.rb
1class Scraper
2
3 attr_reader :parsed_page, :gender
4 attr_writer :dosens
5
6 def initialize(parsed_page)
7 @parsed_page = parsed_page
8 end
9
10 def fetch_all
11 dosens = Array.new
12 dosen_listings = @parsed_page.css('div.elementor-widget-wrap p')
13 dosen_listings[1..-2].each do |dosen_list|
14 collect_dosen(dosen_list, dosens)
15 end
16
17 return dosens
18 end
19
20 def fetch_by_gender(gender)
21 if gender == 'pria'
22 index = 9
23 elsif gender == 'wanita'
24 index = 10
25 else
26 puts 'Gender Not Qualified!'
27 end
28
29 dosens = Array.new
30 dosen_listings = @parsed_page.css('div.elementor-widget-wrap')[index].css('p')
31 dosen_listings.each do |dosen_list|
32 collect_dosen(dosen_list, dosens)
33 end
34
35 return dosens
36 end
37
38 def collect_dosen(dosen_list, dosens)
39 nama_nidn_dosen = dosen_list&.text&.gsub(/(^\w.*?:)|(NIDN :\s)/, "").strip
40 dosen = {
41 nama_dosen: nama_nidn_dosen&.gsub(/[^A-Za-z., ]/i, ''),
42 nidn_dosen: nama_nidn_dosen&.gsub(/[^0-9]/i, '')
43 }
44
45 if dosen[:nama_dosen] != nil
46 dosens << dosen
47 end
48 end
49
50end

template.rb

template.rb
1class Template
2
3 require 'date'
4
5 attr_accessor :dosens, :dosens_pria, :dosens_wanita
6
7 def initialize(dosens, dosens_pria, dosens_wanita)
8 @dosens = dosens
9 @dosens_pria = dosens_pria
10 @dosens_wanita = dosens_wanita
11 end
12
13 def create_html
14 File.delete("daftar_dosen.html") if File.exist?("daftar_dosen.html")
15 File.open("daftar_dosen.html", "w") do |f|
16 f.puts '<!DOCTYPE html>'
17 f.puts '<html lang="en">'
18 f.puts '<head>'
19 f.puts '<meta charset="UTF-8">'
20 f.puts '<meta name="viewport" content="width=device-width, initial-scale=1">'
21 f.puts "<title>Daftar Dosen Universitas Mulia Balikpapan(#{dosens.count} dosen)</title>"
22 f.puts '</head>'
23 f.puts '<body>'
24 f.puts '<h1>Daftar Dosen UM BPPN</h1>'
25 f.puts "<p>Data terakhir diparsing: #{Date.today}</p>"
26
27 f.puts '''
28 <p>Made with ❀ by <a href="https://bandithijo.github.io">Rizqi Nur Assyaufi</a> - 2020/07/12<br>
29 Powered by <a href="http://ruby-lang.org">Ruby</a> |
30 Source Code on <a href="https://github.com/bandithijo/ruby-web-scraper-dosen">GitHub</a></p>
31 '''
32
33 f.puts '<div class="tab">'
34 ['Semua Dosen', 'Dosen Pria', 'Dosen Wanita'].each.with_index(1) do |dosen, index|
35 f.puts "<button class='tablinks' onclick=\"openTab(event, 'tab#{index}')\">#{dosen}</button>"
36 end
37 f.puts '</div>'
38
39 f.puts '<div id="tab1" class="tabcontent active">'
40 f.puts '<h2>Daftar Semua Dosen</h2>'
41 f.puts "<p style='margin-top:-12px;'>Jumlah Seluruh Dosen: #{dosens.size} orang</p>"
42 f.puts '<input type="text" id="inputDosens" onkeyup="cariDosens()" placeholder="Cari nama dosen..">'
43 f.puts '<table id="tableDosens">'
44 dosens.each.with_index(1) do |dosen, index|
45 f.puts '<tr>'
46 f.puts "<td>#{dosen[:nama_dosen]}</td>"
47 f.puts "<td>#{dosen[:nidn_dosen]}</td>"
48 f.puts '</tr>'
49 end
50 f.puts '</table>'
51 f.puts '</div>'
52
53 f.puts '<div id="tab2" class="tabcontent">'
54 f.puts '<h2>Daftar Dosen Pria</h2>'
55 f.puts "<p style='margin-top:-12px;'>Jumlah Dosen Pria: #{dosens_pria.size} orang</p>"
56 f.puts '<input type="text" id="inputDosensPria" onkeyup="cariDosens()" placeholder="Cari nama dosen pria..">'
57 f.puts '<table id="tableDosensPria">'
58 dosens_pria.each.with_index(1) do |dosen, index|
59 f.puts '<tr>'
60 f.puts "<td>#{dosen[:nama_dosen]}</td>"
61 f.puts "<td>#{dosen[:nidn_dosen]}</td>"
62 f.puts '</tr>'
63 end
64 f.puts '</table>'
65 f.puts '</div>'
66
67 f.puts '<div id="tab3" class="tabcontent">'
68 f.puts '<h2>Daftar Dosen Wanita</h2>'
69 f.puts "<p style='margin-top:-12px;'>Jumlah Dosen Wanita: #{dosens_wanita.size} orang</p>"
70 f.puts '<input type="text" id="inputDosensWanita" onkeyup="cariDosens()" placeholder="Cari nama dosen wanita..">'
71 f.puts '<table id="tableDosensWanita">'
72 dosens_wanita.each.with_index(1) do |dosen, index|
73 f.puts '<tr>'
74 f.puts "<td>#{dosen[:nama_dosen]}</td>"
75 f.puts "<td>#{dosen[:nidn_dosen]}</td>"
76 f.puts '</tr>'
77 end
78 f.puts '</table>'
79 f.puts '</div>'
80
81 f.puts '''
82 <style>
83 :root {
84 --fg-color: #000;
85 --bg-color: #fff;
86 --a-color: #0000ff;
87 }
88 ::placeholder {
89 color: var(--fg-color);
90 opacity: 0.5;
91 }
92 body {
93 background-color: var(--bg-color);
94 color: var(--fg-color);
95 font-family: Arial;
96 font-size: 12px;
97 }
98 a, a:visited {
99 color: var(--a-color);
100 }
101 table,th,td {
102 border: 1px solid var(--fg-color);
103 border-collapse: collapse;
104 }
105 td {
106 padding: 3px;
107 }
108 td:nth-child(2) {
109 font-family: monospace;
110 text-align: center;
111 }
112 .tab {
113 overflow: hidden;
114 }
115 .tab button {
116 background-color: inherit;
117 float: left;
118 border: none;
119 outline: none;
120 cursor: pointer;
121 padding: 5px 5px 5px 0;
122 transition: 0.3s;
123 font-family: inherit;
124 font-size: inherit;
125 color: inherit;
126 margin-right: 10px;
127 }
128 .tab button.active {
129 text-decoration: underline;
130 }
131 .tabcontent {
132 display: none;
133 }
134 input:focus, textarea:focus, select:focus{
135 background-color: var(--bg-color);
136 color: var(--fg-color);
137 outline: none;
138 }
139 #inputDosens, #inputDosensPria, #inputDosensWanita {
140 background-color: var(--bg-color);
141 width: 30%;
142 padding: 0;
143 border: 1px solid var(--bg-color);
144 margin: 0 0 12px 0;
145 font-family: inherit;
146 font-size: 12px;
147 }
148 @media screen and (width: 360px) {
149 table, #inputDosens, #inputDosensPria, #inputDosensWanita {
150 width: 100%;
151 }
152 }
153 </style>
154 '''
155
156 f.puts '''
157 <script>
158 // Sumber: https://www.w3schools.com/howto/howto_js_tabs.asp
159 function openTab(evt, tabNumber) {
160 var i, tabcontent, tablinks;
161 tabcontent = document.getElementsByClassName("tabcontent");
162 for (i = 0; i < tabcontent.length; i++) {
163 tabcontent[i].style.display = "none";
164 }
165 tablinks = document.getElementsByClassName("tablinks");
166 for (i = 0; i < tablinks.length; i++) {
167 tablinks[i].className = tablinks[i].className.replace(" active", "");
168 }
169 document.getElementById(tabNumber).style.display = "block";
170 evt.currentTarget.className += " active";
171 }
172
173 // Sumber: https://www.w3schools.com/howto/howto_js_filter_table.asp
174 function cariDosens() {
175 var input, filter, table, tr,
176 inputPria, filterPria, tablePria, trPria,
177 inputWanita, filterWanita, tableWanita, trWanita,
178 td, i, txtValue;
179 '''
180
181 ['', 'Pria', 'Wanita'].each do |dosen|
182 f.puts """
183 input#{dosen} = document.getElementById('inputDosens#{dosen}');
184 filter#{dosen} = input#{dosen}.value.toUpperCase();
185 table#{dosen} = document.getElementById('tableDosens#{dosen}');
186 tr#{dosen} = table#{dosen}.getElementsByTagName('tr');
187 for (i = 0; i < tr#{dosen}.length; i++) {
188 td = tr#{dosen}[i].getElementsByTagName('td')[0];
189 if (td) {
190 txtValue = td.textContent || td.innerText;
191 if (txtValue.toUpperCase().indexOf(filter#{dosen}) > -1) {
192 tr#{dosen}[i].style.display = '';
193 } else {
194 tr#{dosen}[i].style.display = 'none';
195 }
196 }
197 }
198 """
199 end
200
201 f.puts '''
202 }
203 </script>
204 '''
205
206 f.puts '</body>'
207 f.puts '</html>'
208 end
209 end
210
211end

Hasilnya

Gambar 2

Demo

Untuk demonstrasi, teman-teman dapat mengunjungi alamat di bawah ini.

https://daftar-dosen-umb.vercel.app

Source

Bagi yang memerlukan source codenya, dapat mengunjungin alamat di bawah ini.

https://github.com/bandithijo/ruby-web-scraper-dosen

Pesan Penulis

Sepertinya, segini dulu yang saya tuliskan.

Penjelasan dari masing-masing blok kode akan saya tuliskan pada kesempatan yang lain yaa.

Mudah-mudahan kalau teman-teman mampir ke post ini, sudah ada penjelasan per blok kodenya.

Terima kasih sudah mampir.

(^_^)

Referensi

  1. It’s Time To HTTParty!
    Diakses tanggal: 2020/08/20

  2. nokogiri.org
    Diakses tanggal: 2020/08/20