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

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 quanode.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ữaTí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ử

À 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

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


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

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
- Coi xem thằng này làm làm sao https://github.com/mozilla/readability
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é

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!