Generazione di una stringa di 8 caratteri casuale e univoca utilizzando MySQL


110

Sto lavorando a un gioco che coinvolge i veicoli a un certo punto. Ho una tabella MySQL denominata "veicoli" contenente i dati sui veicoli, inclusa la colonna "targa" che memorizza le targhe dei veicoli.

Ora arriva la parte con cui ho problemi. Devo trovare una targa inutilizzata prima di creare un nuovo veicolo: dovrebbe essere una stringa casuale di 8 caratteri alfanumerici. Il modo in cui ho ottenuto ciò è stato utilizzare un ciclo while in Lua, che è il linguaggio in cui sto programmando, per generare stringhe e interrogare il DB per vedere se viene utilizzato. Tuttavia, poiché il numero di veicoli aumenta, mi aspetto che questo diventi ancora più inefficiente di quanto lo sia adesso. Pertanto, ho deciso di provare a risolvere questo problema utilizzando una query MySQL.

La query di cui ho bisogno dovrebbe semplicemente generare una stringa alfanumerica di 8 caratteri che non è già nella tabella. Ho pensato di nuovo all'approccio del ciclo di generazione e verifica, ma non sto limitando questa domanda a questo solo nel caso in cui ce ne sia uno più efficiente. Sono stato in grado di generare stringhe definendo una stringa contenente tutti i caratteri consentiti e inserendola in modo casuale come sottostringa e nient'altro.

Qualsiasi aiuto è apprezzato.


Quanto devono essere casuali? Se qualcuno riceve una targa in particolare, è importante o no se può calcolare la targa successiva o precedente che hai consegnato?
Damien_The_Unbeliever

@YaK Guarda la mia risposta su come evitare anche la minima possibilità di collisione
Eugen Rieck

Risposte:


87

Questo problema consiste in due sotto-problemi molto diversi:

  • la stringa deve essere apparentemente casuale
  • la stringa deve essere univoca

Mentre la casualità è abbastanza facilmente raggiungibile, l'unicità senza un ciclo di ripetizione non lo è. Questo ci porta a concentrarci prima sull'unicità. L'unicità non casuale può essere banalmente ottenuta con AUTO_INCREMENT. Quindi usare una trasformazione pseudo-casuale che preserva l'unicità andrebbe bene:

  • Hash è stato suggerito da @paul
  • Anche la crittografia AES si adatta
  • Ma ce n'è uno carino: RAND(N)se stesso!

È garantita una sequenza di numeri casuali creati dallo stesso seme

  • riproducibile
  • diverso per le prime 8 iterazioni
  • se il seme è un INT32

Quindi usiamo l'approccio di @ AndreyVolk o di @ GordonLinoff, ma con un seeded RAND :

ad esempio, Assumin idè una AUTO_INCREMENTcolonna:

INSERT INTO vehicles VALUES (blah); -- leaving out the number plate
SELECT @lid:=LAST_INSERT_ID();
UPDATE vehicles SET numberplate=concat(
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@lid)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed:=round(rand(@seed)*4294967296))*36+1, 1),
  substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand(@seed)*36+1, 1)
)
WHERE id=@lid;

Approccio molto interessante, ma probabilmente intendevi RAND(LAST_INSERT_ID()); UPDATE vehicles (...) , rand()*36+1, (...)(altrimenti restituisce 8 volte lo stesso carattere). Come possiamo essere sicuri che 8 chiamate successive a rand()siano garantite per restituire una sequenza diversa se inizializzate con un seme diverso?
RandomSeed

8
Mi stavo chiedendo. Perché usi quei numeri ..4294967296)) * 36 + 1?
Mick

7
Questo è un po 'vecchio, ma vorrei notare che ho dovuto aggiungere FLOOR()intorno ai parametri della seconda sottostringa: in substring('ABC … 789', floor(rand(@seed:= … )*36+1), 1), alcune occasioni, la sottostringa stava cercando di selezionare il carattere 36.9, che quando arrotondato a 37 non avrebbe comportato la scelta di alcun carattere.
Phillip Dodson

4
Non è possibile chiamare una stringa casuale se è riproducibile. E sono anche possibili duplicati, perché stai usando floor(). Questo sqlfiddle mostra che i duplicati vengono creati per tre stringhe di caratteri.
Paul Spiegel

