email – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Pardon aux familles, tout ça http://sametmax.com/pardon-aux-familles-tout-ca/ http://sametmax.com/pardon-aux-familles-tout-ca/#comments Tue, 12 Mar 2013 10:08:57 +0000 http://sametmax.com/?p=5387 Voulant faire de la place pour la blockchain de Bitcoin qui prend 6Go (la salope), j’ai fais du tri dans les emails.

Par tri j’entends que j’ai accidentellement effacé tous les mails de la boîte aux lettre de Sam Et Max. L’IMAP est taquin.

Donc, à tous ceux qui nous ont écrit dernièrement, renvoyez-les nous, car on ne peut pas vous répondre.

Désolé. C’est not’ fot’. Nan. C’est MA faute.

Je pense particulièrement aux mails de questions et le très gentil mail disant qu’on était trop cool mais que le tableau des niveaux de log était pas dans le bon ordre.

Ce que je n’ai pas compris d’ailleurs car :

>>> logging.DEBUG
10
>>> logging.INFO
20
>>> logging.WARNING
30
>>> logging.ERROR
40
>>> logging.CRITICAL
50

Bref, tout ça pour dire que ce n’est pas qu’on veut pas vous répondre, c’est qu’on peut pas. Renvoyez tout !

Gif animé d'un enfant qui saute dans une piscine et la rate

Et il fonce vers un disque dur avec plus de place et un monde moins de souci. Il s'approche. Il sauuuuuuuuuuuuuuuuute !

]]>
http://sametmax.com/pardon-aux-familles-tout-ca/feed/ 8 5387
Envoi d’un email par logging en cas de plantage d’un script python (ou: comment faire bouffer u”\xe9″ à SMTPHandler) http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/ http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/#comments Tue, 12 Mar 2013 08:48:23 +0000 http://sametmax.com/?p=5313 (Ceci est un post invité d’un débutant pour les débutants… sous licence creative common 3.0 unported.)

Il y a peu, je me suis mis à utiliser logging pour debugger mes scripts de débutant.

Si comme moi vous avez l’habitude de mettre des print partout pour trouver l’origine d’un problème, et qu’ensuite vous avez passé de longues longues longues minutes/heures à traquer ces foutus print pour dépolluer la sortie console, jetez un oeil à écrire des logs en python.

Après quelques (minutes/heures/jours) de prise en main (vite, quoi…), on se demande comment on a fait pour s’en passer si longtemps. C’est simple, je n’utilise plus les touches “p”, “r”, “i”, “n” et “t” de mon clavier. Elles sont toutes propres.

En plus, logging m’a ouvert de nouvelles perspectives, parmi lesquelles la possibilité d’envoyer les logs par mail. Pas besoin de tout recevoir par mail, mais si ce foutu script de m%@rde pouvait m’envoyer un email quand il plante avec la source détaillé du problème, ce serait super.

Log post mortem par email

Admettons que vous vouliez vérifier que les pensées qui vous traversent l’esprit sont safe for work. Vous écrivez un script génial qui fait le boulot.

Comme vous commencez à savoir y faire en python, vous avez même écrit votre propre exception à vous tout seul…

class NotSafeForWorkError(Exception):
    """
    Exception soulevée si une pensée est NSFW
    """
    def __init__(self, msg):
        self.msg = u"Danger! %s est NSFW." % msg

    def __str__(self):
        return self.msg.encode("utf-8")

# liste des pensées proscrites
# (échantillon, elle est beaucoup plus longue que ça en réalité)
NSFW = ["cul", "seins", "sametmax"]

# boucle de censure qui soulève une exception si une pensée déconne
for pensee in ["pause", "pipi", "sametmax"]:
    if pensee in NSFW:
        raise NotSafeForWorkError(pensee)
    print u"%s est SFW" % pensee

#sortie:
## pause est SFW
## pipi est SFW
## Traceback (most recent call last):
##   File "censure_setm3.py", line 30, in
##     raise NotSafeForWorkError(pensee)
## __main__.NotSafeForWorkError: Danger! sametmax est NSFW.

