Vérification de numéros de Cartes Bleues, RIB (IBAN) et NIRPP (sécu)

Ce petit notebook va implémenter les algorithmes de vérification des numéros de :

  • cartes bleues, sur $4\times4$ chiffres, avec un chiffre de vérification,
  • RIB (identifiant de compte, sur les IBAN), avec deux chiffres de vérifications,
  • NIRPP (numéro de sécurité sociale en France), avec deux chiffres de vérifications.

J'avais déjà implementé les deux derniers, cf. ces scripts : check_IBAN.py, et check_NIRPP.py.

Si vous êtes curieux de l'aspect historique, ce petit article explique très bien les origines de ces chiffres de contrôles et de la formule de Luhn.

Je vais utiliser cette fonction plusieurs fois, qui permet de transformer une lettre 'A',...,'Z' en entier.

In [1]:
def l_to_c(l):
    try:
        return str(int(l))
    except ValueError:
        return str(10 + ord(l.upper()) - ord('A'))
In [2]:
for l in 'ABCDEFGHIJKLMNROPQRSTUVWXYZ':
    print("l = {} --> c = {}".format(l, l_to_c(l)))
l = A --> c = 10
l = B --> c = 11
l = C --> c = 12
l = D --> c = 13
l = E --> c = 14
l = F --> c = 15
l = G --> c = 16
l = H --> c = 17
l = I --> c = 18
l = J --> c = 19
l = K --> c = 20
l = L --> c = 21
l = M --> c = 22
l = N --> c = 23
l = R --> c = 27
l = O --> c = 24
l = P --> c = 25
l = Q --> c = 26
l = R --> c = 27
l = S --> c = 28
l = T --> c = 29
l = U --> c = 30
l = V --> c = 31
l = W --> c = 32
l = X --> c = 33
l = Y --> c = 34
l = Z --> c = 35
In [3]:
exemple_cb = "4970 1012 3456 7890"  # pas valide

D'abord l'algorithme de Luhn :

In [4]:
def verifie_Luhn(numeros):
    numeros = numeros.replace(' ', '')
    nb_chiffres = len(numeros)
    parite = nb_chiffres % 2
    chiffres = [int(l_to_c(l)) for l in numeros]
    somme = chiffres[nb_chiffres - 1]
    for i in range(nb_chiffres - 2, -1, -1):
        chiffre = chiffres[i]
        if i % 2 == parite:
            chiffre *= 2
            if chiffre > 9:
                chiffre -= 9
        somme += chiffre
    return (somme % 10) == 0
In [5]:
verifie_Luhn('972 487 086')
Out[5]:
True
In [6]:
verifie_Luhn('972 487 081')
verifie_Luhn('972 487 082')
verifie_Luhn('972 487 087')
Out[6]:
False
Out[6]:
False
Out[6]:
False
In [7]:
verifie_Luhn(exemple_cb)
Out[7]:
False

Ensuite la vérification pour un numéro de carte bleue :

In [8]:
def verifie_cb(cb):
    print("\nVérification du numéro de CB '%s'..." % cb)
    check = verifie_Luhn(cb)
    if check:
        print("OK '%s' semble être un numéro de CB valide." % cb)
    else:
        print("[ATTENTION] PAS OK '%s' semble ne pas être un numéro de CB valide!" % cb)
    return check

Exemples

In [9]:
verifie_cb(exemple_cb)
Vérification du numéro de CB '4970 1012 3456 7890'...
[ATTENTION] PAS OK '4970 1012 3456 7890' semble ne pas être un numéro de CB valide!
Out[9]:
False

Avec un autre faux numéro mais conçu pour être vrai :

