Sam & Max: Python, Django, Git et du cul » Programmation http://sametmax.com Deux développeurs en vadrouille qui se sortent les doigts du code Wed, 05 Feb 2014 14:20:37 +0000 en hourly 1 http://wordpress.org/?v=3.3.1 Virtualenv EnvironmentError: mysql_config not found sous MAC OSX http://sametmax.com/virtualenv-environmenterror-mysql_config-not-found-sous-mac-osx/ http://sametmax.com/virtualenv-environmenterror-mysql_config-not-found-sous-mac-osx/#comments Tue, 04 Feb 2014 10:56:18 +0000 Max http://sametmax.com/?p=8963 Comme le post précédent est inutile j’en fait un autre qui doit concerner au moins 0.00001% de la population.

A vous qui essayez depuis 2 heures de configurer MySQL-python sous votre virtualenv, cet article vous est dédié.

Cette erreur vicieuse m’arrive à chaque fois que j’installe MySQL-python dans un nouvel environnement virtuel. Ceci dit vous l’aurez peut-être sous un autre OS. Sur centos j’avais qu’à faire un yum install mysql-devel mais sous Mac c’est autre chose. Plus de puissance engendre plus de responsabilités…

 
Max $  pip install MySQL-python
Downloading/unpacking MySQL-python
  Downloading MySQL-python-1.2.5.zip (108Kb): 108Kb downloaded
  Running setup.py egg_info for package MySQL-python
    sh: mysql_config: command not found
    Traceback (most recent call last):
      File "<string>", line 14, in <module>
      File "/Users/Max/.virtualenvs/zooscator/build/MySQL-python/setup.py", line 17, in <module>
        metadata, options = get_config()
      File "setup_posix.py", line 43, in get_config
        libs = mysql_config("libs_r")
      File "setup_posix.py", line 25, in mysql_config
        raise EnvironmentError("%s not found" % (mysql_config.path,))
    EnvironmentError: mysql_config not found
    Complete output from command python setup.py egg_info:
    sh: mysql_config: command not found
 
Traceback (most recent call last):
 
  File "<string>", line 14, in <module>
 
  File "/Users/Max/.virtualenvs/zooscator/build/MySQL-python/setup.py", line 17, in <module>
 
    metadata, options = get_config()
 
  File "setup_posix.py", line 43, in get_config
 
    libs = mysql_config("libs_r")
 
  File "setup_posix.py", line 25, in mysql_config
 
    raise EnvironmentError("%s not found" % (mysql_config.path,))
 
EnvironmentError: mysql_config not found
 
----------------------------------------
Command python setup.py egg_info failed with error code 1
Storing complete log in /Users/Max/.pip/pip.log

Il faut éditer votre fichier activate qui se trouve dans le répertoire bin de votre environnement virtuel: ex “/Users/Max/.virtualenvs/zooscator/bin/activate”

vi /Users/Max/.virtualenvs/zooscator/bin

Trouvez les lignes suivantes et ajoutez:

PATH=”$PATH:/opt/local/lib/mysql5/bin/”

...
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
PATH="$PATH:/opt/local/lib/mysql5/bin/"
export PATH
...

Chez moi le chemin vers mysql est /opt/local/lib/mysql5/bin/, si vous ne connaissez pas le vôtre utilisez la commande locate: ex locate mysql

Une fois le fichier activate édité il faut le resourcer:

source /Users/Max/.virtualenvs/zooscator/bin/activate

Et vous pourrez alors installer MySQL-python tranquillou billou:

Max $  pip install MySQL-python
 
Downloading/unpacking MySQL-python
  Running setup.py egg_info for package MySQL-python
 
Installing collected packages: MySQL-python
  Running setup.py install for MySQL-python
[....................]
Successfully installed MySQL-python
Cleaning up...

Je vais nourir mon cochon d’Inde.

flattr this!

]]>
http://sametmax.com/virtualenv-environmenterror-mysql_config-not-found-sous-mac-osx/feed/ 14
Les ventes aux enchères publiques http://sametmax.com/les-ventes-aux-encheres-publiques/ http://sametmax.com/les-ventes-aux-encheres-publiques/#comments Mon, 03 Feb 2014 17:05:01 +0000 Sam http://sametmax.com/?p=8950 Petite digression de la ligne éditoriale, billet d’humeur, des fois il faut écrire ce qui sort.

Quand j’étais gosse mon père m’avait trainé à des ventes aux enchères, et c’était une vraie corvée : rester assis sur place pendant des heures pour un môme, c’est complètement con (pensez à l’école…) et mon pouvoir d’achat était négligeable. En plus on ne pouvait même pas toucher les objets.

Mais en grandissant, le premier cheveux blanc arrive, on se met à aimer les cornichons et les vides greniers. Et on fini par retourner aux enchères.

Pour ceux qui ne savent pas, il existe ce qu’on appelle des enchères publiques, dans lesquelles des personnes physiques ou morales, souvent endettées, parfois carrément en faillite, vendent leurs biens. On y trouve aussi quantité d’objets non réclamés à la poste. L’avantage de la chose, c’est qu’on trouve des objets de toutes sortes de domaines, potentiellement à un tiers du prix conseillé et possiblement neufs.

Ça se passe ainsi : on se pointe à l’exposition, dans laquelle on a le droit de regarder les choses qui vont être en vente. Ça peut aller de l’écran plat au four à pain de boulanger en passant par une caravane, des bijoux, du matériel d’escalade et une collection de cartes pokémon.

En générale quand on arrive, on sait où regarder car maintenant, magie de l’internet, on a un listing en ligne des biens vendus des jours avant la vente sur les sites des commissaires priseurs ou des OVV. Dans tous les cas, on peut obtenir la liste imprimée sur place, mais certaines salles le facturent une somme symbolique.

Tout le monde peut se pointer, c’est publique, et tout le monde se pointe. Une enchère est socialement très transversale : vous avez des vieux, des jeunes, des jaunes, des blancs, des chauves, des moustachus, des pros, des amateurs, des élégants, des dépareillés… Mais le profile type est un homme de plus de 35 ans.

Dans beaucoup de salles, on a pas le droit d’approcher les objets, exposés derrière des barrières, et groupés par lots numérotés. Quand on est intéressé par un lot, on le signale à un assistant qui va vous le chercher (sauf si c’est un bateau, évidement) pour que vous l’examiniez sous ses yeux.

Au bout d’un temps prévu à l’avance, on va donner un chèque en blanc et une pièce d’identité pour récupérer un numéro d’enchérisseur sur un petit panneau. Ceci dit si vous ne le faites pas, on vous laisse souvent enchérir quand même à main levée, et on vous donne le panneau à la première enchère remportée.

Ensuite on s’assoie, un mec avec un costume datant du temps de votre grand père monte sur un podium avec un marteau et répète à tout le monde une bonne 30aine de fois qu’il lui faut du silence. Il recommencera tout au long de la vente, c’est une tradition.

Là, l’équipe commencent à présenter les lots, on annonce le prix de départ et chacun lève joyeusement son petit panneau ou sa mimine pour enchérir. Pour des petits lots, on enchérie par tranches de 2, 5, 10, 20 ou 50 euros. La taille des tranche peut augmenter au fur et à mesure que l’enchère augmente. Si vous enchérissez sur un bien bien plus cher, l’enchère peut monter par tranche de 100, 500, 1000… Si vous êtes dans le domaine du luxe, les tranches sont un mois ou un an de salaire pour vous.

