Python >> Tutoriel Python >  >> Python Tag >> RegEx

Python Webscraper Regex [Tutoriel de chapitre de livre gratuit]

Ce tutoriel est un extrait de chapitre rédigé pour mon nouveau livre "Python One-Liners" (à paraître en 2020, No Starch Press, San Francisco).

Êtes-vous un employé de bureau, un étudiant, un développeur de logiciels, un gestionnaire, un blogueur, un chercheur, un auteur, un rédacteur, un enseignant ou un travailleur indépendant ? Très probablement, vous passez de nombreuses heures devant votre ordinateur, jour après jour. Dans tous les cas, l'amélioration de votre productivité quotidienne - seulement d'une petite fraction de pourcentage - représentera des milliers, voire des dizaines de milliers de dollars de gain de productivité. Et plus important encore, si vous ne vous contentez pas de chronométrer votre temps au travail, l'amélioration de la productivité de votre ordinateur vous donnera plus de temps libre à utiliser de manière plus efficace.

Ce chapitre vous montre une technologie extrêmement sous-estimée qui aide les maîtres codeurs à utiliser plus efficacement leur temps lorsqu'ils travaillent avec des données textuelles. La technologie est appelée "expressions régulières". Ce chapitre vous montre dix façons d'utiliser des expressions régulières pour résoudre des problèmes quotidiens avec moins d'effort, de temps et d'énergie. Étudiez attentivement ce chapitre sur les expressions régulières, cela vaudra votre investissement en temps !

Article connexe : Python Regex Superpower - Le guide ultime

Écrire votre premier scraper Web avec des expressions régulières

Pourquoi devriez-vous vous soucier des expressions régulières ? Parce que vous les rencontrerez régulièrement si vous poursuivez une carrière en programmation.

Supposons que vous travaillez en tant que développeur de logiciels indépendant. Votre client est une startup Fintech qui doit se tenir au courant des derniers développements dans le domaine des crypto-monnaies. Ils vous engagent pour écrire un grattoir Web qui extrait régulièrement le code source HTML des sites Web d'actualités et y recherche des occurrences de mots commençant par 'crypto' (par exemple 'cryptocurrency' , 'crypto-bot' , 'crypto-crash' , …).

Votre première tentative est l'extrait de code suivant :

import urllib.request

search_phrase = 'crypto'

with urllib.request.urlopen('https://www.wired.com/') as response:
   html = response.read().decode("utf8") # convert to string
   first_pos = html.find(search_phrase)
   print(html[first_pos-10:first_pos+10])

Essayez-le vous-même  :Utilisez notre shell Python de navigateur interactif pour exécuter ce code de manière interactive :

Exercice  :recherchez d'autres mots sur le site Web câblé à l'aide de ce grattoir Web !

La méthode urlopen (du module urllib.request ) extrait le code source HTML de l'URL spécifiée. Comme le résultat est un tableau d'octets, vous devez d'abord le convertir en chaîne en utilisant la méthode decode() . Ensuite, vous utilisez la méthode de chaîne find() qui renvoie la position de la première occurrence de la chaîne recherchée. Avec le découpage en tranches, vous découpez une sous-chaîne qui renvoie l'environnement immédiat de la position. Le résultat est la chaîne suivante :

# ,r=window.crypto||wi