In [10]:
verifie_cb("4976 5301 7218 3533")
Vérification du numéro de CB '4976 5301 7218 3533'...
OK '4976 5301 7218 3533' semble être un numéro de CB valide.
Out[10]:
True
In [11]:
exemple_iban = "FR76 1254 8029 9838 3759 0150 071"
In [12]:
def verifie_iban(iban):
    print("\nVérification du nombre IBAN '%s'..." % iban)
    ib = iban.replace(' ', '')
    ib = ib[4:] + ib[:4]
    print("  De longueur", len(ib))
    i = int(''.join(l_to_c(l) for l in ib))
    check = (i % 97) == 1
    if check:
        print("OK '%s' semble être un nombre IBAN valide." % iban)
    else:
        print("[ATTENTION] PAS OK '%s' semble ne pas être un nombre IBAN valide!" % iban)
    return check

Exemples

Compte français

In [13]:
verifie_iban(exemple_iban)
Vérification du nombre IBAN 'FR76 1254 8029 9838 3759 0150 071'...
  De longueur 27
OK 'FR76 1254 8029 9838 3759 0150 071' semble être un nombre IBAN valide.
Out[13]:
True

Compte anglais

In [14]:
verifie_iban("GB87 BARC 2065 8244 9716 55")
Vérification du nombre IBAN 'GB87 BARC 2065 8244 9716 55'...
  De longueur 22
OK 'GB87 BARC 2065 8244 9716 55' semble être un nombre IBAN valide.
Out[14]:
True
In [15]:
verifie_iban("GB87 BARC 2065 8244 9716 51")
Vérification du nombre IBAN 'GB87 BARC 2065 8244 9716 51'...
  De longueur 22
[ATTENTION] PAS OK 'GB87 BARC 2065 8244 9716 51' semble ne pas être un nombre IBAN valide!
Out[15]:
False

Compte belge

In [16]:
verifie_iban("BE43 0689 9999 9501")
Vérification du nombre IBAN 'BE43 0689 9999 9501'...
  De longueur 16
OK 'BE43 0689 9999 9501' semble être un nombre IBAN valide.
Out[16]:
True
In [17]:
verifie_iban("BE43 0689 9999 9500")
Vérification du nombre IBAN 'BE43 0689 9999 9500'...
  De longueur 16
[ATTENTION] PAS OK 'BE43 0689 9999 9500' semble ne pas être un nombre IBAN valide!
Out[17]:
False
In [18]:
exemple_nirpp = "2 69 05 49 588 157 80"
In [19]:
length_checksum = 2

def verifie_nirpp(nirpp, length_checksum=length_checksum):
    print("\nVérification du nombre NIRPP '%s' ..." % nirpp)
    ib = nirpp.replace(' ', '')
    checksum = int(ib[-length_checksum:])
    ib = ib[:-length_checksum]
    print("  De longueur", len(ib))
    num_nirpp = int(''.join(l_to_c(l) for l in ib))
    print("  De somme de contrôle num_nirpp =", num_nirpp)
    print("  Module à 97 =", (97 - (num_nirpp % 97)))
    print("  Et la somme de contrôle attendue était", checksum)
    check = (97 - (num_nirpp % 97)) == checksum
    if check:
        print("OK '%s' semble être un nombre NIRPP valide." % nirpp)
    else:
        print("[ATTENTION] PAS OK '%s' semble ne pas être un nombre NIRPP valide!" % nirpp)
    return check

Exemples

In [20]:
verifie_nirpp(exemple_nirpp)
Vérification du nombre NIRPP '2 69 05 49 588 157 80' ...
  De longueur 13
  De somme de contrôle num_nirpp = 2690549588157
  Module à 97 = 80
  Et la somme de contrôle attendue était 80
OK '2 69 05 49 588 157 80' semble être un nombre NIRPP valide.
Out[20]:
True

Bonus : affichage d'un NIRPP

Il suffit de récupérer les informations de chaque morceau du code NIRPP, et les stocker comme ça :

