in

Python网络爬虫实战教程和具体示例项目

Python网络爬虫实战教程和具体示例项目

在这个 Python 网络抓取教程中,我们将深入探讨是什么让 Python 在网络抓取方面成为第一语言。我们将介绍使用 Python 进行网络抓取的基础知识和最佳实践。

在本简介中,我们将涵盖以下主要主题:

  • HTTP 协议 – 什么是 HTTP 请求和响应以及如何使用它们从网站收集数据。
  • 数据解析——如何解析收集到的 HTML 和 JSON 文件以提取结构化数据。

最后,我们将通过一个示例项目巩固我们的知识,方法是从remotepython.com/jobs/抓取工作列表数据——远程 Python 工作的工作列表板。

什么是网页抓取?

21 世纪最大的革命之一是意识到数据的价值——互联网上到处都是免费的公共数据!

Web 抓取是一种收集公共 Web 数据的自动化过程。人们可能想要收集这些公共数据的原因有成千上万,例如寻找潜在员工或收集竞争情报。

要使用 Python 抓取网站,我们通常会处理两种类型的问题:收集在线可用的公共数据,然后解析此数据以获取结构化产品信息。

那么,如何使用 Python 从网站上抓取数据呢?在本文中,我们将涵盖您需要了解的所有内容 – 让我们深入了解吧!

设置

在本教程中,我们将介绍几个流行的网络抓取库:

  • httpx – HTTP 客户端库,最常用于网络抓取。另一个流行的替代方法是Requests库,但我们会坚持使用httpx它,因为它更适合网络抓取。
  • beauitifulsoup4 – 我们将使用 BeautifulSoup 进行 HTML 解析。
  • parsel – 另一个支持 XPath 选择器的 HTML 解析库 – 解析 HTML 内容的最强大的标准工具。
  • jmespath – 我们将看看这个用于 JSON 解析的库。

pip install我们可以使用控制台命令安装所有这些库:

$ pip install httpx parsel beautifulsoup4 jmespath

快速开始

在我们深入研究之前,让我们快速看一下一个简单的网络抓取工具:

import httpx
from parsel import Selector

# Retrieve html page
response = httpx.get("https://www.remotepython.com/jobs/")
# check whether request was a success
assert response.status_code == 200
# parse HTML for specific information:
selector = Selector(text=response.text)
for job in selector.css('.box-list .item'):
    title = job.css('h3 a::text').get()
    relative_url = job.css('h3 a::attr(href)').get()
    print(title)
    print(response.url.join(relative_url))
    print('--------------------------')

示例输出

Back-End / Data / DevOps Engineer  
https://www.remotepython.com/jobs/8173028f333140e1b6d74f70dc42a52a/
--------------------------
Lead Software Engineer (Python)  
https://www.remotepython.com/jobs/a63708cb43df422dbe76938c843ed1fb/
--------------------------
Senior Back End Engineer  
https://www.remotepython.com/jobs/de4dab9efc7b435b860cd3003a122c63/
--------------------------
Full Stack Python Developer - remote  
https://www.remotepython.com/jobs/98c317bf6f8b4610a4476407cff32b2d/
--------------------------
Remote Python Developer  
https://www.remotepython.com/jobs/dadf4aacff444043b601f6665b53889c/
--------------------------
Python Developer  
https://www.remotepython.com/jobs/0f52fc0bb2a04a0db67238b63df6d5aa/
--------------------------
Senior Software Engineer  
https://www.remotepython.com/jobs/e0e51ee44bb443e98dde0d9d8390a933/
--------------------------
Remote Senior Back End Developer (Python)  
https://www.remotepython.com/jobs/a6bcd1b264134ef8b6715f2aa05da00f/
--------------------------
Full Stack Software Engineer  
https://www.remotepython.com/jobs/dac24df8ef2a47e6ad41bf05343d74bd/
--------------------------
Remote Python & JavaScript Full Stack Developer  
https://www.remotepython.com/jobs/f9d92f4a5743457d9f7fae31a3ebc057/
--------------------------
Sr. Back-End Developer  
https://www.remotepython.com/jobs/3c70ed5dd269402f83a54f93e35add9c/
--------------------------
Backend Engineer  
https://www.remotepython.com/jobs/ecca5fc4a9194387b19c3bcd491216df/
--------------------------
Miscellaneous tasks for existing Python website, Django CMS and Vue 2  
https://www.remotepython.com/jobs/6edf140866784803a862574861cae487/
--------------------------
Senior Django Developer  
https://www.remotepython.com/jobs/7b04bdee004a4dab9598cc4dfc0ae029/
--------------------------
Sr. Backend Python Engineer  
https://www.remotepython.com/jobs/6b7920f8cd6943ad8fe45c634c3daed6/
--------------------------