Si il n’y a pas d’acheteur, le commissaire priseur va proposer un prix à la baisse. Hé oui, la mise à prix peut diminuer.

La vente peut durer des heures. Ma dernière vente a duré 3h, avec une 1h d’expo, 1/2h pour y aller, 1/2h pour en revenir, soit 5h monopolisées en plein lundi. Tout le monde ne peut pas se le permettre. Heureusement aujourd’hui, on peut enchérir en ligne, sans avoir à se déplacer. Même si personnellement je préfère voir les lots en direct, car c’est dur de se faire une idée.

Histoire que vous puissiez évaluer le contenu d’une vente, voici un extrait (il y avait des centaines de lots, ce ne sont quelques uns) de ce qui est parti à la dernière.

Lot Prix de départ Adjugé pour Prix frais compris Prix en magasin Notes
Carton de 100 clés USB 40 426 511,2 ? Je n’ai pas vérifié la taille des clés
PC ASUS TAICHI 30 300 600 720 1207 Ce genre de matos part assez cher, mais notez que c’est du neuf. Presque 500€ d’économisé sur l’achat d’un portable de ce genre, ce n’est pas rien.
Carton de 80 montres fantasies 100 220 264 ? « fantasie » = casio, diesel, etc.
Acheté par un mec fashion qui voulait sans doute une montre par jour à
porter.
2 bouteilles de Chivas 25 ans d’age ? 210 252 508
20 bouteilles de liqueurs 100 100 120 ? Le lot est vraiment parti pour rien, ça fait
partie de ces « coups de bol » car ça valait 20 fois ce prix
si ont souhaite le vendre en magasin.
12 bouteilles apéro 50 100 120 ? Type « vodka, manzana, pastis, cointreau,
etc ». Pour un BDE, c’est intéressant.
Parfum Chanel N°5, 200ml 50 115 138 177 Ce genre de produit part à des prix bien moins intéressant. Les acheteurs à petit budgets sont généralement
les moins bons en enchères, ils “tentent le coup” sur un produit coup de coeur et montent les prix beaucoup trop haut, sans stratégie d’achat.
Guitar Electrique XP + Guitar
accoustique STOL
80 80 96 Même si les instruments ne sont pas de qualités
incroyables, c’est bien plus rentable de les acheter ici. Surtout que
beaucoup de gens abandonnent la musique en cours de route. Il y a aussi quelques pièces de très grande qualité. Deux luth sont partis à 1300 euros.
Violon Taille Sebim taille ½ 50 60 72 ? ?
Fusil Sniper Airsoft MB01 40 65 78 175 Ce genre de matos ultra spécialisé est rare, et
du coup il y a moins de connaisseurs et on peut tomber sur des lots très sympas.

Notez que le prix sur lequel on enchérie n’est pas celui qu’on paie. C’est la grande enculade des ventes aux enchères, puisque ces frais additionnels doivent être calculés de tête en permanence. Et plein de gens ne le font pas, transformant votre enchère “bonne affaire” en “achat au supermarcher”. Mémé a gagné, elle vous regarde en souriant victorieusement, et vous êtes dégoutés car vous avez raté un super prix, et elle a acheté plus cher que le prix sur le bon coin parce qu’elle est nul en calcul mental. C’est très chiant, mais c’est la jeu ma pauv’ lucette.

flattr this!

]]>
http://sametmax.com/les-ventes-aux-encheres-publiques/feed/ 6
Python a le don d’Ubiquité : Multiprocessing http://sametmax.com/python-ubiuite-multiprocessing/ http://sametmax.com/python-ubiuite-multiprocessing/#comments Sun, 02 Feb 2014 16:41:23 +0000 foxmask http://sametmax.com/?p=8939

Ceci est un post invité de Foxmask posté sous licence creative common 3.0 unported.

Tout récemment j’ai voulu donner un coup de fouet à mon script de traitement de Trigger Happy (que je fais tourner sur ma “raspberry pi” parce que chuis un w4rl0rdZ:P) que j’estimais être trop long dans ses traitements de données.

Actuellement avec Trigger Happy, j’ai 30 sources de données (essentiellement des flux rss), que je parcours, et quand un nouvel item arrive, je l’envoi à pétaouchnock (Evernote ;) Le tout prend 7min, soit 14secondes par source. La loose totale.

Voici le corps du délit :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import datetime
import time
 
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_th.settings")
from django_th.services import default_provider
from django_th.models import TriggerService
from django.utils.log import getLogger
 
# create logger
logger = getLogger('django_th.trigger_happy')
 
def go():
    """
        run the main process
    """
    trigger = TriggerService.objects.filter(status=True)
    if trigger:
        for service in trigger:
[...]
    else:
        print "No trigger set by any user"
 
 
def main():
    default_provider.load_services()
    # let's go
    go()
 
if __name__ == "__main__":
 
    main()

Mais avant que je ne me penche sur le code du script pour l’améliorer, je me suis dit que plutôt que de chercher à corriger un problème, autant chercher la source de celui-ci d’abord (normal hein).

Un HTOP m’a révélé :

  1. que le CPU était à 100% tout le temps, que le script tourne ou pas
  2. quye la raison était double : rabittmq et celery…

Une fois shootés ces derniers, tout va pour le mieux :P
Je ne dis pas que ceux ci sont de la merde, mais que, pour mon cas, la crontab se suffit à elle-même.

Donc une fois désinstallés c’est 2 (sur)consommateurs de ressources, je relance le script pour tomber à un temps de traitement à 5mn

2014-02-02 14:40:51,693 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Sam et Max nothing new
2014-02-02 14:40:53,865 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News de Numerama nothing new
2014-02-02 14:40:56,013 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de La Ferme du Web nothing new
2014-02-02 14:41:01,005 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - flux de Paulds nothing new
2014-02-02 14:41:20,098 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux d'un Odieux Connard nothing new
2014-02-02 14:41:22,142 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Strict minimum nothing new
2014-02-02 14:41:25,868 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Humeurs Illustrées nothing new
2014-02-02 14:41:33,497 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux du Journalisme Total nothing new
2014-02-02 14:41:35,658 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Kernel Panic nothing new
2014-02-02 14:41:44,897 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de AngularJS nothing new
2014-02-02 14:41:49,016 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Odeon nothing new
2014-02-02 14:41:54,186 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Nicolargo nothing new
2014-02-02 14:42:12,525 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de JEEK nothing new
2014-02-02 14:42:21,349 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Recher nothing new
2014-02-02 14:42:31,266 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Un blog d'adminsys Libres nothing new
2014-02-02 14:42:35,824 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - flux de Le bloc-notes de Gee nothing new
2014-02-02 14:42:36,647 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de mere code (atom) nothing new
2014-02-02 14:42:39,616 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Alex Mac Caw nothing new
2014-02-02 14:42:42,985 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Yearofmoo Articles nothing new
2014-02-02 14:43:42,732 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Novapost nothing new
2014-02-02 14:43:46,722 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Les Numériques nothing new
2014-02-02 14:43:58,303 INFO fire 5142 date 2014-02-02 13:00:00 >= date triggered 2014-02-02 09:02:36 title Test du Quechua Phone 5, le smartphone des montagnards ?
2014-02-02 14:44:08,010 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Frandroid = 1 new data
2014-02-02 14:44:17,624 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de TechCrunch Mobile nothing new
2014-02-02 14:44:20,339 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux Django annonces nothing new
2014-02-02 14:44:20,744 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - La Hyène - Python nothing new
2014-02-02 14:44:24,237 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News de PCInpact nothing new
2014-02-02 14:44:29,055 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - PointGPhone nothing new
2014-02-02 14:44:31,299 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - HumanCoders Python nothing new
2014-02-02 14:44:52,751 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - HauteFeuille Lab (python) nothing new
2014-02-02 14:44:58,850 INFO fire 5142 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Python Plone Planet nothing new

Comme je suis un éternel insatisfait de bibi, j’ai cherché des moyens un peu partout, jusqu’à ce que Sam me souffle une suggestion ;)

A présent donc une version modifiée pour exploiter le multiprocessing est la suivante :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import datetime
import time
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "th.settings")
 
from django_th.services import default_provider
from django_th.models import TriggerService
from django.utils.log import getLogger
 
# create logger
logger = getLogger('django_th.trigger_happy')
 
def go(service):
    """
        run the main process
    """
    [...]
 
 
def main():
    default_provider.load_services()
    # let's go
    trigger = TriggerService.objects.filter(status=True)
    if trigger:
        from multiprocessing import Pool
        pool = Pool(processes=5)
        result = pool.map(go, trigger)
    else:
        print "No trigger set by any user"
 
if __name__ == "__main__":
 
    main()

fait tomber le temps de traitement à … 1min …:

$ date && ./fire.sh && date 
dimanche 2 février 2014, 14:58:38 (UTC+0100)
2014-02-02 14:58:48,221 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de La Ferme du Web nothing new
2014-02-02 14:58:48,243 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Humeurs Illustrées nothing new
2014-02-02 14:58:48,256 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Kernel Panic nothing new
2014-02-02 14:58:48,283 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Sam et Max nothing new
2014-02-02 14:58:48,907 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux d'un Odieux Connard nothing new
2014-02-02 14:58:49,267 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - flux de Paulds nothing new
2014-02-02 14:58:49,446 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux du Journalisme Total nothing new
2014-02-02 14:58:49,713 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News de Numerama nothing new
2014-02-02 14:58:49,847 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Strict minimum nothing new
2014-02-02 14:58:50,209 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Odeon nothing new
2014-02-02 14:58:50,353 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de AngularJS nothing new
2014-02-02 14:58:50,830 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Un blog d'adminsys Libres nothing new
2014-02-02 14:58:51,338 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de JEEK nothing new
2014-02-02 14:58:51,396 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Nicolargo nothing new
2014-02-02 14:58:51,476 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Yearofmoo Articles nothing new
2014-02-02 14:58:51,735 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - flux de Le bloc-notes de Gee nothing new
2014-02-02 14:58:52,148 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de mere code (atom) nothing new
2014-02-02 14:58:52,640 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Recher nothing new
2014-02-02 14:58:52,971 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Alex Mac Caw nothing new
2014-02-02 14:58:53,416 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Les Numériques nothing new
2014-02-02 14:58:53,474 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de TechCrunch Mobile nothing new
2014-02-02 14:58:53,870 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Novapost nothing new
2014-02-02 14:58:54,072 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - PointGPhone nothing new
2014-02-02 14:58:54,316 INFO fire 5333 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux Django annonces nothing new
2014-02-02 14:58:54,853 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - La Hyène - Python nothing new
2014-02-02 14:58:55,111 INFO fire 5335 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - HumanCoders Python nothing new
2014-02-02 14:58:55,222 INFO fire 5334 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News Frandroid nothing new
2014-02-02 14:58:55,380 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - HauteFeuille Lab (python) nothing new
2014-02-02 14:58:55,696 INFO fire 5336 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - News de PCInpact nothing new
2014-02-02 14:59:02,214 INFO fire 5337 user: foxmask - provider: ServiceRss - consummer: ServiceEvernote - Flux de Python Plone Planet nothing new
dimanche 2 février 2014, 14:59:02 (UTC+0100)

Comme on l’aura remarqué la différence entre les 2 versions est l’appel fait à a fonction go

avant :

def main():
    default_provider.load_services()
    # let's go
    go()

après :

def main():
    default_provider.load_services()
    # let's go
    trigger = TriggerService.objects.filter(status=True)
    if trigger:
        from multiprocessing import Pool
        pool = Pool(processes=5)
        result = pool.map(go, trigger)
    else:
        print "No trigger set by any user"

du coup l’appel de la fonction “go” implique de changer sa signature en lui filant comme argument “trigger” (le QuerySet de l’appli Django)

A présent donc pool.map fait l’itération des données trouvées dans le modele TriggerService et exécute tout le tintouin. *<:o)

ps : @Sam : chose promise chose dûe ;)

flattr this!

]]>
http://sametmax.com/python-ubiuite-multiprocessing/feed/ 6
Un objet proxy : ce que c’est et à quoi ça sert http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/ http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/#comments Sat, 01 Feb 2014 17:18:13 +0000 Sam http://sametmax.com/?p=8076 autre objet en paramètre et le sauvegarde dans un de ses attributs. Quand on appelle les méthodes du proxy, le proxy appelle la même méthode de l'objet qu'il a en attribut, et retourne le résultat. Quand on set/get/delete un attribut du proxy, il fait la même chose sur l'autre objet.]]> Un objet proxy est un objet qui prend un autre objet en paramètre et le sauvegarde dans un de ses attributs. Quand on appelle les méthodes du proxy, le proxy appelle la même méthode de l’objet qu’il a en attribut, et retourne le résultat. Quand on set/get/delete un attribut du proxy, il fait la même chose sur l’autre objet.

Voilà une implémentation très basique d’un objet proxy en Python :

class Proxy(object):
 
    def __init__(self, obj):
        # L'objet passé en paramètre est
        # sauvegardé dans un attribut.
        # On le fait en utilisant 
        # object.__setattr__, qui est le
        # __setattr__ du parent, et non directement
        # en faisant self._obj = obj
        # afin d'éviter une boucle infinie car
        # nous écrasons __setattr__ plus bas.
        object.__setattr__(self, "_obj", obj)
 
    # On écrase les méthodes magiques __getattribute__
    # (qui est appelée quand on faire self.nom_attribut), 
    # __delattr__ (qui est appelée quand on fait 
    # del self.nom_attribut) et __setattr__ (qui est 
    # appelée quand on fait self.nom_attribut = truc)
    def __getattribute__(self, name):
        return getattr(object.__getattribute__(self, "_obj"), name)
    def __delattr__(self, name):
        delattr(object.__getattribute__(self, "_obj"), name)
    def __setattr__(self, name, value):
        setattr(object.__getattribute__(self, "_obj"), name, value)

Ca s’utilise comme ceci :

class UnObjetOrdinnaire(object):
 
    attribut = 'VALEUR !'
 
    def methode(self, param):
        return param * 2
 
 
objet_ordinnaire = UnObjetOrdinnaire()
# on passe l'objet ordinnaire au proxy
objet_proxy = Proxy(objet_ordinnaire)
 
# Accéder à des méthodes et attribut
# du proxy accède à ceux de l'objet
# derrière le proxy
print(objet_proxy.attribut)
## VALEUR !
 
print(objet_proxy.methode(3))
## 6
 
# Modifier l'objet proxy modifie
# l'objet derrière le proxy
objet_proxy.attribut = 'une autre valeur'
 
print(objet_ordinnaire.attribut)
## une autre valeur

Pour un exemple vraiment à l’épreuve des balles, il faut prendre en compte tout un tas de cas particuliers, ce qui fait qu’il est bien plus rentable d’utiliser une lib solide pour ça.

Attention, un objet proxy peut très bien avoir des méthodes qui n’appellent pas celles de l’objet derrière. On même avoir des méthodes qui appellent des méthodes qui n’ont pas le même nom, ou plusieurs méthodes… Ce que vous voyez en exemple est un proxy très basique.

Pourquoi voudrait-on obtenir ce résultat ?

Plusieurs design patterns font appel à des objets proxy. Par exemple, le design pattern “adapter” consiste à créer un objet proxy qui accepte plusieurs types d’objets en paramètres afin de présenter toujours la même interface.

Imaginez que vous ayez plusieurs objets qui servent à s’authentifier :

 
class Login(object):
 
    _is_logged = False 
 
    def is_logged(self):
        return self._is_logged
 
    def login(self, email, password):
        self._is_logged = True
 
 
class LoginWithUsername(object):
 
    _is_logged = False 
 
    def is_logged(self):
        return self._is_logged
 
    def login(self, username, password):
        self._is_logged = True
 
 
class SignIn(object):
 
    _is_logged = False 
 
    def is_logged(self):
        return self._is_logged
 
    def signin(self, email, password):
        self._is_logged = True
 
 
class LoginWithKey(object):
 
    _is_logged = False 
 
    key = "jfjkdlmqfjdmqsjdk"
 
    def is_logged(self):
        return self._is_logged
 
    def login(self, username):
        self._is_logged = True

Et vous avez un algo de parsing, qui attend un objet d’authentification. Si vous mettez le code qui permet de choisir la bonne API dans l’algo de parsing, vous liez l’algo (qui n’a rien à voir avec l’authentification) à toutes ces implémentations.

Une des manières de faire, est d’utiliser un adaptateur, dont voici une esquisse :

class AuthAdapter(object):
 
 
    def __init__(self, obj):
        object.__setattr__(self, "_obj", obj)
 
    def __getattribute__(self, name):
 
        try:
            # Si l'attribut existe sur le proxy, on l'utilise
            return object.__getattribute__(self, name)
        except AttributeError:
            # Sinon on tente le coup sur l'objet derrière le proxy
            return getattr(object.__getattribute__(self, "_obj"), name)
 
 
    def login(self, id_=None, secret=None):
        # Le login est différent pour chaque classe,
        # donc on s'arrange avec.
        try:
            self._obj.login(id_, secret)
        except AttributeError:
            self._obj.signin(id_, secret)
        except TypeError:
            self._obj.login(id_)

En gros, si on essaye d’appeler login(), il va lisser les contours et nous donner toujours la même interface, même si derrière l’objet peut marcher complètement différement. En revanche, si on appelle n’importe quel autre attribut ou méthode (par exemple is_logged, mais il pourrait y en avoir des dizaines d’autres dans la vrai vie vivante), ça tape directement dans l’objet derrière le proxy.

Donc si j’applique l’adaptateur systématiquement, quelle que soit la classe derrière, le comportement est toujours le même : j’appelle login(un id, un secret), et il se logge.

all_auth = (Login, LoginWithUsername, SignIn, LoginWithKey)
 
for auth in all_auth:
    # 'auth' est une des 4 classes de login. On l'instancie et
    # on met l'instance derrière le proxy
    auth = AuthAdapter(auth())
    print("Testing '%s'" % auth.__class__.__name__)
    print("Is logged : %s" % auth.is_logged())
    # Le login se passe toujours de la même manière, quelle que soit la classe
    auth.login('id', 'secret')
    print("Is logged after logging : %s" % auth.is_logged())

Comme d’habitude, ceci est un exemple naval. Il est bateau quoi. Mais cela vous démontre le principe.

Le design pattern façade ressemble à l’adapter, en fait c’est une spécialisation de l’adapter. Il s’agit juste d’exposer une interface plus simple, de l’objet derrière le proxy.

Un proxy peut aussi servir à hacker une lib. Par exemple, la lib attend un objet d’une ancienne version d’une autre lib dont l’auteur a déprécié un attribut. Avec un proxy, vous pouvez toujours faire semblant que l’attribut est toujours là : enrobez l’objet dans un proxy qui possède cet attribut, tout le reste de l’API sera la même.

Un proxy peut également être utile si vous voulez effectuer des actions quand l’objet est manipulé.

import logging
 
class ProxyLogger(object):
 
    def __init__(self, obj):
        object.__setattr__(self, "_obj", obj)
 
    def __getattribute__(self, name):
        obj = object.__getattribute__(self, "_obj")
        # On lance un warning à chaque accès à un attribut de l'objet
        # derrière le proxy
        logging.warning("%s.%s has been called" % (obj.__class__.__name__, name))
        return getattr(obj, name)

Maintenant, supposons que vous avez un objet d’une lib externe (que vous ne pouvez donc pas modifier) sur lequel vous avez besoin d’infos :

class ObjetExterne(object):
    def ahahah(self):
        pass
 
o = ProxyLogger(ObjetExterne())
o.ahahah()
## WARNING:root:ObjetExterne.ahahah has been called

Vous pouvez refiler le proxy à n’importe quel objet de sa lib d’origine, si le proxy est bien fait, elle ne vera pas la différence.

On finit sur une note culture, puisque le pattern decorator utilise souvent aussi un proxy. Cette fois d’une fonction sur un autre objet (en générale une autre fonction). Mais le principe est le même. Donc quand vous voyez @un_decorateur, il est peut être en train d’appliquer un proxy à la fonction.

D’ailleurs on dit souvent que le proxy décore l’objet qui est derrière.

flattr this!

]]>
http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/feed/ 1
Python, meilleur nul part, excellent partout http://sametmax.com/python-meilleur-nul-part-excellent-partout/ http://sametmax.com/python-meilleur-nul-part-excellent-partout/#comments Fri, 31 Jan 2014 13:57:51 +0000 Sam http://sametmax.com/?p=8328 Je ne sais pas si vous l’aviez compris, mais j’aime bien Python. J’ai essayé de vous le cacher jusqu’ici, mais je sais que mon secret ne tiendra pas longtemps, alors j’avoues tout.

Je n’aime pas Python parce que c’est le meilleur langage pour X. Je ne pense pas que Python soit le meilleur langage pour quoi que ce soit. Regex ? Perl est meilleur ? IA ? Lisp est meilleur. Embarqué ? C est meilleur. Asynchrone ? Javascript est meilleur.

Non. J’aime Python parce qu’il est très bon à X. Presque tous les X. En fait, il existe très peu de X pour lequel Python n’est pas bon.

Python est excellent pour analyser du texte, pour faire un site Web (même asynchrone), de l’administration système, des interfaces graphiques, du calcul scientifique, de la 3D, des crawlers, etc. Pas le meilleur. Mais très bon.

Et ceci est fantastique. Cela veut dire que si je commence un projet, n’importe quel projet, je sais que je peux le commencer en Python. Je ne serai pas bloqué plus tard par tel ou tel besoin. Je sais que je pourrai m’en sortir agréablement avec Python.

Vous faites un site en JS ? Pour le sysadmin, il va falloir un autre langage.

Des scripts de maintenance en Perl ? J’espère que vous n’aurez pas besoin d’une UI.

Un crawler en PHP ? Pour l’analyse des données récupérer, vous aller vous amuser…

De l’extraction de data en Ruby ? Pourvu qu’il ne faille pas traiter plusieurs To.

Ce n’est pas que vous ne pourrez pas. Ils sont tous Turring complet après tout. Mais vous allez vraiment galérer.

Aucun langage n’est parfait, et Python n’est pas une exception.

