Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров2.9K

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

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

Теги:
Хабы:
Всего голосов 2: ↑1 и ↓1+2
Комментарии2

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань