[Web Scraping] Phần 1: Khái quát và minh hoạ lấy dữ liệu từ itviec.com

Posted on December 20th, 2019

Web Scraping là một chủ đề khá thú vị đối với cá nhân mình. Và sau khi đọc bài viết được share trên Viblo này thì đã có nhiều cảm hứng hơn để chia sẻ những gì mình biết về chủ đề này. Trong bài viết trên, tác giả đã chia sẻ cách thực hiện việc lấy dữ liệu từ một trang cụ thể và code demo với Python sử dụng một số thư viện của nó. Vì mình không rành code Python cho lắm, nên đã chỉ đọc lướt qua để nắm ý chính. Còn trong bài này, mình muốn chia sẻ khái quát về Web Scraping và code demo bằng NodeJS. Nhà nghèo anh em đông có chi xài nấy không đòi hỏi ;)

Scraping hay Crawling? Giới thiệu concept

Khi nói về việc lấy dữ liệu từ một trang web, từ Crawling hoặc Scraping rất thường xuyên được dùng mang ý nghĩa thay thế cho nhau, nhưng theo một số nguồn tin thân Google thì hai thuật ngữ này không hoàn toàn giống nhau. Còn khác nhau như thế nào sẽ tổng hợp và chia sẻ ở bài khác. Dù đa số mọi người hay dùng từ Crawling tuy nhiên cá nhân mình thì thích Scraping hơn.

Web Scraping có thể được định nghĩa là việc tạo một agent phục vụ mục đích download, parse và tổ chức dữ liệu lấy từ Web một cách tự động.

Cụ thể hơn, giả sử như có một yêu cầu cần tổng hợp thông tin cơ bản về một doanh nghiệp đang hoạt động, lưu tập trung vào file ví dụ Excel. Thì thay vì phải thực hiện tuần tự các bước: mở browser > truy cập trang nào đó > tìm thông tin muốn lấy > Copy-paste vào file ở máy, sau đó lặp lại cho những doanh nghiệp khác...

...thì kỹ thuật Web Scraping cho phép chúng ta thực thi các tác vụ này một cách tự động. Keyword chính là tự động. Vì với tác vụ lặp đi lặp lại như thế này thì không còn gì phải bàn cãi để nghĩ đến tự động hoá cho nó. Bằng việc "tạo công ăn việc làm cho máy" như thế này thì chúng ta có thể tối đa hoá tốc độ hoàn thành mục tiêu trong khi tối thiểu hoá được các lỗi có thể xảy ra khi làm manual. Tuy nhiên, quyết định automate hay không còn phải xem lại xét một yếu tố gọi là Return On Investment.

Web Scraping methods

Có nhiều công cụ rất cụ thể giúp thực hiện tác vụ trên, nhưng về nguyên lý thì cứ tạm chia làm n cách, với n=2 ;)

  • Cách 1: Browser-based Scraping: dùng browser.
  • Cách 2: Request-based Scraping: không dùng browser.

Vẫn với ví dụ trên, khi nhìn vào các manual steps thì có dùng đến "Mở browser", vậy thì cách một tự nhiên là mệnh đề đúng không cần chứng minh... ngay, mà sẽ chi tiết ở bài sau.

Request-based Web Scraping

Phần còn lại của bài này, chúng ta sẽ tập trung vào cách hai. Cách này tới từ sự đa dạng của thế giới Web. Có những trang rất tử tế, cho chúng ta mọi thứ chúng ta cần trong lần gặp đầu tiên mà không đòi hỏi làm gì cả. Đặc điểm nhận dạng của những trang này là có ít user interaction, gần như không load ajax và url là dễ đoán định.

Đơn cử như trang itviec.com - ít nhưng mà chất, mình sẽ thử dùng cách này để lấy dữ liệu là những job có keyword "NodeJS".

Chống chỉ định: Mình hoàn toàn không có vấn đề đạo đức gì trong việc Scraping này hết. Cơ bản là giống như dùng browser lướt rồi copy-paste thôi, nên cứ nghĩ đơn giản cho đời thanh thản.

Bước một: Nhận dạng

Flow bình thường sẽ là như sau: truy cập vào trang https://itviec.com, điền keyword muốn search > chọn thành phố > nhấn nút "Search". Khi đó hệ thống sẽ trả về trang kết quả list ra tất cả các job thoả mãn điều kiện search. Và đặc biệt, khi nhìn vào url của trang kết quả này, chúng ta có thể đoán nhanh ra pattern của nó.

Ví dụ:

itviec search string

Như vậy có thể mạnh dạn đoán pattern này là: https://itviec.com/it-jobs/{searchKeyword}/{city}

  • giả sử ta muốn search với "ReactJS" và thành phố là "Ho Chi Minh", thì có lẽ cũng không cần fill in search form làm gì mà cứ thử access trực tiếp vào trang kết quả bằng url https://itviec.com/it-jobs/reactjs/ho-chi-minh-hcm

Bam! đã chưa, được kìa

itviec search string

Đối với những keyword mà kết quả search ra nhiều, thì hệ thống sẽ có phân trang paginating bằng nút "Show More Jobs" ở phía dưới. Khi inspect button này chúng ta cũng sẽ được pattern của phân trang sẽ là: https://itviec.com/it-jobs/nodejs/ho-chi-minh-hcm?page={n}

itviec search result pagination

Tiếp theo, nếu click vào một job từ trang kết quả search thì sẽ mở ra chi tiết cho job đó, mà cũng không cần tương tác gì thêm.

