Участник: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 Состояние и развитие
Обсуждение
|