本章为实际书本上的第12章,虽然之前已经做过两个爬虫项目(一个的豆瓣、一个是哔哩哔哩),但还是想看看本书中是不是有新鲜的内容。

那么,开始吧。

模块解释:

webbrowser:是Python自带的,可打开浏览器获取指定页面。

requests:从因特网上下载文件和网页

bs4:解析HTML,即网页编写的格式

selenium:启动并控制一个Web浏览器。selenium能够填写表单,并模拟鼠标在这个浏览器中单击。

12.1 项目:利用webbrowser 模块的maplt.py

webbrowser 模块的open()函数可以启动一个新浏览器来打开指定的URL。在交互式环境中输入以下代码:

In [1]: import webbrowser

In [3]: webbrowser.open('https://www.ailibili.com')
Out[3]: True

Web浏览器的标签页将打开ailibili.com网站。这大概是webbrowser模块能做的唯一的事情了。即使如此,open()函数确实能让一些有趣的事情成为可能。例如,将一条街道的地址复制到剪贴板,并在Google 地图上寻找它,这是很繁琐的事。你可以让这个任务减少几个步骤,方法是写一个脚本,以利用剪贴板中的内容在浏览器中自动加载地图。这样,你只要将地址复制到剪贴板即可。运行该脚本,地图就会加载。

你的程序需要做到下列事情:
1、从命令行参数或剪贴板中取得街道地址
2、打开Web浏览器,指向该地址的Google 地图页面。

这意味着代码需要做下列事情。
1、从sys.argv读取命令行参数
2、读取剪贴板内容
3、调用webbrowser.open()打开外部浏览器。

打开一个新的文件编辑器窗口,将它保存为maplt.py。

第一步:弄清楚URL

首先你需要弄清楚对于制定的街道地址,要使用怎样的URL。(胭惜雨:这里一段内容都是讲谷歌地图的,我尝试用百度地图来实现该功能,所以这部分内容就不需要讲了。)

URL:https://map.baidu.com/poi/%E5%A4%A9%E6%B2%B3%E5%8C%BA%E7%BD%91%E6%98%93%E5%A4%A7%E5%8E%A6/@12621488.862207806,2631602.3210331174,14.83z?uid=83a2978f7228e5b3e607389e&ugc_type=3&ugc_ver=1&device_ratio=2&compat=1&querytype=detailConInfo&da_src=shareurl

第二步:处理命令行参数

让你的代码看起来像这样:
In [4]: import webbrowser,sys

In [5]: if len(sys.argv) > 1:
   ...:     address = ''.join(sys.argv[1:])

导入webbrowser模块来加载浏览器:以及导入sys模块,用于读入可能的命令行参数。sys.argv 变量保存了程序的文件名和命令行参数的列表。如果这个列表中不只有文件名,那么len(sys.argv)的返回值就会大于1,这意味着确实提供了命令行参数。

命令行参数通常用空格分隔,但在这个例子中,你希望将所有参数解释为一个字符串。因为sys.argv是字符串的列表,所以你可以将它传递给join()方法,这将返回一个字符串。你不希望程序的名称出现在这个字符串中,因此不是使用sys.argv,而是使用sys.argv[1:]来去掉这个数组的第一个元素。这个表达式求值得到的字符串保存在address变量中。

如果运行程序时在命令行输入以下内容:
mapit 870 Valencia ST, San Francisco,CA 94110

sys.argv 变量将包含这样的列表值:
['mapit.py','870','Valencia','St,','San','Francisco','CA','94110']

address 变量将包含字符串‘870 Valencia ST, San Francisco,CA 94110’.

这里有两个问题:

1、如何创建执行脚本(.py比较好弄,我这里做了command脚本),在研究了许久后,我得出了答案(以下流程及代码是在MACOS环境下实现):

(1)用textedit创建一个新文档,在“格式”选择“纯文本”。

(2)脚本代码样式如下:

#!/usr/bin/env bash
Python3 /Users/python工程文件/zidonghua/mapit.py

(3)文件重命名为.command

