0%

Python网络数据采集

Python网络数据采集

我最近一时兴起,想学一学爬虫,于是选择了这本书

通过这几天的学习,我的爬虫算是入门了,去网上爬一爬网图应该没有问题

但我暂时不打算继续深入了(后面的知识就比较专业了),这里就记录一下我的学习经过……


思考网络爬虫时通常的想法

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

PS:MAC地址

MAC(Media Access Control,介质访问控制),或称为物理地址,MAC位址,硬件位址,用来定义网络设备的位置

  • 第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 位址
  • 因此一个主机会有一个IP地址,而每个网络位置会有一个专属于它的 MAC 位址
  • MAC 地址,用来表示互联网上每一个站点的标识符,采用十六进制数表示,共六个字节(48位)
  • 前三个字节是由 IEEE 的注册管理机构 RA 负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier)
  • 后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)
  • 一个地址块可以生成2^24个不同的地址
  • MAC地址实际上就是适配器地址或适配器标识符EUI-48

MAC地址具有唯一性,它是雕在硬件设备上的(不能更改),生产厂家一生产就会把它分配给每个机器

Cookie是保存在客户端的纯文本文件(比如txt文件)

所谓的客户端就是我们自己的本地电脑,当我们使用自己的电脑通过浏览器进行访问网页的时候,服务器就会生成一个证书并返回给我的浏览器并写入我们的本地电脑,这个证书就是Cookie

HTTP协议本身是无状态的(即服务器无法判断用户身份),客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器颁发一个Cookie,客户端浏览器会把Cookie保存起来,当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器(添加到请求头中),服务器检查该Cookie,以此来辨认用户状态

属性项 属性项介绍
NAME=VALUE 键值对,可以设置要保存的 Key/Value,注意这里的 NAME 不能和其他属性项的名字一样
Expires 过期时间,在设置的某个时间点后该 Cookie 就会失效
Domain 生成该 Cookie 的域名,如 domain=”www.baidu.com
Path 该 Cookie 是在当前的哪个路径下生成的,如 path=/wp-admin/
Secure 如果设置了这个属性,那么只会在 SSH 连接时才会回传该 Cookie

PS:Session

Session是服务器为了保存用户状态而创建的一个特殊的对象(Session对象,用于储存特定的用户会话所需的信息)

当浏览器第一次访问服务器时,服务器创建一个 Session对象(该对象有一个唯一的ID,一般称之为SessionID),服务器会将 SessionID 以 Cookie 的方式发送给浏览器,当浏览器再次访问服务器时,会将 SessionID 发送过来,服务器依据 SessionID 就可以找到对应的 Session对象

服务器会向客户发送一个名为 JSESSIONID 的 Cookie ,它的值为该Session的ID,用于判断是否为同一用户

PS:Token

Token,又称令牌,其实就是一种验证机制(和Cookie-Session效果类似)

用户请求登录时,服务器后端会生成一个 Token 存储于 Redis 数据库中(临时存放),然后把该 Token 返回到前端最终交给客户端,接下来用户再次请求时便会携带 Token 发送给服务器,服务器再去效验 Token 的正确性

网络浏览器&爬虫原理

网络浏览器是一个非常有用的应用,它创建信息的数据包,发送它们,然后把你获取的数据解释成漂亮的图像、声音、视频和文字

但是,网络浏览器就是代码,而代码是可以分解的,可以分解成许多基本组件,可重写、重用,以及做成我们想要的任何东西

网络浏览器可以让服务器发送一些数据,到那些对接无线(或有线)网络接口的应用上, 但是许多语言也都有实现这些功能的库文件

也就是说,其他的编程语言也可以模仿浏览器的行为,从而向服务器请求数据,这便是网络爬虫的原理

让我们看看 Python 是如何实现的:

1
2
3
4
from urllib.request import urlopen

html = urlopen("https://ywhkkx.github.io/")
print(html.read())
1
2
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
b'<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=......script src="/js/next-boot.js"></script>\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n \n\n</body>\n</html>\n'

这将会输出 https://ywhkkx.github.io/ 这个网页的全部 HTML 代码,更准确地说,这会输出在域名为 ywhkkx.github.io 的 github 服务器上文件夹里的 HTML 文件的源代码

现在大多数网页需要加载许多相关的资源文件:可能是图像文件、JavaScript 文件、CSS 文件,或你需要连接的其他各种网页内容

当网络浏览器遇到一个标签时,比如 <img src="cuteKitten.jpg"> ,会向服务器发起另一个请求,以获取 cuteKitten.jpg 文件中的数据为用户充分渲染网页,但是,我们的 Python 程序没有返回并向服务器请求多个文件的逻辑,它只能读取我们已经请求的单个 HTML 文件,不过 Python 有对付的办法:

1
from urllib.request import urlopen

urllib 是 Python 的标准库,包含了从网络请求数据,处理 cookie,甚至改变像请求头和用户代理这些元数据的方法

urlopen 用来打开并读取一个从网络获取的远程对象,因为它是一个非常通用的库(它可以轻松读取 HTML 文件、图像文件,或其他任何文件流),所以我们将在本书中频繁地使用它

BeautifulSoup简介

BeautifulSoup 尝试化平淡为神奇,它通过定位 HTML 标签来格式化和组织复杂的网络信息,用简单易用的 Python 对象为我们展现 XML 结构信息

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

