Python网页抓取教程:循序渐进

​— Python网页抓取教程:循序渐进

抓取网页入门其实挺简单的。在之前的文章中我们介绍了怎么用C#和JAVA两种方法来抓取网页,这一期给大家介绍一种更容易,也是使用最广泛的一种抓取方法,那就是 Python。

说起Python,大家应该并不陌生,它是目前入门最简单的一种方法了,因为它是 一种面向对象的语言。 Python的类和对象比任何其他语言都更容易使用。此外,Python 存在许多库, 因而在Python中构建用于网页抓取的工具轻而易举。

在这篇Python网络抓取教程中,我们将分步骤讲解如何利用python来抓取目标数据。首先需要从页面源获取基于文本的数据,然后将其存储到文件中并根据设置的参数对输出进行排序。使用Python进行网页抓取时还有一些更高级功能的选项,这些将在最后概述,并提供一些使用上的建议。按照教程下面概述的步骤进行操作,您将能知道如何进行网页抓取。

Python网页抓取教程 适用于所有操作系统。 不同系统安装Python或开发环境时会略有不同,其它部分均无不同。


我们所说的网页抓取是什么?

网络抓取是收集公共数据的自动化过程。爬虫会在几秒钟内自动从目标网站中提取大量公共数据。


#构建网络爬虫:Python准备工作

在整个网络抓取教程中,将使用 Python3.4以上 版本,您可以 此页面 下载。

准确的说,我们使用了 3.8.3, 但任何3.4+版本都应该可以正常运行我们下面用到的代码。

对于Windows系统,安装Python时确保选中 “PATH安装”。 PATH安装将可执行项添加到默认的Windows命令提示符可执行项搜索中。然后Windows将识别诸如 “pip” “python” 之类的命令,而无需用户将其指向可执行文件的目录(例如C:/tools/python/.../python.exe)。如果您已经安装了Python但没有勾选复选框,只需重新运行安装并选择修改。在第二页上选择 “添加到环境变量” 即可。


了解Python库

由于可用的许多有用的库,使用Python进行网页抓取很容易。

Python的一大优势在于 可供选择的库很多。 这些网页抓取用到的库现在已经用于数以万计的Python项目——仅在PyPI上,现在就有超过300,000个项目。您可以选择多种类型的Python网页抓取库:

●Requests

●Beautiful Soup

●lxml

●Selenium


01 #Requests库

网页抓取首先向网站服务器 发送HTTP请求 (例如 POST GET ),该请求会返回一个包含所需数据的响应。但是,标准Python HTTP库难以使用,为了提高效率,需要大量代码行,这进一步加剧了已经存在的问题。

与其他HTTP库不同,Requests库通过减少代码行简化了发出此类请求的过程,使代码更易于理解和调试,而不会影响其有效性。使用 pip命令 就可以从终端内安装该库:

pip install requests

Requests库 提供了发送HTTP GET POST 请求的简单方法。例如,发送HTTP Get请求的函数被恰当地命名为get():

