cparse
TypeScript icon, indicating that this package has built-in type declarations

2.2.0 • Public • Published

cparse

npm version License: MIT Node.js Version

一个基于 Cheerio 的强大 HTML 解析和数据提取工具库,专为简化网页数据抓取而设计。

✨ 核心特性

🎯 语法糖增强

  • 属性提取语法selector@attribute - 直接提取属性值
  • 数组提取语法[selector] - 获取所有匹配元素
  • 标准 CSS 支持:完全兼容 Cheerio 原生 CSS 选择器
  • 自定义伪选择器:not-empty - 扩展的伪选择器

🔧 强大的过滤器系统

  • 30+ 内置过滤器:数据类型转换、字符串处理、数组操作等
  • 过滤器链selector | filter1 | filter2 - 链式数据处理
  • 自定义过滤器:支持扩展自定义过滤器

🚀 Cheerio 功能扩展

  • 扩展方法.string(), .nextNode(), .extract(), .extractAll()
  • HTTP 集成:Axios 和 Got 客户端无缝集成
  • URL 处理:相对 URL 自动转换为绝对 URL

🛡️ 企业级特性

  • 完善的错误处理:多种错误类型和详细错误信息
  • 高性能设计:查询缓存、批量处理优化
  • TypeScript 支持:完整的类型定义
  • 全面测试:200+ 测试用例保证质量

📦 安装

npm install cparse

系统要求:Node.js >= 18.17.0

🚀 快速开始

基础用法

const { loadCheerio, parse } = require('cparse');

const html = '<div class="title">Hello World</div>';
const $ = loadCheerio(html);

// 传统用法
const title = parse('.title', $); // "Hello World"

// 🎯 新增:简化语法 - 直接在 $ 实例上调用 parse
const title2 = $.parse('.title'); // "Hello World"

// 数组提取(语法糖)
const items = $.parse('[.item]'); // 所有 .item 元素的文本数组

// 属性提取(语法糖)
const links = $.parse('[a@href]'); // 所有链接的 href 属性数组

// 过滤器链
const price = $.parse('.price | trim | float'); // 文本 -> 去空格 -> 转浮点数

🎯 简化语法对比

// ❌ 传统用法:需要传递 $ 参数
const title = parse('.title', $);
const data = parse({ title: '.title', count: '.count | int' }, $);

// ✅ 简化用法:直接在 $ 实例上调用
const title = $.parse('.title');
const data = $.parse({ title: '.title', count: '.count | int' });

结构化数据提取

const html = `
<div class="product">
  <h2 class="title">iPhone 15</h2>
  <span class="price">$999.00</span>
  <div class="rating" data-score="4.5">★★★★☆</div>
</div>
`;

const $ = loadCheerio(html);

// 使用简化语法提取结构化数据
const product = $.parse({
  title: '.title',
  price: '.price | regex:\\d+\\.\\d+ | float',
  rating: '.rating@data-score | float'
});

console.log(product);
// { title: "iPhone 15", price: 999.00, rating: 4.5 }

🔗 HTTP 客户端集成

Axios 集成

const axios = require('axios');
const { cheerioHookForAxios, parse } = require('cparse');

const client = axios.create();
cheerioHookForAxios(client);

// 响应自动包含 $ 属性,可以直接使用简化语法
const response = await client.get('https://example.com');
const title = response.$.parse('title');
const links = response.$.parse('[a@href]');

Got 集成

const got = require('got');
const { cheerioHookForGot, parse } = require('cparse');

const client = got.extend({});
cheerioHookForGot(client);

const response = await client.get('https://example.com');
const data = response.$.parse({
  title: 'title',
  description: 'meta[name="description"]@content'
});

🎯 简化语法 - 直接在 $ 实例上调用

v2.0.2+ 新增功能:现在可以直接在 Cheerio 实例上调用 parse 方法,无需传递 $ 参数!

语法对比

