Reemplazar cadena en un enorme (70 GB), una línea, archivo de texto


126

Tengo un archivo de texto enorme (70 GB), una línea y quiero reemplazar una cadena (token). Quiero reemplazar el token <unk>, con otro token ficticio ( problema de guantes ).

Lo intenté sed:

sed 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

¡pero el archivo de salida corpus.txt.newtiene cero bytes!

También intenté usar perl:

perl -pe 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

pero recibí un error de falta de memoria.

Para archivos más pequeños, funcionan los dos comandos anteriores.

¿Cómo puedo reemplazar una cadena es un archivo? Esta es una pregunta relacionada, pero ninguna de las respuestas funcionó para mí.

Editar : ¿Qué hay de dividir el archivo en trozos de 10 GB (o lo que sea) cada uno y aplicarlo seden cada uno de ellos y luego fusionarlos cat? ¿Tiene sentido? ¿Hay una solución más elegante?


Como señaló @Gilles, ¿puede detectar algún carácter repetido que podría servir como un delimitador personalizado en su única línea grande?
RomanPerekhrest

Estoy pensando que una herramienta que solo puede buscar y reemplazar, pero no una expresión regular más compleja, sería más rápida. Tampoco se beneficiaría de hacer una línea a la vez, por lo que no se ahogaría con este archivo. Lamentablemente, no tengo idea de la existencia de dicha herramienta, aunque no sería difícil de escribir. Si es único, entonces sustituir en caracteres de nueva línea como en una de las respuestas probablemente sería más fácil.
ctrl-alt-delor

¿Su archivo contiene algo más que ASCII? Si es así, se podría omitir todo el manejo Unicode y se podrían procesar bytes sin procesar.
Patrick Bucher

Estoy de acuerdo con @PatrickButcher Mira una imagen más grande. Además de la necesidad inmediata de reemplazar este texto, ¿para qué más se debe usar este archivo? Si se trata de un registro de algún tipo, nadie podrá trabajar con él de manera efectiva. Si se trata de un archivo de datos que utiliza alguna aplicación, entonces esa aplicación debería tener la responsabilidad de mantener los datos en ese archivo.
Thomas Carlisle

2
Puede usar splitcon la -bopción que define los tamaños de archivo de fragmentos en bytes. Procese cada uno a su vez usando sedy el remontaje Existe el riesgo de que <unk>se pueda dividir en dos archivos y no se encuentre ...
Vladislavs Dovgalecs

Respuestas:


106

Las herramientas habituales de procesamiento de texto no están diseñadas para manejar líneas que no caben en la RAM. Tienden a funcionar leyendo un registro (una línea), manipulándolo y generando el resultado, luego continúan con el siguiente registro (línea).

Si hay un carácter ASCII que aparece con frecuencia en el archivo y no aparece en <unk>o <raw_unk>, puede usarlo como separador de registros. Como la mayoría de las herramientas no permiten separadores de registros personalizados, cambie entre ese carácter y las nuevas líneas. trprocesa bytes, no líneas, por lo que no le importa ningún tamaño de registro. Suponiendo que ;funciona:

<corpus.txt tr '\n;' ';\n' |
sed 's/<unk>/<raw_unk>/g' |
tr '\n;' ';\n' >corpus.txt.new

También puede anclar en el primer carácter del texto que está buscando, suponiendo que no se repita en el texto de búsqueda y que aparezca con suficiente frecuencia. Si el archivo puede comenzar con unk>, cambie el comando sed sed '2,$ s/…para evitar una coincidencia espuria.

<corpus.txt tr '\n<' '<\n' |
sed 's/^unk>/raw_unk>/g' |
tr '\n<' '<\n' >corpus.txt.new

Alternativamente, use el último personaje.

<corpus.txt tr '\n>' '>\n' |
sed 's/<unk$/<raw_unk/g' |
tr '\n>' '>\n' >corpus.txt.new

Tenga en cuenta que esta técnica supone que sed opera sin problemas en un archivo que no termina con una nueva línea, es decir, que procesa la última línea parcial sin truncarla y sin agregar una nueva línea final. Funciona con GNU sed. Si puede elegir el último carácter del archivo como separador de registros, evitará cualquier problema de portabilidad.


