Tag Archives: Innovative

Vufind con Innovative Millenium y CDS-Invenio: importar registros y configurar facetas

Unos días atrás comenté cómo instalar vufind.

Una vez instalado el software y comprobado que funcionan algunos aspectos fundamentales (como la validación por LDAP, etc), el siguiente paso es proceder a la importación de registros (bien sean desde el Catálogo de la Biblioteca o desde Repositorios OAI).

Paso a comentar algunas conclusiones obtenidas mediante las primeras experiencias de carga.

Carga de registros de prueba procedentes de Innovative Millenium (formato MARC)

Imaginemos que hemos exportado desde Millenium un archivo con registros MARC (.mrc). En caso de que no tengáis una exportación de registros a mano podéis usar (esta página o ésta otra para obtener datos de ejemplo).

Mi fichero se llama $VUFIND_HOME/import/400.mrc

Procedo a importar los registros con la siguiente orden:

/usr/local/vufind/import-marc.sh import/400.mrc

Más información en la wiki de vufind.

Carga de registros de prueba procedentes de CDS-Invenio (formato MARCXML)

Imaginemos que exportamos de un repositorio un conjunto de registros en formato marcxml, por ejemplo éste.

Creo una carpeta para almacenar estos ficheros .xml exportados:

mkdir $VUFIND_HOME/harvest/desdezaguan
mkdir $VUFIND_HOME/harvest/desdezaguan/marcxml
cd $VUFIND_HOME/harvest/desdezaguan/marcxml

Obtengo los registros…

wget http://zaguan.unizar.es/search?as=1&cc=Tesis&m1=a&p1=&f1=&op1=a&m2=a&p2=&f2=&op2=a&m3=a&p3=&f3=&action_search=Buscar&c=Tesis&c=&sf=&so=a&rm=&rg=160&sc=1&of=xm
 
Connecting to zaguan.unizar.es|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: `search?as=1'
 
    [  <=>                                                                                                ] 38,128       131K/s   in 0.3s
 
2010-09-30 14:11:26 (131 KB/s) - `search?as=1' saved [38128]
 
 
[1]+  Done                    wget http://zaguan.unizar.es/search?as=1

Cambio el nombre del fichero xml…

 mv search\?as\=1 tesis0.xml

Y procedemos a realizar la importación. Se invoca poniendo como parametro el directorio donde están los XML’s:

[root@ harvest]# cd /usr/local/vufind/harvest; \
                     ./batch-import-marc.sh desdezaguan-tesis/marcxml/
 
Now Importing /usr/local/vufind/harvest/desdezaguan-tesis/marcxml//tesis0.xml ...
/usr/java/jre1.6.0_17/bin/java -Xms512m -Xmx512m
 
-Dsolrmarc.solr.war.path=/usr/local/vufind/solr/jetty/webapps/solr.war
 
-Dsolr.core.name=biblio -Dsolrmarc.pa
th=/usr/local/vufind/import -Dsolr.path=/usr/local/vufind/solr
 
-Dsolr.solr.home=/usr/local/vufind/solr -jar /usr/local/vufind/import/SolrMarc.jar
 
/usr/local/
vufind/import/import.properties
 
/usr/local/vufind/harvest/desdezaguan-tesis/marcxml/tesis0.xml
 INFO [main] (MarcImporter.java:769) - Starting SolrMarc indexing.
 INFO [main] (Utils.java:189) - Opening file: /usr/local/vufind/import/import.properties
 INFO [main] (MarcHandler.java:325) - Attempting to open data file:
 
/usr/local/vufind/harvest/desdezaguan-tesis/marcxml/tesis0.xml
 INFO [main] (MarcImporter.java:618) -  Updating to Solr index at /usr/local/vufind/solr
 INFO [main] (MarcImporter.java:634) -      Using Solr core biblio
 INFO [main] (SolrCoreLoader.java:102) - Using the data directory of:
 
/usr/local/vufind/solr/biblio
 INFO [main] (SolrCoreLoader.java:104) - Using the multicore schema file at :
 
/usr/local/vufind/solr/solr.xml
 INFO [main] (SolrCoreLoader.java:105) - Using the biblio core
 INFO [main] (MarcImporter.java:266) - Added record 1 read from file: 4841
 INFO [main] (MarcImporter.java:266) - Added record 2 read from file: 4840
 INFO [main] (MarcImporter.java:266) - Added record 3 read from file: 4823
 
....
 
 INFO [main] (MarcImporter.java:516) -  Adding 160 of 160 documents to index
 INFO [main] (MarcImporter.java:517) -  Deleting 0 documents from index
 INFO [main] (MarcImporter.java:391) - Calling commit
 INFO [main] (MarcImporter.java:402) - Done with the commit, closing Solr
 INFO [main] (MarcImporter.java:405) - Setting Solr closed flag
 INFO [main] (MarcImporter.java:431) - Connecting to solr server at URL:
 
http://localhost:8080/solr/biblio/update
 INFO [main] (SolrUpdate.java:135) - <?xml version="1.0" encoding="UTF-8"?>
 INFO [main] (SolrUpdate.java:135) - <response>
 INFO [main] (SolrUpdate.java:135) - <lst name="responseHeader"><int
 
name="status">0</int><int name="QTime">136</int></lst>
 INFO [main] (SolrUpdate.java:135) - </response>
 INFO [main] (MarcImporter.java:526) - Finished indexing in 0:01.00
 INFO [main] (MarcImporter.java:535) - Indexed 10 at a rate of about 8.0 per sec
 INFO [main] (MarcImporter.java:536) - Deleted 0 records
 INFO [Thread-2] (MarcImporter.java:465) - Starting Shutdown hook
 INFO [Thread-2] (MarcImporter.java:484) - Finished Shutdown hook

La invocacion MUEVE el fichero tesis0.xml y crea:

harvest/desdezaguan/marcxml/log y
harvest/desdezaguan/marcxml/processed

Veamos qué tiene cada carpeta:

[root@ marcxml]# ls -l $VUFIND_HOME/harvest/desdezaguan/marcxml/log/
total 8
-rw-r--r-- 1 root root 3095 Sep 29 12:54 tesis0.xml.log
 
[root@olmo marcxml]# ls -l processed/
total 68
-rw-r--r-- 1 root root 59068 Sep 29 12:49 tesis0.xml (el original)
 
[root@olmo marcxml]# cd log/
[root@olmo log]# more tesis0.xml.log

*** NOTA: Si el identificador del registro YA EXISTE en vufind no duplica, actualiza el registro

Más información en la wiki de vufind.

Configurando el display name de las facetas solr en vufind

Las facetas se describen en el archivo facets.ini. Os muestro cómo queda nuestro archivo tras la modificación y customización de los nombres que se mostrarán en las facetas. La parte de la izquierda muestra el ‘nombre lógico’ del índice de SOLR y la parte derecha el display name (aka ‘lo que sale en la web como facetas’).

* Nota: funcionan las tildes perfectamente (thanks vufind guys!)

more $VUFIND_HOME/web/conf/facets.ini
 
; The order of display is as shown below
; The name of the index field is on the left
; The display name of the field is on the right
[Results]
institution        = Origen
building           = Localización
format             = Formato
 
; Use callnumber-first for LC call numbers, dewey-hundreds for Dewey Decimal:
callnumber-first   = "Call Number"
;dewey-hundreds     = "Call Number"
 
authorStr          = Autor
language           = Idioma
genre_facet        = Genero
era                = Era
geographic_facet   = Región
 
; Facets that will appear at the top of search results when the TopFacets
; recommendations module is used.  See the [TopRecommendations] section of
; searches.ini for more details.
[ResultsTop]
topic_facet        = "Suggested Topics"
 
; This section is reserved for special boolean facets.  These are displayed
; as checkboxes.  If the box is checked, the filter on the left side of the
; equal sign is applied.  If the box is not checked, the filter is not applied.
; The value on the right side of the equal sign is the text to display to the
; user.  It will be run through the translation code, so be sure to update the
; language files appropriately.
;
; Leave the section empty if you do not need checkbox facets.
;
; NOTE: Do not create CheckboxFacets using values that also exist in the
;       other facet sections above -- this will not work correctly.

Los nombres de las facetas quedarán tal que asi:
vufind facets example configuration

Leer más sobre configuración de facetas en vufind y solr

Asignación de valores a las facetas solr en vufind

Es el próximo paso que queremos dar. Pero antes observemos cómo podemos hacer consultas al motor SOLR de vufind.
En http://yoursite.com:8080/solr/biblio/admin/form.jsp de tu servidor web podemos ver una amigable interfaz que nos permite consultar cómo son las respuestas XML a peticiones de consulta del motor y, de este modo, hacernos una idea de cómo queremos asignar valores a cada una de las partes.

Interfaz de consultas a SOLR:
vufind solr web interface

Haced una query. Ver cómo es el XML que devuelve. Fijaos en los valores que tienen los distintos registros devueltos en cada campo. En concreto, y para enseñarlo siempre con un ejemplo, vamos a fijarnos en el campo institution, que por defecto tiene asignado siempre el valor estático ‘MyInstitution’ para cualquier registro importado:

<arr name="institution">
    <str>MyInstitution</str>
</arr>

Esto es debido a la siguiente línea del fichero $VUFIND_HOME/import/marc.properties donde a esa faceta se le asigna el valor estático ‘MyInstitution’:

institution = "MyInstitution"

Imaginemos que queremos que la faceta ‘institution’ haga referencia al origen de los datos. Tendremos varios orígenes distintos: catálogo y repositorio.

Queremos que, si el registro viene del repositorio (Esto es, tiene 980a==’TESIS’) en esta faceta se guarde la cadena “Repositorio”.
Para ello debemos editar el fichero marc_local.properties (este fichero sobreescribe los settings por defecto marcados en marc.properties).

vi $VUFIND_HOME/import/marc_local.properties

Y añadimos la siguiente línea:

#asignar a la faceta 'institution' el valor de la etiqueta 980a según el <em>mappeo</em> establecido en el fichero <em>zaguan_map.properties</em>
institution = 980a,zaguan_map.properties

Y el contenido del fichero $VUFIND_HOME/import/zaguan_map.properties es:

[root@ import]# more /usr/local/vufind/import/zaguan_map.properties
 
# Si el valor de la etiqueta marcxml es 'TESIS', asigna a la faceta la cadena 'Repositorio'
TESIS = Repositorio

Del mismo modo imaginemos que, si el registro viene del catálogo (Esto es, tiene 907a==’.b1XXX’) en esta faceta se guarde la cadena “Catálogo”. En el fichero $VUFIND_HOME/import/marc_local.properties pondremos la línea:

# Tomamos los caracteres 1 y 2 de la etiqueta 907a y los <em>mappeamos</em> según el fichero <em>roble_marc.properties</em>.
institution = 907a[1-2],roble_map.properties

Y en roble_marc.properties:

[root@olmo import]# more /usr/local/vufind/import/roble_map.properties
b1 = Catalogo

¡Mucho ojo con los caracteres especiales como el punto (.) pues son interpretados como expresión regular y habría que escaparlos!

También es útil asignar el campo que actuará como identificador de los registros en vufind. En nuestro caso deseamos utilizar el valor de la etiqueta 907a como identificador del registro. Como la etiqueta es repetible deberemos añadir también el modificador first.

Añadimos pues la siguiente línea a marc_local.properties:

id = 907a, first

Acordaos de reiniciar vufind tras estas modificaciones:

$VUFIND_HOME/vufind.sh restart

