Merge pull request #275 from getmaxun/iframe
feat: support `iframe` extraction
This commit is contained in:
@@ -189,111 +189,145 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
* @returns {Array.<Object.<string, string>>}
|
||||
*/
|
||||
window.scrapeSchema = function(lists) {
|
||||
// Utility functions remain the same
|
||||
function omap(object, f, kf = (x) => x) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.map(([k, v]) => [kf(k), f(v)]),
|
||||
);
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.map(([k, v]) => [kf(k), f(v)]),
|
||||
);
|
||||
}
|
||||
|
||||
function ofilter(object, f) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.filter(([k, v]) => f(k, v)),
|
||||
);
|
||||
}
|
||||
|
||||
function findAllElements(config) {
|
||||
if (!config.shadow || !config.selector.includes('>>')) {
|
||||
return Array.from(document.querySelectorAll(config.selector));
|
||||
}
|
||||
|
||||
// For shadow DOM, we'll get all possible combinations
|
||||
const parts = config.selector.split('>>').map(s => s.trim());
|
||||
let currentElements = [document];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const nextElements = [];
|
||||
|
||||
for (const element of currentElements) {
|
||||
let targets;
|
||||
if (i === 0) {
|
||||
// First selector is queried from document
|
||||
targets = Array.from(element.querySelectorAll(part))
|
||||
.filter(el => {
|
||||
// Only include elements that either:
|
||||
// 1. Have an open shadow root
|
||||
// 2. Don't need shadow root (last part of selector)
|
||||
if (i === parts.length - 1) return true;
|
||||
const shadowRoot = el.shadowRoot;
|
||||
return shadowRoot && shadowRoot.mode === 'open';
|
||||
});
|
||||
} else {
|
||||
// For subsequent selectors, only use elements with open shadow roots
|
||||
const shadowRoot = element.shadowRoot;
|
||||
if (!shadowRoot || shadowRoot.mode !== 'open') continue;
|
||||
|
||||
targets = Array.from(shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
nextElements.push(...targets);
|
||||
}
|
||||
|
||||
if (nextElements.length === 0) return [];
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
}
|
||||
|
||||
function getElementValue(element, attribute) {
|
||||
if (!element) return null;
|
||||
|
||||
switch (attribute) {
|
||||
case 'href': {
|
||||
const relativeHref = element.getAttribute('href');
|
||||
return relativeHref ? new URL(relativeHref, window.location.origin).href : null;
|
||||
}
|
||||
case 'src': {
|
||||
const relativeSrc = element.getAttribute('src');
|
||||
return relativeSrc ? new URL(relativeSrc, window.location.origin).href : null;
|
||||
}
|
||||
case 'innerText':
|
||||
return element.innerText?.trim();
|
||||
case 'textContent':
|
||||
return element.textContent?.trim();
|
||||
default:
|
||||
return element.getAttribute(attribute) || element.innerText?.trim();
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.filter(([k, v]) => f(k, v)),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the seed key based on the maximum number of elements found
|
||||
function findAllElements(config) {
|
||||
// Regular DOM query if no special delimiters
|
||||
if (!config.selector.includes('>>') && !config.selector.includes(':>>')) {
|
||||
return Array.from(document.querySelectorAll(config.selector));
|
||||
}
|
||||
|
||||
// Split by both types of delimiters
|
||||
const parts = config.selector.split(/(?:>>|:>>)/).map(s => s.trim());
|
||||
const delimiters = config.selector.match(/(?:>>|:>>)/g) || [];
|
||||
let currentElements = [document];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const nextElements = [];
|
||||
const isLast = i === parts.length - 1;
|
||||
const delimiter = delimiters[i] || '';
|
||||
const isIframeTraversal = delimiter === ':>>';
|
||||
|
||||
for (const element of currentElements) {
|
||||
try {
|
||||
let targets;
|
||||
|
||||
if (i === 0) {
|
||||
// First selector is queried from main document
|
||||
targets = Array.from(element.querySelectorAll(part))
|
||||
.filter(el => {
|
||||
if (isLast) return true;
|
||||
// For iframe traversal, only include iframes
|
||||
if (isIframeTraversal) return el.tagName === 'IFRAME';
|
||||
// For shadow DOM traversal, only include elements with shadow root
|
||||
return el.shadowRoot && el.shadowRoot.mode === 'open';
|
||||
});
|
||||
} else {
|
||||
if (isIframeTraversal) {
|
||||
// Handle iframe traversal
|
||||
const iframeDocument = element.contentDocument || element.contentWindow?.document;
|
||||
if (!iframeDocument) continue;
|
||||
|
||||
targets = Array.from(iframeDocument.querySelectorAll(part));
|
||||
if (!isLast) {
|
||||
targets = targets.filter(el => el.tagName === 'IFRAME');
|
||||
}
|
||||
} else {
|
||||
// Handle shadow DOM traversal
|
||||
const shadowRoot = element.shadowRoot;
|
||||
if (!shadowRoot || shadowRoot.mode !== 'open') continue;
|
||||
|
||||
targets = Array.from(shadowRoot.querySelectorAll(part));
|
||||
if (!isLast) {
|
||||
targets = targets.filter(el => el.shadowRoot && el.shadowRoot.mode === 'open');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextElements.push(...targets);
|
||||
} catch (error) {
|
||||
console.warn('Cannot access content:', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextElements.length === 0) return [];
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
}
|
||||
|
||||
// Modified to handle iframe context for URL resolution
|
||||
function getElementValue(element, attribute) {
|
||||
if (!element) return null;
|
||||
|
||||
// Get the base URL for resolving relative URLs
|
||||
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||
|
||||
switch (attribute) {
|
||||
case 'href': {
|
||||
const relativeHref = element.getAttribute('href');
|
||||
return relativeHref ? new URL(relativeHref, baseURL).href : null;
|
||||
}
|
||||
case 'src': {
|
||||
const relativeSrc = element.getAttribute('src');
|
||||
return relativeSrc ? new URL(relativeSrc, baseURL).href : null;
|
||||
}
|
||||
case 'innerText':
|
||||
return element.innerText?.trim();
|
||||
case 'textContent':
|
||||
return element.textContent?.trim();
|
||||
default:
|
||||
return element.getAttribute(attribute) || element.innerText?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Rest of the functions remain largely the same
|
||||
function getSeedKey(listObj) {
|
||||
const maxLength = Math.max(...Object.values(
|
||||
omap(listObj, (x) => findAllElements(x).length)
|
||||
));
|
||||
return Object.keys(
|
||||
ofilter(listObj, (_, v) => findAllElements(v).length === maxLength)
|
||||
)[0];
|
||||
const maxLength = Math.max(...Object.values(
|
||||
omap(listObj, (x) => findAllElements(x).length)
|
||||
));
|
||||
return Object.keys(
|
||||
ofilter(listObj, (_, v) => findAllElements(v).length === maxLength)
|
||||
)[0];
|
||||
}
|
||||
|
||||
// Find minimal bounding elements
|
||||
function getMBEs(elements) {
|
||||
return elements.map((element) => {
|
||||
let candidate = element;
|
||||
const isUniqueChild = (e) => elements
|
||||
.filter((elem) => e.parentNode?.contains(elem))
|
||||
.length === 1;
|
||||
|
||||
while (candidate && isUniqueChild(candidate)) {
|
||||
candidate = candidate.parentNode;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
let candidate = element;
|
||||
const isUniqueChild = (e) => elements
|
||||
.filter((elem) => {
|
||||
// Handle both iframe and shadow DOM boundaries
|
||||
const sameContext = elem.getRootNode() === e.getRootNode() &&
|
||||
elem.ownerDocument === e.ownerDocument;
|
||||
return sameContext && e.parentNode?.contains(elem);
|
||||
})
|
||||
.length === 1;
|
||||
|
||||
while (candidate && isUniqueChild(candidate)) {
|
||||
candidate = candidate.parentNode;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
});
|
||||
}
|
||||
|
||||
// First try the MBE approach
|
||||
const seedName = getSeedKey(lists);
|
||||
const seedElements = findAllElements(lists[seedName]);
|
||||
const MBEs = getMBEs(seedElements);
|
||||
@@ -347,164 +381,210 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
|
||||
*/
|
||||
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
|
||||
// Shadow DOM query functions remain unchanged
|
||||
const queryShadowDOM = (rootElement, selector) => {
|
||||
if (!selector.includes('>>')) {
|
||||
// Enhanced query function to handle both iframe and shadow DOM
|
||||
const queryElement = (rootElement, selector) => {
|
||||
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||
return rootElement.querySelector(selector);
|
||||
}
|
||||
}
|
||||
|
||||
const parts = selector.split('>>').map(part => part.trim());
|
||||
let currentElement = rootElement;
|
||||
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||
let currentElement = rootElement;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (!currentElement) return null;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (!currentElement) return null;
|
||||
|
||||
if (!currentElement.querySelector && !currentElement.shadowRoot) {
|
||||
currentElement = document.querySelector(parts[i]);
|
||||
continue;
|
||||
}
|
||||
// Handle iframe traversal
|
||||
if (currentElement.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
|
||||
currentElement = iframeDoc.querySelector(parts[i]);
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let nextElement = currentElement.querySelector(parts[i]);
|
||||
// Try regular DOM first
|
||||
let nextElement = currentElement.querySelector(parts[i]);
|
||||
|
||||
if (!nextElement && currentElement.shadowRoot) {
|
||||
nextElement = currentElement.shadowRoot.querySelector(parts[i]);
|
||||
}
|
||||
// Try shadow DOM if not found
|
||||
if (!nextElement && currentElement.shadowRoot) {
|
||||
nextElement = currentElement.shadowRoot.querySelector(parts[i]);
|
||||
}
|
||||
|
||||
if (!nextElement) {
|
||||
const allChildren = Array.from(currentElement.children || []);
|
||||
for (const child of allChildren) {
|
||||
if (child.shadowRoot) {
|
||||
nextElement = child.shadowRoot.querySelector(parts[i]);
|
||||
if (nextElement) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check children's shadow roots if still not found
|
||||
if (!nextElement) {
|
||||
const children = Array.from(currentElement.children || []);
|
||||
for (const child of children) {
|
||||
if (child.shadowRoot) {
|
||||
nextElement = child.shadowRoot.querySelector(parts[i]);
|
||||
if (nextElement) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentElement = nextElement;
|
||||
}
|
||||
currentElement = nextElement;
|
||||
}
|
||||
|
||||
return currentElement;
|
||||
return currentElement;
|
||||
};
|
||||
|
||||
const queryShadowDOMAll = (rootElement, selector) => {
|
||||
if (!selector.includes('>>')) {
|
||||
// Enhanced query all function for both contexts
|
||||
const queryElementAll = (rootElement, selector) => {
|
||||
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||
return rootElement.querySelectorAll(selector);
|
||||
}
|
||||
}
|
||||
|
||||
const parts = selector.split('>>').map(part => part.trim());
|
||||
let currentElements = [rootElement];
|
||||
|
||||
for (const part of parts) {
|
||||
const nextElements = [];
|
||||
|
||||
for (const element of currentElements) {
|
||||
if (element.querySelectorAll) {
|
||||
nextElements.push(...element.querySelectorAll(part));
|
||||
}
|
||||
|
||||
if (element.shadowRoot) {
|
||||
nextElements.push(...element.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
|
||||
const children = Array.from(element.children || []);
|
||||
for (const child of children) {
|
||||
if (child.shadowRoot) {
|
||||
nextElements.push(...child.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||
let currentElements = [rootElement];
|
||||
|
||||
for (const part of parts) {
|
||||
const nextElements = [];
|
||||
|
||||
for (const element of currentElements) {
|
||||
// Handle iframe traversal
|
||||
if (element.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = element.contentDocument || element.contentWindow.document;
|
||||
nextElements.push(...iframeDoc.querySelectorAll(part));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Regular DOM elements
|
||||
if (element.querySelectorAll) {
|
||||
nextElements.push(...element.querySelectorAll(part));
|
||||
}
|
||||
|
||||
// Shadow DOM elements
|
||||
if (element.shadowRoot) {
|
||||
nextElements.push(...element.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
|
||||
// Check children's shadow roots
|
||||
const children = Array.from(element.children || []);
|
||||
for (const child of children) {
|
||||
if (child.shadowRoot) {
|
||||
nextElements.push(...child.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
};
|
||||
|
||||
// Enhanced table processing helper functions with shadow DOM support
|
||||
// Enhanced value extraction with context awareness
|
||||
function extractValue(element, attribute) {
|
||||
if (!element) return null;
|
||||
|
||||
// Check for shadow root first
|
||||
if (element.shadowRoot) {
|
||||
const shadowContent = element.shadowRoot.textContent;
|
||||
if (shadowContent && shadowContent.trim()) {
|
||||
return shadowContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute === 'innerText') {
|
||||
return element.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
return element.innerHTML.trim();
|
||||
} else if (attribute === 'src' || attribute === 'href') {
|
||||
const attrValue = element.getAttribute(attribute);
|
||||
return attrValue ? new URL(attrValue, window.location.origin).href : null;
|
||||
}
|
||||
return element.getAttribute(attribute);
|
||||
if (!element) return null;
|
||||
|
||||
// Get context-aware base URL
|
||||
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||
|
||||
// Check shadow root first
|
||||
if (element.shadowRoot) {
|
||||
const shadowContent = element.shadowRoot.textContent;
|
||||
if (shadowContent?.trim()) {
|
||||
return shadowContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute === 'innerText') {
|
||||
return element.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
return element.innerHTML.trim();
|
||||
} else if (attribute === 'src' || attribute === 'href') {
|
||||
const attrValue = element.getAttribute(attribute);
|
||||
return attrValue ? new URL(attrValue, baseURL).href : null;
|
||||
}
|
||||
return element.getAttribute(attribute);
|
||||
}
|
||||
|
||||
// Enhanced table ancestor finding with context support
|
||||
function findTableAncestor(element) {
|
||||
let currentElement = element;
|
||||
const MAX_DEPTH = 5;
|
||||
let depth = 0;
|
||||
|
||||
while (currentElement && depth < MAX_DEPTH) {
|
||||
// Check if current element is in shadow DOM
|
||||
if (currentElement.getRootNode() instanceof ShadowRoot) {
|
||||
currentElement = currentElement.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentElement.tagName === 'TD') {
|
||||
return { type: 'TD', element: currentElement };
|
||||
} else if (currentElement.tagName === 'TR') {
|
||||
return { type: 'TR', element: currentElement };
|
||||
}
|
||||
currentElement = currentElement.parentElement;
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
let currentElement = element;
|
||||
const MAX_DEPTH = 5;
|
||||
let depth = 0;
|
||||
|
||||
while (currentElement && depth < MAX_DEPTH) {
|
||||
// Handle shadow DOM
|
||||
if (currentElement.getRootNode() instanceof ShadowRoot) {
|
||||
currentElement = currentElement.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentElement.tagName === 'TD') {
|
||||
return { type: 'TD', element: currentElement };
|
||||
} else if (currentElement.tagName === 'TR') {
|
||||
return { type: 'TR', element: currentElement };
|
||||
}
|
||||
|
||||
// Handle iframe crossing
|
||||
if (currentElement.tagName === 'IFRAME') {
|
||||
try {
|
||||
currentElement = currentElement.contentDocument.body;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to get cell index
|
||||
function getCellIndex(td) {
|
||||
let index = 0;
|
||||
let sibling = td;
|
||||
|
||||
// Handle shadow DOM case
|
||||
if (td.getRootNode() instanceof ShadowRoot) {
|
||||
const shadowRoot = td.getRootNode();
|
||||
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
|
||||
return allCells.indexOf(td);
|
||||
}
|
||||
|
||||
while (sibling = sibling.previousElementSibling) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
if (td.getRootNode() instanceof ShadowRoot) {
|
||||
const shadowRoot = td.getRootNode();
|
||||
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
|
||||
return allCells.indexOf(td);
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
let sibling = td;
|
||||
while (sibling = sibling.previousElementSibling) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Helper function to check for TH elements
|
||||
function hasThElement(row, tableFields) {
|
||||
for (const [label, { selector }] of Object.entries(tableFields)) {
|
||||
const element = queryShadowDOM(row, selector);
|
||||
if (element) {
|
||||
let current = element;
|
||||
while (current && current !== row) {
|
||||
// Check if we're in shadow DOM
|
||||
if (current.getRootNode() instanceof ShadowRoot) {
|
||||
current = current.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.tagName === 'TH') {
|
||||
return true;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
for (const [_, { selector }] of Object.entries(tableFields)) {
|
||||
const element = queryElement(row, selector);
|
||||
if (element) {
|
||||
let current = element;
|
||||
while (current && current !== row) {
|
||||
if (current.getRootNode() instanceof ShadowRoot) {
|
||||
current = current.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.tagName === 'TH') return true;
|
||||
|
||||
if (current.tagName === 'IFRAME') {
|
||||
try {
|
||||
current = current.contentDocument.body;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to filter rows
|
||||
function filterRowsBasedOnTag(rows, tableFields) {
|
||||
for (const row of rows) {
|
||||
if (hasThElement(row, tableFields)) {
|
||||
@@ -520,7 +600,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
});
|
||||
}
|
||||
|
||||
// Class similarity functions remain unchanged
|
||||
// Class similarity comparison functions
|
||||
function calculateClassSimilarity(classList1, classList2) {
|
||||
const set1 = new Set(classList1);
|
||||
const set2 = new Set(classList2);
|
||||
@@ -529,189 +609,237 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
return intersection.size / union.size;
|
||||
}
|
||||
|
||||
// Enhanced similar elements finding with context support
|
||||
function findSimilarElements(baseElement, similarityThreshold = 0.7) {
|
||||
const baseClasses = Array.from(baseElement.classList);
|
||||
if (baseClasses.length === 0) return [];
|
||||
const potentialElements = document.getElementsByTagName(baseElement.tagName);
|
||||
return Array.from(potentialElements).filter(element => {
|
||||
if (element === baseElement) return false;
|
||||
const similarity = calculateClassSimilarity(
|
||||
baseClasses,
|
||||
Array.from(element.classList)
|
||||
);
|
||||
return similarity >= similarityThreshold;
|
||||
});
|
||||
const baseClasses = Array.from(baseElement.classList);
|
||||
if (baseClasses.length === 0) return [];
|
||||
|
||||
const allElements = [];
|
||||
|
||||
// Get elements from main document
|
||||
allElements.push(...document.getElementsByTagName(baseElement.tagName));
|
||||
|
||||
// Get elements from shadow DOM
|
||||
if (baseElement.getRootNode() instanceof ShadowRoot) {
|
||||
const shadowHost = baseElement.getRootNode().host;
|
||||
allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName));
|
||||
}
|
||||
|
||||
// Get elements from iframes
|
||||
const iframes = document.getElementsByTagName('iframe');
|
||||
for (const iframe of iframes) {
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return allElements.filter(element => {
|
||||
if (element === baseElement) return false;
|
||||
const similarity = calculateClassSimilarity(
|
||||
baseClasses,
|
||||
Array.from(element.classList)
|
||||
);
|
||||
return similarity >= similarityThreshold;
|
||||
});
|
||||
}
|
||||
|
||||
// Main scraping logic with shadow DOM support
|
||||
let containers = queryShadowDOMAll(document, listSelector);
|
||||
// Main scraping logic with context support
|
||||
let containers = queryElementAll(document, listSelector);
|
||||
containers = Array.from(containers);
|
||||
|
||||
if (containers.length === 0) return [];
|
||||
|
||||
if (limit > 1 && containers.length === 1) {
|
||||
const baseContainer = containers[0];
|
||||
const similarContainers = findSimilarElements(baseContainer);
|
||||
|
||||
if (similarContainers.length > 0) {
|
||||
const newContainers = similarContainers.filter(container =>
|
||||
!container.matches(listSelector)
|
||||
);
|
||||
containers = [...containers, ...newContainers];
|
||||
}
|
||||
const baseContainer = containers[0];
|
||||
const similarContainers = findSimilarElements(baseContainer);
|
||||
|
||||
if (similarContainers.length > 0) {
|
||||
const newContainers = similarContainers.filter(container =>
|
||||
!container.matches(listSelector)
|
||||
);
|
||||
containers = [...containers, ...newContainers];
|
||||
}
|
||||
}
|
||||
|
||||
const containerFields = containers.map(() => ({
|
||||
tableFields: {},
|
||||
nonTableFields: {}
|
||||
tableFields: {},
|
||||
nonTableFields: {}
|
||||
}));
|
||||
|
||||
// Classify fields
|
||||
containers.forEach((container, containerIndex) => {
|
||||
for (const [label, field] of Object.entries(fields)) {
|
||||
const sampleElement = queryShadowDOM(container, field.selector);
|
||||
|
||||
if (sampleElement) {
|
||||
const ancestor = findTableAncestor(sampleElement);
|
||||
if (ancestor) {
|
||||
containerFields[containerIndex].tableFields[label] = {
|
||||
...field,
|
||||
tableContext: ancestor.type,
|
||||
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||
};
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
for (const [label, field] of Object.entries(fields)) {
|
||||
const sampleElement = queryElement(container, field.selector);
|
||||
|
||||
if (sampleElement) {
|
||||
const ancestor = findTableAncestor(sampleElement);
|
||||
if (ancestor) {
|
||||
containerFields[containerIndex].tableFields[label] = {
|
||||
...field,
|
||||
tableContext: ancestor.type,
|
||||
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||
};
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tableData = [];
|
||||
const nonTableData = [];
|
||||
|
||||
// Process table data with shadow DOM support
|
||||
// Process table data with both iframe and shadow DOM support
|
||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||
const container = containers[containerIndex];
|
||||
const { tableFields } = containerFields[containerIndex];
|
||||
const container = containers[containerIndex];
|
||||
const { tableFields } = containerFields[containerIndex];
|
||||
|
||||
if (Object.keys(tableFields).length > 0) {
|
||||
const firstField = Object.values(tableFields)[0];
|
||||
const firstElement = queryShadowDOM(container, firstField.selector);
|
||||
let tableContext = firstElement;
|
||||
|
||||
// Find table context including shadow DOM
|
||||
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||
if (tableContext.getRootNode() instanceof ShadowRoot) {
|
||||
tableContext = tableContext.getRootNode().host;
|
||||
} else {
|
||||
tableContext = tableContext.parentElement;
|
||||
}
|
||||
}
|
||||
if (Object.keys(tableFields).length > 0) {
|
||||
const firstField = Object.values(tableFields)[0];
|
||||
const firstElement = queryElement(container, firstField.selector);
|
||||
let tableContext = firstElement;
|
||||
|
||||
// Find table context including both iframe and shadow DOM
|
||||
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||
if (tableContext.getRootNode() instanceof ShadowRoot) {
|
||||
tableContext = tableContext.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tableContext.tagName === 'IFRAME') {
|
||||
try {
|
||||
tableContext = tableContext.contentDocument.body;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tableContext = tableContext.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (tableContext) {
|
||||
// Get rows from both regular DOM and shadow DOM
|
||||
const rows = [];
|
||||
if (tableContext.shadowRoot) {
|
||||
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
|
||||
}
|
||||
rows.push(...tableContext.getElementsByTagName('TR'));
|
||||
|
||||
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||
const record = {};
|
||||
const currentRow = processedRows[rowIndex];
|
||||
|
||||
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||
let element = null;
|
||||
|
||||
if (cellIndex >= 0) {
|
||||
let td = currentRow.children[cellIndex];
|
||||
|
||||
// Check shadow DOM for td
|
||||
if (!td && currentRow.shadowRoot) {
|
||||
const shadowCells = currentRow.shadowRoot.children;
|
||||
if (shadowCells && shadowCells.length > cellIndex) {
|
||||
td = shadowCells[cellIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (td) {
|
||||
element = queryShadowDOM(td, selector);
|
||||
|
||||
if (!element && selector.split(">").pop().includes('td:nth-child')) {
|
||||
element = td;
|
||||
}
|
||||
if (tableContext) {
|
||||
// Get rows from all contexts
|
||||
const rows = [];
|
||||
|
||||
// Get rows from regular DOM
|
||||
rows.push(...tableContext.getElementsByTagName('TR'));
|
||||
|
||||
// Get rows from shadow DOM
|
||||
if (tableContext.shadowRoot) {
|
||||
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
|
||||
}
|
||||
|
||||
// Get rows from iframes
|
||||
if (tableContext.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document;
|
||||
rows.push(...iframeDoc.getElementsByTagName('TR'));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe rows:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||
const record = {};
|
||||
const currentRow = processedRows[rowIndex];
|
||||
|
||||
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||
let element = null;
|
||||
|
||||
if (cellIndex >= 0) {
|
||||
// Get TD element considering both contexts
|
||||
let td = currentRow.children[cellIndex];
|
||||
|
||||
// Check shadow DOM for td
|
||||
if (!td && currentRow.shadowRoot) {
|
||||
const shadowCells = currentRow.shadowRoot.children;
|
||||
if (shadowCells && shadowCells.length > cellIndex) {
|
||||
td = shadowCells[cellIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (td) {
|
||||
element = queryElement(td, selector);
|
||||
|
||||
if (!element && selector.split(/(?:>>|:>>)/).pop().includes('td:nth-child')) {
|
||||
element = td;
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
const tagOnlySelector = selector.split('.')[0];
|
||||
element = queryShadowDOM(td, tagOnlySelector);
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
let currentElement = td;
|
||||
while (currentElement && currentElement.children.length > 0) {
|
||||
let foundContentChild = false;
|
||||
for (const child of currentElement.children) {
|
||||
if (extractValue(child, attribute)) {
|
||||
currentElement = child;
|
||||
foundContentChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundContentChild) break;
|
||||
}
|
||||
element = currentElement;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element = queryShadowDOM(currentRow, selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
if (!element) {
|
||||
const tagOnlySelector = selector.split('.')[0];
|
||||
element = queryElement(td, tagOnlySelector);
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
let currentElement = td;
|
||||
while (currentElement && currentElement.children.length > 0) {
|
||||
let foundContentChild = false;
|
||||
for (const child of currentElement.children) {
|
||||
if (extractValue(child, attribute)) {
|
||||
currentElement = child;
|
||||
foundContentChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundContentChild) break;
|
||||
}
|
||||
element = currentElement;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element = queryElement(currentRow, selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
tableData.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(record).length > 0) {
|
||||
tableData.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Non-table data scraping remains unchanged
|
||||
// Process non-table data with both contexts support
|
||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||
if (nonTableData.length >= limit) break;
|
||||
if (nonTableData.length >= limit) break;
|
||||
|
||||
const container = containers[containerIndex];
|
||||
const { nonTableFields } = containerFields[containerIndex];
|
||||
const container = containers[containerIndex];
|
||||
const { nonTableFields } = containerFields[containerIndex];
|
||||
|
||||
if (Object.keys(nonTableFields).length > 0) {
|
||||
const record = {};
|
||||
if (Object.keys(nonTableFields).length > 0) {
|
||||
const record = {};
|
||||
|
||||
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||
const relativeSelector = selector.split('>>').slice(-1)[0];
|
||||
const element = queryShadowDOM(container, relativeSelector);
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
nonTableData.push(record);
|
||||
}
|
||||
}
|
||||
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||
// Get the last part of the selector after any context delimiter
|
||||
const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
|
||||
const element = queryElement(container, relativeSelector);
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
nonTableData.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge and limit the results
|
||||
const scrapedData = [...tableData, ...nonTableData];
|
||||
return scrapedData;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all children of the elements matching the listSelector,
|
||||
|
||||
@@ -680,11 +680,25 @@ export default class Interpreter extends EventEmitter {
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private removeSpecialSelectors(workflow: Workflow) {
|
||||
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
||||
const step = workflow[actionId];
|
||||
|
||||
if (step.where && Array.isArray(step.where.selectors)) {
|
||||
// Filter out if selector has EITHER ":>>" OR ">>"
|
||||
step.where.selectors = step.where.selectors.filter(selector =>
|
||||
!(selector.includes(':>>') || selector.includes('>>'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private async runLoop(p: Page, workflow: Workflow) {
|
||||
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
||||
|
||||
// remove shadow selectors
|
||||
workflowCopy = this.removeShadowSelectors(workflowCopy);
|
||||
workflowCopy = this.removeSpecialSelectors(workflowCopy);
|
||||
|
||||
// apply ad-blocker to the current page
|
||||
try {
|
||||
|
||||
@@ -129,6 +129,12 @@ export interface BaseActionInfo {
|
||||
hasOnlyText: boolean;
|
||||
}
|
||||
|
||||
|
||||
interface IframeSelector {
|
||||
full: string;
|
||||
isIframe: boolean;
|
||||
}
|
||||
|
||||
interface ShadowSelector {
|
||||
full: string;
|
||||
mode: string;
|
||||
@@ -148,7 +154,8 @@ export interface Selectors {
|
||||
hrefSelector: string|null;
|
||||
accessibilitySelector: string|null;
|
||||
formSelector: string|null;
|
||||
shadowSelector: ShadowSelector | null;
|
||||
iframeSelector: IframeSelector|null;
|
||||
shadowSelector: ShadowSelector|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,10 @@ export const getBestSelectorForAction = (action: Action) => {
|
||||
case ActionType.DragAndDrop: {
|
||||
const selectors = action.selectors;
|
||||
|
||||
|
||||
if (selectors?.iframeSelector?.full) {
|
||||
return selectors.iframeSelector.full;
|
||||
|
||||
if (selectors?.shadowSelector?.full) {
|
||||
return selectors.shadowSelector.full;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
interface ElementInfo {
|
||||
tagName: string;
|
||||
hasOnlyText?: boolean;
|
||||
isIframeContent?: boolean;
|
||||
isShadowRoot?: boolean;
|
||||
innerText?: string;
|
||||
url?: string;
|
||||
@@ -117,24 +118,47 @@ export const BrowserWindow = () => {
|
||||
}, [screenShot, canvasRef, socket, screencastHandler]);
|
||||
|
||||
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
|
||||
console.log("LIST SELECTOR", listSelector);
|
||||
console.log("DATA SELECTOR", data.selector);
|
||||
console.log("CHILD SELECTORS", data.childSelectors);
|
||||
if (getList === true) {
|
||||
if (listSelector) {
|
||||
socket?.emit('listSelector', { selector: listSelector });
|
||||
|
||||
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
|
||||
|
||||
if (limitMode) {
|
||||
setHighlighterData(null);
|
||||
} else if (paginationMode) {
|
||||
// only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
|
||||
// Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
|
||||
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
|
||||
setHighlighterData(data);
|
||||
} else {
|
||||
setHighlighterData(null);
|
||||
}
|
||||
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
|
||||
// highlight only valid child elements within the listSelector
|
||||
// Highlight only valid child elements within the listSelector
|
||||
setHighlighterData(data);
|
||||
} else if (data.elementInfo?.isIframeContent && data.childSelectors) {
|
||||
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
|
||||
// Check if the selector matches any iframe child selectors
|
||||
const isIframeChild = data.childSelectors.some(childSelector =>
|
||||
data.selector.includes(':>>') && // Iframe uses :>> for traversal
|
||||
childSelector.split(':>>').some(part =>
|
||||
data.selector.includes(part.trim())
|
||||
)
|
||||
);
|
||||
setHighlighterData(isIframeChild ? data : null);
|
||||
} else if (data.selector.includes(':>>') && hasValidChildSelectors) {
|
||||
// Handle mixed DOM cases with iframes
|
||||
// Split the selector into parts and check each against child selectors
|
||||
const selectorParts = data.selector.split(':>>').map(part => part.trim());
|
||||
const isValidMixedSelector = selectorParts.some(part =>
|
||||
// We know data.childSelectors is defined due to hasValidChildSelectors check
|
||||
data.childSelectors!.some(childSelector =>
|
||||
childSelector.includes(part)
|
||||
)
|
||||
);
|
||||
setHighlighterData(isValidMixedSelector ? data : null);
|
||||
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
|
||||
// New case: Handle pure Shadow DOM elements
|
||||
// Check if the selector matches any shadow root child selectors
|
||||
@@ -145,7 +169,7 @@ export const BrowserWindow = () => {
|
||||
)
|
||||
);
|
||||
setHighlighterData(isShadowChild ? data : null);
|
||||
} else if (data.selector.includes('>>') && hasValidChildSelectors) {
|
||||
} else if (data.selector.includes('>>') && hasValidChildSelectors) {
|
||||
// New case: Handle mixed DOM cases
|
||||
// Split the selector into parts and check each against child selectors
|
||||
const selectorParts = data.selector.split('>>').map(part => part.trim());
|
||||
@@ -156,19 +180,18 @@ export const BrowserWindow = () => {
|
||||
)
|
||||
);
|
||||
setHighlighterData(isValidMixedSelector ? data : null);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// if !valid child in normal mode, clear the highlighter
|
||||
setHighlighterData(null);
|
||||
}
|
||||
} else {
|
||||
// set highlighterData for the initial listSelector selection
|
||||
}
|
||||
} else {
|
||||
// Set highlighterData for the initial listSelector selection
|
||||
setHighlighterData(data);
|
||||
}
|
||||
} else {
|
||||
// for non-list steps
|
||||
}
|
||||
} else {
|
||||
// For non-list steps
|
||||
setHighlighterData(data);
|
||||
}
|
||||
}
|
||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user