In [21]:
information_nirpp = {
    (0, 1): {
        "meaning": "sexe",
        "mapping": {
            "1": "homme",
            "2": "femme",
            "3": "personne étrangère de sexe masculin en cours d'immatriculation en France",
            "4": "personne étrangère de sexe féminin en cours d'immatriculation en France"
        }
    },
    (1, 2): {
        "meaning": "deux derniers chiffres de l'année de naissance",
        "mapping": {
            # DONE nothing to do for this information
        }
    },
    (3, 2): {
        "meaning": "mois de naissance",
        "mapping": {
            "01": "janvier",
            "02": "février",
            "03": "mars",
            "04": "avril",
            "05": "mai",
            "06": "juin",
            "07": "juillet",
            "08": "août",
            "09": "septembre",
            "10": "octobre",
            "11": "novembre",
            "12": "décembre",
        }
    },
    # Only case A : TODO implement case B and C
    (5, 2): {
        "meaning": "département de naissance métropolitain",
        "mapping": {  # Cf. http://www.insee.fr/fr/methodes/nomenclatures/cog/documentation.asp
            "01": "Ain",
            "02": "Aisne",
            "03": "Allier",
            "04": "Alpes-de-Haute-Provence",
            "05": "Hautes-Alpes",
            "06": "Alpes-Maritimes",
            "07": "Ardèche",
            "08": "Ardennes",
            "09": "Ariège",
            "10": "Aube",
            "11": "Aude",
            "12": "Aveyron",
            "13": "Bouches-du-Rhône",
            "14": "Calvados",
            "15": "Cantal",
            "16": "Charente",
            "17": "Charente-Maritime",
            "18": "Cher",
            "19": "Corrèze",
            "2A": "Corse-du-Sud",
            "2B": "Haute-Corse",
            "21": "Côte-d'Or",
            "22": "Côtes-d'Armor",
            "23": "Creuse",
            "24": "Dordogne",
            "25": "Doubs",
            "26": "Drôme",
            "27": "Eure",
            "28": "Eure-et-Loir",
            "29": "Finistère",
            "30": "Gard",
            "31": "Haute-Garonne",
            "32": "Gers",
            "33": "Gironde",
            "34": "Hérault",
            "35": "Ille-et-Vilaine",
            "36": "Indre",
            "37": "Indre-et-Loire",
            "38": "Isère",
            "39": "Jura",
            "40": "Landes",
            "41": "Loir-et-Cher",
            "42": "Loire",
            "43": "Haute-Loire",
            "44": "Loire-Atlantique",
            "45": "Loiret",
            "46": "Lot",
            "47": "Lot-et-Garonne",
            "48": "Lozère",
            "49": "Maine-et-Loire",
            "50": "Manche",
            "51": "Marne",
            "52": "Haute-Marne",
            "53": "Mayenne",
            "54": "Meurthe-et-Moselle",
            "55": "Meuse",
            "56": "Morbihan",
            "57": "Moselle",
            "58": "Nièvre",
            "59": "Nord",
            "60": "Oise",
            "61": "Orne",
            "62": "Pas-de-Calais",
            "63": "Puy-de-Dôme",
            "64": "Pyrénées-Atlantiques",
            "65": "Hautes-Pyrénées",
            "66": "Pyrénées-Orientales",
            "67": "Bas-Rhin",
            "68": "Haut-Rhin",
            "69": "Rhône",
            "70": "Haute-Saône",
            "71": "Saône-et-Loire",
            "72": "Sarthe",
            "73": "Savoie",
            "74": "Haute-Savoie",
            "75": "Paris",
            "76": "Seine-Maritime",
            "77": "Seine-et-Marne",
            "78": "Yvelines",
            "79": "Deux-Sèvres",
            "80": "Somme",
            "81": "Tarn",
            "82": "Tarn-et-Garonne",
            "83": "Var",
            "84": "Vaucluse",
            "85": "Vendée",
            "86": "Vienne",
            "87": "Haute-Vienne",
            "88": "Vosges",
            "89": "Yonne",
            "90": "Territoire de Belfort",
            "91": "Essonne",
            "92": "Hauts-de-Seine",
            "93": "Seine-Saint-Denis",
            "94": "Val-de-Marne",
            "95": "Val-d'Oise",
            # TODO support these too
            "971": "Guadeloupe",
            "972": "Martinique",
            "973": "Guyane",
            "974": "La Réunion",
            "975": "Saint-Pierre-et-Miquelon",
            "976": "Mayotte",
            "977": "Saint-Barthélemy",
            "978": "Saint-Martin",
            "984": "Terres australes et antarctiques françaises",
            "986": "Wallis-et-Futuna",
            "987": "Polynésie française",
            "988": "Nouvelle-Calédonie",
            "989": "Île de Clipperton"
        }
    },
    (7, 3): {
        "meaning": "code officiel de la commune de naissance",
        "mapping": {  # TODO
        }
    },
    (10, 3): {
        "meaning": "numéro d’ordre de la naissance dans le mois et la commune (ou le pays)",
        "mapping": {
            # DONE nothing to do for this information
        }
    }
}