这个快速抓取工具将收集我们示例目标首页上的所有职位和 URL。挺容易!让我们更深入地了解所有这些细节。

HTTP 基础知识

要从公共资源收集数据,我们需要先与其建立连接。

大多数网络都是通过 HTTP 提供服务的,这是一种相当简单的数据交换协议:
我们(客户端)向网站(服务器)发送请求以获取特定文档。服务器处理请求并回复包含 Web 数据或错误消息的响应。非常直接的交流!

因此,我们发送一个由 3 部分组成的请求对象:

  • 方法– 少数可能的类型之一。
  • headers – 关于我们请求的元数据。
  • location – 我们要检索的文档。

反过来,我们收到一个响应对象,其中包括:

  • 状态代码– 指示成功或失败的少数可能性之一。
  • headers – 关于响应的元数据。
  • content – 页面数据,如 HTML 或 JSON。

让我们快速浏览一下这些组件中的每一个,它们的含义以及它们在网络抓取中的相关性。

请求和响应

说到网络抓取,我们只需要了解一些 HTTP 基础知识即可。让我们快速浏览一下。

请求方法

HTTP 请求可以很方便地分为执行不同功能的几种类型(称为方法)。
网络抓取中最常用的类型是:

  • GET– 索取文件。
  • POST– 通过发送文件请求文件。
  • HEAD– 请求文档元信息,例如上次更新时间。

在网络抓取中,我们将主要使用 GET 类型的请求来检索文档。
在抓取网页的交互部分(如表单、搜索或分页)时,POST 请求也很常见。
HEAD 请求用于优化——爬虫可以请求元信息,然后决定是否值得下载整个页面。

其他方法不经常使用,但最好了解它们:

  • PATCH– 更新现有文件。
  • PUT– 创建新文档或更新它。
  • DELETE– 删除文件。

请求位置

请求位置由 URL(通用资源位置)定义,该 URL 由几个关键部分构成:

在这里,我们可以可视化 URL 的每个部分:

  • 协议 – 当谈到 HTTP 时,要么是http要么https
  • 主机 – 服务器的地址,可以是域名或 IP 地址。
  • 位置 – 资源所在的唯一路径。

如果您不确定 URL 的结构,您可以随时启动 python 并让它为您解决:

from urllib.parse import urlparse
urlparse("http://www.domain.com/path/to/resource?arg1=true&arg2=false")
# which will print:
ParseResult(
  scheme='http', 
  netloc='www.domain.com', 
  path='/path/to/resource', 
  params='', 
  query='arg1=true&arg2=false', 
  fragment=''
)

请求标头

虽然看起来请求标头只是次要的元数据细节,但在网络抓取中它们非常重要。

标头包含有关请求的基本详细信息 – 谁在请求数据?他们期望什么类型的数据?使用错误或不完整的标头可能会导致错误甚至阻止网络抓取工具

让我们来看看一些最重要的标头及其含义。

用户代理

这是客户端的身份标头。它告诉服务器什么类型的客户端正在发出请求:它是桌面网络浏览器吗?还是电话应用程序?

# example user agent for Chrome browser on Windows operating system:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36

每当您在网络浏览器中访问网页时,它都会使用
类似于“浏览器名称、操作系统、某些版本号”的用户代理字符串来标识自己。

这有助于服务器确定是为客户端提供服务还是拒绝客户端。当然,在网络抓取中,
我们不想被拒绝访问,所以我们必须通过伪造我们的用户代理看起来像浏览器之一来融入其中。

各种平台的用户代理字符串在线数据库很多。例如,whatismybrowser.com 的用户代理数据库

Cookie 用于存储持久数据。这是网站跟踪用户状态的一项重要功能:用户登录、配置首选项等。所有 cookie 信息都通过此Cookie标头进行交换。

