Участник:BsivkoBot/Контрольный список

Описание

Мотивация

Практика показывает, что имеется ряд общих случаев обработки параметров шаблонов, которые неудобно делать с помощью более общих средств, таких как AWB, или регулярных выражений, применяемых для текстов статей. Обработка становится более точной и формализованной может стать тогда, когда абстракция выборки смещается на уровень указания конкретных шаблонов и их параметров, и далее на нём описываются простые действия. Например, может быть описано преобразование «если у шаблонов А и Б параметр В имеет значение Г, то параметр Д удалить, а параметр Е если пуст, то записать в него Ж».

Подобных задач может быть множество, и они далее описываются подобными правилами и заносятся в контрольный список, описывающий действущие преобразования. Они могут решать множество задач. Например, если участники для шаблона «статья» не указывают язык журнала Edge, и мы знаем, что он английский, то мы можем для всех таких случаев проставить «язык=en» в случае, если он не указан. Аанлогично, для этих случаев можно указать в ссылке издание этого журнала Future Publishing. Так, если элемент контрольного списка встречается часто, и имеются недостатки редактирования, то созданное правило позволяет уйти от муторной работы и исправить допущенные ошибки. Помимо этого случая, может производиться викификация, удаление неиспользуемых параметров и др.

Реализация

В задаче контрольного списка боту задается конфигурация, определяющая правила обработки. Все правила записываются в одном json-файле в определённом формате. Бот перебирает все шаблоны текста статьи и в случае выполнения условий правила выполняет предназначенные действия.

Задачи описываются тремя секциями:

  • имя_шаблона — одно или список имён шаблонов, попадающих под данную задачу;
  • условие — условие применения правила; если оно выполняется, для одного из заданных в имя_шаблона шаблонов, то выполняется последующее действие;
  • действия — описание действий, применяемых в данной задаче.

Каждая из задач независима от другой и выполняется отдельно.

Бот поддерживает оформление окончаний значений параметров. Для имени рассматриваемого шаблона определяется окончание (например, наличие перевода строки) и далее оно дописывается в конец изменяемых значений во время обработки.

Действующий контрольный список.

В общем случае можно формировать отдельные списки для отдельных задач.

Примеры конфигураций

Установить неуказанный язык по изданию

{
    "задачи": [
        {
            "имя_шаблона": "статья",
            "условие": {
                "параметр": "издание",
                "равен_одному_из": "Electronic Gaming Monthly"
            },
            "действия": [
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en"
                }
            ]
        }
    ]
}

Задача боту: для всех шаблонов «статья» если в них указан параметр «издание» и он равен «Electronic Gaming Monthly», то если параметр «язык» пуст или отсутствует, то записать в него значение «en».

Установить неуказанный язык с заменой других

При записи значения параметра можно указать, что некоторые значения будут игнорироваться для записи. Например, если язык имеет значение «mis» (т.е. неизвестен), то боту разрешется записать вместо него новое значение:

{
    "задачи": [
        {
            "имя_шаблона": "статья",
            "условие": {
                "параметр": "издание",
                "равен_одному_из": "Electronic Gaming Monthly"
            },
            "действия": [
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": "mis"
                }
            ]
        }
    ]
}

Обработка по нескольким значениям параметров

Для некоторых полей можно указывать список значений, и они будут обрабатываться как «или» или «все». Т.е. например:

{
    "задачи": [
        {
            "имя_шаблона": ["статья", "книга"],
            "условие": {
                "параметр": "издание",
                "равен_одному_из": ["Electronic Gaming Monthly", "[[Electronic Gaming Monthly]]"]
            },
            "действия": [
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": ["mis", "en-US", "en-GB"]
                }
            ]
        }
    ]
}

здесь обрабатываются шаблоны «статья» и «книга». Для них проверяется на равенство одного из двух элементов названия издания, и если одно из них равно, то активируется действие. Во время действия игнорироваться будут все три прописанных значения.

Обработка по нескольким значениям параметров

Если есть уверенность, что при поиске подстроки можно сделать действие, и содержание параметра может быть разнообразным, то срабатывание условия может задаваться через поиск подстроки:

{
    "задачи": [
        {
            "имя_шаблона": ["статья", "книга"],
            "условие": {
                "параметр": "издание",
                "содержит_одно_из": "Electronic Gaming Monthly"
            },
            "действия": [
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": ["mis", "en-US", "en-GB"]
                }
            ]
        }
    ]
}

Т.е. здесь уже будут обрабатываться все шаблоны, где в издании присутствует подстрока (часть строки целиком содержится в строке).

Безусловная запись поверх

В некоторых случаях требуется в любом случае осуществлять запись значения параметра. Так например может быть выполнена викификация ссылок на журнал:

{
    "задачи": [
        {
            "имя_шаблона": ["статья", "книга"],
            "условие": {
                "параметр": "издание",
                "равен_одному_из": ["Crash", "CRASH", "[[CRASH]]", "''[[CRASH]]''"]
            },
            "действия": [
                {
                    "параметр": "издание",
                    "записать_значение_поверх": "[[Crash (журнал)|Crash]]"
                }
            ]
        }
    ]
}

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

Удаление значений параметров

Если требуется удалить параметр по условию, то это может быть описано так:

{
    "задачи": [
        {
            "имя_шаблона": "cite web",
            "действия": [
                {
                    "параметр": "deadurl",
                    "удалить": true
                }
            ]
        }
    ]
}

То есть, для всех (так как условие отсутствует) шаблонов «cite web» будет удаляться параметр «deadurl», если он присутствует.

Исходный код

import json

import mwparserfromhell
import pywikibot

from text_processing.space_chars import get_end_space_chars
from wiki_template_processing.endings_format import detect_format_endings, set_format_ending

TASK_DESCRIPTION_CHECKLIST = u"контрольный список"


def get_checklist():

    pagename = "Участник:BsivkoBot/Контрольный список/Конфигурация"

    site = pywikibot.Site()
    page = pywikibot.Page(site, pagename)
    text = str(page.text)
    return json.loads(text)


def get_single_or_list_as_list(value):
    if isinstance(value, list):
        return value

    if value is None:
        return value

    return [value]


def check_condition(template, condition):

    if condition.get("параметр") is None:
        return False

    param = condition.get("параметр")

    result = False

    if condition.get("равен_одному_из") is not None:
        equal_values = get_single_or_list_as_list(condition.get("равен_одному_из"))
        for equal_value in equal_values:
            if template.has(param):
                param_value = str(template.get(param).value).strip()
                if param_value == equal_value:
                    result = True
                    break

    if not result and condition.get("содержит_одно_из") is not None:
        substr_values = get_single_or_list_as_list(condition.get("содержит_одно_из"))
        for substr_value in substr_values:
            if template.has(param):
                param_value = str(template.get(param).value).strip()
                if substr_value in param_value:
                    result = True
                    break

    return result


def do_action(template, action):

    changed = False

    if action.get("параметр") is not None:
        param = action.get("параметр")
        new_value = action.get("записать_значение_если_пуст")
        if new_value is not None:
            # есть что писать
            if not template.has(param):
                # значения нет
                template.add(param, new_value)
                changed = True
            else:
                current_value = str(template.get(param).value).strip()
                new_value = new_value.strip()
                if current_value == "":
                    template.get(param).value = new_value
                    changed = True
                else:
                    # что-то есть
                    ignores = get_single_or_list_as_list(action.get("игнорировать_значения"))
                    if ignores is not None:
                        if current_value in ignores:
                            if current_value != new_value:
                                template.get(param).value = new_value
                                changed = True

        new_value = action.get("записать_значение_поверх")
        if new_value is not None:
            # есть что писать
            if not template.has(param):
                # значения нет
                template.add(param, new_value)
                changed = True
            else:
                current_value = str(template.get(param).value).strip()
                if current_value != new_value:
                    template.get(param).value = new_value
                    changed = True

        new_value = action.get("удалить")
        if new_value is not None:
            if new_value:
                if template.has(param):
                    template.remove(param)
                    changed = True

    return changed