html = urlopen("https://ywhkkx.github.io/")
bsObj = BeautifulSoup(html.read(),"html.parser")
print(bsObj.h1) # 这里只要求显示"h1"
1
2
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
<h1 class="site-title">Pwn进你的心</h1>

可以发现 BeautifulSoup 把原本的“杂乱无章”变成了“井井有条”

和前面例子一样,我们导入 urlopen,然后调用 html.read() 获取网页的 HTML 内容,这样就可以把 HTML 内容传到 BeautifulSoup 对象,转换成更加易读的结构,其实,任何 HTML(或 XML)文件的任意节点信息都可以被提取出来,只要目标信息的旁边或附近有标记就行

“可靠”的网络连接

网络是十分复杂的,网页数据格式不友好,网站服务器宕机,目标数据的标签找不到,都是很麻烦的事情

让我们看看爬虫 import 语句后面的第一行代码,如何处理那里可能出现的异常:

1
html = urlopen("https://ywhkkx.github.io/")

这行代码主要可能会发生两种异常:

  • 网页在服务器上不存在(或者获取页面的时候出现错误)
  • 服务器不存在

第一种异常发生时,程序会返回 HTTP 错误,HTTP 错误可能是 “404 Page Not Found” ,“500 Internal Server Error” 等,所有类似情形,urlopen 方法抛出“HTTPError”异常,我们可以用下面的方式处理这种异常:

1
2
3
4
5
try:
html = urlopen("https://ywhkkx.github.io/")
except HTTPError as e:
print(e)
else:

第一种异常发生时(服务器不存在),urlopen 会返回一个 None 对象,这个对象与其他编程语言中的 null 类似,我们可以增加一个判断语句检测返回的 html 是不是 None

1
2
3
if html is None:
print("URL is not found")
else:

当然,即使网页已经从服务器成功获取,如果网页上的内容并非完全是我们期望的那样,仍然可能会出现异常,每当你调用 BeautifulSoup 对象里的一个标签时,增加一个检查条件保证标签确实存在是很聪明的做法

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

比如下段代码:(nonExistentTag 是虚拟的标签,BeautifulSoup 对象里实际没有)

1
print(bsObj.nonExistentTag.someTag) 
1
AttributeError: 'NoneType' object has no attribute 'someTag'

那么我们怎么才能避免这两种情形的异常呢?最简单的方式就是对两种情形进行检查:

1
2
3
4
5
6
7
8
9
try:
badContent = bsObj.nonExistingTag.anotherTag
except AttributeError as e:
print("Tag was not found")
else:
if badContent == None:
print ("Tag was not found")
else:
print(badContent)

初看这些检查与错误处理的代码会觉得有点儿累赘,但是,我们可以重新简单组织一下代码,让它变得不那么难写(更重要的是,不那么难读),例如,下面的代码是上面爬虫的另一种写法:

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

def getTitle(url):
try:
html = urlopen(url) # 尝试获取html
except HTTPError as e: # 如果发生"HTTP错误"则返回None
return None
try:
bsObj = BeautifulSoup(html.read(),"html.parser") # 尝试利用BeautifulSoup解析html
title = bsObj.body.h1
except AttributeError as e: # 如果"没有获取到目标标签"则返回None
return None
return title

title = getTitle("https://ywhkkx.github.io/")
if title == None:
print("Title could not be found")
else:
print(title)
1
2
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
<h1 class="site-title">Pwn进你的心</h1>

在写爬虫的时候,思考代码的总体格局,让代码既可以捕捉异常又容易阅读,这是很重要的,如果你还希望能够很大程度地重用代码,那么拥有像 getSiteHTML 和 getTitle 这样的 通用函数(具有周密的异常处理功能)会让快速稳定地网络数据采集变得简单易行

通过属性查找标签

每个网站都会有层叠样式表(Cascading Style Sheet,CSS)

CSS 是专门为了让浏览器和人类可以理解网站内容而设计一个展现样式的层,但是 CSS 的发明却是网络爬虫的福音

CSS 可以让 HTML 元素呈现出差异化,使那些具有完全相同修饰的元素呈现出不同的样式,比如,有一些标签看起来是这样:

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

网络爬虫可以通过 class 属性的值,轻松地区分出两种不同的标签,如果我们想根据 class 属性来爬取需要的内容,就可以用到 findAll 方法

先看看目标网页:https://www.pythonscraping.com/pages/warandpeace.html

它的 html 结构很是简单,寻找 class 很是方便,我们可以用以下代码来爬取绿色字体的内容:

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

html = urlopen("https://www.pythonscraping.com/pages/warandpeace.html")
bsObj = BeautifulSoup(html,"html.parser")

namelist = bsObj.findAll("span",{"class":"green"}) # Python字典 - {"A":"B"}
for name in namelist:
print(name.get_text()) # get_text():把你正在处理的HTML文档中所有的标签都清除,返回一个只包含文字的字符串
1
2
3
4
5
6
7
8
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
Anna
Pavlovna Scherer
Empress Marya
Fedorovna
Prince Vasili Kuragin
Anna Pavlovna
......

BeautifulSoup 里的 find() 和 findAll() 可能是你最常用的两个方法,借助它们,你可以通过标签的不同属性轻松地过滤 HTML 页面,查找需要的标签组或单个标签