6
@EugenRieck Non capisco come ottieni i tuoi numeri ("prime 2 ^ 32 iterazioni"). Ma ho bisogno solo di un esempio di duplicati per confutare questo concetto. Per gli ID 193844e il 775771tuo algoritmo genererà la stessa stringa T82X711( demo ).
Paul Spiegel

113

Come ho affermato nel mio commento, non mi preoccuperei della probabilità di collisione. Genera semplicemente una stringa casuale e controlla se esiste. In tal caso, riprova e non dovresti aver bisogno di farlo più di un paio di volte a meno che tu non abbia già assegnato un numero enorme di piatti.

Un'altra soluzione per generare una stringa pseudo-casuale lunga 8 caratteri in puro (My) SQL:

SELECT LEFT(UUID(), 8);

Puoi provare quanto segue (pseudo-codice):

DO 
    SELECT LEFT(UUID(), 8) INTO @plate;
    INSERT INTO plates (@plate);
WHILE there_is_a_unique_constraint_violation
-- @plate is your newly assigned plate number

Dato che questo post ha ricevuto un livello di attenzione inaspettato, lasciatemi evidenziare il commento di ADTC : il pezzo di codice sopra è abbastanza stupido e produce cifre sequenziali.

Per una casualità leggermente meno stupida prova invece qualcosa di simile:

SELECT LEFT(MD5(RAND()), 8)

E per una vera casualità (crittograficamente sicura), usa RANDOM_BYTES()invece di RAND()(ma poi prenderei in considerazione di spostare questa logica fino al livello dell'applicazione).


Grazie per la tua soluzione, ho una domanda sull'UUID. quante possibilità che l'id che generi 8 caratteri si ripeta di nuovo.
TR-Ahmed

1
@ user3099183 Ufficialmente , "molto basso". 16 ^ 8 è circa 4 miliardi di possibili stringhe.
RandomSeed

23
Tieni presente che i primi 8 caratteri dell'UUID non sono casuali, ma sequenziali, poiché si basano sul timestamp.
ADTC

Voglio generare una stringa casuale lunga 9 caratteri e, quando uso 9nel codice SELECT LEFT(UUID(), 9);, c'è sempre -alla fine della stringa generata come nono carattere. È costante. Perché?
Martin AJ

3
@MartinAJ perché la stringa è un uuid . Puoi facilmente sostituire i trattini, ad esempioSELECT LEFT(REPLACE(UUID(), '-', ''), 16);
jchook

53

Che ne dici di calcolare l'hash MD5 (o altro) di numeri interi sequenziali, quindi prendere i primi 8 caratteri.

vale a dire

MD5(1) = c4ca4238a0b923820dcc509a6f75849b => c4ca4238
MD5(2) = c81e728d9d4c2f636f067f89cc14862c => c81e728d
MD5(3) = eccbc87e4b5ce2fe28308fd9f2a7baf3 => eccbc87e

eccetera.

avvertimento: non ho idea di quanti potresti allocare prima di una collisione (ma sarebbe un valore noto e costante).

modifica: questa è ora una vecchia risposta, ma l'ho vista di nuovo con il tempo sulle mie mani, quindi, dall'osservazione ...

Probabilità di tutti i numeri = 2,35%

Probabilità di tutte le lettere = 0,05%

Prima collisione quando MD5 (82945) = "7b763dcb ..." (stesso risultato di MD5 (25302))


2
Buona idea, potrei usarlo sulla chiave primaria. Lo terrò a mente per progetti futuri, grazie!
funstein

2
C'è una possibilità che questo possa risultare con solo numeri
Mladen Janjetovic

9
Non è affatto casuale.
paul

1
Potresti renderlo più "casuale" se invece di usare l'id incrementale automatico, usassi il datetime in cui è stato fatto l'inserimento, anch'esso unico.
Javier La Banca

35

Crea una stringa casuale

Ecco una funzione MySQL per creare una stringa casuale di una determinata lunghezza.

DELIMITER $$

CREATE DEFINER=`root`@`%` FUNCTION `RandString`(length SMALLINT(3)) RETURNS varchar(100) CHARSET utf8
begin
    SET @returnStr = '';
    SET @allowedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    SET @i = 0;

    WHILE (@i < length) DO
        SET @returnStr = CONCAT(@returnStr, substring(@allowedChars, FLOOR(RAND() * LENGTH(@allowedChars) + 1), 1));
        SET @i = @i + 1;
    END WHILE;

    RETURN @returnStr;
END

Utilizzo SELECT RANDSTRING(8)per restituire una stringa di 8 caratteri.

Puoi personalizzare il file @allowedChars.

L'unicità non è garantita: come vedrai nei commenti ad altre soluzioni, questo non è possibile. Invece dovrai generare una stringa, controllare se è già in uso e riprovare se lo è.


Controlla se la stringa casuale è già in uso

Se vogliamo mantenere il codice di controllo delle collisioni fuori dall'app, possiamo creare un trigger:

DELIMITER $$

CREATE TRIGGER Vehicle_beforeInsert
  BEFORE INSERT ON `Vehicle`
  FOR EACH ROW
  BEGIN
    SET @vehicleId = 1;
    WHILE (@vehicleId IS NOT NULL) DO 
      SET NEW.plate = RANDSTRING(8);
      SET @vehicleId = (SELECT id FROM `Vehicle` WHERE `plate` = NEW.plate);
    END WHILE;
  END;$$
DELIMITER ;

6
questa dovrebbe essere la risposta accettata, chiara e pertinente, ha funzionato bene per me, grazie @ paddy-mann
Saif

Questa è la soluzione migliore credo. Grazie uomo!
Pronoy999

23

Ecco un modo, utilizzando i caratteri alfabetici come caratteri validi:

select concat(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1),
              substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', rand()*36+1, 1)
             ) as LicensePlaceNumber;