Ça marche du tonnerre! C’est là que vous vous dites que ce serait bien si le script envoyait le traceback par email à votre psy pour le prévenir que vous avez déconné.

A la maison, vous avez lu l’article de Sam sur les logs post mortem. Ça a l’air facile à faire.

Vous créez d’abord un logger qui enverra les logs de niveau critique par mail:

import logging
from logging.handlers import SMTPHandler

nom_loggeur = "test_nsfw"

# On crée un logger et on met le niveau à critique:
#  il ne tiendra compte que des logs de ce niveau
logger = logging.getLogger(nom_loggeur)
logger.setLevel(logging.CRITICAL)

# On crée le handler en lui passant les paramètres
#  nécessaires à l'envoi d'un email
mail_handler = SMTPHandler(
    # Host et port
    ('SMTP.GMAIL.COM', 587),
    # From
    "MOI@GMAIL.COM",
    # To (liste)
    ["QUELQU.UN@QUELQUE.PART"],
    # Sujet du message
    "Erreur critique dans %s" % nom_loggeur,
    # pour l'authentification
    credentials = ("MONEMAIL@GMAIL.COM", "MONSUPERPASSWORD"),
    secure = ())

# On met le handler à "critique".
# Il enverra donc par mail les messages de ce niveau
mail_handler.setLevel(logging.CRITICAL)

# On définit un formatter: date, nom du logger, niveau, message
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# On associe le formatter au handler:
#  c'est lui qui formatera les logs de ce handler
mail_handler.setFormatter(formatter)

# ... et on associe le handler au logger:
#  il utilisera ce handler, qui émettra les logs critiques
logger.addHandler(mail_handler)

Ensuite vous redéfinissez sys.excepthook de manière à ce que l’exception soit logguée. La fonction convertit le traceback en string, la loggue au niveau critique et laisse l’exception continuer son chemin.

import sys

# la fonction qui remplacera sys.excepthook
def en_cas_de_plantage(type_except, value, tb):

    # Traceback permettra de formater l'exception.
    import traceback

    # Mise en forme de l'exception. Retourne la trace
    #  sous forme de str avec numéros de lignes et tout
    trace = "".join(traceback.format_exception(type_except, value, tb))

    # On loggue l'exception au niveau "critique",
    #  elle sera donc envoyée par email
    logger.critical(u"Erreur inattendue:\n%s", trace)

    # ... et on laisse le script se planter...
    sys.__excepthook__(type_except, value, tb)

    # on remplace sys.excepthook, et le tour est joué
    sys.excepthook = en_cas_de_plantage

Et voilà. Vous lancez le script, il se casse la gueule dès qu’il rencontre “sametmax”, votre psy reçoit un email avec le traceback, c’est cool.

Sauf que…

SMTPHandler ne gère pas unicode

Si au moment de la création de mail_handler, vous passez comme sujet à SMTPHandler:

u"%s s'est planté" % nom_loggeur

ou que dans votre fonction “en_cas_de_plantage” vous mettez:

logger.critical(u"Bébé a encore glissé dans son caca:\n%s", trace)

… autrement dit si vous avez une chaîne unicode qui ne peut pas être encodée en ascii, ça va pas marcher:

Traceback (most recent call last):
  File "envoie_mail_on_crash.py", line 84, in emit
    smtp.sendmail(self.fromaddr, self.toaddrs, msg)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 734, in sendmail
    (code, resp) = self.data(msg)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 501, in data
    self.send(q)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 321, in send
    self.sock.sendall(str)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ssl.py", line 229, in sendall
    v = self.send(data[count:])
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ssl.py", line 198, in send
    v = self._sslobj.write(data)
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 62: ordinal not in range(128)
Logged from file envoie_mail_on_crash.py, line 163

