Extraction de données sur un site internet avec PHP, MySQL et XML

version 1.0

Olivier Nepomiachty

Cet article présente une méthode pour récupérer des informations sur un site internet et les intégrer dans une base de données ou dans un flux XML.

Thèmes abordés :

PHP, MySQL, MySQL InnoDB, XML, cURL, connexion en socket sous PHP, sécurité internet, flush sur le navigateur.

- Titre : Extraction de données sur un site internet avec PHP, MySQL et XML

- Auteur : Olivier Nepomiachty

- Parution : 1 février 2004

- Mise à jour : 28 avril 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Olivier Népomiachty. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Utilisation du code

- I-B. Versions requises

- II. Analyse du contexte

- III. Modélisation des informations

- III-A. Texte

- III-B. Le flux XML

- III-C. La base de données

- III-C-1. Présentation de la nomenclature

- III-C-2. MySQL

- III-C-3. Variante avec MySQL InnoDB

- IV. Collecter les informations

- IV-A. cURL

- IV-A-1. Introduction

- IV-A-2. Installation

- IV-A-2-a. Installation sous Windows

- IV-A-2-b. Installation sous Linux

- IV-A-3. Exemples d'utilisation de cURL

- IV-A-4. Implémentation de cURL dans notre code

- IV-B. Utilisation des sockets sous PHP

- IV-B-1. Comment se passe le dialogue entre le client et le serveur internet ?

- IV-B-2. Implémentation sous PHP

- V. L'application PHP - architecture

- V-A. Fonctionnement de l'application

- V-B. Architecture de l'application

- VI. L'application PHP - code partie 1 - Extraction des données : la liste des films

- VI-A. Liste des films à rechercher

- VI-B. Extraction des données - La liste des réponses

- VI-C. Le flush sur la sortie HTML

- VII. L'application PHP - code partie 2 - Extraction des données : les fiches films

- VII-A. Principe

- VII-B. L'extraction

- VII-C. Sortie HTML

- VII-D. Flux XML

- VII-E. Insertion MySQL

- VIII. Sources du tutoriel et exemple

- IX. Remerciements

I - Introduction

Cet article suivra un exemple concret et ludique : la génération d'une base de données de films. La mise en œuvre du code présenté vous permettra de récolter des informations exhaustives sur l'ensemble de votre collection vidéo personnelle (DVD, DivX, VHS, etc.) Un module d'interrogation de la base fourni en exemple permettra l'utilisation immédiate des informations collectées.
Dans le présent tutoriel, la collecte des informations se fera sur le site de AlloCiné.

I-A - Utilisation du code

Cet article est écrit dans un but exclusivement pédagogique. Le code et les méthodes proposés ne doivent pas être utilisés à des fins commerciales sans avoir préalablement obtenu une autorisation écrite du responsable légal du site que vous allez "sonder" ainsi que celle de l'auteur de l'article. En l'absence d'autorisation, vous pourrez être poursuivi en justice pour piratage de données. L'auteur décline toute responsabilité de l'utilisation abusive qui pourra être faite du code.

I-B - Versions requises

L'utilisation du code nécessite au minimum une version de PHP 4.x

II - Analyse du contexte

Avant d'établir une méthodologie d'extraction de données, il faut s'intéresser dans un premier temps à la présentation des données sur le site. À partir d'une liste de films, notre application va interroger le moteur de recherche du site et collecter les informations.

Commençons par faire un tour sur le site de AlloCiné. La barre de navigation propose une entrée "Vidéos". Cliquez dessus. Elle donne accès au moteur de recherche du site.

Saisissez "jackie brown".

Voici la page de résultats :

La page nous présente une liste de résultats articulée autour des rubriques :

- 1 réponse dans les avant-premières (de ou avec) ;

- 1 réponse dans les titres de films déjà sortis ;

- 309 réponses dans les films déjà sortis (de ou avec) ;

- 20 réponses dans les prochaines sorties de films (de ou avec) ;

- 151 réponses en vidéo avec AlloCiné Vision.

Dans notre contexte, la rubrique qui va retenir notre attention est "réponse dans les titres de films déjà sortis". En effet, 1) nous avons soumis un titre de film, et 2) il s'agit d'un film de notre vidéothèque, donc déjà sorti dans le commerce (on ne pirate pas !)
Notons au passage l'URL de la page :
http://www.allocine.fr/recherche/default.html?motcle=jackie+brown
Elle servira pour l'extraction de données.

Cliquez sur le lien :

Nous arrivons sur la fiche du film :

Identifions les informations pertinentes à récolter :

- le titre ;

- le pays d'origine ;

- la date de sortie dans le pays d'origine ;

- le genre (liste) ;

- la durée ;

- les acteurs (liste) ;

- le réalisateur ;

- le synopsis.

Notons au passage qu'au cours de notre navigation nous avons été sollicités par des pages de publicité obligatoire. Ceci n'aura aucune incidence sur la solution d'extraction de données présentée plus tard.

III - Modélisation des informations

Le tutoriel va traiter trois façons d'exploiter les informations collectées. Elles seront implémentées dans l'application finale. Ces méthodes sont :

- sortie texte (pour contrôle) ;

- flux XML ;

- base MySQL / MySQL InnoDB.

III-A - Texte

C'est un format non structuré. Le format texte n'existe que pour vérifier que la collecte s'est passée correctement.

Exemple de sortie texte :

Titre : Jackie Brown
Pays : Film américain
Année : 1997
Genres : Policier, Drame
Durée : 150 minutes
Date de sortie : 01 Avril 1998
Acteurs : Avec Pam Grier, Samuel L. Jackson, Robert De Niro, Bridget Fonda, Michael Keaton
Réalisateur : Quentin Tarantino
Synopsis : Jackie Brown, hôtesse de l'air, arrondit ses fins de mois en convoyant de l'argent liquide pour le compte d'un trafiquant d'armes, Ordell Robbie. Un jour, un agent federal et un policier de Los Angeles la cueillent à l'aéroport. Ils comptent sur elle pour faire tomber le trafiquant. Jackie échafaude alors un plan audacieux pour doubler tout le monde lors d'un prochain transfert qui porte sur la modeste somme de cinq cent mille dollars. Mais il lui faudra compter avec les complices d'Ordell, qui ont des méthodes plutôt expéditives.