接受

Accept 标头(还有Accept-EncodingAccept-Language等)包含有关客户端期望接收的内容类型的信息。

通常,当网络抓取时,我们想模仿一种流行的网络浏览器。例如,以下是 Chrome 浏览器使用的值:

对于所有标准值,请参阅MDN 的内容协商标头列表

X- 前缀标头

这些标头是特殊的自定义标头,可能意味着任何东西。这些在网络抓取时非常重要,因为它们可能配置网站/网络应用程序的重要功能。

响应状态码

方便的是,所有 HTTP 响应都带有一个状态代码,指示此请求是成功、失败还是需要更多详细信息(如登录或身份验证令牌)。

让我们快速浏览一下与网络抓取最相关的状态代码:

  • 200个范围码一般代表成功!
  • 300 个范围代码往往意味着重定向 – 换句话说,如果我们请求/product1.html它可能会被移动到一个新的位置,就像/products/1.html300 个状态响应会告诉我们的那样。
  • 400范围代码表示请求格式错误或被拒绝。我们的网络抓取工具可能会丢失一些标头、cookie 或身份验证详细信息。
  • 500 个范围代码通常意味着服务器问题。该网站可能现在不可用或有意禁止访问我们的网络抓取工具。

响应头

在网络抓取方面,响应标头为连接功能和效率提供了一些重要信息,尽管我们很少需要在基本网络抓取中使用响应标头。

Web 抓取中最值得注意的响应标头是Set-Cookie要求我们的客户端为将来的请求保存一些 cookie 的标头。Cookie 对于网站功能至关重要,因此在网络抓取时管理它们非常重要。

带前缀的标X-头是由网站设置的自定义标头,其中可以包含额外的响应详细信息或秘密令牌。

最后,还有一些与缓存相关的标头,对抓取器优化很有用:

  • Etag标头通常指示响应的内容哈希,让抓取器知道自上次抓取以来内容是否发生了变化。

Last-Modifiedheader 告诉页面最后一次更改它的内容是什么时候。

我们简单地忽略了核心 HTTP 组件,现在是时候看看 HTTP 在实际 Python 中是如何工作的了!

Python 中的 HTTP 客户端

在我们开始探索 Python 中的 HTTP 连接之前,我们需要选择一个 HTTP 客户端。让我们来看看在处理 HTTP 连接方面,Python 中最好的 Web 抓取库是什么。

尽管Python 带有一个名为urllib的内置 HTTP 客户端,但它不太适合网络抓取。幸运的是,社区提供了几个很好的选择:

  • httpx(推荐)——功能最丰富的客户端,提供 http2 支持和异步客户端。
  • requests – 最受欢迎的客户端,因为它是最容易使用的客户端之一。
  • aiohttp – 非常快的异步客户端和服务器。

那么,什么才是一个好的 HTTP 客户端用于网络抓取呢?
首先要注意的是 HTTP 版本。网络上使用了 3 个流行版本:

  • HTTP1.1 最简单的基于文本的协议,被更简单的程序广泛使用。由urllib , requests , httpx , aiohttp实现
  • HTTP2 更复杂/高效的基于二进制的协议,主要由网络浏览器使用。由httpx实现
  • HTTP3/QUIC 是网络浏览器主要使用的最新、最高效的协议版本。由aioquic、httpx实现(计划中)

当谈到网络抓取时,HTTP1.1 对于大多数情况来说已经足够好了,尽管 HTTP2/3 对于避免网络抓取器阻塞非常有帮助,因为大多数真实的网络用户都使用 HTTP2+ 网络浏览器。

我们将坚持使用,httpx因为它提供了网络抓取所需的所有功能。也就是说,其他 HTTP 客户端(如请求库)几乎可以互换使用。

使用 httpx 探索 HTTP

现在我们对 HTTP 有了基本的了解,让我们来看看它的实际应用吧!

在本节中,我们将试验基本的网络抓取场景,以在实践中进一步了解 HTTP。对于我们的示例案例研究,我们将使用http://httpbin.org请求测试服务,它允许我们发送请求并准确返回发生的情况。

获取请求

让我们从 -type 请求开始GET,这是网络抓取中最常见的请求类型。

简而言之GET通常意味着:给我位于 URL 的文档。
例如,GET https://www.httpbin.org/html请求将向服务器请求/html文档httpbin.org

