爬虫入门(一)

备注

本篇博客主要参考了图灵程序设计丛书:

  • 《Python网络爬虫权威指南》(Web Scraping with Python, 2E)

作者:【美】瑞安·米切尔

一般步骤

一般人思考网络爬虫时的想法:

  • 通过网站域名获取 HTML 数据
  • 解析数据,获取目标信息
  • 存储目标信息
  • 如果有必要,移动到另一个网页重复这过程

库 BeautifulSoup

功能

BeautifulSoup 库通过定位 HTML 标签来格式化和组织复杂的网页信息,用简单易用的 Python 对象展现 XML 结构信息

安装

直接在相关位置打开终端(最好在虚拟环境中,不容易出错),输入命令

1
pip install beautifulsoup4

运行

BeautifulSoup 库最常用的对象就是BeautifulSoup对象,用一个简单的例子介绍一下

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


url = 'https://www.baidu.com/'
html = urlopen(url)
bs = BeautifulSoup(html.read(), 'html.parser')
print(bs.body)

输出结果

1
2
3
4
5
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>

urlopen函数返回一个http.client.HTTPResponse对象

可以调用.read()方法,把得到的bytes对象传递给BeautifulSoup()的第一个参数markup,以下是BeautifulSoup类的构造函数

1
2
3
def __init__(self, markup="", features=None, builder=None,
parse_only=None, from_encoding=None, exclude_encodings=None,
element_classes=None, **kwargs):

除了文本字符串,BeautifulSoup还可以使用urlopen直接返回的文件对象

1
bs = BeautifulSoup(html, 'html.parser')

BeautifulSoup()的第二个参数,指定了创建该对象的解析器(大多数情况下,解析器的差别都不大)

html.parser 是 Python3 的一个解析器,不需要单独安装
还有 lxml 解析器、html5lib 解析器等等

异常处理

urlopen产生的异常

1
html = urlopen(url)

这类代码可能会发生两种异常:

  • 网页不在服务器上存在 —— HTTPError
  • 服务器不存在 —— URLError

可以嵌套try/except/else语句捕获并处理异常

bs.nonExistenTag产生的异常

nonExistenTag代表不存在的标签

如果输出它,即

1
print(bs.nonExistenTag)

会返回一个None对象,所以处理并检查该对象很重要

如果直接调用了这个None对象的子标签

1
print(bs.nonExistenTag.someTag)

这时会抛出一个异常

1
AttributeError: 'NoneType' object has no attribute 'someTag'

简单来说,处理这类错误,可以对这两种情况进行检查,捕获AttributeError异常

示例代码

总结一下上述情况的处理方法,整合为以下代码

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


def get_title(url):
try:
html = urlopen(url)
except HTTPError as e:
return None
try:
bs = BeautifulSoup(html.read(), 'html.parser')
title = bs.body.h1
except AttributeError as e:
return None
return title


if __name__ == '__main__':
url = 'https://www.baidu.com/'
title = get_title(url)
if title is None:
print('Title could not be found.')
else:
print(title)

复杂 HTML 解析

BeautifulSoup 的 find()find_all()

我们知道,CSS 会让 HTML 元素呈现差异化

网络爬虫就可以通过各种属性(如id, class)提取网页部分信息

比如,提取class="post-footer"footer标签内的所有内容

1
2
3
4
5
html = urlopen(url)
bs = BeautifulSoup(html.read(), 'html.parser')
footer = bs.find_all('footer', {'class': 'post-footer'})
for item in footer:
print(item.get_text())

接下来看一下find()find_all()的部分源码

1
2
3
4
5
def find_all(self, name=None, attrs={}, recursive=True, text=None,
limit=None, **kwargs):

def find(self, name=None, attrs={}, recursive=True, text=None,
**kwargs):

不难看出,实际上find()函数就是find_all()函数limit=1的情况

解释一下find_all()函数的所有参数:

  • name: 标签名,传入字符串
  • attrs: 属性参数,以字典形式传入
  • recursive: 递归参数,默认为True
    • 若为True: 则会查找标签参数的所有子标签
    • 若为False: 就只查找文档的一级标签
  • text: 文本参数,用标签的文本内容去匹配
  • limit: 设定获取的结果项数
  • **kwargs: 关键字参数,可以选择具有指定属性的标签
    • title = bs.find_all(id='title', class_='text')
    • 注意类属性为class_