Nota non vi è alcuna garanzia di unicità. Dovrai verificarlo separatamente.


7
usa floor (rand () * 36 + 1) invece. Altrimenti alcuni risultati saranno "brevi".
Fraggle

2
dovrebbero esserci 35 + 1 non 36 + 1! Altrimenti otterrai alcune stringhe con 8 caratteri e alcune con 7
BIOHAZARD

23

Ecco un altro metodo per generare una stringa casuale:

SELECT SUBSTRING(MD5(RAND()) FROM 1 FOR 8) AS myrandomstring


16

Puoi usare le funzioni rand () e char () di MySQL :

select concat( 
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97),
    char(round(rand()*25)+97)
) as name;

14

Puoi generare una stringa alfanumerica casuale con:

lpad(conv(floor(rand()*pow(36,8)), 10, 36), 8, 0);

Puoi usarlo in un BEFORE INSERTtrigger e verificare la presenza di un duplicato in un ciclo while:

CREATE TABLE `vehicles` (
    `plate` CHAR(8) NULL DEFAULT NULL,
    `data` VARCHAR(50) NOT NULL,
    UNIQUE INDEX `plate` (`plate`)
);

DELIMITER //
CREATE TRIGGER `vehicles_before_insert` BEFORE INSERT ON `vehicles`
FOR EACH ROW BEGIN

    declare str_len int default 8;
    declare ready int default 0;
    declare rnd_str text;
    while not ready do
        set rnd_str := lpad(conv(floor(rand()*pow(36,str_len)), 10, 36), str_len, 0);
        if not exists (select * from vehicles where plate = rnd_str) then
            set new.plate = rnd_str;
            set ready := 1;
        end if;
    end while;

END//
DELIMITER ;

Ora inserisci i tuoi dati come

insert into vehicles(col1, col2) values ('value1', 'value2');

E il trigger genererà un valore per la platecolonna.

( demo sqlfiddle )

Funziona in questo modo se la colonna consente NULL. Se vuoi che sia NOT NULL devi definire un valore predefinito

`plate` CHAR(8) NOT NULL DEFAULT 'default',

Puoi anche utilizzare qualsiasi altro algoritmo di generazione di stringhe casuali nel trigger se i caratteri alfanumerici maiuscoli non sono ciò che desideri. Ma il grilletto si prenderà cura dell'unicità.


Sorprendente! Questo è esattamente quello che volevo. Vorrei solo capire come viene convertito in una stringa. Perché c'è un pow, cosa fare, aggiungere numeri e così via. Comunque, grazie.
Akxe