import httpx
response = httpx.get("https://httpbin.org/html")
html = response.text
metadata = response.headers
print(response.status_code)
print(html)
print(metadata)

示例输出

200
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
      <h1>Herman Melville - Moby-Dick</h1>

      <div>
        <p>
          Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency.
        </p>
      </div>
  </body>
</html>
Headers({'date': 'Thu, 24 Nov 2022 09:48:41 GMT', 'content-type': 'text/html; charset=utf-8', 'content-length': '3741', 'connection': 'keep-alive', 'server': 'gunicorn/19.9.0', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'})

在这里,我们执行一个基本GET请求,尽管真正的网络抓取器请求往往有点复杂。接下来,让我们看一下请求标头。

请求元数据 – 标头

我们已经完成了请求标头的理论概述,因为它们在网络抓取中非常重要,让我们来看看我们如何将它们与我们的 HTTP 客户端一起使用:

import httpx
response = httpx.get('http://httpbin.org/headers')
print(response.text)

在此示例中,我们使用 httpbin.org 测试标头的端点,它将发送的输入(标头、正文)作为响应正文返回给我们。如果我们使用特定的标头运行这段代码,我们可以看到客户端正在自动生成一些基本的标头:

{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Host": "httpbin.org", 
    "User-Agent": "python-httpx/0.19.0", 
  }
}

即使我们没有在我们的请求中明确提供任何标头,也httpx为我们生成了所需的基础知识。
要添加一些自定义标头,我们可以使用headers参数,并以ScrapFly为例

import httpx
response = httpx.get('http://httpbin.org/headers', headers={"User-Agent": "ScrapFly's Web Scraping Tutorial"})
print(response.text)
# will print:
{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Host": "httpbin.org", 
    "User-Agent": "ScrapFly's Web Scraping Tutorial", 
    #  ^^^^^^^ - we changed this!
  }
}

正如您在上面看到的,我们User-Agent为此请求使用了自定义标头,而其他标头仍由我们的客户端自动生成。

POST 请求

正如我们所发现的,GET 类型的请求只是意味着“给我那个文档”,但有时这可能不足以让服务器提供正确的内容,而这正是 POST 类型请求的来源。

POST 类型的请求本质上意味着“拿走这份文件”。但是,为什么我们要在网络抓取时给某人一份文件呢?
一些网站操作需要一组复杂的参数来处理请求。例如,要呈现搜索结果页面,网站可能需要许多不同的参数,如搜索查询、页码和各种过滤器。提供如此庞大参数集的唯一方法是使用 POST 请求将它们作为文档发送。

让我们快速看一下如何在 httpx 中使用 POST 请求:

import httpx
response = httpx.post("http://httpbin.org/post", json={"question": "Why is 6 afraid of 7?"})
print(response.text)
# will print:
# {
#   ...
#   "data": "{\"question\": \"Why is 6 afraid of 7?\"}", 
#   "headers": {
#     "Content-Type": "application/json", 
#      ...
#   }, 
# }

如您所见,如果我们提交此请求,服务器将收到一些 JSON 数据,以及一个Content-Type指示此文档类型的标头(在本例中为application/json)。有了这些信息,服务器会做一些思考并返回一个与我们的请求数据匹配的文档。

配置代理

代理服务器通过中间人服务器路由网络来帮助伪装客户端的原始地址。

许多网站不容忍网络抓取工具,可以在几次请求后阻止它们。因此,代理可用于通过多个代理身份分发请求 – 一种避免阻塞的简单方法。另外,某些网站仅在某些地区可用,代理也可以帮助访问这些网站。

Httpx 支持 HTTP 和 SOCKS5 类型代理的广泛代理选项:

import httpx
response = httpx.get(
    "http://httpbin.org/ip",
    # we can set proxy for all requests
    proxies = {"all://": "http://111.22.33.44:8500"},
    # or we can set proxy for specific domains
    proxies = {"all://only-in-us.com": "http://us-proxy.com:8500"},
)

管理 Cookie

Cookie 用于帮助服务器跟踪其客户端的状态。它启用持久连接功能,例如登录会话或网站首选项(货币、语言等)。