第一个问题解决了,第二个问题我还在想。

2、如何复现课本上所说的

如果运行程序时在命令行输入以下内容:
mapit 870 Valencia ST, San Francisco,CA 94110

sys.argv 变量将包含这样的列表值:
['mapit.py','870','Valencia','St,','San','Francisco','CA','94110']

address 变量将包含字符串‘870 Valencia ST, San Francisco,CA 94110’.

后来翻了半天,发现根本就没有print(),怎么验证内容是否正确呢?

需要增加代码。

import webbrowser,sys

if len(sys.argv) > 1:
    address = ''.join(sys.argv[1:])
print(sys.argv)
print(address)

命令行参数是直接跟在.py后面的。

#!/usr/bin/env bash
python3 /Users/python工程文件/zidonghua/mapit.py mapit 870 Valencia ST, San Francisco,CA 94110

这样才终于达到书中的效果了。

这部分写的不是太好,有点云里雾里的。

第3步:处理剪贴板内容,加载浏览器

让你的代码看起来像这样:

#! python3

import webbrowser,sys,pyperclip

if len(sys.argv) > 1:
    address = ''.join(sys.argv[1:])
else:
    address = pyperclip.paste()
webbrowser.open(r'https://map.baidu.com/poi/%E5%A4%A9%E6%B2%B3%E5%8C%BA%E7%BD%91%E6%98%93%E5%A4%A7%E5%8E%A6/@12621485.125415739,2631329.9856151454,15.86z?uid=83a2978f7228e5b3e607389e&ugc_type=3&ugc_ver=1&device_ratio=2&compat=1&querytype=detailConInfo&da_src=shareurl' + address)

胭惜雨:原文用的是谷歌地图,这里我使用百度地图替换。

如果没有命令行参数,程序将假定地址保存在剪贴板中。可以用pyperclip.paste()取得剪贴板的内容,并将它保存在名为address的变量中。最后,启动外部浏览器访问百度地图的URL,并调用webbrowser.open()。

# 胭惜雨:意思就是,只要你的剪贴板内容(就是“复制”)这部分是网址,即使在上述代码中不填入百度地图的网址,也可以直接打开。但是如果你上述代码中已经固定了A网址,同时你的剪贴板是B网址,那么在运行代码时,打开的依然是A网址。

虽然你写的某些程序可能完成大型任务,从而为你节省数小时的时间,但是用一个程序在每次执行一个常用任务时节省几秒时间,同样令人满意。

第4步:类似程序的想法
只要你有一个URL.webbrowser模块就可以让用户不必打开浏览器而直接加载一个网站。其他程序可以利用这项功能完成以下任务。

(1)在独立的浏览器窗口中,打开一个页面的所有链接。
(2)用浏览器打开本地天气的URL
(3)打开你经常查看的几个社交网站

12.2 用requests模块从Web下载文件

requests.get()函数接收一个要下载的URL字符串。通过在requests.get()的返回值上调用type(),你可以看到它返回一个Response对象,其中包含了Web服务器对你的请求作出的相应。请在交互式环境中输入以下代码,并保持计算机与因特网的链接:

In [11]: import requests

In [12]: url = 'https://www.ailibili.com'

In [13]: res = requests.get(url)

In [14]: res.status_code == requests.codes.ok
Out[14]: True

In [15]: len(res.text)
Out[15]: 53000

In [16]: print(res.text[:250])
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <link rel="profile" href="https://gmpg.org/xfn/11"/>
        <title>胭惜雨的博客 &#8211; 问君何能尔?心远地自偏</title>

# 胭惜雨:原文这里使用的是《罗密欧与朱丽叶》,但是原文上的网址并打不开,所以这里用本博客的网址予以代替。

该URL指向一个网页,通过检查Response对象的status_code属性,你可以了解对这个网页的请求是否成功。如果该值等于requests.codes.ok,那么一切都好(顺便说下,HTTP中的“OK”的状态码是200.你可能已经熟悉404状态码,它表示“没找到”)。