III-B - Le flux XML

L'intérêt du flux XML réside dans sa portabilité. Les informations sont structurées et l'ajout ultérieur de champs est aisé. L'importation du flux dans n'importe quel SGBD est facile.

<?xml version="1.0" encoding="ISO-8859-1" ?>
- <LISTE_FILMS>
- <FILM>
<TITRE>Jackie Brown</TITRE>
<PAYS>Film américain</PAYS>
<ANNEE>1997</ANNEE>
<GENRE>Policier</GENRE>
<GENRE>Drame</GENRE>
<DUREE>150</DUREE>
<DATE_DE_SORTIE>01 Avril 1998</DATE_DE_SORTIE>
<ACTEUR>Pam Grier</ACTEUR>
<ACTEUR>Samuel L. Jackson</ACTEUR>
<ACTEUR>Robert De Niro</ACTEUR>
<ACTEUR>Bridget Fonda</ACTEUR>
<ACTEUR>Michael Keaton</ACTEUR>
<REALISATEUR>Quentin Tarantino</REALISATEUR>
<SYNOPSIS>Jackie Brown, hôtesse de l'air, arrondit ses fins de mois en convoyant de l'argent liquide pour le compte d'un trafiquant d'armes, Ordell Robbie. Un jour, un agent federal et un policier de Los Angeles la cueillent à l'aéroport. Ils comptent sur elle pour faire tomber le trafiquant. Jackie échafaude alors un plan audacieux pour doubler tout le monde lors d'un prochain transfert qui porte sur la modeste somme de cinq cent mille dollars. Mais il lui faudra compter avec les complices d'Ordell, qui ont des méthodes plutôt expéditives.</SYNOPSIS>
</FILM>
</LISTE_FILMS>

III-C - La base de données

III-C-1 - Présentation de la nomenclature

La nomenclature choisie pour le nommage des tables et champs est celle que j'ai mise au point et utilise depuis des années. En voici brièvement les règles principales :

- les noms des tables sont en majuscules ;

- les noms des champs sont en majuscules ;

- les index primaires sont de la forme 'ID_' + 'nom de la table' ;

- les clefs étrangères sont de la forme 'nom de la table référencée' + '_ID' ;

- une table de jointure est de la forme 'nom de la table 1' + '_' + 'nom de la table 2'.

Cet ensemble de règles permet d'identifier, au premier coup d'œil, toutes les clefs étrangères d'une table. D'autre part, la lisibilité des requêtes SQL est largement favorisée. Dans beaucoup de cas, il n'est pas nécessaire de préciser le nom de la table rattachée à un champ. Je vous invite à respecter ces règles sur un projet, et à apprécier leur efficacité

III-C-2 - MySQL

Dans le présent tutoriel, le choix de la base s'est porté sur MySQL ; en effet, la notoriété du couple MySQL/PHP n'est plus à faire.

La structure la plus simple est la suivante :
Il est à noter que la base a été normalisée. Il n'existe pas de référence d'acteurs ou de genres dans la table FILM. Les relations sont faites au moyen des tables associatives ACTEUR_FILM et GENRE_FILM.

Les films :

CREATE TABLE FILM (
ID_FILM int(11) NOT NULL auto_increment,
TITRE varchar(255) NOT NULL default '',
PAYS_ID int(11) NOT NULL default '0',
ANNEE date default NULL,
DUREE int(11) NOT NULL default '0',
DATE_SORTIE date default NULL,
REALISATEUR_ID int(11) NOT NULL default '0',
SYNOPSIS text NOT NULL,
PRIMARY KEY (ID_FILM),
KEY TITRE (TITRE),
KEY PAYS_ID (PAYS_ID),
KEY DUREE (DUREE),
KEY DATE_SORTIE (DATE_SORTIE),
KEY REALISATEUR_ID (REALISATEUR_ID),
KEY ANNEE (ANNEE)
) TYPE=MyISAM;

Les acteurs :

CREATE TABLE ACTEUR (
ID_ACTEUR int(11) NOT NULL auto_increment,
NOM_ACTEUR varchar(255) NOT NULL default '',
PRIMARY KEY (ID_ACTEUR)
) TYPE=MyISAM;

Les genres :

CREATE TABLE GENRE (
ID_GENRE int(11) NOT NULL auto_increment,
NOM_GENRE varchar(255) NOT NULL default '',
PRIMARY KEY (ID_GENRE)
) TYPE=MyISAM;

Une table associative entre les acteurs et les films :

CREATE TABLE ACTEUR_FILM (
ACTEUR_ID int(11) NOT NULL default '0',
FILM_ID int(11) NOT NULL default '0',
KEY ACTEUR (ACTEUR_ID,FILM_ID)
) TYPE=MyISAM;

Une table associative entre les genres et les films :

CREATE TABLE GENRE_FILM (
GENRE_ID int(11) NOT NULL default '0',
FILM_ID int(11) NOT NULL default '0',
KEY GENRE (GENRE_ID,FILM_ID)
) TYPE=MyISAM;

Le pays d'origine du film :

CREATE TABLE PAYS (
ID_PAYS int(11) NOT NULL auto_increment,
NOM_PAYS varchar(255) NOT NULL default '',
PRIMARY KEY (ID_PAYS)
) TYPE=MyISAM;

Le réalisateur du film :

CREATE TABLE REALISATEUR (
ID_REALISATEUR int(11) NOT NULL auto_increment,
NOM_REALISATEUR varchar(255) NOT NULL default '',
PRIMARY KEY (ID_REALISATEUR)
) TYPE=MyISAM;

III-C-3 - Variante avec MySQL InnoDB

Cette variante ne sera pas traitée dans ce tutoriel, elle est juste donnée à titre d'exemple.
Les tables innoDB permettent de travailler avec des clefs étrangères. Ainsi, l'intégration de clefs étrangères dans les tables associatives ACTEUR_FILM et GENRE_FILM permettent d'éviter les suppressions en cascade « à la main » lors des opérations de suppression de films par exemple.

Dans mon environnement, la base de données s'appelle DVD.
Les tables FILM, ACTEUR et GENRE changent de type :
remplacer dans le CREATE TABLE : TYPE=InnoDB

La table ACTEUR_FILM devient :