在网络抓取中,我们会遇到没有 cookie 就无法运行的网站,因此我们必须在 HTTP 客户端连接中复制它们。在 httpx 中我们可以使用cookies参数:

import httpx

# we can either use dict objects
cookies = {"login-session": "12345"}
# or more advanced httpx.Cookies manager:
cookies = httpx.Cookies()
cookies.set("login-session", "12345", domain="httpbin.org")

response = httpx.get('https://httpbin.org/cookies', cookies=cookies)
# new cookies can also be set by the server
response.cookies

大多数 HTTP 客户端可以通过会话对象自动跟踪 cookie。在 httpx 中,它是通过以下方式完成的httpx.Client

import httpx

session = httpx.Client()
# this mock request will ask server to set some cookies for us:
response1 = session.get('http://httpbin.org/cookies/set/mycookie/123')
print(response1.cookies)
# now we don't need to set cookies manually, session keeps track of them
response2 = session.get('http://httpbin.org/cookies')
# we can see the automatic cookies in the response.request object:
response2.request.headers['cookie']
'mycookie=123'

把它们放在一起

现在我们已经简要介绍了 Python 中的 HTTP 客户端,让我们应用我们所学的一切。

在本节中,我们有一个简短的挑战:我们有多个要检索其 HTML 的 URL。让我们看看我们可能会遇到什么样的实际挑战,以及一个真正的网络抓取程序是如何工作的:

import httpx


# as discussed in headers chapter we should always stick to browser-like headers for our 
# requests to prevent being blocked
headers = {
    # lets use Chrome browser on Windows:
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
}

# here is a list of urls, in this example we'll just use some place holders
urls = [
    "http://httbin.org/html", 
    "http://httbin.org/html",
    "http://httbin.org/html",
    "http://httbin.org/html",
    "http://httbin.org/html",
]
# since we have multiple urls we want to scrape we should establish a persistent session
session = httpx.Client(headers=headers)
for url in urls:
    response = session.get(url)
    html = response.text
    meta = response.headers
    print(html)
    print(meta)

我们做的第一件事是设置一些请求标头以防止被立即阻止。虽然 httpbin.org 没有阻止任何请求,但在网络抓取公共目标时
至少设置User-Agent和标头通常是一个好习惯。Accept

是什么httpx.Client
我们可以跳过它并httpx.get()改为调用每个 url:

for url in urls:
    response = httpx.get(url, headers=headers)
# vs
with httpx.Client(headers=headers) as session:
    response = session.get(url)

然而,HTTP 不是持久协议——这意味着每次我们调用时httpx.get()基本上都会启动一个新的独立连接,这是非常低效的。
为了优化这种交流,我们可以建立一个会话。这通常称为“连接池”或HTTP 持久连接

换句话说,一个会话只会建立一次连接并继续交换我们的请求,直到我们关闭它。会话客户端不仅使连接更高效,而且提供许多方便的功能,如全局标头设置、自动 cookie 管理等。

提示:检查网络流量

要完全了解网站如何用于网络抓取目的,我们可以使用网络浏览器开发工具套件。
开发人员工具的网络选项卡跟踪浏览器发出的每个网络请求。这有助于理解如何抓取网站,尤其是在处理 POST 类型的请求时。

解析 HTML 内容

HTML是一种支持网络的文本数据结构。HTML 结构的伟大之处在于它旨在成为机器可读的文本内容。这对于网络抓取来说是个好消息,因为我们可以像用眼睛解析数据一样轻松地使用代码解析数据!

HTML 是一种树型结构,易于解析。例如,让我们来看这个简单的 HTML 内容:

<head>
  <title>
  </title>
</head>
<body>
  <h1>Introduction</h1>
  <div>
    <p>some description text: </p>
    <a class="link" href="http://example.com">example link</a>
  </div>
</body>

在这里,我们看到了一个简单网站可能提供的基本 HTML 文档。你已经可以通过文本的缩进看到树状结构,但我们甚至可以进一步说明它:

这种 HTML 树结构非常适合网络抓取,因为我们可以使用一组简单的指令轻松浏览整个文档。

例如,要在此 HTML 中查找链接,我们可以看到它们位于body->div->anode where下class==link。这些规则通常通过两种标准方式来表达:CSS 选择器和 XPath——让我们来看看它们。

使用 CSS 和 XPATH 选择器