如果请求成功,下载的页面就作为一个字符串保存在Response对象的text变量中。这个变量保存了包含整个网页文本的一个大字符串,调用len(res.text)表明它的长度超过53000个字符。最后,调用print(res.text[:250])显示前250个字符。

如果请求失败并显示错误信息,如“Failed to establish a new connect”(建立新连接失败)或”Max retries exceeded“(超过最大重试次数),那么请检查你的网络连接。
12.2.2 检查错误
正如你看到的,Response对象有一个status_code属性,可以检查它是否等于requests.codes.ok,从而了解下载是否成功。检查成功与否有一种简单的方法,就是在Response对象上调用raise_for_status()方法。如果下载文件出错,将抛出异常;如果下载成功,就什么都不做。在交互式环境中输入以下代码:

In [17]: rres = requests.get('https://ailibili.com/alibili')

In [18]: rres.raise_for_status()
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
<ipython-input-18-2dbef20ba087> in <module>
----> 1 rres.raise_for_status()

/opt/anaconda3/lib/python3.8/site-packages/requests/models.py in raise_for_status(self)
    939 
    940         if http_error_msg:
--> 941             raise HTTPError(http_error_msg, response=self)
    942 
    943     def close(self):

HTTPError: 404 Client Error: Not Found for url: https://ailibili.com/alibili


调用raise_for_status()方法是一种很好的方式,确保程序在下载失败时停止。这是一件好事:你希望程序在发生未预期的错误时马上停止。如果下载失败对程序来说不够严重,可以用try和except语句将raise_for_status()代码包裹起来,处理这一错误,不让程序崩溃:

import requests
res = requests.get('https://ailibili.com/ailibili')
try:
    res.raise_for_status()
except Exception as exc:
    print('There was a problem:%s' % (exc))
res.raise_for_status()

Traceback (most recent call last):
  File "/Users/gregory_mac/SynologyDrive/python工程文件/zidonghua/mapit.py", line 7, in <module>
    res.raise_for_status()
  File "/opt/anaconda3/lib/python3.8/site-packages/requests/models.py", line 941, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://ailibili.com/ailibili
There was a problem:404 Client Error: Not Found for url: https://ailibili.com/ailibili

程序总是在调用requests.get()之后再调用raise_for_status()。确保下载成功后再让程序继续。

12.3 将下载的文件保存到硬盘

现在,可以用标准的open()函数和write()方法,将Web页面保存到硬盘中的一个文件。但是,这里将的方法稍稍有一点不同。首先,必须用“写二进制”模式打开该文件,即向函数传入字符串‘wb’作为open()的第二参数。即使该页面是纯文本的,你也需要写入二进制数据,而不是文本数据。目的是保存该文本的”Unicode编码“。

为了将Web页面写入一个文件,可以使用for循环和Response对象的iter_content()方法:

In [25]: import requests

In [26]: res = requests.get('https://ailibili.com')

In [27]: res.raise_for_status()

In [28]: playFile = open('demaxiya.txt','wb') # 存储到本地一个叫demaxiya.txt的文件中

In [29]: for chunk in res.iter_content(100000):
    ...:     playFile.write(chunk)
    ...: 

In [31]: playFile.close()
打开demaxiya.txt后如图所示
iter_content()方法在循环的每次迭代中返回一段内容,每一段都是bytes数据类型,你需要指定一段包含多少字节。100000字节通常是不错的选择,所以将“100000”作为参数传递给iter_content()。

文件demaxiya.txt将存在于当前工作目录。请注意,虽然在网站上文件名可能是其他名字,但在你的硬盘上,该文件的名字不同。requests模块只处理下载的网页内容。一旦网页下载后,它就只是程序中的数据。即使在下载该网页后断开了网络连接,该页面的所有数据仍然在你的计算机中。

write()方法返回一个数字,表示写入的字节数。在前面的例子中,第一段包含100000字节,文件剩下的部分只需要78981字节。

回顾一下,下载并保存到文件的完整过程如下:
1.调用requests.get()下载该文件。
2.用‘wb’调用open(),以写二进制的方式打开一个新文件。
3.利用Response 对象的iter_content()方法做循环。
4.在每次迭代中调用write(),将内容写入该文件。
5.调用close()关闭该文件。