Mais ce qui est génial, c’est que dans 90% des cas, vous pouvez avoir une solution propre et efficace en Python au problématique que vous ne savez même pas encore que vous aurez. Et le jour où vous atteindrez la limite, alors oui, vous pourrez utiliser une autre techno pour le faire. Mais juste pour cette partie. La plupart du temps ce sera une partie très réduite. En en plus, Python pourra probablement s’interfacer nativement avec le code de l’autre langage.

Il y a des tas de raisons de ne pas choisir Python bien entendu, mais probablement la meilleure raison de le choisir, c’est qu’il y a peu de chance de se planter.

Bon, évidement si vous voulez écrire un driver de carte graphique…

flattr this!

]]>
http://sametmax.com/python-meilleur-nul-part-excellent-partout/feed/ 26
Solution de l’exercice d’hier sur shadow http://sametmax.com/solution-de-lexercice-dhier-sur-shadow/ http://sametmax.com/solution-de-lexercice-dhier-sur-shadow/#comments Wed, 29 Jan 2014 15:25:53 +0000 Sam http://sametmax.com/?p=8917 Ça va de soit, mais ça va mieux en le disant, ceci n’est pas la solution unique de l’exercice d’hier, mais une solution possible parmi d’autres.

On note l’usage de crypt, qui évite de se faire chier à trouver le bon algo de hashing et gère le salt automatiquement. spwd, c’est vraiment pour le grosses larves comme moi qui veulent même pas faire un split.

Et c’est du Python 3, yo dog !

import io
import os
import crypt
import spwd
 
from urllib.request import FancyURLopener
from zipfile import ZipFile
 
PASSWORDS_SOURCE = "http://xato.net/files/10k%20most%20common.zip"
PASSWORDS_LIST = '10k most common.txt'
 
# Le fichier ZIP est derrière cloudflare, qui vous ferme la porte au nez si
# vous n'avez pas de User-Agent. On va donc créer un UrlOpener, un objet qui
# ouvre des ressources en utilisant leurs URLs, qui a un User-Agent 'TA MERE'.
# CloudFlare ne check pas que le UA est valide.
class FFOpener(FancyURLopener):
   version = 'TA MERE'
 
# Si le dictionnaire de passwords n'est pas là, on le télécharge
# via FFOpener().open(PASSWORDS_SOURCE).read(). C'est verbeux, c'est urllib.
# Normalement je ferais ça avec requests. Ensuite on lui donne une interface
# file-like object avec io.BytesIO pour que ZipFile puisse le traiter en mémoire
# sans avoir à le sauvegarder dans un vrai fichier sur le disque, et on
# extrait le ZIP.
if not os.path.isfile(PASSWORDS_LIST):
    ZipFile(io.BytesIO(FFOpener().open(PASSWORDS_SOURCE).read())).extractall()
 
# On extrait les mots de passe de la liste sous forme de tuple car c'est rapide
# à lire. Un petit rstrip vire les sauts de ligne.
passwords = tuple(l.rstrip() for l in open(PASSWORDS_LIST))
 
# spwd.getspall() nous évite de parser le fichier shadow à la main.
for entry in spwd.getspall():
    print('Processing password for user "%s": ' % entry.sp_nam, end='')
 
    # Pas de hash ? On gagne du temps avec 'continue'
    if not '$' in entry.sp_pwd:
        print('no password hash to process.')
        continue
 
    # On teste chaque password avec la fonction crypt, qui accepte en deuxième
    # paramètre le hash du mot de passe complet. Pas besoin de se faire chier
    # à le spliter, il va analyser les '$' et se démerder avec ça. On a juste
    # à comparer le résultat avec le hash d'origine.
    for pwd in passwords:
        if crypt.crypt(pwd, entry.sp_pwd) == entry.sp_pwd:
            print('password is "%s".' % pwd)
            # On break pour gagner quelques tours de boucles, et pouvoir
            # utiliser la condition 'else'.
            break
    else:
        print('fail to break password.')

Télécharger le code.

flattr this!

]]>
http://sametmax.com/solution-de-lexercice-dhier-sur-shadow/feed/ 9
C’est l’heure de faire de l’exercice http://sametmax.com/cest-lheure-de-faire-de-lexercice/ http://sametmax.com/cest-lheure-de-faire-de-lexercice/#comments Tue, 28 Jan 2014 16:19:48 +0000 Sam http://sametmax.com/?p=8910 Le dernier exercice avait bien été apprécié, alors je remet ça.

Consigne :


Créer un script de brute force de passwords Unix par dictionnaire.

Puisqu’on est pas non plus des Kevin Mitnick en puissance, on va supposer que vous êtes connectés sur la machine, que vous avez les droits root dessus et que vous avez localisé les mots de passe comme étant dans “/etc/shadow”.

Votre script va vérifier si il possède un dictionnaire de mots de passe. Si ce n’est pas le cas, il va télécharger celui-ci et le décompresser : http://xato.net/files/10k%20most%20common.zip.

Ensuite vous parcourez le fichier shadow, et vous essayez de trouver quel mot de passe se cache derrière chaque hash. Si il n’y a pas de hash, vous pouvez ignorer l’utilisateur.

Exemple de sortie:

Processing password for user "root": no password hash to process.
Processing password for user "daemon": no password hash to process.
Processing password for user "bin": no password hash to process.
Processing password for user "sys": no password hash to process.
Processing password for user "www-data": no password hash to process.
Processing password for user "sam": fail to break password.
Processing password for user "test": password is "cheese".
Processing password for user "messagebus": no password hash to process.
Processing password for user "avahi-autoipd": no password hash to process.
Processing password for user "avahi": no password hash to process.
...

Afin de simplifier l’exercice, il n’est pas demandé de gestion d’erreur ou de passage en a paramètre du script.

Comme d’habitude, il n’y a pas de solution ultimate de la mort qui tue, c’est juste pour le fun.

La soluce demain.

flattr this!

]]>
http://sametmax.com/cest-lheure-de-faire-de-lexercice/feed/ 28
Cog, l’anti langage de template Python http://sametmax.com/cog-lanti-langage-de-template-python/ http://sametmax.com/cog-lanti-langage-de-template-python/#comments Mon, 27 Jan 2014 17:45:38 +0000 Sam http://sametmax.com/?p=8903 Cog est un outil Python en ligne de commande qui permet d'insérer du code Python dans un fichier, afin qu'il génère une partie de ce fichier]]> Cog est un outil Python en ligne de commande qui permet d’insérer du code Python dans un fichier, afin qu’il génère une partie de ce fichier.

A priori, ça ressemble à un langage de template. Là où ça diffère, c’est que cog ne cherche pas à générer un fichier différent, il insère le code généré dans le fichier original, et garde le code de génération.

Exemple, vous avez envie d’insérer un warning en haut de plusieurs fichiers de code. Vos choix :

  • Copier-coller le warning.
  • Créer un script de build qui insère le warning.

L’alternative des bricoleurs qui ont juste besoin de quelques insertions, c’est Cog. Par exemple, votre fichier contient :

# [[[cog for l in open('warning.txt'): cog.out("# " + l) ]]]
# [[[end]]]
 
import vostrucs
 
vostrucs.faire_vos_machins()

Après un cog.py -r votre_fichier.py, votre fichier sera :

# [[[cog for l in open('warning.txt'): cog.out("# " + l) ]]]
# Attention, ceci est un avertissmement super important.
# Ce logiciel ne vient avec aucune garantie. Il vomira dans votre salon.
# Violera votre femme, lui collera l'hépatite C et offrira un CD de one direction
# à vos enfants.
# [[[end]]]
 