import requests
response = requests.get("https://oxylabs.io/”)
print(response.text)

如果需要发布表单,可以使用 post() 方法轻松完成。表单数据可以作为 字典 发送,如下所示:

form_data = {'key1': 'value1', 'key2': 'value2'}
response = requests.post("https://oxylabs.io/ ", data=form_data)
print(response.text)

请求库还会使那些需要进行身份验证的代理变得非常容易使用。

proxies={'http': 'http://user:password@proxy.oxylabs.io'}
response = requests.get('http://httpbin.org/ip', proxies=proxies)
print(response.text)

但是这个库有一个局限性,它 不解析提取的HTML数据, 也就是说它不能将数据转换成更易读的格式进行分析。此外,它 不能用于抓取纯JavaScript编写的网站。


02 #Beautiful Soup

Beautiful Soup是一个Python库,它与 解析器 一起从HTML中提取数据,甚至可以将无效标记转换为解析树。但是,该库 仅用于解析, 不能以HTML文档/文件的形式从网络服务器请求数据。它主要与Python Requests库一起使用。需要注意的是,Beautiful Soup可以 轻松查询和导航HTML, 但仍需要解析器。以下示例演示了 html.parser 模块的使用,该模块是Python标准库的一部分。

#Part 1–使用Requests获取HTML

import requests
url='https://oxylabs.io/blog'
response = requests.get(url)

#Part 2–查找元素

from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')
print(soup.title)

标题里的元素会输出如下:

<h1 class="blog-header">Oxylabs Blog</h1>

由于其导航、搜索和修改解析树方法均很简单,Beautiful Soup即使对于初学者也是十分不错的一个库,并且通常可以节省开发人员数小时的工作时间。例如,要输出此页面中的所有博客标题,就可以使用 findAll() 。在此页面上,会找到所有 h2 大小,且类属性为 blog-card__content-title 的博客标题。该信息可以配合 findAll 方法使用,如下所示:

blog_titles = soup.findAll('h2', attrs={"class":"blog-card__content-title"})
for title in blog_titles:print(title.text)
# Output:
# Prints all blog tiles on the page

BeautifulSoup还可以轻松使用 CSS selectors 。如果开发人员知道CSS selector,则无需学习 find() find_all() 方法。以下是相同的示例,但使用的是CSS selectors:

blog_titles = soup.select('h2.blog-card__content-title')
for title in blog_titles:
print(title.text)

虽然能解析有问题的HTML是该库的主要功能之一,但它还提供了许多其它功能,包括 检测页面编码,更进一步提高从HTML文件中提取数据的准确性。

更重要的是,它可以 轻松配置, 只需几行代码,即可提取任何自定义的公开可用数据或识别特定的数据类型。我们的 Beautiful Soup教程 包含有关此配置和其他配置的更多信息,以及该库的工作原理。


03 #lxml

lxml是一个解析库。它是一个快速、强大且易于使用的库,适用于 HTML XML 文件。此外,lxml是 大量提取数据的理想选择。 然而,与Beautiful Soup不同的是,这个库针对设计的不好的HTML可能会出现解析不了的情况。

可以使用以下 pip 命令从终端安装lxml库:

pip install lxml

这个库包含一个 html 模块来处理HTML。但是,lxml库首先需要HTML字符串。可以使用上一节中讨论的 Requests库 检索此HTML字符串。一旦HTML可用,就可以使用下面的 fromstring 方法构建树:

# After response = requests.get()
from lxml import html
tree = html.fromstring(response.text)

现在可以使用 XPath 查询此树。继续上一节中讨论的示例,要获取博客的标题,XPath将如下所示:

//h2[@class="blog-card__content-title"]/text()

可以将此XPath提供给 tree.xpath() 函数。这将返回与此XPath匹配的所有元素。注意XPath中的 text() 函数。该函数会提取 h2 元素内的文本。

blog_titles = tree.xpath('//h2[@class="blog-card__content-title"]/text()')
for title in blog_titles:
print(title)

假设您希望学习使用这个库并将其集成到您的网络抓取工作中,或者只是在您现有的专业知识基础上学习更多知识。您可以参见 更详细的lxml教程


04 #Selenium

如上所述,一些网站是使用 JavaScript 编写的,JavaScript是一种允许开发者 动态填充字段和菜单 的语言。这给只能从静态网页中提取数据的Python库带来了问题。事实上,当涉及到JavaScript时,Requests库将无法使用。这个时候就是 Selenium网络抓取 的用武之地。


这个Python网络库是一个 开源的浏览器自动化工具 (网络驱动),它允许您自动执行诸如登录社交媒体平台之类的过程。Selenium广泛用于 在应用程序上测试案例或测试脚本。 它在网页抓取方面的优势源于它能够像任何浏览器一样通过运行JavaScript来呈现网页——标准的网络爬虫无法运行这种编程语言。目前Selenium已被开发人员广泛使用。


Selenium需要三个组件:

●浏览器–支持的浏览器有 Chrome、Edge、Firefox和Safari。

●浏览器驱动程序-请参阅 此页面 以获取驱动程序的链接。

Selenium 安装包。


可以从终端安装selenium包:

pip install selenium

安装后,可以导入浏览器的相应类。导入后,必须创建 类的对象。 注意,这将需要可执行驱动程序的路径。Chrome浏览器示例如下:

from selenium.webdriver import Chrome
driver = Chrome(executable_path='/path/to/driver')

现在可以使用该 get() 方法在浏览器中加载任何页面。

driver.get('https://oxylabs.io/blog')

Selenium允许使用 CSS Selectors XPath 来提取元素。以下示例使用CSS Selectors输出所有博客标题:

blog_titles = driver.get_elements_by_css_selector(' h2.blog-card__content-title')
for title in blog_tiles:
print(title.text)
driver.quit() # closing the browser

通过运行JavaScript,Selenium可以处理动态显示的任何内容,然后可用内置方法甚至Beautiful Soup对网页内容进行解析。此外,它还可以 模仿用户的行为。

在网络抓取中使用Selenium的唯一缺点是 它会减慢过程, 因为它必须先为每个页面执行JavaScript代码,然后才能对其进行解析。因此,它 不适合大规模的数据提取。 但是,如果您希望小规模提取数据或者不在乎数据提取速度,那么Selenium是一个不错的选择。


支持网页抓取的Python库比较


对于这次的Python网页抓取教程,我们将使用三个重要的库—— BeautifulSoup v4、Pandas和Selenium。 请提前安装好这些库。如果您收到“NameError:name* is not defined”,则可能存在没安装成功的库。


#网络驱动程序和浏览器

每个网络爬虫都会使用浏览器,因为它需要连接到目标 URL 。出于测试目的,我们强烈建议使用 常规浏览器 (或不是无头浏览器),尤其是对于新手。查看编写的代码如何与应用程序交互可以进行 简单的故障排除和调试, 也有助于更好地理解整个过程。

无头浏览器可以在后面再使用,因为它们对于 复杂的任务 更有效。在本次网页抓取教程中,我们将使用Chrome浏览器,其实整个过程用Firefox浏览器也几乎相同。

首先,使用您喜欢的搜索引擎查找 “Chrome(或Firefox)的网络驱动” 。记下您浏览器的当前版本。下载与您的浏览器版本匹配的网络驱动程序。

如果适用,请选择所需的软件包,下载并解压缩。将驱动程序的可执行文件复制到任何易于访问的目录即可。操作是否正确,后面运行程序的时候就知道了。


为我们的Python网络爬虫寻找良好的编码环境

在我们进入本次网页抓取教程的编程部分之前,需要采取最后一步: 使用良好的编码环境。 有很多选择,从简单的 文本编辑器 (只需创建*.py文件并直接写下代码就足够了),到功能齐全的 IDE (集成开发环境)。

如果您已经安装了 Visual Studio Code, 选择这个IDE将是最简单的选择。否则,我强烈建议新手使用 PyCharm ,因为它几乎没有入门门槛,并且有直观的用户界面。后面我们将使用PyCharm用于网页抓取教程。

在PyCharm中,右键单击项目区域并“新建->Python文件”。给它取个好听的名字!


Part 1 导入和使用库

是时候使用我们之前安装的所有包了:

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver

PyCharm 可能会以灰色显示这些导入,因为它会自动标记未使用的库。不要接受PyCharm删除未使用的库的建议。

首先,定义我们的浏览器。根据我们在“网络驱动和浏览器”中选择的网络驱动,我们应该输入:

driver = webdriver.Chrome(executable_path='c:\path\to\windows\webdriver\executable.exe')
driver = webdriver.Firefox(executable_path='/nix/path/to/webdriver/executable')


Part 2 选择一个网址

Python网页抓取需要查看网站的来源

在执行我们第一次测试运行之前,选择一个 URL 。由于本次网页抓取教程旨在创建一个基本应用程序,我们强烈建议您选择一个简单的目标URL:

●避开隐藏在 Javascript 元素中的数据。这些数据有时需要通过执行特定操作来触发才能显示。从Javascript元素中抓取数据需要更复杂的Python使用方法及逻辑。

●避开抓取图像。图像可以直接用 Selenium 下载。

●在进行任何抓取活动之前,请确保您正在抓取的是 公共数据, 并且绝不会侵犯第三方权利。另外,不要忘记查看 robots.txt 文件获得指导。

选择您要访问的登录页面并将 URL 输入到 driver.get('URL') 参数中。Selenium要求提供连接协议。因此,始终需要将“http://”或“https://”附加到URL上。

driver.get('https://your.url/here?yes=brilliant')

尝试通过单击左下角的 绿色箭头 或右键单击编码环境并选择 “运行” 来进行测试运行。

点击红色指针指到的地方

如果您收到一条错误消息,指出文件丢失,请仔细检查驱动程序 “webdriver.*” 中提供的路径是否与可执行网络驱动的位置匹配。如果您收到版本不匹配的消息,请重新下载正确的可执行网络驱动。


Part 3 定义对象和构建列表

Python允许编码人员在不指定确切类型的情况下设计对象。可以通过简单地键入其标题并分配一个值来创建对象。

# Object is “results”, brackets make the object an empty list.
# We will be storing our data here.
results = []

Python中的列表是 有序的、可变的 并且 允许复制 列表中的成员。当然您也可以使用其他集合,例如集合或字典。但列表是最容易使用的。下面我们先来添加一些对象。

# Add the page source to the variable `content`.
content = driver.page_source
# Load the contents of the page, its source, into BeautifulSoup
# class, which analyzes the HTML as a nested data structure and allows to select
# its elements by using various selectors.
soup = BeautifulSoup(content)

我们回顾一下之前已经写好的代码:

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)