这就是关于requests模块的全部内容。相对于写入文本文件的open()/write()/close()工作步骤,for循环和iter_content()的部分可能看起来比较复杂,但这是为了确保requests模块即使下载巨大的文件也不会消耗太多内存。

12.5 用bs4 模块解析HTML

# 胭惜雨:12.4部分是简单的HTML知识,这部分就不复述了,如果懂一些的不用看,如果不懂的,看了也没用,需要先去看HTML相关的基础知识(看基础入门半本就够了)。

Beautiful Soup是一个模块,用于从HTML 页面中提取信息(用这个目的时,它比正则表达式好很多)。Beautiful Soup模块的名称是bs4。

在本章中,Beautiful Soup的例子将解析硬盘上的一个HTML文件。在IDLE中打开一个新的文件编辑器窗口,输入以下代码,并保存为example.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The Website Title</title>
</head>
<body>
<p> Download my <strong>Python</strong>> book from <a href="https://ailibili.com">my blog</a></p>
<p class="slogan">Learn Python the easy way!</p>
<p>By <span id=""author">A1 Sweigart</span></p>

</body>
</html>

你可以看到,即使是一个简单的HTML文件,也包含许多不同的标签和属性。对于复杂的网站,事情很快就变得令人困惑。好在,Beautiful Soup 让处理HTML变得容易很多。
12.5.1 从HTML 创建一个BeautifulSoup 对象
bs4.BeautifulSoup()函数调用时需要一个字符串,其中包含将要解析的HTML。bs4.BeautifulSoup()函数返回一个BeautifulSoup对象:
In [32]: import requests,bs4

In [33]: res = requests.get('https://ailibili.com')

In [34]: res.raise_for_status()

In [35]: nos = bs4.BeautifulSoup(res.text,'html.parser')

In [36]: type(nos)
Out[36]: bs4.BeautifulSoup

这段代码利用requests.get()函数从ailibili.com下载主页,然后将响应结果的text属性传递给bs4.BeautifulSoup()。它返回的BeautifulSoup对象保存在变量nos中。

也可以向bs4.BeautifulSoup()传递一个File对象,以及第二个参数,告诉BeautifulSoup使用哪个解析器来分析HTML:
In [37]: exa = open('example.html')

In [38]: exas = bs4.BeautifulSoup(exa,'html.parser')

In [39]: type(exas)
Out[39]: bs4.BeautifulSoup

这里使用的‘html.parser’解析器是Python自带的。但是,如果你安装了第三方lxml模块,你就可以使用更快的‘lxml’解析器。

有了BeautifulSoup对象后,就可以利用它的方法,定位HTML文档中的特定部分。
12.5.2 用select()方法寻找元素
调用一个BeautifulSoup对象的select()方法,传入一个字符串作为CSS选择器,就可以取得一个web页面元素。选择器就像正则表达式:它们指定了要寻找的模式,在这个例子中,是在HTML页面中寻找,而不是在普通的文本字符串中寻找。
CSS选择器的例子
不同的选择器模式可以组合起来形成复杂的匹配。例如,soup.select('p #author')将匹配所有id属性为author的元素,只要它也在一个<p>元素之内。你也可以在浏览器中右击元素,选择Inspect Element,而不是自己编写选择器。当浏览器的开发者控制台打开后,右键单击元素的HTML,选择Copy > CSS Selector,将选择器字符串复制到剪贴板上,然后粘贴到你的源代码中。
就是这个
Select()方法将返回一个Tag对象的列表,这是Beautiful Soup表示一个HTML元素的方式。针对Beautiful Soup对象中的HTML的每次匹配,列表中都有一个Tag对象,Tag值可以传递给str()函数,显示它们代表的HTML标签。Tag值也可以有attrs属性,它将该Tag的所有HTML属性作为一个字典。利用前面的example.html文件,在交互式环境中输入以下代码:

In [51]: import bs4