传统用法 简化用法 说明
parse('.title', $) $.parse('.title') 基本选择器
parse('[.item]', $) $.parse('[.item]') 数组提取
parse('a@href', $) $.parse('a@href') 属性提取
parse('.price | float', $) $.parse('.price | float') 过滤器链
parse({...}, $) $.parse({...}) 结构化数据

使用示例

const { loadCheerio } = require('cparse');
const $ = loadCheerio('<div class="title">Hello</div>');

// ✅ 推荐:使用简化语法
const title = $.parse('.title');
const data = $.parse({
  title: '.title',
  items: '[.item]',
  link: 'a@href'
});

// ❌ 传统用法(仍然支持)
const { parse } = require('cparse');
const title2 = parse('.title', $);

🎯 核心语法糖功能

cparse 的核心价值在于提供简洁的语法糖,简化常见的数据提取操作:

1. 属性提取语法 @

// 传统 Cheerio 写法
$('a').map((i, el) => $(el).attr('href')).get();

// cparse 简化语法
$.parse('[a@href]');

2. 数组提取语法 []

// 传统 Cheerio 写法
$('.item').map((i, el) => $(el).text()).get();

// cparse 简化语法
$.parse('[.item]');

3. 标准 CSS 选择器支持

// 完全支持 Cheerio 原生 CSS 选择器
$.parse('div.active');           // 类选择器
$.parse('input[type="text"]');   // 属性选择器
$.parse('li:first-child');       // 伪选择器

4. 自定义伪选择器

// :not-empty 伪选择器(Cheerio 原生不支持)
parse('p:not-empty', $); // 转换为 p:not(:empty)

🔧 强大的过滤器系统

cparse 提供了 30+ 内置过滤器,支持链式调用进行复杂的数据处理:

// 过滤器链示例
parse('.price | trim | regex:\\d+\\.\\d+ | float', $);
// 文本 -> 去空格 -> 正则提取 -> 转浮点数

📊 过滤器分类

数据类型转换

过滤器 功能 示例
int 转换为整数 parse('.count | int', $)
float 转换为浮点数 parse('.price | float', $)
bool 转换为布尔值 parse('.active | bool', $)

字符串处理

过滤器 功能 示例
trim 去除首尾空白 parse('.title | trim', $)
slice 字符串切片 parse('.text | slice:0:10', $)
regex 正则表达式匹配 parse('.text | regex:\\d+', $)
replace 字符串替换 parse('.text | replace:old:new', $)
split 字符串分割 parse('.text | split:,', $)
upper/lower 大小写转换 parse('.text | upper', $)
capitalize 首字母大写 parse('.text | capitalize', $)
title 标题格式化 parse('.text | title', $)

数组处理

过滤器 功能 示例
length 获取长度 parse('[.items] | length', $)
first/last 首/末元素 parse('[.items] | first', $)
unique 数组去重 parse('[.items] | unique', $)
sort 数组排序 parse('[.items] | sort', $)
compact 过滤空值 parse('[.items] | compact', $)
join 数组连接 parse('[.items] | join:-', $)

特殊处理

过滤器 功能 示例
date 日期解析 parse('.date | date', $)
size 尺寸解析 parse('.filesize | size', $)
number 数字格式化 parse('.price | number:2', $)
default 提供默认值 parse('.optional | default:"N/A"', $)

使用 default 过滤器处理缺失值

在抓取数据时,经常会遇到某些字段缺失的情况。如果一个选择器没有匹配到任何元素,解析结果通常是 null,这可能导致后续的过滤器(如 float)出错。使用 default 过滤器可以优雅地处理这种情况:

const { loadCheerio } = require('cparse');

const html = `
<div class="product">
  <span class="name">Product A</span>
  <span class="price">$19.99</span>
</div>
<div class="product">
  <span class="name">Product B</span>
  // 价格缺失
</div>
`;

const $ = loadCheerio(html);

const products = $.parse(['.product', {
  name: '.name',
  // 如果 .price 不存在,结果为 null,float(null) 会是 NaN
  // 使用 default 过滤器提供一个默认值 0
  price: '.price | float | default:0'
}]);

