server-queryselector aka парсим html в nodejs

от автора

Итак, мы хотим получить информацию с веб сайта — это можно сделать в 3 шага

1) Получить html сайта (пропустим этот шаг)

2) Распарсить html строку и создать dom. — builderdom.js

3) Найти нужные dom_node из dom по кссселекторам.

3.1) Распарсить строку кссселекторов и создать дерево для поиска. — cssselectorparser.js
3.2) Отфильтровать дом_ноды по дереву кссселекторов и найти нужные. — treeworker.js

2) Парсим html:

2.1) Нарезаем строки(выделил в отдельный проект superxmlparser74)
Создаем строки, накапливаем в них токены и обрезаем по маркерам

Таким образом у нас есть тег/innerTEXT — t, аттрибуты в виде массива — attr

клик
class superxmlparser74 {     static parse(str, cbOpenTag, cbInnerText, cbClosedTag, cbSelfOpenTag = () => {     }) {         let isOpen = false;         let startAttr = false;         let t = ''         let tAttrKey = '';         let tAttrValue = '';         let tAttrStart = false;         let tAttr = '';         let attr = [];         let prevCh = '';         for (let i = 0; i <= str.length - 1; i++) {             //(1)<li (2)class="breadcrumb-item-selected text-gray-light breadcrumb-item text-mono h5-mktg" aria-current="GitHub Student Developer Pack"(3)>GitHub Student Developer Pack(4)</li(5)>             //<selfclosing />             //comments // <!-- -->             if (str[i] === '/' && str[i + 1] === "/") {                 for (let j = i + 2; j <= str.length - 1; j++) {                     if (str[j] === '\n') {                         i = j;                         break;                     }                 }                 continue             } else if (str[i] === "<") { //1                 //comments <!-- -->                 if (str[i + 1] === '!' && str[i + 2] === "-" && str[i + 3] === "-") {                     for (let j = i + 4; j <= str.length - 1; j++) {                         if (str[j] === '-' && str[j + 1] === '-' && str[j + 2] === '>') {                             i = j + 2;                             break;                         }                     }                     continue                 }                 ///                  if (t.trim() !== '' && t.trim() !== "\n" && t.trim() !== "\t") {                     //cut innerTEXT 4                     cbInnerText({                         value: t                     });                     t = '';                 } else if (str[i + 1] !== "/") {                     cbInnerText({                         value: ""                     });                 }                 //open tag                 isOpen = true;                 if (str[i + 1] === "/") {                     isOpen = false;                     i = i + 1;                     continue;                 }             } else if (str[i] === '>') {                 ///closed tag - build 3/5                 if (isOpen) {                     if (prevCh === "/") {                         cbSelfOpenTag({                             tag: t,                             attr: attr                         })                     } else {                         cbOpenTag({                             tag: t,                             attr: attr,                         })                     }                 } else {                     cbClosedTag({})                 }                 attr = [];                 t = '';                 startAttr = false;                 isOpen = false;             } else {                 //accum str                 if ((!startAttr && str[i] !== ' ') || !isOpen) {                     t += str[i];                 } else if (startAttr) { //get attr 2                     if (str[i] === '=') {                         tAttrKey = tAttr                         tAttr = '';                     } else if (str[i] === '"') {                         tAttrStart = !tAttrStart;                         if (tAttrStart === false) {                             if (tAttrKey === 'class') {                                 tAttrValue = tAttr.split(" ");                             } else {                                 tAttrValue = [tAttr];                             }                             tAttr = '';                             attr.push({key: tAttrKey, value: tAttrValue});                             if (str[i + 1] === ' ') {                                 i = i + 1;                                 continue;                             }                         }                     } else {                         tAttr += str[i];                     }                  } else if (str[i] === ' ' && isOpen) {                     startAttr = true;                 }              }             prevCh = str[i];         }     } }

2.2) Создаем дерево

const superxmlparser74 = require("superxmlparser74");  class dom_node {     childrens = [];     innerTEXT = '';     tag;     treeWorker;      constructor() {         this.treeWorker = global.treeworker;     }      innerHTML = (cliFormat = false) => {         return this.treeWorker.getInnerHTML(this, cliFormat);     };     querySelector = (selector) => {         this.treeWorker.setCurrentTreeByNode(this);         return this.treeWorker.filtredBySelector(selector);     } }  class BuilderDOM {     html_to_dom(str) {         var utils = {             noEndTag(tag) {                 let noEndTags = [                     'noscript',                     'link',                     'base',                     'meta',                     'input',                     'svg',                     'path',                     'img',                     'br',                     'area',                     'base',                     'br',                     'col',                     'embed',                     'hr',                     'img',                     'input',                     'keygen',                     'link',                     'meta',                     'param',                     'source',                     'track',                     'wbr'                 ];                 return noEndTags.includes(tag);             }         };          let res = [];         let parentStack = [];         superxmlparser74.parse(str,             (item) => {                 //opentag                 if (item.tag === 'p' && parentStack[parentStack.length - 1]?.tag === 'p') {                     parentStack.pop();                 }                 //                 let el = new dom_node();                 el.attr = item.attr;                 el.tag = item.tag;                 res.push(el);                 el.attr.push({                     key: 'tag',                     value: [item.tag]                 })                 if (parentStack[parentStack.length - 1] && el.tag !== 'script') {                     parentStack[parentStack.length - 1].childrens.push(el)                 }                 if (!utils.noEndTag(el.tag)) {                     parentStack.push(el);                 }             },             (item) => {                 //innertext                 if (parentStack[parentStack.length - 1]) {                     parentStack[parentStack.length - 1].innerTEXT += item.value;                 }             },             (item) => {                 //closedtag                 parentStack.pop();             });          return res;     }  }

3) Поиск

3.1) Парсинг кссселекторов

Разбиваем строку кссселекторов по разделителям, определяем какой это кссселектор, обрезаем и создаем дерево.

class cssSelectorParser {     parse(str) {         let res = [];         str = this.utils.lex(str);         for (var i = 0; i <= str.length - 1; i++) {             if (str[i].includes(".")) {                 res.push({key: 'class', value: str[i].substring(1)});             } else if (str[i].includes("#")) {                 res.push({key: 'id', value: str[i].substring(1)});             } else if (str[i].includes("[")) {                 let current = str[i];                 current = current.substring(1);                 current = current.slice(0, -1);                 current = current.split("=");                 res.push({key: current[0], value: current[1]});             } else if (str[i] === '>') {                 res.push({key: '', value: str[i]});             } else if (str[i] === ' ') {                 res.push({key: '', value: str[i]});             } else if(str[i] !== '') {                 res.push({key: 'tag', value: str[i]});             }         }         //merge         let mergeRes = [];         let t = [];         for (var i = 0; i <= res.length - 1; i++) {             if (res[i].value === ' ') {                 mergeRes.push(t);                 t = [];             } else {                 t.push(res[i]);             }         }         mergeRes.push(t);         //         return mergeRes;     }       utils = {         lex(str) {             let res = '';             for (var i = 0; i <= str.length - 1; i++) {                 res += str[i];                 if (str[i + 1] === "." || str[i + 1] === '#' || str[i + 1] === '>' || str[i + 1] === '[' || (str[i] === ' ')) {                     res += "\n";                 } else if (str[i + 1] === " ") {                     res += "\n"                 }             }             return res.split("\n");         }     } }

3.2) Теперь отфильтруем дом_ноды по кссселекторам

class treeWorker {     //Текущий массив дом_ноде     _tree;       //Построить массив элементов всех детей ноды     setCurrentTreeByNode(node) {         let tree = this._getChildrens([node]);         this._tree = tree;     }     //Основной цикл, где мы и фильтруем dom по дереву кссселекторов     filtredBySelector(selector) {         let cssselectorParser = new cssSelectorParser();         selector = cssselectorParser.parse(selector);         let res;         for (let i = 0; i <= selector.length - 1; i++) {             let currentSelector = selector[i];             let key;             let item;             let isArrowSelector = (currentSelector[0].value === '>');             if (isArrowSelector) {                 continue;             }             for (var j = 0; j < currentSelector.length; j++) {                 key = currentSelector[j].key                 item = currentSelector[j].value;                 this._filtredByAttribute(key, item)             }             res = this._tree;             let nextSelectorArrow = selector[i + 1] && selector[i + 1][0] && selector[i + 1][0].value === '>';             this._sliceChildrens(nextSelectorArrow)         }         return res;     }       //Построить весь хтмл ноды     getInnerHTML(dom_node, cliFormat = false) {         let res = '';         let lvl = -1;         function deep(node) {             let leftMargin = '';             for (let i = 0; i <= lvl; i++) {                 leftMargin += (cliFormat) ? '   ' : '';             }             res += leftMargin + '<' + node.tag + ">"             res += (cliFormat) ? "\n" : "";             res += (cliFormat && node.innerTEXT !== '') ? leftMargin + '   ' : '';             res += node.innerTEXT;             res += (cliFormat && node.innerTEXT !== '') ? "\n" : "";             node.childrens.forEach((childNode) => {                 lvl++;                 deep(childNode);                 lvl--;             });             res += leftMargin + '';             res += (cliFormat && lvl !== -1) ? "\n" : "";         }           deep(dom_node);         return res;     }       //Фильтрация текущего массива дом_ноде по аттрибутам     _filtredByAttribute(_key, _value) {         this._tree = this._tree.filter((item) => {             let currentAttr = item.attr.find((attr) => attr.key === _key);             if (currentAttr) {                 return currentAttr.value.includes(_value.trim())             }         });     }     //Получить детей(первый срез или весь) текущего массива дом_ноде     _sliceChildrens(firstChild = false) {         let res = [];         if (firstChild) {             for (let i = 0; i <= this._tree.length - 1; i++) {                 res.push(...this._tree[i].childrens);             }         } else {             res = this._getChildrens(this._tree)         }         this._tree = res;     }       //Получить всех детей дом нод     _getChildrens(currentNodes) {         //get all childs         let allChilds = [...currentNodes];         let queue = [...currentNodes];         while(queue.length){             let item = queue.shift();             for(let i = 0; i <= item.childrens.length - 1; i++){                 queue.push(item.childrens[i]);                 allChilds.push(item.childrens[i]);             }         }         return allChilds;     }   }

Рассмотрим подробнее — Основной цикл, где мы и фильтруем «текущие элементы dom» по дереву кссселекторов.

//
Храним текущие дом_ноды в this._tree, фильтруем их, нарезаем детей, репит

filtredBySelector(selector) {         let cssselectorParser = new cssSelectorParser();         //Получаем дерево кссселекторов         selector = cssselectorParser.parse(selector);         let res;         //проходим по дереву         for (let i = 0; i <= selector.length - 1; i++) {             let currentSelector = selector[i];             let key;             let item;             //если текущ элем дерева - эрров - пропускаем фильтр             let isArrowSelector = (currentSelector[0].value === '>');             if (isArrowSelector) {                 continue;             }             //проходим по всем элементам текущего кссселектора             for (var j = 0; j < currentSelector.length; j++) {                 key = currentSelector[j].key                 item = currentSelector[j].value;                 //фильтруем текущее this._tree по аттрибутам                 this._filtredByAttribute(key, item)                 }             }             res = this._tree;             //если следующий элемент - эрров - срезаем только первый слой, если нет - всех детей             let nextSelectorArrow = selector[i + 1] && selector[i + 1][0] && selector[i + 1][0].value === '>';             this._sliceChildrens(nextSelectorArrow)         }         return res;     }

//

Эти сущности и выполняют основную работу, теперь создадим входную сущность documentServer.

class documentServer {       builderDOM = new BuilderDOM();     domTreeWorker;     startNode;     querySelector(selector) {         this.domTreeWorker.setCurrentTreeByNode(this.startNode);         return this.domTreeWorker.filtredBySelector(selector);     }       build(str) {         this.domTreeWorker = new treeWorker();         global.treeworker = this.domTreeWorker;         let dom = this.builderDOM.html_to_dom(str);         global.treeworker = null;         this.startNode = dom[0];     } }

Осталось реализовать фичу — квериселектор из ноды, поэтому прокинем domTreeWorker в дом_ноду через глобал

class dom_node {     childrens = [];     innerTEXT = '';     tag;     treeWorker;       constructor() {         this.treeWorker = global.treeworker;     }       innerHTML = (cliFormat = false) => {         return this.treeWorker.getInnerHTML(this, cliFormat);     };     querySelector = (selector) => {         this.treeWorker.setCurrentTreeByNode(this);         return this.treeWorker.filtredBySelector(selector);     } } 

Ссылка на гитхаб


ссылка на оригинал статьи https://habr.com/ru/post/703010/