8
No tengo un archivo con el que probar, pero en Awk puede especificar el "Separador de registros" y el "Separador de registros de salida". Entonces, suponiendo que tenga una buena cantidad de comas en su archivo, es posible que pueda resolver esto con: awk -v RS=, -v ORS=, '{gsub(/<unk>/, "<raw_unk>"); print}' ¿No?
Comodín el

44
@Wildcard Sí, esa es otra solución. Sin embargo, Awk tiende a ser más lento que sed, por eso no lo ofrezco como la solución preferida para un archivo enorme.
Gilles

Puede establecer el separador de registros en Perl con la opción de línea de comando -0y el valor octal de un carácter, o dentro del script se puede establecer con una variable especial$/
beasy

@Gilles: Pero awkevite pasar el flujo dos veces a tr. Entonces, ¿sería aún más lento?
usuario285259

2
@ user285259 Normalmente no. tres muy rápido y la tubería incluso se puede paralelizar.
Gilles

110

Para un archivo tan grande, una posibilidad es Flex. Dejar unk.lser:

%%
\<unk\>     printf("<raw_unk>");  
%%

Luego compila y ejecuta:

$ flex -o unk.c  unk.l
$ cc -o unk -O2 unk.c -lfl
$ unk < corpus.txt > corpus.txt.new

55
maketiene reglas predeterminadas para esto, en lugar de flex / cc puede agregar un %option maincomo la primera línea de unk.l y luego simplemente make unk. Yo uso más o menos reflexivamente %option main 8bit fast, y tengo export CFLAGS='-march=native -pipe -Os'en mi .bashrc.
jthill

1
@undercat: Si no fuera del tema, podría mostrarle una serie de aplicaciones front-end no compiladoras, desde la resolución del problema del nivel del agua hasta el análisis de entrada de propósito especial. Es increíble lo que puedes hacer con él, si piensas un poco fuera de la caja :-)
jamesqf

@jthill, gracias: ¡ %option main+ make+ opcionalmente CFLAGSes un truco muy bueno! ¿Es -march=nativeel comportamiento predeterminado?
JJoao

1
@jamesqf como dijiste, será difícil hacer que sea una pregunta sobre el tema, pero me gustaría verlo también
Steven Penny el

1
@jamesqf ¡Un profesor mío en uni usó flex para construir una herramienta que reconocía los tipos de telas para una fábrica! ¿Qué tal si preguntamos algo como: "flex parece una herramienta muy poderosa pero es poco probable que escriba algún compilador / analizador - hay algún otro caso de uso para flex?"
Paul Evans el

40

Por lo tanto, no tiene suficiente memoria física (RAM) para contener todo el archivo a la vez, pero en un sistema de 64 bits tiene suficiente espacio de direcciones virtuales para mapear todo el archivo. Las asignaciones virtuales pueden ser útiles como un simple hack en casos como este.

Todas las operaciones necesarias están incluidas en Python. Hay varias sutilezas molestas, pero evita tener que escribir código C. En particular, se debe tener cuidado para evitar copiar el archivo en la memoria, lo que anularía completamente el punto. En el lado positivo, obtienes informes de errores de forma gratuita ("excepciones" de Python) :).

#!/usr/bin/python3
# This script takes input from stdin
# (but it must be a regular file, to support mapping it),
# and writes the result to stdout.

search = b'<unk>'
replace = b'<raw_unk>'


import sys
import os
import mmap

# sys.stdout requires str, but we want to write bytes
out_bytes = sys.stdout.buffer

mem = mmap.mmap(sys.stdin.fileno(), 0, access=mmap.ACCESS_READ)
i = mem.find(search)
if i < 0:
    sys.exit("Search string not found")

# mmap object subscripts to bytes (making a copy)
# memoryview object subscripts to a memoryview object
# (it implements the buffer protocol).
view = memoryview(mem)

out_bytes.write(view[:i])
out_bytes.write(replace)
out_bytes.write(view[i+len(search):])

