Skip to content

Latest commit

 

History

History
1029 lines (687 loc) · 46.1 KB

File metadata and controls

1029 lines (687 loc) · 46.1 KB

三、构建第一个 Web 抓取应用

在本章中,我们将介绍以下配方:

  • 下载网页
  • 解析 HTML
  • 在网络上爬行
  • 订阅提要
  • 访问 web API
  • 与表单交互
  • 使用 Selenium 进行高级交互
  • 访问受密码保护的页面
  • 加速网页抓取

介绍

互联网和WWW万维网)可能是当今最重要的信息来源。大部分信息都可以通过 HTTP 协议检索。HTTP最初是为了共享超文本页面而发明的(因此被命名为超文本传输协议,它启动了 WWW。

这个操作非常熟悉,因为它是在任何 web 浏览器中发生的。但我们也可以通过编程方式执行这些操作,以自动检索和处理信息。Python 在标准库中包含了一个 HTTP 客户机,但是奇妙的requests模块使它变得非常简单。在本章中,我们将了解如何。

下载网页

下载网页的基本功能包括对 URL 发出 HTTPGET请求。这是任何 web 浏览器的基本操作。让我们快速回顾一下此操作的不同部分:

  1. 使用 HTTP 协议。
  2. 使用GET方法,这是最常见的 HTTP 方法。我们将在访问 web API配方中看到更多内容。
  3. 描述页面完整地址的 URL,包括服务器和路径。

该请求将由服务器处理,并返回响应。此响应将包含一个状态代码,如果一切正常,通常为 200,以及一个带有结果的正文,通常是带有 HTML 页面的文本。

其中大部分由用于执行请求的 HTTP 客户端自动处理。我们将在这个食谱中看到如何发出一个简单的请求来获得一个网页。

HTTP requests and responses can also contain headers. Headers contain extra information, such as the total size of the request, the format of the content, the date of the request, and what browser or server is used. 

准备

使用神奇的requests模块,获取网页非常简单。安装模块:

$ echo "requests==2.18.3" >> requirements.txt
$ source .venv/bin/activate
(.venv) $ pip install -r requirements.txt 

我们将在下载该页面 http://www.columbia.edu/~fdc/sample.html因为它是一个简单的 html 页面,在文本模式下易于阅读。

怎么做。。。

  1. 导入requests模块:
>>> import requests
  1. 向 URL 发出请求,这需要一两秒钟:
>>> url = 'http://www.columbia.edu/~fdc/sample.html'
>>> response = requests.get(url)
  1. 检查返回的对象状态代码:
>>> response.status_code
200
  1. 检查结果的内容:
>>> response.text
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\n<html>\n<head>\n
...
FULL BODY
...
<!-- close the <html> begun above -->\n'
  1. 检查正在进行和返回的标题:
>>> response.request.headers
{'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
>>> response.headers
{'Date': 'Fri, 25 May 2018 21:51:47 GMT', 'Server': 'Apache', 'Last-Modified': 'Thu, 22 Apr 2004 15:52:25 GMT', 'Accept-Ranges': 'bytes', 'Vary': 'Accept-Encoding,User-Agent', 'Content-Encoding': 'gzip', 'Content-Length': '8664', 'Keep-Alive': 'timeout=15, max=85', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html', 'Set-Cookie': 'BIGipServer~CUIT~www.columbia.edu-80-pool=1764244352.20480.0000; expires=Sat, 26-May-2018 03:51:47 GMT; path=/; Httponly'}

它是如何工作的。。。

requests的操作非常简单;在本例中,通过 URL 执行操作GET。这将返回一个可以分析的result对象。主要元素为status_code和主体内容,可表示为text

可在request字段中检查完整请求:

>>> response.request
<PreparedRequest [GET]>
>>> response.request.url
'http://www.columbia.edu/~fdc/sample.html'

完整的申请文件可在此处找到:http://docs.python-requests.org/en/master/ 。在本章中,我们将展示更多功能。

还有更多。。。

可在此网页上查看所有 HTTP 状态代码:https://httpstatuses.com/ 。在httplib模块中也会使用方便的常量名称来描述它们,例如OKNOT_FOUNDFORBIDDEN

The most famous error status code is arguably 404, which happens when a URL is not found. Try it out by doing requests.get('http://www.columbia.edu/invalid').

请求可以使用HTTPS协议(安全 HTTP。它是等效的,但确保请求和响应的内容是私有的。requests处理透明。

Any website that handles any private information will use HTTPS to ensure that the information has not leaked out. HTTP is vulnerable to someone eavesdropping. Use HTTPS where available.

另见

  • 第一章中的安装第三方软件包配方让我们开始我们的自动化之旅
  • 解析 HTML配方

解析 HTML

下载原始文本或二进制文件是一个很好的起点,但 web 的主要语言是 HTML。

HTML 是一种结构化语言,定义文档的不同部分,如标题和段落。HTML 也是分层的,定义子元素。将原始文本解析为结构化文档的能力基本上是能够从网页中自动提取信息。例如,如果某些文本包含在特定的class div或标题h3标记之后,则这些文本可能是相关的。

准备

我们将使用优秀的 Beauty Soup 模块将 HTML 文本解析为可以分析的内存对象。我们需要使用beautifulsoup4包来使用可用的最新 Python 3 版本。将包添加到您的requirements.txt并在虚拟环境中安装依赖项:

$ echo "beautifulsoup4==4.6.0" >> requirements.txt
$ pip install -r requirements.txt

怎么做。。。

  1. 进口BeautifulSouprequests
>>> import requests
>>> from bs4 import BeautifulSoup
  1. 设置要下载和检索的页面 URL:
>>> URL = 'http://www.columbia.edu/~fdc/sample.html'
>>> response = requests.get(URL)
>>> response
<Response [200]>
  1. 解析下载的页面:
>>> page = BeautifulSoup(response.text, 'html.parser')
  1. 获取页面的标题。请确保它与浏览器中显示的内容相同:
>>> page.title
<title>Sample Web Page</title>
>>> page.title.string
'Sample Web Page'
  1. 查找页面中的所有h3元素,以确定现有部分:
>>> page.find_all('h3')
[<h3><a name="contents">CONTENTS</a></h3>, <h3><a name="basics">1\. Creating a Web Page</a></h3>, <h3><a name="syntax">2\. HTML Syntax</a></h3>, <h3><a name="chars">3\. Special Characters</a></h3>, <h3><a name="convert">4\. Converting Plain Text to HTML</a></h3>, <h3><a name="effects">5\. Effects</a></h3>, <h3><a name="lists">6\. Lists</a></h3>, <h3><a name="links">7\. Links</a></h3>, <h3><a name="tables">8\. Tables</a></h3>, <h3><a name="install">9\. Installing Your Web Page on the Internet</a></h3>, <h3><a name="more">10\. Where to go from here</a></h3>]
  1. 提取节链接上的文本。到达下一个<h3>标签时停止:
>>> link_section = page.find('a', attrs={'name': 'links'})
>>> section = []
>>> for element in link_section.next_elements:
...     if element.name == 'h3':
...         break
...     section.append(element.string or '')
...
>>> result = ''.join(section)
>>> result
'7\. Links\n\nLinks can be internal within a Web page (like to\nthe Table of ContentsTable of Contents at the top), or they\ncan be to external web pages or pictures on the same website, or they\ncan be to websites, pages, or pictures anywhere else in the world.\n\n\n\nHere is a link to the Kermit\nProject home pageKermit\nProject home page.\n\n\n\nHere is a link to Section 5Section 5 of this document.\n\n\n\nHere is a link to\nSection 4.0Section 4.0\nof the C-Kermit\nfor Unix Installation InstructionsC-Kermit\nfor Unix Installation Instructions.\n\n\n\nHere is a link to a picture:\nCLICK HERECLICK HERE to see it.\n\n\n'

请注意,没有 HTML 标记;都是原始文本。

它是如何工作的。。。

第一步是下载页面。然后,可以解析原始文本,如步骤 3 所示。生成的page对象包含解析的信息。

The html.parser parser is the default one, but for specific operations it can have problems. For example, for big pages it can be slow, or has issue rendering highly dynamic web pages. You can use other parsers, such as,  lxml, which is much faster, or html5lib, which will be closer to how a browser operates, including dynamic changes produced by HTML5. They are external modules that will need to be added to the requirements.txt file.

BeautifulSoup允许我们搜索 HTML 元素。可以使用.find()搜索第一个,也可以返回一个带有.find_all()的列表。在步骤 5 中,它搜索具有特定属性name=link的特定标记<a>,之后,它继续在.next_elements上迭代,直到找到下一个h3标记,该标记标志着节的结束。

提取每个元素的文本,最后将其合成为单个文本。注意当元素没有文本时返回的避免存储Noneor

HTML is highly versatile, and can have multiple structures. The case presented in this recipe is typical, but other options on dividing sections can be grouping related sections inside a big <div> tag or other elements, or even raw text. Some experimentation will be required until you find the specific process to extract the juicy bits on a web page. Don't be afraid to try!

还有更多。。。

可以使用正则表达式,也可以在.find().find_all()方法中输入正则表达式。例如,此搜索使用h2h3标记:

>>> page.find_all(re.compile('^h(2|3)'))
[<h2>Sample Web Page</h2>, <h3><a name="contents">CONTENTS</a></h3>, <h3><a name="basics">1\. Creating a Web Page</a></h3>, <h3><a name="syntax">2\. HTML Syntax</a></h3>, <h3><a name="chars">3\. Special Characters</a></h3>, <h3><a name="convert">4\. Converting Plain Text to HTML</a></h3>, <h3><a name="effects">5\. Effects</a></h3>, <h3><a name="lists">6\. Lists</a></h3>, <h3><a name="links">7\. Links</a></h3>,
<h3><a name="tables">8\. Tables</a></h3>, <h3><a name="install">9\. Installing Your Web Page on the Internet</a></h3>, <h3><a name="more">10\. Where to go from here</a></h3>]

另一个有用的 find 参数是使用class_参数包含 CSS 类。这将在本书的后面部分显示。

完整的靓汤文档可在此处找到:https://www.crummy.com/software/BeautifulSoup/bs4/doc/

另见

  • 第一章中的安装第三方软件包配方让我们开始我们的自动化之旅
  • 第 1 章引入正则表达式的配方,让我们开始我们的自动化之旅
  • 下载网页配方

在网络上爬行

考虑到超链接页面的性质,从一个已知的位置开始,然后链接到其他页面,这是一个非常重要的工具。

为此,我们抓取一个页面,寻找一个小短语,然后打印包含它的任何段落。我们将只搜索属于同一网站的网页。也就是说,只有以 www.somesite.com 开头的 URL。我们不会跟踪外部站点的链接。

准备

这个菜谱建立在介绍的概念之上,因此它将下载并解析页面以搜索链接并继续下载。

When crawling the web, remember to set limits when downloading. It's very easy to crawl over too many pages. As anyone checking Wikipedia can confirm, the internet is potentially limitless.

我们将使用准备好的示例作为示例,可在 GitHub 回购协议中找到:https://github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter03/test_site 。下载整个站点并运行包含的脚本。

$ python simple_delay_server.py

这为 URLhttp://localhost:8000中的站点提供服务。你可以在浏览器上查看它。这是一个简单的博客,有三个条目。大部分内容都很枯燥,但我们添加了几个包含关键字python的段落

怎么做。。。

  1. 完整的脚本crawling_web_step1.py可在 GitHub 的以下链接中获得:https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter03/crawling_web_step1.py 。此处显示了最相关的位:
...

def process_link(source_link, text):
    logging.info(f'Extracting links from {source_link}')
    parsed_source = urlparse(source_link)
    result = requests.get(source_link)
    # Error handling. See GitHub for details
    ...
    page = BeautifulSoup(result.text, 'html.parser')
    search_text(source_link, page, text)
    return get_links(parsed_source, page)

def get_links(parsed_source, page):
    '''Retrieve the links on the page'''
    links = []
    for element in page.find_all('a'):
        link = element.get('href')
        # Validate is a valid link. See GitHub for details
        ...
        links.append(link)
    return links
  1. 搜索对python的引用,以返回包含该引用和段落的 URL 的列表。请注意,由于链接断开,出现了几个错误:
$ python crawling_web_step1.py https://localhost:8000/ -p python
Link http://localhost:8000/: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/files/5eabef23f63024c20389c34b94dee593-1.html: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/files/33714fc865e02aeda2dabb9a42a787b2-0.html: --> This is the actual bit with a python reference that we are interested in.
Link http://localhost:8000/files/archive-september-2018.html: --> A smaller article , that contains a reference to Python
Link http://localhost:8000/index.html: --> A smaller article , that contains a reference to Python
  1. 另一个好的搜索词是crocodile。试一试:
$ python crawling_web_step1.py http://localhost:8000/ -p crocodile

它是如何工作的。。。

让我们看看脚本的每个组件:

  1. main函数中,通过所有找到的链接的循环:

请注意,检索限制为 10 页,并检查是否已添加任何要添加的新链接。

Note these two things are limits. We won't download the same link twice and we'll stop at some point.

  1. 下载并解析链接,在process_link功能中:

它下载文件,并检查状态是否正确,以跳过错误,例如断开的链接。它还检查类型(如Content-Type中所述)是否为 HTML 页面,以跳过 PDF 和其他格式。最后,它将原始 HTML 解析为一个BeautifulSoup对象。

它还使用urlparse解析源链接,因此稍后在步骤 4 中,它可以跳过对外部源的所有引用。urlparse将 URL 划分为其组成元素:

>>> from urllib.parse import urlparse
>>> >>> urlparse('http://localhost:8000/files/b93bec5d9681df87e6e8d5703ed7cd81-2.html')
ParseResult(scheme='http', netloc='localhost:8000', path='/files/b93bec5d9681df87e6e8d5703ed7cd81-2.html', params='', query='', fragment='')
  1. search_text功能中查找要搜索的文本:

它在已解析的对象中搜索指定的文本。注意,搜索是作为regex完成的,并且仅在文本中进行。它打印结果匹配,包括source_link,引用找到匹配的 URL:

for element in page.find_all(text=re.compile(text)):
    print(f'Link {source_link}: --> {element}')
  1. **get_links**功能检索页面上的所有链接:

它在已解析的页面中搜索所有<a>元素,并检索href元素,但仅检索具有此类href元素且为完全限定 URL(以http开头)的元素。这将删除非 URL 的链接,如'#'链接或页面内部的链接。

进行额外检查以检查它们是否与原始链接具有相同的源,然后将它们注册为有效链接。netloc属性允许检测链接是否来自与步骤 2 中生成的解析 URL 相同的 URL 域。

We won't follow links that point to a different address (for example, a http://www.google.com one).

最后,返回链接,并将它们添加到步骤 1 中描述的循环中。

还有更多。。。

可以实施进一步的筛选,例如,丢弃以.pdf结尾的所有链接,这意味着它们是 PDF 文件:

# In get_links
if link.endswith('pdf'):
  continue

Content-Type的使用也可以确定以不同的方式解析返回的对象。PDF 结果(Content-Type: application/pdf没有要解析的有效response.text对象,但可以用其他方式解析。这同样适用于其他类型,例如 CSV 文件(Content-Type: text/csv)或可能需要解压缩的 ZIP 文件(Content-Type: application/zip)。稍后我们将看到如何处理这些问题。

另见

  • 下载网页配方
  • 解析 HTML配方

订阅提要

RSS 可能是互联网上最大的秘密。虽然它的辉煌时刻似乎发生在 21 世纪,但现在它不再成为人们关注的焦点,它允许用户轻松订阅网站。它在很多地方都有,而且非常有用。

RSS 的核心是一种呈现一系列有序引用(通常是文章,但也有其他元素,如播客插曲或 YouTube 出版物)和发布时间的方式。这是一种非常自然的方式,可以了解自上次检查以来哪些文章是新的,以及呈现有关这些文章的一些结构化数据,例如标题和摘要。

在本食谱中,我们将介绍feedparser模块,并确定如何从 RSS 提要获取数据。

RSS is not the only available feed format. There's also a format called Atom, but both are very much equivalent. feedparser is also capable of parsing it, so both can be used indistinctly.

准备

我们需要将feedparser依赖项添加到requirements.txt文件并重新安装:

$ echo "feedparser==5.2.1" >> requirements.txt
$ pip install -r requirements.txt

提要 URL 可以在几乎所有涉及出版物的页面上找到,包括博客、新闻、播客等。有时它们很容易找到,但有时它们有点隐蔽。通过feedRSS进行搜索。

大多数报纸和通讯社都有按主题划分的 RSS 提要。我们将以解析纽约时报主页提要为例 http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml 。在主提要页面中有更多提要:https://archive.nytimes.com/www.nytimes.com/services/xml/rss/index.html

Please note the feeds may be subjected to terms and conditions of use. In the New York Times case, they are described at the end of the main feed page.

请注意,此提要经常更改,这意味着链接条目将根据本书中的示例进行更改。

怎么做。。。

  1. 导入feedparser模块,同时导入datetimedeloreanrequests
import feedparser
import datetime
import delorean
import requests
  1. 解析提要(它将自动下载)并检查上次更新的时间。提要信息,如提要的标题,可在feed属性中获取:
>>> rss = feedparser.parse('http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml')
>>> rss.updated
'Sat, 02 Jun 2018 19:50:35 GMT'
  1. 获取比六小时新的条目:
>>> time_limit = delorean.parse(rss.updated) - datetime.timedelta(hours=6)
>>> entries = [entry for entry in rss.entries if delorean.parse(entry.published) > time_limit]
  1. 由于某些返回的条目的时间将超过六个小时,因此条目数将少于总条目数:
>>> len(entries)
10
>>> len(rss.entries)
44
  1. 检索有关条目的信息,例如title。完整的输入 URL 可用作link。浏览此特定提要中的可用信息:
>>> entries[5]['title']
'Loose Ends: How to Live to 108'
>>> entries[5]['link']
'https://www.nytimes.com/2018/06/02/opinion/sunday/how-to-live-to-108.html?partner=rss&emc=rss'
>>> requests.get(entries[5].link)
<Response [200]>

它是如何工作的。。。

解析后的feed对象包含条目信息,以及提要本身的一般信息,例如更新的时间。feed信息可以在feed属性中找到:

>>> rss.feed.title
'NYT > Home Page'

每个条目都作为一个字典,因此字段很容易检索。它们也可以作为属性访问,但将它们视为键可以让我们获得所有可用字段:

>>> entries[5].keys()
dict_keys(['title', 'title_detail', 'links', 'link', 'id', 'guidislink', 'media_content', 'summary', 'summary_detail', 'media_credit', 'credit', 'content', 'authors', 'author', 'author_detail', 'published', 'published_parsed', 'tags'])

处理提要时的基本策略是解析它们并遍历条目,快速检查它们是否有趣,例如,通过检查描述摘要。如果是,请使用link字段下载整个页面。然后,为了避免重新检查实体,请存储最新的发布日期,下次只检查较新的条目。

还有更多。。。

完整的feedparser文档可在此处找到:https://pythonhosted.org/feedparser/

每个提要的可用信息可能不同。在《纽约时报》的例子中,有一个带有标记信息的tag字段,但这不是标准字段。作为最低要求,条目将包含标题、说明和链接。

RSS feeds are also a great way of curating your own selection of news sources. There are great feed readers for that.

另见

  • 第一章中的安装第三方软件包配方让我们开始我们的自动化之旅
  • 下载网页配方

访问 web API

可以通过 web 创建丰富的接口,允许通过 HTTP 进行强大的交互。最常见的接口是通过使用 JSON 的 RESTful API。这些基于文本的接口易于理解和编程,并且使用了与语言无关的通用技术,这意味着它们可以在任何具有 HTTPclient模块的编程语言中访问,当然包括 Python。

Formats other than JSON are used, such as XML, but JSON is a very simple and readable format that translates very well into Python dictionaries (and other language equivalents). JSON is, by far, the most common format in RESTful APIs at the moment. Learn more about JSON here: https://www.json.org/.

RESTful 的严格定义需要一些特性,但更非正式的定义可能是通过 URL 访问资源。这意味着 URL 代表特定的资源,例如报纸上的文章或房地产网站上的财产。然后可以通过 HTTP 方法操作资源(GET查看,POST创建,PUT/PATCH编辑,DELETE删除)来操作资源。

Proper RESTful interfaces need to have certain characteristics, and are a way of creating interfaces that is not strictly restricted to HTTP interfaces. You can read more about it here: https://codewords.recurse.com/issues/five/what-restful-actually-means.

使用requests非常容易,因为它包含本机 JSON 支持。

准备

为了演示如何操作 RESTful API,我们将使用示例站点https://jsonplaceholder.typicode.com/ 。它使用帖子、评论和其他公共资源模拟常见案例。我们将使用帖子和评论。要使用的 URL 如下所示:

# The collection of all posts
/posts
# A single post. X is the ID of the post
/posts/X
# The comments of post X
/posts/X/comments

网站会向每个人返回适当的结果。非常方便!

Because it is a test site, data won't be created, but the site will return all the correct responses.

怎么做。。。

  1. 进口requests
>>> import requests
  1. 获取所有帖子的列表并显示最新帖子:
>>> result = requests.get('https://jsonplaceholder.typicode.com/posts')
>>> result
<Response [200]>
>>> result.json()
# List of 100 posts NOT DISPLAYED HERE
>>> result.json()[-1]
{'userId': 10, 'id': 100, 'title': 'at nam consequatur ea labore ea harum', 'body': 'cupiditate quo est a modi nesciunt soluta\nipsa voluptas error itaque dicta in\nautem qui minus magnam et distinctio eum\naccusamus ratione error aut'}
  1. 创建一个新帖子。查看新创建的资源的 URL。该调用还返回以下资源:
>>> new_post = {'userId': 10, 'title': 'a title', 'body': 'something something'}
>>> result = requests.post('https://jsonplaceholder.typicode.com/posts',
              json=new_post)
>>> result
<Response [201]>
>>> result.json()
{'userId': 10, 'title': 'a title', 'body': 'something something', 'id': 101}
>>> result.headers['Location']
'http://jsonplaceholder.typicode.com/posts/101'

注意,创建资源的POST请求返回 201,这是 created 的正确状态。

  1. 使用GET获取现有帖子:
>>> result = requests.get('https://jsonplaceholder.typicode.com/posts/2')
>>> result
<Response [200]>
>>> result.json()
{'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla'}
  1. 使用PATCH更新其值。检查返回的资源:
>>> update = {'body': 'new body'}
>>> result = requests.patch('https://jsonplaceholder.typicode.com/posts/2', json=update)
>>> result
<Response [200]>
>>> result.json()
{'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'new body'}

它是如何工作的。。。

通常会访问两种资源。单个资源(https://jsonplaceholder.typicode.com/posts/X)和集合(https://jsonplaceholder.typicode.com/posts

  • 集合接受GET以检索所有集合,并接受POST以创建新资源
  • 单个元素接受GET获取元素,PUTPATCH进行编辑,DELETE进行删除

所有可用的 HTTP 方法都可以在requests中调用。在之前的配方中,我们使用了.get(),但有.post().patch().put().delete()可供选择。

返回的响应对象有一个解码 JSON 结果的.json()方法

同样,要发送信息,可以使用一个json参数。这将字典编码为 JSON 并将其发送到服务器。数据需要遵循资源的格式,否则可能会引发错误。

GET and DELETE don't require data, while PATCH, PUT, and POST do require data.

将返回引用的资源,其 URL 在标题位置可用。这在创建新资源时非常有用,因为它的 URL 事先不知道。

The difference between PATCH and PUT is that the latter replaces the whole resource, while the first does a partial update.

还有更多。。。

RESTful API 非常强大,但也具有巨大的可变性。请查看特定 API 的文档以了解其详细信息。

另见

  • 下载网页配方
  • 第一章中的安装第三方软件包配方让我们开始我们的自动化之旅

与表单交互

网页中的一个常见元素是表单。表单是将值发送到网页的一种方式,例如,在博客帖子上创建新评论,或提交购买。

浏览器显示表单,这样您就可以输入值,并在按下 submit 或等效按钮后在单个操作中发送它们。我们将在这个菜谱中看到如何通过编程创建这个动作。

Be aware that sending data to a site is normally more sensible than receiving data from it. For example, sending automatic comments to a website is very much the definition of spam. This means that it can be more difficult to automate and include security measures. Double-check that what you're trying to achieve is a valid, ethical use case.

准备

我们将针对测试服务器工作 https://httpbin.org/forms/post ,允许我们发送测试表并发回提交的信息。

以下是订购比萨饼的示例表单:

您可以手动填写表单,并看到它以 JSON 格式返回信息,包括额外的信息,如浏览器使用情况。

以下是生成的 web 表单的前端:

下图是生成的 web 表单的后端:

我们需要分析 HTML 以查看表单的可接受数据。检查源代码,它显示以下内容:

Source code

检查输入的名称,custnamecusttelcustemailsize(单选选项)、topping(多选复选框)、delivery(时间)和comments

怎么做。。。

  1. 导入requestsBeautifulSoupre模块:
>>> import requests
>>> from bs4 import BeautifulSoup
>>> import re
  1. 检索表单页面,对其进行解析,然后打印输入字段。检查发帖 URL 是否为/post(非/forms/post
>>> response = requests.get('https://httpbin.org/forms/post')
>>> page = BeautifulSoup(response.text)
>>> form = soup.find('form')
>>> {field.get('name') for field in form.find_all(re.compile('input|textarea'))}
{'delivery', 'topping', 'size', 'custemail', 'comments', 'custtel', 'custname'}

注意textarea是一个有效的输入,并且以 HTML 格式定义

  1. 准备要作为字典发布的数据。检查值是否与表格中定义的值相同:
>>> data = {'custname': "Sean O'Connell", 'custtel': '123-456-789', 'custemail': '[email protected]', 'size': 'small', 'topping': ['bacon', 'onion'], 'delivery': '20:30', 'comments': ''}
  1. 发布值并检查响应是否与浏览器中返回的相同:
>>> response = requests.post('https://httpbin.org/post', data)
>>> response
<Response [200]>
>>> response.json()
{'args': {}, 'data': '', 'files': {}, 'form': {'comments': '', 'custemail': '[email protected]', 'custname': "Sean O'Connell", 'custtel': '123-456-789', 'delivery': '20:30', 'size': 'small', 'topping': ['bacon', 'onion']}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Content-Length':
'140', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.18.3'}, 'json': None, 'origin': '89.100.17.159', 'url': 'https://httpbin.org/post'}

它是如何工作的。。。

requests直接接受以正确的方式发送数据。默认情况下,以application/x-www-form-urlencoded格式发送POST数据。

Compare that with the Accessing web APIs recipe, where the data is explicitly sent in JSON format using the argument json. This makes the Content-Type be application/json instead of application/x-www-form-urlencoded.

这里的关键是要考虑表单的格式,以及如果不正确可能返回错误的可能值,通常是 400 错误。

还有更多。。。

除了遵循表单格式和输入有效值之外,使用表单时的主要问题是防止垃圾邮件和滥用行为的多种方法。

一个非常常见的限制是确保您在提交表单之前下载表单,以避免提交多个表单或跨站点请求伪造CSRF)。

CSRF, which means producing a malicious call from a page to another taking advantage that  your browser is authenticated, is a serious problem. For example, entering in a puppies site that take advantage of you being logged into your bank page to perform operations "on your behalf". Here is a good description of it: https://stackoverflow.com/a/33829607. New techniques in browsers help with these issues by default.

要获取特定令牌,您需要首先下载表单,如配方中所示,获取 CSRF 令牌的值,然后重新提交。注意,令牌可以有不同的名称;这只是一个例子:

>>> form.find(attrs={'name': 'token'}).get('value')
'ABCEDF12345'

另见

  • 下载网页配方
  • 解析 HTML配方

使用 Selenium 进行高级交互

有时候,只有真正的东西才会起作用。Selenium 是一个在 web 浏览器中实现自动化的项目。它被认为是一种自动测试的方式,但也可以用于自动化与站点的交互。

Selenium 可以控制 Safari、Chrome、Firefox、Internet Explorer 或 Microsoft Edge,但它需要为每种情况安装特定的驱动程序。我们将使用 Chrome

准备

我们需要为 Chrome 安装正确的驱动程序,名为chromedriver。可在此处找到:https://sites.google.com/a/chromium.org/chromedriver/ 。它适用于大多数平台。它还要求您安装了 Chrome:https://www.google.com/chrome/

selenium模块添加到requirements.txt并安装:

$ echo "selenium==3.12.0" >> requirements.txt
$ pip install -r requirements.txt

怎么做。。。

  1. 导入 Selenium,启动浏览器,然后加载表单页面。将打开一个页面,反映以下操作:
>>> from selenium import webdriver
>>> browser = webdriver.Chrome()
>>> browser.get('https://httpbin.org/forms/post')

Note the banner with Chrome is being controlled by automated test software.

  1. 在“客户名称”字段中添加值。记住,它被称为custname
>>> custname = browser.find_element_by_name("custname")
>>> custname.clear()
>>> custname.send_keys("Sean O'Connell")

表格将更新:

  1. 选择比萨饼尺寸为medium
>>> for size_element in browser.find_elements_by_name("size"):
...     if size_element.get_attribute('value') == 'medium':
...         size_element.click()
...
>>>

这将更改比萨饼大小比例框。

  1. 增加baconcheese
>>> for topping in browser.find_elements_by_name('topping'):
...     if topping.get_attribute('value') in ['bacon', 'cheese']:
...         topping.click()
...
>>>

最后,复选框将显示为标记:

  1. 提交表格。页面将提交,结果将显示:
>>> browser.find_element_by_tag_name('form').submit()

将提交表单,并显示来自服务器的结果:

  1. 关闭浏览器:
>>> browser.quit()

它是如何工作的。。。

*如何操作…*部分中的步骤 1 展示了如何创建 Selenium 页面并转到特定 URL。

硒的作用方式与靓汤相似。选择适当的元素,然后对其进行操作。Selenium 中的选择器的工作方式与 Beautiful Soup 中的选择器类似,最常见的选择器为find_element_by_idfind_element_by_class_namefind_element_by_namefind_element_by_tag_namefind_element_by_css_selector。还有等价的find_elements_by_X返回列表,而不是第一个找到的元素(find_elements_by_tag_namefind_elements_by_name等等)。这在检查元素是否存在时也很有用。如果没有元素,find_element将引发错误,find_elements将返回空列表。

元素上的数据可以通过 HTML 属性的.get_attribute().text获取(例如表单元素上的值)。

元素可以通过模拟发送按键输入文本进行操作,方法为.send_keys(),点击.click()或提交.submit(),如果他们接受。.submit()将在表单上搜索正确提交的内容,.click()将以鼠标点击的方式选择/取消选择。

最后,步骤 6 关闭浏览器。

还有更多。。。

以下是完整的 Selenium 文档:http://selenium-python.readthedocs.io/

对于每个元素,都可以提取额外的信息,例如.is_displayed().is_selected()。可使用.find_element_by_link_text().find_element_by_partial_link_text()搜索文本。

有时,打开浏览器会很不方便。另一种方法是在无头模式下启动浏览器,然后从那里对其进行操作,如下所示:

>>> from selenium.webdriver.chrome.options import Options
>>> chrome_options = Options()
>>> chrome_options.add_argument("--headless")
>>> browser = webdriver.Chrome(chrome_options=chrome_options)
>>> browser.get('https://httpbin.org/forms/post')

该页面将不会显示。但屏幕截图可以通过以下行保存:

>>> browser.save_screenshot('screenshot.png')

另见

  • 解析 HTML配方
  • 与表单交互

访问受密码保护的页面

有时网页不向公众开放,但以某种方式受到保护。最基本的方面是使用基本的 HTTP 身份验证,它几乎集成到每个 web 服务器中,是一个用户/密码模式。

准备

我们可以在中测试这种身份验证 https://httpbin.org

它有一个路径/basic-auth/{user}/{password},强制进行身份验证,并声明用户和密码。这对于理解身份验证是如何工作的非常方便。

怎么做。。。

  1. 进口requests
>>> import requests
  1. 使用错误的凭据向 URL 发出GET请求。请注意,我们将 URL 上的凭据设置为userpsswd
>>> requests.get('https://httpbin.org/basic-auth/user/psswd', 
                 auth=('user', 'psswd'))
<Response [200]>
  1. 使用错误的凭据返回 401 状态代码(未经授权):
>>> requests.get('https://httpbin.org/basic-auth/user/psswd', 
                 auth=('user', 'wrong'))
<Response [401]>
  1. 凭证也可以直接在 URL 中传递,在服务器前面用冒号和@符号分隔,如下所示:
>>> requests.get('https://user:[email protected]/basic-auth/user/psswd')
<Response [200]>
>>> requests.get('https://user:[email protected]/basic-auth/user/psswd')
<Response [401]>

它是如何工作的。。。

由于到处都支持 HTTP 基本身份验证,requests的支持非常简单。

*如何操作…*部分中的步骤 2 和 4 说明了如何提供正确的密码。步骤 3 显示了当密码错误时会发生什么。

Remember to always use HTTPS to ensure that the sending of the password is kept secret. If you use HTTP, the password will be sent in the open over the web.

还有更多。。。

将用户和密码添加到 URL 也适用于浏览器。尝试直接访问页面,以查看显示的询问用户名和密码的框:

User credentials page

使用包含用户和密码的 URLhttps://user:[email protected]/basic-auth/user/psswd时,不会出现对话框,并且会自动进行身份验证。

如果您需要访问多个页面,您可以在requests中创建一个会话,并设置认证参数,以避免到处输入:

>>> s = requests.Session()
>>> s.auth = ('user', 'psswd')
>>> s.get('https://httpbin.org/basic-auth/user/psswd')
<Response [200]>

另见

  • 下载网页配方
  • 访问 Web API配方

加速网页抓取

从网页下载信息的大部分时间通常都花在等待上。一个请求从我们的计算机发送到将处理它的任何服务器,在响应被合成并返回到我们的计算机之前,我们不能对此做太多。

在书中的食谱执行过程中,你会注意到在requests调用中有一段等待时间,通常大约一到两秒钟。但是计算机可以在等待时做其他事情,包括同时发出更多的请求。在本食谱中,我们将看到如何并行下载页面列表,并等待它们全部就绪。我们将使用一个故意较慢的服务器来说明这一点。

准备

我们将获得爬网和搜索关键字的代码,利用 Python3 的futures功能同时下载多个页面。

future是代表价值承诺的对象。这意味着您在后台执行代码时立即收到一个对象。只有在特别请求其.result()时,代码才会被阻塞,直到得到它为止。

要生成一个future,您需要一个名为执行器的后台引擎。创建后,submit将向其发送函数和参数,以检索future。结果的检索可以根据需要延迟多长时间,允许一行生成几个futures,并等待所有的futures都完成,并行执行,而不是创建一个,等待它完成,创建另一个,依此类推。

创建执行人有几种方法;在这个配方中,我们将使用ThreadPoolExecutor,它将使用线程。

我们将使用准备好的示例作为示例,可在 GitHub 回购协议中找到:https://github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter03/test_site 。下载整个站点并运行包含的脚本

$ python simple_delay_server.py -d 2

这为 URLhttp://localhost:8000中的站点提供服务。你可以在浏览器上查看它。这是一个简单的博客,有三个条目。大部分内容都很枯燥,但我们添加了几个包含关键字python的段落。参数-d 2故意使服务器变慢,模拟坏连接。

怎么做。。。

  1. 写下面的脚本,speed_up_step1.py。完整代码可在 GitHub 的Chapter03下找到 https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter03/speed_up_step1.py目录。这里只是最相关的部分。基于crawling_web_step1.py
...
def process_link(source_link, text):
    ...
    return source_link, get_links(parsed_source, page)
...

def main(base_url, to_search, workers):
    checked_links = set()
    to_check = [base_url]
    max_checks = 10

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
        while to_check:
            futures = [executor.submit(process_link, url, to_search)
                       for url in to_check]
            to_check = []
            for data in concurrent.futures.as_completed(futures):
                link, new_links = data.result()

                checked_links.add(link)
                for link in new_links:
                    if link not in checked_links and link not in to_check:
                        to_check.append(link)

                max_checks -= 1
                if not max_checks:
                    return

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    ...
    parser.add_argument('-w', type=int, help='Number of workers',
                        default=4)
    args = parser.parse_args()

    main(args.u, args.p, args.w)
  1. 注意main函数中的差异。此外,还添加了一个额外的参数(并发工作者的数量),函数process_link现在返回源链接。
  2. 运行crawling_web_step1.py脚本以获取时间基线。请注意,为清晰起见,此处已删除输出:
$ time python crawling_web_step1.py http://localhost:8000/
... REMOVED OUTPUT
real 0m12.221s
user 0m0.160s
sys 0m0.034s
  1. 使用一个工作进程运行新脚本,这比原始脚本慢:
$ time python speed_up_step1.py -w 1
... REMOVED OUTPUT
real 0m16.403s
user 0m0.181s
sys 0m0.068s
  1. 增加工人数量:
$ time python speed_up_step1.py -w 2
... REMOVED OUTPUT
real 0m10.353s
user 0m0.199s
sys 0m0.068s
  1. 添加更多的工作人员可以缩短时间:
$ time python speed_up_step1.py -w 5
... REMOVED OUTPUT
real 0m6.234s
user 0m0.171s
sys 0m0.040s

它是如何工作的。。。

创建并发请求的主引擎是主要功能。请注意,代码的其余部分基本上未被触及(除了在process_link函数中返回源链接)。

This change is actually quite common when adapting for concurrency. Concurrent tasks need to return all the relevant data, as they cannot rely on an ordered context.

这是处理并发引擎的代码的相关部分:

with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
    while to_check:
        futures = [executor.submit(process_link, url, to_search)
                   for url in to_check]
        to_check = []
        for data in concurrent.futures.as_completed(futures):
            link, new_links = data.result()

            checked_links.add(link)
            for link in new_links:
                if link not in checked_links and link not in to_check:
                    to_check.append(link)

             max_checks -= 1
             if not max_checks:
                return

with上下文创建一个工人池,指定其编号。在内部,将创建一个包含所有要检索的 URL 的未来列表。.as_completed()函数返回已完成的未来,然后有一些工作处理获取新发现的链接,并检查是否需要添加这些链接以进行检索。这个过程与抓取网页配方中的过程类似。

该过程将再次启动,直到检索到足够的链接或没有要检索的链接为止。请注意,链接是批量检索的;第一次,处理基本链接并检索所有链接。在第二次迭代中,将请求所有这些链接。一旦全部下载,将处理一个新批。

When dealing with concurrent requests, keep in mind that they can change order between two executions. If a request takes a little more or a little less time, that can affect the ordering of the retrieved information. Because we stop after downloading 10 pages, that also means that the 10 pages could be different.

还有更多。。。

完整的 Pythonfutures文档可以在这里找到:https://docs.python.org/3/library/concurrent.futures.html

As you can see in steps 4 and 5 in the How to do it… section, properly determining the number of workers can require some tests. Some numbers can make the process slower, due the increase in management. Do not be afraid to experiment!

在 Python 世界中,还有其他方法可以进行并发 HTTP 请求。有一个本地请求模块,允许我们使用futures,称为requests-futures。可以在这里找到:https://github.com/ross/requests-futures

另一种选择是使用异步编程。这种工作方式最近得到了很多关注,因为它在处理许多并发调用时非常有效,但由此产生的编码方式与传统方式不同,需要一些时间来适应。Python 包括asyncio模块以这种方式工作,还有一个名为aiohttp的好模块用于处理 HTTP 请求。您可以在这里找到有关aiohttp的更多信息:https://aiohttp.readthedocs.io/en/stable/client_quickstart.html

关于异步编程的一个很好的介绍可以在本文中找到:https://djangostars.com/blog/asynchronous-programming-in-python-asyncio/

另见

  • 爬行网页配方
  • 下载网页配方