重新运行应用程序,不应显示任何错误。如果出现任何问题,前面的章节中概述了一些可能的故障排除选项。


Part 4 使用Python网页抓取工具提取数据

这部分有趣而又困难——从 HTML文件 中提取数据。由于几乎在所有网页下,我们都会从页面的不同部分中提取需要的部分,并且我们希望将其存储到列表中,因此我们需要处理每个小的部分,然后将其添加到列表中:

# Loop over all elements returned by the `findAll` call. It has the filter `attrs` given
# to it in order to limit the data returned to those elements with a given class only.
for element in soup.findAll(attrs={'class': 'list-item'}):
...

“soup.findAll” 可以接受各种参数。出于本教程的目的,我们仅使用 “attrs” (属性)参数。它允许我们通过设置一个语句“如果属性等于X为真,则……”来缩小搜索范围。很容易就能找到和使用寻找的类,我们下面将会用到该参数。

在继续之前,让我们在真实的浏览器中访问所选的 URL 。然后使用 CTRL+U(Chrome) 打开页面源代码或右键单击并选择“查看页面源代码”。找到嵌套数据的“最近”类。另一种选择是按 F12 打开开发者工具来选择 Element Picker 。例如,它可以嵌套为:

<h4 class="title">
 <a href="...">This is a Title</a>
