Giới thiệu

Xin chào $5$ tỷ anh em, mình đang làm $1$ công việc cũng hết sức phổ biến hiện nay là cào dữ liệu. Nhưng vấn đề của mình không hoàn toàn là phải dùng selenium hỗ trợ lấy page source để trích xuất dữ liệu, nhưng để tối ưu nguồn tài nguyên thì mình có $1$ mini-pc đang trống nên mình quyết định dùng nó để phục vụ việc cào dữ liệu.

selenium

Mình dùng selenium cũng không nhiều nhưng mình cũng biết 1 số option hỗ trợ tối ưu như:

from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument("--start-maximized")  # Mở rộng cửa sổ trình duyệt
chrome_options.add_argument("disable-notifications")  # Tắt thông báo
chrome_options.add_argument("--disable-extensions")  # Tắt các tiện ích mở rộng
chrome_options.add_argument("--no-sandbox")  # Giúp ổn định hơn trên macOS
chrome_options.add_argument("--disable-dev-shm-usage")  # Giảm lỗi chia sẻ bộ nhớ
chrome_options.add_argument("--disable-gpu")  # Tắt GPU
chrome_options.add_argument("--headless") # Chế độ không hiển thị trình duyệt
chrome_options.add_argument("--incognito") # Chế độ ẩn danh
...

Thì mình nhận thấy option --headless là nó giúp mình chạy driver ngầm mà không cần hiển thị giao diện. Thì lúc này mình mới nảy ra suy nghĩ là: “Nếu option này nó ẩn giao diện đi thì với hệ điều hành ubuntu server kia không có giao diện thì có chạy được selenium không nhỉ?”.

Thế là mình lên google search các kiểu con đà điểu 🧐 và cũng tìm được kha khá solution nhưng đa phần mình không cài được. Loay hoay một hồi thì mình cũng làm được! yeah và hôm nay mình sẽ truyền lại bí kíp này cho các bạn. Vỗ tay cái nhờ 👏👏

Thực hiện

Oke, vậy thì mình bắt đầu với giao diện ngầu lòi của mấy anh hacker mà anh em hằng mong ước trước. Tôi sẽ dùng một số lệnh để xem thông tin máy trước.

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04 LTS
Release:        24.04
Codename:       noble
$ hostnamectl
 Static hostname: xxxx
       Icon name: computer-desktop
         Chassis: desktop 🖥️
      Machine ID: xxxx
         Boot ID: xxxx
Operating System: Ubuntu 24.04 LTS                
          Kernel: Linux 6.8.0-44-generic
    Architecture: x86-64
 Hardware Vendor: Default string
  Hardware Model: Default string
Firmware Version: 5.13
   Firmware Date: Sat 2023-06-10
    Firmware Age: 1y 3month 1w 3d

Đầu tiên mình sẽ cập nhật danh sách các gói phần mềm và các phiên bản mới nhất có sẵn từ các kho lưu trữ đã cấu hình trên hệ thống.

$ sudo apt update

Tiếp đó là cài đặt các gói phần mềm wgetunzip để tải và giải nén các tệp tin từ internet.

$ sudo apt install -y wget unzip

Tiếp tục tải về tệp cài đặt Google Chrome từ máy chủ của Google. (tôi hay dùng chrome nếu các bạn xài cái khác thì lên google tìm nhé!)

$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

Khi chạy xong thì tệp tin sẽ được tải xuống ở thư mục hiện tại, và mình sẽ phải di chuyển tệp tin này sang thư mục /tmp/.

$ sudo mv ./google-chrome-stable_current_amd64.deb /tmp/

Cuối cùng là cài đặt chrome vào máy.

$ sudo apt install -y /tmp/google-chrome-stable_current_amd64.deb

😜 thật ra cũng không bắt buộc phải di chuyển file vào /tmp/ đâu nhưng mình muốn mọi thứ được sắp xếp hoàn hảo giống như nên biết vị trí của mình ở đâu trong trái tim cô ấy vậy.

Thử dùng selenium và thực hiện cào 1 trang web

Setup xong rồi thì mình thử xem như nào. Đầu tiên mình tạo môi trường ảo (điều luôn luôn phải làm đầu tiên khi bắt đầu $1$ project).

$ python3 -m venv venv

Activate môi trường ảo đó.

$ source venv/bin/activate

Rồi cài đặt các thư viện cần thiết trong file requirements.txt.

selenium
webdriver_manager
pandas
bs4
openpyxl
$ pip3 install -r requirements.txt

Mình sẽ thử thực hiện lấy thông tin các công ty trong trang web https://pharmed.vn/exhibitors/companies, trang web này cần có mã để accept nhé!

Đây là mẫu sample

Mọi người có thể tham khảo source code của mình nhé.

# main.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.common.exceptions import NoSuchElementException
from time import sleep
from random import randint
from bs4 import BeautifulSoup
import pandas as pd
import ssl, os
ssl._create_default_https_context = ssl._create_stdlib_context
os.makedirs('screenshots', exist_ok=True)