-> Với những điều kiện tiền đề như vậy thì trong trường hợp trang itviec này, cách request-based mà chúng ta đang nói đến có thể kết luận một cách vội vã là chơi được.

Bước hai: Phân tích cấu trúc (thật ra là dò DOM)

Trong ví dụ về trang itviec này, thông thường chúng ta sẽ chỉ cần một số thông tin nhất định thay vì tất cả những gì mà server trả về, cứ cho là một job sẽ bao gồm những thông tin sau:

  • Tên công ty
  • Job title
  • Url tới trang mô tả chi tiết công việc
  • Thời gian mà job này được đăng so với hiện tại và mô tả chi tiết

Với code javascript một sample job sẽ như sau:

const sampleJob = {
    companyName: "Cybozu",
    jobTitle: "Senior Automation Engineer",
    jobUrl: "https://itviec.com/it-jobs/automation-engineer-cybozu-4104",
    timeFromPosted: "3 hours ago",
    jobDescription: "somethings goes here"
};

Dựa trên những thông tin cần thu thập, bước tiếp theo chúng ta sẽ phải dò trong cấu trúc DOM các element này xem nó đang được tổ chức nhu thế nào để có thể dùng các selector tương ứng cho việc thao tác với nó.

itviec job item

Khi inspect element của trang kết quả search, ta có thể thấy những thông tin sau:

  • mỗi block chứa searched job nằm trong thẻ div có class name là job_content.
  • trong mỗi job đó, tên công ty có thể được lấy dựa trên thuộc tính href của thẻ a trong logo của công ty.
  • job title là phần text của thẻ a trong h2.title.
  • và cuối cùng, thời điểm mà job này được đăng so với hiện tại, có thể được lấy trong thẻ span có class name là distance-time.

Lưu ý: Cấu trúc này có thể bị thay đổi so với thời điểm hiện tại, khi trang web mà chúng ta đang scraping thay đổi DOM thì cũng phải inspect lại các bước này.

Bước cuối: Áp dụng TDD vào việc viết scraper (thật ra là vừa viết vừa sửa)

Với những selector trên, chúng ta có thể kiểm tra lại bằng các công cụ trên browser nhằm đảm bảo là lấy đúng đối tượng cần lấy. Trên browser mình thường dùng jQuery cho việc này, nhưng khi viết code NodeJS thì chúng ta có thể dùng cheerio để tương tác với các element trong DOM một cách tương tự như với jQuery. Chi tiết về cheerio tham khảo chi tiết tại đây.

Ngoài ra, để xử lý cho phần phân trang thì chúng ta có thể dựa vào element như hình dưới đây để lấy số lượng searched job, sau đó cho vào một vòng lặp. Trong mỗi vòng lặp sẽ gửi request để lấy trang tương ứng và thực hiện bóc tách kết quả trả về. Xem code demo để rõ hơn.

itviec search counting

Hàm demo cho việc bóc tách dữ liệu như sau:

const request = require('requestretry').defaults({ fullResponse: false }) // khai báo sử dụng thư viện requestretry
const cheerio = require('cheerio');  // khai báo sử dụng thư viện cheerio
const baseUrl = 'https://itviec.com';
const url = `${baseUrl}/it-jobs/reactjs/ho-chi-minh-hcm`; // 'reactjs' là searchKeyword, 'ho-chi-minh-hcm' là city mà bước 1 đã nêu

async function getJobsWithHeader() {
    try {
        let htmlResult = await request.get(url);
        let $ = await cheerio.load(htmlResult);
        const totalJobs = parseInt($("#jobs > h1").text().trim().split(" ")[0]); // Sample: "61 reactjs jobs in Ho Chi Minh for you"
        const jobsPerPage = 20; // kết quả search trên mỗi page

        for (let page = 1; page <= Math.ceil(totalJobs / jobsPerPage); page++) {
            htmlResult = await request.get(`${url}?page=${page}`);
            $ = await cheerio.load(htmlResult);

            $(".job_content").each((index, element) => {
                const companyLink = $(element).find(".logo-wrapper").children().attr("href");
                const companyName = companyLink.split("/")[2];
                const jobSelector = $(element).find("h2.title");
                const jobTitle = jobSelector.text().replaceAll('\n', '');
                const jobUrl = jobSelector.children().attr("href");
                const timeFromPosted = $(element).find("span.distance-time").text().replaceAll('\n', '');
                const jobResult = { companyName, jobTitle, jobUrl, timeFromPosted };
                scrapeResults.push(jobResult);
            });
        }
        return scrapeResults;
    } catch (err) {
        console.error(err)
    }
};

Function trên cho phép chúng ta lấy được các thông tin như: tên công ty, job title, thời gian post so với hiện tại, nhưng phần mô tả chi tiết công việc vẫn chưa có. Mà sẽ phải vào link chi tiết bằng jobUrl, cũng tương tự như trên, chúng ta cũng sẽ gửi 1 request tới jobUrl và thực hiện bóc tách kết quả trả về, lưu vào jobDescription của mỗi job object. Code đầy đủ tham khảo tại link Github này.

Kết

Như vậy toàn bộ ý tưởng cho việc scraping dữ liệu từ trang itviec này là gửi http request tới server > nhận response từ server > bóc tách response dựa trên cấu trúc cần lấy, hay một cách ngắn gọn mô tả vòng lặp này request-based scraping. Cách này nên là lựa chọn ưu tiên khi đã xác định được trang thích hợp do đạt perfomance rất tốt.