# cparse

[![npm version](https://badge.fury.io/js/cparse.svg)](https://badge.fury.io/js/cparse)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js Version](https://img.shields.io/node/v/cparse.svg)](https://nodejs.org/)

一个基于 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+ 测试用例保证质量

## 📦 安装

```bash
npm install cparse
```

**系统要求**：Node.js >= 18.17.0

## 🚀 快速开始

### 基础用法

```javascript
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'); // 文本 -> 去空格 -> 转浮点数
```

### 🎯 简化语法对比

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

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

### 结构化数据提取

```javascript
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 集成

```javascript
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 集成

```javascript
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({...})` | 结构化数据 |

### 使用示例

```javascript
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. 属性提取语法 `@`

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

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

### 2. 数组提取语法 `[]`

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

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

### 3. 标准 CSS 选择器支持

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

### 4. 自定义伪选择器

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

## 🔧 强大的过滤器系统

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

```javascript
// 过滤器链示例
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` 过滤器可以优雅地处理这种情况：

```javascript
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 实例

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

#### `parse(rule, $, filters?)`
数据解析核心函数

```javascript
// 字符串规则
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 选择器支持
```javascript
// 完全支持 Cheerio 原生 CSS 选择器
parse('div.active', $)           // 类选择器
parse('input[type="text"]', $)   // 属性选择器
parse('li:first-child', $)       // 伪选择器
```

#### 2. 自定义伪选择器
```javascript
// 语法糖（cparse 扩展）
$.parse('p:not-empty')
// 转换为 Cheerio 原生
$.parse('p:not(:empty)')
```

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

### 高级用法

#### 结构化数据提取
```javascript
const data = parse({
  title: 'h1',
  price: '.price | float',
  tags: '[.tag]',
  link: 'a@href'
}, $);
```

#### 分割器语法（列表处理）
```javascript
const items = parse([
  '[.product]',  // 分割器：每个 .product 元素
  {
    name: '.name',
    price: '.price | float',
    inStock: '.stock | bool'
  }
], $);
```

#### 函数处理
```javascript
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**: 参数验证错误

```javascript
try {
  parse('.text | unknownFilter', $);
} catch (error) {
  if (error.name === 'FilterError') {
    console.log(`过滤器错误: ${error.filterName}`);
    console.log(`可用过滤器: ${error.context.availableFilters}`);
  }
}
```
## ⚡ 性能优化

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

### 批量处理建议
```javascript
// ✅ 推荐：一次性提取所有数据
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！

### 开发环境

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

# 安装依赖
npm install

# 运行测试
npm test

# 运行 lint
npm run lint
```

## 📄 许可证

MIT License - 详见 [LICENSE](LICENSE) 文件

---

**⭐ 如果这个项目对你有帮助，请给个 Star！**

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

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

`cheerioHookForAxios` 和 `cheerioHookForGot` 内部就是使用这个工厂函数实现的。

#### `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+）封装一个解析钩子。

```javascript
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` 的解析能力无缝集成到任何你选择的工具中。