Practice Test 1

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:

https://timviecnhanh.com/tuyen-nhan-vien-thu-hoi-no-hien-truong-toan-quoc-binh-duong-100114673.html?svs=max_box

So sánh xem xpath của link1 và link thứ hai này có gì khác không?

Convert to Function

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_link1

Convert to Data Frame

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)

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_des1

Adjust the Function

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:

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 tinsố 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)
  
}

Practice Test 2

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.

Alternative Solution

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_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:

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_links

Handle 404 Error

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:

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.

Final Notes

  • 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.