//+------------------------------------------------------------------+ //| WebDataExtractor.mqh | //| Copyright (c) 2015-2019, Marketeer | //| https://www.mql5.com/en/users/marketeer | //| https://www.mql5.com/en/articles/5706/ | //| https://www.mql5.com/en/articles/5913/ | //| HTML to CSV converter | //+------------------------------------------------------------------+ //#define HASHMAP_WARNING // HASHMAP_VERBOSE #include #include input GroupSettings HTMLR_Settings; // H T M L R E P O R T S E T T I N G S input string RowSelector = "tr:has-n-children(13)[bgcolor^=\"#F\"]"; // · RowSelector input string ColumnSettingsFile = "ReportHistoryDeals.cfg.csv"; // · ColumnSettingsFile input string SubstitutionSettingsFile = ""; // · SubstitutionSettingsFile input bool LogDomElements = false; // · LogDomElements input bool LogDomWarnings = false; // · LogDomWarnings input bool LogSelectedElements = false; // · LogSelectedElements #define LOG_MASK_DOM 1 #define LOG_MASK_DOM_WARNING 2 #define LOG_MASK_SELECTOR 4 #define LOG_MASK_MAX 4 #define LOG_MASK_ALL ((LOG_MASK_MAX << 1) - 1) ushort combinators[] = { ' ', '+', '>', '~' }; string empty_tags[] = { #include }; /* CSS selectors: 1 * any element 1 .intro class="intro" 1 #id1 id="id1" 1 p p tag 1 div p p tag somewhere inside div tag 2 div > p p tag with parent div tag 2 div + p p tag as a sibling immediately after div tag, common parent 3 div ~ p p tag as a sibling somewhere after div tag (less strict than +), common parent 2 [target] a tag with target attribute 2 [target=_blank] a tag with attribute target="_blank" 2- [title~=flower] a tag with attribute title containing "space"-separated word "flower" somewhere 3 [title*=flower] a tag with attribute title containing "flower" sequence somewhere (less strict than ~) 2- [data-value|="foo"] a tag whose attribute value has this "foo" in a dash-separated list somewhere 3 [data-value^="foo"] attribute value starts with this 3 [data-value$="foo"] attribute value ends with this 2 p:first-child a tag

that is the first child of its parent 3 p:last-child a tag