CREATE TABLE ACTEUR_FILM (
ACTEUR_ID int(11) NOT NULL default '0',
FILM_ID int(11) NOT NULL default '0',
KEY ACTEUR_ID (ACTEUR_ID),
KEY FILM_ID (FILM_ID),
FOREIGN KEY (`ACTEUR_ID`) REFERENCES `DVD.ACTEUR` (`ID_ACTEUR`) ON DELETE CASCADE,
FOREIGN KEY (`FILM_ID`) REFERENCES `DVD.FILM` (`ID_FILM`) ON DELETE CASCADE
) TYPE=InnoDB;

La table GENRE_FILM devient :

CREATE TABLE GENRE_FILM (
GENRE_ID int(11) NOT NULL default '0',
FILM_ID int(11) NOT NULL default '0',
KEY GENRE_ID (GENRE_ID),
KEY FILM_ID (FILM_ID),
FOREIGN KEY (`GENRE_ID`) REFERENCES `DVD.GENRE` (`ID_GENRE`) ON DELETE CASCADE,
FOREIGN KEY (`FILM_ID`) REFERENCES `DVD.FILM` (`ID_FILM`) ON DELETE CASCADE
) TYPE=InnoDB;

IV - Collecter les informations

L'application doit récupérer automatiquement le contenu de deux pages HTML pour chacun des films. Elle va dialoguer en HTTP avec le serveur cible. La meilleure façon d'établir ce dialogue est d'utiliser la commande shell cURL. cURL est un projet open source porté sous de nombreuses plates-formes, notamment Linux et Windows.
Cependant, pour les personnes qui ne peuvent pas installer cette commande sur leur serveur, nous verrons une autre solution, en établissant un dialogue en sockets avec les fonctions réseau de PHP. C'est une méthode moins élégante, la gestion du dialogue HTTP se fait « à la main ».

IV-A - cURL

IV-A-1 - Introduction

cURL est un outil puissant. cURL signifie « Client URL Request Library », ou encore « see URL ». Si vous êtes administrateur réseau ou développeur internet, vous ne pourrez bientôt plus vous en passer. Il permet d'effectuer de nombreuses opérations en ligne de commande shell. Il peut par exemple récupérer le contenu d'une page internet, un fichier sur un FTP, utiliser le SSL, l'authentification Apache, un proxy, poster des données sur un formulaire HTTP, etc.
Intégré dans un script, cURL permet également d'automatiser des tâches récurrentes de backup. Il sera l'allié indispensable de vos phases de test de sécurité sur vos pages internet.

IV-A-2 - Installation

cURL se télécharge depuis son site : http://curl.haxx.se/.
Il est possible de l'installer avec ou sans le support de SSL. Si vous disposez d'une distribution récente de Linux, cURL est probablement déjà présent sur la machine. Tapez curl dans une invite shell pour le vérifier.

IV-A-2-a - Installation sous Windows

Je préconise le téléchargement du binary Win32. Il suffit ensuite de copier l'exécutable curl.exe dans un répertoire inclus dans le PATH.

IV-A-2-b - Installation sous Linux

Toute la procédure d'installation est expliquée en détail sur le site de cURL : http://curl.haxx.se/docs/install.html.
En voici un bref résumé.

a-) Téléchargez les sources tar.gz, décompressez et compilez.
Pour les moins aguerris, voici la marche à suivre.

b-) décompression des sources :

tar -zxvf curl-7.11.0.tar.gz

c-1) compilation. Cas n° 1 : vous êtes root sur le serveur

cd curl-7.11.0

installez curl avec les options par défaut.

./configure
make
make test
make install

curl est installé dans /usr/bin/, donc accessible dans le PATH.

c-2) compilation. Cas n° 2 : vous n'êtes pas root sur le serveur

cd curl-7.11.0

installez curl dans votre home directory par exemple.

./configure --prefix=$HOME
make
make test
make install

Note : ajouter --with-ssl à configure pour le support de SSL

Vous êtes fin prêt pour utiliser cURL.

IV-A-3 - Exemples d'utilisation de cURL

- Téléchargement d'une page web

curl http://www.google.fr

le source de la page d'index de Google.fr s'écrit sur la sortie standard.

Pour l'enregistrer dans un fichier, redirigez la sortie :

curl http://www.google.fr > index.html

Des informations de progression se sont affichées sous le prompt :

% Total % Received % Xferd Average Speed Time Curr. Dload Upload Total Current Left Speed 100 3165 100 3165 0 0 5692 0 0:00:00 0:00:00 0:00:00 69771

Pour ne plus les afficher, rediriger la sortie programme (Linux seulement) :

curl http://www.google.fr 2> /dev/null > index.html

- Téléchargement d'un fichier en HTTP

curl http://curl.haxx.se/download/curl-7.11.0.tar.gz > curl-7.11.0.tar.gz

- Téléchargement d'un fichier FTP sur un serveur en connexion anonyme

curl ftp://ftp.server.com/pub/fichier.tar.gz > fichier.tar.gz

- Téléchargement d'un fichier FTP sur un serveur avec authentification

curl -u user:password ftp://ftp.server.com/pub/fichier.tar.gz > fichier.tar.gz

- Téléchargement d'une page web avec un .htaccess

curl -u user:password http://www.server.com/private/page.php > page.html

- Téléchargement d'un fichier en HTTP avec précision du referer

curl --referer http://www.server.com/index.php http://www.server.com/image/image01.jpg > image01.jpg

permet de satisfaire la règle de contrôle du referer de Apache qui empêche qu'une image soit référencée par un site concurrent.

- Téléchargement d'un fichier en HTTP en passant par un proxy

curl --proxy proxyhost:port http://www.server.com/image/image01.jpg > image01.jpg