D’où l’on déduit qu’il y a une tentative foirée d’encodage d’une chaîne unicode en ascii par la méthode “write” de ce mystérieux _sslobj, et que c’est une méthode “emit” quelque part dans handlers.py qui lui a passé la chaîne.

Fuck! Investiguons. Et essayons de régler ça par le haut de la pile.

Toi, là au fond! Oui, toi!

C’est la methode emit de SMTPHandler qui est responsable. Elle fait quoi cette méthode? Elle formate le “record”, crée un message et l’envoie à l’adresse mail spécifiée. Voilà le code:

def emit(self, record):
    """
    Emit a record.

    Format the record and send it to the specified addressees.
    """
    try:
        import smtplib
        from email.utils import formatdate
        port = self.mailport
        if not port:
            port = smtplib.SMTP_PORT
        smtp = smtplib.SMTP(self.mailhost, port)

        # C'est ici l'objet "record" est formatté
        msg = self.format(record)

        # Le message est construit ici. Il sera ensuite envoyé par smtp.sendmail
        # C'est là que le bât blesse
        msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
                        self.fromaddr,
                        ",".join(self.toaddrs),
                        # Notons ce getSubject(record). La méthode renvoie
                        #  le sujet du message, passé à la création
                        #  de mail_handler
                        self.getSubject(record),
                        formatdate(),
                        # le record formaté
                        msg)

        if self.username:
            if self.secure is not None:
                smtp.ehlo()
                smtp.starttls(*self.secure)
                smtp.ehlo()
            smtp.login(self.username, self.password)

        # Le mail et envoyé
        smtp.sendmail(self.fromaddr, self.toaddrs, msg)

        smtp.quit()
    except (KeyboardInterrupt, SystemExit):
        raise
    except:
        self.handleError(record)

Qu’est-ce qu’il se passe?

Après quelques tests genre:

    msg = self.format(record)
    # C'est pratique parfois un petit print
    print type(msg)

##

… on voit que la méthode format (qui structure votre log selon la forme passée à logging.Formatter) retourne un objet de type unicode si le message que vous avez loggué (logger.critical(u"% foiré!", trace)) contient de l’unicode.

Idem pour getSubject(record) qui, en l’état, ne fait que retourner le sujet du email tel que vous l’avez passé à l’instanciation de SMTPHandler.

Donc, quand msg est formaté, si le log ou le message sont en unicode, msg sera en unicode.

WTF!?

J’ai été surpris quand j’ai fini par piger comment ça fonctionne. Même pour un débutant comme moi ça a l’air bête.

J’y connais rien en email, mais il ne m’a pas fallu longtemps pour comprendre que, dans sa forme basique, un email c’est du ascii, point. Et que si on veut envoyer autre chose que du ascii, qui soit décodable de l’autre côté, il faut qu’il y ait les headers appropriés, que le contenu soit encodé, que sais-je encore…

Peut-être un truc programmé par des gars qui ne manipulent jamais autre chose que de l’anglais? Ils auraient essayé d’envoyer un mail en russes ou en suédois, ça se serait planté au premier test. Oui, oui, Sam, la lingua franca, tout ça…

Ou alors c’est pour des questions de compatibilité avec des versions antérieures de python qui ne contiendraient pas les ressources nécessaires pour formater correctement un mail? Aucune idée…

Comment régler ça?

Il mangea u”\xe9″ et il en redemanda

Le module email de la lib standard permet de créer des emails. On trouve dans email.mime une classe MIMEText qui nous conviendra parfaitement. On va donc subclasser SMTPHandler et réécrire la méthode emit pour qu’elle construise un message “RFC-compliant”, comme ils disent dans la doc.

