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:
xpath <- '//*[@id="__next"]/main/div/div[3]/div[5]/div/div/div[1]/div/article/div[3]'Sau khi xác định xpath chúng ta có thể lấy thông tin cần lấy như sau:
# Data link:
link1 <- "https://timviecnhanh.com/tuyen-ke-toan-truong-ha-noi-100111868.html?svs=max_box"
# 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_infoChúng ta có thể xem qua dữ liệu có được:
job_infoDữ 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 đó:
extract_from_timviecnhanh <- function(job_link) {
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_link1Dữ 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)
job_des_from_link1 %>% str_split(pattern = ":", simplify = TRUE)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_des1Chú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:
extract_from_timviecnhanh <- function(job_link) {
job_link %>% read_html() -> web_content
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.
extract_from_timviecnhanh <- function(job_link) {
# Load web content:
job_link %>% read_html() -> web_content
# 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:
page1_tưvanBH <- "https://timviecnhanh.com/vieclam/timkiem?field_ids[]=2&exp_session_id=e560af76-5a72-448a-865d-c2efbad51347&action=search&page=1"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_linksCá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:
suffix_links[str_detect(suffix_links, pattern = "max_box")] -> contains_max_boxFull links của 20 vị trí công việc này:
full_links_page1 <- str_c("https://timviecnhanh.com", contains_max_box) 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_page1Rồi convert list job_des_page1 về data frame:
do.call("bind_rows", job_des_page1) -> df_job_des_page1Dự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ể:
extract_all_links_from_page <- function(page_selected) {
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
suffix_links[str_detect(suffix_links, pattern = "max_box")] -> contains_max_box
full_links_page <- str_c("https://timviecnhanh.com", contains_max_box)
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à:
all_pages_tuvanBH <- str_c("https://timviecnhanh.com/vieclam/timkiem?field_ids[]=2&exp_session_id=e560af76-5a72-448a-865d-c2efbad51347&action=search&page=", 1:3)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_BHTươ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_BHTest 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:
all_CV <- "https://timviecnhanh.com/vieclam/timkiem?"
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_linksVì mỗi page có 20 links nên 993 này sẽ được đăng tải trong (làm tròn) 50 pages:
n_pages <- ceiling(number_job_links / 20)Và 50 pages đó là:
all_pages <- str_c("https://timviecnhanh.com/vieclam/timkiem?exp_session_id=8f63c857-e7bd-4e6a-be93-06b2af7c56e9&action=search&page=", 1:n_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_linksSau 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:
extract_from_timviecnhanh_tryCatch <- function(job_link) {
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.