如果您当前的 Python 设置中不存在这些库,请参阅第 2 章、Python 和 Web–使用 urllib 和请求、设置部分,以了解有关它们的安装和如何设置的更多信息。到目前为止,我们学习了 web 技术、数据查找技术以及如何使用 Python 库访问 web 内容。
正则表达式(正则表达式或正则表达式实际上是一种模式,它是使用预定义的命令和格式构建的,以匹配所需的内容。当没有特定的布局或标记模式可供选择时,Regex 在数据提取期间提供了巨大的价值,并且可以与其他技术(如 XPath 和 CSS 选择器)一起应用。
一般文本或字符格式的复杂 web 内容和数据可能需要使用正则表达式来完成活动,例如匹配和提取,以及函数替换、拆分等
在本章中,我们将学习以下主题:
- 正则表达式概述
- 使用正则表达式提取数据
本章要求使用网络浏览器(Google Chrome 或 Mozilla Firefox)。我们将使用以下 Python 库:
requestsrebs4
如果您当前的 Python 设置中不存在这些库,请参阅第 2 章、Python 和 Web–使用 urllib 并请求*、t设置*部分,有关其安装和如何设置的详细信息。
本章的代码文件可在本书的 GitHub 存储库中找到:https://github.com/PacktPublishing/Hands-On-Web-Scraping-with-Python/tree/master/Chapter09 。
Those of you who are already using re can refer to the Using regular expressions to extract data section .
正则表达式用于匹配文本或字符串中的模式。egex 可用于测试和查找文本或 web 内容所需的模式。Regex 包含各种方式来定义模式和特殊符号,例如转义码来应用一些预定义的规则。有关 R egex 的更多信息,请参阅进一步阅读部分。
在各种情况下,R egex 可以非常有效且快速地获得所需的结果。egex 可以单独应用于内容(文本或 web 源),并且可以用于针对特定的信息模式,这些信息模式在使用 XPath、CSS 选择器、BS4*、*PyQuery 等时不容易提取。
有时,可能会出现需要同时使用正则表达式和 XPath 或 CSS 选择器以获得所需输出的情况。然后可以使用 Regex 测试此输出,以查找模式或清理和管理数据。代码编辑器、文档编写器和读者还提供了基于嵌入式正则表达式的实用程序。
正则表达式可以应用于任何包含正确或不正确格式的文本或字符串、HTML 源等。Regex 可用于各种应用程序,例如:
- 基于特定模式的内容
- 页面链接
- 图片标题和链接
- 链接内的文本
- 匹配和验证电子邮件地址
- 从地址字符串匹配邮政编码或邮政编码
- 验证电话号码,等等
使用诸如搜索、查找、拆分、替换、匹配和迭代等工具,无论是否有其他技术干扰,都是适用的
在以下几节中,我们将使用rePython 模块并探索其方法,然后将其应用于正则表达式。
re是用于处理正则表达式的标准 Python 库。每个默认 Python 安装都包含re库。如果库不存在,请参考第 2 章、Python 和 Web–使用 urllib 并请求*、设置设置*部分,了解如何设置。
>>> in code represents the use of the Python IDE. It accepts the code or instructions it's given and displays the output on the next line.
让我们首先使用 Python IDE 导入re并使用dir()函数列出其属性:
>>> import re
>>> print(dir(re)) #listing features from re以下是前面命令的输出:
['A', 'ASCII', 'DEBUG', 'DOTALL', 'I', 'IGNORECASE', 'L', 'LOCALE', 'M', 'MULTILINE', 'S', 'Scanner', 'T', 'TEMPLATE', 'U', 'UNICODE', 'VERBOSE', 'X', '_MAXCACHE', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__versio n__', '_alphanum_bytes', '_alphanum_str', '_cache', '_cache_repl', '_compile', '_compile_repl', '_expand', '_locale', '_pattern_type', '_pickle', '_subx', 'compile', 'copyreg', 'error', 'escape', 'findall', 'finditer', 'fullmatch', 'match', 'purge', 'search', 'split', 'sre_compile', 'sre_parse', 'sub', 'subn', 'sys', 'template']从前面的输出可以看出,re中提供了各种功能。我们将从内容提取的角度使用其中的一些函数,并通过以下示例解释 R egex 基础知识的基础知识:
>>> sentence = """Brief information about Jobs in Python. Programming and Scripting experience in some language (such as Python R, MATLAB, SAS, Mathematica, Java, C, C++, VB, JavaScript or FORTRAN) is expected. Participants should be comfortable with basic programming concepts like variables, loops, and functions. """ sentence我们之前声明的内容包含有关 Python 作业和作业描述的简要信息。我们将用这句话来解释基本的正则表达式功能
split()函数分解字符串并返回单个单词的列表,默认情况下由空格字符分隔。我们也可以使用re.split()拆分字符串对象。在这种情况下,split()接受正则表达式模式来拆分句子,例如,re.split(r'\s+',sentence):
>>> splitSentence = sentence.split() #split sentence or re.split(r'\s',sentence)
>>> print ( "Length of Sentence: " , len (sentence), '& splitSentence: ' , len (splitSentence))
Length of Sentence: 297 & splitSentence: 42
>>> print (splitSentence) #List of words obtained using split()
['Brief', 'information', 'about', 'Jobs', 'in', 'Python.', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', '(such', 'as', 'Python', 'R,', 'MATLAB,', 'SAS,', 'Mathematica,', 'Java,', 'C,', 'C++,', 'VB,', 'JavaScript', 'or', 'FORTRAN)', 'is', 'expected.', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables,', 'loops,', 'and', 'functions.']使用前面的代码获取并打印sentence和 PythonsplitSentence列表对象的长度。在比较从以下示例返回的答案时,这些元素和字符计数将非常有用:
>>> matches = re.findall(r"([A-Z+]+)\,",sentence) #finding pattern with [A-Z+] and comma behind
>>> print("Findall found total ",len(matches)," Matches >> ",matches) Findall found total 6 Matches >> ['R', 'MATLAB', 'SAS', 'C', 'C++', 'VB']
>>> matches = re.findall(r"([A-Z]+)\,",sentence) #finding pattern with [A-Z] and comma behind
>>> print("Findall found total ",len(matches)," Matches >> ",matches)
Findall found total 5 Matches >> ['R', 'MATLAB', 'SAS', 'C', 'VB'] re.findall()接受关于所提供模式的模式搜索和内容查找。通常,模式可以直接作为参数提供给函数,也可以作为前面有r的原始字符串,例如r'([A-Z]+)',或者包含原始字符串的变量
在前面的代码中,我们可以看到提供了某些附加字符的类似模式,但它们的输出不同。对其中一些模式进行了一般性解释,如下所示:
[A-Z]:模式中的方括号匹配一组字符,区分大小写。这里,它匹配从A到Z的字符,但不匹配从a到z的字符。我们可以提供一组字符,如[A-Za-z0-9],匹配A到Z和a到z之间的任意字符,以及0到9之间的数字字符。如果需要,可以在集合内传递附加字符作为[A-Z+];席氏 T13 性状可以与 Ty14T14 的性状存在,如 C++C 或 C.():模式中的圆括号包含匹配的值组+(用于重复):在字符集之外找到,它与它所遵循的模式的一个或多个匹配。[A-Z]+将匹配至少一个或多个与A到Z字符找到的组合,例如前面代码中的R和MATLAB。还有几个字符用于指定重复或出现,也称为正则表达式量词:*匹配零次或多次出现的模式?匹配该模式的零次或一次出现{m,n}分别匹配最小m和最大n重复次数:{2,5}:最少 2 个或最多 5 个{2,}:最小值为 2 或可能更大{,5}:最多 5 个{3}:发生 3 次
\,(逗号):在正则表达式中,[A-Za-z0-9]以外的字符通常作为转义字符写入,以提及该特定字符(\,表示逗号,\.表示句点,\?表示问号,等等)。
正则表达式量词也分类如下:
- 贪婪量词:这些量词尽可能多地匹配任何元素
- 懒惰或非贪婪量词:这些量词尽可能少地匹配任何元素。通常情况下,贪婪量词通过添加
?转换为懒惰量词。
像([A-Z+]+)\,这样的模式匹配从A到Z和+的一组字符,这些字符可以存在于至少一个或多个字符中,后跟,。在前面代码的sentence中,我们可以找到R、MATLAB、SAS、Mathematica、Java、C、C++、VB、JavaScript(还有FORTRAN),即后面跟有,的名称(但FORTRAN的情况不是这样;这就是为什么在提供的模式的输出中被排除的原因)
在下面的代码中,我们正在尝试匹配在sentence中找到的FORTRAN,它与我们在前面的代码中尝试的模式一起被省略:
>>> matches = re.findall(r "\s*([\sorA-Z+]+)\)" ,sentence) #r'\s*([A-Z]+)\)' matches 'FORTRAN'
>>> print ( "Findall found total " , len (matches), " Matches >> " ,matches)
Findall found total 1 Matches >> ['or FORTRAN']
>>> fortran = matches[ 0 ] # 'or FORTRAN'
>>> if re.match( r'or' ,fortran):
fortran = re.sub( r'or\s*' , '' ,fortran) #substitute 'or ' with empty string
>>> print (fortran)
FORTRAN
>>> if re.search(r'^F.*N$',fortran): #using beginning and end of line searching pattern
print("True")
True如前面的代码块所示,Python 库re具有以下各种功能:
re.match():匹配字符串开头提供的模式并返回匹配的对象。re.sub():找到一个模式并用提供的字符串替换它。它的工作原理类似于查找和替换文本re.search():匹配字符串中的模式并返回找到的匹配对象。\s:表示空格、制表符、换行符。此处,[\sorA-Z+]+\)匹配一个或多个字符,包括A-Z、o、r、\s和+,后跟\)(右括号)。在正则表达式中还有一些转义码,如下所示:\d:匹配一个数字\D:匹配非数字\s:匹配空格\S:匹配非空白\w:匹配字母数字字符\W:匹配非字母数字字符\b:匹配单词边界\B:匹配非单词边界
^:这与字符串的开头匹配。
Note : r'[^a-z]' (the caret or ^), when used inside a character set, acts as negation. Here, this means except or exclude [a-z].
$:这与字符串的结尾匹配。|:实现模式中的逻辑表达式OR。例如,r'a|b'将匹配任何真实表达式,即a或b。
下面的代码显示了一些正则表达式模式和findall()函数的使用,以及它们的输出:
>>> matches = re.findall( r'\s(MAT.*?)\,' ,sentence, flags =re.IGNORECASE)
>>> print ( "(MAT.*?)\,: " ,matches) #r'(?i)\s(MAT.*?)\,' can also be used
(MAT.*?)\,: ['MATLAB', 'Mathematica']
>>> matches = re.findall( r'\s(MAT.*?)\,' ,sentence) #findall with 'MAT' case-sensitive
>>> print ( "(MAT.*?)\,: " ,matches)
(MAT.*?)\,: ['MATLAB']
>>> matches = re.findall( r'\s(C.*?)\,' ,sentence)
>>> print ( "\s(C.*?)\,: " ,matches)
\s(C.*?)\,: ['C', 'C++'] 在前面的代码中找到以下函数:
re函数还支持可选的标志参数。这些旗帜还有一个缩写(i代表re.IGNORECASE、s代表re.DOTALL、M代表re.MULTILINE)。通过在表达式的开头包含它们,可以在模式中使用它们。例如,r'(?i)\s(MAT.*?)\,将返回[MATLAB,Mathematica。以下是在代码中找到的一些其他re函数:re.IGNORECASE:忽略所提供模式中的大小写敏感性re.DOTALL:允许.(句点)匹配换行符,并可用于包含多行的字符串re.MULTILINE:处理多行字符串并搜索模式,包括换行符("\n")
.或句点:匹配任何单个字符,但不匹配换行符("\n"。它主要用于有重复字符的图案中。字符串中需要匹配句点或.,且应作为\.使用:
>>> matchesOne = re.split( r"\W+" ,sentence) #split by word, \w (word characters, \W - nonword)
>>> print ( "Regular Split '\W+' found total: " , len (matchesOne ), " \n " ,matchesOne)
Regular Split '\W+' found total: 43
['Brief', 'information', 'about', 'Jobs', 'in', 'Python', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', 'such', 'as', 'Python', 'R', 'MATLAB', 'SAS', 'Mathematica', 'Java', 'C', 'C', 'VB', 'JavaScript', 'or', 'FORTRAN', 'is', 'expected', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables', 'loops', 'and', 'functions', '']
>>> matchesTwo = re.split( r"\s" ,sentence) #split by space
>>> print ( "Regular Split '\s' found total: " , len (matchesTwo), " \n " , matchesTwo)
Regular Split '\s' found total: 42
['Brief', 'information', 'about', 'Jobs', 'in', 'Python.', 'Programming', 'and', 'Scripting', 'experience', 'in', 'some', 'language', '(such', 'as', 'Python', 'R,', 'MATLAB,', 'SAS,', 'Mathematica,', 'Java,', 'C,', 'C++,', 'VB,', 'JavaScript', 'or', 'FORTRAN)', 'is', 'expected.', 'Participants', 'should', 'be', 'comfortable', 'with', 'basic', 'programming', 'concepts', 'like', 'variables,', 'loops,', 'and', 'functions.'] re.split():根据模式分割提供的内容,返回结果列表。还存在一个split(),可以与字符串一起使用,以默认字符或提供的字符进行分解。它的使用方式与本节前面提到的splitSentence类似。
You are suggested to compare the results of matchesOne and matchesTwo from this section**.**
在下面的代码中,我们试图对 datetime 属性中的值应用正则表达式模式。定义的模式将被编译,然后用于在代码块中搜索:
>>> timeDate= '''<time datetime="2019-02-11T18:00:00+00:00"></time>
<time datetime="2018-02-11T13:59:00+00:00"></time>
<time datetime="2019-02-06T13:44:00.000002+00:00"></time>
<time datetime="2019-02-05T17:39:00.000001+00:00"></time>
<time datetime="2019-02-04T12:53:00+00:00"></time> '''
>>> pattern = r'(20\d+)([-]+)(0[1-9]|1[012])([-]+)(0[1-9]|[12][0-9]|3[01])'
>>> recompiled = re.compile(pattern) # <class '_sre.SRE_Pattern'>
>>> dateMatches = recompiled.search(timeDate)re.compile():用于编译 Regex 模式并接收模式对象(_sre.SRE_Pattern)。接收到的对象可以与其他正则表达式功能一起使用。
可以使用group()方法单独探索组匹配,如下代码所示:
>>> print ( "Group : " ,dateMatches.group())
Group : 2019-02-11
>>> print ( "Groups : " ,dateMatches.groups())
Groups : ('2019', '-', '02', '-', '11')
>>> print ( "Group 1 : " ,dateMatches.group( 1 ))
Group 1 : 2019
>>> print ( "Group 5 : " ,dateMatches.group( 5 ))
Group 5 : 11正如我们所看到的,虽然该模式已针对多行timeDate进行搜索,但它会导致一个组;也可以使用索引返回单个组。与re相关的匹配对象包含groups()和group()函数;groups(0)产生与groups()相同的输出。groups()中的单个元素需要从1开始的索引
** re.finditer():用于迭代在提供的内容中找到的模式或模式对象的结果匹配。它返回从re.match()中找到的匹配(_sre.SRE_Match对象。
re.match()返回包含代码示例中使用的各种函数和属性的对象。详情如下:
start():返回与表达式匹配的起始字符索引end():返回与表达式匹配的结束字符索引span():返回匹配表达式的起始字符索引和结束字符索引lastindex:返回最后一个匹配表达式的索引groupdict():返回带有模式字符串和匹配值的匹配组字典groups():返回所有匹配的元素group():返回单个组,可以使用组名访问lastgroup:返回最后一个组的名称
>>> for match in re.finditer(pattern, timeDate): # <class '_sre.SRE_Match'>
# for match in re.finditer(recompiled, timeDate):
s = match.start()
e = match.end()
l = match.lastindex
g = match.groups()
print ( 'Found {} at {}:{}, groups{} lastindex:{}' .format(timeDate[s:e], s, e,g,l))
Found 2019-02-11 at 16:26, groups('2019', '-', '02', '-', '11') lastindex:5
Found 2018-02-11 at 67:77, groups('2018', '-', '02', '-', '11') lastindex:5
Found 2019-02-06 at 118:128, groups('2019', '-', '02', '-', '06') lastindex:5
Found 2019-02-05 at 176:186, groups('2019', '-', '02', '-', '05') lastindex:5
Found 2019-02-04 at 234:244, groups('2019', '-', '02', '-', '04') lastindex:5模式还可以为它们所在的组指定字符串名称;例如,r'(?P<year>[0-9]{4})'与year组匹配。在 Regex 中使用基于组的模式有助于我们更准确地读取模式和管理输出;这意味着我们不必担心索引问题。
让我们考虑模式 To0 T0(实施 To1 T1,Po.T2,,T3,,T4,,T5,和 T6),用一组名字和代码显示日期和时间的输出:
>>> pDate = r'(?P<year>[0-9]{4})(?P<sep>[-])(?P<month>0[1-9]|1[012])-(?P<day>0[1-9]|[12][0-9]|3[01])'
>>> recompiled = re.compile(pDate) #compiles the pattern
>>> for match in re.finditer(recompiled,timeDate): #apply pattern on timeDate
s = match.start()
e = match.end()
l = match.lastindex
print ( "Group ALL or 0: " ,match.groups( 0 )) #or match.groups() that is all
print ( "Group Year: " ,match.group( 'year' )) #return year
print ( "Group Month: " ,match.group( 'month' )) #return month
print ( "Group Day: " ,match.group( 'day' )) #return day
print ( "Group Delimiter: " ,match.group( 'sep' )) #return seperator
print ( 'Found {} at {}:{}, lastindex: {}' .format(timeDate[s:e], s, e,l))
print ( 'year :' ,match.groupdict()[ 'year' ]) #accessing groupdict()
print ( 'day :' ,match.groupdict()[ 'day' ])
print ( 'lastgroup :' ,match.lastgroup) #lastgroup name前面的代码产生以下输出:
Group ALL or 0: ('2019', '-', '02', '11')
Group Year: 2019
Group Month: 02
Group Day: 11
Group Delimiter: -
Found 2019-02-11 at 16:26, lastindex: 4
year : 2019
day : 11
lastgroup : day以下代码显示了pTime的用法(实现span():
>>> pTime = r'(?P<hour>[0-9]{2})(?P<sep>[:])(?P<min>[0-9]{2}):(?P<sec_mil>[0-9.:+]+)'
>>> recompiled = re.compile(pTime)
>>> for match in re.finditer(recompiled,timeDate):
print ( "Group String: " ,match.group()) #groups
print ( "Group ALL or 0: " ,match.groups())
print ( "Group Span: " ,match.span()) #using span()
print ( "Group Span 1: " ,match.span( 1 ))
print ( "Group Span 4: " ,match.span( 4 ))
print('hour :',match.groupdict()['hour']) #accessing groupdict()
print('minute :',match.groupdict()['min'])
print('second :',match.groupdict()['sec_mil'])
print('lastgroup :',match.lastgroup) #lastgroup name上述代码将产生以下输出:
Group String: 12:53:00+00:00
Group ALL or 0: ('12', ':', '53', '00+00:00')
Group Span: (245, 259)
Group Span 1: (245, 247)
Group Span 4: (251, 259)
hour : 12
minute : 53
second : 00+00:00
lastgroup : sec_mil在本节中,我们介绍了 Regex 和rePython 库的特性,以及一些实际示例。有关正则表达式的更多信息,请参阅进一步阅读部分。在下一节中,我们将应用正则表达式从基于 web 的内容中提取数据
既然我们已经介绍了基础知识并对 Regex 进行了概述,那么我们将使用 Regex 批量地刮取(提取)数据,方法与使用 XPath、CSS 选择器、pyquery、bs4等类似,在 Regex、XPath、pyquery等实现之间进行选择。这取决于 web 访问的要求和可行性以及内容的可用性。
应用正则表达式和提取数据并不总是要求内容是非结构化的。R egex 可用于结构化和非结构化 web 内容,以提取所需数据。在本节中,我们将探讨几个使用正则表达式及其各种属性的示例。
在本例中,我们将使用regexHTML.html文件中的 HTML 内容,并应用正则表达式模式来提取如下信息:
- HTML 元素
- 元素的属性(
key和values) - 元素的内容
本例将为您提供一个总体概述,说明我们如何处理 web 内容中存在的各种元素、值等,以及如何应用正则表达式提取该内容。我们将在以下代码中应用的步骤将有助于处理 HTML 和类似内容:
< html >
< head >
< title > Welcome to Web Scraping: Example </ title >
< style type= "text/css" >
....
</ style >
</ head >
< body >
< h1 style= " color:orange; " > Welcome to Web Scraping </ h1 >
Links:
< a href= "https://www.google.com" style= " color:red; " > Google </ a >
< a class= "classOne" href= "https://www.yahoo.com" > Yahoo </ a >
< a id= "idOne" href= "https://www.wikipedia.org" style= " color:blue; " > Wikipedia </ a >
< div >
< p id= "mainContent" class= "content" >
< i > Paragraph contents </ i >
< img src= "mylogo.png" id= "pageLogo" class= "logo" />
</ p >
< p class= "content" id= "subContent" >
< i style= " color:red " > Sub paragraph content </ i >
< h1 itemprop= "subheading" > Sub heading Content! </ h1 >
</ p >
</ div >
</ body >
</ html > 前面的代码是我们将要使用的 HTML 页面源代码。这里的内容是结构化的,我们可以通过多种方式处理它。
在以下代码中,我们将使用以下函数:
read_file():读取 HTML 文件,返回页面源进行进一步处理applyPattern():它接受一个pattern参数,即用于查找内容的 Regex 模式,该模式使用re.findall()应用于 HTML 源,并打印诸如搜索元素列表及其计数等信息。
首先,我们导入re和bs4:
import re
from bs4 import BeautifulSoup
def read_file():
''' Read and return content from file (.html). '''
content = open ( "regexHTML.html" , "r" )
pageSource = content.read()
return pageSource
def applyPattern(pattern):
'''Applies regex pattern provided to Source and prints count and contents'''
elements = re.findall(pattern, page) #apply pattern to source
print ( "Pattern r'{}' ,Found total: {}" .format(pattern, len (elements)))
print (elements) #print all found tags
return
if __name__ == "__main__" :
page = read_file() #read HTML file 这里,page是一个 HTML 页面源,使用read_file()从 HTML 文件中读取。我们还在前面的代码中导入了BeautifulSoup来提取单个 HTML 标记名,只是为了比较代码的实现以及使用soup.find_all()和我们将要应用的正则表达式模式发现的结果:
soup = BeautifulSoup(page, 'lxml' )
print ([element.name for element in soup.find_all()])
['html', 'head', 'title', 'style', 'body', 'h1', 'a', 'a', 'a', 'div', 'p', 'i', 'img', 'p', 'i', 'h1']为了找到page中存在的所有 HTML 标记,我们使用find_all()方法,使用lxml解析器将soup作为BeautifulSoup的对象。
For more information on Beautiful Soup, please visit Chapter 5, Web Scraping using Scrapy and Beautiful Soup, the Web scraping using Beautiful Soup section .
在这里,我们将查找所有没有任何属性的 HTML 标记名。\w+匹配具有一个或多个字符的任何单词:
applyPattern( r'<(\w+)>' ) #Finding Elements without attributes
Pattern r'<(\w+)>' ,Found total: 6
['html', 'head', 'title', 'body', 'div', 'i']在空格字符的帮助下,可以找到所有不以*>*结尾或包含某些属性的 HTML 标记或元素,即\s:
applyPattern( r'<(\w+)\s' ) #Finding Elements with attributes
Pattern r'<(\w+)\s' ,Found total: 10
['style', 'h1', 'a', 'a', 'a', 'p', 'img', 'p', 'i', 'h1']现在,通过组合所有这些模式,我们列出了在页面源代码中找到的所有 HTML 标记。在前面的代码中,通过使用soup.find_all()和name属性也获得了相同的结果:
applyPattern(r'<(\w+)\s?') #Finding all HTML element
Pattern r'<(\w+)\s?' ,Found total: 16
['html', 'head', 'title', 'style', 'body', 'h1', 'a', 'a', 'a', 'div', 'p', 'i', 'img', 'p', 'i', 'h1']让我们查找属性的名称,如 HTML 元素中所示:
applyPattern( r'<\w+\s+(.*?)=' ) #Finding attributes name
Pattern r'<\w+\s+(.*?)=' ,Found total: 10
['type', 'style', 'href', 'class', 'id', 'id', 'src', 'class', 'style', 'itemprop']如我们所见,只列出了 10 个属性。在 HTML 源代码中,一些标记包含多个属性,例如 < a href= "https://www.google.com" style= " color:red; " > Google </ a > ,并且使用提供的模式仅找到第一个属性
让我们纠正这个问题。我们可以使用r'(\w+)='模式选择后面带有=字符的单词,这将导致返回页面源中找到的所有属性:
applyPattern( r'(\w+)=' ) #Finding names of all attributes
Pattern r'(\w+)=' ,Found total: 18
['type', 'style', 'href', 'style', 'class', 'href', 'id', 'href', 'style', 'id', 'class', 'src', 'id', 'class', 'class', 'id', 'style', 'itemprop']类似地,让我们找到找到的属性的所有值。下面的代码列出了属性的值,并比较了我们前面列出的18属性。仅找到了9值。根据我们在这里使用的模式,r'=\"(\w+)\"'将只找到单词字符。某些属性值包含非单词字符,如 < a href=" https://www.google.com" style= " color:red; "> :
applyPattern( r'=\"(\w+)\"' )
Pattern r'=\"(\w+)\"' ,Found total: 9
['classOne', 'idOne', 'mainContent', 'content', 'pageLogo', 'logo', 'content', 'subContent', 'subheading']这里,使用我们分析的适当模式列出了完整的属性值。内容属性值还包含非单词字符,如;、/、:和.*。*在正则表达式中,我们可以在模式中单独包含这些字符,但这种方法可能并不适用于所有情况
在这种情况下,包含\w和非空白字符\S的模式非常适合,即r'=\"([\w\S]+)\":
applyPattern( r'=\"([\w\S]+)\"' )
Pattern r'=\"([\w\S]+)\"' ,Found total: 18
['text/css', 'color:orange;', 'https://www.google.com', 'color:red;', 'classOne', 'https://www.yahoo.com', 'idOne', 'https://www.wikipedia.org', 'color:blue;', 'mainContent', 'content', 'mylogo.png', 'pageLogo', 'logo', 'content', 'subContent', 'color:red', 'subheading']最后,让我们收集 HTML 元素中的所有文本,这些元素位于开始和结束 HTML 标记之间:
applyPattern( r'\>(.*)\<' )
Pattern r'\>(.*)\<' ,Found total: 8
['Welcome to Web Scraping: Example', 'Welcome to Web Scraping', 'Google', 'Yahoo', 'Wikipedia', 'Paragraph contents', 'Sub paragraph content', 'Sub heading Content!'] 将正则表达式应用于内容时,必须对内容类型和要提取的值进行初步分析。这将有助于获得所需的结果,并且可以一次完成。
在本例中,我们将从中提取内容 http://godfreysfeed.com/dealersandlocations.php 。此网站包含经销商位置信息,如以下屏幕截图所示:
import re
import requests
def read_url(url):
'''
Handles URL Request and Response
Loads the URL provided using requests and returns the text of page source
'''
pageSource = requests.get(url).text
return pageSource
if __name__ == "__main__" :对于本节中的这个和其他示例,我们将使用re和requests库来检索页面源,即pageSource。在这里,我们将使用read_url()函数来实现这一点。
该页面包含 HTML<form>元素,因此我们可以根据输入的zipcode搜索经销商。还有一张带有标记的地理地图:
Godfreysfeed Dealers front page
您可以使用zipcode执行表单提交,也可以从地图中提取内容。
通过分析页面源代码,我们会发现没有包含经销商信息的 HTML 元素。实现 Regex 非常适合这种情况。在这里,可以在 JavaScript 代码中找到经销商的信息,代码中有变量,如latLng和infoWindowContent,如以下屏幕截图所示:
Godfreysfeed Dealers page source
现在,我们将继续加载所需 URL 的页面源,并实现 Regex 以查找数据:
dataSet= list () #collecting data extracted
sourceUrl = 'http://godfreysfeed.com/dealersandlocations.php'
page = read_url(sourceUrl) #load sourceUrl and return the page source使用从read_url()获得的页面源,让我们做一个基本分析,并构建一个模式来收集纬度和经度信息。对于经销商的地址和坐标值,我们将分别需要两个不同的模式。两种模式的输出可以组合起来,以获得最终结果:
#Defining pattern matching latitude and longitude as found in page.
pLatLng= r'var latLng = new google.maps.LatLng\((?P<lat>.*)\,\s*(?P<lng>.*)\)\;'
#applying pattern to page source
latlngs = re.findall(pLatLng,page)
print ( "Findall found total *LatLngs: * " , len (latlngs))
#Print coordinates found
print(latlngs)通过使用pLatLng模式,共找到55坐标值:
Findall found total LatLngs: 55
[('33.2509855','-84.2633946'),('31.0426107','-84.8821949'),('34.8761989','-83.9582412'),('32.43158','-81.749293'),('33.8192864','-83.4387722'),('34.2959968','-83.0062267'),
('32.6537561','-83.7596295'),('31.462497','-82.5866503'),('33.7340136','-82.7472304')
,.................................................................,
('32.5444125','-82.8945945'),('32.7302168','-82.7117232'),('34.0082425','-81.7729772'),
('34.6639864', '-82.5126743'),('31.525261','-83.06603'),('34.2068698','-83.4689814'),
('32.9765932','-84.98978'),('34.0412765','-83.2001394'),('33.3066615','-83.6976187'),
('31.3441482','-83.3002373'),('30.02116','-82.329495'),('34.58403','-83.760829')]现在我们有了经销商的坐标,让我们找出经销商的姓名、地址和更多信息:
#Defining pattern to find dealer from page.
pDealers = r'infoWindowContent = infoWindowContent\+\s*\"(.*?)\"\;'
#applying dealers pattern to page source
dealers = re.findall(pDealers, page)
print ( "Findall found total Address: " , len (dealers))
#Print dealers information found
print(dealers)还有总共55条基于地址的信息,通过pDealers模式找到。请注意,经销商的内容为 HTML 格式,需要进一步实施 Regex 才能获得单独的标题,如name、address、city:
**```py Findall found total Address: 55
["Akins Feed & Seed
206 N Hill Street
Griffin, GA
30223
", "Alf's Farm and Garden
101 East 1st Street
Donalsonville, GA
39845
", "American Cowboy Shop
513 D Murphy Hwy
Blairsville, GA
30512
",................................... ....................................,"White Co. Farmers Exchange
951 S Main St
Cleveland, GA
30528
"]
现在我们有了`latlngs`和`dealers`的结果,让我们收集经销商地址的各个部分。经销商的原始数据包含一些 HTML 标记,并已用于拆分和清理经销商的地址信息。由于`re.findall()`返回 Python 列表,索引也可以用于检索地址组件:
```py
d= 0 #maintaining loop counter
for dealer in dealers:
dealerInfo = re.split( r'<br>' ,re.sub( r'<br><br>' , '' ,dealer))
#extract individual item from dealerInfo
name = re.findall( r'\'>(.*?)</span' ,dealerInfo[ 0 ])[ 0 ]
address = re.findall( r'>(.*)<' ,dealerInfo[ 1 ])[ 0 ]
city = re.findall( r'>(.*),\s*(.*)<' ,dealerInfo[ 2 ])[ 0 ][ 0 ]
state = re.findall( r'>(.*),\s*(.*)<' ,dealerInfo[ 2 ])[ 0 ][ 1 ]
zip = re.findall( r'>(.*)<' ,dealerInfo[ 3 ])[ 0 ]
lat = latlngs[d][ 0 ]
lng = latlngs[d][ 1 ]
d+= 1
#appending items to dataset
dataSet.append([name,address,city,state,zip,lat,lng])
print (dataSet) #[[name,address, city, state, zip, lat,lng],]
最后,dataSet将包含从清单中dealers和latlngs合并的单个经销商信息:
[['Akins Feed & Seed', '206 N Hill Street', 'Griffin', 'GA', '30223', '33.2509855', '-84.2633946'], ['Alf's Farm and Garden', '101 East 1st Street', 'Donalsonville', 'GA', '39845', '31.0426107', '-84.8821949'],....................................,
['Twisted Fitterz', '10329 Nashville Enigma Rd', 'Alapaha', 'GA', '31622', '31.3441482', '-83.3002373'],
['Westside Feed II', '230 SE 7th Avenue', 'Lake Butler', 'FL', '32054', '30.02116', '-82.329495'],
['White Co. Farmers Exchange', '951 S Main St', 'Cleveland', 'GA', '30528', '34.58403', '-83.760829']]在本例中,我们尝试使用不同的模式提取数据,并从提供的 URL 检索经销商的信息。
在本例中,我们将从sitemap.xml文件中提取内容,该文件可从**下载 https://webscraping.com/sitemap.xml** :
The sitemap.xml file from https://webscraping.com
通过分析 XML 内容,我们可以看到不同类型的 URL 作为子节点存在,即<loc>。从这些 URL 中,我们将提取以下内容:
- 博客 URL(带有
/blog/字符串的 URL,如https://webscraping.com/blog/Why-Python/ - 从博客 URL 获取的标题(为什么使用 Python)
- 类别 URL(带有
/category/字符串的 URL,如https://webscraping.com/blog/category/beautifulsoup - 从类别 URL 获取的类别标题(beautifulsoup)
Blog titles and category titles that are obtained from code are retrieved from the URL or representations of the real content that's available from the URL. Actual titles might be different.
首先,我们导入rePython 库并读取文件内容,同时创建一些 Python 列表以收集相关数据:
import re
filename = 'sitemap.xml'
dataSetBlog = [] # collect Blog title information from URLs except 'category'
dataSetBlogURL = [] # collects Blog URLs
dataSetCategory = [] # collect Category title
dataSetCategoryURL = [] # collect Category URLs
page = open (filename, 'r' ).read()从 XML 内容,即page,我们需要找到 URL 模式。代码中使用的pattern匹配并返回<loc>节点内的所有 URL。urlPatterns(<class 'list'>是一个 Python 列表对象,包含搜索到的 URL,并进行迭代以收集和处理所需信息:
#Pattern to be searched, found inside <loc>(.*)</loc>
pattern = r"loc>(.*)</loc"
urlPatterns = re.findall(pattern, page) #finding pattern on page
for url in urlPatterns: #iterating individual url inside urlPatterns现在,让我们匹配一个url,比如https://webscraping.com/blog/Google-App-Engine-limitations/ ,其中包含一个blog字符串,并将其追加到dataSetBlogURL。还有一些其他 URL,例如https://webscraping.com/blog/8/ ,提取blogTitle时会忽略
此外,任何被发现为等于category的文本的blogTitle都将被忽略。r'blog/([A-Za-z0-9\-]+)模式将字母和数字值与-字符匹配:
if re.match( r'.*blog' , url): #Blog related
dataSetBlogURL.append(url)
if re.match( r'[\w\-]' , url):
blogTitle = re.findall( r'blog/([A-Za-z0-9\-]+)' , url)
i f len (blogTitle) > 0 and not re.match( '(category)' , blogTitle[ 0 ]):
#blogTitle is a List, so index is applied.
dataSetBlog.append(blogTitle[ 0 ]) 以下是dataSetBlogURL的输出:
print ( "Blogs URL: " , len (dataSetBlogURL))
print (dataSetBlogURL)
Blogs URL: 80
['https://webscraping.com/blog', 'https://webscraping.com/blog/10/',
'https://webscraping.com/blog/11/', .......,
'https://webscraping.com/blog/category/screenshot', 'https://webscraping.com/blog/category/sitescraper', 'https://webscraping.com/blog/category/sqlite', 'https://webscraping.com/blog/category/user-agent', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']dataSetBlog将包含以下标题(URL 部分)。set()方法应用于dataSetBlog时,将从dataSetBlog返回唯一元素。如下代码所示,dataSetBlog内无重复标题:
print ( "Blogs Title: " , len (dataSetBlog))
print ( "Unique Blog Count: " , len ( set (dataSetBlog)))
print (dataSetBlog)
#print ( set (dataSetBlog)) #returns unique element from List similar to dataSetBlog.
Blogs Title: 24
Unique Blog Count: 24
['Android-Apps-Update', 'Apple-Apps-Update', 'Automating-CAPTCHAs', 'Automating-webkit', 'Bitcoin', 'Client-Feedback', 'Fixed-fee-or-hourly', 'Google-Storage', 'Google-interview', 'How-to-use-proxies', 'I-love-AJAX', 'Image-efficiencies', 'Luminati', 'Reverse-Geocode', 'Services', 'Solving-CAPTCHA', 'Startup', 'UPC-Database-Update', 'User-agents', 'Web-Scrapping', 'What-is-CSV', 'What-is-web-scraping', 'Why-Python', 'Why-web']现在,让我们使用category提取与 URL 相关的信息。与迭代中的url匹配的 r'.*category' 正则表达式模式被收集或附加到datasetCategoryURL中。categoryTitle从与r'category/([\w\s\-]+)模式匹配的url中提取并添加到dataSetCategory:
if re.match( r'.*category' , url): #Category Related
dataSetCategoryURL.append(url)
categoryTitle = re.findall( r'category/([\w\s\-]+)' , url)
dataSetCategory.append(categoryTitle[ 0 ])
print ( "Category URL Count: " , len (dataSetCategoryURL))
print (dataSetCategoryURL)dataSetCategoryURL将产生以下值:
Category URL Count: 43
['https://webscraping.com/blog/category/ajax', 'https://webscraping.com/blog/category/android/', 'https://webscraping.com/blog/category/big picture', 'https://webscraping.com/blog/category/business/', 'https://webscraping.com/blog/category/cache', 'https://webscraping.com/blog/category/captcha', ..................................., 'https://webscraping.com/blog/category/sitescraper', 'https://webscraping.com/blog/category/sqlite', 'https://webscraping.com/blog/category/user-agent', 'https://webscraping.com/blog/category/web2py', 'https://webscraping.com/blog/category/webkit', 'https://webscraping.com/blog/category/website/', 'https://webscraping.com/blog/category/xpath']最后,以下输出显示从dataSetCategory检索到的标题及其计数:
print ( "Category Title Count: " , len (dataSetCategory))
print ( "Unique Category Count: " , len ( set (dataSetCategory)))
print (dataSetCategory)
#returns unique element from List similar to dataSetCategory.
#print ( set (dataSetCategory))
Category Title Count: 43
Unique Category Count: 43
['ajax', 'android', 'big picture', 'business', 'cache', 'captcha', 'chickenfoot', 'concurrent', 'cookies', 'crawling', 'database', 'efficiency', 'elance', 'example', 'flash', 'freelancing', 'gae', 'google', 'html', 'image', 'ip', 'ir', 'javascript', 'learn', 'linux', 'lxml', 'mobile', 'mobile apps', 'ocr', 'opensource', 'proxies', 'python', 'qt', 'regex', 'scrapy', 'screenshot', 'sitescraper', 'sqlite', 'user-agent', 'web2py', 'webkit', 'website', 'xpath']从这些示例案例中,我们可以看到,通过使用 Regex,我们可以编写针对来自 web 页面、HTML 或 XML 等源的特定数据的模式。
可以借助rePython 库中的各种函数来实现搜索、拆分和迭代等 Regex 功能。尽管 Regex 可以在任何类型的内容上实现,但非结构化内容是首选。使用 XPath 和 CSS 选择器时,最好使用带有属性的元素的结构化 web 内容。
在本章中,我们通过使用rePython 库了解了正则表达式及其实现。
到目前为止,我们已经了解了各种基于爬取的工具和技术。在提取任务方面,Regex 可以提供更大的灵活性,并且可以与其他工具一起使用
在下一章中,我们将学习在学习环境中可能有益的进一步步骤和主题,如管理废弃数据、可视化和分析、机器学习和数据挖掘介绍,以及探索一些相关资源
- 正则表达式 HOWTO:https://docs.python.org/2/howto/regex.html
- 正则表达式–JavaScript:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
- Python 正则表达式:https://developers.google.com/edu/python/regular-expressions
- 在线正则表达式测试器和调试器:https://regex101.com/
- 正则表达式食谱:2012 年第二版由 Jan Goyvaerts 和 Steven Levithan 编写
- 正则表达式引用:https://regexone.com/references/python
- 正则表达式–信息:http://www.regular-expressions.info/python.html***


