Web scraping
Web scraping, 即网页爬取,是一种从网页上获取页面内容的计算机软件技术。通常透过软件使用低级别的超文本传输协议模仿人类的正常访问。今天,Python
社区为我们提供了许多实用的Web
爬取工具来帮助开发者更加高效的实现网页爬取。
现如今,互联网与我们的生活息息相关,它已然是地球上最大的信息载体。因此,许多学科,例如数据科学、商业智能和调查报告等,都可以从收集和分析网站数据中获取许多有用的信息。
在本教程中,你将会学习以下内容:
-
使用 字符串方法
和正则表达式
解析网站数据 -
通过 HTML
解析器解析网站数据 -
网页表单或组件交互
Source: A Practical Introduction to Web Scraping in Python
Author: David Amos
Date: Oct 17, 2022
Scrape and Parse Text
受某些不可描述的因素制约,现如今有部分网站是禁止用户使用自动化工具进行数据抓取的,其主要原因有两点:
-
数据隐私 -
服务器带宽压力
因此,在我们正式使用所学的Python技能进行网页抓取之前,应该仔细检查下待爬取的目标网站是否有明确的禁令,看看使用自动化工具访问网站是否违反了其使用条款。从法律上讲,违背网站意愿的网页抓取是一个灰色地带
。
注意:爬取某些网站的数据可能涉嫌违法!
在本教程中,我们将使用一个驻留在Real Python
服务器上的页面。当你将访问的页面设置好后,便可以在本教程中使用。在下一节中,我们将正式开始学习如何从单个网页获取所有HTML代码。
Build Your First Web Scraper
你可以在Python
的标准库中找到一个有用的Web
抓取包——urllib
,它包含了处理url
的工具。特别是urllib
。request
模块包含一个名为urlopen()
的函数,通过它,我们可以直接访问网站。
URL,即统一资源定位符(Uniform Resource Locator),或称统一资源定位器、定位地址、URL地址,俗称网页地址,简称网址,是因特网上标准的资源的地址,如同在网络上的门牌。
首先,让我们打开IDLE,具体介绍可见:
在IDLE
的交互窗口中,我们可以输入以下命令导入`urlopen()“:
>>> from urllib.request import urlopen
下面是我们今天为大家准备好的网页地址:
>>> url = "http://olympus.realpython.org/profiles/aphrodite"
接下来,让我们将该url
作为参数
传递给urlopen()
函数来帮助我们打开页面:
>>> page = urlopen(url)
其中,urlopen()
函数会返回一个带HTTP
响应的目标变量,通过打印该变量,我们可以获取对应的类型:
>>> page
<http.client.HTTPResponse object at 0x105fef820>
为了从页面中提取HTML
信息,首先,我们可以借助HTTPResponse object’s .read()
方法来读取,该方法会返回一连串的字节信息。接下来我们需要做的便是使用decode()
函数通过UTF-8
编码将字节流解码成人类能够阅读的字符串信息:
>>> html_bytes = page.read()
>>> html = html_bytes.decode("utf-8")
现在,我便可以愉快的打印HTML
所包含的网页信息了:
>>> print(html)
<html>
<head>
<title>Profile: Aphrodite</title>
</head>
<body bgcolor="yellow">
<center>
<br><br>
<img src="/static/aphrodite.gif" />
<h2>Name: Aphrodite</h2>
<br><br>
Favorite animal: Dove
<br><br>
Favorite color: Red
<br><br>
Hometown: Mount Olympus
</center>
</body>
</html>
HTML
,全称为超文本标记语言,是一种标记语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的Internet
资源连接为一个逻辑整体。HTML
文本是由HTML
命令组成的描述性文本,HTML
命令可以说明文字,图形、动画、声音、表格、链接等。
UTF-8
,即Unicode Transformation Format
,简称8位元,是针对Unicode
的一种可变长度字符编码。它可以用来表示Unicode
标准中的任何字符,而且其编码中的第一个字节仍与ASCII
相容,使得原来处理ASCII
字符的软件无须或只进行少部分修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。
当我们访问该网站时,其页面显示如下:
使用urllib
,访问网站的方式与在浏览器中访问网站的方式类似。只不过这里没有以可视化的方式呈现内容,而是将源代码作为文本进行获取。现在,HTML
已经成为文本,您可以通过几种不同的方式从中提取信息。
Extract Text With String Methods
从网页的HTML
中提取信息的一种方法是使用字符串
方法。例如,我们可以使用find()
函数在HTML
文本中搜索标记的第一个字符的索引,最后再通过字符串切片来提取标题即可。
首先,让我们来提取前一个示例中请求的Web
页面的标题。倘若我们知道标题第一个字符的索引以及结束标记的第一个字符的索引,那么便可以通过字符串切片的方式来提取所需的标题信息。关于字符串切片的相关知识,可参考前文内容:
接下来,使用find()
函数可以返回子字符串第一次出现的索引。为此,我们可以通过将字符串<title>
传递给find()
函数来获得<title>
标签对应的索引:
>>> title_index = html.find("<title>")
>>> title_index
14
显然,我们并不想要<title>
标签的下标索引,而是想获取标签里边的内容。这里,我们只需要加上标签本身所占字符的长度即可获取标签内容的起始位置索引,比如:
>>> start_index = title_index + len("<title>")
>>> start_index
21
现在,我们只需获取结束标注的第一个下标索引便可以获取整个<title>
字段的内容了:
>>> end_index = html.find("</title>")
>>> end_index
39
注意,
HTML
语法遵循的是标签树的格式,其首尾标签相对应,其中结束标签通常都是以左斜杆/
标识。如<title>
和<title>
.
最后,我们便可以通过字符串切片的方法提取整个标题了:
>>> title = html[start_index:end_index]
>>> title
'Profile: Aphrodite'
实际的HTML
可能比Aphrodite
个人资料页面上的HTML
要复杂得多,也难以预测得多。下面是另一个个人资料页面,里面有一些更乱的HTML
,我们可以尝试进行抓取:
>>> url = "http://olympus.realpython.org/profiles/poseidon"
让我们先尝试使用上述方法再次提取这个新的url
链接的标题内容:
>>> url = "http://olympus.realpython.org/profiles/poseidon"
>>> page = urlopen(url)
>>> html = page.read().decode("utf-8")
>>> start_index = html.find("<title>") + len("<title>")
>>> end_index = html.find("</title>")
>>> title = html[start_index: end_index]
>>> title
'n<head>n<title >Profile: Poseidon'
哎呀~~~标题中混合了一些HTML
。大家想想看这是为什么?
/profiles/poseidon
的HTML
页面看起来很像/profiles/aphrodite
,但是这里有少许不同:
>>> print(html)
<html>
<head>
<title >Profile: Poseidon</title>
</head>
<body bgcolor="yellow">
<center>
<br><br>
<img src="/static/poseidon.jpg" />
<h2>Name: Poseidon</h2>
<br><br>
Favorite animal: Dolphin
<br><br>
Favorite color: Blue
<br><br>
Hometown: Sea
</center>
</body>
</html>
通过将其对应的HTML
打印出来,我们可以观察到,这里的标签是<title >
而非<title>
。没错!仅仅是比之前的标签多了一个空格。因此,当我们将<title>
参数传递给find()
函数时会返回一个-1
给我们,表示当前标签不存在。
由于HTML
的每一个标签后面都会接一个换行符n
,因此,当我们调用start_index = html.find("<title>") + len("<title>")
该语句时,返回的是-1+7=6
,即从第6个字符开始,而<head>
标签占第0-5
个字符,故第6个字符刚好就是换行符n
。所以,最后我们获得的结果自然而然就是'n<head>n<title >Profile: Poseidon'
.
这类问题可能会以无数种不可预测的方式发生。因此,我们需要一种更加可靠的方法来从HTML
中提取文本。
Get to Know Regular Expressions
Regular expressions
,即正则表达式,是一种可根据某种特定的模式或规则在字符串中搜索所需的文本信息。Python
为我们提供了相应的正则表示式标准库——re
.
注意:正则表达式并不是
Python
语言特有的。它是一种通用的编程概念并且支持许多的编程语言。
要使用正则表达式,你需要做的第一件事是导入re
模块:
import re
正则表达式使用称为元字符
的特殊字符来表示不同的模式。例如,星号字符(*)表示在星号之前的任何东西的零个或多个实例。
是不是有点复杂?没关系,让我们来举一个简单的案例:
>>> re.findall("ab*c", "ac")
['ac']
这里,findall()
函数用于查找字符串中与给定正则表达式匹配的任何文本。其中,第一个参数是待匹配的正则表达式,第二个参数是要测试的字符串。在上面的例子中,表示的是在字符串ac
中搜索模式ab*c
。
正则表达式
ab*c
匹配字符串中以a
开头,以c
结尾,且两者之间有零个
或多个
b的任何子串。该函数将以列表的格式返回所有的匹配项。对应到上面的例子便是字符串ac
与此模式匹配,因此它在列表中返回。
还没看懂?没事,让我们举多几个示例:
>>> re.findall("ab*c", "abcd")
['abc']
>>> re.findall("ab*c", "acc")
['ac']
>>> re.findall("ab*c", "abcac")
['abc', 'ac']
>>> re.findall("ab*c", "abdc")
[]
可以看到,最后一条命令行返回的是一个空列表,这是因为没有搜索到满足该模式的字段,毕竟中间除了有一个b
还隔着一个d
.
需要注意的是,模式匹配是区分大小写的。如果我们想要忽略大小写,只需要传入相应的参数即可:
>>> re.findall("ab*c", "ABC")
[]
>>> re.findall("ab*c", "ABC", re.IGNORECASE)
['ABC']
此外,我们可以使用点.
来表示正则表达式中的任何单个字符。例如,你可以找到包含字母a
和c
的所有字符串,它们中间以一个字符分隔,如下所示:
>>> re.findall("a.c", "abc")
['abc']
>>> re.findall("a.c", "abbc")
[]
>>> re.findall("a.c", "ac")
[]
>>> re.findall("a.c", "acc")
['acc']
正则表达式中的模式.*
表示重复任意次数的任意字符。例如,你可以使用a.*c
来查找每一个以a
开头、以c
结尾的子字符串,不管中间是哪个字母:
>>> re.findall("a.*c", "abc")
['abc']
>>> re.findall("a.*c", "abbc")
['abbc']
>>> re.findall("a.*c", "ac")
['ac']
>>> re.findall("a.*c", "acc")
['acc']
通常,我们也可以使用re.search()
来搜索字符串中的特定模式。这个函数比re.findall()
要稍微复杂些,因为它返回一个名为MatchObject
的对象,该对象存储不同的数据组。这是因为可能在其他匹配中存在满足要求的项,而re.search()
则是返回所有可能的结果。
本文不涉及MatchObject
的任何细节。现在,我们只需知道在MatchObject
上调用group()
函数将返回第一个最贴切的结果,而这个结果在大多数情况下便是你想要的结果,例如:
>>> match_results = re.search("ab*c", "ABC", re.IGNORECASE)
>>> match_results.group()
'ABC'
re
模块中还有一个用于解析文本的函数。re.sub()
是英文单词substitute
的缩写,它允许你用新文本替换与正则表达式所匹配的字符串中的文本。它的行为有点像字符串中的replace()
方法。
传递给re.sub()
函数的第一个参数是正则表达式,后面是替换文本,最后是字符串。举个例子:
>>> string = "Everything is <replaced> if it's in <tags>."
>>> string = re.sub("<.*>", "ELEPHANTS", string)
>>> string
'Everything is ELEPHANTS.'
也许这不是我们心中期待的结果,对吧?大家可以思考下,这里的结果为什么貌似仅替换了第一个可能的匹配项,同时又省略了两个可能的匹配项中间哪些子串?
好了,为大家揭晓下答案把。其实,在Python
内部所实现的正则表达式,<.*>
表达式是基于一种称为贪心机制
的匹配算法,其实现原理是将第一个匹配项进行替换,同时忽略从第一个匹配项直至最后一个匹配项最后的子串内容。下面给大家再举一个例子应该就更清晰了:
>>> string = "Everything is <tag> if it's in <replaced> abc <test> ending."
>>> string = re.sub("<.*>", "ELEPHANTS", string)
>>> string
'Everything is ELEPHANTS ending.'
那么,我们是不是完全拿它没办法?当然不是,我们可以采用另外一种正则匹配规则<.*?>
,它可以将所有的可能匹配项均进行替换:
>>> string = "Everything is <replaced> if it's in <tags>."
>>> string = re.sub("<.*?>", "ELEPHANTS", string)
>>> string
"Everything is ELEPHANTS if it's in ELEPHANTS."
Extract Text With Regular Expression
学习了这些预备知识,现在我们可以尝试从另外一个个人资料页面中解析我们需要的标题了,哪怕当前标签页写得非常随意(大小写混合、空格、其他字符等),例如:
<TITLE >Profile: Dionysus</title / >
可以发现,如果我们还继续使用上一小节的find()
函数,它几乎处理不了这种不一致的情况,但却可以用正则表达式轻松处理,你可以尝试使用以下代码快速高效的实现:
# 新建一个测试文件|regex_soup.py
import re
from urllib.request import urlopen
url = "http://olympus.realpython.org/profiles/dionysus"
page = urlopen(url)
html = page.read().decode("utf-8")
pattern = "<title.*?>.*?</title.*?>"
match_results = re.search(pattern, html, re.IGNORECASE)
title = match_results.group()
title = re.sub("<.*?>", "", title) # Remove HTML tags
print(title)
仔细看看模式字符串中的第一个正则表达式<title.*?>.*?</title.*?>
,让我们把它拆解成三个部分:
-
首先,
<title.*?>
尝试匹配HTML
开头的<TITLE >
标签。这里我们调用了re.search()
方法并设置了忽略参数re.IGNORECASE
和全局匹配规则<.*?>
. -
中间部分的
.*?
则会匹配加载<title.*?>
和</title.*?>
中间的所有字符,即保留我们需要的标题信息。 -
最后,
</title.*?>
不同于第一个模式,它仅仅匹配带有/
字符的项,所以仅有</title / >
会与之匹配。
第二个正则表示re.sub("<.*?>", "", title
的作用也显而易见,就是将标签头直接替换为空串,因为在大多数情况下我们只需要里面的内容。
当你真正学会了正则表达式后,你会发现它是一个非常强大的工具。在本教程中,我们仅向大家介绍点皮毛,有关正则表达式如何学习及使用,后续我们将会出一篇文章单独讲解,欢迎关注公众号Pytrick
及时订阅最新的消息。
好了,今天的内容就到此为此了,下篇文章我们将带大家学习如何使用HTML
解析器去爬取网页以及如何进行组件交互。亲爱的小伙伴们,让我们下期再见,不见不散!
原文始发于微信公众号(Pytrick):【Python项目实战】网页爬取(上)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/56414.html