class SMTPHandler_unicode(SMTPHandler):

    def emit(self, record):
        try:
            import smtplib
            from email.utils import formatdate

            # On importe MIMEText
            from email.mime.text import MIMEText

            port = self.mailport
            if not port:
                port = smtplib.SMTP_PORT # 25
            smtp = smtplib.SMTP(self.mailhost, port)

            msg = self.format(record)

            # Au moment de la création de l'objet par MIMEText,
            #  si msg est de type unicode, il sera encodé
            #  selon _charset, sinon il sera laissé tel quel
            message = MIMEText(msg, _charset = "utf-8")

            # On ajoute les headers nécessaires. S'il sont de type unicode,
            #  ils seront encodés selon _charset
            message.add_header("Subject", self.getSubject(record))
            message.add_header("From", self.fromaddr)
            message.add_header("To", ",".join(self.toaddrs))
            message.add_header("Date", formatdate())

            if self.username:
                if self.secure is not None:
                    smtp.ehlo()
                    smtp.starttls(*self.secure)
                    smtp.ehlo()
                smtp.login(self.username, self.password)

            # Envoi du message proprement encodé
            smtp.sendmail(self.fromaddr, self.toaddrs, message.as_string())

            smtp.quit()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

Ce qui est intéressant avec MIMEText c’est que si vous déclarez un encodage (_charset = "utf-8"), et que vous passez une chaîne unicode a l’instanciation, il l’encodera selon _charset.

Idem si vous ajoutez un header en appelant add_header, ce qui permet dans notre cas de traiter correctement le sujet du mail.

L’appel à la méthode to_string retournera le message proprement encodé (en base64) pour le transfert, et les headers appropriés seront inclus.

Voilà, il ne reste plus qu’à utiliser cette classe pour créer mail_handler, et le tour est joué.

mail_handler = SMTPHandler_unicode(
    # host et port
    ('SMTP.GMAIL.COM', 587),
    # From
    "MOI@GMAIL.COM",
    # To (liste)s
    ["QUELQU.UN@QUELQUE.PART"],
    # sujet du message
    u"Bonjour Mr Freud. %s a encore repéré des pensées génantes" % nom_loggeur,
    # pour l'authentification
    credentials = ("MONEMAIL@GMAIL.COM", "MONSUPERPASSWORD"),
    secure = () )

Bilan de l’histoire: c’est en débutant qu’on devient débutant…


Télécharger le code de l’article.

]]>
http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/feed/ 9 5313
Spamgourmet, mon amour http://sametmax.com/spamgourmet-mon-amour/ http://sametmax.com/spamgourmet-mon-amour/#comments Fri, 01 Mar 2013 09:55:34 +0000 http://sametmax.com/?p=2640 Spamgourmet, que j’ai découvert grace à Sebsauvage, est un service gratuit et fiable d’alias d’emails. Vous donnez votre email à ce service, et vous pouvez ensuite distribuer des alias de cet email aux autres sites.

Exemple:

Je m’inscris sur Spamgourmet avec l’adresse contact@sametmax.com et le nom d’utilisateur sam.

Si un autre site me demande une adresse, au lieu de lui donner mon email, je lui un alias comme ça:

mot.un_numero.nom_utilisateur@spamgourmet.com

En alias je met souvent le nom du site, donc ça donne un truc comme ça:

sncf.5.sam@spamgourmet.com

Vous n’avez rien besoin de faire pour créer cet alias. Il suffit de l’imaginer dans votre tête, et qu’il respecte la structure “mot.un_numero.nom_utilisateur@spamgourmet”.

Bref, vous donnez cet email imaginaire à tous les sites qui vous en demande un.

Quand spamgourmet va recevoir un email, il va envoyer une copie de cet email à votre adresse, la vraie, qu’il a retrouvé grâce au nom d’utilisateur (sncf.5.sam@spamgourmet.com).

Mais il va faire un truc en plus : il va regarder le numéro dans l’email (sncf.5.sam@spamgourmet.com). Si vous avez reçu plus de mails que le numéro, donc ici plus de 5 mails, sur cette adresse, spamgourmet ne vous enverra plus la copie.

C’est une version plus puissante des alias avec “+”.