Aww. Ça a l'air mauvais. Il s'avère que la phrase de recherche est ambiguë - la plupart des mots contenant 'crypto' sont sémantiquement sans rapport avec les crypto-monnaies. Votre grattoir Web génère des faux positifs (il trouve des résultats de chaîne que vous ne vouliez pas trouver à l'origine).[1] Alors, comment pouvez-vous résoudre ce problème ?

Heureusement, vous venez de lire ce livre Python, la réponse est donc évidente :les expressions régulières ! Votre idée pour supprimer les faux positifs est de rechercher les occurrences où le mot "crypto" est suivi de 30 caractères arbitraires maximum, suivi du mot "coin" . En gros, la requête de recherche est :"crypto" + <up to 30 arbitrary characters> + "coin" . Prenons les deux exemples suivants :

  • "crypto-bot that is trading Bitcoin" — OUI
  • "cryptographic encryption methods that can be cracked easily with quantum computers" — NON

Une expression régulière est comme un mini-langage de programmation à l'intérieur de Python qui vous permet de rechercher une chaîne pour les occurrences d'un modèle de requête. Les expressions régulières sont beaucoup plus puissantes que la fonctionnalité de recherche textuelle par défaut, comme indiqué ci-dessus. Par exemple, l'ensemble des chaînes de requête peut même avoir une taille infinie !

Notre objectif est de résoudre le problème suivant :Étant donné une chaîne, trouver les occurrences où la chaîne "crypto" est suivie de 30 caractères arbitraires maximum, suivis de la chaîne "coin" .

Jetons un premier coup d'œil au résultat avant de discuter, étape par étape, de la manière dont le code résout le problème.

## Dependencies
import re


## Data
text_1 = "crypto-bot that is trading Bitcoin and other currencies"
text_2 = "cryptographic encryption methods that can be cracked easily with quantum computers"


## One-Liner
pattern = re.compile("crypto(.{1,30})coin")


## Result
print(pattern.match(text_1))
print(pattern.match(text_2))

Solution en une seule ligne pour trouver des extraits de texte sous la forme crypto … coing.

Le code recherche deux chaînes différentes text_1 et text_2 . La requête de recherche (modèle) leur correspond-elle ?

Tout d'abord, nous importons le package standard pour les expressions régulières en Python, appelé re. Les choses importantes se passent dans le one-liner u où vous compilez la requête de recherche "crypto(.{1,30})coin" (appelé motif dans la terminologie des regex). C'est la requête que nous pouvons ensuite rechercher dans différentes chaînes. Nous utilisons les caractères regex spéciaux suivants. Lisez-les de haut en bas et vous comprendrez la signification du motif dans l'extrait de code ci-dessus.

  • () correspond à n'importe quelle expression régulière à l'intérieur,
  • . correspond à un caractère arbitraire,
  • {1,30} correspond entre 1 et 30 occurrences de la regex précédente,
  • (.{1,30}) correspond entre 1 et 30 caractères arbitraires, et
  • crypto(.{1,30})coin correspond à l'expression régulière composée de trois parties :le mot "crypto" , une séquence arbitraire de 1 à 30 caractères, suivie du mot "pièce".

On dit que le motif est compilé car Python crée un objet modèle qui peut être réutilisé à plusieurs endroits, un peu comme un programme compilé peut être exécuté plusieurs fois. Maintenant, nous appelons la fonction match() sur notre modèle compilé et le texte à rechercher. Cela conduit au résultat suivant :

## Result
print(pattern.match(text_1))
# <re.Match object; span=(0, 34), match='crypto-bot that is trading Bitcoin'>

print(pattern.match(text_2))
# None

Chaîne text_1 correspond au modèle (indiqué par l'objet match résultant), chaîne text_2 non (indiqué par le résultat None ). Bien que la représentation textuelle du premier objet correspondant ne semble pas jolie, elle indique clairement que la chaîne donnée 'crypto-bot that is trading Bitcoin' correspond à l'expression régulière.

Rechercher des modèles textuels de base dans les chaînes

À ce stade, vous avez appris le moyen le plus puissant de trouver des modèles textuels arbitraires dans les chaînes :les expressions régulières. Partons de là en introduisant l'important re.findall() fonction. En outre, il explique plusieurs expressions régulières de base plus en détail.

Une expression régulière (en abrégé :regex) décrit formellement le modèle de recherche en utilisant une combinaison de quelques commandes de base. Apprenez ces commandes de base et vous comprendrez facilement les expressions régulières complexes. Dans cette section d'une seule ligne, nous nous concentrerons sur les trois commandes regex les plus importantes.

Le point Regex (.)

Tout d'abord, vous devez savoir comment faire correspondre un caractère arbitraire à l'aide de la regex point (.). L'expression régulière point correspond à n'importe quel caractère. Vous pouvez l'utiliser pour indiquer que vous ne vous souciez vraiment pas du caractère qui correspond, tant qu'il y en a exactement un.

import re

text = '''A blockchain, originally block chain,
is a growing list of records, called blocks,
which are linked using cryptography.
'''

print(re.findall('b...k', text))
# ['block', 'block', 'block']

L'exemple utilise le findall() méthode du re package. Le premier paramètre est la regex elle-même :nous recherchons n'importe quel modèle de chaîne commençant par le caractère 'b' , suivi de trois caractères arbitraires (les points …), suivi du caractère 'k' . Notez que non seulement la chaîne 'block' une correspondance mais aussi 'boook' , 'b erk' , et 'bloek' . Le deuxième paramètre est le texte à rechercher. La chaîne de texte contient trois de ces modèles. Il s'agit du résultat de l'instruction d'impression.

L'expression régulière astérisque (*)

Deuxièmement, vous devez savoir comment faire correspondre un nombre arbitraire de caractères spécifiques à l'aide de l'expression régulière astérisque (*).

print(re.findall('y.*y', text))
# ['yptography']

L'opérateur astérisque s'applique à l'expression régulière juste devant. Dans l'exemple, le modèle regex commence par le caractère 'y' , suivi d'un nombre arbitraire de caractères (.*) , suivi du caractère 'y' . Le mot 'cryptography' contient une telle instance.

Si vous lisez attentivement ceci, vous vous demandez peut-être pourquoi il ne trouve pas la longue sous-chaîne entre 'originally' et 'cryptography' qui doit correspondre au modèle regex 'y.*y' , aussi bien. La raison en est simplement que l'opérateur astérisque correspond à un nombre arbitraire de caractères, mais n'inclut pas les retours à la ligne. Sémantiquement, la fin de la ligne réinitialise l'état de la recherche de la regex. Dans la ligne suivante, une nouvelle recherche est lancée. La chaîne stockée dans le texte variable est une chaîne multiligne avec trois nouvelles lignes.

Le point d'interrogation Regex (?)

Troisièmement, vous devez savoir comment faire correspondre zéro ou un caractère à l'aide de l'expression régulière point d'interrogation (?).

print(re.findall('blocks?', text))
# ['block', 'block', 'blocks']

La regex zéro ou un (?) s'applique à la regex immédiatement devant elle. Dans notre cas, il s'agit du caractère 's' . La signification de l'expression régulière zéro ou un est que ce caractère est facultatif.

Un détail important est que le point d'interrogation peut être combiné avec l'opérateur astérisque '*?' pour permettre une correspondance de modèle non gourmande. En revanche, si vous utilisez l'opérateur astérisque '*' sans le point d'interrogation, il associe avidement autant de caractères que possible. Par exemple, lors de la recherche de la chaîne HTML '<div>hello world</div>' en utilisant la regex '<.*>' , il correspond à la chaîne entière '<div>hello world</div>' plutôt que seulement le préfixe '<div>' . Si vous souhaitez atteindre ce dernier, vous pouvez donc utiliser la regex non gourmande '<.*?>' :

txt = '<div>hello world</div>'

print(re.findall('<.*>', txt))
# ['<div>hello world</div>']

print(re.findall('<.*?>', txt))
# ['<div>', '</div>']

Équipé de ces trois outils, vous êtes maintenant en mesure de comprendre la prochaine solution en une seule ligne.

Notre objectif est de résoudre le problème suivant :« Étant donné une chaîne. Utilisez une approche non gourmande pour trouver tous les modèles qui commencent par le caractère 'p' , se termine par le caractère 'r' , et ont une occurrence du caractère 'e' (et un nombre arbitraire d'autres caractères) entre les deux ! Ces types de requêtes textuelles se produisent assez fréquemment, en particulier dans les entreprises qui se concentrent sur le traitement de texte, la reconnaissance vocale ou la traduction automatique (comme les moteurs de recherche, les réseaux sociaux ou les plateformes vidéo).

## Dependencies
import re


## Data
text = 'peter piper picked a peck of pickled peppers'


## One-Liner
result = re.findall('p.*?e.*?r', text)


## Result
print(result)

Solution en une seule ligne pour rechercher des phrases spécifiques (non gourmandes).

La requête de recherche regex est 'p.*?e?.*?r' . Nous recherchons donc une phrase qui commence par le caractère 'p' et se termine par le caractère 'r' . Entre ces deux caractères, nous avons besoin d'une occurrence du caractère 'e' . En dehors de cela, nous autorisons un nombre arbitraire de caractères (espaces blancs ou non). Cependant, nous apparions de manière non gourmande en utilisant la regex '.*?' afin que Python recherche un nombre minimal de caractères arbitraires (plutôt qu'un nombre maximal de caractères arbitraires pour la regex gourmande '.*' ).

## Result
print(result)
# ['peter', 'piper', 'picked a peck of pickled pepper']

Pour saisir pleinement le sens de la correspondance non gourmande, comparez cette solution à celle qui serait obtenue en utilisant la regex gourmande ‘p.*e.*r’.

result = re.findall('p.*e.*r', text)
print(result)
# ['peter piper picked a peck of pickled pepper']

Le premier opérateur astérisque gourmand .* correspond à presque toute la chaîne avant qu'elle ne se termine.

Analyse des liens hypertexte des documents HTML

Dans la dernière section, vous avez appris les trois expressions régulières les plus importantes :la regex point, la regex astérisque et la regex zéro ou un. Cette section va beaucoup plus loin en introduisant de nombreuses autres expressions régulières.

En ajoutant plus d'expressions régulières à votre stock de connaissances, vous augmentez votre capacité à résoudre des problèmes réels de manière rapide, concise et simple. Quelles sont donc les expressions régulières les plus importantes ? Étudiez attentivement la liste suivante car nous les utiliserons toutes dans ce chapitre.

  • Le point regex . correspond à un caractère arbitraire.
  • La regex astérisque A* correspond à un nombre arbitraire d'instances de la regex A.
  • La regex zéro ou un A? correspond à zéro ou à une instance de la regex A.
  • La regex point non gourmande .? correspond à aussi peu de caractères arbitraires que possible de sorte que l'expression régulière globale corresponde si possible.
  • La regex A{m} correspond exactement à m copies de la regex A.
  • La regex A{m,n} correspond entre m et n copies de la regex A.
  • La regex A|B correspond à l'expression régulière A ou à l'expression régulière B (mais pas aux deux).
  • La regex AB correspond d'abord à la regex A, puis à la regex B.
  • La regex (A) correspond à la regex A. La parenthèse regroupe les expressions régulières afin que vous puissiez contrôler l'ordre d'exécution (par exemple, la regex (AB)|C est différent de A(B|C) .

Prenons un court exemple. Dites, vous créez la regex 'b?(.a)*'. À quels modèles la regex correspondra-t-elle ? L'expression régulière correspond à tous les modèles commençant par zéro ou un caractère "b" et un nombre arbitraire de séquences de deux caractères se terminant par le caractère "a". Par conséquent, les chaînes 'bcacaca', ”, et 'aaaaaa' correspondraient toutes à la regex.

Avant de plonger dans le prochain one-liner, discutons rapidement d'un autre sujet d'intérêt pour tout praticien :quand utiliser quelle fonction regex ? Les trois fonctions regex les plus importantes sont re.match(), re.search() et re.findall(). Vous en avez déjà vu deux mais étudions-les plus en détail (par exemple).

import re

text = '''
"One can never have enough socks", said Dumbledore.
"Another Christmas has come and gone and I didn’t
get a single pair. People will insist on giving me books."
Christmas Quote
'''

regex = 'Christ.*'

print(re.match(regex, text))
# None

print(re.search(regex, text))
# <re.Match object; span=(62, 102), match='Christmas has come and gone and I didn’t'>

print(re.findall(regex, text))
# ['Christmas has come and gone and I didn’t', 'Christmas Quote']

Les trois fonctions prennent l'expression régulière et la chaîne à rechercher comme entrée. Les fonctions match() et search() renvoient un objet match (ou None si la regex ne correspond à rien). L'objet de correspondance stocke la position de la correspondance et des méta-informations plus avancées. La fonction match() ne trouve pas la regex dans la chaîne (elle renvoie None). Pourquoi? Parce que la fonction recherche le modèle uniquement au début de la chaîne. La fonction search() recherche la première occurrence de la regex n'importe où dans la chaîne. Par conséquent, il trouve la correspondance "Noël est passé et pas moi".

Je suppose que vous aimez le plus la fonction findall() ? La sortie est intuitive (mais aussi moins utile pour un traitement ultérieur :par exemple, l'objet de correspondance contient des informations intéressantes sur l'emplacement de correspondance précis). Le résultat n'est pas un objet correspondant mais une séquence de chaînes. Contrairement aux fonctions match() et search(), la fonction findall() récupère tous les modèles correspondants.

Supposons que votre entreprise vous demande de créer un petit bot Web qui explore les pages Web et vérifie si elles contiennent des liens vers le domaine "finxter.com". Une exigence supplémentaire est que les descriptions des liens hypertexte doivent également contenir les chaînes « test » ou « puzzle ». Plus précisément, le but est de résoudre le problème suivant :"A partir d'une chaîne, trouver tous les hyperliens qui pointent vers le domaine finxter.com et contiennent les chaînes 'test' ou 'puzzle' dans la description du lien".

## Dependencies
import re


## Data
page = '''
<!DOCTYPE html>
<html>
<body>

<h1>My Programming Links</h1>
<a href="https://app.finxter.com/learn/computer/science/">test your Python skill level</a>
<a href="https://blog.finxter.com/recursion/">Learn recursion</a>
<a href="https://nostarch.com/">Great books from NoStarchPress</a>
<a href="http://finxter.com/">Solve more Python puzzles</a>

</body>
</html>
'''

## One-Liner
practice_tests = re.findall("(<a.*?finxter.*(test|puzzle).*>)", page)


## Result
print(practice_tests)

Solution simple pour analyser les liens vers les pages Web.

Le code trouve deux occurrences de l'expression régulière. Lesquels ?

Les données consistent en une simple page Web HTML (stockée sous la forme d'une chaîne multiligne) contenant une liste d'hyperliens (l'environnement de balise link text ). La solution en une seule ligne utilise la fonction re.findall() pour vérifier l'expression régulière "()". De cette façon, l'expression régulière renvoie toutes les occurrences dans l'environnement de la balise avec les restrictions suivantes :

Après la balise d'ouverture, un nombre arbitraire de caractères est mis en correspondance (non gourmand), suivi de la chaîne 'finxter'. Ensuite, nous faisons correspondre un nombre arbitraire de caractères (gourmand), suivi d'une occurrence de la chaîne « test » ou de la chaîne « puzzle ». Encore une fois, nous faisons correspondre un nombre arbitraire de caractères (avec avidité), suivis de la balise de fermeture. De cette façon, nous trouvons toutes les balises de lien hypertexte qui contiennent les chaînes respectives. Notez que cette expression régulière correspond également aux balises où les chaînes "test" ou "puzzle" apparaissent dans le lien lui-même.

Le résultat de la ligne unique est le suivant :

## Result
print(practice_tests)
# [('<a href="https://app.finxter.com/learn/computer/science/">test your Python skill level</a>', 'test'),
#  ('<a href="http://finxter.com/">Solve more Python puzzles</a>', 'puzzle')]

Deux hyperliens correspondent à notre expression régulière :le résultat de la ligne unique est une liste à deux éléments. Cependant, chaque élément est un tuple de chaînes plutôt qu'une simple chaîne. Ceci est différent des résultats de la fonction findall() dont nous avons discuté dans les extraits de code précédents. Quelle est la raison de ce comportement ? Le type de retour est une liste de tuples, avec une valeur de tuple pour chaque groupe correspondant entre crochets (). Par exemple, la regex ‘(test|puzzle)’ utilise la notation entre parenthèses pour créer un groupe correspondant. La règle est désormais la suivante :si vous utilisez des groupes correspondants dans votre regex, la fonction re.findall() ajoutera une valeur de tuple pour chaque groupe correspondant. La valeur du tuple est la sous-chaîne qui correspond à ce groupe particulier (et non une chaîne qui correspond à l'ensemble de la regex comprenant plusieurs groupes correspondants). C'est pourquoi la deuxième valeur de tuple de la première valeur de liste est la chaîne 'test' et la deuxième valeur de tuple de la deuxième valeur de liste est la chaîne 'puzzle' - celles-ci sont mises en correspondance dans cet ordre respectif.