1
2
findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)
  • tag:标签参数,可以传一个标签的名称或多个标签名称组成的Python列表做标签参数
  • attributes:属性参数,用一个Python字典封装一个标签的若干属性和对应的属性值
  • recursive:递归参数,这是一个布尔变量,设置为True,表示去查找目标标签中所有的子标签,设置为False,表示只查找一级标签(recursive默认为True)
  • text:文本参数,查找文本的内容(它是用标签的文本内容去匹配,而不是用标签的属性)
1
2
nameList = bsObj.findAll(text="the prince") # 查找"the prince",返回所有的目标
print(len(nameList)) # 结果为"7"
  • limit:限制参数,只用于 findAll 方法,find 其实等价于 findAll 的 limit 等于 1 时的情形,如果你只对网页中获取的前 x 项结果感兴趣,就可以设置它,但是要注意,这个参数设置之后,获得的前几项结果是按照网页上的顺序排序的,未必是你想要的那前几项
  • keyword:关键词参数,让你选择那些具有指定属性的标签(这是一个冗余的参数)
1
2
allText = bsObj.findAll(id="text")
print(allText[0].get_text())

PS:面向对象

面向对象(Object Oriented)是软件开发方法,一种编程范式,面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物

通过面向对象的方式,将现实世界的事物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模

面向对象是在结构化设计方法出现很多问题的情况下应运而生的

PS:结构化设计方法求解问题的基本策略是 从功能的角度审视问题域 ,它将应用程序看成实现某些特定任务的 功能模块 ,其中子过程是实现某项具体操作的底层功能模块,在每个功能模块中,用 数据结构描述待处理数据的组织形式,用算法描述具体的操作过程(把C语言的函数往里面套,可以方便理解)

面向对象的几大要数:

一,审视问题域的视角

  • 在现实世界中存在的客体是问题域中的主角(所谓客体是指客观存在的对象实体和主观抽象的概念),在自然界,每个客体都具有一些 属性和行为 ,因此, 每个个体都可以用属性和行为来描述(比如:IO_FILE文件系统中,就是用FILE结构体来描述整个文件)
  • 结构化设计方法所采用的设计思路不是将客体作为一个整体,而是将依附于客体之上的行为抽取出来,以功能为目标来设计构造应用系统

二,抽象级别

  • 抽象主要包括过程抽象和数据抽象(结构化设计方法应用的是过程抽象)
  • 过程抽象:是将问题域中具有明确功能定义的操作抽取出来,并将其作为一个实体看待(比如C语言的函数)
  • 数据抽象:数据抽象是较过程抽象更高级别的抽象方式,将描述客体的属性和行为绑定在一起,实现统一的抽象,从而达到对现实世界客体的真正模拟(说实话,这句话讲的也挺抽象的,不过看看上文中“BeautifulSoup”方法返回的那个“bsObj”,Python支持直接在它的身上使用函数,那么就可以把“bsObj”理解为某种抽象了)

三,封装体

  • 封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内, 该逻辑单元负责将所描述的属性隐藏起来,外界对客体内部属性的所有访问只能通过提供的用户接口实现(还是上文中提及的那个“bsObj”,它就是一个分装体)
  • 结构化设计方法没有做到客体的整体封装,只是封装了各个功能模块,而每个功能模块可以随意地对没有保护能力客体属性实施操作,并且由于描述属性的数据与行为被分割开来,所以一旦某个客体属性的表达方式发生了变化,或某个行为效果发生了改变,就有可能对整个系统产生影响(想想C语言的函数,好像的确是这样)

四,可重用性

  • 可重用性标识着软件产品的可复用能力,是衡量一个软件产品成功与否的重要标志

面向对象的几大概念:

  • 对象:对象所指的是计算机系统中的某一个成分,它有两个含义:其中一个是数据,另外一个是动作,可以说对象则是数据和动作的结合体,对象不仅能够进行操作,同时还能够及时记录下操作结果
  • 方法:方法是指对象能够进行的操作(方法同时还有另外一个名称:函数),方法是类中的定义函数,其具体的作用就是对对象进行描述操作
  • 继承:继承简单地说就是一种层次模型,这种层次模型能够被重用,层次结构的上层具有通用性,但是下层结构则具有特殊性,在继承的过程中类则可以从最顶层的部分继承一些方法和变量(继承是从一般演绎到特殊的过程,可以减少知识表示的冗余内容,知识库的维护和修正都非常方便,更有利于衍生复杂的系统)
  • 类:类是具有相同特性(数据元素)和行为(功能)的对象的抽象(因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象),类实际上就是一种数据类型,类具有属性,它是对象的状态的抽象,用数据结构来描述类的属性
  • 封装:封装是将数据和代码捆绑到一起,对象的某些数据和代码可以是私有的,不能被外界访问,以此实现对数据和代码不同级别的访问权限
  • 多态:多态是指不同事物具有不同表现形式的能力,多态机制使具有不同内部结构的对象可以共享相同的外部接口,通过这种方式减少代码的复杂度(一个接口,多种方式)
  • 动态绑定:动态绑定指的是将一个过程调用与相应代码链接起来的行为,动态绑定是指与给定的过程调用相关联的代码只有在运行期才可知的一种绑定,它是多态实现的具体形式
  • 消息传递:对象之间需要相互沟通,沟通的途径就是对象之间收发信息,消息内容包括接收消息的对象的标识,需要调用的函数的标识,以及必要的信息,消息传递的概念使得对现实世界的描述更容易

导航树

HTML 页面可以映射成一棵树

