Thuật toán frontend: Tìm node chứa content chính

Bài viết được sự cho phép của tác giả Thanh Lê

Tại sao nên đọc bài này

  • Đập vào mặt những đứa nói làm Frontend thì không cần logic, thuật toán
  • Xem tui khoe công việc đang làm thôi

Vấn đề

Chuyện là mình đang build một feature cho https://getnimbus.io, trong đó có một tính năng gọi là Term explain, cơ bản khi bạn đang xem một trang web nào đó mà có một vài từ về web3 thì Nimbus sẽ giải thích từ đó là gì, một cách ngắn gọn nhất.

Đó cơ bản feature là vậy, tuy nhiên có một vấn đề nhỏ: Một trang web sẽ có rất nhiều content, và thường user khi đọc một article hay news thì thường sẽ chỉ focus vào content đó thôi. Nếu vậy sẽ cực kì khó chịu nếu mình show một đống term explain mà không năm trong main content.

Vậy câu hỏi tiếp theo, làm sao mình tự động detect được node nào trong cây DOM chứa main content, để từ đó lấy ra text rồi match trong đó xem có term nào cần explain không.

Thuật toán frontend

Rồi cái khó ở đây là làm sao xác định được cái div nào là main content nhỉ? Trước đây thì mình hard code, mà hôm nay thấy nhiều quá, làm không nổi. Vậy là suy nghĩ có cách nào để tìm được div chứa main content không.

Solution

Vậy là mình này ra một ý tưởng, thường cái div nào mà chứa nhiều text nhất sẽ là chứa content chính của trang. Tuy nhiên vì cấu trúc của một content cũng có thể có nhiều node bên trong đó, nếu vấy thì giải thuyết như này thì sao nhỉ

Ratio = (Node nào có text length / tổng sổ node bên trong) Node nào có cái ratio đó cao nhất thì khả năng cao là main content

Cùng test thử nhé

Một số thử basic
  • Bạn có thể lấy được text content của một node bằng node.innerText
  • Số lượng child node của div qua node.childElementCount hoặc lấy list children qua node.childNodes. (Note là node.childElementCount = node.childNodes.length nhé)

  Tổng hợp các thuật ngữ trong Frontend bạn nhất định phải biết!

  Giới thiệu về StoryBook cho dự án FrontEnd

Children count

Okey, lấy được số lượng text trong một node thì dễ rồi, đếm xem node đó có bao nhiêu node con (bao gồm cả cháu chắt chút chít,…) thì phải cần tí thuật toán rồi
const getTotalChildNode = (node, total = 0) => {
  if (!node.childElementCount) {
    return total;
  }

  return total + (node.childElementCount || 0) + Array.from(node.childNodes).map(child => getTotalChildNode(child)).reduce((a, b) => a + b, 0)
}

Vì cần tìm hết cháu chắt chút chít nên hãy suy nghĩ kiểu gì cùng phải dùng đệ qui (recursive) rồi nhé!

Công thức khi nào nên nghĩ tới đệ quy: – Khi input data có structure lặp lại và kết quả lại là tổng hợp của tất cả các lần lập lại đó.

Cụ thể ở đây là node.childNodes. Mình đang cần lấy cái childNodes để biết có bao nhiêu child đúng không, và từng thằng child trong đó mình cũng lại phải đếm childNodes của nó.

Cách viết đệ quy:

1. Điều kiện dừng

2. Tính kết quả và phân phối đệ quy.

Như ở trên là dk dừng là khi éo còn childElementCount nào nữa

Tính kết quả là total + (node.childElementCount || 0), phân phối là Array.from(node.childNodes).map(child => getTotalChildNode(child)).reduce((a, b) => a + b, 0)

Okey xong rồi á, mọi người có thể cầm lên Browser test thử
Thuật toán frontend
À quên nói, nãy giờ test thử ở trang này luôn nhé https://thanhle.blog/blog/thuat-ngu-trong-frontend-optimization

Ratio

const textNodeRatio = (node) => {
  const totalNodes = getTotalChildNode(node);
  if (!node.innerText || !totalNodes) {
    return 0;
  }

  const textLength = node.innerText.length;
  return textLength / getTotalChildNode(node);
}

