перевод
Some checks failed
continuous-integration/drone Build is failing

This commit is contained in:
2026-04-04 13:43:17 +10:00
parent 60224501ab
commit d4224de2e1
4 changed files with 166 additions and 48 deletions

30
.drone.yml Normal file
View File

@@ -0,0 +1,30 @@
kind: pipeline
type: docker
name: default
steps:
- name: build
image: python:3.11-slim
commands:
- pip install --no-cache-dir -r requirements.txt
- name: deploy
image: appleboy/drone-ssh
settings:
host: allowlgroup.ru
username:
from_secret: ssh_username
password:
from_secret: ssh_password
port: 22
script:
- docker stop parser || true
- docker rm parser || true
- docker pull gitea.allowlgroup.ru/allowlgroup/parser:latest
- docker run -d --name parser -p 8001:8001 -v /opt/parser_data:/app/documents gitea.allowlgroup.ru/allowlgroup/parser:latest
when:
branch:
- main
event:
- push
- custom

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8001
# Запуск с uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

99
main.py
View File

@@ -26,6 +26,8 @@ import requests
import settings_work as sw import settings_work as sw
import work_parser as wp import work_parser as wp
DOCUMENTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "documents")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения""" """Управление жизненным циклом приложения"""
@@ -49,11 +51,6 @@ scheduler = AsyncIOScheduler()
logging.basicConfig(filename="app.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logging.basicConfig(filename="app.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@app.get("/logs")
def get_logs():
with open("app.log", "r") as file:
lines = file.readlines()[-10:] # последние 10 строк
return {"logs": lines}
# Инициализация таблицы статуса парсинга # Инициализация таблицы статуса парсинга
wp.create_table() wp.create_table()
@@ -208,12 +205,21 @@ def extract_text_from_url(url, timeout=10, verify=True):
# Общий запрос на GPT # Общий запрос на GPT
def gpt_response_message(content, ist_number=1): def gpt_response_message(content, ist_number=1):
Promts = sw.read_settings().sources # Promts = sw.read_settings().sources
# if ist_number == 1:
# contentGPT = Promts[0].prompt.replace('{content}', content)
# else:
# contentGPT = Promts[1].prompt.replace('{content}', content)
if ist_number == 1: if ist_number == 1:
contentGPT = Promts[0].prompt.replace('{content}', content) url_ist = "http://epaper.hljnews.cn/hljrb/pc/layout"
else: else:
contentGPT = Promts[1].prompt.replace('{content}', content) url_ist = "https://def.ltn.com.tw/breakingnewslist"
contentGPT = wp.get_promt(url_ist).replace('{content}', content)
url = 'http://45.129.78.228:8484' #10.8.0.14:5500 url = 'http://45.129.78.228:8484' #10.8.0.14:5500
params = {'text': contentGPT} params = {'text': contentGPT}
@@ -230,26 +236,18 @@ def gpt_response_message(content, ist_number=1):
logger.info(f"gpt_response_message: {ex}") logger.info(f"gpt_response_message: {ex}")
retries += 1 retries += 1
else:
restart_service('work_gpt.service')
print(f"\n\n\tПерезапуск GPT\n\n")
try:
response = requests.get(url, params=params, timeout=15)
return response.text
except Exception as ex:
print(f"Ошибка при запросе к GPT: {ex}")
logger.info(f"gpt_response_message: {ex}")
retries += 1 logger.info(f"Привышен лимит запросов {max_retries}")
return ""
# перезапуск сервиса GPT при неудачных попытках запроса # перезапуск сервиса GPT при неудачных попытках запроса
def restart_service(service_name): # def restart_service(service_name):
try: # try:
subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True) # subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True)
time.sleep(30) # time.sleep(30)
print(f"Сервис {service_name} успешно перезапущен") # print(f"Сервис {service_name} успешно перезапущен")
except subprocess.CalledProcessError: # except subprocess.CalledProcessError:
print(f"Не удалось перезапустить сервес {service_name}") # print(f"Не удалось перезапустить сервес {service_name}")
# Общие функции проверки ссылок # Общие функции проверки ссылок
@@ -296,9 +294,10 @@ def update_bd_and_create_document(response_text, article_date, url, parsed_at, o
print(requests.post('http://45.129.78.228:8002/save_parsed_data', json=data)) print(requests.post('http://45.129.78.228:8002/save_parsed_data', json=data))
path_day = article_date.split()[0] path_day = article_date.split()[0]
if not os.path.exists(path_day): documents_path = os.path.join(DOCUMENTS_DIR, path_day)
os.makedirs(path_day) if not os.path.exists(documents_path):
print(f"Создана папка: {path_day}") os.makedirs(documents_path)
print(f"Создана папка: {documents_path}")
doc = Document() doc = Document()
doc.add_heading('Ссылка на статью', level=1) doc.add_heading('Ссылка на статью', level=1)
@@ -316,7 +315,7 @@ def update_bd_and_create_document(response_text, article_date, url, parsed_at, o
doc.add_heading('Оригинальный текст', level=1) doc.add_heading('Оригинальный текст', level=1)
doc.add_paragraph(original_text) doc.add_paragraph(original_text)
doc_name = f"{data['title']}.docx" doc_name = f"{data['title']}.docx"
doc_path = os.path.join(path_day, doc_name) doc_path = os.path.join(documents_path, doc_name)
doc.save(doc_path) doc.save(doc_path)
print(f"Сохранен документ: {doc_path}") print(f"Сохранен документ: {doc_path}")
except Exception as ex: except Exception as ex:
@@ -397,12 +396,10 @@ def start_pars_two_istochnik():
# Функции для автоматического запуска # Функции для автоматического запуска
def scheduled_parser_1(): def scheduled_parser_1():
"""Планировщик для первого парсера"""
istochnik = "" # пустая строка = текущая дата istochnik = "" # пустая строка = текущая дата
start_pars_one_istochnik(istochnik.split(".")) start_pars_one_istochnik(istochnik.split("."))
def scheduled_parser_2(): def scheduled_parser_2():
"""Планировщик для второго парсера"""
start_pars_two_istochnik() start_pars_two_istochnik()
class ParserOneRequest(BaseModel): class ParserOneRequest(BaseModel):
@@ -427,21 +424,51 @@ def get_tasks_offset(limit: int = Query(10, gt=0), offset: int = Query(0, ge=0))
# GET метод для получения настроек # GET метод для получения настроек
@app.get("/settings", summary="Метод получения настроек парсера") @app.get("/settings", summary="Метод получения настроек парсера")
def get_settings(): def get_settings():
return sw.read_settings() return wp.get_all_promt()
# POST метод для установки настроек # POST метод для установки настроек
@app.post("/settings", summary="Метод сохранения настроек парсера") @app.post("/settings", summary="Метод сохранения настроек парсера")
def set_settings(settings: sw.Source): def set_settings(settings: wp.Source):
return sw.update_source(settings) return wp.update_promt(settings)
@app.delete("/delete_task/{task_id}", summary="Метод удаления задачи") @app.delete("/delete_task/{task_id}", summary="Метод удаления задачи")
def delete_task(task_id: int): def delete_task(task_id: int):
return print(wp.delete_task(task_id)) return print(wp.delete_task(task_id))
# @app.get("/file_download", summary="Метод для скачивания файла")
# async def download_file(path: str, title: str):
# path = f"./{path}/{title}.docx" #os.path.abspath(path)
# return FileResponse(path=path, filename=f'{title}.docx', media_type='multipart/form-data')
@app.get("/file_download", summary="Метод для скачивания файла") @app.get("/file_download", summary="Метод для скачивания файла")
async def download_file(path: str, title: str): async def download_file(path: str, title: str):
path = f"./{path}/{title}.docx" #os.path.abspath(path) file_name = f"{title}.docx"
return FileResponse(path=path, filename=f'{title}.docx', media_type='multipart/form-data') file_path = os.path.join(DOCUMENTS_DIR, path, file_name)
logger.warning(f"Файл: {file_path}")
# Проверяем существование файла
if not os.path.exists(file_path):
logger.warning(f"Файл не найден: {file_path}")
return {"error": "Файл не найден", "path": file_path}
# Возвращаем файл
response = FileResponse(
path=file_path,
filename=file_name,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
logger.warning(response)
return response
@app.get("/logs")
def get_logs():
with open("app.log", "r") as file:
lines = file.readlines()[-10:] # последние 10 строк
return {"logs": lines}
# if __name__ == "__main__": # if __name__ == "__main__":
# uvicorn.run("main:app", port=8001, reload=True) # uvicorn.run("main:app", port=8001, reload=True)

View File

@@ -1,5 +1,6 @@
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from pydantic import BaseModel, HttpUrl
# Подключение к БД (укажи свои параметры) # Подключение к БД (укажи свои параметры)
conn = psycopg2.connect( conn = psycopg2.connect(
@@ -75,16 +76,59 @@ def update_task(task_id, **fields):
cur.execute(f"UPDATE work_parser SET {set_sql} WHERE id = %s;", values) cur.execute(f"UPDATE work_parser SET {set_sql} WHERE id = %s;", values)
return True return True
def create_table_config_gpt():
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS config_gpt (
url TEXT PRIMARY KEY,
name VARCHAR(20),
promt TEXT
);
""")
print("Таблица config_gpt создана или уже существует")
class Source (BaseModel):
url: HttpUrl
name: str
promt: str
def update_promt(data: Source):
if isinstance(data, dict):
data = Source.model_validate(data)
with conn.cursor() as cur:
cur.execute("""
INSERT INTO config_gpt (url, name, promt)
VALUES (%s, %s, %s)
ON CONFLICT (url) DO UPDATE SET
name = EXCLUDED.name,
promt = EXCLUDED.promt
""", (str(data.url), data.name, data.promt))
conn.commit()
def get_promt(url):
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT promt FROM config_gpt WHERE url = %s", (url,))
promt = cur.fetchone()
return promt['promt']
def get_all_promt():
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT * FROM config_gpt")
rows = cur.fetchall()
sources = [{"url": row["url"], "name": row["name"], "promt": row["promt"]} for row in rows]
return {"sources": sources}
# Пример использования # Пример использования
# if __name__ == "__main__": # if __name__ == "__main__":
# create_table() # create_table_config_gpt() # <-- раскомментировать эту строку
# update_promt({
# task_id = insert_task(status='queued', source_url='http://example.com', priority=5) # "url": "http://epaper.hljnews.cn/hljrb/pc/layout",
# print("Создана задача с id:", task_id) # "name": "source1",
# "promt": "Задача: Перевод на русский язык и тематическая фильтрация новостных статей из китайской прессы. \n Необходимо переводить текст статьи и определять, относится ли она к КНР по указанным темам: \n 1. Перевод\n Переведи предоставленный китайский текст на русский язык, сохранив оригинальный смысл, стиль и структуру.\n Текст:\n {content}\n -------------------------------------\n 2. Отбирай исключительно новости, прямо относящиеся к Китаю, его безопасности, соседним странам и территориям, влияющим на интересы Китая.\n Если не относится к Китаю — считаем, что статья НЕ подходит, и отдаем пустой JSON:\n {\"text\": \"\", \"pereskas\": \"\", \"title\": \"\", \"topics\": []}\n Если привязка есть — переходи к шагу 3. \n -------------------------------------\n 3. Тематическая классификация\n Определи, относится ли статья к одной или нескольким темам из списка:\n 1) Военные новости — конфликты, учения, мобилизация, закупки вооружений. \n 2) Пограничная деятельность — охрана границы, пограничные учения, строительство или модернизация пограничной инфраструктуры, техника для пограничников. \n 3) Пункты пропуска на границе с РФ — изменения режима работы, строительство, реконструкция, оборудование, логистика. \n 4) Пограничные реки — состояние рек, экология, инфраструктурные проекты, мониторинг. \n 5) Чрезвычайные ситуации — природные и техногенные происшествия, особенно затрагивающие пограничные реки и прилегающие земли. \n 6) Санитарно-эпидемиологическая обстановка — эпидемии, эпизоотии, эпифитотии, угрозы и меры предотвращения. \n 7) Индустриальные проекты (арктическое/антарктическое направление). \n 8) Индустриальные проекты в приграничных районах — заводы, производства, технопарки, новые технологии. \n 9) Инфраструктурные проекты в приграничных районах — дороги, мосты, транспорт, логистика. \n 10) Культура малочисленных народностей (нанайцы, монголы, уйгуры, нанайцы и хэчжэ) — политика, традиции, бытовая жизнь нанайцев, монголов, уйгуров, и хэчжэ (малочисленных народов).\n\n Отметь только те темы, которым статья действительно соответствует.\n\n -------------------------------------\n 4. Формат ответа \n Вернуть строго JSON без пояснений и дополнительных слов:\n {\n \"translation_text\": \"<перевод текста статьи на русский язык (дословный, точный и без сокращений ) >\",\n \"short_text\": \"<пересказ переведённого текста>\",\n \"title\": \"<краткая суть новости (12 предложения)>\",\n \"category\": \"<названий категорий, которым соответствует статья>\"\n }\n Если статья не относится ни к одной теме или не привязана к нужным регионам — вернуть:\n {\"translation_text\": \"\", \"short_text\": \"\", \"title\": \"\", \"category\": \"\"}"
# task = get_task(task_id) # })
# print("Получена задача:", task)
# update_task(task_id, status='in_progress', started_at=datetime.utcnow(), attempts=1)
# task = get_task(task_id)
# print("Обновленная задача:", task)