根据 DOM 树操作

DOM 是 HTML 文档的编程接口,HTML 页面可以映射成一棵树

基于此,BeautifulSoup提供了操作各种标签的方法

  • 子标签 bs.curTag.children
  • 下一个兄弟标签:bs.curTag.next_siblings
  • 上一个兄弟标签:bs.curTag.previous_siblings
  • 父标签:bs.curTag.parent

可以连着使用,如

1
bs.find('img', {'src': 'pic/img1.jpg'}).parent.previous_sibling

正则表达式

常用正则表达式符号

符号 含义
* 匹配前面的字符0-inf次
+ 匹配前面的字符1-inf次
[] 匹配括号里任意一个字符
() 表达式编组(优先运行)
{m,n} 匹配前面的字符 m 到 n 次
[^] 匹配任意一个不在括号内的字符
| 匹配任意一个由竖线分割的字符,例如 b(a|e)d
. 匹配任意单个字符
^ 指字符串开始位置的字符或表达式,如 ^a
\ 转义字符
$ 从字符串末尾匹配
?! 不包含

例如邮箱

[A-Za-z0-9\._+]+@[A-Za-z]+\.(com|org|edu|net)

与 BeautifulSoup 结合起来使用

比如,我们要抓取所有 bilibili 网站主页上的内链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from bs4 import BeautifulSoup
import re


if __name__ == '__main__':
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
url = 'https://www.bilibili.com/'
html = requests.get(url=url, headers=headers)
bs = BeautifulSoup(html.text, 'html.parser')
links = bs.find_all('a', {'href': re.compile('//www.bilibili.com/.*?')})
for link in links:
print(link['href'])

获取属性

对于标签对象,获取其全部属性,可以这样写

1
myTag.attrs

上述代码返回的是一个 Python 字典,那么要得到某张图片的源位置,就可以这样写

1
myImgTag.attrs['src']

Lambda 表达式

BeautifulSoup 允许我们把特定类型的函数作为参数传入 find_all()函数

唯一限制条件就是这些传入的函数必须把一个标签对象作为参数,并且返回布尔类型的结果,类似一个过滤器

如,找出带有两个属性的所有标签

1
bs.find_all(lambda tag: len(tag.attrs) == 2)

编写网络爬虫

遍历单个域名

我们假设有这样一个需求:创建一个项目要来实现“百度百科六度分隔理论”的查找方法

网站 https://baike.baidu.com/item/%E5%85%AD%E5%BA%A6%E5%88%86%E9%9A%94

检查该网站,寻找指向其他词条页面的连接,可以发现:

  • 它们都在class="main-content"div标签里
  • URL 都以/item/开头

我们可以利用这两个规律写出匹配词条链接的正则表达式

^(/item/).*?$

定义函数get_links()来获取所有链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pages = set()


def get_links(page_url):
global pages
html = urlopen('https://baike.baidu.com{}'.format(page_url))
bs = BeautifulSoup(html, 'html.parser')
_links = bs.find('div', {'class': 'main-content'}).find_all(
'a', href=re.compile('^(/item/).*?$'))
for link in _links:
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
new_page = link.attrs['href']
print('https://baike.baidu.com' + new_page)
pages.add(new_page)
get_links(new_page)

最后在主函数中调用get_links()输出到控制台

1
get_links('/item/%E5%85%AD%E5%BA%A6%E5%88%86%E9%9A%94')

就可以看到类似如下的结果

1
2
3
4
5
6
https://baike.baidu.com/item/%E5%85%AD%E5%BA%A6%E5%88%86%E9%9A%94%E5%81%87%E8%AF%B4
https://baike.baidu.com/item/%E5%85%AD%E5%BA%A6%E5%88%86%E9%9A%94
https://baike.baidu.com/item/150%E5%AE%9A%E5%BE%8B
https://baike.baidu.com/item/%E8%8B%B1%E5%9B%BD%E7%89%9B%E6%B4%A5%E5%A4%A7%E5%AD%A6/4693743
https://baike.baidu.com/item/%E7%A0%94%E7%A9%B6%E5%9E%8B%E5%A4%A7%E5%AD%A6/1464251
https://baike.baidu.com/item/985%E5%B7%A5%E7%A8%8B