In [52]: exa = open('example.html')

In [53]: exas = bs4.BeautifulSoup(exa.read(),'html.parser')

In [54]: elems = exas.select('#author')

In [55]: type(elems)
Out[55]: bs4.element.ResultSet

In [56]: len(elems)
Out[56]: 1

In [57]: type(elems[0])
Out[57]: bs4.element.Tag

In [58]: str(elems[0])
Out[58]: '<span id="author">A1 Sweigart</span>'

In [60]: elems[0].getText()
Out[60]: 'A1 Sweigart'

In [61]: elems[0].attrs
Out[61]: {'id': 'author'}

这段代码将带有id="author"的元素从示例HTML中找出来。我们使用select('#author')返回一个列表,其中包含所有带有id='author'的元素。我们将这个Tag对象的列表保存在变量elems中,len(elems)告诉我们列表中只有一个Tag对象,且只有一次匹配。在钙元素上调用getText()方法,以返回该元素的文本或内部的HTML。一个元素的文本是在开始和结束标签之间的内容:在这个例子中,就是‘A1 Sweigart’。

将钙元素传递给str(),以返回一个字符串,其中包含开始和结束标签,以及钙元素的文本。最后,attrs给了我们一个字典,其中包含该元素的属性‘id’,以及id属性的值‘author’。

也可以从BeautifulSoup对象中找出<p>元素,在交互式环境中输入以下代码:
In [62]: pe = exas.select('p')

In [63]: str(pe[0])
Out[63]: '<p> Download my <strong>Python</strong>&gt; book from <a href="https://ailibili.com">my blog</a></p>'

In [64]: pe[0].getText()
Out[64]: ' Download my Python> book from my blog'

In [65]: str(pe[1])
Out[65]: '<p class="slogan">Learn Python the easy way!</p>'

In [66]: str(pe[2])
Out[66]: '<p>By <span id="author">A1 Sweigart</span></p>'

In [67]: pe[2].getText()
Out[67]: 'By A1 Sweigart'

这一次,select()给我们一个列表,该列表包含3次匹配,我们将该列表保存在pe中。在pe[0]、[1]、[2]上使用str(),这将每个元素显示为一个字符串。在每个元素上使用getText()以显示它的文本。
12.5.3 通过元素的属性获取数据
Tag对象的get(0方法让我们很容易从元素中获取属性值。向该方法传入一个属性名称的字符串,它将返回该元素的值。利用example.html,在交互式环境中输入以下代码:
In [69]: import bs4

In [70]: soup = bs4.BeautifulSoup(open('example.html'),'html.parser')

In [71]: spe = soup.select('span')[0]

In [72]: str(spe)
Out[72]: '<span id="author">A1 Sweigart</span>'

In [73]: spe.get('id')
Out[73]: 'author'

In [74]: spe.get('some') == None
Out[74]: True

In [75]: spe.attrs
Out[75]: {'id': 'author'}

这里,我们使用select()来寻找所有<span>元素,然后将第一个匹配的元素保存在spe中。将属性名‘id’传递给get()以返回该属性的值'author'。

12.6 项目:打开所有搜索结果

每次我在谷歌上搜索一个主题时,不会一次只看一个搜索结果。通过鼠标中间单击搜索结果链接,或在单击时按住Ctrl键,我会在一些新的标签页中打开前几个链接以稍后查看。我经常使用谷歌搜索,因此这个工作流程变得很乏味。如果我只要在命令行中输入查找主题,就能让计算机自动打开浏览器,并在新的标签页中显示前几项的查询结果,那就太好了。让我们写一个脚本,针对位于PyPI官网,用它的搜索结果页面来做这个事情。

程序需要完成以下任务:

1、从命令行参数中获取查询关键字

2、取得查询结果页面

3、为每个结果打开一个浏览器标签页

这意味着代码需要执行以下操作。

1、从sys.argv中读取命令行参数

2、用requests模块取得查询结果页面

3、找到每个查询结果的链接

4、调用webbrowser.open()函数打开Web浏览器

打开一个新的文件编辑器窗口,并将其保存为sea.py

第1步:获取命令行参数,并请求查询页面
在开始编码之前,你首先要知道查询结果页面的URL。查看浏览器地址栏,就会发现结果页面的URL类似于后缀为s?wd=www.talkroblox.com。requests模块可以下载这个页面;然后可以用Beautiful Soup模块找到HTML中的查询结果的链接;最后,用webbrowser 模块在浏览器标签页中打开这些链接。

#! python3
import requests,bs4,webbrowser,sys
print('搜索中......')
res = requests.get('https://baidu.com/s?wd='+ ''.join(sys.argv[1:]))
res.raise_for_status()

用户运行该程序时,将通过命令行参数指定查询的主题。这些参数将作为字符串保存在sys.argv列表中。
第2步:找到所有的结果
现在你需要使用的Beautiful Soup从下载的HTML中提取排名靠前的查询结果链接。但如何知道完成这项工作需要怎样的选择器呢?例如,你不能只查找所有的<a>标签,因为这个HTML中,有许多链接你是不关心的,所以必须用浏览器的开发者工具来检查这个查询结果页面,尝试寻找一个选择器,它将挑选出你想要的链接。

在针对Beautiful Soup进行查询后,你可以打开浏览器的开发者工具,查看该页面上的一些链接元素。他们看起来很复杂,但是没有关系,只需要找到查询结果链接都有的模式即可。

#! python3
import requests,bs4,webbrowser,sys
print('搜索中......')
res = requests.get('https://baidu.com/s?wd='+ ''.join(sys.argv[1:]))
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text,'html.parser')
link = soup.select('h3 class="t"')