Pour les villes, on a besoin d'une base de donnée plus grande. J'ai récupéré ce fichier sur le site de l'INSEE (lien mort).

In [22]:
!ls data/
comsimp2016.txt		 Exemple_CB.jpg    Exemple_RIB.jpg
Exemple_CarteVitale.jpg  Exemple_IMEI.jpg
In [23]:
!wc data/comsimp2016.txt
  35886   40952 1685861 data/comsimp2016.txt

Il ressemble à ça :

In [24]:
!head data/comsimp2016.txt
CDC,CHEFLIEU,REG,DEP,COM,AR,CT,TNCC,ARTMAJ,NCC,ARTMIN,NCCENR
0,0,84,01,001,2,08,5,(L') ABERGEMENT-CLEMENCIAT,(L') Abergement-Clémenciat
0,0,84,01,002,1,01,5,(L') ABERGEMENT-DE-VAREY,(L') Abergement-de-Varey
0,1,84,01,004,1,01,1,AMBERIEU-EN-BUGEY,Ambérieu-en-Bugey
0,0,84,01,005,2,22,1,AMBERIEUX-EN-DOMBES,Ambérieux-en-Dombes
0,0,84,01,006,1,04,1,AMBLEON,Ambléon
0,0,84,01,007,1,01,1,AMBRONAY,Ambronay
0,0,84,01,008,1,01,1,AMBUTRIX,Ambutrix
0,0,84,01,009,1,04,1,ANDERT-ET-CONDON,Andert-et-Condon
0,0,84,01,010,1,10,1,ANGLEFORT,Anglefort

Briançon est bien dans la liste :

In [25]:
!grep "BRIANCON" data/comsimp2016.txt
1,2,93,05,023,1,98,0,BRIANCON,Briançon
0,0,93,06,024,1,11,0,BRIANCONNET,Briançonnet

Allons-y :

In [26]:
import subprocess
length_checksum = 2


def pprint_nirpp(nirpp, length_checksum=length_checksum):
    print("\nAffichage d'informations contenues dans le numéro NIRPP '%s' ..." % nirpp)
    nirpp = nirpp.replace(' ', '')
    ib = nirpp[:-length_checksum]
    # Printing
    for (i, l) in sorted(information_nirpp):
        n = nirpp[i: i + l]
        info = information_nirpp[(i, l)]
        if n in info["mapping"]:
            explain = "\"{}\"".format(info["mapping"][n])
        else:
            explain = n
        # For towns, durty hack to extract the town from the INSEE database
        if i == 7:
            try:
                args = [
                    "grep", "--", "',{},{},'".format(
                        nirpp[5: 5 + 2],
                        nirpp[7: 7 + 3]
                    ),
                    "data/comsimp2016.txt",
                    "|", "cut", "-d,", "-f10"
                ]
                command = ' '.join(args)
                # print("Executing subprocess.check_output to \"{}\"".format(command))
                explain = subprocess.check_output(command, shell=True)
                explain = explain[:-1].decode()
                # print("explain =", explain)
                explain = "{} (code {})".format(explain, nirpp[7: 7 + 3])
            except Exception as e:
                # print("e =", e)
                explain = n
        print("- Le nombre '{}' (indice {}:{}) signifie:\n\t\"{}\" : \t{}".format(
            n, i, i + l, info["meaning"], explain)
        )