在 BeautifulSoup 库里,孩子(child)和后代(descendant)有显著的不同:和人类的家谱一样,子标签就是一个父标签的下一级,而后代标签是指一个父标签下面所有级别的标签

一般情况下,BeautifulSoup 函数总是处理当前标签的后代标签(例如,bsObj.body.h1 选择了 body 标签后代里的第一个 h1 标签,不会去找 body 外面的标签)

接下来介绍几个标签处理的方法:

如果你只想找出子标签,可以用 .children(程序默认选择 .children)

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,"html.parser")

for child in bsObj.find("table",{"id":"giftList"}).children:
print(child)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py


<tr><th> /* 打印<tr>(内容为<th>) */
Item Title
</th><th>
Description
</th><th>
Cost
</th><th>
Image
</th></tr>


<tr class="gift" id="gift1"><td> /* 打印<tr>(内容为<td>) */
Vegetable Basket
</td><td>
This vegetable basket is the perfect gift for your health conscious (or overweight) friends!
<span class="excitingNote">Now with super-colorful bell peppers!</span>
</td><td>
$15.00
</td><td>
<img src="../img/gifts/img1.jpg"/>
</td></tr>


<tr class="gift" id="gift2"><td>
Russian Nesting Dolls
</td><td>
Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span>
</td><td>
$10,000.52
</td><td>
<img src="../img/gifts/img2.jpg"/>
</td></tr>


<tr class="gift" id="gift3"><td>
Fish Painting
</td><td>
If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span>
</td><td>
$10,005.00
</td><td>
<img src="../img/gifts/img3.jpg"/>
</td></tr>


<tr class="gift" id="gift4"><td>
Dead Parrot
</td><td>
This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span>
</td><td>
$0.50
</td><td>
<img src="../img/gifts/img4.jpg"/>
</td></tr>


<tr class="gift" id="gift5"><td>
Mystery Box
</td><td>
If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span>
</td><td>
$1.50
</td><td>
<img src="../img/gifts/img6.jpg"/>
</td></tr>



进程已结束,退出代码 0

直观来看,下图中的标签就是“giftList”的子标签,程序就打印了对应几个 <tr> 框架

采用 .descendants,则可以打印所有后代标签

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,"html.parser")

for child in bsObj.find("table",{"id":"giftList"}).descendants:
print(child)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py


<tr><th> /* 打印<tr> */
Item Title
</th><th>
Description
</th><th>
Cost
</th><th>
Image
</th></tr>
<th> /* 打印<th> */
Item Title
</th>

Item Title /* 打印<th>框架中的内容 */

<th>
Description
</th>

Description

<th>
Cost
</th>

Cost

<th>
Image
</th>

Image



<tr class="gift" id="gift1"><td> /* 打印<tr> */
Vegetable Basket
</td><td>
This vegetable basket is the perfect gift for your health conscious (or overweight) friends!
<span class="excitingNote">Now with super-colorful bell peppers!</span>
</td><td>
$15.00
</td><td>
<img src="../img/gifts/img1.jpg"/>
</td></tr>
<td> /* 打印<td> */
Vegetable Basket
</td>

Vegetable Basket /* 打印<td>框架中的内容 */

<td> /* 打印<td> */
This vegetable basket is the perfect gift for your health conscious (or overweight) friends!
<span class="excitingNote">Now with super-colorful bell peppers!</span>
</td>

This vegetable basket is the perfect gift for your health conscious (or overweight) friends! /* 打印<td>框架中的内容 */

<span class="excitingNote">Now with super-colorful bell peppers!</span>
Now with super-colorful bell peppers!


<td> /* 打印<td> */
$15.00 /* 打印<td>框架中的内容 */
</td>

$15.00

<td>
<img src="../img/gifts/img1.jpg"/>
</td>


<img src="../img/gifts/img1.jpg"/>




<tr class="gift" id="gift2"><td>
Russian Nesting Dolls
</td><td>
Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span>
</td><td>
$10,000.52
</td><td>
<img src="../img/gifts/img2.jpg"/>
</td></tr>
<td>
Russian Nesting Dolls
</td>

Russian Nesting Dolls

<td>
Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span>
</td>

Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"!
<span class="excitingNote">8 entire dolls per set! Octuple the presents!</span>
8 entire dolls per set! Octuple the presents!


<td>
$10,000.52
</td>

$10,000.52

<td>
<img src="../img/gifts/img2.jpg"/>
</td>


<img src="../img/gifts/img2.jpg"/>




<tr class="gift" id="gift3"><td>
Fish Painting
</td><td>
If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span>
</td><td>
$10,005.00
</td><td>
<img src="../img/gifts/img3.jpg"/>
</td></tr>
<td>
Fish Painting
</td>

Fish Painting

<td>
If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span>
</td>

If something seems fishy about this painting, it's because it's a fish!
<span class="excitingNote">Also hand-painted by trained monkeys!</span>
Also hand-painted by trained monkeys!


<td>
$10,005.00
</td>

$10,005.00

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


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




<tr class="gift" id="gift4"><td>
Dead Parrot
</td><td>
This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span>
</td><td>
$0.50
</td><td>
<img src="../img/gifts/img4.jpg"/>
</td></tr>
<td>
Dead Parrot
</td>

Dead Parrot

<td>
This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span>
</td>

This is an ex-parrot!
<span class="excitingNote">Or maybe he's only resting?</span>
Or maybe he's only resting?


<td>
$0.50
</td>

$0.50

