Со временем в материалах на моём сайте под управлением Joomla накопился "мусор" — старые теги вида {jd_file file==4}, оставшиеся от какого-то плагина для загрузки файлов. Они больше не работали и только портили вид контента. Вручную чистить сотни статей было не вариант, поэтому я решил написать скрипт на Python, чтобы автоматизировать процесс. Вот как это было.

Проблема

В текстах материалов (в полях introtext и fulltext) встречались конструкции вроде:

  • {jd_file file==4}
  • {jd_file file==123}
  • {jd_file file==7}

Эти теги были частью старой системы, которая уже не используется, и их нужно было просто удалить из всех материалов, чтобы привести контент в порядок.

Решение

Я написал небольшой скрипт на Python, который подключается к базе данных Joomla, ищет эти теги с помощью регулярных выражений и убирает их. Вот как это выглядело шаг за шагом.

Шаг 1: Подключение к базе данных

Для начала я настроил подключение к базе Joomla через pymysql. Параметры подключения хранятся в файле .env:

import pymysql
import os
import re
from dotenv import load_dotenv
import logging

load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s')

jedig_db_config = {
    'host': os.getenv('JEDIG_MYSQL_HOST'),
    'user': os.getenv('JEDIG_MYSQL_USER'),
    'password': os.getenv('JEDIG_MYSQL_PASSWORD'),
    'db': os.getenv('JEDIG_MYSQL_DATABASE'),
    'charset': 'utf8mb4',
    'cursorclass': pymysql.cursors.DictCursor
}

jedig_table_prefix = 'dyutb_'

Префикс dyutb_ соответствует моей базе на сайте jedig.ru.

Шаг 2: Определение паттерна

Чтобы найти все варианты тегов {jd_file file==<цифра>}, я использовал регулярное выражение:

jd_file_pattern = re.compile(r'\{jd_file file==\d+\}')

Здесь \d+ означает одну или более цифр, так что паттерн ловит, например, {jd_file file==4} или {jd_file file==123}.

Шаг 3: Основная функция

Вот код, который выполняет очистку:

def remove_jd_file_tags():
    jedig_connection = pymysql.connect(**jedig_db_config)
    try:
        with jedig_connection.cursor() as jedig_cursor:
            jedig_cursor.execute("""
                SELECT `id`, `title`, `introtext`, `fulltext`
                FROM `dyutb_content`
            """)
            items = jedig_cursor.fetchall()

            for item in items:
                updated = False
                item_id = item['id']
                title = item['title']

                introtext = item['introtext'] or ""
                if introtext and jd_file_pattern.search(introtext):
                    updated_introtext = jd_file_pattern.sub('', introtext)
                    jedig_cursor.execute("""
                        UPDATE `dyutb_content`
                        SET `introtext` = %s
                        WHERE `id` = %s
                    """, (updated_introtext, item_id))
                    updated = True
                    logging.info(f"Удалены теги из introtext материала ID: {item_id}, Заголовок: {title}")

                fulltext = item['fulltext'] or ""
                if fulltext and jd_file_pattern.search(fulltext):
                    updated_fulltext = jd_file_pattern.sub('', fulltext)
                    jedig_cursor.execute("""
                        UPDATE `dyutb_content`
                        SET `fulltext` = %s
                        WHERE `id` = %s
                    """, (updated_fulltext, item_id))
                    updated = True
                    logging.info(f"Удалены теги из fulltext материала ID: {item_id}, Заголовок: {title}")

                if updated:
                    logging.info(f"Материал ID: {item_id} обновлён.")

            jedig_connection.commit()
            logging.info("Очистка завершена.")
    except Exception as e:
        logging.error(f"Ошибка: {e}")
    finally:
        jedig_connection.close()

remove_jd_file_tags()