In [27]:
pprint_nirpp(exemple_nirpp)
Affichage d'informations contenues dans le numéro NIRPP '2 69 05 49 588 157 80' ...
- Le nombre '2' (indice 0:1) signifie:
	"sexe" : 	"femme"
- Le nombre '69' (indice 1:3) signifie:
	"deux derniers chiffres de l'année de naissance" : 	69
- Le nombre '05' (indice 3:5) signifie:
	"mois de naissance" : 	"mai"
- Le nombre '49' (indice 5:7) signifie:
	"département de naissance métropolitain" : 	"Maine-et-Loire"
- Le nombre '588' (indice 7:10) signifie:
	"code officiel de la commune de naissance" : 	 (code 588)
- Le nombre '157' (indice 10:13) signifie:
	"numéro d’ordre de la naissance dans le mois et la commune (ou le pays)" : 	157

Avec un exemple assez proche de mon numéro de sécurité sociale (modifié) :

In [28]:
pprint_nirpp("1 93 01 05 023 122 23")
Affichage d'informations contenues dans le numéro NIRPP '1 93 01 05 023 122 23' ...
- Le nombre '1' (indice 0:1) signifie:
	"sexe" : 	"homme"
- Le nombre '93' (indice 1:3) signifie:
	"deux derniers chiffres de l'année de naissance" : 	93
- Le nombre '01' (indice 3:5) signifie:
	"mois de naissance" : 	"janvier"
- Le nombre '05' (indice 5:7) signifie:
	"département de naissance métropolitain" : 	"Hautes-Alpes"
- Le nombre '023' (indice 7:10) signifie:
	"code officiel de la commune de naissance" : 	Briançon (code 023)
- Le nombre '122' (indice 10:13) signifie:
	"numéro d’ordre de la naissance dans le mois et la commune (ou le pays)" : 	122

IMEI

Les numéros d'identification des téléphones portables (les IMEI) terminent aussi par un chiffre de contrôle, qui utilise aussi la formule de Luhn. Je termine ce notebook en implémentant aussi cette vérification.

In [29]:
exemple_imei = "448674 52 897641 0"  # avant 2014, 6-2-6-1
In [30]:
def verifie_imei(imei):
    print("\nVérification du numéro IMEI '%s'..." % imei)
    check = verifie_Luhn(imei)
    if check:
        print("OK '%s' semble être un numéro IMEI valide." % imei)
    else:
        print("[ATTENTION] PAS OK '%s' semble ne pas être un numéro IMEI valide!" % imei)
    return check
In [32]:
verifie_imei(exemple_imei)
Vérification du numéro IMEI '448674528976410'...
OK '448674528976410' semble être un numéro IMEI valide.
Out[32]:
True

Exemples

In [33]:
exemple_imei = "448674 52 897641 1"
In [34]:
verifie_imei(exemple_imei)
Vérification du numéro IMEI '448674528976411'...
[ATTENTION] PAS OK '448674528976411' semble ne pas être un numéro IMEI valide!
Out[34]:
False
In [35]:
exemple_imei = "468674 52 897641 0"
In [36]:
verifie_imei(exemple_imei)
Vérification du numéro IMEI '468674528976410'...
[ATTENTION] PAS OK '468674528976410' semble ne pas être un numéro IMEI valide!
Out[36]:
False

Avec un IMEI semblable à celui d'un de mes anciens téléphones :

In [39]:
mon_faux_imei_1 = "35569508 262195 2"

verifie_imei(mon_faux_imei_1)
Vérification du numéro IMEI '35569508 262195 2'...
OK '35569508 262195 2' semble être un numéro IMEI valide.
Out[39]:
True
In [40]:
mon_faux_imei_2 = "35569508 283295 5"

verifie_imei(mon_faux_imei_2)
Vérification du numéro IMEI '35569508 283295 5'...
OK '35569508 283295 5' semble être un numéro IMEI valide.
Out[40]:
True

Conclusion

Voilà, c'est tout pour aujourd'hui !

Allez lire ici pour voir mes autres notebooks.