<td>
<img src="../img/gifts/img4.jpg"/>
</td>


<img src="../img/gifts/img4.jpg"/>




<tr class="gift" id="gift5"><td>
Mystery Box
</td><td>
If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span>
</td><td>
$1.50
</td><td>
<img src="../img/gifts/img6.jpg"/>
</td></tr>
<td>
Mystery Box
</td>

Mystery Box

<td>
If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span>
</td>

If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining.
<span class="excitingNote">Keep your friends guessing!</span>
Keep your friends guessing!


<td>
$1.50
</td>

$1.50

<td>
<img src="../img/gifts/img6.jpg"/>
</td>


<img src="../img/gifts/img6.jpg"/>





进程已结束,退出代码 0

可以发现,程序不仅打印了对应几个 <tr> 框架,并且把其后代标签的框架与内容也打印了出来

处理兄弟标签

BeautifulSoup 的 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,"html.parser")

for sibling in bsObj.find("table",{"id":"giftList"}).tr.next_siblings:
print(sibling)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py


<tr class="gift" id="gift1"><td>
Vegetable Basket
</td><td>
This vegetable basket is the perfect gift for your health conscious (or overweight) friends!
<span class="excitingNote">Now with super-colorful bell peppers!</span>
</td><td>
$15.00
</td><td>
<img src="../img/gifts/img1.jpg"/>
</td></tr>


<tr class="gift" id="gift2"><td>
Russian Nesting Dolls
</td><td>
Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span>
</td><td>
$10,000.52
</td><td>
<img src="../img/gifts/img2.jpg"/>
</td></tr>


<tr class="gift" id="gift3"><td>
Fish Painting
</td><td>
If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span>
</td><td>
$10,005.00
</td><td>
<img src="../img/gifts/img3.jpg"/>
</td></tr>


<tr class="gift" id="gift4"><td>
Dead Parrot
</td><td>
This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span>
</td><td>
$0.50
</td><td>
<img src="../img/gifts/img4.jpg"/>
</td></tr>


<tr class="gift" id="gift5"><td>
Mystery Box
</td><td>
If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span>
</td><td>
$1.50
</td><td>
<img src="../img/gifts/img6.jpg"/>
</td></tr>



进程已结束,退出代码 0

这段代码会打印产品列表里的所有行的产品,第一行表格标题除外

  • 首先,对象不能把自己作为兄弟标签,任何时候你获取一个标签的兄弟标签,都不会包含这个标签本身
  • 其次,这个函数只调用后面的兄弟标签(例如,如果我们选择一组标签中位于中间位置的一个标签,然后用 next_siblings() 函数,那么它就只会返回在它后面的兄弟标签)

因此,选择标签行然后调用 next_siblings,可以选择表格中除了标题行以外的所有行

处理父标签

在抓取网页的时候,查找父标签的需求比查找子标签和兄弟标签要少很多

通常情况 下,如果以抓取网页内容为目的来观察 HTML 页面,我们都是从最上层标签开始的,然 后思考如何定位我们想要的数据块所在的位置,但是,偶尔在特殊情况下你也会用到 BeautifulSoup 的父标签查找函数,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,"html.parser")

print(bsObj.find("img",{"src":"../img/gifts/img1.jpg"}).parent.previous_sibling.get_text())
1
2
3
4
5
6
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py

$15.00


进程已结束,退出代码 0

这段代码会打印 ../img/gifts/img1.jpg 这个图片对应商品的价格(previousSibling属性返回:同一树层级中指定节点的前一个节点,刚好就是商品的价格)

PS:正则表达式

正则表达式,可以识别正则字符串(regular string),也就是说,它们可以这么定义:“如果你给我的字符串符合规则,我就返回它,或者是如果字符串不符合规则,我就忽略它”

正则表达式用于匹配一个符合规则的字符串,这在要求快速浏览大文档,以查找像电话号码和邮箱地址之类的字符串时是非常方便的

正则字符串,其实就是任意可以用一系列线性规则构成的字符串:

  • 字母“a”至少出现一次
  • 后面跟着字母“b”重复 5 次
  • 后面再跟字母“c”重复任意偶数次
  • 最后一位是字母“d”,也可以没有

满足上面规则的字符串有:“aaaabbbbbccccd”,“aabbbbbcc”等(有无穷多种变化),而正则表达式就是表达这组规则的缩写,这组规则的正则表达式如下所示:

1
aa*bbbbb(cc)*(d|)
  • aa :a 后面跟着的 `a` 表示“重复任意次 a,包括 0 次”(意思就是a重复任意a次)
  • bbbbb :这没有什么特别的(就是重复 5 次 b)
  • (cc)* :任意偶数个字符都可以编组,这个规则是用括号两个 c,然后后面跟一个星号,表示有 任意次两个 c(也可以是 0 次)
  • (d|) :增加一个竖线(|)在表达式里表示“或”,本例是表示:增加一个后面跟着“\x00”的 d,或者只有一个“\x00”(这样就可以保证字符串结尾为“d”或者“\x00”)

