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

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
 
Tuần này ra bài sớm hơn bình thường nè, vì tự nhiên làm cái này thấy vui nên viết share luôn cho nóng, chứ đang lẽ theo plan sẽ là bài “Chê Nextjs 13 😝” rồ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
notion image
 
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é)
 

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ử
notion image
 
À 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
Ahuhu 😭
Ahuhu 😭
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?

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
notion image
 
notion image
 

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
notion image
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é
notion image
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!

Bài viết “lan quyên”

 

Loading Comments...

Follow me @thanhledev

Xin lỗi các bạn vì thời gian qua mình không dành thời gian viết nhiều. Dạo này mình khá bận cho dự án https://getnimbus.io. Check it out 🥳