Utilise un proxy pour effectuer la connexion sur le serveur cible. Indispensable si vous devez passer par un proxy pour sortir de votre réseau. Exemple Proxy+ (voir mon article "Démarrer avec Proxy Plus" : https://nepomiachty.developpez.com/proxyplus/).
Ou bien aussi pour des raisons moins avouables, car les logs du serveur cible ne retiendront que l'IP du proxy.

- Changer le nom du navigateur
Par défaut, lorsque cURL se connecte sur un serveur HTTP, il envoie comme valeur pour NavigatorName quelque chose comme :

"curl/7.9.8 (i386-redhat-linux-gnu) libcurl 7.9.8 (OpenSSL 0.9.7a) (ipv6 enabled)"

Si vous souhaitez changer cette valeur :

curl --user-agent "Mozilla/4.0 (compatible; MSIE 6.0;Windows NT 5.0)" http://www.server.com/image/image01.jpg > image01.jpg

Conclusion :
Toutes ces options peuvent être combinées, ce qui fait de cURL un outil extrêmement puissant.

IV-A-4 - Implémentation de cURL dans notre code

Nous allons (enfin) commencer à taper un peu de code. Je suggère d'écrire une fonction qui prendra ses paramètres au moyen d'un tableau associatif. C'est une méthode qui laisse une grande liberté pour l'appel de la fonction.
Les paramètres retenus sont : l'URL à appeler, le referrer, le proxy et le nom du navigateur. L'URL à appeler est un paramètre obligatoire, les autres paramètres passeront par le tableau.

L'appel de la fonction se fera de la façon suivante :

<?php
$HTML = get_html(
'http://www.server.com/page_lambda.php',
array(
'Referer'=>'http://www.server.com/index.php',
'Proxy'=>'monproxy:8080',
'BrowserName'=>'Mozilla/4.0 (compatible; MSIE 6.0;Windows NT 5.0)'
)
);
?>

L'utilisation d'un tableau associatif nous permet d'alléger l'appel si on a moins de paramètres :

$HTML = get_html('http://www.server.com/page_lambda.php', array('Proxy'=>'monproxy:8080');

ou encore :

$HTML = get_html('http://www.server.com/page_lambda.php');

Voici la fonction PHP :

function get_html($url, $parametres=array()) {
$params='';
if ($parametres['Referer']!="") $params.='-e '.$parametres['Referer'];
if ($parametres['Proxy']!="") $params.='-x '.$parametres['Proxy'];
if ($parametres['BrowserName']!="") $params.='-A "'.$parametres['BrowserName'].'"';
return (`curl $params $url`);
}

Notes :
a-) j'ai utilisé la notation courte de cURL :

-e = --referer
-x = --proxy
-A = --user-agent

b-) l'appel se fait en laissant un shell (guillemets inversés)
ici, on suppose que cURL est dans le PATH, sinon, il faudra préciser son chemin d'accès.

IV-B - Utilisation des sockets sous PHP

IV-B-1 - Comment se passe le dialogue entre le client et le serveur internet ?

Le dialogue HTTP se passe en connexion TCP sur le port 80. Les données échangées sont du texte. Ce dialogue est aisément reproductible grâce à la commande Telnet. Exemple d'une connexion sur le site de Yahoo :

telnet www.yahoo.com 80

Puis sur la sortie standard, tapez les trois lignes suivantes :
<entrée> signifie que vous tapez sur la touche entrée du clavier.

GET / HTTP/1.1 <entrée>
Host: www.yahoo.com <entrée>
Connection: Close <entrée><entrée>

Le source de la page HTML s'affiche sur la sortie standard et Telnet se ferme.
La première ligne signifie que la connexion se déroule suivant le protocole HTTP/1.1 La page demandée est la racine du site. Pour demander la page correspondant à l'URL http://www.yahoo.com/news/europ_news.html (exemple fictif) la première ligne devient :

GET /news/europ_news.html HTTP/1.1 <entrée>

IV-B-2 - Implémentation sous PHP

Il faut reproduire le même dialogue au moyen des fonctions réseau de PHP. La fonction à écrire n'a pas besoin de prendre autant de paramètres que celle écrite précédemment pour la fonction cURL, car certaines options comme l'utilisation d'un proxy n'est pas possible. Par souci de simplification, cette fonction ne prend en paramètre que l'URL.

<?php
function get_html($url) {
if (strtoupper(substr($url,0,7))=="HTTP://") $url=substr($url,7);
$p = strpos($url,"/");
if ($p===FALSE) {
$nom_domaine=$url;
$get="/";
}
else {
$nom_domaine=substr($url,0,$p);
$get=substr($url,$p);
}

$errno=""; $errstr=""; $r="";
$fp = fsockopen($nom_domaine, 80, &$errno, &$errstr, 15);
if($fp) {
socket_set_timeout($fp, 15);
fputs($fp,"GET $get HTTP/1.1\r\n");
fputs($fp,"Host: $nom_domaine\r\n");
fputs($fp,"Connection: Close\r\n\r\n");
$r="";
while(!feof($fp)) {
$r.=fgets($fp,1024);
}
fclose($fp);
return($r);
}
return('');
}
?>

Explication
Cette fonction prend en entrée une chaîne correspondant à l'URL. Elle peut commencer par http://, ces caractères seront supprimés. Ensuite, elle sépare l'URL en deux variables le nom de domaine ($nom_domaine) et la page sollicitée ($get).
La commande fsockopen initie le dialogue avec le serveur. socket_set_timeout règle le temps de réponse maximum du serveur à 15 secondes.
fputs écrit sur le flux ouvert. La réponse se lit immédiatement avec fgets (boucle while). Le dialogue est terminé lorsque le flux est vide, mais on le ferme proprement avec fclose.

V - L'application PHP - architecture

V-A - Fonctionnement de l'application

L'application va récolter les informations en deux temps :

1) à partir d'une liste de films contenue dans un fichier texte, elle va interroger le moteur de recherche du site internet. À chaque nom de film correspondront une ou plusieurs propositions. Ces choix seront suggérés à l'utilisateur final. Ce dernier cochera le bouton radio correspondant au bon titre. La première suggestion sera précochée (statistiquement, ce sera presque toujours le bon choix).
Afin d'optimiser le temps de traitement de l'opérateur, les propositions seront affichées sur le navigateur au fur et à mesure (au lieu de s'afficher toutes à la fin du chargement de la page). Cette même page proposera à l'utilisateur les trois formats d'exportation possibles : texte, XML et MySQL ;

2) ensuite, l'application appellera individuellement chaque fiche de film et récoltera les informations. Leur traitement dépendra du format d'exportation choisi : page HTML, flux XML, insertion dans la base MySQL.

V-B - Architecture de l'application

L'application va s'articuler autour de deux fichiers : extraction1.php et extraction2.php qui vont respectivement exécuter les tâches 1) et 2) décrites dans le paragraphe précédent.
Ces fichiers utiliseront deux bibliothèques :

