Итак, мы хотим получить информацию с веб сайта — это можно сделать в 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/
Добавить комментарий