常见的正则表达式:

  • 限定符

    • ?(问号):表示符号前的字符需要出现0次或1次
    • *(星号):表示符号前的字符可以出现0次或多次
    • +(加号):表示符号前的字符可以出现1次或多次
    • {n,m}(花括号):表示符号前面的字符需要出现n~m次
    • 注意:当需要对多个字符进行操作时,可以先把字符括起来,然后在后面添加限定符
  • 或运算符

    • a (n|m):程序会先去匹配“a+空格”,然后要么匹配“n”,要么匹配“m”
  • 字符类

    • [ … ](方括号):方括号中的内容可以很灵活,可以是单个字母,也可以用 “-” 来指定范围
    • ^ (尖号,脱字符):只能在方括号内部使用,表示把所写入的内容除外
  • 元字符

    • \d :代表数字字符(相当于[0 - 9])
    • \w :代表单词字符
    • \s :代表空白字符(包括Tab字符,换行字符)
    • \D :代表非数字字符(相当于[ ^ 0 - 9])
    • \W :代表非单词字符
    • \S :代表非空白字符(包括Tab字符,换行字符)
    • .(句点):代表不包含换行字符的任意字符
    • ^ n:匹配行首的字符“n”
    • n $:匹配行尾的字符“n”

正则表达式中的贪婪匹配和懒惰匹配:

  • 贪婪匹配:“ * ”,“ + ”,“ {} ”,都是默认采用贪婪匹配,它们会尽可能多的匹配字符
  • 懒惰匹配:如果在“ * ”,“ + ”,“ {} ”的后面加“ ? ”,就可以把它们切换为懒惰匹配,它们会尽可能少的匹配字符

通过正则表达式查找标签

在本例中,我们直接通过商品图片的文件路径来查找:

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,"html.parser")
images = bsObj.findAll("img",{"src":re.compile("\.\.\/img\/gifts/img.*\.jpg")})

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

re.compile(),是用来优化正则的,它将正则表达式转化为对象

1
2
3
4
5
6
7
8
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg

进程已结束,退出代码 0

现在解释一下这个正则表达式的内容:

1
re.compile("\.\.\/img\/gifts/img.*\.jpg")

首先用转义符号索引两个“ . ”,然后用转义符号索引“/img”,“/gifts”,“/img”(其实这里的转义符号可以不加,因为“/”没有什么特殊含义),“ .* ”代表任意字符出现0次或者多次(这里改成“+”也没有什么问题),最后索引“.jpg”

获取标签的属性

在网络数据采集时你经常不需要查找标签的内容,而是需要查找标签属性,对于一个标签对象,可以用下面的代码获取它的全部属性:(要注意这行代码返回的是一个 Python 字典对象,可以获取和操作这些属性)

1
myTag.attrs
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,"html.parser")

print(bsObj.img.attrs)
1
2
3
4
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
{'src': '../img/gifts/logo.jpg', 'style': 'float:left;'}

进程已结束,退出代码 0

要获取图片的资源位置 src,可以用下面这行代码:

1
myImgTag.attrs["src"]
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,"html.parser")

print(bsObj.img.attrs["src"])
1
2
3
4
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
../img/gifts/logo.jpg

进程已结束,退出代码 0

遍历单个域名

我们需要先进行一个游戏:维基百科六度分隔理论,是把两个不相干的主题用一个总数不超过六条的主题连接起来

我们将创建一个项目来实现“维基百科六度分隔理论”的查找方法,要实现从 埃里克· 艾德尔 的词条页面(https://en.wikipedia.org/wiki/Eric_Idle)开始,经过最少的链接点击次数找到 凯文· 贝肯 的词条页面(https://en.wikipedia.org/wiki/Kevin_Bacon

首先进行页面分析:

鼠标移动到某个词条页面上时,对应的 HTML 代码会高亮,由此可以大致锁定词条页面对应的 HTML 标签(<a>),可以用以下代码来收集所有的 <a> 标签:

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

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

for link in bsObj.findAll("a"):
if 'href' in link.attrs:
print(link.attrs['href'])

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
26
27
28
29
30
31
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
/wiki/Wikipedia:Protection_policy#semi
#mw-head
#searchInput
/wiki/Kevin_Bacon_(disambiguation)
/wiki/File:Kevin_Bacon_SDCC_2014.jpg
/wiki/Philadelphia,_Pennsylvania
/wiki/Kevin_Bacon_filmography
/wiki/Kyra_Sedgwick
/wiki/Sosie_Bacon
#cite_note-1
/wiki/Edmund_Bacon_(architect)
/wiki/Michael_Bacon_(musician)

.................................

//foundation.wikimedia.org/wiki/Terms_of_Use
//foundation.wikimedia.org/wiki/Privacy_policy
//www.wikimediafoundation.org/
https://foundation.wikimedia.org/wiki/Privacy_policy
/wiki/Wikipedia:About
/wiki/Wikipedia:General_disclaimer
//en.wikipedia.org/wiki/Wikipedia:Contact_us
//en.m.wikipedia.org/w/index.php?title=Kevin_Bacon&mobileaction=toggle_view_mobile
https://www.mediawiki.org/wiki/Special:MyLanguage/How_to_contribute
https://stats.wikimedia.org/#/en.wikipedia.org
https://foundation.wikimedia.org/wiki/Cookie_statement
https://wikimediafoundation.org/
https://www.mediawiki.org/

进程已结束,退出代码 0

发现有一些条目不是我们需要的内容(这里只展示部分),现在要近一步对样本进行分析:

首先需要比较“词条链接”和“其他链接”的差异,发现“词条链接”有3个共同点:

  • 它们都在 id 是 bodyContent 的 div 标签里(利用 find 嵌套可以解决)
  • 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, "html.parser")