import vostrucs
 
vostrucs.faire_vos_machins()

cog vient avec plusieurs options, par exemple la possibilité de retirer le code de génération ou de lire le précédent texte généré depuis le code de génération.

La véritable force de l’outil c’est qu’il peut utiliser n’importe quel module Python, et donc générer du texte à partir d’un contenu en ligne ou un fichier CSV. Il est d’ailleurs né parce que l’auteur était codeur C et avait besoin de générer du code à partir d’un fichier XML.

Le seul défaut de Cog, c’est que c’est verbeux. Et moche.

flattr this!

]]>
http://sametmax.com/cog-lanti-langage-de-template-python/feed/ 4
La différence entre paramètres et arguments http://sametmax.com/la-difference-entre-parametres-et-arguments/ http://sametmax.com/la-difference-entre-parametres-et-arguments/#comments Sun, 26 Jan 2014 16:54:42 +0000 Sam http://sametmax.com/?p=8896 On a tendance à utiliser l’un pour l’autre, et inversement. Moi-même je le fais régulièrement, y compris dans plein d’articles de ce blog. Mais il y a pourtant une différence sémantique, bien que dans la pratique on s’y retrouve.

Les paramètres sont les noms que l’on spécifie dans la signature de la fonction :

def ma_fonction(arg1, arg2='valeur', *args):
    # faire un truc

Ici arg1, arg2 et args sont des paramètres. Ce sont les noms des différents points d’entrée de données dans la fonction.

Les arguments sont les valeurs passées à l’appel de la fonction :

ma_fonction('foo', arg2='bar')

'foo' et 'bar' sont des arguments. C’est ce que l’on passe via les différents points d’entrée dans la fonction, au moment de son utilisation.

Maintenant, pour les tatillons, ce n’est valable que pour ceux qui aiment les anglicismes. Car en français, normalement, on a que le mot “paramètre”, et on utilise :

  • “Paramètre formel” traduire “parameter”.
  • “Paramètre effectif” pour traduire “argument”.

Certaines personnes, qui aiment bien embrouiller les choses, parlent d’”argument muet” pour traduire “parameter”. Mais on va pas trop se plaindre, car les anglais ont encore plus de synonymes la con pour parler des paramètres et arguments.

Bref, en résumé : on définit des paramètres, et on passe des arguments. Je continuerai probablement à mélanger les deux moi-même, alors détendez-vous du slip.

flattr this!

]]>
http://sametmax.com/la-difference-entre-parametres-et-arguments/feed/ 7
Parser du HTML avec BeautifulSoup http://sametmax.com/parser-du-html-avec-beautifulsoup/ http://sametmax.com/parser-du-html-avec-beautifulsoup/#comments Thu, 23 Jan 2014 22:46:31 +0000 k3c http://sametmax.com/?p=6161 Ceci est un post invité de k3c posté sous licence creative common 3.0 unported.

Un exemple de parsing HTML avec BeautifulSoup.

Cet article ne traitera pas l’écriture ou la modification de HTML, et pompera allègrement la doc BeautifulSoup (traduite).

De manière générale, pour télécharger une vidéo sur un site de replay, il faut

  • récupérer un identifiant de la vidéo
  • le passer à un autre site
  • analyser le résultat pour trouver l’adresse de la vidéo.

Prenons un exemple sur les replays de d8.tv, par exemple

une vidéo de D8

(attention cet exemple sera rapidement obsolète, mais c’est le principe qui nous intéresse)

L’installation de BeautifulSoup 4 se fait avec, au choix

$ apt-get install python-bs4
$ easy_install beautifulsoup4
$ pip install beautifulsoup4

La documentation de BeautifulSoup 4 est à

http://www.crummy.com/software/BeautifulSoup/bs4/doc/

Si on regarde le code source de la page (CTRL U sous Firefox, sinon voyez avec votre navigateur préféré), on voit que la partie qui nous intéresse et qui contient videoId est courte

Ici l’identifiant recherché est 943696

En Python, on va donc faire quelque chose comme

from urllib2 import urlopen
import bs4 as BeautifulSoup
html = urlopen('http://www.d8.tv/d8-series/pid6654-d8-longmire.html').read()
soup = BeautifulSoup.BeautifulSoup(html)

Comme le dit la doc BeautifulSoup

début du pompage de la doc BeautifulSoup

Beautiful Soup transforme un document HTML complexe en un arbre complexe d’objets Python. Mais vous aurez à manipuler seulement quatre types d’objets : Tag, NavigableString, BeautifulSoup, et Comment.

  • Tag
    Un objet Tag correspond à un tag HTML ou XML dans le document
    Les Tags ont de nombreux attributs et méthodes, que nous verrons dans
    naviguer dans l’arborescence
    et
    chercher dans l’arborescence

    Pour l’instant, les caractéristiques les plus importantes d’un tag sont
    son nom

    >>> tag.name
     u'b'

    ses attributs
    Un tag peut avoir n’importe quel nombre d’attributs. Le tag

    <b class="boldest">

    possède un attribut “class” dont la valeur est “boldest”. On peut accéder les attributs d’un tag en le traitant comme un dictionaire :

    >>> tag['class']
     u'boldest'

    On peut accéder directement ce dictionaire avec .attrs:

    >>>tag.attrs
     {u'class': u'boldest'}
  • NavigableString
    Une chaîne de caractères est un peu de texte l’intérieur d’un tag. Beautiful Soup utilise la classe NavigableString class pour contenir ces morceaux de texte:

    >>> tag.string
     u'Extremely bold'
    >>> type(tag.string)
     class 'bs4.element.NavigableString'

    Un NavigableString est comme une chaîne de caractères Python Unicode, sauf que elle supporte aussi quelques unes des caractéristiques décrites dans Navigating the tree et Searching the tree. Vous pouvez convertir une chaîne de caractères NavigableString en Unicode avec unicode():

    >>> unicode_string = unicode(tag.string)
    >>> unicode_string
     u'Extremely bold'
    >>> type(unicode_string)
     type 'unicode'
  • BeautifulSoup
    L’objet BeautifulSoup lui-même représente le document dans son ensemble. Dans la plupart des cas, vous pouvez le traiter en tant qu’objet Cela signifie qu’il supporte la plupart des méthodes décrites dans Navigating the tree et Searching the tree.

    Comme l’objet BeautifulSoup ne correspond pas à un tag final HTML ou XML, il n’a pas de nom et pas d’attributs. Mais il est parfois utile de rechercher son .name, donc on lui a donné le .name “[document]”:

    >>> soup.name
     u'[document]'
  • Commentaires et autres chaînes de caractères spéciales

    Tag, NavigableString, et BeautifulSoup couvrent presque tout ce que vous verrez dans un fichier HTML ou XML, mais il y a quelques cas à part. Le seul qui doit vous inquiéter (un peu) est le commentaire :

    markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
    >>> soup = BeautifulSoup(markup)
    >>> comment = soup.b.string
    >>> type(comment)
     class 'bs4.element.Comment'

    L’objet Comment est simplement un type spécial de NavigableString:

    >>> comment
     u'Hey, buddy. Want to buy a used parser'

    Mais quand il apparaît en tant que morceau de document HTML, un Comment est affiché avec un formattage spécial :

    # <b>
    #  <!--Hey, buddy. Want to buy a used parser?-->
    # </b>

    Beautiful Soup définit des classes pour n’importe quoi d’autre qui apparaîtrait dans un document XML : CData, ProcessingInstruction, Declaration, et Doctype. De la meme manière que Comment, ces classes sont des sous-classes de NavigableString qui ajoutent quelque chose à la chaîne de caractères. Voici un exemple qui remplace le commentaire par un block CDATA :

    from bs4 import CData
    cdata = CData("A CDATA block")
    comment.replace_with(cdata)
    print(soup.b.prettify())
    # <b>
    #  <![CDATA[A CDATA block]]>
    # </b>