Attention, le nombre max qu’on peut donner est 20. Mais si vous en voulez plus, alors c’est qu’il est temps de mettre votre véritable adresse email. Généralement je vais dans le formulaire de profile du site en question et change mon adresse pour la bonne à ce moment là, ou avec un alias en “+”.

]]>
http://sametmax.com/spamgourmet-mon-amour/feed/ 11 2640
Quelqu’un peut-il m’expliquer cet étrange phénomène mailistique ? http://sametmax.com/quelquun-peut-il-mexpliquer-cet-etrange-phenomene-mailistique/ http://sametmax.com/quelquun-peut-il-mexpliquer-cet-etrange-phenomene-mailistique/#comments Fri, 15 Feb 2013 10:08:52 +0000 http://sametmax.com/?p=4556 J’ai une adresse email en nomprenom@gmail.com (hé oui, il m’en reste quelques unes… Il faut dire que j’ai 7 emails, sans compter les alias, les antispam et les comptes bidons).

Par erreur, je m’inscris sur un service avec nom.prenom@gmail.com. Notez le point, que mon adresse n’a PAS. Qu’elle n’a jamais eu. Je reçois régulièrement des mails à cette adresse SANS le point.

Malgré cela, je reçois le mail de confirmation d’inscription à nomprenom@gmail.com (la bonne, sans le point).

Dans le message, il y a clairement :

has received a request to create a user account using your email address (nom.prenom@gmail.com).

Je check les sources:

Delivered-To: nom.prenom@gmail.com
Received: by IP_ADDRESS with SMTP id u10csp168283wja;
        Thu, 14 Feb 2013 05:48:57 -0800 (PST)
X-Received: by IP_ADDRESS with SMTP id xl7mr4158572pbc.167.1360849736470;
        Thu, 14 Feb 2013 05:48:56 -0800 (PST)
Return-Path: <register@service.org>
Received: from subdomaine.service.org (vbox17.service.org. [IP_ADDRESS])
        by mx.google.com with ESMTP id k8si8969185pax.IP_ADDRESS.05.48.55;
        Thu, 14 Feb 2013 05:48:56 -0800 (PST)
Received-SPF: pass (google.com: domain of register@service.org designates IP_ADDRESS as permitted sender) client-ip=IP_ADDRESS;
Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of register@service.org designates IP_ADDRESS as permitted sender) smtp.mail=register@service.org
Received: from localhost (localhost.localdomain [IP_ADDRESS])
    by subdomaine.service.org (Postfix) with ESMTP id 94EBC769B5
    for <nom.prenom@gmail.com>; Thu, 14 Feb 2013 13:48:55 +0000 (UTC)
X-Virus-Scanned: by amavisd-new at service.org
Received: from subdomaine.service.org ([IP_ADDRESS])
    by localhost (subdomaine.service.org [IP_ADDRESS]) (amavisd-new, port 10024)
    with ESMTP id FybIuo+abpEQ for <nom.prenom@gmail.com>;
    Thu, 14 Feb 2013 13:48:55 +0000 (UTC)
Received: from register-web.service.org (register-web-back [IP_ADDRESS])
    by subdomaine.service.org (Postfix) with ESMTP id 7A92976993
    for <nom.prenom@gmail.com>; Thu, 14 Feb 2013 13:48:55 +0000 (UTC)
Received: by register-web.service.org (Postfix, from userid 502)
    id 7695FEE8F3; Thu, 14 Feb 2013 13:48:54 +0000 (UTC)
From: register@service.org
To: nom.prenom@gmail.com

Donc l’email a bien été envoyé à l’adresse AVEC le point.

WTF ?

 

]]>
http://sametmax.com/quelquun-peut-il-mexpliquer-cet-etrange-phenomene-mailistique/feed/ 35 4556
Arf, on avait laissé l’ancien formulaire de contact http://sametmax.com/arf-on-avait-laisse-lancien-formulaire-de-contact/ Thu, 11 Oct 2012 15:53:48 +0000 http://sametmax.com/?p=2553 Le lien vers l’ancien formulaire de contact était toujours accessible, et du coup on a reçu un mail depuis celui-ci, alors qu’on ne peut pas y répondre.