for link in bsObj.find("div", {"id":"bodyContent"}).findAll("a",href=re.compile("^(/wiki/)((?!:).)*$")):
if 'href' in link.attrs:
print(link.attrs['href'])
1
2
3
4
5
6
7
8
9
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
/wiki/Kevin_Bacon_(disambiguation)
/wiki/Philadelphia,_Pennsylvania
/wiki/Kevin_Bacon_filmography
/wiki/Kyra_Sedgwick
/wiki/Sosie_Bacon
/wiki/Edmund_Bacon_(architect)

.................................

如果你运行代码,就会看到维基百科上 凯文·贝肯 词条里所有指向其他词条的链接,但是现在找到的所有词条链接都是静态的模式,我们需要对其进行修改:

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

nowTime=time.time()
random.seed(nowTime)

def getlinks(articleUrl):
html = urlopen("http://en.wikipedia.org"+articleUrl)
bsObj = BeautifulSoup(html,"html.parser")
return bsObj.find("div",{"id":"bodyContent"}).findAll("a",href=re.compile("^(/wiki/)((?!:).)*$"))

links = getlinks("/wiki/Kevin_Bacon")
while len(links) > 0:
newArticle = links[random.randint(0 ,len(links)-1)].attrs["href"]
print(newArticle)
links = getlinks(newArticle)
1
2
3
4
5
6
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
/wiki/Seattle_International_Film_Festival
/wiki/Bernardo_Bertolucci
/wiki/The_Dreamers_(2003_film)
/wiki/Classical_Hollywood
/wiki/Make-Up_Artists_and_Hair_Stylists_Guild_Awards

程序的逻辑就是,先爬取某个词条里所有指向其他词条的链接,然后用随机数再次打开其中一个链接,重复进行此操作

为了完成“维基百科六度分隔理论”,还需要对收集词条链接的数据进行分析,那就是后话了

采集整个网站

遍历整个网站的网络数据是一件费时费力的事情,但是采集它们有许多好处

一个简单的方法就是:从顶级页面开始(比如主页),然后搜索页面上的所有链接,形成列表,再去采集这些链接的每一个页面,然后把在每个页面上找到的链接形成新的列表,重复执行下一轮采集

很明显,这是一个复杂度增长很快的情形,为了避免一个页面被采集两次,链接去重是非常重要的,在代码运行时,把已发现的所有链接都放到一起,并保存在方便查询的列表里,只有“新”链接才会被采集,之后再从页面中搜索其他链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,"html.parser")
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("")
1
2
3
4
5
6
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
/wiki/Wikipedia
/wiki/Wikipedia:Protection_policy#semi
/wiki/Wikipedia:Requests_for_page_protection
/wiki/Wikipedia:Requests_for_permissions
/wiki/Wikipedia:Protection_policy#extended

这个爬虫只收集了词条链接的信息,当然也可以改进下,使其可以收集更多信息:

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 bs4 import BeautifulSoup
import re

pages = set()
def getLinks(pageUrl):
global pages
html = urlopen("http://en.wikipedia.org"+pageUrl)
bsObj = BeautifulSoup(html,"html.parser")
try:
print(bsObj.h1.get_text())
print(bsObj.find(id="mw-content-text").findAll("p")[0])
print(bsObj.find(id="ca-edit").find("span").find("a").attrs['href'])
except AttributeError:
print("The page is missing some properties! But don't worry!")

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("")

因为数据有点多并且没有什么价值,所以就不展示了

通过互联网采集

就像之前的例子一样,我们后面要建立的网络爬虫也是顺着链接从一个页面跳到另一个页面,描绘出一张网络地图

但是这一次,它们不再忽略外链,而是跟着外链跳转,我们想看看爬虫是不是可以记录我们浏览过的每一个页面上的信息,这将是一个新的挑战

相比我们之前做的单个域名采集,互联网采集要难得多 —— 不同网站的布局迥然不同,这就意味着我们必须在要寻找的信息以及查找方式上都极具灵活性

  • 我要收集哪些数据?这些数据可以通过采集几个已经确定的网站(永远是最简单的做法)完成吗?或者我的爬虫需要发现那些我可能不知道的网站吗?
  • 当我的爬虫到了某个网站,它是立即顺着下一个出站链接跳到一个新网站,还是在网站上呆一会儿,深入采集网站的内容?
  • 有没有我不想采集的一类网站?我对非英文网站的内容感兴趣吗?
  • 如果我的网络爬虫引起了某个网站网管的怀疑,我如何避免法律责任?

先看一个案例:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import time
import random

pages = set()
nowTime = time.time()
random.seed(nowTime)