connect.php :
définition de l'environnement. Utilisation de cURL ou des sockets PHP, connexion éventuelle à la base MySQL.

<?php
Define("CONNEXION","curl");
//Define("CONNEXION","php");

Define("MYSQL",true);
//Define("MYSQL",false);

if (MYSQL) {
$host="localhost";
$user="user_base";
$pwd="user_password";
$base="DVD";
$mysql_link = mysql_connect($host,$user,$pwd);
mysql_select_db($base);
}
?>

lib.php :
regroupe les fonctions appelées dans extraction1.php et extraction2.php. Par exemple, la fonction get_html() va regrouper les deux possibilités de connexion (cURL ou socket) en fonction des constantes définies dans connect.php

<?php
function get_html($url, $parametres=array()) {
if (CONNEXION=="curl") {
$params='';
if ($parametres['Referer']!="") $params.='-e '.$parametres['Referer'];
if ($parametres['Proxy']!="") $params.='-x '.$parametres['Proxy'];
if ($parametres['BrowserName']!="") $params.='-A "'.$parametres['BrowserName'].'"';
return (`curl $params $url`);
}
else {
if (strtoupper(substr($url,0,7))=="HTTP://") $url=substr($url,7);
$p = strpos($url,"/");
if ($p===FALSE) {
$nom_domaine=$url;
$get="/";
}
else {
$nom_domaine=substr($url,0,$p);
$get=substr($url,$p);
}

$errno=""; $errstr=""; $r="";
$fp = fsockopen($nom_domaine, 80, &$errno, &$errstr, 15);
if($fp) {
socket_set_timeout($fp, 15);
fputs($fp,"GET $get HTTP/1.1\r\n");
fputs($fp,"Host: $nom_domaine\r\n");
fputs($fp,"Connection: Close\r\n\r\n");
$r="";
while(!feof($fp)) {
$r.=fgets($fp,1024);
}
fclose($fp);
return($r);
}
return('');
}
}
?>

VI - L'application PHP - code partie 1 - Extraction des données : la liste des films

fonctionnement de l'application
Les chapitres VI et VII présentent le fonctionnement détaillé de l'application avec des exemples de code en PHP. Le chapitre VI aborde l'extraction de la liste des films (extraction1.php), le chapitre VII celle de la fiche film.

VI-A - Liste des films à rechercher

La liste des films à rechercher est contenue dans un fichier texte liste_films.txt qui sera ouvert par PHP. On utilisera le signe « # » en début de ligne pour la placer en commentaire. Cette convention permet d'assouplir la phase de test et d'éviter de jongler entre différents fichiers d'entrée.

Exemple de fichier :

# (c) Olivier Népomiachty, février 2004
# olivier.nepomiachty@developpez.com
ed tv
jackie brown
# anastasia
pulp fiction
chicken run
shining

Voici le code PHP de lecture du fichier texte :

<?php
$handle = fopen ("liste_films.txt", "r");
while (!feof ($handle)) {
$titre_recherche = trim(fgets($handle, 4096));
if (!(ereg("^#", $titre_recherche)) && ($titre_recherche!='')) {
... TRAITEMENT ...
}
fclose ($handle);
?>

La liste sera ensuite soumise au site de AlloCiné. L'URL sera appelée par cURL ou en socket PHP. L'URL sera de la forme http://www.allocine.fr/recherche/default.html?motcle=titredufilm
Cependant, pour que l'URL soit valide, il faut l'encoder :

<?php
$titre_recherche_url = urlencode($titre_recherche);
$url="http://www.allocine.fr/recherche/default.html?motcle=$titre_recherche_url";
?>

Cette manipulation permet de convertir les caractères spéciaux dans leur valeur hexa pour un envoi en GET sur URL. Par exemple, le caractère espace est converti en . 20 en hexa correspond à 32 en décimal, qui correspond au code ASCII de l'espacement.

VI-B - Extraction des données - La liste des réponses

Recommençons une recherche sur le film "shining". Voici la liste des réponses :

Il y a quatre propositions. L'étude du code HTML de la page permettra de définir une méthode d'extraction de données. Celle-ci doit être la plus simple et intuitive possible, de façon qu'une petite modification du template utilisé sur le site permette toujours que l'extraction soit possible. D'une façon évidente, une refonte graphique importante du site impliquera la réécriture des règles d'extraction.

1) Voici le dump du code HTML concerné. Lorsque l'on regarde le source complet de la page, c'est le morceau commençant par : "dans les titres de films déjà sortis" et finissant par « </TABLE> ». Nous conviendrons d'appeler ce morceau « bloc de réponse ».

<TR><TD><FONT class=size2 color=#AA0000><B>4 réponses dans les titres de films déjà sortis :</B></FONT></TD></TR>
<TR><TD><IMG Border=0 Src='http://a69.g.akamai.net/7/69/10688/v1/img5.allocine.fr/acmedia/skin/AlloCineV4/image/habillage/empty.gif' Width=1 Height=1></TD></TR>
<TR><TD><LI><A HREF="/film/fichefilm_gen_cfilm=863.html"><FONT color=#003399><B>Shining</B></FONT></A> (The <B>Shining</B>) de Stanley Kubrick avec Jack Nicholson, Shelley Duvall (1980)</TD></TR>
<TR><TD><LI><A HREF="/film/fichefilm_gen_cfilm=28224.html"><FONT color=#003399>Une lueur dans la nuit</FONT></A> (<B>Shining</B> through) de David Seltzer avec Michael Douglas, Melanie Griffith (1992)</TD></TR>
<TR><TD><LI><A HREF="/film/fichefilm_gen_cfilm=934.html"><FONT color=#003399>L' Ensorceleuse</FONT></A> (The <B>Shining</B> Hour) de Frank Borzage (1938)</TD></TR>
<TR><TD><LI><A HREF="rubrique.html?typerecherche=3&motcle=shining" class=size2><B>Plus…</B></A></TD></TR></TABLE>

2) Chaque ligne de réponse est contenue dans une ligne de tableau, suivant la structure :

<TR>
<TD>
<LI> ligne de réponse
</TD>
</TR>

(ici le code est indenté pour une meilleure lisibilité)

Pour récupérer une ligne, on repère le texte contenu entre « <TR><TD><LI> » et « </TD></TR> ».
La ligne est de la forme :