def perform_tasks(text, tasks, title):

    parsed = mwparserfromhell.parse(text)

    for template in parsed.ifilter_templates():

        for task in tasks:
            if task.get("имя_шаблона") is not None:
                names = get_single_or_list_as_list(task.get("имя_шаблона"))
                for name in names:
                    if template.name.matches(name):
                        need_to_process = True #< по умолчанию для "условие" - если его нет
                        condition = task.get("условие")
                        if condition is not None:
                            need_to_process = check_condition(template, condition)

                        if not need_to_process:
                            continue

                        # условие выполнено
                        actions = task.get("действия")
                        ending = detect_format_endings(template)
                        changed = False
                        for action in actions:
                            changed |= do_action(template, action)
                        if changed:
                            set_format_ending(template, ending)

    output = str(parsed)

    return output


def checklist_for_article(text, cfg, title="unknown"):

    if cfg.get("задачи") is not None:
        tasks = cfg["задачи"]

        return perform_tasks(text, tasks, title)

    return text

Тестирование

import mwparserfromhell

from checklist_template.checklist import check_condition, do_action, checklist_for_article, get_checklist

cfg = {
    "задачи": [
        {
            "имя_шаблона": "статья",
            "условие" : {
                "параметр": "заглавие",
                "равен_одному_из": "Crash",
            },
            "действия": [
                {
                    "параметр": "издательство",
                    "записать_значение_если_пуст": "XXX"
                },
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": "mis",
                },
            ]
        },
        {
            "имя_шаблона": ["статья", "книга"],
            "условие": {
                "параметр": "издание",
                "содержит_одно_из": "Electronic Gaming Monthly",
            },
            "действия": [
                {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": ["mis"],
                }
            ]
        }
    ]
}


def get_template(text, title):
    parsed = mwparserfromhell.parse(text)

    for template in parsed.ifilter_templates():
        if template.name.matches(title):
            return template

    return None


def test_check_condition_equal():
    template = get_template("{{статья|заглавие=Crash|год=2000}}", "статья")

    assert check_condition(template,
                           {
                               "параметр": "заглавие",
                               "равен_одному_из": "Crash",
                           })

    assert check_condition(template,
                           {
                               "параметр": "год",
                               "равен_одному_из": "2000",
                           })

    assert not check_condition(template,
                               {
                                   "параметр": "год",
                                   "равен_одному_из": "2001",
                               })

    assert not check_condition(template,
                               {
                                   "параметр": "статья",
                                   "равен_одному_из": " ",
                               })


def test_check_condition_substr():
    template = get_template("{{статья|заглавие=Crash|год=2000}}", "статья")

    assert check_condition(template,
                           {
                               "параметр": "заглавие",
                               "содержит_одно_из": "Crash",
                           })

    assert check_condition(template,
                           {
                               "параметр": "год",
                               "содержит_одно_из": "2000",
                           })

    assert not check_condition(template,
                               {
                                   "параметр": "год",
                                   "содержит_одно_из": "2001",
                               })

    assert not check_condition(template,
                               {
                                   "параметр": "статья",
                                   "содержит_одно_из": " ",
                               })

    assert check_condition(template,
                           {
                               "параметр": "заглавие",
                               "содержит_одно_из": "ras",
                           })

    assert check_condition(template,
                           {
                               "параметр": "год",
                               "содержит_одно_из": "000",
                           })

    assert not check_condition(template,
                           {
                               "параметр": "год",
                               "содержит_одно_из": ["a", "год"]
                           })

    assert check_condition(template,
                           {
                               "параметр": "год",
                               "содержит_одно_из": ["год", "000"],
                           })


def test_action():
    template = get_template("{{статья|заглавие=Crash|год=2000}}", "статья")

    result = do_action(template, {
        "параметр": "год",
        "записать_значение_если_пуст": "2001"
    })

    assert not result
    assert str(template.get("год").value) == "2000"
    assert str(template.get("заглавие").value) == "Crash"

    result = do_action(template, {
        "параметр": "издательство",
        "записать_значение_если_пуст": "Асвета"
    })

    assert result
    assert str(template.get("год").value) == "2000"
    assert str(template.get("заглавие").value) == "Crash"
    assert str(template.get("издательство").value) == "Асвета"

    result = do_action(template, {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "en",
                    "игнорировать_значения": ["mis", "und"]
                })

    assert result
    assert str(template.get("год").value) == "2000"
    assert str(template.get("заглавие").value) == "Crash"
    assert str(template.get("издательство").value) == "Асвета"
    assert str(template.get("язык").value) == "en"

    result = do_action(template, {
                    "параметр": "язык",
                    "записать_значение_если_пуст": "ru",
                    "игнорировать_значения": ["mis", "und"]
                })

    assert not result
    assert str(template.get("язык").value) == "en"

    result = do_action(template, {
        "параметр": "язык",
        "записать_значение_если_пуст": "ru",
        "игнорировать_значения": ["en"]
    })

    assert result
    assert str(template.get("язык").value) == "ru"

    result = do_action(template, {
        "параметр": "язык",
        "записать_значение_если_пуст": "mis",
        "игнорировать_значения": ["mis", "ru", "und"]
    })

    assert result
    assert str(template.get("язык").value) == "mis"