def getInternalLinks(bsObj, includeUrl): # 获取页面所有内链的列表
internalLinks = []
for link in bsObj.findAll("a", href=re.compile("^(/|.*"+includeUrl+")")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in internalLinks:
internalLinks.append(link.attrs['href'])
return internalLinks

def getExternalLinks(bsObj, excludeUrl): # 获取页面所有外链的列表
externalLinks = []
for link in bsObj.findAll("a",href=re.compile("^(http|www)((?!"+excludeUrl+").)*$")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in externalLinks:
externalLinks.append(link.attrs['href'])
return externalLinks

def splitAddress(address): # 加工静态链接
addressParts = address.replace("http://", "").split("/")
return addressParts

def getRandomExternalLink(startingPage): # 从内&外链列表中获取随机的外链
html = urlopen(startingPage)
bsObj = BeautifulSoup(html,"html.parser")
externalLinks = getExternalLinks(bsObj, splitAddress(startingPage)[0])
if len(externalLinks) == 0:
internalLinks = getInternalLinks(startingPage)
return getRandomExternalLink(internalLinks[random.randint(0,len(internalLinks)-1)])
else:
return externalLinks[random.randint(0, len(externalLinks)-1)]

def followExternalOnly(startingSite): # 程序开始
externalLink = getRandomExternalLink(startingSite)
print("随机外链是:"+externalLink)
followExternalOnly(externalLink)

followExternalOnly("http://oreilly.com")
1
2
3
4
C:\Users\ywx813\anaconda3\python.exe D:/PythonProject/Crawler/test.py
随机外链是:https://twitter.com/oreillymedia
随机外链是:https://help.twitter.com/using-twitter/twitter-supported-browsers
随机外链是:https://microsoft.com/edge

网站首页上并不能保证一直能发现外链,这时为了能够发现外链,就需要用一种类似前面案例中使用的采集方法,即递归地深入一个网站直到找到一个外链才停止

如果想要收集内&外链,则可以加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
allExtLinks = set()
allIntLinks = set()

def getAllExternalLinks(siteUrl):
html = urlopen(siteUrl)
bsObj = BeautifulSoup(html)
internalLinks = getInternalLinks(bsObj,splitAddress(siteUrl)[0])
externalLinks = getExternalLinks(bsObj,splitAddress(siteUrl)[0])
for link in externalLinks:
if link not in allExtLinks:
allExtLinks.add(link)
print(link)
for link in internalLinks:
if link not in allIntLinks:
print("即将获取链接的URL是:"+link)
allIntLinks.add(link)
getAllExternalLinks(link)

getAllExternalLinks("http://oreilly.com")

写代码之前拟个大纲或画个流程图是很好的编程习惯,这么做不仅可以为你后期处理节省很多时间,更重要的是可以防止自己在爬虫变得越来越复杂时乱了分寸

通过Scrapy采集

Scrapy 就是一个帮你大幅度降低网页链接查找和识别工作复杂度的 Python 库,它可以 让你轻松地采集一个或多个域名的信息

首先在命令行输入:

1
$scrapy startproject wikiSpider

系统就会自动帮你创建一个 Scrapy 爬虫模板,Scrapy 的每个 Item(条目)对象表示网站上的一个页面,当然,你可以根据需要定义不同的条目(比如 url、content、header image 等),但是现在我只演示收集每页的 title 字段 (field)

先在你的 items.py 文件中写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

from scrapy import Item, Field
class Article(Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = Field()

然后在 spiders 目录中新建一个 articleSpider.py 文件,然后写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from scrapy.selector import Selector
from scrapy import Spider
from wikiSpider.items import Article

class ArticleSpider(Spider):
name="article"
allowed_domains = ["en.wikipedia.org"]
start_urls = ["http://en.wikipedia.org/wiki/Main_Page",
"http://en.wikipedia.org/wiki/Python_%28programming_language%29"]

def parse(self, response):
item = Article()
title = response.xpath('//h1/text()')[0].extract()
print("Title is: "+title)
item['title'] = title
return item

最后在 pycharm 的控制台中输入以下命令来执行爬虫:

1
$ scrapy crawl article

PS:API

API 为不同的应用提供了方便友好的接口,不同的开发者用不同的架构,甚至不同的语言编写软件都没问题 —— 因为 API 设计的目的就是要成为一种通用语言,让不同的软件进行信息共享

API 可以通过 HTTP 协议下载文件,和 URL 访问网站获取数据的协议一 样,它几乎可以实现所有在网上干的事情,API 之所以叫 API 而不是叫网站的原因,其实是首先 API 请求使用非常严谨的语法,其次 API 用 JSON 或 XML 格式表示数据,而不是 HTML 格式

API通用规则:方法

和大多数网络数据采集的方式不同,API 用一套非常标准的规则生成数据,而且生成的数据也是按照非常标准的方式组织的

利用 HTTP 从网络服务获取信息有四种方式:

  • GET
    • GET 就是你在浏览器中输入网址浏览网站所做的事情,当你访问某个网站时,就会使用 GET 方法(可以想象成 GET 在说:“喂,网络服务器,请按 照这个网址给我信息”)
  • POST
    • POST 基本就是当你填写表单或提交信息到网络服务器的后端程序时所做的事情,每次当你登录网站的时候,就是通过用户名和(有可能加密的)密码发起一个 POST 请求(如果你用 API 发起一个 POST 请求,相当于说“请把信息保存到你的数据库里”)
  • PUT
    • PUT 请求用来更新一个对象或信息(例如:API 可能会要求用 POST 请求来创建新用户,但是如果你要更新老用户的邮箱地址,就要用 PUT 请求了)
  • DELETE
    • 用于删除一个对象

API通用规则:验证

虽然有些 API 不需要验证操作(就是说任何人都可以使用 API,不需要注册),但是很多新式 API 在使用之前都要求客户验证

通常 API 验证的方法都是用类似令牌(token)的方式调用,每次 API 调用都会把令牌传递到服务器上,这种令牌要么是用户注册的时候分配给用户,要么就是在用户调用的时候才提供,可能是长期固定的值,也可能是频繁变化的,通过服务器对用户名和密码的组合处理后生成

令牌除了在 URL 链接中传递,还会通过请求头里的 cookie 把用户信息传递给服务器