你不需要知道CSS类class="t"是什么,或者它会做什么。只需要利用它作为一个标记,查找你需要的<a>元素。可以通过下载页面的HTML文本创建一个Beautiful Soup对象,然后用选择器‘h3 class=“t”’找到匹配的标签。

第3步:针对每个结果打开Web浏览器
#胭惜雨:这里我做了些改动,本来想用百度搜索,但百度那些高T们呵呵一笑,做了加密,根本爬取不到内容。于是就用本博客的自带搜索,做了一个。但是因为用的地址跟书上的不同,不能按照书上的代码直接照抄。于是就增加了re模块,用正则表达式的方式提取网址,然后直接输入即可。

#! python3
import requests
import bs4
import webbrowser
import sys
import re
print('搜索中......')
# 前面部分为固定地址,+的内容为搜索内容如:https://www.ailibili.com/?s=python
res = requests.get('https://www.ailibili.com/?s=' + ''.join(sys.argv[1:]))

res.raise_for_status()  # 判断是否正常,如无法打开,则抛出异常代码
soup = bs4.BeautifulSoup(res.text, 'html.parser')  # 用html.parser解析res网页里的文本信息
link = soup.select('.entry-title')  # 用筛选器筛选出对应的网址链接
# 正则表达式规则为什么用了筛选器后还要用正则表达式呢?而不是直接用正则表达式?因为从这里开始,代码开始与书中不一样,要对上面的代码进行二次清洗
res = re.compile(r'href="(.*?)"')
link = str(link)  # 将清洗出来的内容定义成字符串,这样才可以进行正则表达式匹配
ress = re.findall(res, link)  # 对link内容进行清洗

for i in ress:  # 把每个地址赋给变量i
    print('打开', i, '中...')  # 做输出提示。
    webbrowser.open(i)  # 用浏览器新标签页打开i的网址

第4步:类似程序的想法
分标签页浏览的好处在于很容易在新标签页中打开一些链接,可以稍后再来查看。一个自动打开几个链接的程序,很适合快捷地完成下列人物。
1、查找亚马逊这样的电商网站后,打开所有的产品页面 # 胭惜雨:我怀疑淘宝应该也做了加密或屏蔽
2、打开针对一个产品的所有评论的链接
3查找Flickr或Imgur这样的招聘网站后,打开查找结果中的所有照片的链接。

胭惜雨

2021年06月02日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据