De momento os dejo con el manual de facetas de la wiki de vufind para que sigáis leyendo ;)

Más experiencias en breves!

III Innovative Millenium software: change “holdings” text

In my library we use Innovative Millennium integrated library system.

If you are used to Millenium you’ll know this system uses a kinda-weird way to generate HTML output pages.

My boss told me to change a text which is displayed in some pages (refer to this link for further details).

Here is the *spanish-version* of the HTML output to be modified:
innovative millenium holdings text

Easy, I thought. Well, I was wrong. It took me a while to figure out how to solve this.

The text to be changed is the “Latest received” information displayed in a record page. This page is produced by bibdisplay.html file. More precisely, with the tag which is included in bibdisplay.html. (Note: in my case, the spanish output is produced by bibdisplay_spi.html. SPI means “spanish scope” or something like that).

I downloaded all of the /screens folder to my linux system and grep’d for the Latest received text to change (in spanish: Últimos recibidos). No luck. Then I downloaded the wwwoptions file (this text-messages are usually saved in this file). Still no luck.

After some more research I noticed a webpub_spi.def which contains that string. There is also a webpub.def with the english messages!)
So I just modified it (

vi webpub_spi.def

) and saved it again.

Voilá, it works!

CDS-Invenio: import records from WebOPAC PRO (Millennium)

One of the most common needs in nowadays libraries is to move information from the catalogue into the repository. To do so I’ve developed a bash script that calls a python program which checks for new or modified records in the library catalogue and loads them into CDS Invenio repository:

First we’ll take a look at the bash script:

#!/bin/bash
# author: miguelm[at]unizar[dot]es
# date: 2009-03-29
# comments:
#     -  This script loads into your repository some records from the
#         library's catalog.
#     -  Parameters: this script does not need any parameters at all
#     -  This script will create the following files:
#           1. reg_nuevosYYYYmmdd.xml
#           2. reg_modificadosYYYYmmdd.xml
#           3. logYYYYmmddd.xml
 
clear
PATH=$PATH:$HOME/bin
ORACLE_HOME=/usr/lib/oracle/11.1/client64
LD_LIBRARY_PATH=/usr/lib/oracle/11.1/client64/lib
export ORACLE_HOME
export LD_LIBRARY_PATH
export PATH
 
ruta="/home/apache/"
 
sal=`cd "$ruta"`
hoy2=`/bin/date`
#echo "Actual date: " $hoy2
fechaini=`/bin/date --date "$hoy2 -60 days"`
#echo "Two-months-ago date: " $fechaini
fechainimyformat=`/bin/date --d "$fechaini $1 sec" "+%d/%m/%Y"`
 
fechainiformatfile=`/bin/date --d "$fechaini $1 sec" "+%Y%m%d"`
echo $fechainiformatfile
fichcreados=$ruta"importaFHdesdeRoble/reg_nuevos"$fechainiformatfile".xml"
# the name for the xml file that contains the NEW records
 
fichmodif=$ruta"importaFHdesdeRoble/reg_modif"$fechainiformatfile".xml"
# the name for the xml file that contains the MODIFIED records
 
fichlog=$ruta"importaFHdesdeRoble/log"$fechainiformatfile".log"
# the name for the xml file that logs the proccess.
 
echo "Begining the import process  (FH records) from catalogue to repository"
echo "------------------------------------------------------------------------"
echo "Begin date: " $fechainimyformat
echo "Output file for new records: "$fichcreados
echo "Output file for modified records: "$fichmodif
echo "Log file: "$fichlog
echo "------------------------------------------------------------------------"
echo ""
s2=`/bin/touch "$fichcreados"`
s2=`/bin/touch "$fichmodif"`
comm = "/usr/bin/python "$ruta" importaFHdesdeRoble/importaFH.py"
salida=
<br />`"$comm" "$fechainimyformat" "$fichcreados" "$fichmodif" &gt; $fichlog`
 
# Now that records have been created, you should check if their sintax is correct:
# we use xmlmarclint for this purpose
 
echo "Checking file integrity..."
 
file="temp.tmp"
salida1=`/soft/cds-invenio/bin/xmlmarclint "$fichcreados" &gt; "$file"`
if [ -s $file ]; then
   # the output file of xmlmarclint command is not empty =&gt; there are errors...
   echo "Sintax of new-files XML....[FAIL]"
   mens="Sintax check failed for "$fichcreados
   echo "$mens" &gt;&gt; "$fichlog"
else
   echo "Sintax of new-files XML....[OK]"
   # if XML file is NOT empty, load its data into repository...
   if [ -s $fichcreados ]; then
       echo "Begining to upload the new records into the repository......."
       salida1=`/soft/cds-invenio/bin/bibupload -i "$fichcreados" &gt; "$fichlog"`
   else
        echo "There are no new files to load (the XML file is EMPTY)"
   fi
 
fi
alias rm='rm'
rm "$file"
 
# Similar behaviour for the modified records...(comments in spanish, sorry)
file="temp.tmp"
salida2=`/soft/cds-invenio/bin/xmlmarclint "$fichmodif" &gt; "$file"`
if [ -s $file ]; then
   # el fichero de salida de error de xmlmarclint tiene contenido =&gt; hay errores
   echo "Sintaxis del fichero de MODIFICADOS (ya existen en Zaguan).....[FAIL]"
   mens2="Fallo en la sintaxis de "$fichmodif
   echo "$mens2" &gt;&gt; "$fichlog"
else
   echo "Sintaxis del fichero de MODIFICADOS (ya existen en Zaguan).....[OK]"
   if [ -s $fichmodif ]; then
        echo "Se va a proceder a modificar estos registros.............."
        salida1=`/soft/cds-invenio/bin/bibupload -r "$fichmodif" &gt; "$fichlog"`
   else
        echo "El fichero MODIF. "$fichmodif" esta vacio = no registros que modificar"
   fi
 