@Akxe conv () può essere utilizzato per convertire un numero in una stringa alfanumerica. pow(36,8)-1è la rappresentazione numerica di ZZZZZZZZ. Quindi generiamo un numero intero casuale tra 0e '36 ^ 8-1 '(da 0a 2821109907455) e lo convertiamo in una stringa alfanumerica tra 0e ZZZZZZZZunsing conv(). lapad () riempirà la stringa di zeri fino a quando non avrà una lunghezza di 8.
Paul Spiegel

Lei, signore, è un genio. Immagino che l'aggiunta di lettere minuscole sia impossibile a causa della non continuità dei caratteri? (91-96) Non che ne abbia bisogno, solo curioso ...
Akxe

@Akxe conv()supporta solo una base fino a 36 (10 cifre + 26 lettere maiuscole). Se desideri includere lettere minuscole, avrai bisogno di un altro modo per convertire un numero in una stringa.
Paul Spiegel

Avvertenza: non funziona per str_len> 13. Da 14 in poi, ottieni sempre "3W5E11264SGSF". ;-)
Gerard H. Pille

6

Per generare una stringa casuale, puoi usare:

SUBSTRING(MD5(RAND()) FROM 1 FOR 8)

Riceverai così:

353E50CC


5

Per una stringa composta da 8 numeri casuali e lettere maiuscole e minuscole, questa è la mia soluzione:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), 8), 8, 0)

Spiegato dall'interno verso l'esterno:

  1. RAND genera un numero casuale compreso tra 0 e 1
  2. MD5 calcola la somma MD5 di (1), 32 caratteri da af e 0-9
  3. UNHEX traduce (2) in 16 byte con valori da 00 a FF
  4. TO_BASE64 codifica (3) come base64, 22 caratteri da az e AZ e 0-9 più "/" e "+", seguito da due "="
  5. i tre REPLACErimuovono i caratteri "/", "+" e "=" da (4)
  6. LEFT prende i primi 8 caratteri da (5), cambia 8 in qualcos'altro se hai bisogno di più o meno caratteri nella tua stringa casuale
  7. LPADinserisce zeri all'inizio di (6) se è lungo meno di 8 caratteri; di nuovo, cambia 8 in qualcos'altro se necessario

Fantastico, esattamente quello che stavo cercando per creare un ID simile a un token in modo nativo in MySQL
rabudde

4

Utilizzo i dati di un'altra colonna per generare un "hash" o una stringa univoca

UPDATE table_name SET column_name = Right( MD5(another_column_with_data), 8 )

4

8 lettere dell'alfabeto - Tutte maiuscole:

UPDATE `tablename` SET `tablename`.`randomstring`= concat(CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25)))CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))),CHAR(FLOOR(65 + (RAND() * 25))));

3

Se non hai un id o un seme, come se fosse per un elenco di valori in insert:

REPLACE(RAND(), '.', '')

2

Soluzione semplice ed efficiente per ottenere una stringa casuale di 10 caratteri con lettere e cifre maiuscole e minuscole:

select substring(base64_encode(md5(rand())) from 1+rand()*4 for 10);

1

Se stai bene con targhe "casuali" ma del tutto prevedibili, puoi utilizzare un registro a scorrimento a feedback lineare per scegliere il numero di targa successivo: è garantito che passi attraverso ogni numero prima di ripetere. Tuttavia, senza una matematica complessa, non sarai in grado di passare attraverso ogni stringa alfanumerica di 8 caratteri (otterrai 2 ^ 41 su 36 ^ 8 (78%) possibili piastre). Per far sì che questo riempia meglio il tuo spazio, potresti escludere una lettera dai piatti (forse O), dandoti il ​​97%.


1

Tenendo conto del numero totale di caratteri richiesto, avresti una minima possibilità di generare due targhe esattamente simili. Quindi potresti probabilmente farla franca generando i numeri in LUA.

Hai 36 ^ 8 diverse targhe univoche (2.821.109.907.456, è molto), anche se avessi già un milione di targhe, avresti una minima possibilità di generarne una che hai già, circa lo 0,000035%

Certo, tutto dipende da quante targhe finirai per creare.


È vero, continuerò a farlo nel gioco vero e proprio invece che in SQL. Grazie mille.
funstein

1