def test_action_overwrite():
    template = get_template("{{статья|заглавие=Crash|год=2000}}", "статья")

    result = do_action(template, {
        "параметр": "год",
        "записать_значение_поверх": "2001"
    })

    assert result
    assert str(template.get("год").value) == "2001"
    assert str(template.get("заглавие").value) == "Crash"


def test_action_delete():
    template = get_template("{{статья|заглавие=Crash|год=2000}}", "статья")

    result = do_action(template, {
        "параметр": "год",
        "удалить": True
    })

    assert result
    assert not template.has("год")
    assert str(template.get("заглавие").value) == "Crash"

    result = do_action(template, {
        "параметр": "год",
        "удалить": False
    })

    assert not result
    assert not template.has("год")
    assert str(template.get("заглавие").value) == "Crash"


def test_checklist_article():

    text = checklist_for_article("{{статья|заглавие=Crash|год=2000}}", cfg)
    assert "издательство=XXX" in text

    text = checklist_for_article("{{статья|издание=[[Electronic Gaming Monthly]]|заглавие=Klonoa: Empire of Dreams|номер=146|страницы=148|язык=mis|автор=EGM Staff|месяц=9|год=2001}}", cfg)
    assert "язык=en" in text

    # ending
    text = checklist_for_article("{{статья|заглавие=Crash\n|год=2000}}", cfg)
    assert "издательство=XXX\n" in text


def test_load_cfg():
    cfg = get_checklist()
    assert cfg is not None
    assert cfg["задачи"] is not None

    text = checklist_for_article("{{статья\n|заглавие=QWERTY|издание=[[The Astrophysical Journal]]}}", cfg)
    assert "язык=en" in text

    text = checklist_for_article("{{статья\n| автор    = Игорь Асанов\n | заглавие = Left 4 Dead 2\n | издание  = [[Игромания (журнал)|Игромания]]\n | год      = 2010\n | номер    = 1 (148)\n | страницы = 78—83}}", cfg)
    assert "\n\n" not in text

    text = checklist_for_article("{{статья\n| автор    = Александр Башкиров\n| заглавие = Natural Selection 2\n| ссылка   = http://www.igromania.ru/articles/141067/Natural_Selection_2.htm\n| издание  = [[Игромания (журнал)|Игромания]]\n| год      = 2011\n| номер    = 3 (162)\n| страницы = 48-50\n}}\n", cfg)
    assert "\n\n" not in text

Состояние и развитие

  • Первые запуски. Bsivko (обс.) 19:54, 24 мая 2019 (UTC)
  • Исправлена ошибка перевода строк. Введена работа не с mis, а с 'und' в т.ч. на тестах. Bsivko (обс.) 23:26, 22 июня 2019 (UTC)
  • Введено условие проверки внесения изменений. Ранее в некоторых случаях если производилась замена, но содержимое не менялось, то проиводилась запись (с изменением формата, в т.ч. окончаний значений параметров). Сейчас изменение окончаний происходит только если были функциональные правки в значениях шаблона. Bsivko (обс.) 20:53, 23 июня 2019 (UTC)

Обсуждение

  • Бот ошибочно изменил (см.) |издание={{Нп3|Journal of Bacteriology}} на |издание={{Нп3|American Society for Microbiology}} — Эта реплика добавлена участником Grumbler eburg (ов)
    • Спасибо что заметили! Исправил ошибку в контрольном списке. Bsivko (обс.) 00:34, 28 июля 2019 (UTC)
    • Перебрал все правки бота по этому издательству и исправил. Bsivko (обс.) 01:17, 28 июля 2019 (UTC)