fi
alias rm='rm'
rm "$file"
 
alias rm='rm -i'
mail=`/bin/mail miguelm@unizar.es &lt; "$fichlog"`
echo "Finalizado"



Now, the python program importaFH.py (which queries catalogue’s ORACLE DB):

#! /usr/bin/python
# This program is aimed to query the catalogue's
# ORACLE DB to export the CREATED/MODIFIED records
# Parameters:
#    This program should be called like:
#    python importaFH.py 01/01/2009 salida_crear.xml salida_modificar.xml
#        salida_crear  = the XML file that contains the NEW
#                             records (the ones that are NOT
#                             in your repository)
#        salida_modificar = the XML file that contains the
#                             MODIFIED records (they are already
#                             in your repository, so they have to be updated!
 
import sys
import time
import os
os.environ["NLS_LANG"] = "AMERICAN_AMERICA.UTF8"
import cx_Oracle
import cgi
 
class RegistroFH(object):
 def __init__(self, controlfield001 = "", controlfield008 = "",
    datafield040a = "", datafield040d = "", datafield100a = "",
    datafield245a = "", datafield245c = "", datafield260a = "",
    datafield260b = "", datafield260c = "", datafield300a = "",
    datafield300b = "", datafield300c = "", datafield500a = [],
    datafield538a = [], datafield700a = [], datafield700e = [],
    datafield752a = "", datafield752d = "", datafield907a = ".",
    datafield907b = "", datafield907c = "", datafield945a = "",
    datafield945t = "", datafield945y = "", datafield945z = "",
    datafield980a = "", datafield998a = "", datafield998b = "",
    datafield998f = "", datafield998g = "", datafield8564u = [],
    datafield8564z = []):
  self.controlfield001 = controlfield001
  self.controlfield008 = controlfield008
  self.datafield040a = datafield040a
  self.datafield040d = datafield040d
  self.datafield100a = datafield100a
  self.datafield245a = datafield245a
  self.datafield245c = datafield245c
  self.datafield260a = datafield260a
  self.datafield260b = datafield260b
  self.datafield260c = datafield260c
  self.datafield300a = datafield300a
  self.datafield300b = datafield300b
  self.datafield300c = datafield300c
  self.datafield500a = datafield500a
  self.datafield538a = datafield538a
  self.datafield700a = datafield700a
  self.datafield700e = datafield700e
  self.datafield752a = datafield752a
  self.datafield752d = datafield752d
  self.datafield907a = datafield907a
  self.datafield907b = datafield907b
  self.datafield907c = datafield907c
  self.datafield945a = datafield945a
  self.datafield945t = datafield945t
  self.datafield945y = datafield945y
  self.datafield945z = datafield945z
  self.datafield980a = datafield980a
  self.datafield998a = datafield998a
  self.datafield998b = datafield998b
  self.datafield998f = datafield998f
  self.datafield998g = datafield998g
  self.datafield8564u = datafield8564u
  self.datafield8564z = datafield8564z
 
# Searches in your repository the record identified by rec_key
# Returns '-1' if the record is not found
#            'tag001 value' if the record exists
def existe_en_zaguan(rec_key):
  url_buscar = """http://zaguan.unizar.es/search?p="""
  url_buscar += """%s""" % (rec_key)
  url_buscar += """*&amp;f=&amp;action_search=Buscar&amp;c=Repositorio+Digital+de+la+Universidad+de+Zaragoza&amp;sf=&amp;so=d&amp;rm=&amp;rg=10&amp;sc=1&amp;of=xm"""
  #print url_buscar
  import urllib
  f = urllib.urlopen(url_buscar)
  contenido = f.read()
  cadena_a_buscar = ''
  position1 = contenido.find(cadena_a_buscar)
  if position1 == -1:
    #print "(funcion existe_en_zaguan): %s DOES NOT EXIST en zaguan " % (rec_key)
    return -1
  else: # return records' tag001 value
    position1 = position1 + len(cadena_a_buscar)
    position2 = contenido.find("")
    return contenido[position1:position2]
 
# Creates MARCXML from a record identified by rec_key and repositories' controlfield001
# If tag001 == '' is passed, it will create MARCXML __WITHOUT TAG 001__
# If tag001 != '' it will create the MARCXML __WITH TAG 001__
# Note it will only create MARCXML if the record belongs to collection FH (bcode3=a) and it is a
#    monografy(bib_lvl=m)
def marcxml_de_registro(rec_key, tag001):
  registro = make_fh_record(rec_key)
  registro.controlfield001 = tag001
  # now connect to the catalogues Oracle DB....
  dsn = cx_Oracle.makedsn('155.210.5.40', 1521, 'IIIDB')
  connection = cx_Oracle.connect('III', 'III', dsn)
  cursor = connection.cursor()
  cursor.arraysize = 50
  stmt = """SELECT v.rec_key, v.MARC_TAG,  v.INDICATOR1, v.INDICATOR2, v.REC_DATA FROM biblio2base b, locations2 l, var_fields2 v WHERE b.bcode3='a' AND b.bib_lvl='m' AND b.REC_KEY=l.REC_KEY AND b.REC_KEY=v.REC_KEY AND"""
  stmt += """b.REC_KEY = '%s'""" % (rec_key)
  stmt += """ORDER BY v.rec_key, v.marc_tag;"""
  cursor.execute(stmt)
  fila = cursor.fetchone()
  out = ""
  while (fila != None):
    interpreta_valores(registro, fila[0], fila[1], fila[2], fila[3], fila[4]) # llama a interpreta_valores(recordFH,rec_key,marc_tag,ind1,ind2,rec_data)
  return crea_marcxml_fh(registro)
 
