Web Scraping with Python


通过《Web Scraping with Python》学习爬虫

中文版:《Python网络数据采集》

开篇demo:

1
2
3
4
5
6
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/page1.html")
bsObj = BeautifulSoup(html.read(), "html5lib")
print(bsObj.h1)

其中,html的内容为:

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<title>A Useful Page</title>
</head>
<body>
<h1>An Interesting Title</h1>
<div>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
</body>
</html>

程序的输出为:

1
<h1>An Interesting Title</h1>

urllib 是 Python 的标准库(不用额外安装),参考:https://docs.python.org/3/library/urllib.html

Python 2.x 中是 urllib2。在Python 3.x 里,urllib2 改名为 urllib。urllib 包含一些子模块: urllib.request、urllib.parse 和 urllib.error。

BeautifulSoup 能定位 HTML 标签来格式化和组织复杂的网络信息,用简单易用的 Python 对象为我们展现 XML 结构信息。他不是标准库,需要额外安装 (使用conda安装):

1
conda install beautifulsoup4

bsObj.h1换成下面的几行代码都有相同的输出:

1
2
3
bsObj.html.body.h1
bsObj.body.h1
bsObj.html.h1

奇怪的是如果在bsObj = BeautifulSoup(html.read(), "html5lib")之前运行过html.read(),那么 bsObj 就拿不到数据,和文件差不多,指针已经到末尾?

爬数据时可能遇到的问题

对于上面程序中的这行代码:

1
html = urlopen("http://www.pythonscraping.com/pages/page1.html")

可能会产生三种错误情况:

1.服务器在,网页不在

这时程序会返回 HTTP 错误。 HTTP 错误可能是“404 Page Not Found”、“500 Internal Server Error”等:

1
urllib.error.HTTPError: HTTP Error 404: Not Found

所有类似情形, urlopen 函数都会抛出HTTPError异常。我们 可以用下面的方式处理这种异常:

1
2
3
4
5
6
7
8
try:
html = urlopen("http://www.pythonscraping.com/pages/page1.html")
except HTTPError as e:
print(e)
# 返回空值,中断程序,或者执行另一个方案
else:
# 程序继续。注意:如果你已经在上面异常捕捉那一段代码里返回或中断(break),
# 那么就不需要使用else语句了,这段代码也不会执行

如果程序返回 HTTP 错误代码,程序就会显示错误内容,不再执行 else 语句后面的代码。

2.服务器不存在

此时 urlopen 会返回一个 None 对象。

3.标签不存在

print(bsObj.h1)中的bsObj.h1不存在。

如果你想要调用的标签不存在, BeautifulSoup 就会返回 None 对象。不过,如果再调用这个 None 对象下面的子标签,就会发生 AttributeError 错误:

1
2
3
4
Traceback (most recent call last):
File "E:/PyCharm_project/web_data/test.py", line 19, in <module>
print(bsObj.html.h1.a.b)
AttributeError: 'NoneType' object has no attribute 'b'

4.总结

上面1、3中异常情况,分别返回urllib.error.HTTPErrorAttributeError。通过:

1
from urllib.error import HTTPError

可以直接:

1
except HTTPError as e:

为了处理上面的异常情况,可以写一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup


def getTitle(url):
try:
html = urlopen(url)
except HTTPError as e:
return None
try:
bsObj = BeautifulSoup(html.read())
title = bsObj.body.h1
except AttributeError as e:
return None
return title

title = getTitle("http://www.pythonscraping.com/pages/page1.html")
if title == None:
print("Title could not be found")
else:
print(title)

BeautifulSoup的find()和findAll()

CSS 可以让 HTML 元素呈现出差异化,比如标签:

1
2
3
<span class="green"></span>
...
<span class="red"></span>

这两个标签可以将文字编程绿色或者红色。

http://www.pythonscraping.com/pages/warandpeace.html这个网页来测试。(在这个页面里, 小说人物的对话内容都是红色的,人物名称都是绿色的),通过 BeautifulSoup 对象,我们可以用 findAll 函数抽取只包含在 <span class="green"></ span> 标签里的文字,这样就会得到一个人物名称的 Python 列表:

1
2
3
4
5
6
7
8
9
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/warandpeace.html")

bsObj = BeautifulSoup(html, "html5lib")

nameList = bsObj.findAll("span", {"class":"green"})
for name in nameList:
print(name.get_text())

同样,在其他位置调用html.read()会与bsObj = BeautifulSoup(html, "html5lib")产生冲突,类似于读到文件末尾,让后面进行读取的操作读不到数据。