const checkAllNodes = (node, result = []) => {
  result.push({node, ratio: textNodeRatio(node)});
  if (node.childElementCount) {
    Array.from(node.childNodes).forEach(child => {
      checkAllNodes(child, result);
    })
  }

  return result.sort((a, b) => b.ratio - a.ratio).slice(0, 10);
}

Như nãy đã giải thuyết, lấy số từ trong node rồi chia ra theo tổng số cháu chắt, thằng nào có ratio đó cao nhất nghĩa là main

Thuật toán frontend

A… đuồi ròi, list kết quả chỉ là cái div chứa content (Môt div chứa đoạn văn). Uhm đúng nhỉ, éo có childrent nào cả, toàn là text bên trong thì chả có ratio cao nhất rồi

DKM, cay, giả thuyết mình sai sao?

Xem ngay các tin đăng tuyển dụng Front-end lương cao trên TopDev

Context của bài toán

Pop lên trong đầu mình , nếu mình đi check các trang về tin tức, news đồ, chả lẽ cả trang tin thì dài có mấy trăm chữ?

Vậy mình nên filter bỏ ra mấy thằng div mà text ít quá nhỉ. Đơn giản.

const textNodeRatio = (node) => {
  const totalNodes = getTotalChildNode(node);
  if (!node.innerText || !totalNodes) {
    return 0;
  }

  const textLength = node.innerText.length;
  if (textLength < 1000) {
	// Thằng nào có text ít quá thì cho ra đê luôn 
    return 0;
  }

  return textLength / getTotalChildNode(node);
}

Test lại

Thuật toán frontend
Thuật toán frontend

Full script

const getTotalChildNode = (node, total = 0) => {
  if (!node.childElementCount) {
    return total;
  }

  return total + (node.childElementCount || 0) + Array.from(node.childNodes).map(child => getTotalChildNode(child)).reduce((a, b) => a + b, 0)
}

const textNodeRatio = (node) => {
  const totalNodes = getTotalChildNode(node);
  if (!node.innerText || !totalNodes) {
    return 0;
  }

  const textLength = node.innerText.length;
  if (textLength < 1000) {
    return 0;
  }

  return textLength / getTotalChildNode(node);
}

const checkAllNodes = (node, result = []) => {
  result.push({node, ratio: textNodeRatio(node)});
  if (node.childElementCount) {
    Array.from(node.childNodes).forEach(child => {
      checkAllNodes(child, result);
    })
  }

  return result.sort((a, b) => b.ratio - a.ratio).slice(0, 10);
}

Độ hoàn thiện

Nói chung là mình test một vài trang khá ok, một vài trang content rất dài nên dẫn tới cái điều kiện < 1000 có vẻ không ổn lắm.

So far so good, thôi tạm ổn để sài đã . Để suy nghĩ thêm xem có cách gì khiến nó stable hơn không đã. Bạn đọc có idea gì thì giúp mình với nhá.

Đánh giá performance

Nói chung đoạn này nên check thử xem cái thuật toán của mình có thực sự chạy được không, kiểu logic đúng mà 5p sau biến laptop của user thành cái chảo chiên trứng thì hơi… độc ác phải không nào.

Đánh giá performance
Tuy nhiên, khi test thử trên con Macbook Pro M1 16GB RAM 512GB SSD thì thấy nó khá nhanh, nên thôi đoạn này mình lười viết quá. Rảnh sẽ bổ sung thêm.

Một số idea khác

  • Tìm xem div nào chiếm nhiều viewport nhất
  • Div nào chứa nhiều thẻ p trong đó nhất

Tổng kết

Đó thấy đó, đôi khi giỏi thuật toán sẽ giúp bạn giải quyết những bài toán ảo ma canada, xuka yêu chai en vậy. Vì vậy đừng coi thường thuật toán nhé!

Thuật toán frontend
Nói đi thì cũng phải nói lại, thuật toán chỉ là công cụ để bạn giải quyết vấn đề, mình thấy quan trọng vẫn là cách bạn nghĩ về vấn đề và idea để solve nó. Vì vậy lâu lâu hãy thử luyện tập nhìn xem có vấn đề gì hay ho có thể giải quyết được không nhé!

Have a good weekend!