console.log(products);
// [
//   { name: 'Product A', price: 19.99 },
//   { name: 'Product B', price: 0 }
// ]

📚 API 参考

核心函数

loadCheerio(html, options?, baseUrl?)

加载 HTML 并返回扩展的 Cheerio 实例

const $ = loadCheerio('<div>Hello</div>', {}, 'https://example.com');

parse(rule, $, filters?)

数据解析核心函数

// 字符串规则
parse('h1', $)

// 对象规则
parse({ title: 'h1', links: '[a@href]' }, $)

// 数组规则(分割器语法)
parse(['[.item]', { name: '.name', price: '.price | float' }], $)

HTTP 集成

cheerioHookForAxios(instance, options?)

为 Axios 添加 Cheerio 支持

cheerioHookForGot(instance, options?)

为 Got 添加 Cheerio 支持

🎯 查询语法详解

基础语法

语法 说明 示例
selector 标准 CSS 选择器 parse('h1', $)
selector@attr 属性提取语法糖 parse('a@href', $)
[selector] 数组提取语法糖 parse('[.item]', $)
selector | filter 过滤器链 parse('.price | float', $)

语法糖功能

1. 标准 CSS 选择器支持

// 完全支持 Cheerio 原生 CSS 选择器
parse('div.active', $)           // 类选择器
parse('input[type="text"]', $)   // 属性选择器
parse('li:first-child', $)       // 伪选择器

2. 自定义伪选择器

// 语法糖(cparse 扩展)
$.parse('p:not-empty')
// 转换为 Cheerio 原生
$.parse('p:not(:empty)')

3. 复杂选择器支持

// 直接使用 Cheerio 原生选择器
parse('nav > ul > li:first-child', $)
parse('input[type="text"]:focus', $)
parse('p:contains("重要")', $)

高级用法

结构化数据提取

const data = parse({
  title: 'h1',
  price: '.price | float',
  tags: '[.tag]',
  link: 'a@href'
}, $);

分割器语法(列表处理)

const items = parse([
  '[.product]',  // 分割器:每个 .product 元素
  {
    name: '.name',
    price: '.price | float',
    inStock: '.stock | bool'
  }
], $);

函数处理

const result = parse([
  '.content',
  text => text.toUpperCase(),
  text => text.trim()
], $);

🚀 Cheerio 扩展方法

cparse 为 Cheerio 添加了便捷的扩展方法:

扩展方法列表

方法 功能 示例
.string() 纯文本内容(不含子元素标签) $('.content').string()
.nextNode() 下一个兄弟节点的文本 $('.label').nextNode()
.extract(attr) 提取单个元素的属性/内容 $('.item').extract('href')
.extractAll(attr) 提取所有元素的属性/内容 $('.items').extractAll('text')

特殊属性值

extract()extractAll() 中可使用的特殊属性:

  • text: 文本内容
  • html: HTML 内容
  • outerHtml: 包含元素本身的 HTML
  • string: 纯文本内容
  • nextNode: 下一个兄弟节点文本

🛡️ 错误处理

cparse 提供完善的错误处理机制:

错误类型

  • QueryParseError: 查询语法错误
  • FilterError: 过滤器执行错误
  • ValidationError: 参数验证错误
try {
  parse('.text | unknownFilter', $);
} catch (error) {
  if (error.name === 'FilterError') {
    console.log(`过滤器错误: ${error.filterName}`);
    console.log(`可用过滤器: ${error.context.availableFilters}`);
  }
}

⚡ 性能优化

自动查询缓存

// 第一次解析会缓存结果
parse('h1', $);
// 第二次使用相同查询直接使用缓存
parse('h1', $); // 更快

批量处理建议

// ✅ 推荐:一次性提取所有数据
const data = parse({
  titles: '[h1]',
  links: '[a@href]',
  prices: '[.price | float]'
}, $);

// ❌ 避免:多次单独查询

🔄 重构优化

v2.0.0 重大更新