调用 bsObj.tagName 只能获取页面中的第一个指定的标签。现在,调用 bsObj.findAll(tagName, tagAttributes) 可以获取页面中所有指定的标签,不再只是第一个了。

.get_text() 会把你正在处理的 HTML 文档中所有的标签都清除,然后返回 一个只包含文字的字符串。通常在你准备打印、存储和操作数据时,应该最后才使 用 .get_text()。一般情况下,你应该尽可能地保留 HTML 文档的标签结构。

find()findAll():

1
2
findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)

tag

  • 一个标签的名称或多个标签名称组成的 Python 列表

attributes

  • 用一个 Python 字典封装一个标签的若干属性和对应的属性值
  • .findAll("span", {"class":{"green", "red"}})返回 HTML 文档里红色与绿色两种颜色的 span 标签,注意 span 是标签,{“class”:{“green”, “red”} 是属性

recursive

  • 设置为 True(默认),findAll 就会根据你的要求去查找标签参数的所有子标签,以及子标签下的子标签
  • 设置为 False,findAll 就只查找文档的一级标签

text

  • 用标签的文本内容去匹配,而不是用标签的属性

    1
    2
    3
    4
    nameList = bsObj.findAll(text="the prince")
    print(len(nameList))

    output :7

limit(findAll 方法独有)

  • find 其实等价于 findAll 的 limit 等于 1 时的情形
  • 只获取网页中 的前 x 项结果

keywords

  • 选择那些具有指定属性的标签

  • 1
    2
    allText = bsObj.findAll(id="text")
    print(allText[0].get_text())

    这两行代码完全一样:

    1
    2
    bsObj.findAll(id="text")
    bsObj.findAll("", {"id":"text"})
  • 用 keyword 偶尔会出现问题,尤其是在用 class 属性查找标签的时候, 因为 class 是 Python 中的关键字,下面的代码会出错:

    1
    bsObj.findAll(class="green")

    可以改成这样:

    1
    bsObj.findAll(class_="green") # 后面加一个下划线

    或者:

    1
    bsObj.findAll("", {"class":"green"})

bsObj.div.findAll(“img”) 会找出文档中第一个 div 标签,然后获取这个 div 后 代里所有的 img 标签列表.

标签之间的关系

child、descendant

.children 找出子标签

.descendant找出后代标签

(注意子标签与后代标签的区别)

.next_siblings()处理兄弟标签

http://www.pythonscraping.com/pages/page3.html 的结构:

1
2
3
4
5
6
7
8
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)

for child in bsObj.find("table",{"id":"giftList"}).children:
print(child)

上面这段代码会将<table id="giftList">...</table>之间的内容(子标签,包括标签和文本)全部显示出来

sibling

获取兄弟标签内容:next_siblings()

1
2
3
4
5
6
7
8
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html, "html5lib")

for sibling in bsObj.find("table",{"id":"giftList"}).tr.next_siblings:
print(sibling)

上面这一段代码可以将gift1---gift6之间的信息打印出来,因为他们是.tr的兄弟标签

next_siblings()这个函数只调用后面的兄弟标签。例如,如果选择一组标签中位于中间位置的一个标签, 然后用next_siblings()函数,那么它就 只会返回在它后面的兄弟标签。

类似的还有.previous_siblings、.next_sibling 、 .previous_sibling,后面两个返回的是单个标签。

parent

.parent表示上一级标签,还有.parents

看一下结构图:

1
2
3
4
5
6
7
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)

print(bsObj.find("img",{"src":"../img/gifts/img1.jpg"}).parent.previous_sibling.get_text())

上面这段代码打印的结果是:$15.00

利用正则表达式获取数据

正则表达式(regular expression / regex)

正则表达式常用符号

有些语言, 比如 Java,其正则表达式和 Python 不太一样。

使用正则表达式获取数据

注意观察网页上有几个商品图片——它们的源代码形式如下:

1
<img src="../img/gifts/img3.jpg">

运行以下代码:

1
2
3
4
5
6
7
8
9
10
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)
images = bsObj.findAll("img", {"src":re.compile("\.\.\/img\/gifts/img.*\.jpg")})

for image in images:
print(image["src"])

这段代码会打印出图片的相对路径,都是以 ../img/gifts/img 开头,以 .jpg 结尾,其结果如下所示:

1
2
3
4
5
../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg

获取属性

对于:

1
<img src="../img/gifts/img3.jpg">

标签是img,属性是src="../img/gifts/img3.jpg"

对于一个标签对象,可以用下面的代码获取它的全部属性:

1
myTag.attrs

要注意这行代码返回的是一个 Python 字典对象,可以获取和操作这些属性。比如要获取图 片的资源位置 src,可以用下面这行代码:

1
myImgTag.attrs["src"]

Lambda表达式

BeautifulSoup 允许我们把特定函数类型当作 findAll 函数的参数。唯一的限制条件是这些 函数必须把一个标签作为参数且返回结果是布尔类型。 BeautifulSoup 用这个函数来评估它 遇到的每个标签对象,最后把评估结果为“真”的标签保留,把其他标签剔除。

下面的代码就是获取有两个属性的标签:

1
soup.findAll(lambda tag: len(tag.attrs) == 2)

结果是找到类似这样的标签:

1
2
<div class="body" id="content"></div>
<span style="color:red" class="title"></span>

如果你愿意多写一点儿代码,那么在 BeautifulSoup 里用 Lambda 表达式选择标签,将是正 则表达式的完美替代方案。

采集数据

提取维基百科某页面上的所有链接

HTML 链接是通过 <a> 标签进行定义的。

1
2
> <a href="http://www.w3school.com.cn">This is a link</a>
>

在 href 属性中指定链接的地址

1
2
3
4
5
6
7
8
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html)
for link in bsObj.findAll("a"):
if 'href' in link.attrs:
print(link.attrs['href'])

结果中包含页面上的所有链接。

维基百科的每个页面都充满了侧边栏、页眉、页脚链接,以及连接到分类页面、对话 页面和其他不包含词条的页面的链接:

1
2
/wiki/Category:Articles_with_unsourced_statements_from_April_2014
/wiki/Talk:Kevin_Bacon

只提取词条链接

可以将上面程序运行获取的链接分为词条连接其他链接

仔细观察那些指向词条页面(不是指向其他内容页面)的链接,会发现它们都有三个共同点:

  • 它们都在 id 是 bodyContent 的 div 标签里
  • URL 链接不包含分号
  • URL 链接都以 /wiki/ 开头

调整一下代码来获取词条链接:

1
2
3
4
5
6
7
8
9
10
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html)

for link in bsObj.find("div", {"id": "bodyContent"}).findAll("a", {'href': re.compile("^(/wiki/)((?!:).)*$")}):
if 'href' in link.attrs:
print(link.attrs['href'])

其中,.find()的第二个参数也可以写成href=re.compile("^(/wiki/)((?!:).)*$")

采集整个网站

用递归的方法寻找整个网站的链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

pages = set()

def getLinks(pageUrl):
global pages
html = urlopen("http://en.wikipedia.org"+pageUrl)
bsObj = BeautifulSoup(html)
for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
# 我们遇到了新页面
newPage = link.attrs['href']
print(newPage)
pages.add(newPage)
getLinks(newPage)

getLinks("")

将找到的链接保存在集合(set)中,防止链接(不再仅仅是词条)重复。

一开始,用 getLinks 处理一个空 URL,其实是维基百科的主页,因为在函数里空 URL 就 是 http://en.wikipedia.org

关于递归的警告

这个警告在软件开发书籍里很少提到, 但是我觉得你应该注意:如果递归运 行的次数非常多,前面的递归程序就很可能崩溃。

Python 默认的递归限制(程序递归地自我调用次数)是 1000 次。因为维基 百科的网络链接浩如烟海, 所以这个程序达到递归限制后就会停止,除非你 设置一个较大的递归计数器,或用其他手段不让它停止。

对于那些链接深度少于 1000 的“普通”网站,这个方法通常可以正常运行, 一些奇怪的异常除外。 例如,我曾经遇到过一个网站,有一个在生成博文内 链的规则。这个规则是“当前页面把 /blog/title_of_blog.php 加到它后面,作 为本页面的 URL 链接”。 问题是它们可能会把 /blog/title_of_blog.php 加到一个已经有 /blog/ 的 URL 上 面了。因此,网站就多了一个 /blog/。 最后,我的爬虫找到了这样的 URL 链 接: /blog/blog/blog/blog…/blog/title_of_blog.php。

后来, 我增加了一些条件,对可能导致无限循环的部分进行检查,确保那些 URL 不是这么荒谬。但是,如果你不去检查这些问题,爬虫很快就会崩溃。

Requests库

http://pythonscraping.com/pages/files/form.html 可以模拟一个登陆界面(一个最简单的表单)。

这个表单的源代码是:

1
2
3
4
5
<form method="post" action="processing.php">
First name: <input type="text" name="firstname"><br>
Last name: <input type="text" name="lastname"><br>
<input type="submit" value="Submit">
</form>

