Les captchas sont pénibles

Les spammers le sont aussi, pénibles, clairement, tout comme les 0.1% de débiles qui cliquent sur les liens pourris justifiant leur existence.

Les spammers le sont aussi, pénibles, clairement, tout comme les 0.1% de débiles qui cliquent sur les liens pourris justifiant leur existence. D'ailleurs, je ne sais pas à quel point les spams dans les commentaires arrivent à gruger l'indexation de Google. Mais j'insiste, les captchas sont pénibles.

Parmi les alternatives proposées, il existe les captchas en mode texte, qui évitent de se casser les yeux.

C'est gentil, mais ça ne protège pas grand-chose. C'est même très pédagogique de proposer ce genre de challenge. J'en ai pris un que j'ai vu passer, qui propose des devinettes mathématiques, niveau fin de primaire. "Un + _ = 3", ce genre de challenge. Voilà, un four à micro-ondes a plus de CPU qu'il n'en a fallu pour envoyer un homme sur la Lune, et mon ordinateur n'arriverait pas à répondre à ce genre de question ?

Bon, comme chaque fois que l'on pense avoir découvert quelque chose sur Internet, quelqu'un l'a déjà fait avant, mieux, et plus joli. De toute façon, vu le temps que ça a pris pour résoudre le puzzle, le mérite est faible. Ce n'est pas avec ce genre de code que l'on peut postuler à la NSA.

Pour rester dans le pédagogique, le code est en Python, et s'appuie sur des bibliothèques tierces éprouvées, pour traiter les soucis d'intendances. Request pour gérer la discussion HTTP avec sa victime. Pyquery pour lire le HTML. On peut faire autrement et souffrir. De plus, ce sont deux paquets Debian.

#!/usr/bin/env python
#encoding: utf8

import requests
import re
from pyquery import PyQuery as pq
import sys

Lire des nombres en toutes lettres est un peu laborieux, mais tout à fait faisable. De toute façon le captcha a un peu la flemme, il parle de "soixante-dix deux", plutôt que l'élégant soixante-douze.

SPACES = re.compile("\s+")
NUMBERS = u"zéro un deux trois quatre cinq six sept huit neuf dix onze douze treize quatorze quinze seize dix-sept dix-huit dix-neuf vingt".split(u" ")
TENS = u"dix vingt trente quarante cinquante soixante soixante-dix quatre-vingt quatre-vingt-dix cent".split(u" ")
OPERATORS = u"+ − ×".split(u" ")
ACTION = { u"+": "+",
          u"−": "-",
          u"×": "*"}
INVERSE = { u"+": "-",
          u"−": "+",
          u"×": "/"}

def maybe_int(stuff):
    if stuff.isdigit():
        return int(stuff)
    if stuff in TENS:
        return (TENS.index(stuff) + 1) * 10
    if stuff in NUMBERS:
        return NUMBERS.index(stuff)
    return stuff

On teste 5 fois l'URL, passée comme argument, on vérifie que le serveur répond bien.

for i in range(5):
    r = requests.get(sys.argv[1])
    assert r.status_code == 200

Pyquery est un hommage à Jquery, et il va aller chercher le bloc dont la class est "cptch_block"

    d = pq(r.text)

    bloc = d('.cptch_block')

Maintenant, un peu de NLP, natural language processing. Les phrases sont découpées en jetons (en mots, quoi), et on essaye d'en faire des nombres. Les opérateurs sont en UTF8, pour faire plus joli.

Comme j'ai eu la flemme de traiter la position du "input" HTML, je bataille pour gérer le trou dans le motif "A opérateur B = C".

 
    poz = 0
    poz_operator = None
    poz_equal = None
    operator = None
    values = []
    tokens = SPACES.split(bloc.text())
    previous_is_int = False
    for token in tokens:
        token = maybe_int(token)
        if type(token) == int:
            if previous_is_int:
                values[-1] += token
            else:
                values.append(token)
        elif token in OPERATORS:
            poz_operator = poz
            operator = token
        elif token == u"=":
            poz_equal = poz
        else:
            raise Exception("Unmanaged token: %s" % token)
        previous_is_int = type(token) == int
        poz += 1
Il faut maintenant faire un peu de math. Coup de bol, python sait traiter des opérations simples, et eval a ici un usage légitime. "eval is evil"™ L'autre personne ayant fait un code similaire résoud le problème mathématique en brut force, il essaye de remplacer le trou par un nombre entre 1 et 1000, jusqu'à ce que ça passe.
    answer = None
    if poz_equal == 3:  # 4 * 2 = X
        answer = eval("%i %s %i" % (values[0], ACTION[operator], values[1]))
    elif poz_operator == 0:  # x * 2 = 4
        answer = eval("%i %s %i" % (values[1], INVERSE[operator], values[0]))
    elif poz_operator == 1:  # 2 * x = 4
        if operator == u"−":
            answer = eval("%i - %i" % (values[0], values[1]))
        else:
            answer = eval("%i %s %i" % (values[1], INVERSE[operator], values[0]))
    else:
        raise Exception()
    print u" ".join(tokens), " => ", answer

Ensuite, il faut faire la partie qui remplit le formulaire et qui poste des messages à caractères publicitaires. Mais j'ai la flemme, et je n'ai pas grand-chose à spammer, en fait.

Le résultat va ressembler à ça :

− deux = 4  =>  6
+ sept = 9  =>  2
neuf + = 14  =>  5
neuf − 6 =  =>  3
+ huit = 12  =>  4