<TR><TD><LI><A HREF="/film/fichefilm_gen_cfilm=863.html"><FONT color=#003399><B>Shining</B></FONT></A> (The <B>Shining</B>) de Stanley Kubrick avec Jack Nicholson, Shelley Duvall (1980)</TD></TR>

3) Le titre du film s'obtient en appliquant un strip_tags à la ligne.

4) L'URL de la fiche du film est celle contenue dans la balise <A HREF>. On la récupère en prenant le texte compris entre « <A HREF= » et « > ». Pour obtenir une URL absolue, on lui rajoute http://www.allocine.fr/, ce qui donne :
http://www.allocine.fr/film/fichefilm_gen_cfilm=863.html

5) Ensuite, on affiche sur la sortie HTML une liste de boutons radio, chacun correspondant à une des possibilités :

<input type="radio" name="film_1" value="http://www.allocine.fr/film/fichefilm_gen_cfilm=28224.html" > <a href="http://www.allocine.fr/film/fichefilm_gen_cfilm=28224.html" target="_new">Une lueur dans la nuit (Shining through) de David Seltzer avec Michael Douglas, Melanie Griffith (1992)</a>

Le dernier bouton permet d'annuler l'extraction de données pour ce film. Un lien HREF mis sur le titre permet d'afficher dans une nouvelle fenêtre la fiche dans son contexte. Cette option permet d'aider l'opérateur dans sa décision lorsqu'il hésite entre plusieurs titres.

Une fois le groupe de boutons radio affiché, on flush la sortie HTML et le processus repart en 2).

VI-C - Le flush sur la sortie HTML

Faisons tomber le mythe autour de ce procédé. Cette technique permet de forcer l'affichage sur le navigateur du contenu de la page avant la fin de son chargement. Ceci permet de traiter de longues opérations et d'afficher un résultat intermédiaire sans déclencher le timeout PHP. Ce dernier se déclenchera uniquement si le temps écoulé entre deux flushs est trop long.

Conditions de fonctionnement de flush

1) Le code HTML envoyé sur le navigateur doit être correctement balisé. Toutes les balises doivent être fermées à l'exception de « </body> » et « </HTML> ». Si vous avez ouvert un tableau pour améliorer la présentation de la page, le navigateur ne saura pas terminer votre tableau et par conséquent gèlera l'affichage jusqu'au prochain flush (modulo que le tableau soit fermé à ce moment).

2) Le flush ne fonctionne que si un minimum de caractères a été envoyé. Pour le forcer, on remplit le tampon de sortie avec des espaces.
Code à placer à la fin du bloc de lecture du fichier texte « while (!feof ($handle)) { » :

<?php
echo str_pad(" ",300);
echo "\n";
ob_flush();
flush();
?>

L'opérateur choisit le type de données en sortie (HTML, XML, insertion MySQL).
Le traitement se poursuit dans le script extraction2.php.

VII - L'application PHP - code partie 2 - Extraction des données : les fiches films

VII-A - Principe

Le procédé est similaire à celui décrit précédemment.
Il s'agit de repérer dans la page HTML le bloc de code contenant les informations.

Le résultat sera placé dans un tableau associatif $film qui contiendra les champs :
'TITRE', 'PAYS', 'ANNEE', 'GENRES', 'DUREE', 'DATE_DE_SORTIE', 'ACTEURS', 'REALISATEUR' et 'SYNOPSIS'.
Les champs 'GENRES' et 'ACTEURS' sont eux-mêmes des tableaux.
$film sera ensuite donné en paramètre à la fonction de traitement correspondant.

VII-B - L'extraction

1) Voici le dump du code HTML concerné. Lorsque l'on regarde le source complet de la page, c'est le morceau commençant par la balise « <FONT Class="titrePage"> » et finissant par « </TABLE> ». Nous conviendrons d'appeler ce morceau « bloc fiche ».

<FONT Class="titrePage">Jackie Brown</FONT></TD></TR><TR>
<TD><font >Film</font> <FONT Class="size2">américain</FONT> <font >(1997)</font>. <FONT Class="size2">Policier</FONT>, <FONT Class="size2">Drame</FONT>. <FONT Class="size2">Durée : 2h 30mn. </FONT></TD></TR><TR><TD><FONT Class="titreDescription">Date de sortie : </FONT><A href='/film/agenda_gen_semaine=01/04/1998.html#Semaine' Class="link1">01 Avril 1998</A></TD></TR>
<TR><TD><FONT Class="titreDescription">Avec </FONT><A Href="/personne/fichepersonne_gen_cpersonne=20218.html" Class="link1">Pam Grier</A>, <A Href="/personne/fichepersonne_gen_cpersonne=14454.html" Class="link1">Samuel L. Jackson</A>, <A Href="/personne/fichepersonne_gen_cpersonne=8.html" Class="link1">Robert De Niro</A>, <A Href="/personne/fichepersonne_gen_cpersonne=11404.html" Class="link1">Bridget Fonda</A>, <A Href="/personne/fichepersonne_gen_cpersonne=11657.html" Class="link1">Michael Keaton</A> <a href="/film/casting_gen_cfilm=16876.html" class="link14">Plus…</a></TD></TR>
<TR><TD><FONT Class="titreDescription">Réalisé par </FONT><A Href="/personne/fichepersonne_gen_cpersonne=15570.html" Class="link1">Quentin Tarantino</A></TD></TR></TABLE>

2) Si on regarde attentivement la fiche, il est possible d'effectuer un traitement assez simple :
- remplacement des fins de ligne de tableau par un retour chariot ;
- suppression du HTML.
Le résultat donne :

Jackie Brown
Film américain (1997). Policier, Drame. Durée : 2h 30mn.
Date de sortie : 01 Avril 1998
Avec Pam Grier, Samuel L. Jackson, Robert De Niro, Bridget Fonda, Michael Keaton Plus…
Réalisé par Quentin Tarantino

L'extraction des données est facile à appréhender !

Le traitement est en fait un tout petit peu plus compliqué. Il est réalisé dans la fonction suivante :

<?php
function filtre_html($s) {
$s = str_replace("\r\n","",$s);
$s = str_replace("\n","",$s);
$s = str_replace("</TR>","</TR>\n",$s);
$s = strip_tags($s);
$s = str_replace(" "," ",$s);
return($s);
}
?>