Что делает этот код:

  • Выбирает все материалы из таблицы dyutb_content.
  • Для каждого материала проверяет introtext и fulltext на наличие тегов.
  • Если теги найдены, заменяет их на пустую строку и обновляет запись в базе.
  • Логирует каждое изменение и фиксирует результат.

Шаг 4: Запуск

Я сохранил скрипт в файл clean_jd_file.py и запустил его:

python clean_jd_file.py

Логи показали, где были изменения, например:

2025-03-11 12:34:56,123:INFO:Удалены теги из fulltext материала ID: 45, Заголовок: Land Rover Maintenance
2025-03-11 12:34:56,456:INFO:Очистка завершена.

Результат

После запуска скрипта все теги {jd_file file==<цифра>} исчезли из материалов. Например, если раньше было:

Текст статьи {jd_file file==4} и ещё немного текста.

Теперь стало:

Текст статьи и ещё немного текста.

Контент стал чище и аккуратнее, без старого "мусора".

Вывод

Python снова выручил меня, когда нужно было быстро привести в порядок старые материалы. Регулярные выражения и прямой доступ к базе данных Joomla сделали задачу простой и эффективной. Если в будущем появится другой "мусор", я просто скорректирую паттерн в скрипте и повторю процесс.

Полный код:

import pymysql
import os
import re
from dotenv import load_dotenv
import logging

load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s')

jedig_db_config = {
    'host': os.getenv('JEDIG_MYSQL_HOST'),
    'user': os.getenv('JEDIG_MYSQL_USER'),
    'password': os.getenv('JEDIG_MYSQL_PASSWORD'),
    'db': os.getenv('JEDIG_MYSQL_DATABASE'),
    'charset': 'utf8mb4',
    'cursorclass': pymysql.cursors.DictCursor
}

jedig_table_prefix = 'dyutb_'

# Регулярное выражение для поиска конструкций {jd_file file==<цифра>}
jd_file_pattern = re.compile(r'\{jd_file file==\d+\}')

def remove_jd_file_tags():
    jedig_connection = None
    try:
        jedig_connection = pymysql.connect(**jedig_db_config)
        with jedig_connection.cursor() as jedig_cursor:
            # Выбираем все материалы с полями introtext и fulltext
            jedig_cursor.execute(f"""
                SELECT `id`, `title`, `introtext`, `fulltext`
                FROM `{jedig_table_prefix}content`
            """)
            items = jedig_cursor.fetchall()

            for item in items:
                updated = False
                item_id = item['id']
                title = item['title']

                # Обработка introtext
                introtext = item['introtext'] or ""
                if introtext and jd_file_pattern.search(introtext):
                    updated_introtext = jd_file_pattern.sub('', introtext)
                    jedig_cursor.execute(f"""
                        UPDATE `{jedig_table_prefix}content`
                        SET `introtext` = %s
                        WHERE `id` = %s
                    """, (updated_introtext, item_id))
                    updated = True
                    logging.info(f"Удалены конструкции jd_file из introtext материала ID: {item_id}, Заголовок: {title}")

                # Обработка fulltext
                fulltext = item['fulltext'] or ""
                if fulltext and jd_file_pattern.search(fulltext):
                    updated_fulltext = jd_file_pattern.sub('', fulltext)
                    jedig_cursor.execute(f"""
                        UPDATE `{jedig_table_prefix}content`
                        SET `fulltext` = %s
                        WHERE `id` = %s
                    """, (updated_fulltext, item_id))
                    updated = True
                    logging.info(f"Удалены конструкции jd_file из fulltext материала ID: {item_id}, Заголовок: {title}")

                if updated:
                    logging.info(f"Материал ID: {item_id} обновлён.")

            jedig_connection.commit()
            logging.info("Обработка завершена.")

    except Exception as e:
        logging.error(f"Ошибка при выполнении скрипта: {e}")
    finally:
        if jedig_connection:
            jedig_connection.close()

if __name__ == "__main__":
    remove_jd_file_tags()