跳转到主要内容

HTTP 请求(+Cookie 处理、代理、会话)

基类方法

要从网页采集数据,需要执行 HTTP 请求。在 A-Parser 的 JavaScript API v2 中,实现了一个易于使用的 HTTP 请求执行方法,该方法根据指定的方法参数返回一个 JSON 对象。接下来您将了解到:如何发起 HTTP 请求、该方法具有哪些参数和选项、指定选项的结果、如何指定 HTTP 请求成功的条件等。 这些方法继承自 BaseParser,是创建自定义爬虫工具的基础。 根据请求获取 HTTP 响应,参数说明如下:

  • method - 请求方法 (GET, POST...)

  • url - 请求链接

  • queryParams - 包含 GET 参数的哈希表或包含 POST 请求体的哈希表 HTTP 请求,或使用 会话管理器 保存以便由另一个线程执行。

这些方法继承自 BaseParser,是创建自定义爬虫工具的基础

await this.request(method, url[, queryParams][, opts])

await this.request(method, url, queryParams, opts)

根据请求获取 HTTP 响应,参数说明如下:

  • method - 请求方法 (GET, POST...)
  • url - 请求链接
  • queryParams - 包含 GET 参数的哈希表或包含 POST 请求体的哈希表
  • opts - 包含请求选项的哈希表

opts.check_content

check_content: [ 条件1, 条件2, ...] - 用于检查所获取内容的条件数组,如果检查未 通过,则将使用另一个代理重试请求。

功能:

  • 支持使用字符串作为条件(按字符串包含关系搜索)
  • 支持使用正则表达式作为条件
  • 支持使用自定义检查函数,函数会接收响应数据和响应头
  • 可以同时指定多种不同类型的条件
  • 如需逻辑非,请将条件放入数组中,例如 check_content: ['xxxx', [/yyyy/]] 表示如果获取的数据 包含子字符串 xxxx 且正则表达式 /yyyy/ 在页面上 未找到匹配项,则请求将被视为成功

为了使请求成功,必须通过数组中指定的所有检查

示例(注释中说明了请求被视为成功所需的条件):

let response = await this.request('GET', set.query, {}, {
check_content: [
/<\/html>|<\/body>/, // 在获取的页面上应触发此正则表达式
['XXXX'], // 在获取的页面上不应有此子字符串
'</html>', // 在获取的页面上应有此子字符串
(data, hdr) => {
return hdr.Status == 200 && data.length > 100;
} // 此函数应返回 true
]
});

opts.decode

decode: 'auto-html' - 自动检测编码并转换为 utf8

可选值:

  • auto-html - 基于响应头、meta 标签和页面内容(推荐的最佳方案)
  • utf8 - 指定文档编码为 utf8
  • <encoding> - 任何其他编码

opts.headers

headers: { ... } - 包含响应头的哈希表,响应头名称需使用小写字母,也可以指定 cookie 等。

示例:

headers: {
accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'accept-encoding': 'gzip, deflate, br',
cookie: 'a=321; b=test',
'user-agent' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36'
}

opts.headers_order

headers_order: ['cookie', 'user-agent', ...] - 允许重新定义响应头的排序顺序

opts.onlyheaders

onlyheaders: 0 - 决定是否读取 data,如果启用(1),则只获取标头

opts.recurse

recurse: N - 最大重定向跳转次数,默认值为 7,可使用 0 来关闭 重定向

opts.proxyretries

proxyretries: N - 请求重试次数,默认取自爬虫工具设置

opts.parsecodes

parsecodes: { ... } - 爬虫工具视为成功的 HTTP 响应代码列表,默认取自 爬虫工具设置。如果指定 '*': 1,则所有响应都将被视为成功。

示例:

parsecodes: {
200: 1,
403: 1,
500: 1
}

opts.timeout

timeout: N - 响应超时时间(秒),默认取自爬虫工具设置

opts.do_gzip

do_gzip: 1 - 确定是否使用压缩 (gzip/deflate/br),默认开启 (1),如需关闭 需设置为 0

opts.max_size

max_size: N - 最大响应大小(字节),默认取自爬虫工具设置

opts.cookie_jar

cookie_jar: { ... } - 包含 Cookie 的哈希表。哈希表示例:

"cookie_jar": {
"version": 1,
".google.com": {
"/": {
"login": {
"value": "true"
},
"lang": {
"value": "ru-RU"
}
}
},
".test.google.com": {
"/": {
"id": {
"value": 155643
}
}
}

opts.attempt

attempt: N - 指定当前尝试次数,使用此参数时将忽略该请求的 内置重试处理器

opts.browser

browser: 1 - 自动模拟浏览器请求头 (1 - 开启, 0 - 关闭)

opts.use_proxy

use_proxy: 1 - 在 JS 爬虫工具内部为单个请求覆盖全局参数 Use proxy 的设置 (1 - 开启, 0 - 关闭)

opts.noextraquery

noextraquery: 0 - 禁用在请求 URL 中添加 Extra query string (1 - 开启, 0 - 禁用)

opts.save_to_file

save_to_file: file - 允许将文件直接下载到磁盘,无需先写入内存。改为 file 时需指定保存文件的名称和 保存文件的路径。使用此选项时,将忽略所有与 data 相关的内容(不会执行 opts.check_content 中的内容检查,response.data 将为空等)

opts.bypass_cloudflare

bypass_cloudflare: 0 - 使用 Chrome 浏览器自动绕过 CloudFlare 的 JavaScript 防护 (1 - 开启, 0 - 关闭)

在这种情况下,Chrome Headless 的控制由爬虫工具设置 bypassCloudFlareChromeMaxPages 执行 和 bypassCloudFlareChromeHeadless 来实现,需要在 static defaultConfstatic editableConf 中指定:

static defaultConf: typeof BaseParser.defaultConf = {
version: '0.0.1',
results: {
flat: [
['title', 'Title'],
]
},
max_size: 2 * 1024 * 1024,
parsecodes: {
200: 1,
},
results_format: "$title\n",
bypass_cloudflare: 1,
bypassCloudFlareChromeMaxPages: 20,
bypassCloudFlareChromeHeadless: 0
};

static editableConf: typeof BaseParser.editableConf = [
['bypass_cloudflare', ['textfield', 'bypass_cloudflare']],
['bypassCloudFlareChromeMaxPages', ['textfield', 'bypassCloudFlareChromeMaxPages']],
['bypassCloudFlareChromeHeadless', ['textfield', 'bypassCloudFlareChromeHeadless']],
];

async parse(set, results) {
const {success, data, headers} = await this.request('GET', set.query, {}, {
bypass_cloudflare: this.conf.bypass_cloudflare
});
return results;
}

opts.follow_meta_refresh

follow_meta_refresh: 0 - 允许跟随通过 HTML meta 标签声明的重定向:

<meta http-equiv="refresh" content="time; url=..."/>

opts.redirect_filter

redirect_filter: (hdr) => 1 | 0 - 允许设置重定向跳转的过滤函数,如果函数 返回 1,则爬虫工具将执行重定向(考虑 opts.recurse 参数),返回 0 时将停止 重定向跳转:

redirect_filter: (hdr) => {
if (hdr.location.match(/login/))
return 1;
return 0;
}

opts.follow_common_rediects

opts.follow_common_rediects: 0 - 决定是否跟随标准重定向(例如 http -> https 以及 www.domain.com -> domain.com),如果设置为 1,则爬虫工具会跟随标准重定向,不考虑 参数 opts.recurse

opts.http2

opts.http2: 0 - 确定在执行请求时是否使用 HTTP/2 协议,默认 使用 HTTP/1.1

opts.randomize_tls_fingerprint

opts.randomize_tls_fingerprint: 0 - 此选项允许通过 TLS 指纹绕过网站封禁 (1 - 开启, 0 - 关闭)

opts.tlsOpts

tlsOpts: { ... } – 允许 为 HTTPS 连接传递 设置 HTTPS 连接 ​

await this.cookies.*

处理当前请求的 Cookie

.getAll()

获取 Cookie 数组

await this.cookies.getAll();
获取 Cookie 数组的结果示例

.setAll(cookie_jar)

设置 Cookie,必须传递一个包含 Cookie 的哈希表作为参数

async parse(set, results) {
this.logger.put("Start scraping query: " + set.query);

await this.cookies.setAll({
"version": 1,
".google.com": {
"/": {
"login": {
"value": "true"
},
"lang": {
"value": "ru-RU"
}
}
},
".test.google.com": {
"/": {
"id": {
"value": 155643
}
}
}
});

let cookies = await this.cookies.getAll();

this.logger.put("Cookies: " + JSON.stringify(cookies));

results.SKIP = 1;
return results;
}
设置 Cookie 数组的结果示例

.set(host, path, name, value)

await this.cookies.set(host, path, name, value) - 设置单个 Cookie。

Cookie 的作用域直接取决于指定的域名格式,因此在 host 中会考虑主机名前是否有点:

  • 如果指定了点 (this.cookies.set('.domain.com', ...)),则 Cookie 将用于所有子域名(例如 a.domain.com, b.a.domain.com
  • 如果主机名开头没有点 (this.cookies.set('site.com', ...)),则 Cookie 将严格用于指定的主机(host-only cookie),不会传递给子域名
信息

这种区别至关重要,因为带点和不带点的 Cookie 同时存在可能导致重复并引起网站运行不可预测。为了正确模拟,请务必检查目标网站是如何设置 Cookie 的(是否带有 Domain 属性),并使用相应的格式。

async parse(set, results) {
this.logger.put("Start scraping query: " + set.query);

await this.cookies.set('.a-parser.com', '/', 'Test-cookie-1', 1);
await this.cookies.set('.a-parser.com', '/', 'Test-cookie-2', 'test-value');

let cookies = await this.cookies.getAll();

this.logger.put("Cookies: " + JSON.stringify(cookies));

results.SKIP = 1;
return results;
}
设置单个 Cookie 的结果示例

await this.proxy.*

处理代理

.next()

切换到下一个代理,旧代理将不再用于当前请求

.ban()

切换并封禁代理(当服务按 IP 封锁操作时需使用),代理将被封禁一段时间, 时长由爬虫工具设置 (proxybannedcleanup) 指定

.get()

获取当前代理(最后一次发出请求时使用的代理)

.set(proxy, noChange?)

await this.proxy.set('http://127.0.0.1:8080', true) - 为下一次请求设置代理。参数 noChange 为可选,如果设为 true,代理在各次尝试之间不会变化。默认 noChange = false

await this.sessionManager.*

会话处理方法。每个会话必须存储所使用的代理和 Cookie。此外还可以额外保存任意数据。 要在 JS 爬虫工具中使用会话,首先必须初始化会话管理器。这可以通过在 init() 中调用 await this.sessionManagerinit() 方法来完成

.init(opts?)

初始化会话管理器。可以传递一个包含附加参数的对象 (opts) 作为参数(所有参数均为可选):

  • name - 允许重新定义会话所属的爬虫工具名称,默认为执行初始化的爬虫工具名称
  • waitForSession - 指示爬虫工具在会话出现之前一直等待(这只在多个任务同时运行时才相关,例如一个生成会话,另一个使用会话),也就是说 .get().reset() 将始终等待会话
  • domain - 指示在该爬虫工具保存的所有会话中查找(如果未指定值),或者只针对特定域名查找(需要在域名前加点,例如 .site.com
  • sessionsKey - 允许手动指定会话存储名称;如果未指定,则会根据 name(或未指定 name 时的爬虫工具名称)、域名和代理检测器自动生成
  • expire - 设置会话有效期(分钟),默认无限制

使用示例:

async init() {
await this.sessionManager.init({
name: 'JS::test',
expire: 15 * 60
});
}

.get(opts?)

获取新会话,必须在执行请求之前(第一次尝试之前)调用。返回保存在会话中的任意数据对象。可以传递一个包含附加参数的对象 (opts) 作为参数(所有参数均为可选):

  • waitTimeout - 可指定等待会话出现的分钟数,独立于 .init() 中的 waitForSession 参数(会忽略它),到期后将使用空会话
  • tag - 获取带有指定标签的会话,例如可以使用域名将会话绑定到获取它们的域名

使用示例:

await this.sessionManager.get({
waitTimeout: 10,
tag: 'test session'
})

.reset(opts?)

清除 Cookie 并获取新会话。如果当前会话的请求不成功,则必须使用此方法。返回保存在会话中的任意数据对象。可以传递一个包含附加参数的对象 (opts) 作为参数(所有参数均为可选):

  • waitTimeout - 可指定等待会话出现的分钟数,独立于 .init() 中的 waitForSession 参数(会忽略它),到期后将使用空会话
  • tag - 获取带有指定标签的会话,例如可以使用域名将会话绑定到获取它们的域名

使用示例:

await this.sessionManager.reset({
waitTimeout: 5,
tag: 'test session'
})

.save(sessionOpts?, saveOpts?)

保存成功的会话,并支持在会话中保存任意数据。支持 2 个可选参数:

  • sessionOpts - 要存储在会话中的任意数据,可以是数字、字符串、数组或对象
  • saveOpts - 包含会话保存参数的对象:
    • multiply - 可选参数,允许复制会话,需指定一个数字作为值
    • tag - 可选参数,为保存的会话设置标签,例如可以使用域名将会话绑定到获取它们的域名

使用示例:

await this.sessionManager.save('some data here', {
multiply: 3,
tag: 'test session'
})

.count()

返回当前会话管理器的会话数量

使用示例:

let sesCount = await this.sessionManager.count();

.removeById(sessionId)

删除所有具有指定 id 的会话。返回已删除会话的数量。当前会话的 id 包含在变量 this.sessionId 中 使用示例:

const removedCount = await this.sessionManager.removeById(this.sessionId);

会话管理器综合使用示例

async init() {
await this.sessionManager.init({
expire: 15 * 60
});
}

async parse(set, results) {
let ses = await this.sessionManager.get();

for(let attempt = 1; attempt <= this.conf.proxyretries; attempt++) {
if(ses)
this.logger.put('Data from session:', ses);
const { success, data } = await this.request('GET', set.query, {}, { attempt });
if(success) {
// process data here
results.success = 1;
break;
} else if(attempt < this.conf.proxyretries) {
const removedCount = await this.sessionManager.removeById(this.sessionId);
this.logger.put(`Removed ${removedCount} bad sessions with id #${this.sessionId}`);
ses = await this.sessionManager.reset();
}
}

if(results.success) {
await this.sessionManager.save('Some data', { multiply: 2 });
this.logger.put(`Total we have ${await this.sessionManager.count()} sessions`);
}

return results;
}
保存任意数据并随后获取的示例

请求方法 await this.request

GET 方法

可以直接在请求字符串 https://a-parser.com/users/?type=staff 中传递请求参数:

const { success, data, headers } = await this.request('GET', 'https://a-parser.com/users/?type=staff');

或者作为 queryParams 中的对象传入,其中 key: value 等同于 param=value

const { success, data, headers } = await this.request('GET', 'https://a-parser.com/users/', {
type: 'staff'
});

POST 方法

如果使用 POST 方法,可以通过两种方式传递请求体:

  • queryParams 中列出变量名及其值,例如:

    {
    "key": set.query,
    "id": 1234,
    "type": "text"
    }
  • opts.body 中列出它们,例如:

    body: 'key=' + set.query + '&id=1234&type=text'

如果 请求体 以对象形式传入,它会自动转换为 form-urlencoded 格式;同样,如果指定了 body 且未 指定 content-type 标头,则会自动设置为 content-type: application/x-www-form-urlencoded

const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {
title: 'foo,',
body: 'bar',
userId: 1
});

如果 POST 请求体是字符串或 Buffer,则按原样传递:

// 带字符串的请求
const string = 'title=foo&body=bar&userId=1';
const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {}, {
body: string
});

// 带 Buffer 的请求
const string = 'title=foo&body=bar&userId=1';
const buf = Buffer.from(string, 'utf8');
const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {}, {
body: buf
});

文件上传

使用 form-data 模块通过 POST 请求发送文件:

const file = fs.readFileSync('pathToFile');
const FormData = require('form-data');
const format = new FormData();
format.append('file', file, 'fileName.ext');

const { success, data, headers } = await this.request('POST', 'https://file.io', {}, {
headers: format.getHeaders(),
body: format.getBuffer()
});

在内容类型为 multipart/form-dataPOST 请求中发送文件的示例:

const EOL = '\r\n';
const file = fs.readFileSync('pathToFile');
const boundary = '----WebKitFormBoundary' + String(Math.random()).slice(2);
const requestHeaders = {
'content-type': 'multipart/form-data; boundary=' + boundary
};

const body = '--'
+ boundary
+ EOL
+ 'Content-Disposition: form-data; name="file"; filename="fileName.ext"'
+ EOL
+ 'Content-Type: text/html'
+ EOL
+ EOL
+ file
+ EOL
+ '--'
+ boundary
+ '--';

const { success, data, headers } = await this.request('POST', 'https://file.io', {}, {
headers: requestHeaders,
body
});