fin du pompage de la doc BeautifulSoup

On peut faire un

print soup.prettify()

pour voir à quoi ressemble le code HTML de la page

Il faut d’abord analyser la page et rechercher ce qui suit videoId

Pour commencer nous allons naviguer dans le document.

BeautifulSoup permet de multiples syntaxes, par exemple, on n’est pas obligé de donner le chemin complet

soup.head.meta

ou

soup.meta

affichent le meme résultat, vu que la première balise meta est sous la balise head

<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>

Si on regarde les méthodes disponibles

dir(soup.meta)
['FORMATTERS', '__call__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__doc__', '__eq__', '__format__', '__getattr__', '__getattribute__', '__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_all_strings', '_attr_value_as_string', '_attribute_checker', '_find_all', '_find_one', '_lastRecursiveChild', '_last_descendant', 'append', 'attribselect_re', 'attrs', 'can_be_empty_element', 'childGenerator', 'children', 'clear', 'contents', 'decode', 'decode_contents', 'decompose', 'descendants', 'encode', 'encode_contents', 'extract', 'fetchNextSiblings', 'fetchParents', 'fetchPrevious', 'fetchPreviousSiblings', 'find', 'findAll', 'findAllNext', 'findAllPrevious', 'findChild', 'findChildren', 'findNext', 'findNextSibling', 'findNextSiblings', 'findParent', 'findParents', 'findPrevious', 'findPreviousSibling', 'findPreviousSiblings', 'find_all', 'find_all_next', 'find_all_previous', 'find_next', 'find_next_sibling', 'find_next_siblings', 'find_parent', 'find_parents', 'find_previous', 'find_previous_sibling', 'find_previous_siblings', 'format_string', 'get', 'getText', 'get_text', 'has_attr', 'has_key', 'hidden', 'index', 'insert', 'insert_after', 'insert_before', 'isSelfClosing', 'is_empty_element', 'name', 'namespace', 'next', 'nextGenerator', 'nextSibling', 'nextSiblingGenerator', 'next_element', 'next_elements', 'next_sibling', 'next_siblings', 'parent', 'parentGenerator', 'parents', 'parserClass', 'parser_class', 'prefix', 'prettify', 'previous', 'previousGenerator', 'previousSibling', 'previousSiblingGenerator', 'previous_element', 'previous_elements', 'previous_sibling', 'previous_siblings', 'recursiveChildGenerator', 'renderContents', 'replaceWith', 'replaceWithChildren', 'replace_with', 'replace_with_children', 'select', 'setup', 'string', 'strings', 'stripped_strings', 'tag_name_re', 'text', 'unwrap', 'wrap']

on voit que de nombreuses méthodes sont disponibles, et suivant la doc, on peut donc le traiter comme un dictionnaire

>>> soup.meta['http-equiv']
'Content-Type'

et tester quelques méthodes

>>> soup.meta.name
'meta'
>>> soup.meta.find_next_sibling()
'<meta content="D8" name="author"/>'
soup.meta.find_previous_sibling()

Nous voyons que soup.meta a un sibling (frère ou soeur) suivant, mais pas de précédent, c’est le premier de l’arborescence.
Bon, la balise meta a pour nom meta, pas un scoop, on continue avec les clés de dictionnaire, sans surprise

>>> soup.meta.find_next_sibling()
'<meta content="D8" name="author"/>'
>>> soup.meta.find_next_sibling()['content']
'D8'
>>> soup.meta.find_next_sibling()['name']
'author'

Pour le fun, regardez ce que renvoie

soup.meta.find_next_sibling().parent

et

soup.meta.find_next_sibling().parent.parent

et je vous laisse deviner la prochaine commande que vous allez passer…

Revenons à une recherche qui va trouver de nombreuses occurences

soup.find('div')

va trouver la première balise div, et

soup.findall('div')

va renvoyer une liste contenant tous les div de la page, mais cela ne permet pas de trouver facilement la portion contenant videoId, par contre, la documentation de BeautifulSoup montre comment trouver spécifiquement une CSS class, voir
searching by css class dans la doc BeautifulSoup
dans la documentation BeautifulSoup
Voici la syntaxe à utiliser

soup.find('div',attrs={"class":u"block-common block-player-programme"})

va renvoyer la partie qui nous intéresse, par exemple

        <div class="block-common block-player-programme">
 
            <div class="bpp-player">
                <div class="playerVideo player_16_9">
 
		<div class="itemprop" itemprop="video" itemscope itemtype="http://schema.org/VideoObject">
			<h1>Vidéo : <span itemprop="name">Longmire - Samedi 30 novembre à 20h50</span></h1>
			<meta itemprop="duration" content="" />
			<meta itemprop="thumbnailUrl" content="http://media.canal-plus.com/wwwplus/image/53/1/1/LONGMIRE___BANDE_ANNONCE__131120_UGC_3279_image_L.jpg" />			
 
			<meta itemprop="embedURL" content="http://player.canalplus.fr/embed/flash/CanalPlayerEmbarque.swf?vid=975153" />
			<meta itemprop="uploadDate" content="2013-11-29T00:00:00+01:00" />
			<meta itemprop="expires" content="2014-02-18T00:00:00+01:00" />
 
    <canal:player videoId="975153" width="640" height="360" id="CanalPlayerEmbarque"></canal:player>

Le type de cette donnée est bs4 élément tag

Comme l’a dit un homme célèbre
si vous ne savez pas ce que contient une variable, vous ne comprenez pas le programme
on peut donc faire un type, dir, help, doc, repr, par exemple

>>> type(soup.find('div',attrs={"class":u"block-common block-player-programme"}))
class 'bs4.element.Tag'

donc nous pouvons rechercher un tag, comme
canal:player

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player')
'<canal:player height="360" id="CanalPlayerEmbarque" videoid="786679" width="640"></canal:player>'
>>> soup.findAll('div', attrs={"class":u"tlog-inner"})

renvoie une liste

[<div class="tlog-inner">
<div class="tlog-account">
<span class="tlog-avatar"><img height="30" src="http://media.canal-plus.com/design/front_office_d8/images/xtrans.gif" width="30"/></span>
<a class="tlog-logout le_btn" href="#">x</a>
</div>
<form action="#" method="post">
<label class="switch-fb">
<span class="cursor traa"> </span>
<input checked="" id="check-switch-fb" name="switch-fb" type="checkbox" value="1"/>
</label>
</form>
<div id="headerFbLastActivity">
<input id="name_facebook_user" type="hidden"/>
<div class="top-arrow"></div>
<div class="top">
<div class="top-bg"></div>
<div class="top-title">Activité récente</div>
</div>
<div class="middle">
<div class="wrap-last-activity">
<div class="entry">Aucune</div>
</div>
<div class="wrap-notification"></div>
</div>
<div class="bottom">
<a class="logout" href="#logout">Déconnexion</a>
</div>
</div>
</div>]

On peut prendre le premier élément de cette liste

soup.findAll('div', attrs={"class":u"tlog-inner"})[0]

et ne vouloir que la ligne commençant par “span class”

soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span

ce qui affiche

<span class="tlog-avatar"><img height="30" src="http://media.canal-plus.com/design/front_office_d8/images/xtrans.gif" width="30"/></span>