Si Mi sistema tiene aproximadamente 4 gb de memoria libre de 8 gb, ¿mem = mmap.mmap (sys.stdin.fileno (), 0, access = mmap.ACCESS_READ) significa que coloca los datos en ese espacio? ¿O sería mucho más bajo (1gb?)>
Rahul

1
@Rahul "Así que no tienes suficiente RAM, pero en un sistema de 64 bits tienes suficiente espacio de direcciones virtuales para mapear todo el archivo". Está paginado dentro y fuera del carnero físico bajo demanda (o falta de él). Este programa debería funcionar sin requerir una gran cantidad de RAM física. Los sistemas de 64 bits tienen mucho más espacio de direcciones virtuales que la memoria RAM física máxima. Además, cada proceso en ejecución tiene su propio espacio de direcciones virtuales. Esto significa que el sistema en su conjunto se queda sin espacio de direcciones virtuales no es una cosa, no es un concepto válido.
sourcejedi

44
@Rahul sí! python mmap.mmap () es un envoltorio bastante delgado alrededor de la función C mmap (). Y mmap () es el mismo mecanismo utilizado para ejecutar ejecutables y el código de las bibliotecas compartidas.
sourcejedi

2
@jamesqf Podría estar equivocado, pero siento que es solo una elección personal. Dado que las pérdidas de rendimiento serían insignificantes (porque, como él dijo, la función real llama a la función c), el desperdicio de gastos generales es muy bajo, ya que no están sucediendo otras cosas en el medio. C hubiera sido mejor, pero esta solución no tenía como objetivo la optimización, solo para resolver el problema más grande y difícil de 70 gb.
Rahul

1
En general, escribir en python es más compacto. En este caso resultó que hay un par de detalles en la versión de Python, y la versión C podría haber sido más agradable de escribir. (Aunque no es tan simple si searchpuede contener un carácter NUL. Y noto que la otra versión de C aquí no admite caracteres NUL replace). Le invitamos a obtener la versión C para fines de comparación. Sin embargo, recuerde que mi versión incluye informes básicos de errores para las operaciones que realiza. La versión C al menos sería más molesta para leer IMO, cuando se incluye el informe de errores.
sourcejedi

16

Hay una replaceutilidad en el paquete mariadb-server / mysql-server. Reemplaza cadenas simples (no expresiones regulares) y, a diferencia de grep / sed / awk, replaceno le importa \ny \0. El consumo de memoria es constante con cualquier archivo de entrada (aproximadamente 400 kb en mi máquina).

Por supuesto, no necesita ejecutar un servidor mysql para usarlo replace, solo está empaquetado de esa manera en Fedora. Otras distribuciones / sistemas operativos pueden tenerlo empaquetado por separado.


16

Creo que la versión C podría funcionar mucho mejor:

#include <stdio.h>
#include <string.h>

#define PAT_LEN 5

int main()
{
    /* note this is not a general solution. In particular the pattern
     * must not have a repeated sequence at the start, so <unk> is fine
     * but aardvark is not, because it starts with "a" repeated, and ababc
     * is not because it starts with "ab" repeated. */
    char pattern[] = "<unk>";          /* set PAT_LEN to length of this */
    char replacement[] = "<raw_unk>"; 
    int c;
    int i, j;

    for (i = 0; (c = getchar()) != EOF;) {
        if (c == pattern[i]) {
            i++;
            if (i == PAT_LEN) {
                printf("%s", replacement);
                i = 0;
            }
        } else {
            if (i > 0) {
                for (j = 0; j < i; j++) {
                    putchar(pattern[j]);
                }
                i = 0;
            }
            if (c == pattern[0]) {
                i = 1;
            } else {
                putchar(c);
            }
        }
    }
    /* TODO: fix up end of file if it ends with a part of pattern */
    return 0;
}

EDITAR: modificado de acuerdo con las sugerencias de los comentarios. También se corrigió un error con el patrón <<unk>.