有两种 HTML 解析标准:

  • CSS 选择器 – 更简单、更简洁、功能更弱
  • XPATH 选择器——更复杂、更长、非常强大

通常,现代网站可以单独使用 CSS 选择器进行解析,但是,有时 HTML 结构可能非常复杂,拥有额外的 XPath 功能会使事情变得容易得多。我们将混合使用两者——我们将在可以退回到 XPath 的地方使用 CSS。

由于 Python 没有内置的 HTML 解析器,我们必须选择一个提供这种能力的库。在 Python 中,有几个选项,但两个最大的库是beautifulsoup (beautifulsoup4) 和parsel

parsel我们将在本章中使用 HTML 解析包,但由于 CSS 和 XPath 选择器实际上是解析 HTML 的标准方法,我们可以轻松地将相同的知识应用于 BeautifulSoup 库以及其他编程语言的其他 HTML 解析库。

让我们看一个快速示例,了解如何在 Python 中使用 Parsel 来使用 CSS 选择器和 XPath 解析 HTML:

# for this example we're using a simple website page
HTML = """
<head>
    <title>My Website</title>
</head>
<body>
    <div class="content">
        <h1>First blog post</h1>
        <p>Just started this blog!</p>
        <a href="http://github.com/scrapfly">Checkout My Github</a>
        <a href="http://twitter.com/scrapfly_dev">Checkout My Twitter</a>
    </div>
</body>
"""
from parsel import Selector

# first we must build parsable tree object from HTML text string
tree = Selector(HTML)
# once we have tree object we can start executing our selectors
# we can use ss selectors:
github_link = tree.css('.content a::attr(href)').get()
# we can also use xpath selectors:
twitter_link = tree.xpath('//a[contains(@href,"twitter.com")]/@href').get()
title = tree.css('title').get()
github_link = tree.css('.content a::attr(href)').get()
article_text = ''.join(tree.css('.content ::text').getall()).strip()
print(title)
print(github_link)
print(twitter_link)
print(article_text)
# will print:
# <title>My Website</title>
# http://github.com/scrapfly
# http://twitter.com/scrapfly_dev
# First blog post
# Just started this blog!
# Checkout My Github

在这个例子中,我们使用 parsel 包从 HTML 文本创建一个解析树。然后,我们使用这个解析树的 CSS 和 XPath 选择器函数来提取标题、Github 链接、Twitter 链接和文章的正文。

提示:使用浏览器的开发工具

当网络抓取特定目标时,我们可以使用网络浏览器的开发人员工具套件来快速可视化网站的 HTML 结构并构建我们的 CSS 和 XPath 选择器。请参阅此演示视频:

演示如何使用Chrome开发者工具查找HTML元素位置

示例项目

我们已经介绍了如何使用httpx客户端下载 HTML 文档以及如何使用 CSS 和 XPath 选择器使用 Parsel 解析 HTML 数据。现在让我们将所有这些放在一个示例项目中!

对于我们的真实项目,我们将抓取remotepython.com/jobs/,其中包含 Python 的远程工作列表。

我们将抓取网站上显示的所有职位列表,这涉及几个步骤:

  1. 检索第一页:remotepython.com/jobs/
  2. 从第一页解析结果。
  3. 查找其他页面的链接。
  4. 抓取并解析其他页面。

让我们从第一页抓取器开始

import httpx
import json
from parsel import Selector

# first we need to configure default headers to avoid being blocked.
DEFAULT_HEADERS = {
    # lets use Chrome browser on Windows:
    "User-Agent": "Mozilla/4.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=-1.9,image/webp,image/apng,*/*;q=0.8",
}
# then we should create a persistent HTTP client:
client = httpx.Client(headers=DEFAULT_HEADERS)

# to start, let's scrape first page
response_first = client.get("https://www.remotepython.com/jobs/")

