Comment valider un ISBN ? Voilà une question digne d’une première année de cours d’informatique. À tel point qu’elle m’a rappelé mes premiers travaux avec le schéma TEI, les bidouilles de scripts PHP pour préparer les fichiers avant d’utiliser l’affreux Lexico 3, et bien sûr ma découverte du langage Python. Je n’avais pas utilisé l’opération modulo depuis cette époque — or elle est indispensable à la validation d’un ISBN.

Le « numéro international normalisé du livre » identifie chaque édition d’un ouvrage de manière unique et immuable. C’est un rouage essentiel de BookFinder, la machine de Rube Goldberg qui me permet de maintenir mes fiches de lecture sur métro[biblio]dodo, puisqu’il me permet de retrouver les informations d’une édition spécifique dans la catalogue de l’Open Library. Je ne risque pas de rentrer un ISBN erroné, et cela ne risquerait pas de causer de problèmes insurmontables, mais c’est un défi intéressant.

D’abord parce qu’il faut prendre en compte deux formats : le format ISBN-10 à dix caractères, normalisé en 1970, et le format ISBN-13 à treize chiffres, standardisé en 2007 pour assurer la compatibilité avec les identifiants GTIN-13 et les codes EAN 13. Les deux ou trois premiers chiffres d’un code EAN 13 sont censés correspondre au pays de provenance du produit, mais les ISBN-13 commencent par 978 ou 9791, les codes du « pays des livres ».

Les numéros sont divisés en plusieurs éléments :

Ces éléments devraient être séparés par un tiret, à la limite une espace, mais ne le sont pas toujours. J’ai croisé des numéros tous collés, d’autres avec des tirets placés au petit bonheur la chance, et certains franchement farfelus. Avant toute chose, mieux vaut donc nettoyer les codes fournis :

isbn = isbn.replace("-", "").replace(" ", "")

Il ne suffit pas de retirer 978 ou 979 pour transformer un ISBN-13 en ISBN-10. Cette manipulation change forcément la clé de contrôle, et surtout, les ISBN-13/979 inaugurent de nouveaux domaines de chalandise2 qui cassent définitivement la rétrocompatibilité avec les ISBN-10. Mieux vaut donc distinguer les deux formats :

if len(isbn) == 10:
    # C’est un ISBN-10
elif len(isbn) == 13:
    # C’est un ISBN-13
else:
    raise ValueError("Vous n’avez pas entré un ISBN valide.")

Dans un cas comme dans l’autre, il faut retirer la clé de contrôle… que l’on cherche à vérifier :

digits = list(isbn)[:-1]

Le format ISBN-10 reprend la méthode de contrôle des anciens SBN britanniques : chaque chiffre est multiplié par un poids descendant depuis dix, la somme des produits est divisée par onze, et le reste est retranché de onze. La fameuse opération modulo simplifie considérablement l’écriture :

sum = 0
weight = 10 # Le poids descend depuis dix

for digit in digits:
    sum += weight*int(digit) # Le chiffre est multiplié par son poids
    weight -= 1 # Puis le poids est réduit pour le prochain chiffre

check = 11 - (sum%11)

La clé est X si le résultat est égal à dix, ou 0 si le résultat est égal à onze :

if check == 10:
    check = "X"

if check == 11:
    check = 0

Le format ISBN-13 reprend la méthode de contrôle des EAN européens : les chiffres en position paire sont pris tels quels, les chiffres en position impaire sont multipliés par trois, et la somme de l’ensemble est divisée par dix. Le reste est retranché de dix :

sum = 0
position = 0

for digit in digits:
    if (position%2 == 0): # Si a mod 2 = 0, alors a est pair
        sum += int(digit)
    else: # Les chiffres en position impaire sont multipliés par trois
        sum += 3*int(digit)
    position += 1

check = 10 - (sum%10)

La clé est 0 si le résultat est égal à dix :

if check == 10:
    check = "0"

Ne reste plus qu’à comparer la clé calculée avec la clé fournie :

if isbn[-1] == str(check):
    print(isbn + " est un ISBN valide.")
    return isbn
else:
    raise ValueError(isbn + " n’est pas un ISBN valide.")

Ce petit exercice m’a rappelé l’importance de considérer l’environnement d’exécution du code Python. Lorsqu’il est interprété directement, le script est exécuté dans l’environnement d’exécution principal, nommé __main__. Une fonction spécifique peut ainsi être appelée dans ce contexte :

if __name__ == '__main__':
    main()

Cette fonction peut renfermer les instructions nécessaires à l’analyse des arguments passés en ligne de commande :

def main():
    parser = argparse.ArgumentParser(description="Checks whether an ISBN-10 or ISBN-13 is valid or not.")
    parser.add_argument("isbn", help="An ISBN-10 or ISBN-13.")
	
    args = parser.parse_args()
    isbn = clean_isbn(args.isbn)
    isbn = check_isbn(isbn)
	
    return isbn

Une autre fonction peut être employée lorsque le script est importé sous le forme d’un module :

def ISBNChecker(isbn):
    isbn = clean_isbn(isbn)
    isbn = check_isbn(isbn)
    
    return isbn

Ce qui est le cas dans BookFinder :

from ISBNChecker.ISBNChecker import ISBNChecker

isbn = ISBNChecker(args.isbn)

Le script complet et documenté peut être consulté sur mon espace Github.


  1. Les codes 978 et 979 n’étaient pas attribués, et ont donc été réservés aux livres. La pénurie de codes commençant par 978 n’est pas impossible, et les agences nationales de numérotation attribuent de plus en plus régulièrement des codes commençant par 979. ↩︎

  2. Comme 10 pour le domaine français. MacGeneration possède ainsi des ISBN-13 commençant par 979-10. ↩︎