2
puede imprimir (patrón [j]) en lugar de (buf [j]) (son iguales en este punto, por lo que no necesita buffer
RiaD

3
también el código no funcionará para la cadena "<" ideone.com/ncM2yy
RiaD

10
30 MB en 0.3 segundos? Eso es solo 90 MB / segundo. memcpyla velocidad (es decir, el cuello de botella de memoria) es algo así como 12 GB / segundo en una CPU x86 reciente (por ejemplo, Skylake). Incluso con la sobrecarga de llamadas al sistema stdio +, para un archivo de 30 MB en caliente en caché de disco, esperaría quizás 1 GB / segundo para una implementación eficiente. ¿Compiló con la optimización deshabilitada o la E / S de un solo carácter es realmente tan lenta? getchar_unlocked/ putchar_unlockedpodría ayudar, pero definitivamente es mejor leer / escribir en fragmentos de quizás 128 kB (la mitad del tamaño de caché L2 en la mayoría de las CPU x86, por lo que en su mayoría golpeó en L2 mientras realizaba un bucle después de la lectura)
Peter Cordes

2
Desde lo alto de mi cabeza, getchar y putchar son lentos.
Rui F Ribeiro

3
El fixprograma "<<unk>"todavía no funciona si patterncomienza con una secuencia repetida de caracteres (es decir, no funcionaría si intentara reemplazar el oso hormiguero con cebra y tuviera información de aaardvak, o si estuviera tratando de reemplazar ababc y tuvo aporte de abababc). En general, no puede avanzar por el número de caracteres que ha leído a menos que sepa que no hay posibilidad de que una coincidencia comience en los caracteres que ha leído.
icarus

14

GNU greppuede mostrarle el desplazamiento de coincidencias en archivos "binarios", sin tener que leer líneas completas en la memoria. Luego puede usar ddpara leer hasta este desplazamiento, omitir la coincidencia y luego continuar copiando del archivo.

file=...
newfile=...
replace='<raw_unk>'
grep -o -b -a -F '<unk>' <"$file" |
(   pos=0
    while IFS=$IFS: read offset pattern
    do size=${#pattern}
       let skip=offset-pos
       let big=skip/1048576
       let skip=skip-big*1048576
       dd bs=1048576 count=$big <&3
       dd bs=1 count=$skip <&3
       dd bs=1 count=$size of=/dev/null <&3
       printf "%s" "$replace"
       let pos=offset+size
    done
    cat <&3
) 3<"$file" >"$newfile"

En cuanto a la velocidad, la he dividido dden una gran lectura de tamaño de bloque 1048576 y una lectura más pequeña de 1 byte a la vez, pero esta operación aún será un poco lenta en un archivo tan grande. El grepresultado es, por ejemplo, 13977:<unk>y esto se divide en los dos puntos por la lectura en variables offsety pattern. Tenemos que hacer un seguimiento posde cuántos bytes ya se han copiado del archivo.


11

Aquí hay otra línea de comando UNIX única que podría funcionar mejor que otras opciones, porque puede "buscar" un "tamaño de bloque" que funcione bien. Para que esto sea robusto, debe saber que tiene al menos un espacio en cada X caracteres, donde X es su "tamaño de bloque" arbitrario. En el siguiente ejemplo, he elegido un "tamaño de bloque" de 1024 caracteres.

fold -w 1024 -s corpus.txt | sed 's/<unk>/<raw_unk>/g' | tr '/n' '/0'

Aquí, el plegado capturará hasta 1024 bytes, pero el -s se asegura de que se rompa en un espacio si hay al menos uno desde el último salto.

El comando sed es tuyo y hace lo que esperas.

Luego, el comando tr "desplegará" el archivo convirtiendo las nuevas líneas que se insertaron de nuevo en nada.

Debería considerar probar tamaños de bloque más grandes para ver si funciona más rápido. En lugar de 1024, puede probar 10240 y 102400 y 1048576 para la opción -w de plegar.

Aquí hay un ejemplo desglosado por cada paso que convierte todas las N en minúsculas:

[root@alpha ~]# cat mailtest.txt
test XJS C4JD QADN1 NSBN3 2IDNEN GTUBE STANDARD ANTI UBE-TEST EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt
test XJS C4JD QADN1
NSBN3 2IDNEN GTUBE
STANDARD ANTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g'
test XJS C4JD QADn1
nSBn3 2IDnEn GTUBE
STAnDARD AnTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g' | tr '\n' '\0'
test XJS C4JD QADn1 nSBn3 2IDnEn GTUBE STAnDARD AnTI UBE-TEST EMAIL*C.34X test

Tendrá que agregar una nueva línea al final del archivo si tiene una, porque el comando tr lo eliminará.


1
¿Cómo se asegura de no romper el patrón en casos extremos donde no hay suficiente espacio en blanco disponible?
rackandboneman el

1
Como se indicó, para que esto sea robusto, se requiere que haya al menos un espacio cada X caracteres. Puede hacer ese análisis lo suficientemente fácil, con cualquier tamaño de bloque que elija: fold -w X mailtest.txt | grep -v "" | wc -l El número que devuelve es el número de líneas plegadas con posibles casos de borde. Si es cero, la solución está garantizada para funcionar.
alfreema

10

Utilizando perl

Administrar tus propios buffers

Puede usar IO::Handle's setvbufpara administrar las memorias intermedias predeterminadas, o puede administrar sus propias memorias intermedias con sysready syswrite. Compruebe perldoc -f sysready perldoc -f syswritepara obtener más información, esencialmente omiten io almacenado en el búfer.

Aquí hacemos rodar nuestro propio buffer IO, pero lo hacemos de forma manual y arbitraria en 1024 bytes. También abrimos el archivo para RW, así que lo hacemos todo en el mismo FH a la vez.

use strict;
use warnings;
use Fcntl qw(:flock O_RDWR);
use autodie;
use bytes;

use constant CHUNK_SIZE => 1024 * 32;

sysopen my $fh, 'file', O_RDWR;
flock($fh, LOCK_EX);

my $chunk = 1;
while ( sysread $fh, my $bytes, CHUNK_SIZE * $chunk ) {
  if ( $bytes =~ s/<unk>/<raw_unk>/g ) {
    seek( $fh, ($chunk-1)* CHUNK_SIZE, 0 );
    syswrite( $fh, $bytes, 1024);
    seek( $fh, $chunk * CHUNK_SIZE, 0 );
  }
  $chunk++;
}

Si vas a ir por esta ruta

  1. Asegúrese <unk>y <raw_unk>son el mismo tamaño en bytes.
  2. Es posible que desee asegurarse de que nuestro método protegido no cruce el CHUNKSIZElímite, si está reemplazando más de 1 byte.

2
¿Qué pasa si <unk>cae en un límite entre trozos?
liori

8

Puede probar bbe ( editor de bloques binarios ), un " sedpara archivos binarios".

Tuve un buen éxito al usarlo en un archivo de texto de 7GB sin EOLcaracteres, reemplazando múltiples ocurrencias de una cadena con una de diferente longitud. Sin intentar ninguna optimización, dio un rendimiento de procesamiento promedio de> 50 MB / s.


5

Con perl, podría trabajar con registros de longitud fija como:

perl -pe 'BEGIN{$/=\1e8}
          s/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

Y espero que no haya <unk>más de dos de esos registros de 100 MB.


También estaba pensando en este método, pero usando el while read -N 1000 chunk;(el 1000elegido como ejemplo). La solución para el <unk>, dividido entre los fragmentos, es dos pasos a través del archivo: el primero con los fragmentos de 100 MB y el segundo con los fragmentos de '100 MB + 5 bytes'. Pero no es la solución óptima en el caso del archivo de 70 GB.
MiniMax

3
Ni siquiera necesitas dos pases. Lea el bloque A. Si bien no es EOF, lea el bloque B. Buscar / reemplazar en A + B. A: = B. Bucle. La complejidad garantiza que no reemplace dentro del reemplazo.
roaima

@MiniMax, esa segunda pasada no necesariamente ayudaría, ya que la primera pasada habría agregado 5 bytes por cada aparición de <unk>.
Stéphane Chazelas

1
@roaima, sí, esa sería una solución mucho más complicada. Aquí es un enfoque simple que solo es altamente probable (suponiendo que las <unk>ocurrencias estén muy lejos, si no, use $/ = ">"y s/<unk>\z/<raw_unk>/g) de ser correcto.
Stéphane Chazelas

5

Aquí hay un pequeño programa Go que realiza la tarea ( unk.go):

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    const (
        pattern     = "<unk>"
        replacement = "<raw_unk>"
    )
    var match int
    var char rune
    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        char = rune(scanner.Text()[0])
        if char == []rune(pattern)[match] {
            match++
            if match == len(pattern) {
                fmt.Print(replacement)
                match = 0
            }
        } else {
            if match > 0 {
                fmt.Print(string(pattern[:match]))
                match = 0
            }
            if char == rune(pattern[0]) {
                match = 1
            } else {
                fmt.Print(string(char))
            }
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Simplemente compílalo go build unk.goy ejecútalo como ./unk <input >output.

EDITAR:

Lo siento, no leí que todo está en una línea, así que intenté leer el archivo carácter por carácter ahora.

EDITAR II:

Se aplicó la misma corrección que al programa C.


1
¿Esto evita leer todo el archivo en la memoria?
gato

1
Lee el archivo carácter por carácter y nunca guarda todo el archivo en la memoria, solo caracteres individuales.
Patrick Bucher

1
scanner.Split(bufio.ScanRunes)hace la magia
Patrick Bucher

Compruebe también go doc bufio.MaxScanTokenSizeel tamaño predeterminado del búfer.
Patrick Bucher

Al igual que su Cprograma, esto no funciona para reemplazar el oso hormiguero por cebra con una entrada de oso hormiguero.
icarus

1

Esto puede ser excesivo para un archivo de 70GB y una simple búsqueda y reemplazo, pero el marco Hadoop MapReduce resolvería su problema en este momento sin costo alguno (elija la opción 'Nodo único' cuando lo configure para ejecutarlo localmente), y puede ser escalado a capacidad infinita en el futuro sin la necesidad de modificar su código.

El tutorial oficial en https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html usa Java (extremadamente simple) pero puede encontrar bibliotecas de clientes para Perl o cualquier idioma que quieras usar.

Entonces, si más adelante descubre que está realizando operaciones más complejas en archivos de texto de 7000 GB, y que tiene que hacer esto 100 veces al día, puede distribuir la carga de trabajo en varios nodos que aprovisiona o que se aprovisionan automáticamente para usted en la nube. basado en clúster Hadoop.


1
sí, así es. "No use Hadoop: sus datos no son tan grandes" . Este es un problema de E / S de transmisión muy simple.
sourcejedi

0

Todas las sugerencias anteriores requieren leer el archivo completo y escribir todo el archivo. Esto no solo lleva mucho tiempo, sino que también requiere 70 GB de espacio libre.

1) Si entiendo su caso específico correctamente, ¿sería aceptable reemplazar con alguna otra cadena de la MISMA longitud?

2a) ¿Hay múltiples ocurrencias? 2b) Si es así, ¿sabes cuántos?

Estoy seguro de que ya resolvió este problema de más de un año y me gustaría saber qué solución utilizó.

Propondría una solución (muy probablemente en C) que leería los BLOQUES del archivo buscando cada cadena para tener en cuenta el posible cruce de bloques. Una vez encontrado, reemplace la cadena con la MISMA longitud alternativa y escriba solo ese BLOQUE. Continuando por el número conocido de ocurrencias o hasta el final del archivo. Esto requeriría tan pocas escrituras de número de ocurrencias y, como máximo, el doble (si cada ocurrencia se dividiera entre 2 bloques). ¡Esto no requeriría espacio adicional!


-1

Si tenemos una cantidad mínima de <unk>(como lo espera la ley de Zipf),

awk -v RS="<unk>" -v ORS="<raw_unk>" 1

1
No. sedlee una línea a la vez en la memoria independientemente. No podrá ajustarse a esta línea.
Kusalananda

1
No puedo encontrar ninguna documentación que diga otra cosa que GNU sedno hará el almacenamiento en búfer de entrada / salida cuando use este indicador. No puedo ver que leerá líneas parciales.
Kusalananda
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.