Il n'est pas nécessaire de s'attarder sur le code d'extraction. Le script PHP est commenté.

<?php
$HTML = get_html($url,$parametres);

$pos1 = strpos($HTML, "<FONT Class=\"titrePage\">");
$pos2 = strpos($HTML, "<FONT Class=link7>Synopsis</FONT>",$pos1);
$data_brut = filtre_html(substr($HTML,$pos1,$pos2-$pos1));

/* suite à cette manipulation $data_brut est de la forme :
l0 : Jackie Brown
l1 : Film américain (1997). Policier, Drame. Durée : 2h 30mn.
l2 : Date de sortie : 01 Avril 1998
l3 : Avec Pam Grier, Samuel L. Jackson, Robert De Niro, Bridget Fonda, Michael Keaton Plus...
l4 : Réalisé par Quentin Tarantino
(etc)
*/
$data=split("\n", $data_brut);
$no_ligne=0;
// ligne 0 : titre
$titre = trim($data[$no_ligne]);

// ligne 1 :
$no_ligne++;
list($pays, $genres, $duree) = split("\.", $data[$no_ligne]);
// $pays = "Film américain (1997)"
$pos1b=strpos_reverse($pays,'(', strlen($pays));
$pos2b=strpos($pays,')',$pos1b);
$annee = trim(substr($pays,$pos1b+1,$pos2b-$pos1b-1));
$pays=trim(substr($pays,0,$pos1b));
// $genres : tableau
// $genres = "Policier, Drame"
$genres = explode(",",$genres);
trim_tableau($genres);
// $duree = "Durée : 2h 30mn";
// convertie en minutes
$duree = trim(str_replace("Durée : ","",$duree));
$duree = hoursmin_to_minutes($duree);
if ($duree==0) $duree='';

// ligne 2 :
// $date_de_sortie = "Date de sortie : 01 Avril 1998"
// attention, cette ligne n'est pas obligatoire
$no_ligne++;
if (strpos($data[$no_ligne],'Date de sortie')===FALSE)
$date_de_sortie = "";
else {
$date_de_sortie = trim(str_replace("Date de sortie : ","",$data[$no_ligne]));
$no_ligne++;
}

// ligne 3 :
// $acteurs : tableau
// $acteurs = "Avec Pam Grier, Samuel L. Jackson, Robert De Niro, Bridget Fonda, Michael Keaton Plus...";
$acteurs = trim(str_replace("Avec","",$data[$no_ligne]));
$acteurs = trim(str_replace("Plus...","",$acteurs));
$acteurs=explode(",",$acteurs);
trim_tableau($acteurs);
$no_ligne++;

// ligne 4 :
// $réalisateur = "Réalisé par Quentin Tarantino"
$realisateur = trim(str_replace("Réalisé par ","",$data[$no_ligne]));

// extraction du synopsis
// on reprend le fichier à partir de $pos2 (position du Synopsis)
$pos1 = $pos2;
$pos2 = strpos($HTML, "</FONT></DIV></TD></TR>",$pos1);
$synopsis = filtre_html(substr($HTML,$pos1,$pos2-$pos1));
$synopsis = trim(str_replace("Synopsis","",$synopsis));
?>

On notera l'utilisation de fonctions situées dans lib.php comme hoursmin_to_minutes(). Cette fonction convertit une durée de la forme « 2h 29mn » en son équivalent en minutes :

<?php
function hoursmin_to_minutes($s) {
// convertit une heure "2h 29mn" en minutes => 149
$r=0;
$s=trim($s);
$pos1 = strpos($s, "h");
if ((!$pos1===FALSE)) {
$r=substr($s,0,$pos1);
$r=((int)substr($s,0,$pos1)) * 60;
$pos2 = strpos($s, "mn", $pos1);
if ((!$pos2===FALSE)) {
$r += (int)trim(substr($s,$pos1+1,$pos2-$pos1-1));
}
}
return($r);
}
?>

Le résultat est stocké dans les variables : $titre, $pays, $annee, $genres, $duree, $date_de_sortie, $acteurs, $realisateur, $synopsis.
Ensuite, ces variables viennent alimenter un tableau associatif :

<?php
$film = array (
'TITRE' => $titre,
'PAYS' => $pays,
'ANNEE' => $annee,
'GENRES' => $genres,
'DUREE' => $duree,
'DATE_DE_SORTIE' => $date_de_sortie,
'ACTEURS' => $acteurs,
'REALISATEUR' => $realisateur,
'SYNOPSIS' => $synopsis
);
?>

Puis, le traitement se poursuit en fonction du choix de la sortie :

<?php
if ($HTTP_POST_VARS['format_resultat']=="HTML") {
... TRAITEMENT SORTIE HTML ...
}
else if ($HTTP_POST_VARS['format_resultat']=="xml") {
... TRAITEMENT FLUX XML ...
}
else if ($HTTP_POST_VARS['format_resultat']=="mysql") {
... TRAITEMENT INSERTION MySQL ...
}
?>

VII-C - Sortie HTML

C'est le traitement le plus simple. Il n'est là que pour contrôler le fonctionnement du script.

<?php
if ($HTTP_POST_VARS['format_resultat']=="HTML") {
echo genere_html_film($film)."<br>\n";
}
?>

La fonction genere_html_film() crée une chaîne HTML contenant les informations. On notera que les tableaux ont un traitement particulier (boucle foreach).

<?php
function genere_html_film($a) {
$r = "<b>Titre :</b> " . $a['TITRE'] . "<br>\n";
$r .= "<b>Pays :</b> " . $a['PAYS'] . "<br>\n";
$r .= "<b>Année :</b> " . $a['ANNEE'] . "<br>\n";
$r .= "<b>Genres :</b> ";
foreach($a['GENRES'] as $i)
$r .= "$i, ";
$r = substr($r, 0, strlen($r) - 2) . "<br>\n";
$r .= "<b>Durée :</b> " . $a['DUREE'] . " minutes<br>\n";
$r .= "<b>Date de sortie :</b> " . $a['DATE_DE_SORTIE'] . "<br>\n";
$r .= "<b>Acteurs :</b> ";
foreach($a['ACTEURS'] as $i)
$r .= "$i, ";
$r = substr($r, 0, strlen($r) - 2) . "<br>\n";
$r .= "<b>Réalisateur :</b> " . $a['REALISATEUR'] . "<br>\n";
$r .= "<b>Synopsis :</b> " . $a['SYNOPSIS'] . "<br>\n";
return($r);
}
?>

