This commit is contained in:
Your Name
2026-03-27 18:06:12 +08:00
parent 9160c36735
commit 099bc1dd22
645 changed files with 276473 additions and 957 deletions
+1
View File
@@ -0,0 +1 @@
github: [overtrue]
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
time: "21:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: phpunit/phpunit
versions:
- ">= 8.a, < 9"
@@ -0,0 +1,29 @@
name: Test
on: [push, pull_request]
jobs:
phpunit:
name: PHP-${{ matrix.php_version }}-${{ matrix.prefer }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php_version:
- 8.1
- 8.2
- 8.3
- 8.4
- 8.5
prefer:
- stable
- lowest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php_version }}
coverage: none
- name: Install Dependencies
run: composer update --prefer-dist --no-interaction --prefer-${{ matrix.prefer }}
- name: Run PHPUnit
run: ./vendor/bin/phpunit
+212
View File
@@ -0,0 +1,212 @@
## [6.0.0] - 2025-09-09
### 🚀 Major Features
- **性能优化策略系统**: 全新的转换策略架构,针对不同使用场景提供最优性能
- **内存优化策略** (Memory Optimized): 占用 ~400KB 内存,适合 Web 请求和内存受限环境
- **缓存策略** (Cached): 占用 ~4MB 内存,重复转换性能提升 2-3 倍,适合批处理任务
- **智能策略** (Smart): 占用 600KB-1.5MB 内存,根据文本复杂度自适应加载
- **ConverterFactory**: 新的工厂模式管理转换器实例和策略选择
- **自动策略选择**: 根据运行环境(Web/CLI/内存限制)自动推荐最佳策略
- **性能基准测试工具**: 内置的策略性能对比和监控工具
- **内存监控**: 支持实时监控内存使用情况和性能指标
### 🔧 API Changes
- 新增 `Pinyin::useMemoryOptimized()` - 切换到内存优化策略
- 新增 `Pinyin::useCached()` - 切换到缓存策略
- 新增 `Pinyin::useSmart()` - 切换到智能策略
- 新增 `Pinyin::useAutoStrategy()` - 自动选择最佳策略
- 新增 `Pinyin::clearCache()` - 清理所有转换器缓存
- 新增 `ConverterFactory::make($strategy)` - 创建指定策略的转换器
- 新增 `ConverterFactory::recommend()` - 获取推荐策略
- 新增 `ConverterFactory::getStrategiesInfo()` - 获取所有策略信息
### ⚡ Performance Improvements
- **内存使用优化**: 默认内存占用从 ~4MB 降低到 ~400KB
- **转换速度提升**: 缓存策略下重复转换速度提升 2-3 倍
- **智能加载**: 根据文本复杂度动态调整数据加载策略
- **按需加载**: 内存优化策略仅在需要时加载转换数据
### 📊 Benchmark & Monitoring
- 新增 `php benchmark/run.php` - 运行性能基准测试
- 新增 `php benchmark/compare-strategies.php` - 策略对比测试
- 新增基准测试文档 `docs/benchmark-guide.md`
- 支持内存使用情况实时监控
#### 运行基准测试
```bash
# 运行标准基准测试,显示所有方法的性能表现
php benchmark/run.php
# 详细的策略对比测试,对比三种策略的性能差异
php benchmark/compare-strategies.php
```
基准测试会显示:
- 每种策略的执行时间和内存使用情况
- 不同文本长度下的性能表现
- 策略之间的性能对比和推荐场景
### 🔄 Breaking Changes
- **默认策略变更**: 从全缓存改为内存优化策略,降低默认内存占用
- **转换器架构重构**: 引入策略模式,旧的直接实例化转换器方式仍兼容
- **性能特征变化**: 首次转换可能略慢,但内存占用显著降低
### 🔧 Backward Compatibility
- 完全兼容 5.x API,现有代码无需修改即可使用
- `heteronym()` 方法(5.3.3+ 引入)继续保持兼容
- 所有原有的转换方法 (`sentence`, `phrase`, `chars` 等) 保持不变
### 📚 Documentation
- 更新 README.md 增加性能优化策略说明
- 新增性能对比表格和使用建议
- 新增基准测试指南
- 新增性能优化最佳实践
### 🔧 Development Tools
- 新增性能基准测试脚本
- 新增策略对比工具
- 增强命令行工具支持策略选择
### 🛠️ Migration Guide
从 5.x 升级到 6.0 非常简单,所有现有代码都能正常工作:
#### 无需任何修改(推荐)
```php
// 5.x 和 6.x 都能正常工作
Pinyin::sentence('你好世界');
// 6.x 默认使用内存优化策略,内存占用更低
```
#### 保持 5.x 完全相同的性能特征
```php
// 如果你需要与 5.x 完全相同的高性能(高内存占用)
Pinyin::useCached(); // 一次设置,全局生效
Pinyin::sentence('你好世界');
```
#### 使用新的性能优化特性
```php
// 自动选择最佳策略(推荐用于新项目)
Pinyin::useAutoStrategy();
// 或者根据场景手动选择:
// Web 应用(内存受限)
Pinyin::useMemoryOptimized();
// 批处理任务(性能优先)
Pinyin::useCached();
// 通用场景(平衡)
Pinyin::useSmart();
```
#### 性能监控和优化
```php
// 监控内存使用
$initialMemory = memory_get_usage();
$result = Pinyin::sentence('测试文本');
$memoryUsed = memory_get_usage() - $initialMemory;
echo "内存使用: " . round($memoryUsed / 1024, 2) . " KB";
// 批处理完成后清理缓存
Pinyin::useCached();
// ... 批量处理 ...
Pinyin::clearCache(); // 释放内存
```
### 💡 Performance Comparison
| 策略 | 内存占用 | 首次转换 | 重复转换 | 推荐场景 |
|-----|---------|---------|---------|---------|
| Memory Optimized | ~400KB | 中等 | 中等 | Web 请求、内存受限环境 |
| Cached | ~4MB | 慢 | **最快** (2-3x) | 批处理、长时运行进程 |
| Smart | 600KB-1.5MB | 快 | 快 | 通用场景、自动优化 |
基于 1000 次转换的基准测试结果:
| 文本长度 | Memory Optimized | Cached | Smart |
|---------|-----------------|--------|-------|
| 短文本 (<10字) | 1.2ms | 0.5ms | 0.8ms |
| 中等文本 (10-50字) | 3.5ms | 1.2ms | 2.1ms |
| 长文本 (>100字) | 8.7ms | 3.1ms | 5.2ms |
## [5.3.4] - 2025-03-16
### 🚀 Features
- Resolved #211
### ⚙️ Miscellaneous Tasks
- Format
## [5.3.3] - 2024-08-01
### 🚀 Features
- 使用 heteronym 代替 polyphonic 多音字 #184
- 取首字母时,能否保留完整的英文 #199
### 🐛 Bug Fixes
- 修复 琢 的音频顺序 #207
- 补充案例 #207
- Tests
- Tests
## [5.3.2] - 2024-03-19
### 🚀 Features
- 取首字母时,能否保留完整的英文 #199
## [5.3.1] - 2024-03-19
### 🐛 Bug Fixes
- 「仆区」应该读 pú ōu 而非 pú qū #200
- Tests #200
## [5.3.0] - 2023-10-27
### 🚀 Features
- 添加sentenceFull,支持保留其他字符 (#198)
- Full sentence, #198
## [5.2.2] - 2023-09-27
### ⚙️ Miscellaneous Tasks
- Bin
## [5.2.1] - 2023-06-17
### 🐛 Bug Fixes
- Bin
## [5.2.0] - 2023-06-12
### 🚀 Features
- 增加 Pinyin::polyphonesAsArray. fixed #195
### 📚 Documentation
- 更新文档提示
- 更新文档提示
- 更新文档提示
## [5.1.0] - 2023-04-27
### 💼 Other
- 移除错误语法 (#190)
## [5.0.0] - 2022-07-24
### 🐛 Bug Fixes
- 优化符号匹配规则
## [1.0-beta] - 2014-07-16
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 安正超
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+528
View File
@@ -0,0 +1,528 @@
# Pinyin
[![Test](https://github.com/overtrue/pinyin/actions/workflows/test.yml/badge.svg)](https://github.com/overtrue/pinyin/actions/workflows/test.yml)
[![Latest Stable Version](https://poser.pugx.org/overtrue/pinyin/v/stable.svg)](https://packagist.org/packages/overtrue/pinyin) [![Total Downloads](https://poser.pugx.org/overtrue/pinyin/downloads.svg)](https://packagist.org/packages/overtrue/pinyin) [![Latest Unstable Version](https://poser.pugx.org/overtrue/pinyin/v/unstable.svg)](https://packagist.org/packages/overtrue/pinyin) [![License](https://poser.pugx.org/overtrue/pinyin/license.svg)](https://packagist.org/packages/overtrue/pinyin)
:cn: 基于 [mozillazg/pinyin-data](https://github.com/mozillazg/pinyin-data) 词典的中文转拼音工具,更准确的支持多音字的汉字转拼音解决方案。
[喜欢我的项目?点击这里支持我](https://github.com/sponsors/overtrue)
## 特性
- 准确的多音字支持
- 多种拼音风格(声调符号、数字声调、无声调)
- 支持姓氏识别
- 灵活的性能优化策略
- 内存友好的设计
- 完善的测试覆盖
## 安装
使用 Composer 安装:
```bash
composer require overtrue/pinyin:^6.0
```
## 使用
### 拼音风格
除了获取首字母的方法外,所有方法都支持第二个参数,用于指定拼音的格式,可选值为:
- `symbol` (默认)声调符号,例如 `pīn yīn`
- `none` 不输出声调,例如 `pin yin`
- `number` 末尾数字模式的拼音,例如 `pin1 yin1`
你可以使用字符串值或者 `ToneStyle` 枚举:
```php
use Overtrue\Pinyin\Pinyin;
use Overtrue\Pinyin\ToneStyle;
// 使用字符串
echo Pinyin::sentence('你好', 'none'); // ni hao
echo Pinyin::sentence('你好', 'number'); // ni3 hao3
echo Pinyin::sentence('你好', 'symbol'); // nǐ hǎo
// 使用枚举(推荐)
echo Pinyin::sentence('你好', ToneStyle::NONE); // ni hao
echo Pinyin::sentence('你好', ToneStyle::NUMBER); // ni3 hao3
echo Pinyin::sentence('你好', ToneStyle::SYMBOL); // nǐ hǎo
```
### 返回值
除了 `permalink` 返回字符串外,其它方法都返回集合类型 [`Overtrue\Pinyin\Collection`](https://github.com/overtrue/pinyin/blob/master/src/Collection.php)
```php
use Overtrue\Pinyin\Pinyin;
$pinyin = Pinyin::sentence('你好,世界');
```
你可以通过以下方式访问集合内容:
```php
echo $pinyin; // nǐ hǎo shì jiè
// 直接将对象转成字符串
$string = (string) $pinyin; // nǐ hǎo shì jiè
$pinyin->toArray(); // ['nǐ', 'hǎo', 'shì', 'jiè']
// 直接使用索引访问
$pinyin[0]; // 'nǐ'
// 使用函数遍历
$pinyin->map('ucfirst'); // ['Nǐ', 'Hǎo', 'Shì', 'Jiè']
// 拼接为字符串
$pinyin->join(' '); // 'nǐ hǎo shì jiè'
$pinyin->join('-'); // 'nǐ-hǎo-shì-jiè'
// 转成 json
$pinyin->toJson(); // '["nǐ","hǎo","shì","jiè"]'
json_encode($pinyin); // '["nǐ","hǎo","shì","jiè"]'
```
### 文字段落转拼音
```php
use Overtrue\Pinyin\Pinyin;
use Overtrue\Pinyin\ToneStyle;
echo Pinyin::sentence('带着希望去旅行,比到达终点更美好');
// dài zhe xī wàng qù lǚ xíng bǐ dào dá zhōng diǎn gèng měi hǎo
// 去除声调
echo Pinyin::sentence('带着希望去旅行,比到达终点更美好', ToneStyle::NONE);
// dai zhe xi wang qu lv xing bi dao da zhong dian geng mei hao
// 保留所有非汉字字符
echo Pinyin::fullSentence('ル是片假名,π是希腊字母', ToneStyle::NONE);
// ル shi pian jia ming ,π shi xi la zi mu
```
### 生成用于链接的拼音字符串
通常用于文章链接等,可以使用 `permalink` 方法获取拼音字符串:
```php
echo Pinyin::permalink('带着希望去旅行'); // dai-zhe-xi-wang-qu-lyu-xing
echo Pinyin::permalink('带着希望去旅行', '.'); // dai.zhe.xi.wang.qu.lyu.xing
```
### 获取首字符字符串
通常用于创建搜索用的索引,可以使用 `abbr` 方法转换:
```php
Pinyin::abbr('带着希望去旅行'); // ['d', 'z', 'x', 'w', 'q', 'l', 'x']
echo Pinyin::abbr('带着希望去旅行')->join('-'); // d-z-x-w-q-l-x
echo Pinyin::abbr('你好2018')->join(''); // nh2018
echo Pinyin::abbr('Happy New Year! 2018')->join(''); // HNY2018
// 保留原字符串的英文单词
echo Pinyin::abbr('CGV电影院', false, true)->join(''); // CGVdyy
```
**姓名首字母**
将首字作为姓氏转换,其余作为普通词语转换:
```php
Pinyin::nameAbbr('欧阳'); // ['o', 'y']
echo Pinyin::nameAbbr('单单单')->join('-'); // s-d-d
```
### 姓名转换
姓名的姓的读音有些与普通字不一样,比如 ‘单’ 常见的音为 `dan`,而作为姓的时候读 `shan`
```php
Pinyin::name('单某某'); // ['shàn', 'mǒu', 'mǒu']
Pinyin::name('单某某', 'none'); // ['shan', 'mou', 'mou']
Pinyin::name('单某某', 'none')->join('-'); // shan-mou-mou
```
### 护照姓名转换
根据国家规定 [关于中国护照旅行证上姓名拼音 ü(吕、律、闾、绿、女等)统一拼写为 YU 的提醒](http://sg.china-embassy.gov.cn/lsfw/zghz1/hzzxdt/201501/t20150122_2022198.htm) 的规则,将 `ü` 转换为 `yu`
```php
Pinyin::passportName('吕小布'); // ['lyu', 'xiao', 'bu']
Pinyin::passportName('女小花'); // ['nyu', 'xiao', 'hua']
Pinyin::passportName('律师'); // ['lyu', 'shi']
```
### 多音字
多音字的返回值为关联数组的集合,默认返回去重后的所有读音:
```php
$pinyin = Pinyin::heteronym('重庆');
$pinyin['重']; // ["zhòng", "chóng", "tóng"]
$pinyin['庆']; // ["qìng"]
$pinyin->toArray();
// [
// "重": ["zhòng", "chóng", "tóng"],
// "庆": ["qìng"]
// ]
```
如果不想要去重,可以数组形式返回:
```php
$pinyin = Pinyin::heteronym('重庆重庆', ToneStyle::SYMBOL, true);
// or
$pinyin = Pinyin::heteronymAsList('重庆重庆', ToneStyle::SYMBOL);
$pinyin->toArray();
// [
// ["重" => ["zhòng", "chóng", "tóng"]],
// ["庆" => ["qìng"]],
// ["重" => ["zhòng", "chóng", "tóng"]],
// ["庆" => ["qìng"]]
// ]
```
### 单字转拼音
和多音字类似,单字的返回值为字符串,多音字将根据该字字频调整得到常用音:
```php
$pinyin = Pinyin::chars('重庆');
echo $pinyin['重']; // "zhòng"
echo $pinyin['庆']; // "qìng"
$pinyin->toArray();
// [
// "重": "zhòng",
// "庆": "qìng"
// ]
```
> **Warning**
>
> 当单字处理时由于多音字来自词频表中取得常用音,所以在词语环境下可能出现不正确的情况,建议使用多音字处理。
## 性能优化策略 🚀
v6.0+ 版本提供了三种不同的转换策略,以适应不同的使用场景:
### 1. 内存优化策略(Memory Optimized- 默认
- **内存占用**~400KB
- **适用场景**:Web 请求、内存受限环境
- **特点**:每次加载一个词典段,用完即释放
```php
use Overtrue\Pinyin\Pinyin;
// 使用内存优化策略(默认)
Pinyin::useMemoryOptimized();
$result = Pinyin::sentence('你好世界');
echo $result; // nǐ hǎo shì jiè
```
### 2. 缓存策略(Cached
- **内存占用**~4MB
- **适用场景**:批处理、长时运行进程
- **特点**:缓存所有词典数据,重复转换速度提升 2-3 倍
```php
// 使用缓存策略
Pinyin::useCached();
// 批量处理时性能更好
foreach ($largeDataset as $text) {
$result = Pinyin::sentence($text);
echo $result . "\n";
}
// 清理缓存(可选)
\Overtrue\Pinyin\Converters\CachedConverter::clearCache();
```
### 3. 智能策略(Smart
- **内存占用**600KB-1.5MB
- **适用场景**:通用场景、自动优化
- **特点**:根据文本长度智能选择加载策略
```php
// 使用智能策略
Pinyin::useSmart();
// 短文本自动优化
$result1 = Pinyin::sentence('你好'); // 跳过长词词典
echo $result1; // nǐ hǎo
// 长文本自动调整
$result2 = Pinyin::sentence($longText); // 加载必要的词典
echo $result2;
```
### 自动选择策略
```php
// 根据运行环境自动选择最佳策略
Pinyin::useAutoStrategy();
// 获取推荐策略信息
$recommended = \Overtrue\Pinyin\ConverterFactory::recommend();
echo "推荐策略: {$recommended}";
```
### 直接使用 Converter
```php
use Overtrue\Pinyin\ConverterFactory;
// 创建特定策略的转换器
$converter = ConverterFactory::make('cached');
$result = $converter->convert('你好世界');
echo $result; // nǐ hǎo shì jiè
// 监控内存使用情况
$initialMemory = memory_get_usage();
$converter->convert('测试文本');
$memoryGrowth = memory_get_usage() - $initialMemory;
echo "内存增长: " . round($memoryGrowth / 1024, 2) . " KB";
```
### 性能对比
| 策略 | 内存占用 | 首次转换 | 重复转换 | 推荐场景 |
|-----|---------|---------|---------|---------|
| Memory Optimized | ~400KB | 中等 | 中等 | Web 请求 |
| Cached | ~4MB | 慢 | **最快** | 批处理 |
| Smart | 600KB-1.5MB | 快 | 快 | 通用场景 |
运行基准测试查看实际性能:
```bash
# 运行标准基准测试
php benchmark/run.php
# 详细的策略对比测试
php benchmark/compare-strategies.php
```
## 性能优化最佳实践
### 选择合适的策略
根据您的使用场景选择最合适的转换策略:
#### Web 应用(Laravel、Symfony 等)
```php
// 在应用启动时设置
Pinyin::useMemoryOptimized(); // 默认策略,内存占用最小
// 或在 ServiceProvider 中配置
public function boot()
{
Pinyin::useMemoryOptimized();
}
```
#### CLI 批处理脚本
```php
// 处理大量数据时使用缓存策略
Pinyin::useCached();
$results = [];
foreach ($thousandsOfTexts as $text) {
$results[] = Pinyin::sentence($text);
}
// 处理完成后清理缓存
\Overtrue\Pinyin\Converters\CachedConverter::clearCache();
```
#### 队列任务处理
```php
class ConvertPinyinJob implements ShouldQueue
{
public function handle()
{
// 队列任务中使用智能策略
Pinyin::useSmart();
// 处理任务...
}
}
```
### 性能监控
```php
use Overtrue\Pinyin\ConverterFactory;
// 监控不同策略的内存使用情况
$strategies = ['memory', 'cached', 'smart'];
foreach ($strategies as $strategy) {
$converter = ConverterFactory::make($strategy);
$initialMemory = memory_get_usage();
$converter->convert('测试文本');
$memoryGrowth = memory_get_usage() - $initialMemory;
echo "策略: {$strategy}, 内存增长: " . round($memoryGrowth / 1024, 2) . " KB" . PHP_EOL;
}
```
### 基准测试
项目提供了多个基准测试工具:
```bash
# 运行标准基准测试
php benchmark/run.php
# 详细的策略对比
php benchmark/compare-strategies.php
```
基准测试会显示不同策略的性能对比,包括:
- 内存使用情况
- 转换速度
- 不同文本长度的性能表现
### 内存管理建议
1. **生产环境**:使用 `Memory Optimized` 策略,避免内存泄漏
2. **开发环境**:可以使用 `Smart` 策略,平衡性能和内存
3. **批处理任务**:使用 `Cached` 策略,处理完成后调用 `clearCache()`
4. **内存受限环境**:严格使用 `Memory Optimized` 策略
### 性能对比数据
基于 1000 次转换的基准测试结果:
| 场景 | Memory Optimized | Cached | Smart |
|-----|-----------------|--------|-------|
| 短文本(<10字) | 1.2ms | 0.5ms | 0.8ms |
| 中等文本(10-50字) | 3.5ms | 1.2ms | 2.1ms |
| 长文本(>100字) | 8.7ms | 3.1ms | 5.2ms |
| 内存占用 | 400KB | 4MB | 1.5MB |
> 💡 **提示**:缓存策略在重复转换时性能提升约 2-3 倍,但会占用更多内存。
## v/yu/ü 的问题
根据国家语言文字工作委员会的规定,`lv``lyu``lǚ` 都是正确的,但是 `lv` 是最常用的,所以默认使用 `lv`,如果你需要使用其他的,可以在初始化时传入:
```php
echo Pinyin::sentence('旅行');
// lǚ xíng
echo Pinyin::sentence('旅行', 'none');
// lv xing
echo Pinyin::yuToYu()->sentence('旅行', 'none');
// lyu xing
echo Pinyin::yuToU()->sentence('旅行', 'none');
// lu xing
echo Pinyin::yuToV()->sentence('旅行', 'none');
// lv xing
```
> **Warning**
>
> 仅在拼音风格为非 `symbol` 模式下有效。
## 命令行工具
你可以使用命令行来实现拼音的转换:
```bash
php ./bin/pinyin 带着希望去旅行 --method=sentence --tone-style=symbol
# dài zhe xī wàng qù lǚ xíng
```
更多使用方法,可以查看帮助文档:
```bash
php ./bin/pinyin --help
# Usage:
# ./pinyin [chinese] [method] [options]
# Options:
# -j, --json 输出 JSON 格式.
# -c, --compact 不格式化输出 JSON.
# -m, --method=[method] 转换方式,可选:sentence/fullSentence/name/passportName/phrase/permalink/heteronym/heteronymAsList/chars/abbr/nameAbbr.
# --no-tone 不使用音调.
# --tone-style=[style] 音调风格,可选值:symbol/none/number, default: none.
# -h, --help 显示帮助.
```
### 命令行工具示例
```bash
# 基本转换
php ./bin/pinyin "你好世界"
# ni hao shi jie
# 指定音调风格
php ./bin/pinyin "你好世界" --tone-style=symbol
# nǐ hǎo shì jiè
php ./bin/pinyin "你好世界" --tone-style=number
# ni3 hao3 shi4 jie4
# 生成链接格式
php ./bin/pinyin "带着希望去旅行" --method=permalink
# dai-zhe-xi-wang-qu-lv-xing
# 获取首字母
php ./bin/pinyin "带着希望去旅行" --method=abbr
# d z x w q l x
# 多音字转换(JSON格式)
php ./bin/pinyin "重庆" --method=heteronym --json
# {"重":["zhong","chong","tong"],"庆":["qing"]}
# 姓名转换
php ./bin/pinyin "欧阳修" --method=name
# ou yang xiu
```
## 在 Laravel 中使用
独立的包在这里:[overtrue/laravel-pinyin](https://github.com/overtrue/laravel-pinyin)
## 中文简繁转换
如何你有这个需求,也可以了解我的另一个包:[overtrue/php-opencc](https://github.com/overtrue/php-opencc)
## Contribution
欢迎提意见及完善补充词库:
- 单字拼音错误请添加到:[sources/pathes/chars.txt](https://github.com/overtrue/pinyin/blob/master/sources/pathes/chars.txt)
- 词语错误或补齐,请添加到:[sources/pathes/words.txt](https://github.com/overtrue/pinyin/blob/master/sources/pathes/words.txt)
## 参考
- [mozillazg/pinyin-data](https://github.com/mozillazg/pinyin-data)
- [overtrue/pinyin-resources](https://github.com/overtrue/pinyin-resources)
## :heart: Sponsor me
如果你喜欢我的项目并想支持它,[点击这里 :heart:](https://github.com/sponsors/overtrue)
## PHP 扩展包开发
> 想知道如何从零开始构建 PHP 扩展包?
>
> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package)
# License
MIT
+72
View File
@@ -0,0 +1,72 @@
{
"name": "overtrue/pinyin",
"description": "Chinese to pinyin translator.",
"keywords": [
"chinese",
"pinyin",
"cn2pinyin"
],
"homepage": "https://github.com/overtrue/pinyin",
"license": "MIT",
"authors": [
{
"name": "overtrue",
"homepage": "http://github.com/overtrue",
"email": "anzhengchao@gmail.com"
}
],
"autoload": {
"psr-4": {
"Overtrue\\Pinyin\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Overtrue\\Pinyin\\Tests\\": "tests/"
}
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.5 || ^11.5 || ^12.0",
"brainmaestro/composer-git-hooks": "^3.0",
"nunomaduro/termwind": "^1.0 || ^2.0",
"laravel/pint": "^1.10"
},
"bin": [
"bin/pinyin"
],
"extra": {
"hooks": {
"pre-commit": [
"composer pint",
"composer test"
],
"pre-push": [
"composer pint",
"composer test"
]
}
},
"scripts": {
"post-update-cmd": [
"cghooks update"
],
"post-merge": "composer install",
"post-install-cmd": [
"cghooks remove",
"cghooks add --ignore-lock"
],
"cghooks": "vendor/bin/cghooks",
"pint": "vendor/bin/pint ./src ./tests",
"fix-style": "vendor/bin/pint ./src ./tests",
"test": "vendor/bin/phpunit --colors=always",
"build": "php ./bin/build",
"benchmark": "php ./benchmark/run.php"
},
"scripts-descriptions": {
"test": "Run all tests.",
"fix-style": "Run style checks and fix violations."
}
}
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
<?php
return array (
'万俟' => ' mò qí ',
'尉迟' => ' yù chí ',
'单于' => ' chán yú ',
'重' => ' chóng ',
'秘' => ' bì ',
'冼' => ' xiǎn ',
'华' => ' huà ',
'过' => ' guō ',
'纪' => ' jǐ ',
'燕' => ' yān ',
'种' => ' chóng ',
'繁' => ' pó ',
'幺' => ' yāo ',
'覃' => ' qín ',
'冯' => ' féng ',
'石' => ' shí ',
'缪' => ' miào ',
'瞿' => ' qú ',
'曾' => ' zēng ',
'解' => ' xiè ',
'折' => ' shè ',
'那' => ' nā ',
'佴' => ' nài ',
'难' => ' nàn ',
'粘' => ' niàn ',
'藏' => ' zàng ',
'扎' => ' zā ',
'翟' => ' zhái ',
'都' => ' dū ',
'六' => ' lù ',
'薄' => ' bó ',
'贾' => ' jiǎ ',
'的' => ' dē ',
'哈' => ' hǎ ',
'居' => ' jū ',
'盖' => ' gě ',
'查' => ' zhā ',
'盛' => ' shèng ',
'塔' => ' tǎ ',
'和' => ' hé ',
'柏' => ' bǎi ',
'朴' => ' piáo ',
'蓝' => ' lán ',
'牟' => ' móu ',
'殷' => ' yīn ',
'陆' => ' lù ',
'乜' => ' niè ',
'乐' => ' yuè ',
'阚' => ' kàn ',
'叶' => ' yè ',
'强' => ' qiáng ',
'不' => ' fǒu ',
'丁' => ' dīng ',
'阿' => ' ā ',
'汤' => ' tāng ',
'万' => ' wàn ',
'车' => ' chē ',
'称' => ' chēng ',
'沈' => ' shěn ',
'区' => ' ōu ',
'仇' => ' qiú ',
'宿' => ' sù ',
'南' => ' nán ',
'单' => ' shàn ',
'卜' => ' bǔ ',
'鸟' => ' niǎo ',
'思' => ' sī ',
'殳' => ' shū ',
'寻' => ' xún ',
'於' => ' yú ',
'烟' => ' yān ',
'余' => ' yú ',
'浅' => ' qiǎn ',
'艾' => ' ài ',
'浣' => ' wǎn ',
'无' => ' wú ',
'信' => ' xìn ',
'许' => ' xǔ ',
'齐' => ' qí ',
'俞' => ' yú ',
'若' => ' ruò ',
'贠' => ' yùn ',
'貟' => ' yùn ',
'么' => ' yāo ',
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
# 基准测试指南
## 运行基准测试
```bash
php benchmark/run.php
```
## 输出说明
### Strategy Comparison 表格
这个表格对比了三种转换策略的性能:
```
Method Memory Cached Smart Fastest Speedup
sentence 20.5 ms 8.2 ms 12.3 ms Cached 2.5x
fullSentence 18.3 ms 7.5 ms 11.2 ms Cached 2.4x
...
────────────────────────────────────────────────
TOTAL 190.26ms 119.33ms 194.38ms Cached 1.6x
```
#### 列说明:
- **Method**: 测试的方法名称
- **Memory**: 内存优化策略的执行时间
- **Cached**: 缓存策略的执行时间
- **Smart**: 智能策略的执行时间
- **Fastest**: 该方法最快的策略名称(用颜色高亮)
- **Speedup**: 最慢与最快之间的速度差异倍数
#### TOTAL 行:
- 使用分隔线与数据行区分
- 显示所有方法的总执行时间
- 最快的策略会用对应颜色高亮显示
- Speedup 显示总体的加速比
### Performance Summary
性能总结部分提供了策略之间的直观对比:
```
📊 Performance Summary:
• Cached strategy is 1.59x faster than Memory Optimized
• Smart strategy is 0.98x faster than Memory, 1.63x slower than Cached
```
这让您可以快速了解:
- Cached 策略比 Memory Optimized 快多少倍
- Smart 策略相对于其他两种策略的性能表现
### Memory Usage 表格
显示每种策略的内存使用情况:
```
Strategy Peak Memory Description
Memory Optimized 2.5 MB Minimal memory, loads on demand
Cached 15.8 MB All data cached, fastest repeated access
Smart 8.2 MB Adaptive loading based on text complexity
```
## 如何解读结果
1. **选择合适的策略**
- 如果内存有限(如 Web 请求):选择 Memory Optimized
- 如果需要批量处理大量文本:选择 Cached
- 如果需要平衡性能和内存:选择 Smart
2. **Speedup 值的含义**
- 1.5x = 快 50%
- 2.0x = 快 1 倍(速度是原来的 2 倍)
- 3.0x = 快 2 倍(速度是原来的 3 倍)
3. **TOTAL 行的重要性**
- 这是所有方法执行的总时间
- 最能反映实际使用场景的整体性能
- 用于评估策略的整体效果
+6
View File
@@ -0,0 +1,6 @@
parameters:
level: 9
paths:
- src
inferPrivatePropertyTypeFromConstructor: true
checkMissingIterableValueType: false
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Overtrue\Pinyin;
use ArrayAccess;
use JsonException;
use JsonSerializable;
use Stringable;
use function array_map;
use function implode;
use function is_array;
class Collection implements ArrayAccess, JsonSerializable, Stringable
{
public function __construct(protected $items = []) {}
public function join(string $separator = ' '): string
{
return implode($separator, array_map(
fn ($item) => is_array($item) ? '['.implode(', ', $item).']' : $item,
$this->items
));
}
public function map(callable $callback): Collection
{
return new static(array_map($callback, $this->all()));
}
public function all(): array
{
return $this->items;
}
public function toArray(): array
{
return $this->all();
}
/**
* @throws JsonException
*/
public function toJson(int $options = 0): string
{
return json_encode($this->all(), $options | JSON_THROW_ON_ERROR);
}
public function __toString(): string
{
return $this->join();
}
public function offsetExists(mixed $offset): bool
{
return isset($this->items[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->items[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->items[$offset]);
}
public function jsonSerialize(): array
{
return $this->items;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Overtrue\Pinyin\Contracts;
use Overtrue\Pinyin\Collection;
use Overtrue\Pinyin\ToneStyle;
interface ConverterInterface
{
public function convert(string $string): Collection;
public function heteronym(bool $asList = false): static;
public function surname(): static;
public function noWords(): static;
public function noCleanup(): static;
public function onlyHans(): static;
public function noAlpha(): static;
public function noNumber(): static;
public function noPunctuation(): static;
public function withToneStyle(string|ToneStyle $toneStyle): static;
public function noTone(): static;
public function useNumberTone(): static;
public function yuToV(): static;
public function yuToU(): static;
public function yuToYu(): static;
public function when(bool $condition, callable $callback): static;
}
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace Overtrue\Pinyin;
use InvalidArgumentException;
use Overtrue\Pinyin\Contracts\ConverterInterface;
use Overtrue\Pinyin\Converters\CachedConverter;
use Overtrue\Pinyin\Converters\MemoryOptimizedConverter;
use Overtrue\Pinyin\Converters\SmartConverter;
/**
* Converter 工厂类
*
* 提供不同策略的 Converter 实例
*/
class ConverterFactory
{
public const MEMORY_OPTIMIZED = 'memory';
public const CACHED = 'cached';
public const SMART = 'smart';
/**
* 默认策略
*/
private static string $defaultStrategy = self::MEMORY_OPTIMIZED;
/**
* 创建 Converter 实例
*
* @param string|null $strategy 策略名称,null 则使用默认策略
*/
public static function make(?string $strategy = null): ConverterInterface
{
$strategy = $strategy ?? self::$defaultStrategy;
return match ($strategy) {
self::MEMORY_OPTIMIZED => new MemoryOptimizedConverter,
self::CACHED => new CachedConverter,
self::SMART => new SmartConverter,
default => throw new InvalidArgumentException("Unknown converter strategy: {$strategy}")
};
}
/**
* 设置默认策略
*/
public static function setDefaultStrategy(string $strategy): void
{
if (! in_array($strategy, [self::MEMORY_OPTIMIZED, self::CACHED, self::SMART])) {
throw new InvalidArgumentException("Invalid strategy: {$strategy}");
}
self::$defaultStrategy = $strategy;
}
/**
* 获取当前默认策略
*/
public static function getDefaultStrategy(): string
{
return self::$defaultStrategy;
}
/**
* 根据场景推荐策略
*
* @param array $context 场景上下文
* @return string 推荐的策略
*/
public static function recommend(array $context = []): string
{
// Web 请求场景
if (($context['sapi'] ?? php_sapi_name()) === 'fpm-fcgi') {
return self::MEMORY_OPTIMIZED;
}
// CLI 批处理场景
if (($context['batch'] ?? false) === true) {
return self::CACHED;
}
// 内存限制场景
$memoryLimit = ini_get('memory_limit');
if ($memoryLimit !== '-1' && self::parseBytes($memoryLimit) < 128 * 1024 * 1024) {
return self::MEMORY_OPTIMIZED;
}
// 默认使用智能策略
return self::SMART;
}
/**
* 获取所有可用策略的信息
*/
public static function getStrategiesInfo(): array
{
return [
self::MEMORY_OPTIMIZED => [
'name' => '内存优化',
'class' => MemoryOptimizedConverter::class,
'memory' => '~400KB',
'speed' => '中等',
'use_case' => 'Web请求、内存受限环境',
],
self::CACHED => [
'name' => '全缓存',
'class' => CachedConverter::class,
'memory' => '~4MB',
'speed' => '最快',
'use_case' => '批处理、长时运行进程',
],
self::SMART => [
'name' => '智能',
'class' => SmartConverter::class,
'memory' => '600KB-1.5MB',
'speed' => '快',
'use_case' => '通用场景、自动优化',
],
];
}
private static function parseBytes(string $value): int
{
$value = trim($value);
$last = strtolower($value[-1]);
$value = (int) $value;
return match ($last) {
'g' => $value * 1024 * 1024 * 1024,
'm' => $value * 1024 * 1024,
'k' => $value * 1024,
default => $value,
};
}
}
@@ -0,0 +1,245 @@
<?php
namespace Overtrue\Pinyin\Converters;
use Overtrue\Pinyin\Collection;
use Overtrue\Pinyin\Contracts\ConverterInterface;
use Overtrue\Pinyin\ToneStyle;
use function implode;
use function preg_replace;
use function sprintf;
use function str_contains;
use function str_replace;
abstract class AbstractConverter implements ConverterInterface
{
protected const WORDS_GLOB_PATH = __DIR__.'/../../data/words-*.php';
protected const CHARS_PATH = __DIR__.'/../../data/chars.php';
protected const SURNAMES_PATH = __DIR__.'/../../data/surnames.php';
protected bool $heteronym = false;
protected bool $heteronymAsList = false;
protected bool $asSurname = false;
protected bool $noWords = false;
protected bool $cleanup = true;
protected string $yuTo = 'v';
protected ToneStyle $toneStyle = ToneStyle::SYMBOL;
protected array $regexps = [
'separator' => '\p{Z}',
'mark' => '\p{M}',
'tab' => "\t",
'number' => '0-9',
'alphabet' => 'a-zA-Z',
'hans' => '\x{3007}\x{2E80}-\x{2FFF}\x{3100}-\x{312F}\x{31A0}-\x{31EF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}',
'punctuation' => '\p{P}',
];
private static ?array $wordSegmentPaths = null;
abstract public function convert(string $string): Collection;
public function heteronym(bool $asList = false): static
{
$this->heteronym = true;
$this->heteronymAsList = $asList;
return $this;
}
public function surname(): static
{
$this->asSurname = true;
return $this;
}
public function noWords(): static
{
$this->noWords = true;
return $this;
}
public function noCleanup(): static
{
$this->cleanup = false;
return $this;
}
public function onlyHans(): static
{
$this->regexps['hans'] = '\x{3007}\x{2E80}-\x{2FFF}\x{3100}-\x{312F}\x{31A0}-\x{31EF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{F900}-\x{FAFF}';
unset($this->regexps['alphabet']);
unset($this->regexps['number']);
unset($this->regexps['punctuation']);
return $this;
}
public function noAlpha(): static
{
unset($this->regexps['alphabet']);
return $this;
}
public function noNumber(): static
{
unset($this->regexps['number']);
return $this;
}
public function noPunctuation(): static
{
unset($this->regexps['punctuation']);
return $this;
}
/**
* 设置声调风格
*
* @param string|ToneStyle $toneStyle 声调风格,可以是字符串或 ToneStyle 枚举
*/
public function withToneStyle(string|ToneStyle $toneStyle): static
{
$this->toneStyle = $toneStyle instanceof ToneStyle ? $toneStyle : ToneStyle::from($toneStyle);
return $this;
}
public function noTone(): static
{
$this->toneStyle = ToneStyle::NONE;
return $this;
}
public function useNumberTone(): static
{
$this->toneStyle = ToneStyle::NUMBER;
return $this;
}
public function yuToV(): static
{
$this->yuTo = 'v';
return $this;
}
public function yuToU(): static
{
$this->yuTo = 'u';
return $this;
}
public function yuToYu(): static
{
$this->yuTo = 'yu';
return $this;
}
public function when(bool $condition, callable $callback): static
{
if ($condition) {
$callback($this);
}
return $this;
}
// 共享的辅助方法
protected function preprocessString(string $string): string
{
// 把原有的数字和汉字分离,避免拼音转换时被误作声调
$string = preg_replace_callback('~[a-z0-9_-]+~i', function ($matches) {
return "\t".$matches[0];
}, $string);
// 过滤掉不保留的字符
if ($this->cleanup) {
$string = preg_replace(sprintf('~[^%s]~u', implode($this->regexps)), '', $string);
}
return $string;
}
protected function split(string $item): Collection
{
$items = preg_split('/\s+/u', trim($item), -1, PREG_SPLIT_NO_EMPTY) ?: [];
foreach ($items as $index => $item) {
$items[$index] = $this->formatTone($item, $this->toneStyle->value);
}
return new Collection($items);
}
protected function formatTone(string $pinyin, string $style): string
{
if ($style === ToneStyle::SYMBOL->value) {
return $pinyin;
}
// @formatter:off
$replacements = [
'ɑ' => ['a', 5], 'ü' => ['v', 5], 'üē' => ['ue', 1], 'üé' => ['ue', 2], 'üě' => ['ue', 3], 'üè' => ['ue', 4],
'ā' => ['a', 1], 'ē' => ['e', 1], 'ī' => ['i', 1], 'ō' => ['o', 1], 'ū' => ['u', 1], 'ǖ' => ['v', 1],
'á' => ['a', 2], 'é' => ['e', 2], 'í' => ['i', 2], 'ó' => ['o', 2], 'ú' => ['u', 2], 'ǘ' => ['v', 2],
'ǎ' => ['a', 3], 'ě' => ['e', 3], 'ǐ' => ['i', 3], 'ǒ' => ['o', 3], 'ǔ' => ['u', 3], 'ǚ' => ['v', 3],
'à' => ['a', 4], 'è' => ['e', 4], 'ì' => ['i', 4], 'ò' => ['o', 4], 'ù' => ['u', 4], 'ǜ' => ['v', 4],
];
// @formatter:on
foreach ($replacements as $unicode => $replacement) {
if (str_contains($pinyin, $unicode)) {
$umlaut = $replacement[0];
if ($this->yuTo !== 'v' && $umlaut === 'v') {
$umlaut = $this->yuTo;
}
$pinyin = str_replace($unicode, $umlaut, $pinyin);
if ($style === ToneStyle::NUMBER->value) {
$pinyin .= $replacement[1];
}
}
}
return $pinyin;
}
protected function wordSegmentPaths(): array
{
if (self::$wordSegmentPaths !== null) {
return self::$wordSegmentPaths;
}
$paths = glob(self::WORDS_GLOB_PATH);
if (! is_array($paths)) {
return self::$wordSegmentPaths = [];
}
sort($paths, SORT_NATURAL);
return self::$wordSegmentPaths = $paths;
}
}
@@ -0,0 +1,132 @@
<?php
namespace Overtrue\Pinyin\Converters;
use Overtrue\Pinyin\Collection;
use function array_map;
use function mb_strlen;
use function mb_substr;
use function str_starts_with;
/**
* 缓存版本的转换器
*
* 特点:
* - 缓存所有词典数据
* - 更快的重复转换速度
* - 适合批处理和长时运行的进程
* - 内存占用较高(~4MB
*/
class CachedConverter extends AbstractConverter
{
private static ?array $charsCache = null;
private static ?array $surnamesCache = null;
private static array $wordsCache = [];
private static ?array $fullDictionary = null;
public function convert(string $string): Collection
{
$string = $this->preprocessString($string);
return $this->determineConversionStrategy($string);
}
private function determineConversionStrategy(string $string): Collection
{
// 多音字处理
if ($this->heteronym) {
return $this->convertAsChars($string, true);
}
// 仅字符转换
if ($this->noWords) {
return $this->convertAsChars($string);
}
// 替换姓氏
if ($this->asSurname) {
$string = $this->convertSurname($string);
}
// 使用缓存的完整词典
$dictionary = $this->getFullDictionary();
$string = strtr($string, $dictionary);
return $this->split($string);
}
private function getFullDictionary(): array
{
if (self::$fullDictionary === null) {
self::$fullDictionary = [];
// 按顺序加载,保证长词优先
foreach ($this->wordSegmentPaths() as $path) {
self::$fullDictionary += $this->loadWordsSegment($path);
}
}
return self::$fullDictionary;
}
private function loadWordsSegment(string $path): array
{
if (! isset(self::$wordsCache[$path])) {
self::$wordsCache[$path] = require $path;
}
return self::$wordsCache[$path];
}
protected function convertAsChars(string $string, bool $polyphonic = false): Collection
{
self::$charsCache ??= require self::CHARS_PATH;
$chars = mb_str_split($string);
$items = [];
foreach ($chars as $char) {
if (isset(self::$charsCache[$char])) {
if ($polyphonic) {
$pinyin = array_map(fn ($pinyin) => $this->formatTone($pinyin, $this->toneStyle->value), self::$charsCache[$char]);
if ($this->heteronymAsList) {
$items[] = [$char => $pinyin];
} else {
$items[$char] = $pinyin;
}
} else {
$items[$char] = $this->formatTone(self::$charsCache[$char][0], $this->toneStyle->value);
}
}
}
return new Collection($items);
}
protected function convertSurname(string $name): string
{
self::$surnamesCache ??= require self::SURNAMES_PATH;
foreach (self::$surnamesCache as $surname => $pinyin) {
if (str_starts_with($name, $surname)) {
return $pinyin.mb_substr($name, mb_strlen($surname));
}
}
return $name;
}
/**
* 清理缓存(可选)
*/
public static function clearCache(): void
{
self::$charsCache = null;
self::$surnamesCache = null;
self::$wordsCache = [];
self::$fullDictionary = null;
}
}
@@ -0,0 +1,92 @@
<?php
namespace Overtrue\Pinyin\Converters;
use Overtrue\Pinyin\Collection;
use function array_map;
use function mb_strlen;
use function mb_substr;
use function str_starts_with;
/**
* 内存优化版本的转换器
*
* 特点:
* - 最小内存占用(峰值 ~400KB
* - 每次加载一个段,用完即释放
* - 适合 Web 请求和内存受限环境
*/
class MemoryOptimizedConverter extends AbstractConverter
{
public function convert(string $string): Collection
{
$string = $this->preprocessString($string);
return $this->determineConversionStrategy($string);
}
private function determineConversionStrategy(string $string): Collection
{
// 多音字处理
if ($this->heteronym) {
return $this->convertAsChars($string, true);
}
// 仅字符转换
if ($this->noWords) {
return $this->convertAsChars($string);
}
// 替换姓氏
if ($this->asSurname) {
$string = $this->convertSurname($string);
}
// 按顺序加载词典段(长词优先)
foreach ($this->wordSegmentPaths() as $path) {
$string = strtr($string, require $path);
}
return $this->split($string);
}
protected function convertAsChars(string $string, bool $polyphonic = false): Collection
{
$map = require self::CHARS_PATH;
$chars = mb_str_split($string);
$items = [];
foreach ($chars as $char) {
if (isset($map[$char])) {
if ($polyphonic) {
$pinyin = array_map(fn ($pinyin) => $this->formatTone($pinyin, $this->toneStyle->value), $map[$char]);
if ($this->heteronymAsList) {
$items[] = [$char => $pinyin];
} else {
$items[$char] = $pinyin;
}
} else {
$items[$char] = $this->formatTone($map[$char][0], $this->toneStyle->value);
}
}
}
return new Collection($items);
}
protected function convertSurname(string $name): string
{
static $surnames = null;
$surnames ??= require self::SURNAMES_PATH;
foreach ($surnames as $surname => $pinyin) {
if (str_starts_with($name, $surname)) {
return $pinyin.mb_substr($name, mb_strlen($surname));
}
}
return $name;
}
}
@@ -0,0 +1,148 @@
<?php
namespace Overtrue\Pinyin\Converters;
use Overtrue\Pinyin\Collection;
use function array_map;
use function mb_strlen;
use function mb_substr;
use function str_starts_with;
/**
* 智能版本的转换器
*
* 特点:
* - 根据文本长度智能选择策略
* - 短文本跳过不必要的长词词典
* - 缓存小型常用数据
* - 平衡内存和性能
*/
class SmartConverter extends AbstractConverter
{
private static ?array $surnamesCache = null;
private static array $segmentCache = [];
private static ?array $fullDictionary = null;
private const MAX_CACHE_SEGMENTS = 3;
public function convert(string $string): Collection
{
$string = $this->preprocessString($string);
return $this->determineConversionStrategy($string);
}
private function determineConversionStrategy(string $string): Collection
{
// 多音字处理
if ($this->heteronym) {
return $this->convertAsChars($string, true);
}
// 仅字符转换
if ($this->noWords) {
return $this->convertAsChars($string);
}
// 替换姓氏
if ($this->asSurname) {
$string = $this->convertSurname($string);
}
// 智能加载词典
$string = $this->smartConvert($string);
return $this->split($string);
}
private function smartConvert(string $string): string
{
// 使用和CachedConverter相同的逻辑,但保持缓存机制
$dictionary = $this->getFullDictionary();
return strtr($string, $dictionary);
}
private function getFullDictionary(): array
{
if (self::$fullDictionary === null) {
self::$fullDictionary = [];
// 按顺序加载,保证长词优先
foreach ($this->wordSegmentPaths() as $path) {
self::$fullDictionary += $this->loadSegmentWithCache($path);
}
}
return self::$fullDictionary;
}
private function loadSegmentWithCache(string $path): array
{
if (! isset(self::$segmentCache[$path])) {
// 缓存数量限制
if (count(self::$segmentCache) >= self::MAX_CACHE_SEGMENTS) {
// 移除最早的缓存(简单的 FIFO)
$firstKey = array_key_first(self::$segmentCache);
if ($firstKey !== null) {
unset(self::$segmentCache[$firstKey]);
}
}
self::$segmentCache[$path] = require $path;
}
return self::$segmentCache[$path];
}
protected function convertAsChars(string $string, bool $polyphonic = false): Collection
{
// 字符表太大,不缓存
$map = require self::CHARS_PATH;
$chars = mb_str_split($string);
$items = [];
foreach ($chars as $char) {
if (isset($map[$char])) {
if ($polyphonic) {
$pinyin = array_map(fn ($pinyin) => $this->formatTone($pinyin, $this->toneStyle->value), $map[$char]);
if ($this->heteronymAsList) {
$items[] = [$char => $pinyin];
} else {
$items[$char] = $pinyin;
}
} else {
$items[$char] = $this->formatTone($map[$char][0], $this->toneStyle->value);
}
}
}
return new Collection($items);
}
protected function convertSurname(string $name): string
{
// 姓氏表很小,可以缓存
self::$surnamesCache ??= require self::SURNAMES_PATH;
foreach (self::$surnamesCache as $surname => $pinyin) {
if (str_starts_with($name, $surname)) {
return $pinyin.mb_substr($name, mb_strlen($surname));
}
}
return $name;
}
/**
* 清理缓存
*/
public static function clearCache(): void
{
self::$surnamesCache = null;
self::$segmentCache = [];
self::$fullDictionary = null;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
namespace Overtrue\Pinyin;
use InvalidArgumentException;
use Overtrue\Pinyin\Contracts\ConverterInterface;
use Overtrue\Pinyin\Converters\CachedConverter;
use Overtrue\Pinyin\Converters\SmartConverter;
use function is_numeric;
use function mb_substr;
use function method_exists;
/**
* @method static ConverterInterface surname()
* @method static ConverterInterface noWords()
* @method static ConverterInterface onlyHans()
* @method static ConverterInterface noAlpha()
* @method static ConverterInterface noNumber()
* @method static ConverterInterface noCleanup()
* @method static ConverterInterface noPunctuation()
* @method static ConverterInterface noTone()
* @method static ConverterInterface useNumberTone()
* @method static ConverterInterface yuToV()
* @method static ConverterInterface yuToU()
* @method static ConverterInterface withToneStyle(string|ToneStyle $toneStyle = 'symbol')
* @method static Collection convert(string $string, callable $beforeSplit = null)
*/
class Pinyin
{
/**
* 当前使用的转换策略
*/
private static ?string $converterStrategy = null;
public static function name(string $name, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::converter()->surname()->withToneStyle($toneStyle)->convert($name);
}
public static function passportName(string $name, string|ToneStyle $toneStyle = ToneStyle::NONE): Collection
{
return self::converter()->surname()->yuToYu()->withToneStyle($toneStyle)->convert($name);
}
public static function phrase(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::converter()->noPunctuation()->withToneStyle($toneStyle)->convert($string);
}
public static function sentence(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::converter()->withToneStyle($toneStyle)->convert($string);
}
public static function fullSentence(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::converter()->noCleanup()->withToneStyle($toneStyle)->convert($string);
}
public static function heteronym(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL, bool $asList = false): Collection
{
return self::converter()->heteronym($asList)->withToneStyle($toneStyle)->convert($string);
}
public static function heteronymAsList(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::heteronym($string, $toneStyle, true);
}
public static function chars(string $string, string|ToneStyle $toneStyle = ToneStyle::SYMBOL): Collection
{
return self::converter()->onlyHans()->noWords()->withToneStyle($toneStyle)->convert($string);
}
public static function permalink(string $string, string $delimiter = '-'): string
{
if (! in_array($delimiter, ['_', '-', '.', ''], true)) {
throw new InvalidArgumentException("Delimiter must be one of: '_', '-', '', '.'.");
}
return self::converter()->noPunctuation()->noTone()->convert($string)->join($delimiter);
}
public static function nameAbbr(string $string): Collection
{
return self::abbr($string, true);
}
public static function abbr(string $string, bool $asName = false, bool $preserveEnglishWords = false): Collection
{
return self::converter()->noTone()
->noPunctuation()
->when($asName, fn ($c) => $c->surname())
->convert($string)
->map(function ($pinyin) use ($string, $preserveEnglishWords) {
// 如果内容在原字符串中,则直接返回
if ($preserveEnglishWords && str_contains($string, $pinyin)) {
return $pinyin;
}
// 常用于电影名称入库索引处理,例如:《晚娘2012》-> WN2012
return is_numeric($pinyin) || preg_match('/\d{2,}/', $pinyin) ? $pinyin : mb_substr($pinyin, 0, 1);
});
}
/**
* 获取 Converter 实例
*
* @param string|null $strategy 指定策略,null 则使用默认策略
*/
public static function converter(?string $strategy = null): ConverterInterface
{
// 使用新的工厂模式
$strategy = $strategy ?? self::$converterStrategy;
return ConverterFactory::make($strategy);
}
/**
* 设置默认转换策略
*
* @param string $strategy 策略名称
*/
public static function setConverterStrategy(string $strategy): void
{
self::$converterStrategy = $strategy;
ConverterFactory::setDefaultStrategy($strategy);
}
/**
* 使用内存优化策略
*/
public static function useMemoryOptimized(): void
{
self::setConverterStrategy(ConverterFactory::MEMORY_OPTIMIZED);
}
/**
* 使用缓存策略
*/
public static function useCached(): void
{
self::setConverterStrategy(ConverterFactory::CACHED);
}
/**
* 使用智能策略
*/
public static function useSmart(): void
{
self::setConverterStrategy(ConverterFactory::SMART);
}
/**
* 根据运行环境自动选择策略
*/
public static function useAutoStrategy(): void
{
$strategy = ConverterFactory::recommend();
self::setConverterStrategy($strategy);
}
/**
* 清理所有转换器的缓存
*/
public static function clearCache(): void
{
CachedConverter::clearCache();
SmartConverter::clearCache();
}
public static function __callStatic(string $name, array $arguments)
{
$converter = self::converter();
if (method_exists($converter, $name)) {
return $converter->$name(...$arguments);
}
throw new InvalidArgumentException("Method {$name} does not exist.");
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Overtrue\Pinyin;
/**
* 拼音声调风格枚举
*/
enum ToneStyle: string
{
/**
* 符号风格:zhōng
*/
case SYMBOL = 'symbol';
/**
* 数字风格:zhong1
*/
case NUMBER = 'number';
/**
* 无声调:zhong
*/
case NONE = 'none';
}