Sejak memasang "dark" theme, saya cenderung menjadi malas menulis. Untuk sementara, dark theme saya disable dulu yaa. Terima kasih (^_^) (bandithijo, 2024/09/15) ●
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.
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.
1
2
3
4
5
source 'https://rubygems.org'
gem 'httparty', '~> 0.18.1'
gem 'nokogiri', '~> 1.10', '>= 1.10.9'
gem '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.
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
require 'httparty'
require 'nokogiri'
require 'byebug'
require_relative './scraper'
require_relative './template'
def main
begin
target_url = "http://baak.universitasmulia.ac.id/dosen/"
unparsed_page = HTTParty.get(target_url)
rescue SocketError
puts "ERROR: Target URL tidak dikenal (salah alamat)"
exit
end
parsed_page = Nokogiri::HTML(unparsed_page)
# daftar semua dosen
dosens = Scraper.new(parsed_page).fetch_all
# daftar dosen pria
dosens_pria = Scraper.new(parsed_page).fetch_by_gender('pria')
# daftar dosen wanita
dosens_wanita = Scraper.new(parsed_page).fetch_by_gender('wanita')
# byebug
# template
Template.new(dosens, dosens_pria, dosens_wanita).create_html
puts "TOTAL SELURUH DOSEN : #{dosens.count} orang"
puts "TOTAL DOSEN PRIA : #{dosens_pria.count} orang"
puts "TOTAL DOSEN WANITA : #{dosens_wanita.count} orang"
end
main
# Create index.html from daftar_dosen.html for rendering on netlify & vercel
%x(cp -f daftar_dosen.html index.html)
scraper.rb
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
class Scraper
attr_reader :parsed_page, :gender
attr_writer :dosens
def initialize(parsed_page)
@parsed_page = parsed_page
end
def fetch_all
dosens = Array.new
dosen_listings = @parsed_page.css('div.elementor-widget-wrap p')
dosen_listings[1..-2].each do |dosen_list|
collect_dosen(dosen_list, dosens)
end
return dosens
end
def fetch_by_gender(gender)
if gender == 'pria'
index = 9
elsif gender == 'wanita'
index = 10
else
puts 'Gender Not Qualified!'
end
dosens = Array.new
dosen_listings = @parsed_page.css('div.elementor-widget-wrap')[index].css('p')
dosen_listings.each do |dosen_list|
collect_dosen(dosen_list, dosens)
end
return dosens
end
def collect_dosen(dosen_list, dosens)
nama_nidn_dosen = dosen_list&.text&.gsub(/(^\w.*?:)|(NIDN :\s)/, "").strip
dosen = {
nama_dosen: nama_nidn_dosen&.gsub(/[^A-Za-z., ]/i, ''),
nidn_dosen: nama_nidn_dosen&.gsub(/[^0-9]/i, '')
}
if dosen[:nama_dosen] != nil
dosens << dosen
end
end
end
template.rb
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
class Template
require 'date'
attr_accessor :dosens, :dosens_pria, :dosens_wanita
def initialize(dosens, dosens_pria, dosens_wanita)
@dosens = dosens
@dosens_pria = dosens_pria
@dosens_wanita = dosens_wanita
end
def create_html
File.delete("daftar_dosen.html") if File.exist?("daftar_dosen.html")
File.open("daftar_dosen.html", "w") do |f|
f.puts '<!DOCTYPE html>'
f.puts '<html lang="en">'
f.puts '<head>'
f.puts '<meta charset="UTF-8">'
f.puts '<meta name="viewport" content="width=device-width, initial-scale=1">'
f.puts "<title>Daftar Dosen Universitas Mulia Balikpapan(#{dosens.count} dosen)</title>"
f.puts '</head>'
f.puts '<body>'
f.puts '<h1>Daftar Dosen UM BPPN</h1>'
f.puts "<p>Data terakhir diparsing: #{Date.today}</p>"
f.puts '''
<p>Made with ❤ by <a href="https://bandithijo.github.io">Rizqi Nur Assyaufi</a> - 2020/07/12<br>
Powered by <a href="http://ruby-lang.org">Ruby</a> |
Source Code on <a href="https://github.com/bandithijo/ruby-web-scraper-dosen">GitHub</a></p>
'''
f.puts '<div class="tab">'
['Semua Dosen', 'Dosen Pria', 'Dosen Wanita'].each.with_index(1) do |dosen, index|
f.puts "<button class='tablinks' onclick=\"openTab(event, 'tab#{index}')\">#{dosen}</button>"
end
f.puts '</div>'
f.puts '<div id="tab1" class="tabcontent active">'
f.puts '<h2>Daftar Semua Dosen</h2>'
f.puts "<p style='margin-top:-12px;'>Jumlah Seluruh Dosen: #{dosens.size} orang</p>"
f.puts '<input type="text" id="inputDosens" onkeyup="cariDosens()" placeholder="Cari nama dosen..">'
f.puts '<table id="tableDosens">'
dosens.each.with_index(1) do |dosen, index|
f.puts '<tr>'
f.puts "<td>#{dosen[:nama_dosen]}</td>"
f.puts "<td>#{dosen[:nidn_dosen]}</td>"
f.puts '</tr>'
end
f.puts '</table>'
f.puts '</div>'
f.puts '<div id="tab2" class="tabcontent">'
f.puts '<h2>Daftar Dosen Pria</h2>'
f.puts "<p style='margin-top:-12px;'>Jumlah Dosen Pria: #{dosens_pria.size} orang</p>"
f.puts '<input type="text" id="inputDosensPria" onkeyup="cariDosens()" placeholder="Cari nama dosen pria..">'
f.puts '<table id="tableDosensPria">'
dosens_pria.each.with_index(1) do |dosen, index|
f.puts '<tr>'
f.puts "<td>#{dosen[:nama_dosen]}</td>"
f.puts "<td>#{dosen[:nidn_dosen]}</td>"
f.puts '</tr>'
end
f.puts '</table>'
f.puts '</div>'
f.puts '<div id="tab3" class="tabcontent">'
f.puts '<h2>Daftar Dosen Wanita</h2>'
f.puts "<p style='margin-top:-12px;'>Jumlah Dosen Wanita: #{dosens_wanita.size} orang</p>"
f.puts '<input type="text" id="inputDosensWanita" onkeyup="cariDosens()" placeholder="Cari nama dosen wanita..">'
f.puts '<table id="tableDosensWanita">'
dosens_wanita.each.with_index(1) do |dosen, index|
f.puts '<tr>'
f.puts "<td>#{dosen[:nama_dosen]}</td>"
f.puts "<td>#{dosen[:nidn_dosen]}</td>"
f.puts '</tr>'
end
f.puts '</table>'
f.puts '</div>'
f.puts '''
<style>
:root {
--fg-color: #000;
--bg-color: #fff;
--a-color: #0000ff;
}
::placeholder {
color: var(--fg-color);
opacity: 0.5;
}
body {
background-color: var(--bg-color);
color: var(--fg-color);
font-family: Arial;
font-size: 12px;
}
a, a:visited {
color: var(--a-color);
}
table,th,td {
border: 1px solid var(--fg-color);
border-collapse: collapse;
}
td {
padding: 3px;
}
td:nth-child(2) {
font-family: monospace;
text-align: center;
}
.tab {
overflow: hidden;
}
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 5px 5px 5px 0;
transition: 0.3s;
font-family: inherit;
font-size: inherit;
color: inherit;
margin-right: 10px;
}
.tab button.active {
text-decoration: underline;
}
.tabcontent {
display: none;
}
input:focus, textarea:focus, select:focus{
background-color: var(--bg-color);
color: var(--fg-color);
outline: none;
}
#inputDosens, #inputDosensPria, #inputDosensWanita {
background-color: var(--bg-color);
width: 30%;
padding: 0;
border: 1px solid var(--bg-color);
margin: 0 0 12px 0;
font-family: inherit;
font-size: 12px;
}
@media screen and (width: 360px) {
table, #inputDosens, #inputDosensPria, #inputDosensWanita {
width: 100%;
}
}
</style>
'''
f.puts '''
<script>
// Sumber: https://www.w3schools.com/howto/howto_js_tabs.asp
function openTab(evt, tabNumber) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabNumber).style.display = "block";
evt.currentTarget.className += " active";
}
// Sumber: https://www.w3schools.com/howto/howto_js_filter_table.asp
function cariDosens() {
var input, filter, table, tr,
inputPria, filterPria, tablePria, trPria,
inputWanita, filterWanita, tableWanita, trWanita,
td, i, txtValue;
'''
['', 'Pria', 'Wanita'].each do |dosen|
f.puts """
input#{dosen} = document.getElementById('inputDosens#{dosen}');
filter#{dosen} = input#{dosen}.value.toUpperCase();
table#{dosen} = document.getElementById('tableDosens#{dosen}');
tr#{dosen} = table#{dosen}.getElementsByTagName('tr');
for (i = 0; i < tr#{dosen}.length; i++) {
td = tr#{dosen}[i].getElementsByTagName('td')[0];
if (td) {
txtValue = td.textContent || td.innerText;
if (txtValue.toUpperCase().indexOf(filter#{dosen}) > -1) {
tr#{dosen}[i].style.display = '';
} else {
tr#{dosen}[i].style.display = 'none';
}
}
}
"""
end
f.puts '''
}
</script>
'''
f.puts '</body>'
f.puts '</html>'
end
end
end
Hasilnya
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
-
It’s Time To HTTParty!
Diakses tanggal: 2020/08/20 -
nokogiri.org
Diakses tanggal: 2020/08/20
Lisensi
Atribusi-NonKomersial-BerbagiSerupa 4.0 Internasional (CC BY-NC-SA 4.0)
Penulis
My journey kicks off from reading textbooks as a former Medical Student to digging bugs as a Software Engineer – a delightful rollercoaster of career twists. Embracing failure with the grace of a Cat avoiding water, I've seamlessly transitioned from Stethoscope to Keyboard. Armed with ability for learning and adapting faster than a Heart Beat, I'm on a mission to turn Code into a Product.
- Rizqi Nur Assyaufi