Xét một link về vị trí công việc kế toán dưới đây:
https://timviecnhanh.com/tuyen-ke-toan-truong-ha-noi-100111868.html?svs=max_box
Giả sử chúng ta muốn lấy, ví dụ, vùng dữ liệu nằm trong hình chữ nhật viền đỏ dưới đây:
Trước hết cần xác định cái gọi là xpath tương ứng với vùng dữ liệu này bằng phím F12. Cụ thể thì xpath tương ứng chính là vùng đánh dấu đỏ ở hình dưới đây:
Kích chuột phải vào khu vực text đánh dấu (ở khu vực trong hình chữ nhật viền đỏ) và chọn cửa sổ Copy Xpath:
Đến đây ta xác định được xpath là:
# Select xpath:
<- '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]' xpath
Sau khi xác định xpath chúng ta có thể lấy thông tin cần lấy như sau:
# Data link:
<- "https://timviecnhanh.com/tuyen-ke-toan-truong-ha-noi-100111868.html?svs=max_box"
link1
# Load rvest package:
library(rvest)
# Scrap data from the link:
%>%
link1 read_html() %>%
html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]') %>%
html_text() -> job_info
Chúng ta có thể xem qua dữ liệu có được:
job_info
Dữ liệu có được là một chuỗi text - một kiểu dữ liệu phi cấu trúc. Muốn convert dữ liệu này về dạng bảng (Data Frame) thì cần một bước xử lí nữa. Cái này tính sau.
Test 1 Lấy thông tin về số lượt xem tin nhắn này như trong hình dưới đây:
Test 2 Lấy các thông tin cho vị trí công việc từ link thứ hai dưới đây:
So sánh xem xpath của link1 và link thứ hai này có gì khác không?
Chúng ta viết một hàm có tên là extract_from_timviecnhanh() với input đầu vào là một link mô tả một công việc cụ thể và đầu ra là các thông tin cơ bản về vị trí công việc đó:
<- function(job_link) {
extract_from_timviecnhanh
%>%
job_link read_html() %>%
html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]') %>%
html_text() -> job_info
return(job_info)
}
Test hàm:
extract_from_timviecnhanh(job_link = link1) -> job_des_from_link1
Dữ liệu thu về là kiểu dữ liệu text không có cấu trúc. Chúng ta cần convert dữ liệu này về dạng table. Object có tên job_des_from_link1 chứa các mô tả về vị trí công việc được tuyển và chú ý rằng các thông tin về công việc này được phân biệt với nhau bằng dấu hai chấm. Chúng ta sử dụng dấu hai chấm này làm “manh mối” để xử lí chuỗi text bằng hàm str_split()
của thư viện stringr:
library(stringr)
%>% str_split(pattern = ":", simplify = TRUE) job_des_from_link1
Với các máy tính hệ điều hành Win thì có lẽ nên chuyển dữ liệu đã có về kí tự không dấu trước khi xử lí bằng hàm stri_trans_general():
library(stringi)
%>%
job_des_from_link1 stri_trans_general("Latin-ASCII") %>%
str_split(pattern = ":", simplify = TRUE)
Kết quả thu được là một chuỗi text gồm 11 phần tử. Chúng ta convert chuỗi text này về data frame có 11 cột như sau:
%>%
job_des_from_link1 stri_trans_general("Latin-ASCII") %>%
str_split(pattern = ":", simplify = TRUE) %>%
matrix(nrow = 1) %>%
as.data.frame() -> df_des1
Chúng ta điều chỉnh lại hàm extract_from_timviecnhanh() để nó trả kết quả về một DF chứ không phải là một chuỗi text như thiết kế ban đầu như sau:
<- function(job_link) {
extract_from_timviecnhanh
%>% read_html() -> web_content
job_link
%>%
web_content html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]') %>%
html_text() -> job_info
%>%
job_info stri_trans_general("Latin-ASCII") %>%
str_split(pattern = ":", simplify = TRUE) %>%
matrix(nrow = 1) %>%
as.data.frame() -> df_des1
return(df_des1)
}
Còn nếu muốn bổ sung thêm hai trường thông tin là thời điểm đăng tin và số lượt xem thì hàm trên được điều chỉnh như sau. Chú ý rằng mảng thông tin này được chia tách nhau bởi dấu | chứ không phải dấu : như mảng thông tin về mức lương:
library(dplyr) # For data manipulation.
<- function(job_link) {
extract_from_timviecnhanh
# Load web content:
%>% read_html() -> web_content
job_link
# Part 1 about job description:
%>%
web_content html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]') %>%
html_text() %>%
stri_trans_general("Latin-ASCII") %>%
str_split(pattern = ":", simplify = TRUE) %>%
matrix(nrow = 1) %>%
as.data.frame() -> df_des1
# Part 2:
%>%
web_content html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[1]/div') %>%
html_text() %>%
stri_trans_general("Latin-ASCII") %>%
str_split(pattern = "\\|", simplify = TRUE) %>%
matrix(nrow = 1) %>%
as.data.frame() %>%
rename(job_date = V1, n_views = V2) %>%
mutate(source_link = job_link) -> df_des2
# Combine the two data frames:
bind_cols(df_des1, df_des2) -> df_des
# Return final output:
return(df_des)
}
Mục này chúng ta sẽ extract tất cả các links về một nhóm công việc nào đó (như Tư Vấn Bảo Hiểm) có trong một page. Cụ thể, page 1 của nhóm việc này có link như sau:
<- "https://timviecnhanh.com/vieclam/timkiem?field_ids[]=2&exp_session_id=e560af76-5a72-448a-865d-c2efbad51347&action=search&page=1" page1_tưvanBH
Nội dung của page này:
Có thể thấy mỗi page có chứa 20 links về các vị trí công việc. Chúng ta cần extract ra 20 links công việc này. Vì chúng sẽ là input đầu vào cho hàm extract_from_timviecnhanh()
ở trên:
%>%
page1_tuvanBH read_html() %>%
html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[4]/div/div/div[1]/div[2]/table/tbody') %>%
html_nodes("a") %>%
html_attr("href") -> suffix_links
Các linh chính chứa thông tin về job sẽ có cụm kí tự max_box ở cuối cùng. Lấy ra chỉ những links này như sau:
str_detect(suffix_links, pattern = "max_box")] -> contains_max_box suffix_links[
Full links của 20 vị trí công việc này:
<- str_c("https://timviecnhanh.com", contains_max_box) full_links_page1
Chúng ta có thể lấy đồng thời thông tin về 20 vị trí công việc này bằng cách sử dụng lapply()
:
lapply(full_links_page1, extract_from_timviecnhanh) -> job_des_page1
Rồi convert list job_des_page1 về data frame:
do.call("bind_rows", job_des_page1) -> df_job_des_page1
Dựa trên codes ở trên chúng ta viết hàm extract_all_links_from_page()
để lấy ra tất cả job links có trong một page cụ thể:
<- function(page_selected) {
extract_all_links_from_page
%>%
page_selected read_html() %>%
html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[4]/div/div/div[1]/div[2]/table/tbody') %>%
html_nodes("a") %>%
html_attr("href") -> suffix_links
str_detect(suffix_links, pattern = "max_box")] -> contains_max_box
suffix_links[
<- str_c("https://timviecnhanh.com", contains_max_box)
full_links_page
return(full_links_page)
}
Data frame có tên df_job_des_page1 là thông tin về 20 vị trí công việc được đăng tuyển của nhóm Bảo Hiểm. Nhóm công việc này có tất cả 47 vị trí (xem hình), mà mỗi page có thông tin của 20 vị trí như vậy thì sẽ cần đến tối đa là 3 pages cho 47 links tương ứng. Các pages đó sẽ là:
<- str_c("https://timviecnhanh.com/vieclam/timkiem?field_ids[]=2&exp_session_id=e560af76-5a72-448a-865d-c2efbad51347&action=search&page=", 1:3) all_pages_tuvanBH
Sử dụng hàm extract_all_links_from_page()
ở trên chúng ta có thể lấy ra tất cả 47 links này:
lapply(all_pages_tuvanBH, extract_all_links_from_page) -> all_links_BH
# Unlist:
unlist(all_links_BH) -> all_links_BH
Tương tự chúng ta lấy thông tin về 47 vị trí công việc Tư Vấn Bảo Hiểm:
lapply(all_links_BH, extract_from_timviecnhanh) -> job_des_all_BH
do.call("bind_rows", job_des_all_BH) -> job_des_all_BH
Test 1 Lấy tất cả các link về vị trí công việc của ba nhóm nghề bất kì được cung cấp bởi timviecnhanh.
Nếu chúng ta lấy tất cả các links về vị trí công việc được tuyển theo từng nhóm công việc thì chúng ta phải xác định tất cả links tương ứng với 56 nhóm (theo cách phân loại của web timviecnhanh). Cách tiếp cận này khá rắc rối. Một cách tiếp cận khác là chúng ta tìm theo Tất cả ngành nghề - một lựa chọn được phép chọn của website này:
Tại thời điểm R codes này được viết thì có 993 vị trí công việc được đang tuyển. Để R codes có thể vận hành ở những thời điểm khác chúng ta extract thông tin này:
<- "https://timviecnhanh.com/vieclam/timkiem?"
all_CV
%>%
all_CV read_html() %>%
html_nodes(xpath = '//*[@id="__next"]/main/div/div[3]/div[4]/div/div/div[1]/div[2]/div[2]/h3/span[1]/span') %>%
html_text() %>%
str_replace_all(pattern = "[^1-9]", replacement = "") %>%
as.numeric() -> number_job_links
Vì mỗi page có 20 links nên 993 này sẽ được đăng tải trong (làm tròn) 50 pages:
<- ceiling(number_job_links / 20) n_pages
Và 50 pages đó là:
<- str_c("https://timviecnhanh.com/vieclam/timkiem?exp_session_id=8f63c857-e7bd-4e6a-be93-06b2af7c56e9&action=search&page=", 1:n_pages) all_pages
OK. Đến đây chúng ta có thể lấy ra link của 993 vị trí công việc này:
lapply(all_pages, extract_all_links_from_page) %>% unlist() -> all_job_links
Sau khi có tất cả links ở trên, như đã biết chúng ta có thể sử dụng code như sau để lấy dữ liệu:
lapply(all_job_links, extract_from_timviecnhanh) -> all_job_data
Code này sẽ chạy tốt trừ khi không một link nào trong số 933 links ở trên bị lỗi 404 (hoặc một link trong số này không thể truy cập được vì bất cứ lí do gì). Trong tình huống đó giả sử 992 links đầu tiên OK nhưng cái link 993 cuối cùng bị lỗi 404 thì coi như hỏng cả. Vì lí do này, chúng ta sẽ tìm cách “tránh/bỏ qua” khi có một link bất kì bị lỗi 404 bằng cách “viết lại” hàm extract_from_timviecnhanh()
như sau:
<- function(job_link) {
extract_from_timviecnhanh_tryCatch
tryCatch(extract_from_timviecnhanh(job_link), error = function(e) {NULL}) -> results
return(results)
}
Lúc này chúng ta có thể sử dụng hàm này đồng thời tính xem mấy bao lâu để lấy toàn bộ dữ liệu:
# About 420s:
system.time(lapply(all_job_links, extract_from_timviecnhanh_tryCatch) -> all_job_data)
## user system elapsed
## 39.41 2.22 413.62
Rồi convert về data frame quen thuộc và lưu lại dữ liệu ở dạng, ví dụ, định dạng csv:
do.call("bind_rows", all_job_data) -> df_all_job_data
write.csv(df_all_job_data, "df_all_job_data.csv", row.names = FALSE)
Đến bước này có thể coi như toàn bộ mô tả về các jobs được đăng trên timviecnhanh đã được lấy về. Chúng ta có thể sử dụng dữ liệu để, ví dụ, đánh giá đòi hỏi về bằng cấp của các vị trí công việc bằng cách sử dụng cột biến V4:
%>%
df_all_job_data pull(V4) %>%
str_split(pattern = "-", simplify = TRUE) %>%
as.data.frame() %>%
group_by(V1) %>%
count(sort = TRUE) %>%
ungroup() %>%
mutate(per = n / sum(n)) %>%
mutate(per = round(per, 3))
## # A tibble: 8 x 3
## V1 n per
## <chr> <int> <dbl>
## 1 Cao dang 294 0.296
## 2 Dai hoc 249 0.251
## 3 Trung cap 243 0.245
## 4 Khong yeu cau 86 0.087
## 5 Trung hoc 84 0.085
## 6 Lao dong pho thong 20 0.02
## 7 Chung chi 15 0.015
## 8 Cao hoc 2 0.002
Có thể tạm coi “Cao dang” là kiểu bằng cấp/học vấn được đòi hỏi bởi gần 30% vị trí công việc. Sử dụng từ “gần bằng” là bởi vì, nếu muốn biết con số chính xác, thì cần nhân vị trí công việc đó với số lượng được tuyển ở cột V7.
Cách thức lấy và xử lí dữ liệu trình bày ở post này có thể được điều chỉnh để lấy dữ liệu từ các website tương tự khác như là vieclam24h hay một số website khác.
Việc lấy dữ liệu từ các dynamic website không được trình bày ở post này. Kiểu website này, muốn lấy được dữ liệu của nó cần sử dụng RSelenium. Tuy nhiên cách thức lấy dữ liệu ở những website kiểu này sẽ có bước lặp lại những gì chúng ta đã làm ở trên.
Khi lấy dữ liệu từ website cần cân nhắc khía cạnh đạo đức cũng như một số quy định về hành vi này. Đọc thêm về vấn đề này tại đây.