@jayfong/x-utils 1.0.0-202407012223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_cjs/ai.js +80 -0
- package/_cjs/cli.js +51 -0
- package/_cjs/clipboard.js +65 -0
- package/_cjs/cmd.js +42 -0
- package/_cjs/crawler.js +341 -0
- package/_cjs/csv.js +97 -0
- package/_cjs/data_set.js +115 -0
- package/_cjs/env.js +63 -0
- package/_cjs/fs.js +35 -0
- package/_cjs/html.js +62 -0
- package/_cjs/http_server.js +56 -0
- package/_cjs/json.js +49 -0
- package/_cjs/ksb.js +217 -0
- package/_cjs/number.js +39 -0
- package/_cjs/ocr.js +74 -0
- package/_cjs/path.js +35 -0
- package/_cjs/pinyin.js +32 -0
- package/_cjs/puppeteer.js +271 -0
- package/_cjs/runner.js +88 -0
- package/_cjs/task.js +41 -0
- package/_cjs/text.js +136 -0
- package/_cjs/unicode/lib_unicode.js +58643 -0
- package/_cjs/unicode/unicode.js +441 -0
- package/_cjs/upload.js +100 -0
- package/_cjs/yaml.js +52 -0
- package/ai.d.ts +26 -0
- package/ai.js +60 -0
- package/cli.d.ts +14 -0
- package/cli.js +21 -0
- package/clipboard.d.ts +6 -0
- package/clipboard.js +35 -0
- package/cmd.d.ts +5 -0
- package/cmd.js +22 -0
- package/crawler.d.ts +162 -0
- package/crawler.js +332 -0
- package/csv.d.ts +14 -0
- package/csv.js +67 -0
- package/data_set.d.ts +22 -0
- package/data_set.js +85 -0
- package/env.d.ts +4 -0
- package/env.js +32 -0
- package/fs.d.ts +2 -0
- package/fs.js +5 -0
- package/html.d.ts +7 -0
- package/html.js +45 -0
- package/http_server.d.ts +10 -0
- package/http_server.js +26 -0
- package/json.d.ts +6 -0
- package/json.js +19 -0
- package/ksb.d.ts +32 -0
- package/ksb.js +197 -0
- package/number.d.ts +3 -0
- package/number.js +9 -0
- package/ocr.d.ts +10 -0
- package/ocr.js +54 -0
- package/ofd.d.ts +1 -0
- package/package.json +158 -0
- package/path.d.ts +2 -0
- package/path.js +5 -0
- package/pinyin.d.ts +3 -0
- package/pinyin.js +12 -0
- package/puppeteer.d.ts +38 -0
- package/puppeteer.js +241 -0
- package/runner.d.ts +13 -0
- package/runner.js +58 -0
- package/task.d.ts +6 -0
- package/task.js +21 -0
- package/text.d.ts +63 -0
- package/text.js +119 -0
- package/unicode/lib_unicode.d.ts +4 -0
- package/unicode/lib_unicode.js +58623 -0
- package/unicode/unicode.d.ts +347 -0
- package/unicode/unicode.js +426 -0
- package/upload.d.ts +22 -0
- package/upload.js +70 -0
- package/yaml.d.ts +6 -0
- package/yaml.js +22 -0
package/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Table from "cli-table3";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
class XCli {
|
|
4
|
+
static async prompt(options) {
|
|
5
|
+
const { x } = await inquirer.prompt({
|
|
6
|
+
name: "x",
|
|
7
|
+
message: options.message,
|
|
8
|
+
type: options.type,
|
|
9
|
+
default: options.default
|
|
10
|
+
});
|
|
11
|
+
return x;
|
|
12
|
+
}
|
|
13
|
+
static table(data) {
|
|
14
|
+
const table = new Table();
|
|
15
|
+
table.push(...data);
|
|
16
|
+
return table.toString();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
XCli
|
|
21
|
+
};
|
package/clipboard.d.ts
ADDED
package/clipboard.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import clipboardy from "clipboardy";
|
|
2
|
+
import { XCmd } from "./cmd.js";
|
|
3
|
+
class XClipboard {
|
|
4
|
+
static async readText() {
|
|
5
|
+
return clipboardy.read();
|
|
6
|
+
}
|
|
7
|
+
static async readHtml() {
|
|
8
|
+
const hex = await XCmd.exec(
|
|
9
|
+
// 读取剪切板为 HTML
|
|
10
|
+
// https://stackoverflow.com/questions/2545289/getting-rtf-data-out-of-mac-os-x-pasteboard-clipboard
|
|
11
|
+
// https://stackoverflow.com/a/17217886
|
|
12
|
+
`osascript -e "the clipboard as \xABclass HTML\xBB"`
|
|
13
|
+
);
|
|
14
|
+
const html = Buffer.from(
|
|
15
|
+
hex.substring(10, hex.length - 1),
|
|
16
|
+
"hex"
|
|
17
|
+
).toString();
|
|
18
|
+
return html;
|
|
19
|
+
}
|
|
20
|
+
static async writeText(text) {
|
|
21
|
+
await clipboardy.write(text);
|
|
22
|
+
}
|
|
23
|
+
static async writeHtml(html) {
|
|
24
|
+
const hex = Buffer.from(html).toString("hex");
|
|
25
|
+
await XCmd.exec(
|
|
26
|
+
// 设置剪切板为 HTML
|
|
27
|
+
// https://stackoverflow.com/questions/11085654/apple-script-how-can-i-copy-html-content-to-the-clipboard
|
|
28
|
+
// https://stackoverflow.com/a/11089226
|
|
29
|
+
`osascript -e "set the clipboard to \xABdata HTML${hex}\xBB"`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
XClipboard
|
|
35
|
+
};
|
package/cmd.d.ts
ADDED
package/cmd.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
class XCmd {
|
|
3
|
+
static execDivider = "clrue91c60000e6rl7w03b40p";
|
|
4
|
+
static async exec(command, options) {
|
|
5
|
+
const { stdout } = await execa(
|
|
6
|
+
"zsh",
|
|
7
|
+
[
|
|
8
|
+
"-c",
|
|
9
|
+
`
|
|
10
|
+
echo ${XCmd.execDivider}
|
|
11
|
+
${command}
|
|
12
|
+
echo ${XCmd.execDivider}
|
|
13
|
+
`
|
|
14
|
+
],
|
|
15
|
+
options
|
|
16
|
+
);
|
|
17
|
+
return stdout.split(XCmd.execDivider)[1].trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
XCmd
|
|
22
|
+
};
|
package/crawler.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { EventBus } from 'vtils';
|
|
3
|
+
import { AsyncOrSync, OneOrMore } from 'vtils/types';
|
|
4
|
+
import { VaeObjectSchemaShapeOf, v } from 'vtils/vae';
|
|
5
|
+
import { XPuppeteerGetPageHtmlOptions } from './puppeteer';
|
|
6
|
+
export type XCrawlerData = Record<any, any>;
|
|
7
|
+
export type XCrawlerDataFieldSelectSelector = string;
|
|
8
|
+
export type XCrawlerDataFieldSelectFilter = [
|
|
9
|
+
string,
|
|
10
|
+
string | RegExp | ((el: HTMLElement) => boolean)
|
|
11
|
+
];
|
|
12
|
+
export type XCrawlerDataFieldSelectFunction = (el: HTMLElement) => AsyncOrSync<string | HTMLElement | undefined>;
|
|
13
|
+
export type XCrawlerDataFieldSelect = XCrawlerDataFieldSelectSelector | XCrawlerDataFieldSelectFilter | XCrawlerDataFieldSelectFunction;
|
|
14
|
+
export type XCrawlerDataFieldExtractExtract = 'getPlainText' | 'getHTML' | 'getFirstImageUrl' | 'getFirstLinkUrl';
|
|
15
|
+
export type XCrawlerDataFieldExtract<V> = {
|
|
16
|
+
/**
|
|
17
|
+
* 选择元素
|
|
18
|
+
*
|
|
19
|
+
* - 可以是一个选择器字符串;
|
|
20
|
+
* - 可以是一个筛选器数组,[选择器, 筛选条件];
|
|
21
|
+
* - 也可以是一个返回元素的函数;
|
|
22
|
+
* - 也可以是一个返回文本的函数,若此,将跳过 ocr 和 action。
|
|
23
|
+
*/
|
|
24
|
+
select?: XCrawlerDataFieldSelect;
|
|
25
|
+
/**
|
|
26
|
+
* 是否识别选中内容中的图片为文本并替换
|
|
27
|
+
*
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
ocr?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 对选中元素导出结果文本
|
|
33
|
+
*
|
|
34
|
+
* @default 'getPlainText'
|
|
35
|
+
*/
|
|
36
|
+
extract?: XCrawlerDataFieldExtractExtract;
|
|
37
|
+
/**
|
|
38
|
+
* 若有需要,可对结果文本进行转换
|
|
39
|
+
*
|
|
40
|
+
* @param text 结果文本
|
|
41
|
+
*/
|
|
42
|
+
transform?: (text: string) => AsyncOrSync<V>;
|
|
43
|
+
};
|
|
44
|
+
export type XCrawlerDataExtract<T extends XCrawlerData> = {
|
|
45
|
+
[K in keyof T]?: XCrawlerDataFieldSelect | XCrawlerDataFieldExtract<T[K]>;
|
|
46
|
+
};
|
|
47
|
+
export type XCrawlerEngine = 'headless' | 'headful' | 'request';
|
|
48
|
+
export type XCrawlerContentType = 'html' | 'json';
|
|
49
|
+
export type XCrawlerNext<T extends XCrawlerData> = {
|
|
50
|
+
/**
|
|
51
|
+
* ID,可给其他 next 跳转以实现逻辑复用
|
|
52
|
+
*/
|
|
53
|
+
id?: string;
|
|
54
|
+
/**
|
|
55
|
+
* 过滤
|
|
56
|
+
*/
|
|
57
|
+
filter?: (data: T) => AsyncOrSync<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* 内容类型
|
|
60
|
+
*/
|
|
61
|
+
contentType?: XCrawlerContentType;
|
|
62
|
+
/**
|
|
63
|
+
* 通过元素
|
|
64
|
+
*/
|
|
65
|
+
element?: string | ((el: HTMLElement, extra: {
|
|
66
|
+
index: number;
|
|
67
|
+
root: HTMLElement;
|
|
68
|
+
}) => AsyncOrSync<OneOrMore<HTMLElement>>);
|
|
69
|
+
/**
|
|
70
|
+
* 通过字段(json)
|
|
71
|
+
*/
|
|
72
|
+
field?: string | ((json: any) => AsyncOrSync<any>);
|
|
73
|
+
/**
|
|
74
|
+
* 通过网址
|
|
75
|
+
*/
|
|
76
|
+
url?: keyof T | ((payload: {
|
|
77
|
+
data: T;
|
|
78
|
+
el: HTMLElement;
|
|
79
|
+
}) => AsyncOrSync<OneOrMore<string>>);
|
|
80
|
+
/**
|
|
81
|
+
* 下一页网址(有分页时用)
|
|
82
|
+
*
|
|
83
|
+
* 返回空字符串或跟当前页面网址相同的网址时将跳过
|
|
84
|
+
*/
|
|
85
|
+
nextPageUrl?: (payload: {
|
|
86
|
+
data: T;
|
|
87
|
+
/** 当前页面元素 */
|
|
88
|
+
el: HTMLElement;
|
|
89
|
+
/** 当前页面网址 */
|
|
90
|
+
url: string;
|
|
91
|
+
}) => AsyncOrSync<string | undefined>;
|
|
92
|
+
/** @inner */
|
|
93
|
+
_url?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* 抓取引擎
|
|
96
|
+
*/
|
|
97
|
+
engine?: XCrawlerEngine;
|
|
98
|
+
engineOptions?: XPuppeteerGetPageHtmlOptions;
|
|
99
|
+
/**
|
|
100
|
+
* 数据提取
|
|
101
|
+
*/
|
|
102
|
+
extract?: XCrawlerDataExtract<T>;
|
|
103
|
+
/**
|
|
104
|
+
* 下一步
|
|
105
|
+
*/
|
|
106
|
+
next?: OneOrMore<string | XCrawlerNext<T>>;
|
|
107
|
+
};
|
|
108
|
+
export type XCrawlerConfig<T extends XCrawlerData> = {
|
|
109
|
+
/**
|
|
110
|
+
* 入口页链接
|
|
111
|
+
*/
|
|
112
|
+
entry: string;
|
|
113
|
+
/**
|
|
114
|
+
* 使用代理
|
|
115
|
+
*/
|
|
116
|
+
proxy?: string;
|
|
117
|
+
/**
|
|
118
|
+
* 内容类型
|
|
119
|
+
*/
|
|
120
|
+
contentType?: XCrawlerContentType;
|
|
121
|
+
/**
|
|
122
|
+
* 抓取引擎
|
|
123
|
+
*/
|
|
124
|
+
engine?: XCrawlerEngine;
|
|
125
|
+
engineOptions?: XPuppeteerGetPageHtmlOptions;
|
|
126
|
+
/**
|
|
127
|
+
* 数据格式
|
|
128
|
+
*/
|
|
129
|
+
schema: (_: typeof v) => VaeObjectSchemaShapeOf<T>;
|
|
130
|
+
/**
|
|
131
|
+
* 数据提取
|
|
132
|
+
*/
|
|
133
|
+
extract?: XCrawlerDataExtract<T>;
|
|
134
|
+
/**
|
|
135
|
+
* 下一步
|
|
136
|
+
*/
|
|
137
|
+
next?: OneOrMore<XCrawlerNext<T>>;
|
|
138
|
+
/**
|
|
139
|
+
* 数据监听
|
|
140
|
+
*/
|
|
141
|
+
listen?: (data: T) => any;
|
|
142
|
+
};
|
|
143
|
+
export declare class XCrawler<T extends XCrawlerData> extends EventBus<{
|
|
144
|
+
newData: (data: T) => any;
|
|
145
|
+
}> {
|
|
146
|
+
private config;
|
|
147
|
+
private schemaMap;
|
|
148
|
+
private id;
|
|
149
|
+
constructor(config: XCrawlerConfig<T>);
|
|
150
|
+
private normalizeUrl;
|
|
151
|
+
private fetchUrlHeadless;
|
|
152
|
+
private fetchUrlHeadful;
|
|
153
|
+
private fetchUrlRequest;
|
|
154
|
+
private fetchUrl;
|
|
155
|
+
private fetchUrlAsHTMLElement;
|
|
156
|
+
private fetchUrlAsJSON;
|
|
157
|
+
private ocr;
|
|
158
|
+
private findNextById;
|
|
159
|
+
private pageUrlCache;
|
|
160
|
+
next(pageUrl: string, contentType: XCrawlerContentType, engine: XCrawlerEngine, elOrField: any, data: Partial<T>, next: XCrawlerNext<T>, index: number, rootElement: HTMLElement): Promise<void>;
|
|
161
|
+
start(): Promise<void>;
|
|
162
|
+
}
|
package/crawler.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { xr } from "@jayfong/x-request";
|
|
2
|
+
import { parse as parseHTML } from "node-html-parser";
|
|
3
|
+
import {
|
|
4
|
+
EventBus,
|
|
5
|
+
castArray,
|
|
6
|
+
cloneDeepFast,
|
|
7
|
+
escape,
|
|
8
|
+
get,
|
|
9
|
+
isRegExp,
|
|
10
|
+
isUrl,
|
|
11
|
+
keysStrict,
|
|
12
|
+
memoize,
|
|
13
|
+
omitStrict
|
|
14
|
+
} from "vtils";
|
|
15
|
+
import { v } from "vtils/vae";
|
|
16
|
+
import { XOcr } from "./ocr.js";
|
|
17
|
+
import { XPuppeteer } from "./puppeteer.js";
|
|
18
|
+
class XCrawler extends EventBus {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.schemaMap = config.schema(v);
|
|
23
|
+
if (config.listen) {
|
|
24
|
+
this.on("newData", (newData) => {
|
|
25
|
+
config.listen(newData);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
schemaMap;
|
|
30
|
+
id = Date.now().toString(36);
|
|
31
|
+
normalizeUrl(pageUrl, url) {
|
|
32
|
+
url = url.trim();
|
|
33
|
+
return !url ? "" : isUrl(url) ? url : new URL(
|
|
34
|
+
url.startsWith(".") || url.startsWith("/") ? url : `/${url}`,
|
|
35
|
+
pageUrl
|
|
36
|
+
).toString();
|
|
37
|
+
}
|
|
38
|
+
async fetchUrlHeadless(pageUrl, url, options) {
|
|
39
|
+
const text = await XPuppeteer.getPageHtml(this.normalizeUrl(pageUrl, url), {
|
|
40
|
+
headful: false,
|
|
41
|
+
proxy: this.config.proxy,
|
|
42
|
+
blockAssets: true,
|
|
43
|
+
instanceId: this.id,
|
|
44
|
+
...options
|
|
45
|
+
});
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
async fetchUrlHeadful(pageUrl, url, options) {
|
|
49
|
+
const text = await XPuppeteer.getPageHtml(this.normalizeUrl(pageUrl, url), {
|
|
50
|
+
headful: true,
|
|
51
|
+
proxy: this.config.proxy,
|
|
52
|
+
blockAssets: true,
|
|
53
|
+
instanceId: this.id,
|
|
54
|
+
...options
|
|
55
|
+
});
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
async fetchUrlRequest(pageUrl, url) {
|
|
59
|
+
const absoluteUrl = this.normalizeUrl(pageUrl, url);
|
|
60
|
+
const { data: text } = await xr.getText({
|
|
61
|
+
url: absoluteUrl,
|
|
62
|
+
autoReferer: true,
|
|
63
|
+
proxyUrl: this.config.proxy
|
|
64
|
+
});
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
async fetchUrl(pageUrl, url, engine, options) {
|
|
68
|
+
const text = engine === "headless" ? await this.fetchUrlHeadless(pageUrl, url, options) : engine === "headful" ? await this.fetchUrlHeadful(pageUrl, url, options) : await this.fetchUrlRequest(pageUrl, url);
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
async fetchUrlAsHTMLElement(pageUrl, url, engine, options) {
|
|
72
|
+
const text = await this.fetchUrl(pageUrl, url, engine, options);
|
|
73
|
+
return parseHTML(text);
|
|
74
|
+
}
|
|
75
|
+
async fetchUrlAsJSON(pageUrl, url, engine) {
|
|
76
|
+
const text = await this.fetchUrl(pageUrl, url, engine);
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
async ocr(pageUrl, el) {
|
|
80
|
+
const imgDocs = el.querySelectorAll("img");
|
|
81
|
+
for (const imgDoc of imgDocs) {
|
|
82
|
+
const imgUrl = imgDoc.getAttribute("src");
|
|
83
|
+
if (imgUrl) {
|
|
84
|
+
const ocrText = await XOcr.recognize(
|
|
85
|
+
this.normalizeUrl(pageUrl, imgUrl)
|
|
86
|
+
);
|
|
87
|
+
imgDoc.replaceWith(`<div>${escape(ocrText)} </div>`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
findNextById = memoize((id) => {
|
|
92
|
+
const findNextById = (nexts) => {
|
|
93
|
+
for (const next of castArray(nexts)) {
|
|
94
|
+
if (typeof next !== "string") {
|
|
95
|
+
if (next.id === id) {
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
if (next.next) {
|
|
99
|
+
const _next = findNextById(next.next);
|
|
100
|
+
if (_next) {
|
|
101
|
+
return _next;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return this.config.next && findNextById(this.config.next);
|
|
108
|
+
});
|
|
109
|
+
pageUrlCache = /* @__PURE__ */ new Set();
|
|
110
|
+
async next(pageUrl, contentType, engine, elOrField, data, next, index, rootElement) {
|
|
111
|
+
if (next.filter && !await next.filter(data)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
contentType = next.contentType || contentType;
|
|
115
|
+
engine = next.engine || engine;
|
|
116
|
+
if (contentType === "json") {
|
|
117
|
+
const field = elOrField;
|
|
118
|
+
const nextFields = next.url ? await Promise.all(
|
|
119
|
+
castArray(
|
|
120
|
+
typeof next.url === "function" ? await next.url(data) : data[next.url]
|
|
121
|
+
).map((url) => this.fetchUrlAsJSON(pageUrl, url, engine))
|
|
122
|
+
) : !next.field || next.field === "." ? [field] : typeof next.field === "string" ? get(field, next.field) : castArray(await next.field(field));
|
|
123
|
+
for (const nextField of nextFields) {
|
|
124
|
+
const newData = cloneDeepFast(data);
|
|
125
|
+
if (next.extract) {
|
|
126
|
+
const fieldKeys = keysStrict(next.extract);
|
|
127
|
+
for (const fieldKey of fieldKeys) {
|
|
128
|
+
const fieldConfig = typeof next.extract[fieldKey] === "string" || typeof next.extract[fieldKey] === "function" || Array.isArray(next.extract[fieldKey]) ? { select: next.extract[fieldKey] } : next.extract[fieldKey];
|
|
129
|
+
let fieldValue = !fieldConfig.select || fieldConfig.select === "." ? nextField : typeof fieldConfig.select === "string" ? get(nextField, fieldConfig.select) : Array.isArray(fieldConfig.select) ? "TODO" : "TODO";
|
|
130
|
+
if (fieldValue != null) {
|
|
131
|
+
fieldValue = fieldConfig.transform ? await fieldConfig.transform(fieldValue) : fieldValue;
|
|
132
|
+
const fieldSchema = this.schemaMap[fieldKey];
|
|
133
|
+
fieldValue = fieldSchema.cast(fieldValue);
|
|
134
|
+
newData[fieldKey] = fieldValue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (next.next) {
|
|
139
|
+
for (const nextItem of castArray(next.next)) {
|
|
140
|
+
await this.next(
|
|
141
|
+
pageUrl,
|
|
142
|
+
contentType,
|
|
143
|
+
engine,
|
|
144
|
+
nextField,
|
|
145
|
+
newData,
|
|
146
|
+
typeof nextItem === "string" ? this.findNextById(nextItem) : nextItem,
|
|
147
|
+
0,
|
|
148
|
+
rootElement
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
this.emit("newData", newData);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
const el = elOrField;
|
|
157
|
+
const nextEls = next.url ? await Promise.all(
|
|
158
|
+
next._url.map(
|
|
159
|
+
(fullUrl) => this.fetchUrlAsHTMLElement(
|
|
160
|
+
pageUrl,
|
|
161
|
+
fullUrl,
|
|
162
|
+
engine,
|
|
163
|
+
next.engineOptions
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
) : !next.element || next.element === "." ? [el] : typeof next.element === "string" ? el.querySelectorAll(next.element) : castArray(
|
|
167
|
+
await next.element(el, {
|
|
168
|
+
index,
|
|
169
|
+
root: rootElement
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
const nextParamsList = [];
|
|
173
|
+
let nextElIndex = 0;
|
|
174
|
+
for (const nextEl of nextEls) {
|
|
175
|
+
const newData = cloneDeepFast(data);
|
|
176
|
+
if (next.extract) {
|
|
177
|
+
const fieldKeys = keysStrict(next.extract);
|
|
178
|
+
for (const fieldKey of fieldKeys) {
|
|
179
|
+
const fieldConfig = typeof next.extract[fieldKey] === "string" || typeof next.extract[fieldKey] === "function" || Array.isArray(next.extract[fieldKey]) ? { select: next.extract[fieldKey] } : next.extract[fieldKey];
|
|
180
|
+
const fieldEl = !fieldConfig.select || fieldConfig.select === "." ? nextEl : typeof fieldConfig.select === "string" ? nextEl.querySelector(fieldConfig.select) : Array.isArray(fieldConfig.select) ? nextEl.querySelectorAll(fieldConfig.select[0]).find((item) => {
|
|
181
|
+
const filter = fieldConfig.select[1];
|
|
182
|
+
return typeof filter === "string" ? item.text.includes(filter) : isRegExp(filter) ? filter.test(item.text) : filter(item);
|
|
183
|
+
}) : await fieldConfig.select(nextEl);
|
|
184
|
+
if (fieldEl != null) {
|
|
185
|
+
let fieldText = "";
|
|
186
|
+
if (typeof fieldEl === "string") {
|
|
187
|
+
fieldText = fieldEl.trim();
|
|
188
|
+
} else {
|
|
189
|
+
if (fieldConfig.ocr) {
|
|
190
|
+
await this.ocr(pageUrl, fieldEl);
|
|
191
|
+
}
|
|
192
|
+
switch (fieldConfig.extract) {
|
|
193
|
+
case "getHTML":
|
|
194
|
+
fieldText = fieldEl.outerHTML;
|
|
195
|
+
break;
|
|
196
|
+
case "getFirstImageUrl":
|
|
197
|
+
fieldText = this.normalizeUrl(
|
|
198
|
+
pageUrl,
|
|
199
|
+
fieldEl.getAttribute("src") || fieldEl.querySelector("img")?.getAttribute("src") || ""
|
|
200
|
+
);
|
|
201
|
+
break;
|
|
202
|
+
case "getFirstLinkUrl":
|
|
203
|
+
fieldText = this.normalizeUrl(
|
|
204
|
+
pageUrl,
|
|
205
|
+
fieldEl.getAttribute("href") || fieldEl.querySelector("a")?.getAttribute("href") || ""
|
|
206
|
+
);
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
fieldText = fieldEl.text.trim();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let fieldValue = fieldConfig.transform ? await fieldConfig.transform(fieldText) : fieldText;
|
|
214
|
+
const fieldSchema = this.schemaMap[fieldKey];
|
|
215
|
+
if (fieldSchema.options.type !== "array") {
|
|
216
|
+
fieldValue = fieldSchema.cast(fieldValue);
|
|
217
|
+
} else {
|
|
218
|
+
fieldValue = fieldSchema.cast([
|
|
219
|
+
...newData[fieldKey] || [],
|
|
220
|
+
fieldValue
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
newData[fieldKey] = fieldValue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (next.next) {
|
|
228
|
+
const nexts = await Promise.all(
|
|
229
|
+
castArray(next.next).map(async (nextItem) => {
|
|
230
|
+
nextItem = cloneDeepFast(
|
|
231
|
+
typeof nextItem === "string" ? this.findNextById(nextItem) : nextItem
|
|
232
|
+
);
|
|
233
|
+
if (nextItem.url) {
|
|
234
|
+
nextItem._url = castArray(
|
|
235
|
+
typeof nextItem.url === "function" ? await nextItem.url({
|
|
236
|
+
data: newData,
|
|
237
|
+
el: nextEl
|
|
238
|
+
}) : newData[nextItem.url]
|
|
239
|
+
).map((url) => this.normalizeUrl(pageUrl, url)).filter((fullUrl) => !this.pageUrlCache.has(fullUrl)).map((fullUrl) => {
|
|
240
|
+
this.pageUrlCache.add(fullUrl);
|
|
241
|
+
return fullUrl;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return nextItem;
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
let i = 0;
|
|
248
|
+
for (const nextItem of nexts) {
|
|
249
|
+
nextParamsList.push([
|
|
250
|
+
nextItem.url && nextItem._url[i] || pageUrl,
|
|
251
|
+
contentType,
|
|
252
|
+
engine,
|
|
253
|
+
nextEl,
|
|
254
|
+
newData,
|
|
255
|
+
nextItem,
|
|
256
|
+
nextElIndex,
|
|
257
|
+
rootElement
|
|
258
|
+
]);
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
this.emit("newData", newData);
|
|
263
|
+
}
|
|
264
|
+
nextElIndex++;
|
|
265
|
+
}
|
|
266
|
+
for (const nextParams of nextParamsList) {
|
|
267
|
+
await this.next(...nextParams);
|
|
268
|
+
if (next.nextPageUrl) {
|
|
269
|
+
const nextPageUrl = await next.nextPageUrl({
|
|
270
|
+
data: nextParams[4],
|
|
271
|
+
el: nextParams[3],
|
|
272
|
+
url: nextParams[0]
|
|
273
|
+
});
|
|
274
|
+
if (nextPageUrl) {
|
|
275
|
+
const nextPageAbsoluteUrl = this.normalizeUrl(
|
|
276
|
+
nextParams[0],
|
|
277
|
+
nextPageUrl
|
|
278
|
+
);
|
|
279
|
+
if (nextPageAbsoluteUrl !== nextParams[0]) {
|
|
280
|
+
const nextPageElement = await this.fetchUrlAsHTMLElement(
|
|
281
|
+
nextPageAbsoluteUrl,
|
|
282
|
+
nextPageAbsoluteUrl,
|
|
283
|
+
engine,
|
|
284
|
+
this.config.engineOptions
|
|
285
|
+
);
|
|
286
|
+
await this.next(
|
|
287
|
+
nextPageAbsoluteUrl,
|
|
288
|
+
contentType,
|
|
289
|
+
engine,
|
|
290
|
+
nextPageElement,
|
|
291
|
+
nextParams[4],
|
|
292
|
+
omitStrict(next, ["url"]),
|
|
293
|
+
index,
|
|
294
|
+
rootElement
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async start() {
|
|
303
|
+
const initialContentType = this.config.contentType || "html";
|
|
304
|
+
const initialEngine = this.config.engine || "request";
|
|
305
|
+
const rootElement = initialContentType === "json" ? await this.fetchUrlAsJSON(
|
|
306
|
+
this.config.entry,
|
|
307
|
+
this.config.entry,
|
|
308
|
+
initialEngine
|
|
309
|
+
) : await this.fetchUrlAsHTMLElement(
|
|
310
|
+
this.config.entry,
|
|
311
|
+
this.config.entry,
|
|
312
|
+
initialEngine,
|
|
313
|
+
this.config.engineOptions
|
|
314
|
+
);
|
|
315
|
+
await this.next(
|
|
316
|
+
this.config.entry,
|
|
317
|
+
initialContentType,
|
|
318
|
+
initialEngine,
|
|
319
|
+
rootElement,
|
|
320
|
+
{},
|
|
321
|
+
{
|
|
322
|
+
extract: this.config.extract,
|
|
323
|
+
next: this.config.next
|
|
324
|
+
},
|
|
325
|
+
0,
|
|
326
|
+
rootElement
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
XCrawler
|
|
332
|
+
};
|
package/csv.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type XCsvParseOptions = {
|
|
2
|
+
arrayFieldNames?: string[];
|
|
3
|
+
arrayDelimiter?: string;
|
|
4
|
+
objectFieldNames?: string[];
|
|
5
|
+
booleanFieldNames?: string[];
|
|
6
|
+
dateFieldNames?: string[];
|
|
7
|
+
};
|
|
8
|
+
export type XCsvStringifyOptions = XCsvParseOptions;
|
|
9
|
+
export declare class XCsv {
|
|
10
|
+
static parse<T>(value: string, options?: XCsvParseOptions): Promise<T[]>;
|
|
11
|
+
static stringify(value: any[], options?: XCsvStringifyOptions): Promise<string>;
|
|
12
|
+
static read<T>(file: string, options?: XCsvParseOptions): Promise<T[]>;
|
|
13
|
+
static write(file: string, value: any, options?: XCsvStringifyOptions): Promise<void>;
|
|
14
|
+
}
|
package/csv.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as csv from "csv";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { mapValues } from "vtils";
|
|
4
|
+
import { formatDate } from "vtils/date";
|
|
5
|
+
class XCsv {
|
|
6
|
+
static async parse(value, options = {}) {
|
|
7
|
+
const res = [];
|
|
8
|
+
for await (const item of csv.parse(value, {
|
|
9
|
+
columns: true
|
|
10
|
+
})) {
|
|
11
|
+
res.push(
|
|
12
|
+
options.objectFieldNames?.length || options.booleanFieldNames?.length || options.dateFieldNames?.length || options.arrayFieldNames?.length && options.arrayDelimiter ? mapValues(item, (v, k) => {
|
|
13
|
+
if (options.arrayFieldNames.includes(k)) {
|
|
14
|
+
return v.split(options.arrayDelimiter).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
if (options.objectFieldNames?.includes(k)) {
|
|
17
|
+
return v ? JSON.parse(v) : {};
|
|
18
|
+
}
|
|
19
|
+
if (options.booleanFieldNames?.includes(k)) {
|
|
20
|
+
return v ? v === 1 : void 0;
|
|
21
|
+
}
|
|
22
|
+
if (options.dateFieldNames?.includes(k)) {
|
|
23
|
+
return v ? new Date(v) : void 0;
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
}) : item
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return res;
|
|
30
|
+
}
|
|
31
|
+
static async stringify(value, options = {}) {
|
|
32
|
+
let res = "";
|
|
33
|
+
for await (const line of csv.stringify(
|
|
34
|
+
options.objectFieldNames?.length || options.booleanFieldNames?.length || options.dateFieldNames?.length || options.arrayFieldNames?.length && options.arrayDelimiter ? value.map(
|
|
35
|
+
(item) => mapValues(item, (v, k) => {
|
|
36
|
+
if (options.arrayFieldNames.includes(k)) {
|
|
37
|
+
return v ? v.join(options.arrayDelimiter) : "";
|
|
38
|
+
}
|
|
39
|
+
if (options.objectFieldNames?.includes(k)) {
|
|
40
|
+
return JSON.stringify(v || {});
|
|
41
|
+
}
|
|
42
|
+
if (options.booleanFieldNames?.includes(k)) {
|
|
43
|
+
return v ? "1" : "0";
|
|
44
|
+
}
|
|
45
|
+
if (options.dateFieldNames?.includes(k)) {
|
|
46
|
+
return v ? formatDate(v, "yyyy-mm-dd hh:ii:ss") : v;
|
|
47
|
+
}
|
|
48
|
+
return v;
|
|
49
|
+
})
|
|
50
|
+
) : value,
|
|
51
|
+
{ header: true }
|
|
52
|
+
)) {
|
|
53
|
+
res += line;
|
|
54
|
+
}
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
static async read(file, options = {}) {
|
|
58
|
+
const value = await fs.promises.readFile(file, "utf8");
|
|
59
|
+
return XCsv.parse(value, options);
|
|
60
|
+
}
|
|
61
|
+
static async write(file, value, options = {}) {
|
|
62
|
+
await fs.promises.writeFile(file, await XCsv.stringify(value, options));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
XCsv
|
|
67
|
+
};
|
package/data_set.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OmitStrict } from 'vtils/types';
|
|
2
|
+
import { VaeObjectSchemaShapeOf, v } from 'vtils/vae';
|
|
3
|
+
export type XDataSetOptions<T extends Record<string, any>, K extends keyof T> = {
|
|
4
|
+
storeFile: string;
|
|
5
|
+
dataSchema: (_: typeof v) => VaeObjectSchemaShapeOf<T>;
|
|
6
|
+
dataKey: K;
|
|
7
|
+
csvArrayDelimiter?: string;
|
|
8
|
+
autoWriteInterval?: number;
|
|
9
|
+
noAutoWrite?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare class XDataSet<T extends Record<string, any>, K extends keyof T> {
|
|
12
|
+
private options;
|
|
13
|
+
private storeType;
|
|
14
|
+
private dataShapeSchema;
|
|
15
|
+
private dataSchema;
|
|
16
|
+
private csvOptions;
|
|
17
|
+
currentMap: Record<T[K], OmitStrict<T, K>>;
|
|
18
|
+
get current(): T[];
|
|
19
|
+
constructor(options: XDataSetOptions<T, K>);
|
|
20
|
+
read(): Promise<T[]>;
|
|
21
|
+
write(dataSet?: T[]): Promise<void>;
|
|
22
|
+
}
|