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
@@ -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;
}
}