home_url = 'https://pharmed.vn/exhibitors'
url_companies = home_url + '/companies?page={page_number}'
url_detail = home_url + '/company/{company_id}'

# Khởi tạo ChromeDriver
service = Service(ChromeDriverManager().install())

chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_argument("disable-notifications")
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument('--headless')
chrome_options.add_argument('--incognito')

def csv_to_xlsx(path):
    """
    Converts a CSV file to an XLSX file.
    
    Args:
    path: Path to the input CSV file.
    """
    # Read the CSV file
    df = pd.read_csv(path)
    
    # Save to an Excel file
    df.to_excel(f'{path[:-4]}.xlsx', index=False)
    return True

def scroll_down_until_element_found(driver, xpath, scroll_pause_time=1):
    """
    Scrolls down the page until the element specified by the given XPath is found.
    
    Args:
    driver: WebDriver object (Selenium).
    xpath: XPath string to locate the target element.
    scroll_pause_time: Time in seconds to pause between scrolls (default: 1).
    """
    last_height = driver.execute_script("return document.body.scrollHeight")

    while True:
        try:
            # Check if the element is present on the page
            element = driver.find_element(By.XPATH, xpath)
            if element:
                print("Element found!")
                break
        except NoSuchElementException:
            pass

        # Scroll down to the bottom of the page
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        
        # Wait for the page to load
        sleep(scroll_pause_time)

        # Calculate new scroll height and compare with the last scroll height
        new_height = driver.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            print("Reached the bottom of the page, element not found.")
            break

        last_height = new_height

def enter_code(driver):
    '''
    Accept the code and submit the search form

    Args:
    driver: WebDriver object (Selenium).
    '''
    input_xpath = '//div[@class="job-field"]//input'
    keyword = 'JP3HP'
    driver.find_element(By.XPATH, input_xpath).send_keys(keyword)
    print(f'Entered keyword: {keyword}')
    sleep(1)
    submit_xpath = '//button[@type="submit"]'
    driver.find_element(By.XPATH, submit_xpath).click()
    print('Submitted search form')
    sleep(1)

def get_company_info(driver):
    '''
    Get company information
    
    Args:
    driver: WebDriver object (Selenium).
    '''
    data = {}
    soup = BeautifulSoup(driver.page_source, 'html.parser')
    data['name'] = soup.find('div', class_='job-single-info3').h3.text
    data['logo'] = soup.find('div', class_='job-thumb').img['src']
    data['address'] = soup.find('div', class_='job-single-info3').span.text.split('Địa chỉ:')[-1].strip()
    data['description'] = soup.find('div', class_='job-single-info3').find('ul').find_all('li')[0].text.strip()
    data['about'] = soup.find('div', class_='job-details').p.text
    overviews = soup.find('div', class_='job-overview').find('ul').find_all('li')
    overviews = {item.find('h3').text.lower(): item.find('span').text.strip() for item in overviews}
    data.update(overviews)
    return data

def get_link(driver, pages):
    '''
    Get company links
    
    Args:
    driver: WebDriver object (Selenium).
    pages: List of page numbers.
    '''
    links = []
    for page_number in pages:
        driver.get(url_companies.format(page_number=page_number))
        sleep(randint(1, 2))
        page_xpath = '//div[@class="pagination pagination-homepage-v2"]'
        scroll_down_until_element_found(driver, page_xpath)
        link_path = '//div[@class="job-title-sec"]//h3//a'
        link = driver.find_elements(By.XPATH, link_path)
        links.extend([l.get_attribute('href') for l in link])
    return links

def get_data(driver, links, output = 'pharmed.csv'):
    '''
    Get company data

    Args:
    driver: WebDriver object (Selenium).
    links: List of company links.
    output: Output file path.
    '''
    stop_xpath = '//footer[@class="style2"]'
    for idx, link in enumerate(links, 1):
        try:
            print(f'> {idx}/{len(links)} -> {link}')
            driver.get(link)
            sleep(randint(1, 2))
            scroll_down_until_element_found(driver, stop_xpath)
            data = get_company_info(driver)
            name_screen = link.split('/')[-1]
            driver.save_screenshot(f"./screenshots/{name_screen}.png")
            data['link'] = link
            df = pd.DataFrame([data])
            df.to_csv(output, index=False, mode='a', header=not os.path.exists(output))
        except Exception as e:
            write_log(f'> {idx}/{len(links)} -> {link}: {e}')

def write_log(text):
    '''
    Write log to file

    Args:
    text: Log message.
    '''
    with open('log.txt', 'a', encoding='utf-8') as f:
        f.write(text+'\n')

if __name__ == "__main__":
    driver = webdriver.Chrome(service=service, options=chrome_options)
    driver.get(url_companies.format(page_number=1))
    sleep(randint(1, 2))
    enter_code(driver)
    links = get_link(driver, range(1, 25))
    get_data(driver, links)
    driver.quit()

Tips nhỏ để các bạn có thể xem giao diện trong đó chính là dùng

driver.save_screenshot(<path_save>)

Chúc các bạn thành công!