</h4>

我们的属性 “class” 就是 “title”。 如果您选择了一个简单的目标,在大多数情况下,数据将以与上述示例类似的方式嵌套。获取复杂的目标数据可能需要更多尝试。让我们回到编码并添加我们在源代码中找到的类:

# Change ‘list-item’ to ‘title’.
for element in soup.findAll(attrs={'class': 'title'}):
  ...    

我们的循环现在将遍历页面源中具有 “title” 类的所有对象。我们会处理每一个对象:

name = element.find('a')

让我们看看我们的循环是如何遍历HTML的:

<h4 class="title">
 <a href="...">This is a Title</a>
</h4>

我们的第一个语句(在循环本身中)查找所有匹配标签的元素,其 “class” 属性包含 “title”。 然后我们在该类中执行另一个搜索。我们的第二次搜索查找文档中的所有标签(被包括在内,而像这样的部分匹配则不被包括在内)。最后,对象被分配给变量 “name”。

然后,我们可以将对象名称分配给我们之前创建的列表数组 “results”, 但这样做会将整个标签及其内部的文本合并到一个元素中。在大多数情况下,我们只需要文本本身而不需要任何额外的标签。

# Add the object of “name” to the list “results”.
# `.text` extracts the text in the element, omitting the HTML tags.
results.append(name.text)

我们的循环将遍历整个页面源,找到上面列出的所有出现的类,然后将嵌套数据附加到我们的列表中:

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for element in soup.findAll(attrs={'class': 'title'}):
name = element.find('a')
results.append(name.text)

请注意,循环后的两个语句是缩进的。循环需要缩进来表示嵌套。任何一致的缩进都将被视为合法。没有缩进的循环将输出 “IndentationError” 报错,并用“箭头”指出违规语句。


Part 5 导出数据

Python网页抓取需要不断仔细地检查代码

即使在运行我们的程序时没有出现语法或运行时的错误,仍然可能存在 语义 错误。您需要检查我们获得的数据 是不是分配给指定对象并正确移动到数组的。

检查您获取的数据是否正确收集的最简单方法之一是使用 “print”。 由于数组有许多不同的值,因此通常使用一个简单的循环将每个条目分行进行输出:

for x in results:
   print(x)    

在这一点上, “print ”和 “for” 是配合使用的。我们只是为了快速测试和调试目的进行循环。直接输出结果也是完全可行的:

print(results)

到目前为止,我们的代码应该是这样的:

driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
    results.append(name.text)
for x in results:
print(x)

现在运行我们的程序应该不会报错,调试窗口中也应该会显示获取的数据。虽然 “print” 非常适合用于测试目的,但它对于解析和分析数据并不是很有用。

您可能已经注意到,到目前为止, “import pandas” 仍然是灰色的。我们最终还是会充分利用库。建议现在删除 “print” 循环,因为接下来我们要做的事情与此类似,并且会将数据移动到 csv 文件。

df = pd.DataFrame({'Names': results})
df.to_csv('names.csv', index=False, encoding='utf-8')

我们的两个新语句依赖于 pandas 库。我们的第一个语句创建了一个变量 “df” 并将其对象转换为二维数据表。 “Names” 是我们列的名称,而 “results” 是我们要输出的列表。注意, pandas 可以创建多个列,我们只是没有足够的列表来使用这些参数(目前)。

我们的第二个语句将变量 “df” 的数据移动到特定的文件类型(在本例中为 “csv” )。我们的第一个参数为我们即将创建的文件分配一个名称和一个扩展名。添加扩展名是必要的,否则 “pandas” 将输出一个没有扩展名的文件,并且必须手动更改。“索引”可用于为列分配特定的起始编号。“编码”用于以特定格式保存数据。一般情况下使用 UTF-8 就足够了。

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
    results.append(name.text)
df = pd.DataFrame({'Names': results})
df.to_csv('names.csv', index=False, encoding='utf-8')

现在所有导入的库应该都不是灰色的了,并且运行我们的应用程序可以将 “names.csv” 输出到我们的项目目录中。注意, “Guesed At Parser” 警告仍然存在。我们可以通过安装第三方解析器来删除它,但对于本Python网页抓取教程而言,默认的HTML选项就可以了。


Part 6 更多清单

Python网页抓取通常需要很多数据点

许多网页抓取操作需要获取多组数据。例如,仅提取电子商务网站上列出项目的标题几乎没用。为了收集有意义的信息并从中得出结论,至少需要 两个数据点。

出于本教程的目的不同,我们将尝试一些稍微不同的代码。由于从同一个类中获取数据只是意味着一个额外的列表,我们应该尝试从不同的类中提取数据,但同时保持我们表的结构。

显然,我们需要另一个列表来存储我们的数据。

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
for b in soup.findAll(attrs={'class': 'otherclass'}):
# Assume that data is nested in ‘span’.
name2 = b.find('span')
other_results.append(name.text)

由于我们将从HTML的不同部分提取额外的数据点,因此我们需要一个额外的循环。如果需要,我们还可以添加另一个 “if” 条件来控制重复条目:

最后,我们需要改变我们的数据表的形成方式:

df = pd.DataFrame({'Names': results, 'Categories': other_results})

到目前为止,我们代码的最新迭代应该是这样的:

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
content = driver.page_source
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
    results.append(name.text)
for b in soup.findAll(attrs={'class': 'otherclass'}):
name2 = b.find('span')
other_results.append(name.text)
df = pd.DataFrame({'Names': results, 'Categories': other_results})
df.to_csv('names.csv', index=False, encoding='utf-8')

现在可以试试看,如果一切顺利,运行此代码不会输出任何错误。在某些情况下, “pandas” 会输出 “ValueError:arrays must all be the same length” 报错消息。简单来说, “results” “other_results” 列表的长度不相等,因此pandas无法创建二维表。

有多种方法可以解决该错误消息。从用“空”值填充最短列表到创建字典,再到创建两个系列并列出它们。我们选择第三种做法:

series1 = pd.Series(results, name = 'Names')
series2 = pd.Series(other_results, name = 'Categories')
df = pd.DataFrame({'Names': series1, 'Categories': series2})
df.to_csv('names.csv', index=False, encoding='utf-8')

请注意,数据不会匹配,因为列表长度不均匀,但如果需要两个数据点,创建两个系列是最简单的解决方法。我们的最终代码应该是这样的:

import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
driver = webdriver.Chrome(executable_path='/nix/path/to/webdriver/executable')
driver.get('https://your.url/here?yes=brilliant')
results = []
other_results = []
content = driver.page_source
soup = BeautifulSoup(content)
for a in soup.findAll(attrs={'class': 'class'}):
name = a.find('a')
if name not in results:
    results.append(name.text)
for b in soup.findAll(attrs={'class': 'otherclass'}):