- BrainTools - https://www.braintools.ru -
Это статья — пример небольшого личного опыта [1], где я пытался решить одну чисто техническую задачу для одного из моих текущих проектов. Задача в конце‑концов была решена, насколько правильно — не знаю, но надеюсь многим будет интересен и полезен мой опыт. Итак, небольшая драма в 5 актах.
Итак, недавно в одном из проектов над которым я работаю и где ядро написано на PHP возникла одна тривиальная некая задача. Если не вдаваться в детали самого проекта (вам будет неинтересно), то суть её можно описать следующим: на вход подаётся текст, а на выход нужно выдать NER.
Для тех, кто не знает — NER (Named Entity Recognition) — это задача из области NLP (Natural Language Processing) — на Хабре можно найти пару довольно подробных статей на этот счёт (например: тут [2], тут [3] и ещё много). Её суть в том, чтобы находить в тексте всякие сущности (имена людей, компании, города, даты и т. д.) и определять их тип.
Простой пример:
Apple открыла новый офис в N-ске в 2023 году.
NER-модель разметит это примерно так:
Apple → ORG (организация)
N-ске → LOC (место)
2023 году → DATE (дата)
То есть NER помогает превратить обычный текст в структурированные данные, с которыми уже можно работать в коде: строить аналитические отчёты, автоматизировать поиск информации или даже ловить фишинг в письмах, в общем — кому что.
Ну и, конечно, хочется, чтобы «Apple» определялось как компания, а не фрукт, и «N‑ске» — как город, а не что‑то ещё.
Мой PHP-бэкэнд — отличное место для обработки форм, SQL‑запросов и довольно сложной бизнес логики. Но когда я попытался понять, что в PHP есть сегодня для NLP, то возникло ощущение, будто пришёл на рок‑концерт с блокфлейтой.
В Python для этого всё готово: HuggingFace Transformers [4], Torch [5] и т.д. и т.п.
А в PHP… ну, вариантов немного, и каждый со своими проблемами.
И тут я задумался: «А как это вообще сделать в PHP?»
В результате недолгого исследования начала вырисовываться картина.
Ниже варианты, которые я нашёл для PHP.
Самый очевидный путь. Берёшь готовый API — OpenAI, HuggingFace Inference API, Rasa, watson‑nlp — и дергаешь его из PHP.
Плюсы: просто, быстро, почти без настройки и почти бесплатно на старте.
Минусы: нужен интернет, надо платить за токены, скорость иногда подводит. А ещё душит жаба платить за каждый токен, если у тебя поток текста на десятки тысяч строк в день (хотя, конечно можно выбрать модели подешевле).
Делаешь микросервис на Python (например, вместе с spaCy). Да, именно так. Ставишь себе микросервис, который гоняет spaCy или какую-нибудь модель, и PHP просто бьёт туда запросами. По сути, превращаешь PHP в «тонкого клиента», а всю магию перекладываешь на соседний контейнер.
Плюсы: мощно, гибко, можно ставить любые модели.
Минусы: приходится поддерживать два стека — Composer и pip, потенциально конфликт [6] версий, плюс лишний DevOps.
Есть энтузиасты, которые попытались втащить использование NLP моделей в PHP.
Плюсы: не нужен второй язык.
Минусы: проекты часто заброшены, документация слабая, модели не всегда самые новые, ограниченная поддержка языков (часто только английский или парочка других европейских).
Пример с mitie-php [7]:
$model = new MitieNER('ner_model.dat');
$doc = $model->doc('Nat works at GitHub in San Francisco');
$doc->entities();
Вывод будет что-то вроде:
[
['text' => 'Nat', 'tag' => 'PERSON', 'score' => 0.31123712, 'offset' => 0],
['text' => 'GitHub', 'tag' => 'ORGANIZATION', 'score' => 0.56601151, 'offset' => 13],
['text' => 'San Francisco', 'tag' => 'LOCATION', 'score' => 1.38905243, 'offset' => 23]
]
ONNX — это формат, в котором можно запускать модели без «тяжёлого» Python‑стека.
Есть расширения и для PHP через C++‑библиотеки.
Плюсы: работает быстрее, чем тянуть целый Python, нет нужды в Torch.
Минусы: мало примеров, надо руками возиться с конвертацией модели и сборкой расширений.
Пример с transformers-php [8]:
require 'vendor/autoload.php';
use CodewithkyrianTransformersTransformers;
$pipeline = Transformers::pipeline('token-classification', 'Xenova/bert-base-NER');
// Perform NER on a sentence
$result = $pipeline->run("Apple opened a new office in N-sk.");
print_r($result);
Вывод будет что-то вроде:
Array
(
[0] => Array
(
[entity] => ORG
[word] => Apple
)
[1] => Array
(
[entity] => LOC
[word] => N-sk
)
)
Теоретически можно написать свой regex-based NER.
Если у вас ограниченный домен — например, нужно только города и компании — можно сделать словари и паттерны.
Работает на удивление хорошо, но при первом же «Мета» вместо Facebook ты вспоминаешь, что живёшь в 2025, а не в 2005.
Можно попытать напрямую вызывать Python прямо из PHP
Не микросервис, не API — а реально запустить скрипт Python из PHP-процесса.
С помощью exec() или shell_exec() можно дернуть команду:
$input = "Apple opened a new office in N-sk.";
$escaped = escapeshellarg($input);
$result = shell_exec("python3 ner.py $escaped");
$entities = json_decode($result, true);
var_dump($entities);
А в ner.py какой-нибудь простой код на spaCy:
import sys, json, spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp(sys.argv[1])
entities = [{"text": e.text, "label": e.label_} for e in doc.ents]
print(json.dumps(entities))
Это не суперэффективно (каждый вызов поднимает интерпретатор Python), но для небольших задач — может быть вполне сносным решением. Особенно если не хочется городить инфраструктуру ради пары запросов в час.
Ну и, наконец, можно использовать библиотеку RubixML [9], чтобы создать и обучить свою собственную модель. Если есть время и желание, чтобы собрать свой датасет (желательно побольше), разметить токены и построить пайплайн признаков — то у этого «академического» подхода тоже есть право на жизнь.
Поигравшись с разными опциями я остановился на варианте «Python рядом».
В результате у меня бежит отдельный контейнер со spaCy, куда я обращаюсь из контейнера с PHP‑FPM.
Ниже приведена примерная структура проекта:
app/
docker/
spacy/
- app.py
- Dockerfile
- requirements.txt
docker-compose.yml
Пример app.py
from fastapi import FastAPI, Request
import spacy
from transformers import pipeline
import torch
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
app = FastAPI()
# Select device: prefer CUDA, then MPS, else CPU
if torch.cuda.is_available():
device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
device = "mps"
else:
device = "cpu"
logger.info(f"Using device: {device}")
# Ask spaCy to use GPU if available and supported (requires cupy/cuda extras)
try:
if device == "cuda":
spacy.prefer_gpu()
logger.info("spaCy: prefer_gpu() called")
except Exception as e:
logger.warning(f"spaCy GPU preference failed or unavailable: {e}")
# Loading spaCy models for EN and RU
models_spacy = {
'en': spacy.load('en_core_web_md'),
'ru': spacy.load('ru_core_news_md')
}
def clean_entity_text(text: str) -> str:
"""Clean entity text by removing any unwanted characters.
Args:
text: The text to clean
Returns:
Cleaned text, or empty string if text should be filtered out
"""
# Return empty string for specific values
if text in ("#", "0 &&!", "https://www", "//"):
return ""
# Clean up and return
return text.strip("#➡ ").strip()
@app.post("/annotate")
async def annotate(request: Request):
data = await request.json()
text = data.get('text', '')
lang = data.get('lang', 'en')
if lang in models_spacy:
nlp = models_spacy[lang]
doc = nlp(text)
annotations = [
{"text": clean_entity_text(ent.text), "label": ent.label_}
for ent in doc.ents
]
else:
annotations = []
return {"annotations": annotations}
Пример Dockerfile
ARG BASE_IMAGE=python:3.10-slim
FROM ${BASE_IMAGE}
# Control torch install for CPU vs CUDA base
ARG INSTALL_TORCH=true
ARG TORCH_INDEX_URL=https://download.pytorch.org/whl/cpu
WORKDIR /app
# Copy requirements first for better caching
COPY requirements.txt .
ENV PIP_NO_CACHE_DIR=1
RUN pip install --no-cache-dir -r requirements.txt --upgrade
&& if [ "$INSTALL_TORCH" = "true" ]; then
pip install --no-cache-dir --index-url ${TORCH_INDEX_URL} torch;
fi
# Download spaCy models for English and Russian
RUN python -m spacy download en_core_web_md
RUN python -m spacy download ru_core_news_md
# Copy application code
COPY app.py .
EXPOSE 8001
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8001"]
Пример requirements.txt
fastapi
uvicorn[standard]
spacy
transformers
Пример с docker-compose.local.yml
app:
build:
context: .
dockerfile: docker/app/Dockerfile
depends_on:
- db
- redis
volumes:
- ./app:/var/www
ports:
- "9000:9000"
networks:
- my-network
env_file:
- ./app/.env
spacy:
build:
context: ./docker/spacy
dockerfile: Dockerfile
restart: always
volumes:
- ./docker/spacy:/app
ports:
- "8001:8001"
networks:
- my-network
Пример вызова из PHP
private const SPACY_URL = 'http://spacy:8001';
/**
* Annotate text
* @param string $text
* @param string $lang
* @return mixed
*/
public static function annotateText(string $text, string $lang = 'en'): mixed
{
$data = json_encode(['text' => $text, 'lang' => $lang]);
$ch = curl_init(self::SPACY_URL . '/annotate');
if ($ch === false) {
echo 'Failed to initialize curl';
return null;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $data,
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Curl error: ' . curl_error($ch);
}
curl_close($ch);
if (is_string($response)) {
return json_decode($response, true);
}
return null;
}
Тест: обработка 1000 предложений (~20k токенов).
Условия: средний сервер (8 vCPU, 16 GB RAM).
Оценка — ориентировочная, основана на общих замерах из документации и личном опыте.
|
Вариант |
Среднее время на 1000 предложений |
Задержка (latency) на один запрос |
Затраты CPU/RAM |
Масштабируемость |
|---|---|---|---|---|
|
Внешние API (OpenAI) |
40–90 сек (зависит от сети и тарифа) |
300–800 мс (иногда скачет до секунд) |
CPU/RAM на клиенте почти нет |
Масштаб по токенам и тарифам провайдера |
|
Python-сервис (spaCy) |
8–12 сек |
30–50 мс |
Высокая загрузка CPU, RAM ~2–4 GB на модель |
Горизонтально (несколько контейнеров) |
|
PHP-библиотеки (mitie-php) |
25–40 сек |
80–120 мс |
CPU средне, RAM до 1 GB |
Ограничено – редко оптимизировано под многопоточность |
|
ONNX через PHP-расширение |
6–10 сек |
20–40 мс |
RAM ~1–2 GB, CPU умеренно |
Хорошо масштабируется, но нужна ручная сборка |
|
Regex + словари |
<1 сек |
<1 мс |
Незначительно |
Бесконечно, но только в узком домене |
Самый быстрый (при грамотной настройке) — ONNX, но дорог в интеграции.
Самый стабильный и гибкий — Python‑сервис (баланс скорости и поддержки).
Самый непредсказуемый — внешние API (скорость зависит от сети и тарифа).
Regex бьёт всех по скорости, но совершенно бесполезен для сложных сценариев.
Итого, после экспериментов с API, PHP-библиотеками, ONNX и regex я остановился на варианте с отдельным Python-сервисом (spaCy). Для продакшена это оказалось самым устойчивым решением: оно масштабируемо, понятно в поддержке и позволяет обновлять модели независимо от PHP-бэкенда.
Гибкость: позволяет легко менять модели и языки без переписывания PHP‑кода (сегодня spaCy, а завтра что угодно — хоть Paraphrase).
Изоляция: NLP‑стек вынесен в отдельный контейнер, PHP остаётся «тонким клиентом».
Масштабирование: сервис можно запустить в нескольких экземплярах за балансировщиком.
Обновления: для обновления модели достаточно собрать новый Docker‑образ — PHP‑часть не трогаем.
Прозрачность: логи, мониторинг, метрики можно настроить отдельно, не перегружая PHP‑приложение.
Если вам нужно встроить NER (или вообще NLP) в проект на PHP, самый надёжный и предсказуемый путь сегодня — соседний Python‑сервис в Docker. Да, это требует чуть больше DevOps‑усилий, но зато вы получаете реальную мощь Python‑экосистемы и не зависите от состояния случайных PHP‑библиотек.
Вот такой опыт. Если вы тоже мучились с NER на PHP — расскажите, что выбрали. Может, кто‑то уже придумал элегантное решение, о котором мы все мечтаем.
*Meta Platforms Inc. (Facebook, Instagram) — признана экстремистской организацией, ее деятельность запрещена на территории России.
Автор: samako
Источник [10]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/19624
URLs in this post:
[1] опыта: http://www.braintools.ru/article/6952
[2] тут: https://habr.com/ru/articles/921698/
[3] тут: https://habr.com/ru/articles/826820/
[4] HuggingFace Transformers: https://huggingface.co/
[5] Torch: https://pytorch.org/
[6] конфликт: http://www.braintools.ru/article/7708
[7] mitie-php: https://github.com/ankane/mitie-php
[8] transformers-php: https://github.com/CodeWithKyrian/transformers-php
[9] RubixML: https://github.com/RubixML
[10] Источник: https://habr.com/ru/articles/948014/?utm_campaign=948014&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.