Questa funzione genera una stringa casuale in base alla lunghezza dell'input e ai caratteri consentiti come questo:

SELECT str_rand(8, '23456789abcdefghijkmnpqrstuvwxyz');

codice funzione:

DROP FUNCTION IF EXISTS str_rand;

DELIMITER //

CREATE FUNCTION str_rand(
    u_count INT UNSIGNED,
    v_chars TEXT
)
RETURNS TEXT
NOT DETERMINISTIC
NO SQL
SQL SECURITY INVOKER
COMMENT ''
BEGIN
    DECLARE v_retval TEXT DEFAULT '';
    DECLARE u_pos    INT UNSIGNED;
    DECLARE u        INT UNSIGNED;

    SET u = LENGTH(v_chars);
    WHILE u_count > 0
    DO
      SET u_pos = 1 + FLOOR(RAND() * u);
      SET v_retval = CONCAT(v_retval, MID(v_chars, u_pos, 1));
      SET u_count = u_count - 1;
    END WHILE;

    RETURN v_retval;
END;
//
DELIMITER ;

Questo codice si basa sulla funzione stringa casuale inviata da "Ross Smith II"


Questa funzione non genererà un valore univoco casuale.
Faisal

1

Per creare un alfanumerico casuale di 10 cifre , esclusi i caratteri simili 01oOlI:

LPAD(LEFT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(TO_BASE64(UNHEX(MD5(RAND()))), "/", ""), "+", ""), "=", ""), "O", ""), "l", ""), "I", ""), "1", ""), "0", ""), "o", ""), 10), 10, 0)

Questo è esattamente ciò di cui avevo bisogno per creare un codice voucher . I caratteri confusi vengono rimossi per ridurre gli errori durante la digitazione in un modulo di codice voucher.

Spera che questo aiuti qualcuno, sulla base della brillante risposta di Jan Uhlig .

Si prega di vedere la risposta di Jan per una ripartizione su come funziona questo codice.


0
DELIMITER $$

USE `temp` $$

DROP PROCEDURE IF EXISTS `GenerateUniqueValue`$$

CREATE PROCEDURE `GenerateUniqueValue`(IN tableName VARCHAR(255),IN columnName VARCHAR(255)) 
BEGIN
    DECLARE uniqueValue VARCHAR(8) DEFAULT "";
    WHILE LENGTH(uniqueValue) = 0 DO
        SELECT CONCAT(SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
                SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1)
                ) INTO @newUniqueValue;
        SET @rcount = -1;
        SET @query=CONCAT('SELECT COUNT(*) INTO @rcount FROM  ',tableName,' WHERE ',columnName,'  like ''',@newUniqueValue,'''');
        PREPARE stmt FROM  @query;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    IF @rcount = 0 THEN
            SET uniqueValue = @newUniqueValue ;
        END IF ;
    END WHILE ;
    SELECT uniqueValue;
    END$$

DELIMITER ;

Usa questa procedura memorizzata e usala ogni volta che vuoi

Call GenerateUniqueValue('tableName','columnName')

0

Un modo semplice per generare un numero univoco

set @i = 0;
update vehicles set plate = CONCAT(@i:=@i+1, ROUND(RAND() * 1000)) 
order by rand();


0

Stavo cercando qualcosa di simile e ho deciso di creare una mia versione in cui puoi anche specificare un seme diverso se lo desideri (elenco di caratteri) come parametro:

CREATE FUNCTION `random_string`(length SMALLINT(3), seed VARCHAR(255)) RETURNS varchar(255) CHARSET utf8
    NO SQL
BEGIN
    SET @output = '';

    IF seed IS NULL OR seed = '' THEN SET seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; END IF;

    SET @rnd_multiplier = LENGTH(seed);

    WHILE LENGTH(@output) < length DO
        # Select random character and add to output
        SET @output = CONCAT(@output, SUBSTRING(seed, RAND() * (@rnd_multiplier + 1), 1));
    END WHILE;

    RETURN @output;
END

Può essere utilizzato come:

SELECT random_string(10, '')

Che userebbe il seme incorporato di caratteri maiuscoli e minuscoli + cifre. NULL sarebbe anche valore invece di "".

Ma si potrebbe specificare un seme personalizzato durante la chiamata:

SELECT random_string(10, '1234')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.