VII-D - Flux XML

Le traitement est presque identique au précédent. En amont, il a fallu préciser l'en-tête de la page :

<?xml version="1.0" encoding="ISO-8859-1"?>

et ouvrir une balise XML <LISTE_FILMS>. Elle contiendra tous les films, chacun placé dans une balise <FILM>.

<?php
else if ($HTTP_POST_VARS['format_resultat']=="xml") {
echo genere_xml_film($film);
}
?>

La sortie XML sera de la forme :

<?xml version="1.0" encoding="ISO-8859-1" ?>
- <LISTE_FILMS>
- <FILM>
<TITRE>Jackie Brown</TITRE>
<PAYS>Film américain</PAYS>
<ANNEE>1997</ANNEE>
<GENRE>Policier</GENRE>
<GENRE>Drame</GENRE>
<DUREE>150</DUREE>
<DATE_DE_SORTIE>01 Avril 1998</DATE_DE_SORTIE>
<ACTEUR>Pam Grier</ACTEUR>
<ACTEUR>Samuel L. Jackson</ACTEUR>
<ACTEUR>Robert De Niro</ACTEUR>
<ACTEUR>Bridget Fonda</ACTEUR>
<ACTEUR>Michael Keaton</ACTEUR>
<REALISATEUR>Quentin Tarantino</REALISATEUR>
<SYNOPSIS>Jackie Brown, hôtesse de l'air, arrondit ses fins de mois en convoyant de l'argent liquide pour le compte d'un trafiquant d'armes, Ordell Robbie. Un jour, un agent federal et un policier de Los Angeles la cueillent à l'aéroport. Ils comptent sur elle pour faire tomber le trafiquant. Jackie échafaude alors un plan audacieux pour doubler tout le monde lors d'un prochain transfert qui porte sur la modeste somme de cinq cent mille dollars. Mais il lui faudra compter avec les complices d'Ordell, qui ont des méthodes plutôt expéditives.</SYNOPSIS>
</FILM>
</LISTE_FILMS>

Il est possible d'exploiter directement le XML en appelant extraction2.php depuis une application, avec les mêmes paramètres que ceux envoyés en POST depuis le script extraction1.php, et de récupérer le flux.

VII-E - Insertion MySQL

L'insertion se fait sur les tables principales : PAYS, REALISATEUR, FILM, ACTEUR et GENRE, ensuite sur les tables associatives ACTEUR_FILM et GENRE_FILM.
Avant une insertion, on vérifie que l'enregistrement n'existe pas dans la base. Cette opération étant très répétitive, elle a été factorisée en une fonction : insertion_table().
Les paramètres : la valeur à insérer, le nom de la table, le nom de l'index primaire de la table et le nom du champ. Si l'enregistrement existe, son id est retourné, sinon, l'enregistrement est créé et le nouvel id retourné.

<?php
function insertion_table($element, $table, $id_primaire_table, $nom_champ_element) {
// insertion de l'élément $element dans la table $table et renvoie son id
// si l'élément existe déjà, retourne son id
global $mysql_link;
$element=trim($element);
$sql = "SELECT $id_primaire_table FROM $table WHERE $nom_champ_element LIKE '$element'";
$result = mysql_query($sql, $mysql_link);
if (mysql_num_rows($result)==0) {
mysql_free_result($result);
$sql = "INSERT INTO $table($nom_champ_element) VALUES('$element')";
mysql_query($sql, $mysql_link);
$id_primaire_table = mysql_insert_id($mysql_link);
}
else {
$row = mysql_fetch_row($result);
$id_primaire_table = $row[0];
mysql_free_result($result);
}
return($id_primaire_table);
}
?>

Pour les besoins de notre application, une fonction similaire existe_table() vérifie l'existence d'un enregistrement dans une table et retourne son id, ou 0 s'il n'existe pas.

Voici le code d'insertion dans la base :

<?php
// ajout du film
$id_film = existe_table($film['TITRE'], 'FILM', 'ID_FILM', 'TITRE');
if ($id_film==0) {
// ajout du pays
$id_pays=insertion_table($film['PAYS'], 'PAYS', 'ID_PAYS', 'NOM_PAYS');
// ajout du réalisateur
$id_realisateur=insertion_table($film['REALISATEUR'], 'REALISATEUR', 'ID_REALISATEUR', 'NOM_REALISATEUR');
if ($film['ANNEE']!='') $film['ANNEE']=$film['ANNEE'].'-01-01';
$sql="INSERT INTO FILM(TITRE, PAYS_ID, ANNEE, DUREE, DATE_SORTIE, REALISATEUR_ID, SYNOPSIS)
VALUES('".addslashes($film['TITRE'])."', $id_pays, '".$film['ANNEE']."', '".$film['DUREE']."',
'".$film['DATE_DE_SORTIE']."', $id_realisateur, '".addslashes($film['SYNOPSIS'])."')";
mysql_query($sql, $mysql_link);
$id_film=mysql_insert_id($mysql_link);

// ajout des acteurs
foreach($film['ACTEURS'] as $acteur) {
$id_acteur=insertion_table($acteur, 'ACTEUR', 'ID_ACTEUR', 'NOM_ACTEUR');
$sql="INSERT INTO ACTEUR_FILM(ACTEUR_ID,FILM_ID) VALUES($id_acteur, $id_film)";
mysql_query($sql, $mysql_link);
}

// ajout des genres
foreach($film['GENRES'] as $acteur) {
$id_genre=insertion_table($acteur, 'GENRE', 'ID_GENRE', 'NOM_GENRE');
$sql="INSERT INTO GENRE_FILM(GENRE_ID,FILM_ID) VALUES($id_genre, $id_film)";
mysql_query($sql, $mysql_link);
}
}
?>

VIII - Sources du tutoriel et exemple

Les sources du tutoriel sont disponibles au format .tar.gz ou .zip.

J'ai écrit un mini formulaire de recherche permettant d'interroger la base MySQL : recherche.php ; les résultats s'affichent dans la page resultat.php. Ces fichiers sont présents dans les archives tar.gz et zip.

IX - Remerciements

Alacazam pour sa relecture attentive.