that is the last child of its parent 3 p:nth-child(n) 3 p:nth-last-child(n) 3- p:not(selector) http://www.w3.org/TR/CSS2/selector.html A simple selector is either a type selector or universal selector followed immediately by zero or more attribute selectors, ID selectors, or pseudo-classes, in any order. The simple selector matches if all of its components match. A selector is a chain of one or more simple selectors separated by combinators. It always begins with a type selector or a universal selector. No other type selector or universal selector is allowed in the sequence. Combinators are: white space, ">", and "+" (and "~" css3) */ enum StateBit { blank, insideTagOpen, insideTagClose, insideComment, insideScript }; enum AttrBit { name, value }; class AttributesParser { private: AttrBit state; int offset; int cursor; int length; string key; void skipWhiteSpace(const string &data) { int n = StringLen(data); while(offset < n && StringGetCharacter(data, offset) <= 32) { offset++; cursor++; } } public: AttributesParser(): state(AttrBit::name), offset(0), cursor(0), key(""){} void parseAll(const string &data, IndexMap &attributes) { length = StringLen(data); while(parse(data, attributes)); if(offset < length) { string ending = StringSubstr(data, cursor); StringTrimRight(ending); StringTrimLeft(ending); if(StringLen(ending) > 0) { attributes.set(ending); } else if(key != "") { attributes.set(key); } } else if(key != "") { attributes.set(key); } } bool parse(const string &data, IndexMap &attributes) { skipWhiteSpace(data); int x = 0; if(state == AttrBit::name) { while(offset < length) { x = StringGetCharacter(data, offset); if(x == '=' || (x <= 32)) break; offset++; } string attr = StringSubstr(data, cursor, offset - cursor); if(StringLen(attr) > 0) { key = attr; } if(x == '=') { state = AttrBit::value; offset++; cursor = offset; } // else - attribute without value return(offset < length); } else if(state == AttrBit::value) { if(key == "") { Print("Wrong state::value with empty key"); } ushort c = StringGetCharacter(data, offset); if(c == '"' || c == '\'') { offset++; cursor++; while(offset < length) { if(StringGetCharacter(data, offset) == c) break; offset++; } attributes.setValue(key, StringSubstr(data, cursor, offset - cursor)); key = ""; state = AttrBit::name; offset++; cursor = offset; return(offset < length); } else if(c > 32) { while(offset < length) { if(StringGetCharacter(data, offset) == ' ') break; offset++; } attributes.setValue(key, StringSubstr(data, cursor, offset - cursor)); key = ""; state = AttrBit::name; offset++; cursor = offset; return(offset < length); } else if(c <= 32) // empty value, 'attr=' { while(offset < length) { if(StringGetCharacter(data, offset) > ' ') break; offset++; } attributes.set(key); key = ""; state = AttrBit::name; cursor = offset; return(offset < length); } } return(false); } }; enum PseudoClassModifier { none, firstChild, lastChild, nthChild, nthLastChild, hasNthChildren }; class SubSelector { public: ushort type; string value; PseudoClassModifier modifier; string param; SubSelector(ushort t, string v): type(t), value(v), modifier(PseudoClassModifier::none) {} SubSelector(ushort t, string v, PseudoClassModifier m): type(t), value(v), modifier(m) {} SubSelector(ushort t, string v, PseudoClassModifier m, string p): type(t), value(v), modifier(m), param(p) {} }; class SubSelectorArray { private: SubSelector *selectors[]; IndexMap mod; static TypeContainer first; static TypeContainer last; static TypeContainer nth; static TypeContainer nthLast; static TypeContainer hasN; void init() { mod.add(":first-child", &first); mod.add(":last-child", &last); mod.add(":nth-child", &nth); mod.add(":nth-last-child", &nthLast); mod.add(":has-n-children", &hasN); } void createFromString(const string &selector) { ushort p = 0; // previous/pending type int ppos = 0; int i, n = StringLen(selector); bool quotes = false; for(i = 0; i < n; i++) { ushort t = StringGetCharacter(selector, i); if(t == '"') { quotes = !quotes; } if(quotes) continue; if(t == '.' || t == '#' || t == '[' || t == ']') { string v = StringSubstr(selector, ppos, i - ppos); if(i == 0) v = "*"; if(p == '[' && StringLen(v) > 0 && StringGetCharacter(v, StringLen(v) - 1) == ']') { v = StringSubstr(v, 0, StringLen(v) - 1); } add(p, v); p = t; if(p == ']') p = 0; ppos = i + 1; } } if(ppos < n) { string v = StringSubstr(selector, ppos, n - ppos); if(p == '[' && StringLen(v) > 0 && StringGetCharacter(v, StringLen(v) - 1) == ']') { v = StringSubstr(v, 0, StringLen(v) - 1); } add(p, v); } } public: SubSelectorArray() { init(); } SubSelectorArray(const string selector) { init(); createFromString(selector); } int size() const { return ArraySize(selectors); } SubSelector *operator[](int i) const { return selectors[i]; } void add(const ushort t, string v) { int n = ArraySize(selectors); ArrayResize(selectors, n + 1); PseudoClassModifier m = PseudoClassModifier::none; string param; for(int j = 0; j < mod.getSize(); j++) { int p = StringFind(v, mod.getKey(j)); if(p > -1) { if(p + StringLen(mod.getKey(j)) < StringLen(v)) { param = StringSubstr(v, p + StringLen(mod.getKey(j))); if(StringGetCharacter(param, 0) == '(' && StringGetCharacter(param, StringLen(param) - 1) == ')') { param = StringSubstr(param, 1, StringLen(param) - 2); } else { param = ""; } } m = mod[j].get(); v = StringSubstr(v, 0, p); break; } } if(t == '[' && m == PseudoClassModifier::none) { AttributesParser p; IndexMap attr; p.parseAll(v, attr); // attributes are selected one by one: element[attr1=value][attr2=value] // the map should contain only 1 valid pair at a time if(attr.getSize() > 0) { param = attr.getKey(0); v = attr[0] != NULL ? attr[0].get() : ""; } } if(StringLen(param) == 0) { selectors[n] = new SubSelector(t, v, m); } else { selectors[n] = new SubSelector(t, v, m, param); } } ~SubSelectorArray() { int i, n = ArraySize(selectors); for(i = 0; i < n; i++) { delete(selectors[i]); } } }; TypeContainer SubSelectorArray::first(PseudoClassModifier::firstChild); TypeContainer SubSelectorArray::last(PseudoClassModifier::lastChild); TypeContainer SubSelectorArray::nth(PseudoClassModifier::nthChild); TypeContainer SubSelectorArray::nthLast(PseudoClassModifier::nthLastChild); TypeContainer SubSelectorArray::hasN(PseudoClassModifier::hasNthChildren); class DomIterator; class DomElement { private: string name; string content; IndexMap attributes; DomElement *parent; int level; protected: bool childrenOwner; DomElement *children[]; void clear() { int i, n = ArraySize(children); for(i = 0; i < n; i++) { delete children[i]; } ArrayResize(children, 0); } bool isCombinator(ushort c) { for(int i = 0; i < ArraySize(combinators); i++) { if(combinators[i] == c) return(true); } return(false); } public: DomElement(): parent(NULL), childrenOwner(true) {} DomElement(const string n): parent(NULL), childrenOwner(true) { name = n; } DomElement(const string n, const string text): parent(NULL), childrenOwner(true) { name = n; content = text; } DomElement(DomElement *p, const string &n, const string text = ""): childrenOwner(true) { p.addChild(&this); parent = p; level = p.level + 1; name = n; if(text != "") content = text; } ~DomElement() { if(childrenOwner) { clear(); } } void addChild(DomElement *child) { int n = ArraySize(children); ArrayResize(children, n + 1); children[n] = child; } int getChildrenCount() const { return ArraySize(children); } DomElement *getChild(const int i) const { if(i >= 0 && i < ArraySize(children)) { return children[i]; } return NULL; } void addChildren(DomElement *p) { if(CheckPointer(p) == POINTER_DYNAMIC) { for(int i = 0; i < p.getChildrenCount(); i++) { addChild(p.getChild(i)); } } } int getChildIndex(DomElement *e) const { for(int i = 0; i < ArraySize(children); i++) { if(children[i] == e) return(i); } return(-1); } void setName(string n) { name = n; } void setText(string t) { content = t; } DomElement *getParent() const { return parent; } string getName() const { return name; } string getText() const { string text; for(int i = 0; i < ArraySize(children); i++) { text += children[i].getText(); } return content + text; } int getLevel() const { return level; } void print(bool full = true) { PrintFormat("%" + IntegerToString(level) + "c %s, %s", ' ', name, attributes.asString()); if(full) { int i, n = ArraySize(children); for(i = 0; i < n; i++) { children[i].print(); } } } void printWithContent(bool full = true) { PrintFormat("%" + IntegerToString(level) + "c %s, %s, %s", ' ', name, content, attributes.asString()); if(full) { int i, n = ArraySize(children); for(i = 0; i < n; i++) { children[i].printWithContent(); } } } void printShort() { string id = ""; Container *pid = attributes["id"]; if(pid != NULL) id = pid.get(); PrintFormat("%" + IntegerToString(level) + "c %d %s %s", ' ', level, name, id); } void printShortWithChildren() { string id = ""; Container *pid = attributes["id"]; if(pid != NULL) id = pid.get(); PrintFormat("%" + IntegerToString(level) + "c %d %s %s", ' ', level, name, id); int i, n = ArraySize(children); for(i = 0; i < n; i++) { children[i].printShort(); } } void parseAttributes(const string &data) { AttributesParser p; p.parseAll(data, attributes); } void setAttribute(const string id, Container *value) { attributes.set(id, value); } bool hasAttribute(const string key) const { return (attributes.isKeyExisting(key)); } string getAttribute(const string key) const { Container *c = attributes[key]; if(c != NULL) return c.get(); return ""; } string getAttribute(const int index) const { Container *c = attributes[index]; if(c != NULL) return c.get(); return ""; } bool match(const SubSelectorArray *u) { bool matched = true; int i, n = u.size(); for(i = 0; i < n && matched; i++) { if(u[i].type == 0) // tag name { if(u[i].value == "*") { // any tag } else if(StringLen(u[i].value) > 0 && StringCompare(name, u[i].value) != 0) { matched = false; } else if(u[i].modifier == PseudoClassModifier::firstChild) { if(parent != NULL && parent.getChildIndex(&this) != 0) { matched = false; } } else if(u[i].modifier == PseudoClassModifier::lastChild) { if(parent != NULL && parent.getChildIndex(&this) != parent.getChildrenCount() - 1) { matched = false; } } else if(u[i].modifier == PseudoClassModifier::nthChild) { int x = (int)StringToInteger(u[i].param); if(parent != NULL && parent.getChildIndex(&this) != x - 1) // children are counted starting from 1 { matched = false; } } else if(u[i].modifier == PseudoClassModifier::nthLastChild) { int x = (int)StringToInteger(u[i].param); if(parent != NULL && parent.getChildrenCount() - parent.getChildIndex(&this) - 1 != x - 1) { matched = false; } } else if(u[i].modifier == PseudoClassModifier::hasNthChildren) { int x = (int)StringToInteger(u[i].param); if(getChildrenCount() != x) { matched = false; } } } else if(u[i].type == '.') // class { if(attributes.isKeyExisting("class")) { Container *c = attributes["class"]; if(c == NULL || StringFind(" " + c.get() + " ", " " + u[i].value + " ") == -1) { matched = false; } } else { matched = false; } } else if(u[i].type == '#') // id { if(attributes.isKeyExisting("id")) { Container *c = attributes["id"]; if(c == NULL || StringCompare(c.get(), u[i].value) != 0) { matched = false; } } else { matched = false; } } else if(u[i].type == '[') // attributes { string key = u[i].param; string v = u[i].value; ushort suffix = StringGetCharacter(key, StringLen(key) - 1); if(suffix == '*' || suffix == '^' || suffix == '$') // contains, starts with, or ends with { key = StringSubstr(key, 0, StringLen(key) - 1); } else { suffix = 0; } if(hasAttribute(key) && attributes[key] != NULL) { if(StringLen(v) > 0) { if(suffix == 0) { if(key == "class") { matched &= (StringFind(" " + attributes[key].get() + " ", " " + v + " ") > -1); } else { matched &= (StringCompare(v, attributes[key].get()) == 0); } } else if(suffix == '*') { matched &= (StringFind(attributes[key].get(), v) != -1); } else if(suffix == '^') { matched &= (StringFind(attributes[key].get(), v) == 0); } else if(suffix == '$') { string x = attributes[key].get(); if(StringLen(x) > StringLen(v)) { matched &= (StringFind(x, v, StringLen(x) - StringLen(v)) == StringLen(v)); } } } } else { matched = false; } } } return matched; } bool find(const ushort op, const SubSelectorArray *selectors, DomIterator *output) { bool found = false; int i, n; if(op == ' ' || op == '>' || op == '/') { n = ArraySize(children); for(i = 0; i < n; i++) { if(children[i].match(selectors)) { if(op == '/') { found = true; output.addChild(GetPointer(children[i])); } else if(op == ' ') { DomElement *p = &this; while(p != NULL) { if(output.getChildIndex(p) != -1) { found = true; output.addChild(GetPointer(children[i])); break; } p = p.parent; } } else // op == '>' { if(output.getChildIndex(&this) != -1) { found = true; output.addChild(GetPointer(children[i])); } } } children[i].find(op, selectors, output); } } else if(op == '+' || op == '~') { if(CheckPointer(parent) == POINTER_DYNAMIC) { if(output.getChildIndex(&this) != -1) { int q = parent.getChildIndex(&this); if(q != -1) { n = (op == '+') ? (q + 2) : parent.getChildrenCount(); if(n > parent.getChildrenCount()) n = parent.getChildrenCount(); for(i = q + 1; i < n; i++) { DomElement *m = parent.getChild(i); if(CheckPointer(m) != POINTER_DYNAMIC) { Print("bad:", name, " i=", i, " q=", q, " n=", n); } if(m.match(selectors)) { found = true; output.addChild(m); } } } else { Print("Error: can't find 'this' element"); } } } else { if(name != "root") { Print("No parent for: ", name); } } for(i = 0; i < ArraySize(children); i++) { found = children[i].find(op, selectors, output) || found; } } else { Print("Error: unknown combinator:", ShortToString(op)); } return found; } DomIterator *querySelect(const string q) { DomIterator *result = new DomIterator(); if(q == ".") { result.addChild(&this); // root return result; } int cursor = 0; // where selector string started int i, n = StringLen(q); ushort p = 0; // previous character ushort a = 0; // next/pending operator ushort b = '/'; // current operator, root notation from the start string selector = "*"; // current simple selector, 'any' by default int index = 0; // position in the resulting array of objects for(i = 0; i < n; i++) { ushort c = StringGetCharacter(q, i); if(isCombinator(c)) { a = c; if(!isCombinator(p)) { selector = StringSubstr(q, cursor, i - cursor); } else { // suppress blanks around other combinators a = MathMax(c, p); } cursor = i + 1; } else { if(isCombinator(p)) { index = result.getChildrenCount(); SubSelectorArray selectors(selector); find(b, &selectors, result); b = a; // now we can delete outdated results in positions up to 'index' result.removeFirst(index); } } p = c; } if(cursor < i) { selector = StringSubstr(q, cursor, i - cursor); index = result.getChildrenCount(); SubSelectorArray selectors(selector); find(b, &selectors, result); result.removeFirst(index); } return result; } IndexMap *tableSelect(const string rowSelector, const string &headers[], const string &columSelectors[], const string &dataSelectors[], const IndexMap *subst = NULL, const bool numericKeys = false) { if(ArraySize(columSelectors) != ArraySize(dataSelectors)) return NULL; int n = ArraySize(columSelectors); DomIterator *r = querySelect(rowSelector); if((HtmlParser::isDebug() & LOG_MASK_SELECTOR) != 0) { r.printAllWithContent(); } IndexMap *data = new IndexMap('\n'); int counter = 0; r.rewind(); while(r.hasNext()) { DomElement *e = r.next(); if((HtmlParser::isDebug() & LOG_MASK_SELECTOR) != 0) { Print("row N" + (string)counter); } string id = IntegerToString(counter); if(!numericKeys) // we can store IDs of found elements in results, optionally { if(e.hasAttribute("id")) id += "-" + e.getAttribute("id"); } IndexMap *row = new IndexMap(); for(int i = 0; i < n; i++) { if(StringLen(columSelectors[i])) { DomIterator *d = e.querySelect(columSelectors[i]); if((HtmlParser::isDebug() & LOG_MASK_SELECTOR) != 0) { d.printAllWithContent(); } string value; if(d.getChildrenCount() > 0) { if(d.getChildrenCount() > 1) { if((HtmlParser::isDebug() & LOG_MASK_SELECTOR) != 0) { Print("Too many elements selected:", d.getChildrenCount()); } } if(dataSelectors[i] == "") { value = d[0].getText(); } else { value = d[0].getAttribute(dataSelectors[i]); } if(CheckPointer(subst) != POINTER_INVALID) { if(subst.isKeyExisting(columSelectors[i])) { IndexMap *rules = dynamic_cast(subst[columSelectors[i]]); if(rules != NULL) { for(int j = 0; j < rules.getSize(); j++) { StringReplace(value, rules.getKey(j), (rules[j] != NULL ? rules[j].asString() : "")); } } } } StringTrimLeft(value); StringTrimRight(value); if(numericKeys) { row.setValue(IntegerToString(i), value); } else { row.setValue(headers[i]/*columSelectors[i] + dataSelectors[i]*/, value); } } else // field not found { if(numericKeys) { row.set(IntegerToString(i)); } else { row.set(columSelectors[i] + dataSelectors[i]); } } delete d; } else // constant data { if(numericKeys) { row.setValue(IntegerToString(i), dataSelectors[i]); } else { row.setValue(dataSelectors[i], dataSelectors[i]); } } } if(row.getSize() > 0) { data.set(id, row); counter++; } else { delete row; } } delete r; return data; } }; class DomIterator: public DomElement { private: int cursor; public: DomIterator() { childrenOwner = false; } bool hasNext() { return(cursor < ArraySize(children)); } DomElement *next() { if(hasNext()) { return children[cursor++]; } return NULL; } DomElement *operator[](int index) { if(index >= 0 && index < ArraySize(children)) { return children[index]; } return NULL; } void rewind() { cursor = 0; } void removeFirst(const int n) { int m = ArraySize(children); if(n < m) { ArrayCopy(children, children, 0, n); ArrayResize(children, m - n); } else if(n == m) { ArrayResize(children, 0); } } void printAll() { rewind(); while(hasNext()) { DomElement *e = next(); e.print(false); } } void printAllWithContent() { rewind(); while(hasNext()) { DomElement *e = next(); e.printWithContent(false); } } }; class HtmlParser { private: const string TAG_OPEN_START; const string TAG_OPEN_STOP; const string TAG_OPENCLOSE_STOP; const string TAG_CLOSE_START; const string TAG_CLOSE_STOP; const string COMMENT_START; const string COMMENT_STOP; const string SCRIPT_STOP; StateBit state; DomElement *root; DomElement *cursor; int offset; IndexMap empties; static int debugLevel; public: HtmlParser(): TAG_OPEN_START("<"), TAG_OPEN_STOP(">"), TAG_OPENCLOSE_STOP("/>"), TAG_CLOSE_START(""), COMMENT_START(""), SCRIPT_STOP("/script>"), state(blank) { for(int i = 0; i < ArraySize(empty_tags); i++) { empties.set(empty_tags[i]); } } ~HtmlParser() { if(root != NULL) { delete root; } } DomElement *parse(const string &html) { if(root != NULL) { delete root; } root = new DomElement("root"); cursor = root; offset = 0; while(processText(html)); return root; } bool processText(const string &html) { int p; if(state == blank) { p = StringFind(html, "<", offset); if(p == -1) // no more tags { return(false); } else if(p > 0) { if(p > offset) { string text = StringSubstr(html, offset, p - offset); StringTrimLeft(text); StringTrimRight(text); StringReplace(text, " ", ""); if(StringLen(text) > 0) { cursor.setText(text); } } } offset = p; if(IsString(html, COMMENT_START)) state = insideComment; else if(IsString(html, TAG_CLOSE_START)) state = insideTagClose; else if(IsString(html, TAG_OPEN_START)) state = insideTagOpen; return(true); } else if(state == insideTagOpen) { offset++; int pspace = StringFind(html, " ", offset); int pright = StringFind(html, ">", offset); p = MathMin(pspace, pright); if(p == -1) { p = MathMax(pspace, pright); } if(p == -1 || pright == -1) // no tag closing { return(false); } if(pspace > pright) { pspace = -1; // outer space } bool selfclose = false; if(IsString(html, TAG_OPENCLOSE_STOP, pright - StringLen(TAG_OPENCLOSE_STOP) + 1)) { selfclose = true; if(p == pright) p--; pright--; } string name = StringSubstr(html, offset, p - offset); StringToLower(name); StringTrimRight(name); DomElement *e = new DomElement(cursor, name); if(pspace != -1) { string txt; if(pright - pspace > 1) { txt = StringSubstr(html, pspace + 1, pright - (pspace + 1)); e.parseAttributes(txt); } } if((HtmlParser::isDebug() & LOG_MASK_DOM) != 0) { e.print(false); } bool softSelfClose = false; if(!selfclose) { if(empties.isKeyExisting(name)) { selfclose = true; softSelfClose = true; } } pright++; if(!selfclose) { cursor = e; } else { if(!softSelfClose) pright++; } offset = pright; if((name == "script") && !selfclose) { state = insideScript; } else { state = blank; } return(true); } else if(state == insideTagClose) { offset += StringLen(TAG_CLOSE_START); p = StringFind(html, ">", offset); if(p == -1) { return(false); } string tag = StringSubstr(html, offset, p - offset); StringToLower(tag); DomElement *rewind = cursor; while(StringCompare(cursor.getName(), tag) != 0) { string previous = cursor.getName(); cursor = cursor.getParent(); if(cursor == NULL) { // orphan closing tag cursor = rewind; state = blank; offset = p + 1; return(true); } if((isDebug() & LOG_MASK_DOM_WARNING) != 0) { Print("Misplaced /", tag, ">. Go from ", previous, " up to ", cursor.getName(), " ", cursor.getLevel()); } } cursor = cursor.getParent(); if(cursor == NULL) return(false); state = blank; offset = p + 1; return(true); } else if(state == insideComment) { offset += StringLen(COMMENT_START); p = StringFind(html, COMMENT_STOP, offset); if(p == -1) { return(false); } offset = p + StringLen(COMMENT_STOP); state = blank; return(true); } else if(state == insideScript) { p = StringFind(html, SCRIPT_STOP, offset); if(p == -1) { return(false); } offset = p + StringLen(SCRIPT_STOP); state = blank; cursor = cursor.getParent(); if(cursor == NULL) return(false); return(true); } return(false); } bool IsString(const string &html, const string x, int subset = -1) { if(subset == -1) subset = offset; return(StringSubstr(html, subset, StringLen(x)) == x); } static void enableDebug(int level) { debugLevel = level; } static int isDebug() { return debugLevel; } }; int HtmlParser::debugLevel = 0; class HTMLConverter { protected: static bool loadColumnConfig(string &columnSelectors[], string &dataSelectors[], string &headers[]) { Print("Reading column configuration ", ColumnSettingsFile); int h = FileOpen(ColumnSettingsFile, FILE_READ|FILE_ANSI|FILE_TXT|FILE_SHARE_READ|FILE_SHARE_WRITE, '|', CP_UTF8); if(h == -1) { Print("Error reading file '", ColumnSettingsFile, "': ", GetLastError()); return false; } int n = 0; bool headerRead = false; while(!FileIsEnding(h)) { string stParts[]; string stLine = FileReadString(h); int nParts = StringSplit(stLine, ',', stParts); if(nParts == 3) { if(!headerRead) { headerRead = true; continue; } ArrayResize(columnSelectors, n + 1); ArrayResize(dataSelectors, n + 1); ArrayResize(headers, n + 1); headers[n] = stParts[0]; columnSelectors[n] = stParts[1]; dataSelectors[n] = stParts[2]; // Print("Column ", (n + 1), ": '", stParts[0], "', selector: '", stParts[1], "', locator: '", stParts[2], "'"); n++; } } FileClose(h); return true; } static bool loadSubstConfig(int &rulesColumns[], string &rulesHave[], string &rulesWant[]) { Print("Reading column configuration ", SubstitutionSettingsFile); int h = FileOpen(SubstitutionSettingsFile, FILE_READ|FILE_ANSI|FILE_TXT|FILE_SHARE_READ|FILE_SHARE_WRITE, '|', CP_UTF8); if(h == -1) { Print("Error reading file '", SubstitutionSettingsFile, "': ", GetLastError()); return false; } int n = 0; bool headerRead = false; while(!FileIsEnding(h)) { string stParts[]; string stLine = FileReadString(h); int nParts = StringSplit(stLine, ',', stParts); if(nParts == 3) { if(!headerRead) { headerRead = true; continue; } ArrayResize(rulesColumns, n + 1); ArrayResize(rulesHave, n + 1); ArrayResize(rulesWant, n + 1); rulesColumns[n] = (int)StringToInteger(stParts[0]) - 1; if(rulesColumns[n] < 0) { Print("Invalid column number ", rulesColumns[n], " changed to 0"); rulesColumns[n] = 0; } rulesHave[n] = stParts[1]; rulesWant[n] = stParts[2]; // Print("Rule ", (n + 1), " for column ", (rulesColumns[n] + 1), ": find '", stParts[1], "', replace with: '", stParts[2], "'"); n++; } } FileClose(h); return true; } public: static IndexMap *convertReport2Map(const string URL, const bool columnNames = false) { if(URL == "") { Print("Parameter URL can not be empty"); return NULL; } if(RowSelector == "") { Print("Enter at least one of parameters: RowSelector, SaveName, TestQuery"); return NULL; } HtmlParser p; string xml; Print("Reading html-file ", URL); int h = FileOpen(URL, FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_SHARE_READ|FILE_ANSI, 0, CP_UTF8); if(h == INVALID_HANDLE) { Print("Error reading file '", URL, "': ", GetLastError()); return NULL; } StringInit(xml, (int)FileSize(h)); while(!FileIsEnding(h)) { xml += FileReadString(h) + "\n"; } // xml = FileReadString(h, (int)FileSize(h)); - has 4095 bytes limit in binary files! FileClose(h); int logLevel = 0; if(LogDomElements) logLevel |= LOG_MASK_DOM; if(LogDomWarnings) logLevel |= LOG_MASK_DOM_WARNING; if(LogSelectedElements) logLevel |= LOG_MASK_SELECTOR; if(logLevel != 0) { p.enableDebug(logLevel); } DomElement *document = p.parse(xml); Print("Row selector: '", RowSelector, "'"); string columnSelectors[]; string dataSelectors[]; string headers[]; if(!loadColumnConfig(columnSelectors, dataSelectors, headers)) return NULL; IndexMap subst; int rulesColumns[]; string rulesHave[]; string rulesWant[]; if(SubstitutionSettingsFile != "") { if(!loadSubstConfig(rulesColumns, rulesHave, rulesWant)) return NULL; } for(int i = 0; i < ArraySize(rulesHave); i++) { string key = columnSelectors[rulesColumns[i]]; if(!subst.isKeyExisting(key)) { subst.add(key, new IndexMap("id" + IntegerToString(i))); } IndexMap *hm = (IndexMap *)subst[key]; hm.setValue(rulesHave[i], rulesWant[i]); } IndexMap *data = document.tableSelect(RowSelector, headers, columnSelectors, dataSelectors, &subst, !columnNames); return data; } };