这里有几点需要注意一下:首先,两个输入字段的名称是 firstname 和 lastname,这一点非常重要。字段的名称决定了表单被确认后要被传送到服务器上的变量名称。如果你想模拟表单提交数据的行为,你就需要保证你的变量名称与字段名称是一一对应的。

注意表单的真实行为其实发生在 processing.php(绝对路径是http://pythonscraping.com/files/processing.php action="processing.php"决定)。表单的任何 POST 请求其实都发生在这个页面上,并非表单本身 所在的页面。 切记: HTML 表单的目的,只是帮助网站的访问者发送格式合理的请求,向服务器请求没有出现的页面。

表单中,最重要的信息是:

  • 提交数据的字段名称 name – name="firstname"
  • 表单提交后网站会显示的页面 action – action="processing.php"

用 Requests 库提交表单只用四行代码就可以实现,包括导入库文件和打印内容的语句:

1
2
3
4
5
import requests

params = {'firstname': 'Ryan', 'lastname': 'Mitchell'}
r = requests.post("http://pythonscraping.com/files/processing.php", data=params)
print(r.text)

处理登录和cookie

大多数新式的网站都用 cookie 跟踪用户是否已登录的状态信息。一旦网站验证了你的登录权证,它就会将它们保存在你的浏览器的 cookie 中, 里面通常包含一个服务器生成的令牌、登录有效时限和状态跟踪信息。 网站会把这个 cookie 当作信息验证的证据,在你浏览网站的每个页面时出示给服务器。

一个简单的登录表单(用户名可 以是任意值,但是密码必须是“password”): http://pythonscraping.com/pages/cookies/login.html 如果在登录网站之前你想进入欢迎页面或者简介页面, 会看到一个错误信息和访问前请先登录的指令。 在简介页面中,网站会检测浏览器的 cookie,看它有没有页面已登录的设置信息。

如果你面对的网站比较复杂,它经常暗自调整 cookie,或者如果你从一开始就完全不想要用 cookie,该怎么处理呢? Requests 库的 session 函数可以完美地解决这些问题:

HTML知识

参考 W3school : http://www.w3school.com.cn/html/index.asp

一个例子:

1
2
3
4
5
6
7
8
9
<html>
<body>

<h1>我的第一个标题</h1>

<p>我的第一个段落。</p>

</body>
</html>
  • <html></html> 之间的文本描述网页
  • <body></body> 之间的文本是可见的页面内容
  • <h1></h1> 之间的文本被显示为标题
  • <p></p> 之间的文本被显示为段落

标题

HTML 标题(Heading)是通过 <h1> - <h6> 等标签进行定义的。

1
<h3>This is a heading</h3>

段落

HTML 段落是通过 <p> 标签进行定义的。

1
<p>This is a paragraph.</p>

链接

HTML 链接是通过 <a> 标签进行定义的。

1
<a href="http://www.w3school.com.cn">This is a link</a>

在 href 属性中指定链接的地址

图像

HTML 图像是通过 <img> 标签进行定义的。

1
<img src="w3school.jpg" width="104" height="142" />

图像的名称和尺寸是以属性的形式提供的

元素

HTML 元素指的是从开始标签(start tag)到结束标签(end tag)的所有代码。

开始标签 元素内容 结束标签
<p> This is a paragraph </p>
<a href="default.htm" > This is a link </a>
<br />

元素的内容是开始标签与结束标签之间的内容

即使忘记了使用结束标签,大多数浏览器也会正确地显示 HTML,但不要依赖这种做法。忘记使用结束标签会产生不可预料的结果或错误

<br> 就是没有关闭标签的空元素(换行),最好用<br />

标签最好用小写

属性

HTML 标签可以拥有属性。属性提供了有关 HTML 元素的更多的信息

属性总是以名称/值对的形式出现,比如:name=”value”

属性总是在 HTML 元素的开始标签中规定。

疑问:

1
2
3
4
html = urlopen("http://www.pythonscraping.com/pages/page1.html")
bsObj = BeautifulSoup(html.read(), "html5lib")
# bsObj = BeautifulSoup(html, "html5lib")
print(bsObj.h1)

两行代码效果相同

BeautifulSoup(html)BeautifulSoup(html.read())有什么区别?


----------over----------


文章标题:Web Scraping with Python

文章作者:Ge垚

发布时间:2018年09月02日 - 21:09

最后更新:2018年09月26日 - 23:09

原始链接:http://geyao1995.com/python_web_scraping/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。