# It makes a query against DB and fills the file with records in XML
# All the records require these conditions:
#      1. FECHA DE CREACION &gt; fecha_ini (y menor, logicamente, que la actual)
#      2. Que TENGAN URL apuntando a ZAGUAN (856 like %zaguan%)
#      3. Que sean FONDO ANTIGUO (bcode3=a)
#      4. Que sean MONOGRAFIAS (bib_lvl=m)
#
# Vuelca la salida a fich_salida_name
def consulta_creados(fecha_ini, fich_salida_name):
  dsn = cx_Oracle.makedsn('155.210.5.40', 1521, 'IIIDB')
  connection = cx_Oracle.connect('---------', '--------', dsn)
  #change the '---' with your user/pass values
  cursor = connection.cursor()
  # cursor = connection.cursor()
  cursor.arraysize = 50
  cadena_url = "zaguan"
  # c = time.strptime(fecha_ini,"%d/%m/%Y") # tipo fecha
  # ahora pasamos la fecha a entero
  #print fecha_ini
  dia = fecha_ini[:2]
  mes = fecha_ini[3:5]
  anyo = fecha_ini[6:]
  cadena_fecha = dia+mes+anyo
  #print cadena_fecha
  # print time.strftime("%d/%m/%Y",c)
  #print "Connection encoding = " + connection.encoding, connection.nencoding, connection.maxBytesPerCharacter
  stmt = """ SELECT
                v2.rec_key, v2.MARC_TAG,  v2.INDICATOR1, v2.INDICATOR2,
                v2.REC_DATA FROM biblio2base b, locations2 l, var_fields2 v1,
                var_fields2 v2 WHERE b.bcode3='a' AND b.bib_lvl='m' AND """
  stmt += """ b.created &gt;= to_date('%d', 'DDMMYYYY' ) AND""" % (int(cadena_fecha))
  stmt += """  b.REC_KEY=l.REC_KEY AND
	       l.location LIKE '100%' AND
	       b.REC_KEY=v1.REC_KEY AND
	       v1.MARC_TAG LIKE '856' AND"""
  stmt += """  v1.REC_DATA LIKE '%s' AND """ %('%'+cadena_url+'%')
  stmt += """  v1.REC_KEY=v2.REC_KEY ORDER BY v2.rec_key, v2.marc_tag"""
  cursor.execute(stmt)
  fila = cursor.fetchone()
  out = ""
  while (fila != None):
    record1 = make_fh_record(fila[0]) # se le pasa el rec_key
    interpreta_valores(record1, fila[0], fila[1], fila[2], fila[3], fila[4]) # llama a interpreta_valores(recordFH,rec_key,marc_tag,ind1,ind2,rec_data)
    fila2 = cursor.fetchone()
    if fila == None or fila == "": break
    if fila2 == None or fila2 == "" : break
    while fila[0] == fila2[0]:
      interpreta_valores(record1, fila2[0], fila2[1], fila2[2], fila2[3], fila2[4])
      fila2 = cursor.fetchone()
      if fila2 == None or fila2 == "" : break
    # en este punto o fila2[0]!=fila[0] o ya no hay mas filas que leer
    # si fila2[0] =! fila[0] hay que crear el nuevo registro
    #     (asignar fila2 a fila y comenzar de nuevo el bucle)
    out += crea_marcxml_fh(record1)
    # print "Creado registro '." + fila[0] + "'"
    #print out
    #print "Durmiendo 5segundos..."
    #time.sleep(50)
    fila = fila2
  #en este punto, fila=none
  escribe_a_fichero(fich_salida_name, out)
 
# A partir de un registroFH __YA CREADO__ y con la informacion
#  de alguna consulta (tras hacer un fetchrow)
# se van rellenando los campos del registro hasta completarlo.
def interpreta_valores(record, rec_key, marc_tag, indicator1, indicator2, rec_data):
    if marc_tag == '008':
	    record.controlfield008 = rec_data
    elif marc_tag == '040':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a': # si el primer caracter es a correspondia a un |a =&gt; nombre del autor
			    record.datafield040a  = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'd': #fechas relacionadas con el nombre del autor
	    		    record.datafield040d = sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '100':
	    # primero hacer un split por | de los campos que puede haber...
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a': # si el primer caracter es a correspondia a un |a =&gt; nombre del autor
			    record.datafield100a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'c':
			    record.datafield100a = sbf[1:].replace('&amp;', '&amp;') + record.datafield100a
		    elif sbf[0] == 'd': #fechas relacionadas con el nombre del autor
	    		    record.datafield100a = record.datafield100a + '(' + sbf[1:].replace('&amp;', '&amp;') + ')'
    elif marc_tag == '110':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a': # si el primer caracter es a correspondia a un |a =&gt; nombre del autor
			    record.datafield110a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'b':
			    record.datafield110a = record.datafield110a + sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '245':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a': # si el primer caracter es a correspondia a un |a =&gt; nombre del autor
			    record.datafield245a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'b':
			    record.datafield245a =  record.datafield245a + sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'c': #fechas relacionadas con el nombre del autor
	    		    record.datafield245c =  sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '260':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a':
			    record.datafield260a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'b':
			    record.datafield260b = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'c': #fechas relacionadas con el nombre del autor
	    		    record.datafield260c = sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '300':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a':
			    record.datafield300a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'b': #repetible!
			    record.datafield300b = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'c': #fechas relacionadas con el nombre del autor
	    		    record.datafield300c = sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '500':  #repetible
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if (sbf[0] == 'a') &amp; (esta(sbf[1:],record.datafield500a) == -1):
			    record.datafield500a.append(sbf[1:].replace('&amp;', '&amp;'))
    elif marc_tag == '700':  #repetible
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if (sbf[0] == 'a') &amp; (esta(sbf[1:],record.datafield700a) == -1):
			    record.datafield700a.append(sbf[1:].replace('&amp;', '&amp;'))
		    elif (sbf[0] == 'e') &amp; (esta(sbf[1:],record.datafield700e) == -1):
			    record.datafield700e.append(sbf[1:].replace('&amp;', '&amp;'))
    elif marc_tag == '752':
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada uno de los subcampos...
		if sbf != "":
		    if sbf[0] == 'a':
			    record.datafield752a = sbf[1:].replace('&amp;', '&amp;')
		    elif sbf[0] == 'e':
			    record.datafield752e = sbf[1:].replace('&amp;', '&amp;')
    elif marc_tag == '856':  #repetible
	    subfields = rec_data.split('|')
	    for sbf in subfields: # para cada |u debe haber un |z
		if (sbf != "") and (sbf.rfind('http://www.lizardtech.es') == -1) and (sbf.rfind('Para ver el documento necesita') == -1) and (sbf.rfind('img src=/screens/logo/djvu.png border') == -1) : # comprobar que no sea el link al plugin lizardtech...
		    if sbf[0] == 'u':
			    if (esta(sbf[1:],record.datafield8564u) == -1):
			    	record.datafield8564u.append(sbf[1:].replace('&amp;', '&amp;'))
				return
		    if sbf[0] == 'z':
			    record.datafield8564z.append(sbf[1:].replace('&amp;', '&amp;'))
 