🎯 核心优化

  • 移除重复实现:删除与 Cheerio 原生功能重复的代码
  • 专注语法糖:保留真正有价值的语法糖功能
  • 性能提升:直接使用 Cheerio 原生选择器,性能更优
  • 代码简化:代码量减少 40%,维护性大幅提升

🚀 保留的核心价值

  • ✅ 属性提取语法:selector@attribute
  • ✅ 数组提取语法:[selector]
  • ✅ 标准 CSS 支持:完全兼容 Cheerio 原生选择器
  • ✅ 自定义伪选择器::not-empty
  • ✅ 强大的过滤器系统
  • ✅ 结构化数据提取
  • ✅ HTTP 客户端集成

🗑️ 移除的重复功能

  • ❌ 条件查询处理(Cheerio 原生支持)
  • ❌ 嵌套查询处理(Cheerio 原生支持)
  • ❌ 伪选择器重复实现(Cheerio 原生支持)

🤝 贡献

欢迎提交 Issue 和 Pull Request!

开发环境

# 克隆项目
git clone https://github.com/your-username/cparse.git

# 安装依赖
npm install

# 运行测试
npm test

# 运行 lint
npm run lint

📄 许可证

MIT License - 详见 LICENSE 文件


⭐ 如果这个项目对你有帮助,请给个 Star!

🔌 插件化架构与自定义集成

v2.1.0+ 新增功能cparse 引入了通用的插件创建工厂函数 createCheerioHook,允许你为任何 HTTP 客户端(或任何返回 HTML 的数据源)轻松创建集成插件。

cheerioHookForAxioscheerioHookForGot 内部就是使用这个工厂函数实现的。

createCheerioHook(options)

这个函数接受一个配置对象,并返回一个标准的钩子函数。

配置项 options:

  • name (string, 必需): 插件的名称,用于错误和警告信息 (例如: 'node-fetch')。
  • validate(instance) (function, 必需): 一个函数,用于验证传入的客户端实例是否有效。如果无效,应返回 false
  • attach(instance, hookFn) (function, 必需): 一个函数,负责将核心处理逻辑 (hookFn) 附加到客户端实例的生命周期钩子上(例如,在响应完成后执行)。
  • getBody(response) (function, 必需): 一个函数,告诉 cparse 如何从客户端的响应对象中提取 HTML 文本。
  • getUrl(response) (function, 必需): 一个函数,用于从响应对象中获取最终的请求 URL,这对于解析页面上的相对链接至关重要。

示例:为 node-fetch 创建集成插件

下面是一个完整的示例,展示了如何为 node-fetch(v3+)封装一个解析钩子。

const fetch = require('node-fetch');
const { createCheerioHook, parse } = require('cparse');

// 1. 使用 createCheerioHook 定义针对 node-fetch 的钩子
const cheerioHookForFetch = createCheerioHook({
  name: 'node-fetch',
  // 验证 fetch 函数本身(在这个例子中我们直接扩展 fetch)
  validate: (instance) => typeof instance === 'function',
  // getBody 和 getUrl 是异步的,所以 attach 也要是 async
  getBody: async (response) => await response.text(),
  getUrl: (response) => response.url,
  // 核心:替换原始的 fetch 函数
  attach: (originalFetch, hook) => {
    // 返回一个新的、被包装过的 fetch 函数
    return async (...args) => {
      const response = await originalFetch(...args);
      // 调用我们通用的处理逻辑
      // 注意:由于 getBody 是异步的,这里也需要 await
      return await hook(response);
    };
  },
});

// 2. 包装原始的 fetch 函数
const enhancedFetch = cheerioHookForFetch(fetch);

// 3. 使用增强后的 fetch
async function main() {
  const response = await enhancedFetch('https://example.com');
  const title = response.$.parse('title');
  console.log(title); // "Example Domain"
}

main();

通过这种方式,你可以将 cparse 的解析能力无缝集成到任何你选择的工具中。

Package Sidebar

Install

npm i cparse

Weekly Downloads

219

Version

2.2.0

License

MIT

Unpacked Size

122 kB

Total Files

25

Last publish

Collaborators

  • wind2sing