El propósito de este pequeño manual es ilustrar cómo se crea un bot personalizado para realizar una tarea en una wiki usando las librerías de programación Pywikibot en el lenguaje de programación Python.
- Requisitos
- Tener instaladas y configuradas las librerías de pywikipedia.
- Poseer conocimientos básicos de programación en Python y expresiones regulares.
Como ejemplo, crearemos un bot que realice la siguiente tarea (ref.):
- Reemplazar la plantilla {{edad}} en los artículos de personas fallecidas.
- (La edad ya no cambiará, es innecesario y desaconsejable usar una plantilla que hace el cálculo en vez de indicar directamente el número de años).
Un primer acercamiento es usar los bots predefinidos que incluye pywikipedia.
Intentamos usar template.py para «substituir» la plantilla. Sin embargo el resultado no es adecuado pues el código queda ilegible y de todas formas aparece la función que realiza el cálculo en vez del número.
Otra alternativa que considerar es usar replace.py para cambiar la plantilla por un número, pero este camino se descarta, puesto que no es posible realizar operaciones aritméticas en el reemplazo (calcular la edad de defunción y ponerla).
Dado que necesito hacer un bot especial para el caso, aprovecho para preparar este manual que ilustra cómo se elabora un bot que haga una tarea no contemplada por los bots predefinidos.
Preliminares
El esquema general de todos los bots es el siguiente:
- Se indica un conjunto de páginas en las cuales trabajar.
- Por ejemplo: Todos los artículos que contengan la plantilla
{{Ficha de cuerpo de agua}}
, o todos los artículos en la categoría Categoría:Inventores de Alemania, o los que contengan la palabra «Petersburgo». - En cada artículo del conjunto indicado, se determina si procede realizar un cambio.
- En el ejemplo en cuestión, determinar si aparece la plantilla
{{edad}}
y de ser así, calcular la edad correcta. - Se efectúan los cambios y se graba el artículo
- Repetir y enjuagar.
Cuando yo era joven, uno tenía que caminar dos kilómetros en la nieve para comprar refacciones y hacer un bot. Sin embargo, los niños de hoy en día lo tienen más fácil, porque existe ya un esqueleto de bot que se puede usar como punto de partida.
Dicho esqueleto se llama basic.py, en el directorio de pywikipedia. Ahora, antes de proceder con nuestro propio bot, explicaremos las principales partes del código del bot básico.
basic.py
La primera sección consiste en las declaraciones generales.
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (C) Pywikipedia bot team, 2006-2010
#
# Distributed under the terms of the MIT license.
#
__version__ = '$Id: basic.py 8807 2010-12-27 21:34:11Z purodha $'
#
import wikipedia as pywikibot
import pagegenerators
# This is required for the text that is shown when you run this script
# with the parameter -help.
docuReplacements = {
'¶ms;': pagegenerators.parameterHelp
}
Las dos líneas import de arriba se encargan de proporcionarnos los elementos básicos, en particular los generadores (funciones que nos permiten localizar los artículos sobre los cuales trabajar).
class BasicBot:
# Edit summary message that should be used.
# NOTE: Put a good description here, and add translations, if possible!
msg = {
'ar': u'روبوت: تغيير ...',
'cs': u'Robot změnil ...',
'de': u'Bot: Ändere ...',
'en': u'Robot: Changing ...',
'fa': u'ربات: تغییر ...',
'fr': u'Robot: Changé ...',
'ja':u'ロボットによる:編集',
'ksh': u'Bot: Änderung ...',
'nds': u'Bot: Ännern ...',
'nl': u'Bot: wijziging ...',
'pl': u'Bot: zmienia ...',
'pt': u'Bot: alterando...',
'ru': u'Бот: изменил ...',
'sv': u'Bot: Ändrar ...',
'uk': u'Бот: змінив ...',
'zh': u'機器人:編輯.....',
}
def __init__(self, generator, dry):
"""
Constructor. Parameters:
@param generator: The page generator that determines on which pages
to work.
@type generator: generator.
@param dry: If True, doesn't do any real changes, but only shows
what would have been changed.
@type dry: boolean.
"""
self.generator = generator
self.dry = dry
# Set the edit summary message
self.summary = pywikibot.translate(pywikibot.getSite(), self.msg)
Las líneas de arriba preparan el robot; fíjate en la sintaxis para «msg =», que almacena los resúmenes de edición en cada idioma.
A continuación se listan las funciones que componen el bot. El esquema de ejecución es el siguiente:
- Primero se ejecuta la función main().
- La función main() realiza varios ajustes; dependiendo de los parámetros,
- usa el generador para crear la lista de artículos sobre los cuales operar...
- inicializa el robot (ejecutando BasicBot() )...
- y finalmente ejecuta la función run().
- La función run() itera sobre cada página de la lista de artículos,
- y usa la función treat() para hacer los cambios en esa página.
Función run() recorriendo la lista de páginas obtenidas y ejecutando treat() sobre cada una de ellas.
def run(self):
for page in self.generator:
self.treat(page)
Los cambios por realizar en cada página. Esta es la función central. En este ejemplo, únicamente añade la palabra "Text" al inicio del artículo.
def treat(self, page):
"""
Loads the given page, does some changes, and saves it.
"""
text = self.load(page)
if not text:
return
################################################################
# NOTE: Here you can modify the text in whatever way you want. #
################################################################
# If you find out that you do not want to edit this page, just return.
# Example: This puts the text 'Test' at the beginning of the page.
text = 'Test ' + text
if not self.save(text, page, self.summary):
pywikibot.output(u'Page %s not saved.' % page.title(asLink=True))
La función anterior primero carga la página y obtiene el texto de la misma con la instrucción text = self.load(page). Después modifica el valor de la variable text y usa la función self.save para guardar los cambios.
Las siguientes dos funciones son las que se encargarán de leer y guardar el contenido de los artículos.
def load(self, page):
"""
Loads the given page, does some changes, and saves it.
"""
try:
# Load the page
text = page.get()
except pywikibot.NoPage:
pywikibot.output(u"Page %s does not exist; skipping."
% page.title(asLink=True))
except pywikibot.IsRedirectPage:
pywikibot.output(u"Page %s is a redirect; skipping."
% page.title(asLink=True))
else:
return text
return None
def save(self, text, page, comment, minorEdit=True, botflag=True):
# only save if something was changed
if text != page.get():
# Show the title of the page we're working on.
# Highlight the title in purple.
pywikibot.output(u"\n\n>>> \03{lightpurple}%s\03{default} <<<"
% page.title())
# show what was changed
pywikibot.showDiff(page.get(), text)
pywikibot.output(u'Comment: %s' %comment)
if not self.dry:
choice = pywikibot.inputChoice(
u'Do you want to accept these changes?',
['Yes', 'No'], ['y', 'N'], 'N')
if choice == 'y':
try:
# Save the page
page.put(text, comment=comment,
minorEdit=minorEdit, botflag=botflag)
except pywikibot.LockedPage:
pywikibot.output(u"Page %s is locked; skipping."
% page.title(asLink=True))
except pywikibot.EditConflict:
pywikibot.output(
u'Skipping %s because of edit conflict'
% (page.title()))
except pywikibot.SpamfilterError, error:
pywikibot.output(
u'Cannot change %s because of spam blacklist entry %s'
% (page.title(), error.url))
else:
return True
return False
Finalmente, la función principal que dará inicio a todo el proceso descrito antes:
def main():
# This factory is responsible for processing command line arguments
# that are also used by other scripts and that determine on which pages
# to work on.
genFactory = pagegenerators.GeneratorFactory()
# The generator gives the pages that should be worked upon.
gen = None
# This temporary array is used to read the page title if one single
# page to work on is specified by the arguments.
pageTitleParts = []
# If dry is True, doesn't do any real changes, but only show
# what would have been changed.
dry = False
# Parse command line arguments
for arg in pywikibot.handleArgs():
if arg.startswith("-dry"):
dry = True
else:
# check if a standard argument like
# -start:XYZ or -ref:Asdf was given.
if not genFactory.handleArg(arg):
pageTitleParts.append(arg)
if pageTitleParts != []:
# We will only work on a single page.
pageTitle = ' '.join(pageTitleParts)
page = pywikibot.Page(pywikibot.getSite(), pageTitle)
gen = iter([page])
if not gen:
gen = genFactory.getCombinedGenerator()
if gen:
# The preloading generator is responsible for downloading multiple
# pages from the wiki simultaneously.
gen = pagegenerators.PreloadingGenerator(gen)
bot = BasicBot(gen, dry)
bot.run()
else:
pywikibot.showHelp()
if __name__ == "__main__":
try:
main()
finally:
pywikibot.stopme()
Creando nuestro propio bot
El primer paso a realizar será crear una copia del bot básico con el nombre de archivo de nuestro bot, en este caso muertos.py:
cp basic.py muertos.py
A continuación abrimos el archivo muertos.py en un editor de texto para realizar los siguientes cambios.
- Cerca del inicio:
- añadimos la línea import re necesaria para poder usar expresiones regulares en nuestro bot
- añadimos la línea para msg= que corresponde al español con el texto
'es': u'Cambiando plantilla {{edad}} por número en artículos de personas fallecidas',
- Creamos la función CuentaAnyo(diaA, mesA, anyoA, diaB,mesB, anyoB), que calculará la edad a partir de la fecha de nacimiento (diaA, mesA, añoA) y la fecha de defunción (diaB, mesB, anyoB) como sigue:
def CuentaAnyo( match):
diaA=int(match.group(1))
mesA=int(match.group(2))
anyoA=int(match.group(3))
diaB=int(match.group(4))
mesB=int(match.group(5))
anyoB=int(match.group(6))
cuenta = anyoB-anyoA # Inicialmente, restamos el año de defunción al año de nacimiento.
if mesB<mesA: # Pero si el mes de defunción es anterior al de nacimiento...
cuenta = cuenta-1 # entonces a la edad hay que restar 1
elif (mesB == mesA) and (diaB < diaA): # lo mismo si el mes es el mismo pero los días quedan al revés
cuenta = cuenta-1
if (anyoB>0) and (anyoA<0): #Finalmente, si la persona nació antes del año 1 pero murió después
cuenta=cuenta-1 # debemos hacer un pequeño ajuste al no existir año cero.
s= str(cuenta)+u" años"
return s
Asegúrate que esta función aparezca antes de la función treat() que describimos enseguida.
- Reemplazamos la función treat por una que localice la plantilla {{edad}}, si existe, por el número correspondiente.
def treat(self, page):
"""
Lee la página, busca la plantilla {{edad}} y la reemplaza por un número
"""
text = self.load(page)
if not text:
return
# Una vez que se lee el contenido del artículo, debemos localizar la plantilla {{edad}}.
# Ahora, la sintaxis de la plantilla para estos artículos es
# {{edad|día inicial|mes inicial|año inicial|día final|mes final|año final}}
# Por lo que usaremos una [[expresión regular]] para localizarla.
p = re.compile('{{edad\|(\d+)\|(\d+)\|(-?\d+)\|(\d+)\|(\d+)\|(-?\d+)}}', re.IGNORECASE)
# La expresión anterior busca el texto "{{edad" seguido de 6 números, separados por una barra.
# y reemplaza el texto que coincida con el resultado de la función CuentaAnyo():
text=p.sub(CuentaAnyo, text)
if not self.save(text, page, self.summary):
pywikibot.output(u'Page %s not saved.' % page.title(asLink=True))
Resultado final
Al final de las modificaciones debe haber quedado el siguiente resultado.
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
This is not a complete bot; rather, it is a template from which simple
bots can be made. You can rename it to mybot.py, then edit it in
whatever way you want.
The following parameters are supported:
¶ms;
-dry If given, doesn't do any real changes, but only shows
what would have been changed.
All other parameters will be regarded as part of the title of a single page,
and the bot will only work on that single page.
"""
#
# (C) Pywikipedia bot team, 2006-2010
#
# Distributed under the terms of the MIT license.
#
__version__ = '$Id: basic.py 8807 2010-12-27 21:34:11Z purodha $'
#
import wikipedia as pywikibot
import pagegenerators
import re
# This is required for the text that is shown when you run this script
# with the parameter -help.
docuReplacements = {
'¶ms;': pagegenerators.parameterHelp
}
def CuentaAnyo( match):
diaA=int(match.group(1))
mesA=int(match.group(2))
anyoA=int(match.group(3))
diaB=int(match.group(4))
mesB=int(match.group(5))
anyoB=int(match.group(6))
cuenta = anyoB-anyoA # Inicialmente, restamos el año de defunción al año de nacimiento.
if mesB<mesA: # Pero si el mes de defunción es anterior al de nacimiento...
cuenta = cuenta-1 # entonces a la edad hay que restar 1
elif (mesB == mesA) and (diaB < diaA): # lo mismo si el mes es el mismo pero los días quedan al revés
cuenta = cuenta-1
if (anyoB>0) and (anyoA<0): #Finalmente, si la persona nació antes del año 1 pero murió después
cuenta=cuenta-1 # debemos hacer un pequeño ajuste al no existir año cero.
s= str(cuenta)+u" años"
return s
class BasicBot:
# Edit summary message that should be used.
# NOTE: Put a good description here, and add translations, if possible!
msg = {
'es': u'Cambiando plantilla {{edad}} por número en artículos de personas fallecidas',
}
def __init__(self, generator, dry):
"""
Constructor. Parameters:
@param generator: The page generator that determines on which pages
to work.
@type generator: generator.
@param dry: If True, doesn't do any real changes, but only shows
what would have been changed.
@type dry: boolean.
"""
self.generator = generator
self.dry = dry
# Set the edit summary message
self.summary = pywikibot.translate(pywikibot.getSite(), self.msg)
def run(self):
for page in self.generator:
self.treat(page)
def treat(self, page):
"""
Lee la página, busca la plantilla {{edad}} y la reemplaza por un número
"""
text = self.load(page)
if not text:
return
# Una vez que se lee el contenido del artículo, debemos localizar la plantilla {{edad}}.
# Ahora, la sintaxis de la plantilla para estos artículos es
# {{edad|día inicial|mes inicial|año inicial|día final|mes final|año final}}
# Por lo que usaremos una [[expresión regular]] para localizarla.
p = re.compile('{{edad\|(\d+)\|(\d+)\|(-?\d+)\|(\d+)\|(\d+)\|(-?\d+)}}', re.IGNORECASE)
# La expresión anterior busca el texto "{{edad" seguido de 6 números, separados por una barra.
# y reemplaza el texto que coincida con el resultado de la función CuentaAnyo():
text=p.sub(CuentaAnyo, text)
if not self.save(text, page, self.summary):
pywikibot.output(u'Page %s not saved.' % page.title(asLink=True))
def load(self, page):
"""
Loads the given page, does some changes, and saves it.
"""
try:
# Load the page
text = page.get()
except pywikibot.NoPage:
pywikibot.output(u"Page %s does not exist; skipping."
% page.title(asLink=True))
except pywikibot.IsRedirectPage:
pywikibot.output(u"Page %s is a redirect; skipping."
% page.title(asLink=True))
else:
return text
return None
def save(self, text, page, comment, minorEdit=True, botflag=True):
# only save if something was changed
if text != page.get():
# Show the title of the page we're working on.
# Highlight the title in purple.
pywikibot.output(u"\n\n>>> \03{lightpurple}%s\03{default} <<<"
% page.title())
# show what was changed
pywikibot.showDiff(page.get(), text)
pywikibot.output(u'Comment: %s' %comment)
if not self.dry:
choice = pywikibot.inputChoice(
u'Do you want to accept these changes?',
['Yes', 'No'], ['y', 'N'], 'N')
if choice == 'y':
try:
# Save the page
page.put(text, comment=comment,
minorEdit=minorEdit, botflag=botflag)
except pywikibot.LockedPage:
pywikibot.output(u"Page %s is locked; skipping."
% page.title(asLink=True))
except pywikibot.EditConflict:
pywikibot.output(
u'Skipping %s because of edit conflict'
% (page.title()))
except pywikibot.SpamfilterError, error:
pywikibot.output(
u'Cannot change %s because of spam blacklist entry %s'
% (page.title(), error.url))
else:
return True
return False
def main():
# This factory is responsible for processing command line arguments
# that are also used by other scripts and that determine on which pages
# to work on.
genFactory = pagegenerators.GeneratorFactory()
# The generator gives the pages that should be worked upon.
gen = None
# This temporary array is used to read the page title if one single
# page to work on is specified by the arguments.
pageTitleParts = []
# If dry is True, doesn't do any real changes, but only show
# what would have been changed.
dry = False
# Parse command line arguments
for arg in pywikibot.handleArgs():
if arg.startswith("-dry"):
dry = True
else:
# check if a standard argument like
# -start:XYZ or -ref:Asdf was given.
if not genFactory.handleArg(arg):
pageTitleParts.append(arg)
if pageTitleParts != []:
# We will only work on a single page.
pageTitle = ' '.join(pageTitleParts)
page = pywikibot.Page(pywikibot.getSite(), pageTitle)
gen = iter([page])
if not gen:
gen = genFactory.getCombinedGenerator()
if gen:
# The preloading generator is responsible for downloading multiple
# pages from the wiki simultaneously.
gen = pagegenerators.PreloadingGenerator(gen)
bot = BasicBot(gen, dry)
bot.run()
else:
pywikibot.showHelp()
if __name__ == "__main__":
try:
main()
finally:
pywikibot.stopme()
Usando el bot
Uno de los beneficios de usar el esqueleto provisto es que no nos preocupamos por programar los generadores usuales, estos ya están incluidos.
Por ejemplo, podemos pedir que el bot reemplace en la categoría de Fallecidos en 1940:
python muertos.py -cat:"Fallecidos en 1940"
y el resultado será:
Observamos por ejemplo, que en las primeras páginas no hay necesidad de hacer cambio (porque no aparece la plantilla {{edad}}
) pero que cuando encuentra una, indica el cambio en 2 líneas, la primera es el contenido que está en el artículo y la segunda el contenido que quedará. Si presionamos [y] el cambio se guarda y procede a buscar el siguiente artículo.
Tarea
Como ejercicio para continuar la práctica se sugiere implementar en el bot la posibilidad de que, además de presionar [y] o [n] para aceptar o rechazar un cambio, se pueda indicar también que todos los cambios sean automáticos (no se necesite confirmar).