更新
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Overtrue\Pinyin;
|
||||
|
||||
/**
|
||||
* 拼音声调风格枚举
|
||||
*/
|
||||
enum ToneStyle: string
|
||||
{
|
||||
/**
|
||||
* 符号风格:zhōng
|
||||
*/
|
||||
case SYMBOL = 'symbol';
|
||||
|
||||
/**
|
||||
* 数字风格:zhong1
|
||||
*/
|
||||
case NUMBER = 'number';
|
||||
|
||||
/**
|
||||
* 无声调:zhong
|
||||
*/
|
||||
case NONE = 'none';
|
||||
}
|
||||
Reference in New Issue
Block a user