C’est bon, on l’a viré.

En attendant, voici le mail, et la réponse (je strip l’intro et la conclusion.):

Je viens de lire le post sur virtualenv+django et je me disais qu’il existe un moyen différent, oserais-je dire plus propre (pas sur la tête pitié …), de faire quasiment la même chose.

Je m’explique : je vois dans le script l’utilisation de workon, je ne me rappelle plus trop mais je crois que cette commande est amenée par virtualenvwrapper. Dans ce cas là, je voulais vous faire partager le contenu de quelques fichiers contenu dans mon dossier .virtualenvs.

Tout d’abord le fichier postactivate (hook fourni par virtualenvwrapper s’insérant après l’initialisation de l’environnement) :

PROJECT_PATH="$PROJECT_HOME"/"$env_name"

if [ -a "$PROJECT_PATH" ] ;then
    cd "$PROJECT_PATH"
fi

ce petit script permet de faire ce que tu décris dans le post, si jamais ton dossier contenant le projet ne se trouve pas à l’endroit défini par la variable d’environnement $PROJECT_HOME, il est possible d’overrider son comportement en modifiant PROJECT_PATH par le chemin que tu souhaite dans le fichier .virtualenvs/nom_de_l’environnement/bin/postactivate

Voili voilou et sinon pour encore me faire mousser (mais pas trop hein), je voulais juste vous faire partager un autre script que je place dans postmkproject :

echo "Create a django project y/n ?"
while true
do
    read INPUT
    case "$INPUT" in
        "y" )
            cd "$PROJECT_HOME"
            rm -r "$envname"
            pip install django
            django-admin.py startproject "$envname"
            cd "$envname"
            break
            ;;
        "n" )
            break
            ;;
    esac
done

En clair, ça demande à l’utilisateur si son projet sera un projet django, ça télécharge le framework via pip et cela créé directement le projet dans notre dossier PROJECT_HOME, en évitant d’avoir à créer un autre sous-dossier rien que pour notre application django.

Bonjour l’ami,

Merci de l’info. Comme tu vois, on l’a fait passer. Quand tu as un truc comme ça à mettre, utilise les commentaires, ça en fait profiter tout le monde !

@+

]]>
2553
Valider une adresse email avec une regex en Python http://sametmax.com/valider-une-adresse-email-avec-une-regex-en-python/ http://sametmax.com/valider-une-adresse-email-avec-une-regex-en-python/#comments Tue, 06 Mar 2012 23:55:21 +0000 http://sametmax.com/?p=253 la norme qui définit le format des adresses emails est vraiment tordue.]]> Problème de tous les jours, et on a tous essayé de concocter notre solution maison. Mais il se trouve que la norme qui définit le format des adresses emails est vraiment tordue.

Si on est trop rigide, on interdit les cas contre-intuitifs comme le signe ‘+’ dans le destinataire, le domaine “.museum”, l’adresse IP dans l’email ou le sous domaine comportant un seul caractère.

Si on est trop flexible, l’utilisateur va rentrer truc@chose et ne pas s’apercevoir de sa faute de frappe. Il oubliera alors votre site, sans email pour le contacter.

Il existe une version bullet proof de regex qui valide une adresse email, mais c’est un truc de malade.

Au final, on veut juste attraper 99% des cas les plus courrants, un compromis en solidité et praticité. Le code source de Django nous fournit justement une solution équilibrée dans le fichier core/validators.py:

>>> email_re = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
>>> email_re.search('test+12@moi.n.museum')
<_sre.SRE_Match object at 0x1205160>
>>> email_re.search('test@moi')
>>>

Par contre, ça ne gère pas l’adresse IP en tant que nom de domaine, mais c’est le plus souvent suffisant.

]]>
http://sametmax.com/valider-une-adresse-email-avec-une-regex-en-python/feed/ 2 253