# Returns -1 if 'cadena' is NOT in the strings 'array'
#           0 in other cases
def esta(cadena,array):
  i = 0
  encontrado = -1
  while (encontrado == -1) &amp; (i  " % sys.argv[0]
        return
    salida = "Obteniendo registros de FH con link a zaguan desde fecha: '%(fecha)s' \nSe crea fichero MARCXML con ellos '%(fich)s'" % { 'fecha': sys.argv[1], 'fich':sys.argv[2] }
    #os.environ["NLS_LANG"] = "SPANISH_SPAIN.AL32UTF8"
 
    print salida
    consulta_creados(sys.argv[1], sys.argv[2])
 
# Genera en sys.argv[2] el MARCXML correspondiente a los registros modificados entre la fecha determinada por sys.argv[1]
# y la fecha actual.
# formato de fecha: dd/mm/aaaa, con el mes en numero
def importar_los_modificados():
  if len(sys.argv) != 4:
    print "Uso: %s    " % sys.argv[0]
    return
  salida = "Obteniendo registros MODIFICADOS del FH con link a zaguan desde fecha: '%(fecha)s'" % { 'fecha': sys.argv[1] }
  print salida
  consulta_modificados(sys.argv[1], sys.argv[2], sys.argv[3])
 
# Realiza la consulta a la BD y va rellenando el fichero con los registros en XML
# Con los registros con:
#      1. FECHA DE CREACION &gt; fecha_ini (y menor, logicamente, que la actual)
#      2. Que TENGAN URL apuntando a ZAGUAN (856 like %zaguan%)
#      3. Que sean FONDO ANTIGUO (bcode3=a)
#      4. Que sean MONOGRAFIAS (bib_lvl=m)
#
# Vuelca la salida en fich_salida_nuevos (para ficheros que hay que cargar con bibupload -i)
#                     fich_salida_modificados (para ficheros que hay que cargar con bibupload -r)
def consulta_modificados(fecha_ini, fich_salida_nuevos, fich_salida_modificados):
  dsn = cx_Oracle.makedsn('155.210.5.40', 1521, 'IIIDB')
  connection = cx_Oracle.connect('III', 'III', dsn)
  cursor = connection.cursor()
  cursor.arraysize = 50
  cadena_url = "zaguan"
  # c = time.strptime(fecha_ini,"%d/%m/%Y") # tipo fecha
  # ahora pasamos la fecha a entero
  #print fecha_ini
  dia = fecha_ini[:2]
  mes = fecha_ini[3:5]
  anyo = fecha_ini[6:]
  cadena_fecha = dia+mes+anyo
  stmt = """ SELECT v2.rec_key, v2.MARC_TAG,  v2.INDICATOR1, v2.INDICATOR2, v2.REC_DATA FROM biblio2base b, locations2 l, var_fields2 v1,  var_fields2 v2 WHERE b.bcode3='a' AND b.bib_lvl='m' AND """
  stmt += """ b.updated &gt;= to_date('%d', 'DDMMYYYY' ) AND""" % (int(cadena_fecha))
  stmt += """  b.REC_KEY=l.REC_KEY AND
	       l.location LIKE '100%' AND
	       b.REC_KEY=v1.REC_KEY AND
	       v1.MARC_TAG LIKE '856' AND"""
  stmt += """  v1.REC_DATA LIKE '%s' AND """ %('%'+cadena_url+'%')
  stmt += """  v1.REC_KEY=v2.REC_KEY ORDER BY v2.rec_key, v2.marc_tag"""
  cursor.execute(stmt)
  fila = cursor.fetchone()
  out = ""
  out_mod = ""
  while (fila != None):
   record001tag = existe_en_zaguan(fila[0])
   if (record001tag == -1): # no existe en ZAGUAN =&gt; lo anyadimos al fichero de nuevas incorporaciones
       print "Tratando registro %s - no existe en zaguan -" % (fila[0])
       record1 = make_fh_record(fila[0]) # se le pasa el rec_key
       interpreta_valores(record1, fila[0], fila[1], fila[2], fila[3], fila[4])
       # llama a interpreta_valores(recordFH,rec_key,marc_tag,ind1,ind2,rec_data)
       fila2 = cursor.fetchone()
       if fila == None or fila == "": break
       if fila2 == None or fila2 == "" : break
       while fila[0] == fila2[0]:
          interpreta_valores(record1, fila2[0], fila2[1], fila2[2], fila2[3], fila2[4])
          fila2 = cursor.fetchone()
          if fila2 == None or fila2 == "" : break
       # en este punto o fila2[0]!=fila[0] o ya no hay mas filas que leer
       # si fila2[0] =! fila[0] hay que crear el nuevo registro (asignar fila2 a fila y comenzar de nuevo el bucle)
       out += crea_marcxml_fh(record1)
       global cuantosnew
       cuantosnew += 1
       fila = fila2
   else: # el fichero EXISTE en Zaguan =&gt; lo anyadimos al fichero de registros a modificar
       print "Tratando registro %s - existe en zaguan como id001=%s -" % (fila[0], record001tag)
       record2 = make_fh_record(fila[0]) # se le pasa el rec_key
       interpreta_valores(record2, fila[0], fila[1], fila[2], fila[3], fila[4])
       # llama a interpreta_valores(recordFH,rec_key,marc_tag,ind1,ind2,rec_data)
       fila2 = cursor.fetchone()
       if fila == None or fila == "": break
       if fila2 == None or fila2 == "" : break
       while fila[0] == fila2[0]:
          interpreta_valores(record2, fila2[0], fila2[1], fila2[2], fila2[3], fila2[4])
          fila2 = cursor.fetchone()
          if fila2 == None or fila2 == "" : break
       # en este punto o fila2[0]!=fila[0] o ya no hay mas filas que leer
       # si fila2[0] =! fila[0] hay que crear el nuevo registro (asignar fila2 a fila y comenzar de nuevo el bucle)
       record2.controlfield001 = record001tag
       out_mod += crea_marcxml_fh(record2)
       global cuantosmod
       cuantosmod += 1
       fila = fila2
  #en este punto, fila=none
  concatena_a_fichero(fich_salida_nuevos, out)
  escribe_a_fichero(fich_salida_modificados, out_mod)
 
# Crea registro vacio rellenando solo el campo 907a con el rec_key
def make_fh_record(identificador):
    controlfield001 = ""
    controlfield008 = ""
    datafield040a = ""
    datafield040d = ""
    datafield100a = ""
    datafield245a = ""
    datafield245c = ""
    datafield260a = ""
    datafield260b = ""
    datafield260c = ""
    datafield300a = ""
    datafield300b = ""
    datafield300c = ""
    datafield500a = []
    datafield538a = []
    datafield700a = []
    datafield700e = []
    datafield752a = ""
    datafield752d = ""
    datafield907a = "." + identificador
    datafield907b = ""
    datafield907c = ""
    datafield945a = ""
    datafield945t = ""
    datafield945y = ""
    datafield945z = ""
    datafield980a = ""
    datafield998a = ""
    datafield998b = ""
    datafield998f = ""
    datafield998g = ""
    datafield8564u = []
    datafield8564z = []
    datafield538a.append("System requirements: PC, World Wide Web Browser and DJVU reader")
    datafield538a.append("Available electronically via Internet")
    record = RegistroFH(
    		 controlfield001,
                 controlfield008,
                 datafield040a,
		 datafield040d,
                 datafield100a,
                 datafield245a,
                 datafield245c,
                 datafield260a,
                 datafield260b,
                 datafield260c,
                 datafield300a,
                 datafield300b,
                 datafield300c,
                 datafield500a,
                 datafield538a,
                 datafield700a,
                 datafield700e,
                 datafield752a,
                 datafield752d,
                 datafield907a,
                 datafield907b,
                 datafield907c,
                 datafield945a,
                 datafield945t,
                 datafield945y,
                 datafield945z,
                 datafield980a,
                 datafield998a,
                 datafield998b,
                 datafield998f,
                 datafield998g,
                 datafield8564u,
		 datafield8564z
                 );
    return record
 
# Devuelve el registro en MARCXML (relleno con los campos propios del PFC)
# de un registro (del tipo registroFH) __YA CREADO__
# algunos campos son repetibles.
def crea_marcxml_fh(registro):
	cadena = ""
	if len(registro.datafield8564u) == 0: return cadena
	# sigue siempre que el registro TENGA URL...
	cadena += '\n'
	if registro.controlfield001 != "":
		cadena += '''\t%(controlfield)s\n''' % { 'controlfield' : registro.controlfield001 }
	if registro.controlfield008 != "":
		cadena += '''\t%(controlfield)s\n''' % { 'controlfield' : registro.controlfield008 }
	if registro.datafield040a != "":
		cadena += '''\t\n\t\t%(datafield040a)s\n'''% { 'datafield040a' : registro.datafield040a }
		if registro.datafield040d != "":
			cadena += '''\t\t%(datafield040d)s\n''' % { 'datafield040d' : registro.datafield040d }
		cadena += '''\t\n'''
	if registro.datafield100a != "":
                cadena += '''\t\n\t\t%(datafield100a)s\n\t\n''' % { 'datafield100a' : registro.datafield100a }
        if registro.datafield245a != "":
                cadena += '''\t\n\t\t%(datafield245a)s\n\t\n''' % { 'datafield245a' : registro.datafield245a }
        if registro.datafield260a != "":
		cadena += '''\t\n\t\t%(datafield260a)s\n''' % { 'datafield260a' : registro.datafield260a }
		if registro.datafield260b!= "":
			cadena += '''\t\t%(datafield260b)s\n''' % { 'datafield260b' : registro.datafield260b }
		if registro.datafield260c!= "":
			cadena += '''\t\t%(datafield260c)s\n''' % { 'datafield260c' : registro.datafield260c }
		cadena += '''\t\n'''
	if registro.datafield300a != "":
		cadena += '''\t\t\n\t%(datafield300a)s\n\t\n''' % { 'datafield300a' : registro.datafield300a }
	# Generacion de palabras clave (almacenadas en el array de strings keywords)
	for contador in range(len(registro.datafield500a)):
                cadena += '''\t\n\t\t%(comentario_i)s\n\t\n'''	% { 'comentario_i': registro.datafield500a[contador]}
        for contador in range(len(registro.datafield538a)):
                cadena += '''\t\n\t\tTexto completo %(etiq538)s\n\t\n''' % { 'etiq538': registro.datafield538a[contador] }
        if (len(registro.datafield700a)&gt;0) and (len(registro.datafield700a) == len(registro.datafield700e)):
            for i in range(len(registro.datafield700a)):
        	cadena += '''\t\n\t\t%(700a_i)s\n\t\t%(700e_i)s\n\t\n'''	% { '700a_i': registro.datafield700a[i], '700e_i': registro.datafield700e[i]}
        if registro.datafield752a != "":
            cadena += '''\t\n\t\t%(datafield752a)s\n''' % { 'datafield752a' : registro.datafield752a }
            if registro.datafield752d != "":
               cadena += '''\t\t%(datafield752d)s\n''' % { 'datafield752d' : registro.datafield752d }
            cadena += '''\t\n'''
        for contador in range(len(registro.datafield8564u)):
	    try:
		cadena += '''\t\n\t\t%(texto)s\n\t\t%(url)s\n\t\n''' % { 'url': registro.datafield8564u[contador], 'texto' : registro.datafield8564z[contador]}
	    except:
		cadena += '''\t\n\t\tTexto completo\n\t\t%(url)s\n\t\n''' % { 'url': registro.datafield8564u[contador]}
	if registro.datafield907a != "":
            cadena += '''\t\n\t\t%(datafield907a)s\n''' % { 'datafield907a' : registro.datafield907a }
            if registro.datafield907b != "":
                  cadena += '''\t\t%(datafield907b)s\n''' % { 'datafield907b' : registro.datafield907b }
            if registro.datafield907c != "":
                  cadena += '''\t\t%(datafield907c)s\n''' % { 'datafield907c' : registro.datafield907c }
            cadena += '''\t\n'''
        if registro.datafield998a != "":
            cadena += '''\t\n\t\t%(datafield998a)s\n''' % { 'datafield998a' : registro.datafield998a }
            if registro.datafield998b != "":
                  cadena += '''\t\t%(datafield998b)s\n''' % { 'datafield998b' : registro.datafield998b }
            if registro.datafield998f != "":
                  cadena += '''\t\t%(datafield998f)s\n''' % { 'datafield998f' : registro.datafield998f }
            if registro.datafield998g != "":
                  cadena += '''\t\t%(datafield998g)s\n''' % { 'datafield998g' : registro.datafield998g }
            cadena += '''\t\n'''
 
        if len(registro.datafield8564u) &gt; 0:
        # al menos debe haber una URL para que se genere el XML...
		cadena += '''\t\n\t\tFH\n\t\n\n'''
	return cadena
 
# Concatena al fichero   el  pasado por parametro
# Si el fichero  existe, lo borra y crea uno nuevo. __NO CONCATENA__
def escribe_a_fichero(filename, contenidofichero):
  # Abrir fichero 'filename' para escritura
  file_handle = open (filename, 'w' )
  # Escribe contenidofichero al fichero 'filename'
  file_handle.write(contenidofichero)
  # Muestra el fichero por pantalla
  # print contenidofichero
  # cierra el fichero 'filename'
  file_handle.close
 
# Concatena al fichero   el  pasado por parametro
# Si el fichero  existe, lo borra y crea uno nuevo. __NO CONCATENA__
def concatena_a_fichero(filename, contenidofichero):
  # Abrir fichero 'filename' para escritura
  file_handle = open (filename, 'a' )
  # Escribe contenidofichero al fichero 'filename'
  file_handle.write(contenidofichero)
  # Muestra el fichero por pantalla
  # print contenidofichero
  # cierra el fichero 'filename'
  file_handle.close
 
# Informa de los resultados del proceso de consulta
def informa_resultados():
  print "Total de registros tratados: %d, de los cuales" %(cuantosnew + cuantosmod)
  print "NUEVOS: %d" %(cuantosnew)
  print "MODIFICADOS: %d" %(cuantosmod)
 
# MAIN -------------------------------------------------
global cuantosmod
cuantosmod = 0
global cuantosnew
cuantosnew = 0
importar_los_modificados()
informa_resultados()


The last thing that remains is to add a cron task which calls the bash script. My crontab looks like:

sudo -u apache crontab -l

# Added by Miguel -------------------
SHELL=/bin/bash
PATH=$PATH:$HOME/bin
ORACLE_HOME=/usr/lib/oracle/11.1/client64
LD_LIBRARY_PATH=/usr/lib/oracle/11.1/client64/lib
# ----------------------------------------------
#Mins  Horas  Días   Meses  Dia de la semana
# Para hacerlo todos los días a las 2:30
30 02 15 * * /home/apache/importaFHdesdeRoble/importaFH.sh > /home/apache/importaFHdesdeRoble/salidacron.txt


Please make sure that the home directory for apache has permissions like:

ls -l /home/apache
total 3132
-rw-r--r-- 1 apache apache 3137967 Feb 16 13:18 fh_conzaguan.xml
-rw-r--r-- 1 apache apache 32783 Feb 19 09:36 garciala.xml
drwxr-xr-x 7 apache apache 4096 Jun 15 02:30 importaFHdesdeRoble
-rw-r--r-- 1 apache apache 2909 Feb 23 14:37 tbzn.xml


And importaFHdesdeRoble folder is like:

[root@zaguan cdsadmin]# ls -l /home/apache/importaFHdesdeRoble/
total 504
-rw-r--r-- 1 apache apache 27588 Mar 30 13:55 importaFH.py
-rwxr-xr-x 1 apache apache 3516 Apr 2 07:53 importaFH.sh
drwxr-xr-x 2 root root 4096 Apr 16 11:35 log_hechos
drwxr-xr-x 2 apache apache 4096 Mar 30 13:55 logs
drwxr-xr-x 2 apache apache 4096 Mar 30 13:55 salidas_xml

You can download the full code here:
[1] Bash script (called importaFH.sh)
[2] Python program (called importaFH.py)