From d4224de2e19af384f62293ad7b30eaab74358b29 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 4 Apr 2026 13:43:17 +1000 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .drone.yml | 30 +++++++++++++++ Dockerfile | 17 +++++++++ main.py | 101 +++++++++++++++++++++++++++++++------------------ work_parser.py | 66 ++++++++++++++++++++++++++------ 4 files changed, 166 insertions(+), 48 deletions(-) create mode 100644 .drone.yml create mode 100644 Dockerfile diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..ca3ded5 --- /dev/null +++ b/.drone.yml @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0dc68ec --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/main.py b/main.py index dac1c75..502b8d5 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,8 @@ import requests import settings_work as sw import work_parser as wp +DOCUMENTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "documents") + @asynccontextmanager 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") 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() @@ -208,12 +205,21 @@ def extract_text_from_url(url, timeout=10, verify=True): # Общий запрос на GPT 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: - contentGPT = Promts[0].prompt.replace('{content}', content) + url_ist = "http://epaper.hljnews.cn/hljrb/pc/layout" 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 params = {'text': contentGPT} @@ -230,26 +236,18 @@ def gpt_response_message(content, ist_number=1): logger.info(f"gpt_response_message: {ex}") 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 при неудачных попытках запроса -def restart_service(service_name): - try: - subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True) - time.sleep(30) - print(f"Сервис {service_name} успешно перезапущен") - except subprocess.CalledProcessError: - print(f"Не удалось перезапустить сервес {service_name}") +# def restart_service(service_name): +# try: +# subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True) +# time.sleep(30) +# print(f"Сервис {service_name} успешно перезапущен") +# except subprocess.CalledProcessError: +# 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)) path_day = article_date.split()[0] - if not os.path.exists(path_day): - os.makedirs(path_day) - print(f"Создана папка: {path_day}") + documents_path = os.path.join(DOCUMENTS_DIR, path_day) + if not os.path.exists(documents_path): + os.makedirs(documents_path) + print(f"Создана папка: {documents_path}") doc = Document() 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_paragraph(original_text) 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) print(f"Сохранен документ: {doc_path}") except Exception as ex: @@ -397,12 +396,10 @@ def start_pars_two_istochnik(): # Функции для автоматического запуска def scheduled_parser_1(): - """Планировщик для первого парсера""" istochnik = "" # пустая строка = текущая дата start_pars_one_istochnik(istochnik.split(".")) def scheduled_parser_2(): - """Планировщик для второго парсера""" start_pars_two_istochnik() class ParserOneRequest(BaseModel): @@ -427,21 +424,51 @@ def get_tasks_offset(limit: int = Query(10, gt=0), offset: int = Query(0, ge=0)) # GET метод для получения настроек @app.get("/settings", summary="Метод получения настроек парсера") def get_settings(): - return sw.read_settings() + return wp.get_all_promt() # POST метод для установки настроек @app.post("/settings", summary="Метод сохранения настроек парсера") -def set_settings(settings: sw.Source): - return sw.update_source(settings) +def set_settings(settings: wp.Source): + return wp.update_promt(settings) @app.delete("/delete_task/{task_id}", summary="Метод удаления задачи") def delete_task(task_id: int): 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="Метод для скачивания файла") 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') + file_name = f"{title}.docx" + 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__": # uvicorn.run("main:app", port=8001, reload=True) diff --git a/work_parser.py b/work_parser.py index 896757c..8baa4bb 100644 --- a/work_parser.py +++ b/work_parser.py @@ -1,5 +1,6 @@ import psycopg2 from psycopg2.extras import RealDictCursor +from pydantic import BaseModel, HttpUrl # Подключение к БД (укажи свои параметры) 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) 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__": -# create_table() - -# task_id = insert_task(status='queued', source_url='http://example.com', priority=5) -# print("Создана задача с id:", task_id) - -# 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) + # create_table_config_gpt() # <-- раскомментировать эту строку + # update_promt({ + # "url": "http://epaper.hljnews.cn/hljrb/pc/layout", + # "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\": \"<краткая суть новости (1–2 предложения)>\",\n \"category\": \"<названий категорий, которым соответствует статья>\"\n }\n Если статья не относится ни к одной теме или не привязана к нужным регионам — вернуть:\n {\"translation_text\": \"\", \"short_text\": \"\", \"title\": \"\", \"category\": \"\"}" + # }) \ No newline at end of file