Voyons le type de donnée

>>> type(soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span)
<class 'bs4.element.Tag'>

et voyons les méthodes disponibles

>>> dir(soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span)
['FORMATTERS', '__call__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__doc__', '__eq__', '__format__', '__getattr__', '__getattribute__',
'__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__'
, '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_all_strings', '_attr_value_as_string', '_attribute_checker', '_find_all',
'_find_one', '_lastRecursiveChild', '_last_descendant', 'append', 'attribselect_re', 'attrs', 'can_be_empty_element', 'childGenerator', 'children', 'clear', 'contents',
 'decode', 'decode_contents', 'decompose', 'descendants', 'encode', 'encode_contents', 'extract', 'fetchNextSiblings', 'fetchParents', 'fetchPrevious', 'fetchPreviousSi
blings', 'find', 'findAll', 'findAllNext', 'findAllPrevious', 'findChild', 'findChildren', 'findNext', 'findNextSibling', 'findNextSiblings', 'findParent', 'findParents
', 'findPrevious', 'findPreviousSibling', 'findPreviousSiblings', 'find_all', 'find_all_next', 'find_all_previous', 'find_next', 'find_next_sibling', 'find_next_sibling
s', 'find_parent', 'find_parents', 'find_previous', 'find_previous_sibling', 'find_previous_siblings', 'format_string', 'get', 'getText', 'get_text', 'has_attr', 'has_k
ey', 'hidden', 'index', 'insert', 'insert_after', 'insert_before', 'isSelfClosing', 'is_empty_element', 'name', 'namespace', 'next', 'nextGenerator', 'nextSibling', 'ne
xtSiblingGenerator', 'next_element', 'next_elements', 'next_sibling', 'next_siblings', 'parent', 'parentGenerator', 'parents', 'parserClass', 'parser_class', 'prefix',
'prettify', 'previous', 'previousGenerator', 'previousSibling', 'previousSiblingGenerator', 'previous_element', 'previous_elements', 'previous_sibling', 'previous_sibli
ngs', 'recursiveChildGenerator', 'renderContents', 'replaceWith', 'replaceWithChildren', 'replace_with', 'replace_with_children', 'select', 'setup', 'string', 'strings'
, 'stripped_strings', 'tag_name_re', 'text', 'unwrap', 'wrap']

Nous voulons maintenant juste ce qui suit videoId.

dir(soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player'))

montre, entre autres choses, que la méthode get est disponible.
Pour récupérer l’identifiant qui nous intéresse, on peut donc faire

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player').get('videoid')
'975153'

ou utiliser une autre syntaxe

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player')['videoid']
'975153'

De la même manière, on peut récupérer le titre de la vidéo

>>> soup.find('h3',attrs={"class":u"bpp-title"})
'<h3 class="bpp-title">Longmire - Samedi 30 novembre à 20h50</h3>'

mais on veut juste le titre, donc

>>> soup.find('h3',attrs={"class":u"bpp-title"}).text
uu'Longmire - Samedi 30 novembre \xe0 20h50'

Maintenant que l’on a le numéro de la vidéo, on peut le passer au site qui contient l’adresse, et avec un peu de scripting XML, récupérer l’adresse de la vidéo (un autre article sera consacré au scripting XML)
Selon que la vidéo vient de D8 ou de canal, elle sera sur
vidéo de d8
ou
vidéo de Canal Plus
et avec un peu de code

from lxml import objectify
def get_HD(d8_cplus,vid):
    root = objectify.fromstring(urlopen('http://service.canal-plus.com/video/rest/getVideosLiees/'+d8_cplus+'/'+vid).read())
    for x in root.iter():
        if x.tag == 'VIDEO' and x.ID.text == vid:
            for vres in vidattr:
                if hasattr(x.MEDIA.VIDEOS, vres):
                    print 'Resolution :', vres
                    videoUrl = getattr(x.MEDIA.VIDEOS, vres).text
                break
        break
    print videoUrl
for x in ['d8','cplus']:
    get_HD(x,vid)

on peut trouver l’adresse de la vidéo.
Il reste juste à envoyer la commande rtmpdump, dans ce cas

rtmpdump -r rtmp://ugc-vod-fms.canalplus.fr/ondemand/videos/1311/LONGMIRE___BANDE_ANNONCE__131120_UGC_3279_video_HD.mp4 -c 1935 -m 10 -B 1 -o mavideo.mp4

Voilà, il reste à noter que BeautifulSoup peut restreindre sa recherche à une partie du document, utiliser une regex (même si c’est le mal), on peut limiter la taille de la liste renvoyée par findAll

Quelles sont les méthodes les plus utiles, si vous avez la flemme de lire toute la doc ?

  • .contents, les enfants d’un tag sont disponibles dans une liste appelée .contents
  • .has_key(‘value’) vous rendra de grands services, associé parfois à
    ['value'] != u''
  • pour extraire un tag sans attribut, par exemple pour un tag p, la syntaxe sera simplement
    soup.findAll('p', {'class': None})
  • .attrs vous affichera un dictionnaire qui peut être intéressant, par exemple :
    >>>soup.head.link
     '<link href="http://media.canal-plus.com/design_pack/front_office_d8/css/d8.d25540b7a93dba7baf89e5ca53ef00e5.min.css" rel="stylesheet" type="text/css"/>'
    >>> soup.head.link.attrs
     {'href': 'http://media.canal-plus.com/design_pack/front_office_d8/css/d8.d25540b7a93dba7baf89e5ca53ef00e5.min.css', 'type': 'text/css', 'rel': ['stylesheet']}
  • .previousSibling (et .next_sibling) peut être utile, par exemple, avec un simple HTML comme ce qui suit
    <div class="category_link">
    Category:
    <a href="/category/personal">Personal</a>
    </div>

    on peut récupérer la chaîne Category : de plusieurs manières, par exemple l’évident

    >>> soup.findAll('div')[0].contents[0]
    u'\n  Category:\n  '

    mais aussi en remontant depuis la balise a

    >>> soup.find('a').previousSibling
    u'\n  Category:\n  '

    Sinon cela est aussi utile avec un HTML mal foutu comme

    <p>z1</p>tagada
    <p>z2</p>tsointsoin

    Dans ce cas, pour récupérer tagada tsointsoin
    on fera par exemple

    soup.findAll('p')[0].next_sibling
    soup.findAll('p')[1].next_sibling

    ou, pour faire plaisir à Sam/Max

    >>> [p.next_sibling for p in soup.findAll('p')]
    [u'tagada', u'tsoitsoin']
  • A quoi sert text=True ?
    Simplement à récupérer juste du texte dans du Html, sans toute la syntaxe HTML.
    Un exemple, toujours sur la vidéo de D8 va illustrer cela

    >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a
    '<a class="tmlogin-btn" href="#">'
    '<span>Se connecter</span>'
    '</a>'
    >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.contents
     [u'\n', <span>Se connecter</span>, u'\n']
    >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.text
     u'\nSe connecter\n'
    >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.findAll(text=True)
     [u'\n', u'Se connecter', u'\n']
  • Une dernière chose, si vous avez besoin de parser des documents de plusieurs centaines de Mo et donc de performances, oubliez le parser HTML par défaut dans BeautifulSoup, et installez le rapide lxml ou un autre parser. Si comme moi vous traitez des documents “petits”, ça n’a pas d’importance.

flattr this!

]]>
http://sametmax.com/parser-du-html-avec-beautifulsoup/feed/ 8