# and create a function to parse job listings from a page - we'll use this for all pages
def parse_jobs(response: httpx.Response):
    selector = Selector(text=response.text)
    parsed = []
    # find all job boxes and iterate through them:
    for job in selector.css('.box-list .item'):
        # note that web pages use relative urls (e.g. /jobs/1234)
        # which we can convert to absolute urls (e.g. remotepython.com/jobs/1234 )
        relative_url = job.css('h3 a::attr(href)').get()
        absolute_url = response.url.join(relative_url)
        # rest of the data can be parsed using CSS or XPath selectors:
        parsed.append({
            "url": absolute_url,
            "title": job.css('h3 a::text').get(),
            "company": job.css('h5 .color-black::text').get(),
            "location": job.css('h5 .color-white-mute::text').get(),
            "date": job.css('div>.color-white-mute::text').get('').split(': ')[-1],
            "short_description": job.xpath('.//h5/following-sibling::p[1]/text()').get("").strip(),
        })
    return parsed

results = parse_jobs(response_first)
# print results as pretty json:
print(json.dumps(results, indent=2))

示例输出

[
  {
    "url": "https://www.remotepython.com/jobs/8173028f333140e1b6d74f70dc42a52a/",
    "title": "Back-End / Data / DevOps Engineer  ",
    "company": "Publisher Discovery",
    "location": "Bristol, UK, United Kingdom",
    "date": "Nov. 23, 2022",
    "short_description": "Publisher Discovery is hiring a remote Back-End & Data Engineer to help build, run and evolve the pipelines and platform that underpin our business insights technology.\r\n\r\nWe \u2026"
  },
  {
    "url": "https://www.remotepython.com/jobs/a63708cb43df422dbe76938c843ed1fb/",
    "title": "Lead Software Engineer (Python)  ",
    "company": "Hashtrust Technologies",
    "location": "gurgaon, India",
    "date": "Nov. 23, 2022",
    "short_description": "Job Description:\r\n\r\nHashtrust Technologies is looking for a Lead Software Engineer (Python) with system architecture experience to work with our clients, design solutions, develop\u2026"
  },
  {
    "url": "https://www.remotepython.com/jobs/de4dab9efc7b435b860cd3003a122c63/",
    "title": "Senior Back End Engineer  ",
    "company": "Cube Software",
    "location": "New York City, United States",
    "date": "Nov. 22, 2022",
    "short_description": "We're on a mission to help every company hit their numbers.\r\n\r\nThe world has evolved, but business planning has not. Most Finance teams still manage their planning and analysi\u2026"
  },
  ... etc
]

这个简短的抓取器抓取了结果的第一页,让我们进一步扩展它以收集剩余的页面:

import json
from parsel import Selector

# to scrape other pages we need to find their links and repeat the scrape process:
other_page_urls = Selector(text=response_first.text).css('.pagination a::attr(href)').getall()
for url in other_page_urls:
    # we need to turn relative urls (like ?page=2) to absolute urls (like http://remotepython.com/jobs?page=2)
    absolute_url = response_first.url.join(url)
    response = client.get(absolute_url)
    results.extend(parse_jobs(response))
print(json.dumps(results, indent=2))

在上面,我们提取剩余的页面 URL 并以与抓取第一页相同的方式抓取它们。

我们的简短示例项目到此结束,但我们给您留下了额外的挑战——如何抓取详细的职位列表数据?

常见的数据采集挑战

让我们来看看一些流行的网络抓取挑战以及解决这些挑战的方法。

动态内容

一些网站需要 javascript,这似乎很难用 Python 抓取。有几种方法可以处理动态数据抓取。

对于初学者,我们可以使用真正的网络浏览器通过Selenium、Playwright或Puppeteer等库为我们呈现动态页面。一些动态数据可能存在于 HTML 中,只是隐藏在一个 javascript 对象中,这被称为隐藏的网络数据抓取。

网络爬虫缩放

在线数据很多,虽然抓取少量页面很容易,但将其扩展到成千上万的 HTTP 请求和文档​​会很快带来很多挑战,从 Web 抓取器阻塞到处理多个并发连接。

对于更大的 scrapers,我们强烈建议利用 Python 的异步生态系统。由于 HTTP 连接涉及大量等待,异步编程允许我们同时调度和处理多个连接。例如在 httpx 中我们可以同时管理同步和异步连接:

import httpx
import asyncio
from time import time

urls_20 = [f"http://httpbin.org/links/20/{i}" for i in range(20)]

def scrape_sync():
    _start = time()
    with httpx.Client() as session:
        for url in urls_20:
            session.get(url)
    return time() - _start

async def scrape_async():
    _start = time()
    async with httpx.AsyncClient() as session:
        await asyncio.gather(*[session.get(url) for url in urls_20])
    return time() - _start

