1
This commit is contained in:
@@ -0,0 +1,226 @@
|
|||||||
|
import type { PlatformFieldConfig } from '@/types';
|
||||||
|
|
||||||
|
/** DOM 抓取后的通用结果结构。 */
|
||||||
|
export type DomScrapeResult = Record<string, unknown>;
|
||||||
|
|
||||||
|
/** 默认等待时间,用于点击后或翻页后等待页面渲染。 */
|
||||||
|
const DEFAULT_DELAY = 1500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在目标网页上下文中执行 DOM 抓取。
|
||||||
|
*
|
||||||
|
* 注意:该方法会通过 chrome.scripting.executeScript 注入到页面中执行,
|
||||||
|
* 所以依赖的辅助方法都写在函数内部,避免注入后丢失模块作用域。
|
||||||
|
*/
|
||||||
|
export async function scrapeDomFields(fields: PlatformFieldConfig[]): Promise<DomScrapeResult | null> {
|
||||||
|
if (!document.body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processFields(fields, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 睡眠工具,给点击、翻页、异步渲染留出等待时间。 */
|
||||||
|
const sleep = (ms?: number) => new Promise((resolve) => window.setTimeout(resolve, ms ?? DEFAULT_DELAY));
|
||||||
|
|
||||||
|
/** 从元素中提取实际值,默认取文本,也支持 attr、图片 src、链接 href。 */
|
||||||
|
function extractValue(el: Element | null, config: PlatformFieldConfig): string | null {
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.attr) {
|
||||||
|
return (el.getAttribute(config.attr) || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = el.tagName.toUpperCase();
|
||||||
|
|
||||||
|
if (tagName === 'IMG') {
|
||||||
|
return el.getAttribute('src');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagName === 'A') {
|
||||||
|
const href = el.getAttribute('href');
|
||||||
|
return href && !href.startsWith('http') ? window.location.origin + href : href;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (el.textContent || '').replace(/\n/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据字段 condition 配置在指定 DOM 范围内自动点击目标元素。 */
|
||||||
|
async function autoClick(config: PlatformFieldConfig, rootDom: ParentNode): Promise<void> {
|
||||||
|
if (!config.condition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const selector of config.condition.list) {
|
||||||
|
const targets = Array.from(rootDom.querySelectorAll<HTMLElement>(selector));
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
target.click();
|
||||||
|
await sleep(config.condition.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 递归处理字段配置,支持普通字段、嵌套 row、列表和表格。 */
|
||||||
|
async function processFields(columns: PlatformFieldConfig[], rootDom: ParentNode): Promise<DomScrapeResult> {
|
||||||
|
const result: DomScrapeResult = {};
|
||||||
|
|
||||||
|
for (const item of columns) {
|
||||||
|
await autoClick(item, rootDom);
|
||||||
|
|
||||||
|
const element = rootDom.querySelector(item.className);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
result[item.label] = '没找到该元素';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.type) {
|
||||||
|
if (item.keys && item.keys.length > 0) {
|
||||||
|
await autoClick(item, element);
|
||||||
|
result[item.label] = await processFields(item.keys, element);
|
||||||
|
} else {
|
||||||
|
await autoClick(item, element);
|
||||||
|
result[item.label] = extractValue(element, item);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 1) {
|
||||||
|
result[item.label] = await processList(item, rootDom);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 2) {
|
||||||
|
result[item.label] = await processTable(item, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按列表配置抓取所有列表项,并按分页配置继续翻页。 */
|
||||||
|
async function processList(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
||||||
|
const allList: DomScrapeResult[] = [];
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
pageCount += 1;
|
||||||
|
|
||||||
|
const elements = Array.from(rootDom.querySelectorAll(config.className));
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
const itemData = await processFields(config.keys ?? [], element);
|
||||||
|
allList.push(itemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.pagination) {
|
||||||
|
console.log('未配置分页信息,抓取单页后结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.pagination.maxPage && pageCount >= config.pagination.maxPage) {
|
||||||
|
console.log('已达到配置的最大页数,停止。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextBtn = document.querySelector<HTMLElement>(config.pagination.nextBtn);
|
||||||
|
|
||||||
|
if (!nextBtn) {
|
||||||
|
console.log('未找到下一页按钮,抓取结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = config.pagination.disabledClass
|
||||||
|
? nextBtn.classList.contains(config.pagination.disabledClass)
|
||||||
|
: nextBtn.hasAttribute('disabled');
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
console.log('下一页按钮已禁用,抓取结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextBtn.click();
|
||||||
|
await sleep(config.pagination.delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按表格配置抓取表格行数据,并按分页配置继续翻页。 */
|
||||||
|
async function processTable(config: PlatformFieldConfig, rootDom: ParentNode): Promise<DomScrapeResult[]> {
|
||||||
|
const allTableData: DomScrapeResult[] = [];
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
pageCount += 1;
|
||||||
|
|
||||||
|
const partsNodes: Record<string, Element[]> = {};
|
||||||
|
|
||||||
|
for (const part of config.tableParts ?? []) {
|
||||||
|
const partKey = part.name ?? part.label;
|
||||||
|
const partSelector = part.select ?? part.className;
|
||||||
|
const rowSelector = part.rowSelector ?? `${partSelector} tr`;
|
||||||
|
partsNodes[partKey] = Array.from(rootDom.querySelectorAll(rowSelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPart = config.tableParts?.[0];
|
||||||
|
const firstPartKey = firstPart ? firstPart.name ?? firstPart.label : '';
|
||||||
|
const rowCount = partsNodes[firstPartKey]?.length || 0;
|
||||||
|
|
||||||
|
for (let index = 0; index < rowCount; index += 1) {
|
||||||
|
const rowData: DomScrapeResult = {};
|
||||||
|
|
||||||
|
for (const keyItem of config.keys ?? []) {
|
||||||
|
const partKey = keyItem.part ?? firstPartKey;
|
||||||
|
const targetRowNode = partsNodes[partKey]?.[index];
|
||||||
|
|
||||||
|
if (!targetRowNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyItem.keys) {
|
||||||
|
rowData[keyItem.label] = await processFields(keyItem.keys, targetRowNode);
|
||||||
|
} else {
|
||||||
|
rowData[keyItem.label] = extractValue(targetRowNode.querySelector(keyItem.className), keyItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allTableData.push(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.pagination) {
|
||||||
|
console.log('未配置分页信息,抓取单页后结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.pagination.maxPage && pageCount >= config.pagination.maxPage) {
|
||||||
|
console.log('已达到配置的最大页数,停止。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextBtn = document.querySelector<HTMLElement>(config.pagination.nextBtn);
|
||||||
|
|
||||||
|
if (!nextBtn) {
|
||||||
|
console.log('未找到下一页按钮,抓取结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = config.pagination.disabledClass
|
||||||
|
? nextBtn.classList.contains(config.pagination.disabledClass)
|
||||||
|
: nextBtn.hasAttribute('disabled');
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
console.log('下一页按钮已禁用,抓取结束。');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextBtn.click();
|
||||||
|
await sleep(config.pagination.delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTableData;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getPlatformById } from '@/config/platforms';
|
import { getPlatformById } from '@/config/platforms';
|
||||||
import type { CrawlProgressStep, CrawlTaskState } from '@/types';
|
import type { CrawlProgressStep, CrawlTaskState, PlatformConfig, PlatformStepConfig } from '@/types';
|
||||||
|
import { scrapeDomFields, type DomScrapeResult } from './domScraper';
|
||||||
import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types';
|
import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types';
|
||||||
|
|
||||||
/** chrome.storage.local 中保存当前爬取任务状态的键名。 */
|
/** chrome.storage.local 中保存当前爬取任务状态的键名。 */
|
||||||
@@ -83,6 +84,7 @@ async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
|||||||
/** 补充 windowId 后的任务状态,后续可用于取消或监听窗口关闭。 */
|
/** 补充 windowId 后的任务状态,后续可用于取消或监听窗口关闭。 */
|
||||||
const stateWithWindow = { ...nextState, windowId: windowInfo.id };
|
const stateWithWindow = { ...nextState, windowId: windowInfo.id };
|
||||||
await setCrawlTaskState(stateWithWindow);
|
await setCrawlTaskState(stateWithWindow);
|
||||||
|
void runCrawlSteps(platform, stateWithWindow);
|
||||||
return { ok: true, data: stateWithWindow };
|
return { ok: true, data: stateWithWindow };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
/** 窗口创建失败时写入的失败状态,供 popup/content 显示错误进度。 */
|
/** 窗口创建失败时写入的失败状态,供 popup/content 显示错误进度。 */
|
||||||
@@ -98,6 +100,229 @@ async function startCrawl(platformId: string): Promise<CrawlStateResponse> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按平台 steps 顺序执行页面跳转、DOM 等待、字段抓取和进度更新。 */
|
||||||
|
async function runCrawlSteps(platform: PlatformConfig, initialState: CrawlTaskState): Promise<void> {
|
||||||
|
if (!initialState.windowId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** 新窗口中的目标标签页 ID,后续所有跳转和脚本注入都依赖它。 */
|
||||||
|
const tabId = await getWindowActiveTabId(initialState.windowId);
|
||||||
|
|
||||||
|
for (let stepIndex = 0; stepIndex < platform.steps.length; stepIndex += 1) {
|
||||||
|
/** 当前正在执行的平台页面步骤配置。 */
|
||||||
|
const step = platform.steps[stepIndex];
|
||||||
|
|
||||||
|
if (!(await isTaskRunning(initialState.id))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await markStepRunning(initialState.id, stepIndex);
|
||||||
|
await openStepPage(tabId, step.url);
|
||||||
|
|
||||||
|
/** 当前页面核心 DOM 是否已经出现。 */
|
||||||
|
const isReady = await waitForStepReady(tabId, step);
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
await markStepFailed(initialState.id, stepIndex, '页面关键 DOM 未加载完成');
|
||||||
|
await markTaskFailed(initialState.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入页面执行后的字段抓取结果。 */
|
||||||
|
const data = await scrapeStepFields(tabId, step);
|
||||||
|
console.log(`[crawl] ${platform.name} - ${step.name} 提取成功`, data);
|
||||||
|
await markStepSuccess(initialState.id, stepIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
await markTaskCompleted(initialState.id);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('[crawl] 执行失败', error);
|
||||||
|
await markTaskFailed(initialState.id, error instanceof Error ? error.message : '爬取执行失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定窗口中的活动 tab ID。 */
|
||||||
|
async function getWindowActiveTabId(windowId: number): Promise<number> {
|
||||||
|
/** 指定窗口中查询到的标签页列表。 */
|
||||||
|
const tabs = await chrome.tabs.query({ windowId, active: true });
|
||||||
|
/** 当前窗口里用于承载爬取页面的活动标签页。 */
|
||||||
|
const tab = tabs[0];
|
||||||
|
|
||||||
|
if (!tab?.id) {
|
||||||
|
throw new Error('未找到爬取窗口中的标签页');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开某个 steps 页面,并等待浏览器报告 tab 加载完成。 */
|
||||||
|
async function openStepPage(tabId: number, url: string): Promise<void> {
|
||||||
|
await chrome.tabs.update(tabId, { url, active: true });
|
||||||
|
await waitForTabLoaded(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待 tab 完成页面加载。 */
|
||||||
|
function waitForTabLoaded(tabId: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
/** 页面加载兜底定时器,避免某些站点不触发 complete 时流程永久挂起。 */
|
||||||
|
const timeout = globalThis.setTimeout(() => {
|
||||||
|
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||||
|
resolve();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
/** chrome.tabs.onUpdated 的监听器,用于捕获指定 tab 的 complete 状态。 */
|
||||||
|
function handleUpdated(updatedTabId: number, changeInfo: { status?: string }) {
|
||||||
|
if (updatedTabId === tabId && changeInfo.status === 'complete') {
|
||||||
|
globalThis.clearTimeout(timeout);
|
||||||
|
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener(handleUpdated);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待步骤配置中的 checkSelector 出现;第一次超时后刷新页面再重试一次。 */
|
||||||
|
async function waitForStepReady(tabId: number, step: PlatformStepConfig): Promise<boolean> {
|
||||||
|
if (await waitForSelector(tabId, step.checkSelector, 5000)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.tabs.reload(tabId);
|
||||||
|
await waitForTabLoaded(tabId);
|
||||||
|
return waitForSelector(tabId, step.checkSelector, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在目标页面轮询检查指定 selector 是否存在。 */
|
||||||
|
async function waitForSelector(tabId: number, selector: string, timeoutMs: number): Promise<boolean> {
|
||||||
|
/** 轮询开始时间,用于控制最大等待时长。 */
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
/** 当前页面是否已经能查询到目标元素。 */
|
||||||
|
const exists = await checkSelectorExists(tabId, selector);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入轻量脚本检查页面里是否存在指定 selector。 */
|
||||||
|
async function checkSelectorExists(tabId: number, selector: string): Promise<boolean> {
|
||||||
|
/** chrome.scripting.executeScript 返回的注入执行结果。 */
|
||||||
|
const results = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: (targetSelector: string) => Boolean(document.querySelector(targetSelector)),
|
||||||
|
args: [selector],
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(results[0]?.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入 domScraper 到目标页面,并根据当前 step.fields 提取页面数据。 */
|
||||||
|
async function scrapeStepFields(tabId: number, step: PlatformStepConfig): Promise<DomScrapeResult | null> {
|
||||||
|
/** 目标页面执行 DOM 抓取后返回的结果数组。 */
|
||||||
|
const results = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: scrapeDomFields,
|
||||||
|
args: [step.fields],
|
||||||
|
});
|
||||||
|
|
||||||
|
return results[0]?.result ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断指定任务是否仍处于 running 状态。 */
|
||||||
|
async function isTaskRunning(taskId: string): Promise<boolean> {
|
||||||
|
/** 当前 storage 中的任务状态。 */
|
||||||
|
const state = await getCrawlTaskState();
|
||||||
|
return state?.id === taskId && state.status === 'running';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将指定步骤标记为运行中,同时把其它未完成步骤保持为等待。 */
|
||||||
|
async function markStepRunning(taskId: string, stepIndex: number): Promise<void> {
|
||||||
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
|
...state,
|
||||||
|
currentStepIndex: stepIndex,
|
||||||
|
status: 'running',
|
||||||
|
steps: state.steps.map((step, index) => ({
|
||||||
|
...step,
|
||||||
|
status: index === stepIndex ? 'running' : step.status,
|
||||||
|
message: index === stepIndex ? undefined : step.message,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将指定步骤标记为成功。 */
|
||||||
|
async function markStepSuccess(taskId: string, stepIndex: number): Promise<void> {
|
||||||
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
|
...state,
|
||||||
|
steps: state.steps.map((step, index) =>
|
||||||
|
index === stepIndex ? { ...step, status: 'success', message: undefined } : step,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将指定步骤标记为失败,并记录失败原因。 */
|
||||||
|
async function markStepFailed(taskId: string, stepIndex: number, message: string): Promise<void> {
|
||||||
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
|
...state,
|
||||||
|
currentStepIndex: stepIndex,
|
||||||
|
steps: state.steps.map((step, index) =>
|
||||||
|
index === stepIndex ? { ...step, status: 'failed', message } : step,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将整个任务标记为完成。 */
|
||||||
|
async function markTaskCompleted(taskId: string): Promise<void> {
|
||||||
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
|
...state,
|
||||||
|
status: 'completed',
|
||||||
|
steps: state.steps.map((step) => (step.status === 'running' ? { ...step, status: 'success' } : step)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将整个任务标记为失败。 */
|
||||||
|
async function markTaskFailed(taskId: string, message = '爬取失败'): Promise<void> {
|
||||||
|
await updateCrawlTaskState(taskId, (state) => ({
|
||||||
|
...state,
|
||||||
|
status: 'failed',
|
||||||
|
steps: state.steps.map((step, index) =>
|
||||||
|
index === state.currentStepIndex && step.status === 'running' ? { ...step, status: 'failed', message } : step,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取任务状态后执行不可变更新,避免覆盖已取消或已替换的任务。 */
|
||||||
|
async function updateCrawlTaskState(
|
||||||
|
taskId: string,
|
||||||
|
updater: (state: CrawlTaskState) => CrawlTaskState,
|
||||||
|
): Promise<void> {
|
||||||
|
/** 当前 storage 中最新的任务状态。 */
|
||||||
|
const state = await getCrawlTaskState();
|
||||||
|
|
||||||
|
if (!state || state.id !== taskId || state.status === 'canceled') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setCrawlTaskState(updater(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 睡眠工具,用于轮询 DOM 等待。 */
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
globalThis.setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 */
|
/** 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 */
|
||||||
async function cancelCrawl(): Promise<CrawlStateResponse> {
|
async function cancelCrawl(): Promise<CrawlStateResponse> {
|
||||||
/** 当前保存的爬取任务状态。 */
|
/** 当前保存的爬取任务状态。 */
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ export interface PlatformPaginationConfig {
|
|||||||
export interface PlatformTablePartConfig {
|
export interface PlatformTablePartConfig {
|
||||||
/** 当前 table 或表格片段的名称。 */
|
/** 当前 table 或表格片段的名称。 */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** 当前 table 或表格片段的兼容名称,兼容 message.js 中的 name 写法。 */
|
||||||
|
name?: string;
|
||||||
/** 当前 table 或表格片段的 CSS 选择器。 */
|
/** 当前 table 或表格片段的 CSS 选择器。 */
|
||||||
className: string;
|
className: string;
|
||||||
|
/** 当前 table 或表格片段的兼容选择器,兼容 message.js 中的 select 写法。 */
|
||||||
|
select?: string;
|
||||||
/** 行元素选择器,不填时由采集逻辑使用默认行选择器。 */
|
/** 行元素选择器,不填时由采集逻辑使用默认行选择器。 */
|
||||||
rowSelector?: string;
|
rowSelector?: string;
|
||||||
/** 当前 table 或表格片段下需要采集的字段。 */
|
/** 当前 table 或表格片段下需要采集的字段。 */
|
||||||
@@ -49,6 +53,10 @@ export interface PlatformFieldConfig {
|
|||||||
label: string;
|
label: string;
|
||||||
/** 字段对应的 CSS 选择器。 */
|
/** 字段对应的 CSS 选择器。 */
|
||||||
className: string;
|
className: string;
|
||||||
|
/** 需要提取的属性名;不填时默认提取文本,图片和链接会自动取 src/href。 */
|
||||||
|
attr?: string;
|
||||||
|
/** 表格字段所属的表格分段名称,用于横向拼接多 table 行数据。 */
|
||||||
|
part?: string;
|
||||||
/** 字段类型:0 普通元素(默认),1 列表,2 表格。 */
|
/** 字段类型:0 普通元素(默认),1 列表,2 表格。 */
|
||||||
type?: PlatformFieldType;
|
type?: PlatformFieldType;
|
||||||
/** 进入该字段采集前需要执行的点击条件。 */
|
/** 进入该字段采集前需要执行的点击条件。 */
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/index.ts","./src/background/service.ts","./src/background/types.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/main.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/shared/auth.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
{"root":["./manifest.config.ts","./message.js","./vite.config.ts","./src/background/domscraper.ts","./src/background/index.ts","./src/background/service.ts","./src/background/types.ts","./src/config/platforms.ts","./src/content/app.vue","./src/content/main.ts","./src/options/app.vue","./src/options/main.ts","./src/popup/app.vue","./src/popup/main.ts","./src/shared/auth.ts","./src/types/crawl.ts","./src/types/index.ts","./src/types/platform.ts"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user