if __name__ == "__main__":
    print(f"sync code finished in: {scrape_sync():.2f} seconds")
    print(f"async code finished in: {asyncio.run(scrape_async()):.2f} seconds")

在这里,我们有两个函数可以抓取 20 个 url。一种是同步的,另一种是利用 asyncio 的并发性。如果我们运行它们,我们可以看到巨大的速度差异:

在哪里寻求帮助?

幸运的是,使用 Python 进行网络数据抓取的社区非常庞大,通常可以帮助解决这些问题。我们最喜欢的帮助资源是:

常问问题

我们在本文中介绍了很多内容,但网络抓取是一个庞大的主题,我们无法将所有内容都放在一篇文章中。但是,我们可以回答人们关于 Python 中的网络抓取的一些常见问题:

Python 适合网络抓取吗?

用 Python 构建网络抓取工具非常简单!毫不奇怪,它是迄今为止网络抓取中使用最流行的语言。
Python 是一种简单但功能强大的语言,在数据解析和 HTTP 连接领域拥有丰富的生态系统。由于网络抓取缩放主要是基于 IO 的(等待连接完成占用了大部分程序的运行时间),Python 的性能非常好,因为它本身支持异步代码范例!因此,用于网络抓取的 Python 速度快、易于访问并且拥有庞大的社区。

Python 最好的 HTTP 客户端库是什么?

目前,我们认为网络抓取的最佳选择是httpx库,因为它支持同步和异步 python,并且易于配置以避免网络抓取器阻塞。或者,requests库是初学者的不错选择,因为它具有最简单的 API。

如何加速 python 网页抓取?

在 Python 中加速 Web 抓取的最简单方法是使用异步 HTTP 客户端,例如httpx,并为所有 HTTP 连接相关代码使用异步函数(协程)。

如何防止python网页抓取阻塞?

使用 Python 抓取网站时最常见的挑战之一是阻塞。发生这种情况是因为与 Web 浏览器相比,抓取工具的行为本质上有所不同,因此可以检测到并阻止它们。
目标是确保来自 python 网络抓取器的 HTTP 连接看起来类似于 Chrome 或 Firefox 等网络浏览器的连接。这涉及所有连接方面:使用 http2 而不是 http1.1,使用与 Web 浏览器相同的标头,以与浏览器相同的方式处理 cookie 等。

为什么我的抓取工具看不到我浏览器的数据?

当我们使用请求、httpx 等 HTTP 客户端时,我们只抓取原始页面源,这通常看起来与浏览器中的页面源不同。这是因为浏览器运行页面中存在的所有可以更改它的 javascript。我们的 python 抓取器没有 javascript 功能,因此我们要么需要对 javascript 代码进行逆向工程,要么需要控制 Web 浏览器实例。查看我们的了解更多。

网络抓取工具开发中使用的最佳工具是什么?

有很多很棒的工具,但是当谈到 Python 中最好的网络抓取工具时,最重要的工具必须是网络浏览器开发人员工具。可以在大多数 Web 浏览器(Chrome、Firefox、Safari,通过 F12 键或右键单击“检查元素”)中访问这套工具。
该工具集对于了解网站的工作方式至关重要。它允许我们检查 HTML 树、测试我们的 xpath/css 选择器以及跟踪网络活动——所有这些都是开发网络抓取工具的绝佳工具。

我们建议通过阅读官方文档页面来熟悉这些工具。

概括

在此 Python 网络抓取教程中,我们涵盖了开始使用 Python 进行网络抓取所需了解的所有内容。

我们已经介绍了 HTTP 协议,它是所有互联网连接的支柱。我们探讨了 GET 和 POST 请求,以及请求标头对于避免阻塞的重要性。

然后,我们了解了在 Python 中解析 HTML:如何使用 CSS 和 XPath 选择器将数据从原始 HTML 内容解析为清晰的数据集。

最后,我们通过一个示例项目巩固了这些知识,我们在该示例项目中抓取了 remotepython.org 上显示的职位列表。我们使用 Chrome 开发人员工具来检查网站的结构以构建我们的 CSS 选择器并抓取工作结果的每一页。

这个网络抓取教程应该让您走上正确的道路,但这只是网络抓取的冰山一角!

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。