爬虫笔记

e1.HTTP和HTTPS

HTTP协议(HyperText Transfer Protocol,超文本传输协议):是一种发布和接收 HTML页面的方法。

HTTPS(Hypertext Transfer Protocol over Secure Socket Layer)简单讲是HTTP的安全版,在HTTP下加入SSL层。

SSL(Secure Sockets Layer 安全套接层)主要用于Web的安全传输协议,在传输层对网络连接进行加密,保障在Internet上数据传输的安全。

· HTTP的端口号为80,

· HTTPS的端口号为443

1.1.1 HTTP工作原理

网络爬虫抓取过程可以理解为模拟浏览器操作的过程。

浏览器的主要功能是向服务器发出请求,在浏览器窗口中展示您选择的网络资源,HTTP是一套计算机通过网络进行通信的规则。

1.2 HTTP的请求与响应

HTTP通信由两部分组成: 客户端请求消息 与服务器响应消息

1.2.1 浏览器发送HTTP请求的过程:

1)当用户在浏览器的地址栏中输入一个URL并按回车键之后,浏览器会向HTTP服务器发送HTTP请求。HTTP请求主要分为“Get”和“Post”两种方法。

2)当我们在浏览器输入URL http://www.baidu.com 的时候,浏览器发送一个Request请求去获取 http://www.baidu.com 的html文件,服务器把Response文件对象发送回给浏览器。

3)浏览器分析Response中的 HTML,发现其中引用了很多其他文件,比如Images文件,CSS文件,JS文件。 浏览器会自动再次发送Request去获取图片,CSS文件,或者JS文件。

4)当所有的文件都下载成功后,网页会根据HTML语法结构,完整的显示出来了。

URL(Uniform / Universal Resource Locator的缩写):统一资源定位符,是用于完整地描述Internet上网页和其他资源的地址的一种标识方法。

img

基本格式:scheme://host[:port#]/path/…/[?query-string][#anchor]

· scheme:协议(例如:http, https, ftp)

· host:服务器的IP地址或者域名

· port#:服务器的端口(如果是走协议默认端口,缺省端口80)

· path:访问资源的路径

· query-string:参数,发送给http服务器的数据

· anchor:锚(跳转到网页的指定锚点位置)

例如:

· ftp://192.168.0.116:8080/index

· http://www.baidu.com

· http://item.jd.com/11936238.html#product-detail

1.3 客户端HTTP请求

URL只是标识资源的位置,而HTTP是用来提交和获取资源。客户端发送一个HTTP请求到服务器的请求消息,包括以下格式:

请求行、请求头部、空行、请求数据 四个部分组成

1.3.1 一个典型的HTTP请求示例

GET https://www.baidu.com/ HTTP/1.1

Host: www.baidu.com

Connection: keep-alive

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8

Referer: http://www.baidu.com/

Accept-Encoding: gzip, deflate, sdch, br

Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

Cookie: BAIDUID=04E4001F34EA74AD4601512DD3C41A7B:FG=1; BIDUPSID=04E4001F34EA74AD4601512DD3C41A7B; PSTM=1470329258; MCITY=-343%3A340%3A; BDUSS=nF0MVFiMTVLcUh-Q2MxQ0M3STZGQUZ4N2hBa1FFRkIzUDI3QlBCZjg5cFdOd1pZQVFBQUFBJCQAAAAAAAAAAAEAAADpLvgG0KGyvLrcyfrG-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFaq3ldWqt5XN; H_PS_PSSID=1447_18240_21105_21386_21454_21409_21554; BD_UPN=12314753; sug=3; sugstore=0; ORIGIN=0; bdime=0; H_PS_645EC=7e2ad3QHl181NSPbFbd7PRUCE1LlufzxrcFmwYin0E6b%2BW8bbTMKHZbDP0g; BDSVRTM=0

1.3.2 请求方法

GET https://www.baidu.com/ HTTP/1.1

根据HTTP标准,HTTP请求可以使用多种请求方法。

HTTP 0.9:只有基本的文本 GET 功能。

HTTP 1.0:完善的请求/响应模型,并将协议补充完整,定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP 1.1:在 1.0 基础上进行更新,新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

HTTP 2.0(未普及):请求/响应首部的定义基本没有改变,只是所有首部键必须全部小写,而且请求行要独立为 :method、:scheme、:host、:path这些键值对。

序号 方法 描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件),数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。

1.3.3 HTTP请求主要分为Get和Post两种方法

· GET是从服务器上获取数据,POST是向服务器传送数据

· GET请求参数显示,都显示在浏览器网址上,HTTP服务器根据该请求所包含URL中的参数来产生响应内容,即“Get”请求的参数是URL的一部分。 例如: http://www.baidu.com/s?wd=Chinese

· POST请求参数在请求体当中,消息长度没有限制而且以隐式的方式进行发送,通常用来向HTTP服务器提交量比较大的数据(比如请求中包含许多参数或者文件上传操作等),请求的参数包含在“Content-Type”消息头里,指明该消息体的媒体类型和编码,

注意:避免使用Get方式提交表单,因为有可能会导致安全问题。比如说在登陆表单中用Get方式,用户输入的用户名和密码将在地址栏中暴露无遗。

1.3.4 常用的请求报头

Host (主机和端口号)

Host:对应网址URL中的Web名称和端口号,用于指定被请求资源的Internet主机和端口号,通常属于URL的一部分。

Connection (链接类型)

Connection:表示客户端与服务连接类型

1)Client 发起一个包含 Connection:keep-alive 的请求,HTTP/1.1使用 keep-alive 为默认值。

2) Server收到请求后:

o 如果 Server 支持 keep-alive,回复一个包含 Connection:keep-alive 的响应,不关闭连接;

o 如果 Server 不支持 keep-alive,回复一个包含 Connection:close 的响应,关闭连接。

3)如果client收到包含 Connection:keep-alive 的响应,向同一个连接发送下一个请求,直到一方主动关闭连接。

keep-alive在很多情况下能够重用连接,减少资源消耗,缩短响应时间,比如当浏览器需要多个文件时(比如一个HTML文件和相关的图形文件),不需要每次都去请求建立连接。

1.3.5 Upgrade-Insecure-Requests (升级为HTTPS请求)

Upgrade-Insecure-Requests:升级不安全的请求,意思是会在加载 http 资源时自动替换成 https 请求,让浏览器不再显示https页面中的http请求警报。

HTTPS 是以安全为目标的 HTTP 通道,所以在 HTTPS 承载的页面上不允许出现 HTTP 请求,一旦出现就是提示或报错。

1.3.6 User-Agent (浏览器名称)

User-Agent:是客户浏览器的名称,以后会详细讲。

1.3.7 Accept (传输文件类型)

Accept:指浏览器或其他客户端可以接受的MIME(Multipurpose Internet Mail Extensions(多用途互联网邮件扩展))文件类型,服务器可以根据它判断并返回适当的文件格式。

举例:

Accept: /:表示什么都可以接收。

Accept:image/gif:表明客户端希望接受GIF图像格式的资源;

Accept:text/html:表明客户端希望接受html文本。

Accept: text/html, application/xhtml+xml;q=0.9, image/*;q=0.8:表示浏览器支持的 MIME 类型分别是 html文本、xhtml和xml文档、所有的图像格式资源。

q是权重系数,范围 0 =< q <= 1,q 值越大,请求越倾向于获得其“;”之前的类型表示的内容。若没有指定q值,则默认为1,按从左到右排序顺序;若被赋值为0,则用于表示浏览器不接受此内容类型。

Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的;Application:用于传输应用程序数据或者二进制数据。详细请点击

1.3.8 Referer (页面跳转处)

Referer:表明产生请求的网页来自于哪个URL,用户是从该 Referer页面访问到当前请求的页面。这个属性可以用来跟踪Web请求来自哪个页面,是从什么网站来的等。

有时候遇到下载某网站图片,需要对应的referer,否则无法下载图片,那是因为人家做了防盗链,原理就是根据referer去判断是否是本网站的地址,如果不是,则拒绝,如果是,就可以下载;

1.3.9 Accept-Encoding(文件编解码格式)

Accept-Encoding:指出浏览器可以接受的编码方式。编码方式不同于文件格式,它是为了压缩文件并加速文件传递速度。浏览器在接收到Web响应之后先解码,然后再检查文件格式,许多情形下这可以减少大量的下载时间。

举例:Accept-Encoding:gzip;q=1.0, identity; q=0.5, *;q=0

如果有多个Encoding同时匹配, 按照q值顺序排列,本例中按顺序支持 gzip, identity压缩编码,支持gzip的浏览器会返回经过gzip编码的HTML页面。 如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。

1.3.10 Accept-Language(语言种类)

Accept-Langeuage:指出浏览器可以接受的语言种类,如en或en-us指英语,zh或者zh-cn指中文,当服务器能够提供一种以上的语言版本时要用到。

1.3.11 Accept-Charset(字符编码)

Accept-Charset:指出浏览器可以接受的字符编码。

举例:Accept-Charset:iso-8859-1,gb2312,utf-8

· ISO8859-1:通常叫做Latin-1。Latin-1包括了书写所有西方欧洲语言不可缺少的附加字符,英文浏览器的默认值是ISO-8859-1.

· gb2312:标准简体中文字符集;

· utf-8:UNICODE 的一种变长字符编码,可以解决多种语言文本显示问题,从而实现应用国际化和本地化。

如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。

Cookie:浏览器用这个属性向服务器发送Cookie。Cookie是在浏览器中寄存的小型数据体,它可以记载和服务器相关的用户信息,也可以用来实现会话功能,以后会详细讲。

1.3.13 Content-Type (POST数据类型)

Content-Type:POST请求里用来表示的内容类型。

举例:Content-Type = Text/XML; charset=gb2312:

指明该请求的消息体中包含的是纯文本的XML类型的数据,字符编码采用“gb2312”。

1.4 服务端HTTP响应

HTTP响应也由四个部分组成,分别是: 状态行、消息报头、空行、响应正文

img

HTTP/1.1 200 OK

Server: Tengine

Connection: keep-alive

Date: Wed, 30 Nov 2016 07:58:21 GMT

Cache-Control: no-cache

Content-Type: text/html;charset=UTF-8

Keep-Alive: timeout=20

Vary: Accept-Encoding

Pragma: no-cache

X-NWS-LOG-UUID: bd27210a-24e5-4740-8f6c-25dbafa9c395

Content-Length: 180945

1.4.1 常用的响应报头(了解)

理论上所有的响应头信息都应该是回应请求头的。但是服务端为了效率,安全,还有其他方面的考虑,会添加相对应的响应头信息:

1.4.2 Cache-Control:must-revalidate, no-cache, private。

这个值告诉客户端,服务端不希望客户端缓存资源,在下次请求资源时,必须要从新请求服务器,不能从缓存副本中获取资源。

· Cache-Control是响应头中很重要的信息,当客户端请求头中包含Cache-Control:max-age=0请求,明确表示不会缓存服务器资源时,Cache-Control作为作为回应信息,通常会返回no-cache,意思就是说,”那就不缓存呗”。

· 当客户端在请求头中没有包含Cache-Control时,服务端往往会定,不同的资源不同的缓存策略,比如说oschina在缓存图片资源的策略就是Cache-Control:max-age=86400,这个意思是,从当前时间开始,在86400秒的时间内,客户端可以直接从缓存副本中读取资源,而不需要向服务器请求。

1.4.3 Connection:keep-alive

这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。

1.4.4 Content-Encoding:gzip

告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。

1.4.5 Content-Type:text/html;charset=UTF-8

告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。

1.4.6 Date:Sun, 21 Sep 2016 06:18:21 GMT

这个是服务端发送资源时的服务器时间,GMT是格林尼治所在地的标准时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。

1.4.7 Expires:Sun, 1 Jan 2000 01:00:00 GMT

这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=*这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。

1.4.8 Pragma:no-cache

这个含义与Cache-Control等同。

1.4.9 Server:Tengine/1.4.6

这个是服务器和相对应的版本,只是告诉客户端服务器的信息。

1.4.10 Transfer-Encoding:chunked

这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。

1.4.11 Vary: Accept-Encoding

告诉缓存服务器,缓存压缩文件和非压缩文件两个版本,现在这个字段用处并不大,因为现在的浏览器都是支持压缩的。

1.5 响应状态码

响应状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

1.5.1 常见状态码:

· 100~199:表示服务器成功接收部分请求,要求客户端继续提交其余请求才能完成整个处理过程。

· 200~299:表示服务器成功接收请求并已完成整个处理过程。常用200(OK 请求成功)。

· 300~399:为完成请求,客户需进一步细化请求。例如:请求的资源已经移动一个新地址、常用302(所请求的页面已经临时转移至新的url)、307和304(使用缓存资源)。

· 400~499:客户端的请求有错误,常用404(服务器无法找到被请求的页面)、403(服务器拒绝访问,权限不够)。

· 500~599:服务器端出现错误,常用500(请求未完成。服务器遇到不可预知的情况)。

服务器和客户端的交互仅限于请求/响应过程,结束之后便断开,在下一次请求时,服务器会认为新的客户端。

为了维护他们之间的链接,让服务器知道这是前一个用户发送的请求,必须在一个地方保存客户端的信息。

Cookie:通过在 客户端 记录的信息确定用户的身份。

Session:通过在 服务器端 记录的信息确定用户的身份。

1.6 HTTP代理神器Fiddler

Fiddler是一款强大Web调试工具,它能记录所有客户端和服务器的HTTP请求。 Fiddler启动的时候,默认IE的代理设为了127.0.0.1:8888,而其他浏览器是需要手动设置。

1.6.1 工作原理

Fiddler 是以代理web服务器的形式工作的,它使用代理地址:127.0.0.1,端口:8888

img

1.6.2 Fiddler抓取HTTPS设置

1)启动Fiddler,打开菜单栏中的 Tools > Telerik Fiddler Options,打开“Fiddler Options”对话框。

img

2)对Fiddler进行设置:

o 打开工具栏->Tools->Fiddler Options->HTTPS,

o 选中Capture HTTPS CONNECTs (捕捉HTTPS连接),

o 选中Decrypt HTTPS traffic(解密HTTPS通信)

o 另外我们要用Fiddler获取本机所有进程的HTTPS请求,所以中间的下拉菜单中选中…from all processes (从所有进程)

o 选中下方Ignore server certificate errors(忽略服务器证书错误)

3)为 Fiddler 配置Windows信任这个根证书解决安全警告:Trust Root Certificate(受信任的根证书)。

4) Fiddler 主菜单 Tools -> Fiddler Options…-> Connections

o 选中Allow remote computers to connect(允许远程连接)

o Act as system proxy on startup(作为系统启动代理)

5) 重启Fiddler,使配置生效(这一步很重要,必须做)。

1.6.3 Fiddler 如何捕获Chrome的会话

1) 安装SwitchyOmega 代理管理 Chrome 浏览器插件

img

2)设置代理服务器为127.0.0.1:8888

3)通过浏览器插件切换为设置好的代理。

1.6.4 Fiddler界面

设置好后,本机HTTP通信都会经过127.0.0.1:8888代理,也就会被Fiddler拦截到。

img

1)请求 (Request) 部分详解

1) Headers —— 显示客户端发送到服务器的 HTTP 请求的 header,显示为一个分级视图,包含了 Web 客户端信息、Cookie、传输状态等。

2)Textview —— 显示 POST 请求的 body 部分为文本。

3)WebForms —— 显示请求的 GET 参数 和 POST body 内容。

4) HexView —— 用十六进制数据显示请求。

5)Auth —— 显示响应 header 中的 Proxy-Authorization(代理身份验证) 和 Authorization(授权) 信息.

6)Raw —— 将整个请求显示为纯文本。

7)JSON - 显示JSON格式文件。

8)XML —— 如果请求的 body 是 XML 格式,就是用分级的 XML 树来显示它。

2) 响应 (Response) 部分详解

  1. Transformer —— 显示响应的编码信息。

  2. Headers —— 用分级视图显示响应的 header。

  3. TextView —— 使用文本显示相应的 body。

  4. ImageVies —— 如果请求是图片资源,显示响应的图片。

  5. HexView —— 用十六进制数据显示响应。

  6. WebView —— 响应在 Web 浏览器中的预览效果。

  7. Auth —— 显示响应 header 中的 Proxy-Authorization(代理身份验证) 和 Authorization(授权) 信息。

  8. Caching —— 显示此请求的缓存信息。

  9. Privacy —— 显示此请求的私密 (P3P) 信息。

  10. Raw —— 将整个响应显示为纯文本。

  11. JSON - 显示JSON格式文件。

  12. XML —— 如果响应的 body 是 XML 格式,就是用分级的 XML 树来显示它 。

2. urllib2库的基本使用

所谓网页抓取,就是把URL地址中指定的网络资源从网络流中读取出来,保存到本地。 在Python中有很多库可以用来抓取网页,我们先学习urllib2。

urllib2 是 Python2.7 自带的模块(不需要下载,导入即可使用)

urllib2 官方文档:https://docs.python.org/2/library/urllib2.html

urllib2 源码:https://hg.python.org/cpython/file/2.7/Lib/urllib2.py

urllib2 在 python3.x 中被改为urllib.request

2.1 urlopen

我们先来段代码:

# urllib2_urlopen.py

# 导入urllib2 库

import urllib2

# 向指定的url发送请求,并返回服务器响应的类文件对象

response = urllib2.urlopen(“http://www.baidu.com“)

# 类文件对象支持 文件对象的操作方法,如read()方法读取文件全部内容,返回字符串

html = response.read()

# 打印字符串

print html

执行写的python代码,将打印结果

Power@PowerMac ~$: python urllib2_urlopen.py

实际上,如果我们在浏览器上打开百度主页, 右键选择“查看源代码”,你会发现,跟我们刚才打印出来的是一模一样。也就是说,上面的4行代码就已经帮我们把百度的首页的全部代码爬了下来。

一个基本的url请求对应的python代码真的非常简单。

2.2 Request

在我们第一个例子里,urlopen()的参数就是一个url地址;

但是如果需要执行更复杂的操作,比如增加HTTP报头,必须创建一个 Request 实例来作为urlopen()的参数;而需要访问的url地址则作为 Request 实例的参数。

我们编辑urllib2_request.py

# urllib2_request.py

import urllib2

# url 作为Request()方法的参数,构造并返回一个Request对象

request = urllib2.Request(“http://www.baidu.com“)

# Request对象作为urlopen()方法的参数,发送给服务器并接收响应

response = urllib2.urlopen(request)

html = response.read()

print html

2.2.1 运行结果是完全一样的:

新建Request实例,除了必须要有 url 参数之外,还可以设置另外两个参数:

  1. data(默认空):是伴随 url 提交的数据(比如要post的数据),同时 HTTP 请求将从 “GET”方式 改为 “POST”方式。

  2. headers(默认空):是一个字典,包含了需要发送的HTTP报头的键值对。

这两个参数下面会说到。

2.2.2 User-Agent

但是这样直接用urllib2给一个网站发送请求的话,确实略有些唐突了,就好比,人家每家都有门,你以一个路人的身份直接闯进去显然不是很礼貌。而且有一些站点不喜欢被程序(非人为访问)访问,有可能会拒绝你的访问请求。

但是如果我们用一个合法的身份去请求别人网站,显然人家就是欢迎的,所以我们就应该给我们的这个代码加上一个身份,就是所谓的User-Agent头。

· 浏览器 就是互联网世界上公认被允许的身份,如果我们希望我们的爬虫程序更像一个真实用户,那我们第一步,就是需要伪装成一个被公认的浏览器。用不同的浏览器在发送请求的时候,会有不同的User-Agent头。 urllib2默认的User-Agent头为:Python-urllib/x.y(x和y是Python主版本和次版本号,例如 Python-urllib/2.7)

#urllib2_useragent.py

import urllib2

url = “http://www.itxdl.cn”

#IE 9.0 的 User-Agent,包含在 ua_header里

ua_header = {“User-Agent” : “Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;”}

# url 连同 headers,一起构造Request请求,这个请求将附带 IE9.0 浏览器的User-Agent

request = urllib2.Request(url, headers = ua_header)

# 向服务器发送这个请求

response = urllib2.urlopen(request)

html = response.read()

print html

2.2.3 添加更多的Header信息

在 HTTP Request 中加入特定的 Header,来构造一个完整的HTTP请求消息。

可以通过调用Request.add_header() 添加/修改一个特定的header 也可以通过调用Request.get_header()来查看已有的header。

· 添加一个特定的header

# urllib2_headers.py

import urllib2

url = “http://www.itxdl.cn”

#IE 9.0 的 User-Agent

header = {“User-Agent” : “Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;”}

request = urllib2.Request(url, headers = header)

#也可以通过调用Request.add_header() 添加/修改一个特定的header

request.add_header(“Connection”, “keep-alive”)

# 也可以通过调用Request.get_header()来查看header信息

# request.get_header(header_name=”Connection”)

response = urllib2.urlopen(req)

print response.code #可以查看响应状态码

html = response.read()

print html

· 随机添加/修改User-Agent

# urllib2_add_headers.py

import urllib2

import random

url = “http://www.itxdl.cn”

ua_list = [

​ “Mozilla/5.0 (Windows NT 6.1; ) Apple…. “,

​ “Mozilla/5.0 (X11; CrOS i686 2268.111.0)… “,

​ “Mozilla/5.0 (Macintosh; U; PPC Mac OS X…. “,

​ “Mozilla/5.0 (Macintosh; Intel Mac OS… “

]

user_agent = random.choice(ua_list)

request = urllib2.Request(url)

#也可以通过调用Request.add_header() 添加/修改一个特定的header

request.add_header(“User-Agent”, user_agent)

# 第一个字母大写,后面的全部小写

request.get_header(“User-agent”)

response = urllib2.urlopen(req)

html = response.read()

print html

2.3 urllib2默认只支持HTTP/HTTPS的GET和POST方法

2.3.1 urllib.urlencode()

1)urllib 和 urllib2 都是接受URL请求的相关模块,但是提供了不同的功能。两个最显著的不同如下:

· urllib 仅可以接受URL,不能创建 设置了headers 的Request 类实例;

· 但是 urllib 提供 urlencode 方法用来GET查询字符串的产生,而 urllib2 则没有。(这是 urllib 和 urllib2 经常一起使用的主要原因)

· 编码工作使用urllib的urlencode()函数,帮我们将key:value这样的键值对转换成”key=value”这样的字符串,解码工作可以使用urllib的unquote()函数。(注意,不是urllib2.urlencode() )

# IPython2 中的测试结果

In [1]: import urllib

In [2]: word = {“wd” : “兄弟连”}

# 通过urllib.urlencode()方法,将字典键值对按URL编码转换,从而能被web服务器接受。

In [3]: urllib.urlencode(word)

Out[3]: “wd=%e5%85%84%e5%bc%9f%e8%bf%9e”

# 通过urllib.unquote()方法,把 URL编码字符串,转换回原先字符串。

In [4]: print urllib.unquote(“wd=%e5%85%84%e5%bc%9f%e8%bf%9e”)

wd=兄弟连

2)一般HTTP请求提交数据,需要编码成 URL编码格式,然后做为url的一部分,或者作为参数传到Request对象中。

2.4 Get方式

GET请求一般用于我们向服务器获取数据,比如说,我们用百度搜索兄弟连:https://www.baidu.com/s?wd=兄弟连

浏览器的url会跳转成如下所示:

https://www.baidu.com/s?wd=%e5%85%84%e5%bc%9f%e8%bf%9e

在其中我们可以看到在请求部分里,http://www.baidu.com/s? 之后出现一个长长的字符串,其中就包含我们要查询的关键词兄弟连,于是我们可以尝试用默认的Get方式来发送请求。

# urllib2_get.py

import urllib #负责url编码处理

import urllib2

url = “http://www.baidu.com/s”

word = {“wd”:”兄弟连”}

word = urllib.urlencode(word) #转换成url编码格式(字符串)

newurl = url + “?” + word # url首个分隔符就是 ?

headers={ “User-Agent”: “Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36”}

request = urllib2.Request(newurl, headers=headers)

response = urllib2.urlopen(request)

print response.read()

2.5 批量爬取贴吧页面数据

首先我们创建一个python文件, tiebaSpider.py,我们要完成的是,输入一个百度贴吧的地址,比如:

百度贴吧LOL吧第一页:http://tieba.baidu.com/f?kw=lol&ie=utf-8&pn=0

第二页: http://tieba.baidu.com/f?kw=lol&ie=utf-8&pn=50

第三页: http://tieba.baidu.com/f?kw=lol&ie=utf-8&pn=100

发现规律了吧,贴吧中每个页面不同之处,就是url最后的pn的值,其余的都是一样的,我们可以抓住这个规律。

简单写一个小爬虫程序,来爬取百度LOL吧的所有网页。

· 先写一个main,提示用户输入要爬取的贴吧名,并用urllib.urlencode()进行转码,然后组合url,假设是lol吧,那么组合后的url就是:http://tieba.baidu.com/f?kw=lol

# 模拟 main 函数

if name == “main“:

​ kw = raw_input(“请输入需要爬取的贴吧:”)

​ # 输入起始页和终止页,str转成int类型

​ beginPage = int(raw_input(“请输入起始页:”))

​ endPage = int(raw_input(“请输入终止页:”))

​ url = “http://tieba.baidu.com/f?”

​ key = urllib.urlencode({“kw” : kw})

​ # 组合后的url示例:http://tieba.baidu.com/f?kw=lol

​ url = url + key

​ tiebaSpider(url, beginPage, endPage)

· 接下来,我们写一个百度贴吧爬虫接口,我们需要传递3个参数给这个接口, 一个是main里组合的url地址,以及起始页码和终止页码,表示要爬取页码的范围。

def tiebaSpider(url, beginPage, endPage):

​ “”“

​ 作用:负责处理url,分配每个url去发送请求

​ url:需要处理的第一个url

​ beginPage: 爬虫执行的起始页面

​ endPage: 爬虫执行的截止页面

​ “”“

​ for page in range(beginPage, endPage + 1):

​ pn = (page - 1) * 50

​ filename = “第” + str(page) + “页.html”

​ # 组合为完整的 url,并且pn值每次增加50

​ fullurl = url + “&pn=” + str(pn)

​ #print fullurl

​ # 调用loadPage()发送请求获取HTML页面

​ html = loadPage(fullurl, filename)

​ # 将获取到的HTML页面写入本地磁盘文件

​ writeFile(html, filename)

· 我们已经之前写出一个爬取一个网页的代码。现在,我们可以将它封装成一个小函数loadPage,供我们使用。

def loadPage(url, filename):

​ ”’

​ 作用:根据url发送请求,获取服务器响应文件

​ url:需要爬取的url地址

​ filename: 文件名

​ ”’

​ print “正在下载” + filename

​ headers = {“User-Agent”: “Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;”}

​ request = urllib2.Request(url, headers = headers)

​ response = urllib2.urlopen(request)

​ return response.read()

· 最后如果我们希望将爬取到了每页的信息存储在本地磁盘上,我们可以简单写一个存储文件的接口。

def writeFile(html, filename):

​ “”“

​ 作用:保存服务器响应文件到本地磁盘文件里

​ html: 服务器响应文件

​ filename: 本地磁盘文件名

​ “”“

​ print “正在存储” + filename

​ with open(filename, ‘w’) as f:

​ f.write(html)

​ print “-” * 20

其实很多网站都是这样的,同类网站下的html页面编号,分别对应网址后的网页序号,只要发现规律就可以批量爬取页面了。

2.6 POST方式:

上面我们说了Request请求对象的里有data参数,它就是用在POST里的,我们要传送的数据就是这个参数data,data是一个字典,里面要匹配键值对。

2.6.1 有道词典翻译网站:

输入测试数据,再通过使用Fiddler观察,其中有一条是POST请求,而向服务器发送的请求数据并不是在url里,那么我们可以试着模拟这个POST请求。

img

于是,我们可以尝试用POST方式发送请求。

import urllib

import urllib2

# POST请求的目标URL

url = “http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule&smartresult=ugc&sessionFrom=null”

headers={“User-Agent”: “Mozilla….”}

formdata = {

​ “type”:”AUTO”,

​ “i”:”i love python”,

​ “doctype”:”json”,

​ “xmlVersion”:”1.8”,

​ “keyfrom”:”fanyi.web”,

​ “ue”:”UTF-8”,

​ “action”:”FY_BY_ENTER”,

​ “typoResult”:”true”

}

data = urllib.urlencode(formdata)

request = urllib2.Request(url, data = data, headers = headers)

response = urllib2.urlopen(request)

print response.read()

发送POST请求时,需要特别注意headers的一些属性:

Content-Length: 144: 是指发送的表单数据长度为144,也就是字符个数是144个。

X-Requested-With: XMLHttpRequest :表示Ajax异步请求。

Content-Type: application/x-www-form-urlencoded : 表示浏览器提交 Web 表单时使用,表单数据会按照 name1=value1&name2=value2 键值对形式进行编码。

2.7 获取AJAX加载的内容

有些网页内容使用AJAX加载,只要记得,AJAX一般返回的是JSON,直接对AJAX地址进行post或get,就返回JSON数据了。

“作为一名爬虫工程师,你最需要关注的,是数据的来源”

import urllib

import urllib2

# demo1

url = “https://movie.douban.com/j/chart/top_list?type=11&interval_id=100%3A90&action”

headers={“User-Agent”: “Mozilla….”}

# 变动的是这两个参数,从start开始往后显示limit个

formdata = {

​ ‘start’:’0’,

​ ‘limit’:’10’

}

data = urllib.urlencode(formdata)

request = urllib2.Request(url, data = data, headers = headers)

response = urllib2.urlopen(request)

print response.read()

# demo2

url = “https://movie.douban.com/j/chart/top_list?”

headers={“User-Agent”: “Mozilla….”}

# 处理所有参数

formdata = {

​ ‘type’:’11’,

​ ‘interval_id’:’100:90’,

​ ‘action’:”,

​ ‘start’:’0’,

​ ‘limit’:’10’

}

data = urllib.urlencode(formdata)

request = urllib2.Request(url, data = data, headers = headers)

response = urllib2.urlopen(request)

print response.read()

2.7.1 问题:为什么有时候POST也能在URL内看到数据?

· GET方式是直接以链接形式访问,链接中包含了所有的参数,服务器端用Request.QueryString获取变量的值。如果包含了密码的话是一种不安全的选择,不过你可以直观地看到自己提交了什么内容。

· POST则不会在网址上显示所有的参数,服务器端用Request.Form获取提交的数据,在Form提交的时候。但是HTML代码里如果不指定 method 属性,则默认为GET请求,Form中提交的数据将会附加在url之后,以?分开与url分开。

· 表单数据可以作为 URL 字段(method=”get”)或者 HTTP POST (method=”post”)的方式来发送。比如在下面的HTML代码中,表单数据将因为 (method=”get”) 而附加到 URL 上:

First name:

Last name:

img

2.8 处理HTTPS请求 SSL证书验证

现在随处可见 https 开头的网站,urllib2可以为 HTTPS 请求验证SSL证书,就像web浏览器一样,如果网站的SSL证书是经过CA认证的,则能够正常访问,如:https://www.baidu.com/等…

如果SSL证书验证不通过,或者操作系统不信任服务器的安全证书,比如浏览器在访问12306网站如:https://www.12306.cn/mormhweb/的时候,会警告用户证书不受信任。(据说 12306 网站证书是自己做的,没有通过CA认证)

img

urllib2在访问的时候则会报出SSLError:

import urllib2

url = “https://www.12306.cn/mormhweb/”

headers = {“User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36”}

request = urllib2.Request(url, headers = headers)

response = urllib2.urlopen(request)

print response.read()

运行结果:

urllib2.URLError:

2.9 关于CA

CA(Certificate Authority)是数字证书认证中心的简称,是指发放、管理、废除数字证书的受信任的第三方机构,如北京数字认证股份有限公司、上海市数字证书认证中心有限公司等…

CA的作用是检查证书持有者身份的合法性,并签发证书,以防证书被伪造或篡改,以及对证书和密钥进行管理。

现实生活中可以用身份证来证明身份, 那么在网络世界里,数字证书就是身份证。和现实生活不同的是,并不是每个上网的用户都有数字证书的,往往只有当一个人需要证明自己的身份的时候才需要用到数字证书。

普通用户一般是不需要,因为网站并不关心是谁访问了网站,现在的网站只关心流量。但是反过来,网站就需要证明自己的身份了。

比如说现在钓鱼网站很多的,比如你想访问的是www.baidu.com,但其实你访问的是www.daibu.com”,所以在提交自己的隐私信息之前需要验证一下网站的身份,要求网站出示数字证书。

一般正常的网站都会主动出示自己的数字证书,来确保客户端和网站服务器之间的通信数据是加密安全的。

2.正则

2.1 为什么要学正则表达式

实际上爬虫一共就四个主要步骤:

  1. 明确目标 (要知道你准备在哪个范围或者网站去搜索)

  2. 爬 (将所有的网站的内容全部爬下来)

  3. 取 (去掉对我们没用处的数据)

  4. 处理数据(按照我们想要的方式存储和使用)

那么对于文本的过滤或者规则的匹配,最强大的就是正则表达式,是Python爬虫世界里必不可少的神兵利器。

2.1.1 什么是正则表达式

正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

· 给定的字符串是否符合正则表达式的过滤逻辑(“匹配”);

· 通过正则表达式,从文本字符串中获取我们想要的特定部分(“过滤”)。

img

2.2 正则表达式匹配规则

img

2.2.1 Python 的re 模块

在 Python 中,我们可以使用内置的 re 模块来使用正则表达式。

有一点需要特别注意的是,正则表达式使用 对特殊字符进行转义,所以如果我们要使用原始字符串,只需加一个 r 前缀,示例:

r’chuanzhiboke\t.\tpython’

2.2.2 re 模块的一般使用步骤如下:

  1. 使用 compile() 函数将正则表达式的字符串形式编译为一个 Pattern 对象

  2. 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果,一个 Match 对象。

  3. 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作

2.2.3 compile 函数

compile 函数用于编译正则表达式,生成一个 Pattern 对象,它的一般使用形式如下:

import re

1) 将正则表达式编译成 Pattern 对象

pattern = re.compile(r’\d+’)

在上面,我们已将一个正则表达式编译成 Pattern 对象,接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了。

Pattern 对象的一些常用方法主要有:

· match 方法:从起始位置开始查找,一次匹配

· search 方法:从任何位置开始查找,一次匹配

· findall 方法:全部匹配,返回列表

· finditer 方法:全部匹配,返回迭代器

· split 方法:分割字符串,返回列表

· sub 方法:替换

2.2.4 match 方法

match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果。它的一般使用形式如下:

match(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。因此,当你不指定 pos 和 endpos 时,match 方法默认匹配字符串的头部。

当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。

>>> import re

>>> pattern = re.compile(r’\d+’) # 用于匹配至少一个数字

>>> m = pattern.match(‘one12twothree34four’) # 查找头部,没有匹配

>>> print m

None

>>> m = pattern.match(‘one12twothree34four’, 2, 10) # 从’e’的位置开始匹配,没有匹配

>>> print m

None

>>> m = pattern.match(‘one12twothree34four’, 3, 10) # 从’1’的位置开始匹配,正好匹配

>>> print m # 返回一个 Match 对象

<_sre.SRE_Match object at 0x10a42aac0>

>>> m.group(0) # 可省略 0

‘12’

>>> m.start(0) # 可省略 0

3

>>> m.end(0) # 可省略 0

5

>>> m.span(0) # 可省略 0

(3, 5)

在上面,当匹配成功时返回一个 Match 对象,其中:

· group([group1, …]) 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0);

· start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;

· end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;

· span([group]) 方法返回 (start(group), end(group))。

再看看一个例子:

>>> import re

>>> pattern = re.compile(r’([a-z]+) ([a-z]+)’, re.I) # re.I 表示忽略大小写

>>> m = pattern.match(‘Hello World Wide Web’)

>>> print m # 匹配成功,返回一个 Match 对象

<_sre.SRE_Match object at 0x10bea83e8>

>>> m.group(0) # 返回匹配成功的整个子串

‘Hello World’

>>> m.span(0) # 返回匹配成功的整个子串的索引

(0, 11)

>>> m.group(1) # 返回第一个分组匹配成功的子串

‘Hello’

>>> m.span(1) # 返回第一个分组匹配成功的子串的索引

(0, 5)

>>> m.group(2) # 返回第二个分组匹配成功的子串

‘World’

>>> m.span(2) # 返回第二个分组匹配成功的子串

(6, 11)

>>> m.groups() # 等价于 (m.group(1), m.group(2), …)

(‘Hello’, ‘World’)

>>> m.group(3) # 不存在第三个分组

Traceback (most recent call last):

File “”, line 1, in

IndexError: no such group

2.2.5 search 方法

search 方法用于查找字符串的任何位置,它也是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果,它的一般使用形式如下:

search(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。

当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。

让我们看看例子:

>>> import re

>>> pattern = re.compile(‘\d+’)

>>> m = pattern.search(‘one12twothree34four’) # 这里如果使用 match 方法则不匹配

>>> m

<_sre.SRE_Match object at 0x10cc03ac0>

>>> m.group()

‘12’

>>> m = pattern.search(‘one12twothree34four’, 10, 30) # 指定字符串区间

>>> m

<_sre.SRE_Match object at 0x10cc03b28>

>>> m.group()

‘34’

>>> m.span()

(13, 15)

再来看一个例子:

# -- coding: utf-8 --

import re

# 将正则表达式编译成 Pattern 对象

pattern = re.compile(r’\d+’)

# 使用 search() 查找匹配的子串,不存在匹配的子串时将返回 None

# 这里使用 match() 无法成功匹配

m = pattern.search(‘hello 123456 789’)

if m:

​ # 使用 Match 获得分组信息

​ print ‘matching string:’,m.group()

​ # 起始位置和结束位置

​ print ‘position:’,m.span()

执行结果:

matching string: 123456

position: (6, 12)

2.2.6 findall方法

上面的 match 和 search 方法都是一次匹配,只要找到了一个匹配的结果就返回。然而,在大多数时候,我们需要搜索整个字符串,获得所有匹配的结果。

findall 方法的使用形式如下:

findall(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。

findall 以列表形式返回全部能匹配的子串,如果没有匹配,则返回一个空列表。

看看例子:

import re

pattern = re.compile(r’\d+’) # 查找数字

result1 = pattern.findall(‘hello 123456 789’)

result2 = pattern.findall(‘one1two2three3four4’, 0, 10)

print result1

print result2

执行结果:

[‘123456’, ‘789’]

[‘1’, ‘2’]

再先看一个栗子:

# re_test.py

import re

#re模块提供一个方法叫compile模块,提供我们输入一个匹配的规则

#然后返回一个pattern实例,我们根据这个规则去匹配字符串

pattern = re.compile(r’\d+.\d*’)

#通过partten.findall()方法就能够全部匹配到我们得到的字符串

result = pattern.findall(“123.141593, ‘bigcat’, 232312, 3.15”)

#findall 以 列表形式 返回全部能匹配的子串给result

for item in result:

​ print item

运行结果:

123.141593

3.15

——————————————————————————————————

2.2.7 finditer方法

finditer 方法的行为跟 findall 的行为类似,也是搜索整个字符串,获得所有匹配的结果。但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器。

看看例子:

# -- coding: utf-8 --

import re

pattern = re.compile(r’\d+’)

result_iter1 = pattern.finditer(‘hello 123456 789’)

result_iter2 = pattern.finditer(‘one1two2three3four4’, 0, 10)

print type(result_iter1)

print type(result_iter2)

print ‘result1…’

for m1 in result_iter1: # m1 是 Match 对象

​ print ‘matching string: {}, position: {}’.format(m1.group(), m1.span())

print ‘result2…’

for m2 in result_iter2:

​ print ‘matching string: {}, position: {}’.format(m2.group(), m2.span())

执行结果:

2.2.8 split 方法

split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下:

split(string[, maxsplit])

其中,maxsplit 用于指定最大分割次数,不指定将全部分割。

看看例子:

import re

p = re.compile(r’[\s,\;]+’)

print p.split(‘a,b;; c d’)

执行结果:

[‘a’, ‘b’, ‘c’, ‘d’]

——————————————————————————————————

2.2.9 sub 方法

sub 方法用于替换。它的使用形式如下:

sub(repl, string[, count])

其中,repl 可以是字符串也可以是一个函数:

· 如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 id 的形式来引用分组,但不能使用编号 0;

· 如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。

· count 用于指定最多替换次数,不指定时全部替换。

看看例子:

import re

p = re.compile(r’(\w+) (\w+)’) # \w = [A-Za-z0-9]

s = ‘hello 123, hello 456’

print p.sub(r’hello world’, s) # 使用 ‘hello world’ 替换 ‘hello 123’ 和 ‘hello 456’

print p.sub(r’\2 \1’, s) # 引用分组

def func(m):

​ return ‘hi’ + ’ ’ + m.group(2)

print p.sub(func, s)

print p.sub(func, s, 1) # 最多替换一次

执行结果:

hello world, hello world

123 hello, 456 hello

hi 123, hi 456

hi 123, hello 456

——————————————————————————————————

2.2.10 匹配中文

在某些情况下,我们想匹配文本中的汉字,有一点需要注意的是,中文的 unicode 编码范围 主要在 [u4e00-u9fa5],这里说主要是因为这个范围并不完整,比如没有包括全角(中文)标点,不过,在大部分情况下,应该是够用的。

假设现在想把字符串 title = u’你好,hello,世界’ 中的中文提取出来,可以这么做:

import re

title = u’你好,hello,世界’

pattern = re.compile(ur’[\u4e00-\u9fa5]+’)

result = pattern.findall(title)

print result

注意到,我们在正则表达式前面加上了两个前缀 ur,其中 r 表示使用原始字符串,u 表示是 unicode 字符串。

执行结果:

[u’\u4f60\u597d’, u’\u4e16\u754c’]

注意:贪婪模式与非贪婪模式

  1. 贪婪模式:在整个表达式匹配成功的前提下,尽可能多的匹配 ( * );

  2. 非贪婪模式:在整个表达式匹配成功的前提下,尽可能少的匹配 ( ? );

  3. Python里数量词默认是贪婪的。

示例一:源字符串:abbbc

· 使用贪婪的数量词的正则表达式 ab* ,匹配结果: abbb。

  • 决定了尽可能多匹配 b,所以a后面所有的 b 都出现了。

· 使用非贪婪的数量词的正则表达式ab*?,匹配结果: a。

即使前面有 *,但是 ? 决定了尽可能少匹配 b,所以没有 b。

示例二 :源字符串:aa

test1
bb
test2
cc

· 使用贪婪的数量词的正则表达式:

.*

· 匹配结果:

test1
bb
test2

这里采用的是贪婪模式。在匹配到第一个“”时已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串。匹配到第二个“”后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为“

test1
bb
test2

· 使用非贪婪的数量词的正则表达式:

.*?

· 匹配结果:

test1

正则表达式二采用的是非贪婪模式,在匹配到第一个“”时使整个表达式匹配成功,由于采用的是非贪婪模式,所以结束匹配,不再向右尝试,匹配结果为“

test1
”。

3. CSS 选择器:BeautifulSoup4

和 lxml 一样,Beautiful Soup 也是一个 HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据。

lxml 只会局部遍历,而Beautiful Soup 是基于HTML DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多,所以性能要低于lxml。

BeautifulSoup 用来解析 HTML 比较简单,API非常人性化,支持CSS选择器、Python标准库中的HTML解析器,也支持 lxml 的 XML解析器。

Beautiful Soup 3 目前已经停止开发,推荐现在的项目使用Beautiful Soup 4。使用 pip 安装即可:pip install beautifulsoup4

官方文档:http://beautifulsoup.readthedocs.io/zh_CN/v4.4.0

抓取工具 速度 使用难度 安装难度
正则 最快 困难 无(内置)
BeautifulSoup 最简单 简单
lxml 简单 一般

示例:

首先必须要导入 bs4 库

# beautifulsoup4_test.py

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's storytitle>head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's storyb>p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">a>,
<a href="http://example.com/lacie" class="sister" id="link2">Laciea> and
<a href="http://example.com/tillie" class="sister" id="link3">Tilliea>;
and they lived at the bottom of a well.p>
<p class="story">...p>
"""

#创建 Beautiful Soup 对象
soup = BeautifulSoup(html)

#打开本地 HTML 文件的方式来创建对象
#soup = BeautifulSoup(open('index.html'))

#格式化输出 soup 对象的内容
print soup.prettify()

运行结果:

<html>
 <head>
  <title>
   The Dormouse's story
  title>
 head>
 <body>
  <p class="title" name="dromouse">
   <b>
    The Dormouse's story
   b>
  p>
  <p class="story">
   Once upon a time there were three little sisters; and their names were
   <a class="sister" href="http://example.com/elsie" id="link1">
    
   a>
   ,
   <a class="sister" href="http://example.com/lacie" id="link2">
    Lacie
   a>
   and
   <a class="sister" href="http://example.com/tillie" id="link3">
    Tillie
   a>
   ;
and they lived at the bottom of a well.
  p>
  <p class="story">
   ...
  p>
 body>
html>

· 如果我们在 Terminal下执行,会看到这样一段警告: img

· 意思是,如果我们没有显式地指定解析器,所以默认使用这个系统的最佳可用HTML解析器(“lxml”)。如果你在另一个系统中运行这段代码,或者在不同的虚拟环境中,使用不同的解析器造成行为不同。

· 但是我们可以通过soup = BeautifulSoup(html,“lxml”)方式指定lxml解析器。

3.1 四大对象种类

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:

· Tag

· NavigableString

· BeautifulSoup

· Comment

3.1.1 Tag

Tag 通俗点讲就是 HTML 中的一个个标签,例如:

<head><title>The Dormouse's storytitle>head>
<a class="sister" href="http://example.com/elsie" id="link1">a>
<p class="title" name="dromouse"><b>The Dormouse's storyb>p>

上面的 title head a p等等 HTML 标签加上里面包括的内容就是 Tag,那么试着使用 Beautiful Soup 来获取 Tags:

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's storytitle>head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's storyb>p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">a>,
<a href="http://example.com/lacie" class="sister" id="link2">Laciea> and
<a href="http://example.com/tillie" class="sister" id="link3">Tilliea>;
and they lived at the bottom of a well.p>
<p class="story">...p>
"""

#创建 Beautiful Soup 对象
soup = BeautifulSoup(html)


print soup.title
# <title>The Dormouse's storytitle>

print soup.head
# <head><title>The Dormouse's storytitle>head>

print soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">a>

print soup.p
# <p class="title" name="dromouse"><b>The Dormouse's storyb>p>

print type(soup.p)
# <class 'bs4.element.Tag'>

我们可以利用 soup 加标签名轻松地获取这些标签的内容,这些对象的类型是bs4.element.Tag。但是注意,它查找的是在所有内容中的第一个符合要求的标签。如果要查询所有的标签,后面会进行介绍。

对于 Tag,它有两个重要的属性,是 name 和 attrs

print soup.name
# [document] #soup 对象本身比较特殊,它的 name 即为 [document]

print soup.head.name
# head #对于其他内部标签,输出的值便为标签本身的名称

print soup.p.attrs
# {'class': ['title'], 'name': 'dromouse'}
# 在这里,我们把 p 标签的所有属性打印输出了出来,得到的类型是一个字典。

print soup.p['class'] # soup.p.get('class')
# ['title'] #还可以利用get方法,传入属性的名称,二者是等价的

soup.p['class'] = "newClass"
print soup.p # 可以对这些属性和内容等等进行修改
# 

The Dormouse's story

del soup.p['class'] # 还可以对这个属性进行删除 print soup.p #

The Dormouse's story

3.1.2 NavigableString

既然我们已经得到了标签的内容,那么问题来了,我们要想获取标签内部的文字怎么办呢?很简单,用 .string 即可,例如

print soup.p.string
# The Dormouse's story

print type(soup.p.string)
# In [13]: 

3.1.3 BeautifulSoup

BeautifulSoup 对象表示的是一个文档的内容。大部分时候,可以把它当作 Tag 对象,是一个特殊的 Tag,我们可以分别获取它的类型,名称,以及属性来感受一下

print type(soup.name)
# 

print soup.name 
# [document]

print soup.attrs # 文档本身的属性为空
# {}

3.1.4 Comment

Comment 对象是一个特殊类型的 NavigableString 对象,其输出的内容不包括注释符号。

print soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">a>

print soup.a.string
# Elsie 

print type(soup.a.string)
# <class 'bs4.element.Comment'>

a 标签里的内容实际上是注释,但是如果我们利用 .string 来输出它的内容时,注释符号已经去掉了。

3.2 遍历文档树

3.2.1 直接子节点:.contents .children属性

.content

tag 的 .content 属性可以将tag的子节点以列表的方式输出

print soup.head.contents 
#[The Dormouse's story]

输出方式为列表,我们可以用列表索引来获取它的某一个元素

print soup.head.contents[0]
#The Dormouse's story

.children

它返回的不是一个 list,不过我们可以通过遍历获取所有子节点。

我们打印输出 .children 看一下,可以发现它是一个 list 生成器对象

print soup.head.children
#

for child in  soup.body.children:
    print child

结果:

<p class="title" name="dromouse"><b>The Dormouse's storyb>p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">a>,
<a class="sister" href="http://example.com/lacie" id="link2">Laciea> and
<a class="sister" href="http://example.com/tillie" id="link3">Tilliea>;
and they lived at the bottom of a well.p>

<p class="story">...p>

3.2.2 所有子孙节点: .descendants属性

.contents 和 .children 属性仅包含tag的直接子节点,.descendants 属性可以对所有tag的子孙节点进行递归循环,和 children类似,我们也需要遍历获取其中的内容。

for child in soup.descendants:
    print child

运行结果:

<html><head><title>The Dormouse's storytitle>head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's storyb>p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">a>,
<a class="sister" href="http://example.com/lacie" id="link2">Laciea> and
<a class="sister" href="http://example.com/tillie" id="link3">Tilliea>;
and they lived at the bottom of a well.p>
<p class="story">...p>
body>html>
<head><title>The Dormouse's storytitle>head>
<title>The Dormouse's storytitle>
The Dormouse's story


<body>
<p class="title" name="dromouse"><b>The Dormouse's storyb>p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">a>,
<a class="sister" href="http://example.com/lacie" id="link2">Laciea> and
<a class="sister" href="http://example.com/tillie" id="link3">Tilliea>;
and they lived at the bottom of a well.p>
<p class="story">...p>
body>


<p class="title" name="dromouse"><b>The Dormouse's storyb>p>
<b>The Dormouse's storyb>
The Dormouse's story


<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">a>,
<a class="sister" href="http://example.com/lacie" id="link2">Laciea> and
<a class="sister" href="http://example.com/tillie" id="link3">Tilliea>;
and they lived at the bottom of a well.p>
Once upon a time there were three little sisters; and their names were

<a class="sister" href="http://example.com/elsie" id="link1">a>
 Elsie 
,

<a class="sister" href="http://example.com/lacie" id="link2">Laciea>
Lacie
 and

<a class="sister" href="http://example.com/tillie" id="link3">Tilliea>
Tillie
;
and they lived at the bottom of a well.


<p class="story">...p>
...

3.2.3 节点内容 : .stringb 属 性

如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点。如果一个tag仅有一个子节点,那么这个tag也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同。

通俗点说就是:如果一个标签里面没有标签了,那么 .string 就会返回标签里面的内容。如果标签里面只有唯一的一个标签了,那么 .strin`g 也会返回最里面的内容。例如:

print soup.head.string

#The Dormouse’s story

print soup.title.string

#The Dormouse’s story

3.3 搜索文档 树

3.3.1 find_all(name, attrs, recursive, text, kwargs)

1 ) name 参 数

name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉

A. 传字符 串

最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的标签:

“`
soup.find_all(‘b’)

[The Dormouse’s story]

print soup.find_all(‘a’)

[ , Lacie, Tillie]

“`

B. 传正则表达 式

如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容.下面例子中找出所有以b开头的标签,这表示和标签都应该被找到

“`
import re
for tag in soup.find_all(re.compile(“^b”)):
print(tag.name)

body

b

“`

C. 传列 表

如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有标签和标签:

“`
soup.find_all([“a”, “b”])

[The Dormouse’s story,

Elsie,

Lacie,

Tillie]

“`

2)keyword参数

soup.find_all(id='link2')
# ["sister" href="http://example.com/lacie" id="link2">Lacie]

3 ) text参数

通过 text 参数可以搜搜文档中的字符串内容,与 name 参数的可选值一样, text 参数接受 字符串 , 正则表达式 , 列表

“`
soup.find_all(text=”Elsie”)

[u’Elsie’]

soup.find_all(text=[“Tillie”, “Elsie”, “Lacie”])

[u’Elsie’, u’Lacie’, u’Tillie’]

soup.find_all(text=re.compile(“Dormouse”))
[u”The Dormouse’s story”, u”The Dormouse’s story”]
“`

3.4 CSS选择器

这就是另一种与 find_all 方法有异曲同工之妙的查找方法.

· 写 CSS 时,标签名不加任何修饰,类名前加.,id名前加#

· 在这里我们也可以利用类似的方法来筛选元素,用到的方法是 soup.select(),返回类型是 list

3.4.1 通过标签名查找

print soup.select('title') 
#[<title>The Dormouse's storytitle>]

print soup.select('a')
#[<a class="sister" href="http://example.com/elsie" id="link1">a>, <a class="sister" href="http://example.com/lacie" id="link2">Laciea>, <a class="sister" href="http://example.com/tillie" id="link3">Tilliea>]

print soup.select('b')
#[<b>The Dormouse's storyb>]

3.4.2 通过类名查找

print soup.select('.sister')
#[<a class="sister" href="http://example.com/elsie" id="link1">a>, <a class="sister" href="http://example.com/lacie" id="link2">Laciea>, <a class="sister" href="http://example.com/tillie" id="link3">Tilliea>]

3.4.3 通过 id名查找

“`
print soup.select(‘#link1’)

[ ]

“`

3.4.4 组合查找

组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如查找 p 标签中,id 等于 link1的内容,二者需要用空格分开

“`
print soup.select(‘p #link1’)

[ ]

“`

直接子标签查找,则使用 > 分隔

“`
print soup.select(“head > title”)

[The Dormouse’s story]

“`

3.4.5 属性查找

查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。

“`
print soup.select(‘a[class=”sister”]’)

[ , Lacie, Tillie]

print soup.select(‘a[href=”http://example.com/elsie”]’)

[ ]

“`

同样,属性仍然可以与上述查找方式组合,不在同一节点的空格隔开,同一节点的不加空格

“`
print soup.select(‘p a[href=”http://example.com/elsie”]’)

[ ]

“`

3.4.6 获取内容

以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。

soup = BeautifulSoup(html, 'lxml')
print type(soup.select('title'))
print soup.select('title')[0].get_text()

for title in soup.select('title'):
    print title.get_text()

4. 数据提取之JSON与JsonPATH

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它使得人们很容易的进行阅读和编写。同时也方便了机器进行解析和生成。适用于进行数据交互的场景,比如网站前台与后台之间的数据交互。

JSON和XML的比较可谓不相上下。

Python 2.7中自带了JSON模块,直接import json就可以使用了。

官方文档:http://docs.python.org/library/json.html

Json在线解析网站:http://www.json.cn/#

4.1 JSON

json简单说就是javascript中的对象和数组,所以这两种结构就是对象和数组两种结构,通过这两种结构可以表示各种复杂的结构

  1. 对象:对象在js中表示为{ }括起来的内容,数据结构为 { key:value, key:value, … }的键值对的结构,在面向对象的语言中,key为对象的属性,value为对应的属性值,所以很容易理解,取值方法为 对象.key 获取属性值,这个属性值的类型可以是数字、字符串、数组、对象这几种。

  2. 数组:数组在js中是中括号[ ]括起来的内容,数据结构为 [“Python”, “javascript”, “C++”, …],取值方式和所有语言中一样,使用索引获取,字段值的类型可以是 数字、字符串、数组、对象几种。

4.2 import json

json模块提供了四个功能:dumps、dump、loads、load,用于字符串 和 python数据类型间进行转换。

4.2.1 json.loads()

把Json格式字符串解码转换成Python对象 从json到python的类型转化对照如下:

# json_loads.py

import json

strList = '[1, 2, 3, 4]'

strDict = '{"city": "北京", "name": "大猫"}'

json.loads(strList) 
# [1, 2, 3, 4]

json.loads(strDict) # json数据自动按Unicode存储
# {u'city': u'\u5317\u4eac', u'name': u'\u5927\u732b'}

4.2.2. json.dumps()

实现python类型转化为json字符串,返回一个str对象 把一个Python对象编码转换成Json字符串

从python原始类型向json类型的转化对照如下:

# json_dumps.py

import json
import chardet

listStr = [1, 2, 3, 4]
tupleStr = (1, 2, 3, 4)
dictStr = {"city": "北京", "name": "大猫"}

json.dumps(listStr)
# '[1, 2, 3, 4]'
json.dumps(tupleStr)
# '[1, 2, 3, 4]'

# 注意:json.dumps() 序列化时默认使用的ascii编码
# 添加参数 ensure_ascii=False 禁用ascii编码,按utf-8编码
# chardet.detect()返回字典, 其中confidence是检测精确度

json.dumps(dictStr) 
# '{"city": "\\u5317\\u4eac", "name": "\\u5927\\u5218"}'

chardet.detect(json.dumps(dictStr))
# {'confidence': 1.0, 'encoding': 'ascii'}

print json.dumps(dictStr, ensure_ascii=False) 
# {"city": "北京", "name": "大刘"}

chardet.detect(json.dumps(dictStr, ensure_ascii=False))
# {'confidence': 0.99, 'encoding': 'utf-8'}

chardet是一个非常优秀的编码识别模块,可通过pip安装

4.2.3 json.dump()

将Python内置类型序列化为json对象后写入文件

# json_dump.py

import json

listStr = [{"city": "北京"}, {"name": "大刘"}]
json.dump(listStr, open("listStr.json","w"), ensure_ascii=False)

dictStr = {"city": "北京", "name": "大刘"}
json.dump(dictStr, open("dictStr.json","w"), ensure_ascii=False)

4.2.4. json.load()

读取文件中json形式的字符串元素 转化成python类型

# json_load.py

import json

strList = json.load(open("listStr.json"))
print strList

# [{u'city': u'\u5317\u4eac'}, {u'name': u'\u5927\u5218'}]

strDict = json.load(open("dictStr.json"))
print strDict
# {u'city': u'\u5317\u4eac', u'name': u'\u5927\u5218'}

4.3 JsonPah

JsonPath 是一种信息抽取类库,是从JSON文档中抽取指定信息的工具,提供多种语言实现版本,包括:Javascript, Python, PHP 和 Java。

JsonPath 对于 JSON 来说,相当于 XPATH 对于 XML。

下载地址:https://pypi.python.org/pypi/jsonpath

安装方法:点击Download URL链接下载jsonpath,解压之后执行python setup.py install

官方文档:http://goessner.net/articles/JsonPath

4.4 JsonPath与XPath语法对比:

Json结构清晰,可读性高,复杂度低,非常容易匹配,下表中对应了XPath的用法。

XPath JSONPath 描述
/ $ 根节点
. @ 现行节点
/ .or[] 取子节点
.. n/a 取父节点,Jsonpath未支持
// .. 就是不管位置,选择所有符合条件的条件
* * 匹配所有元素节点
@ n/a 根据属性访问,Json不支持,因为Json是个Key-value递归结构,不需要。
[] [] 迭代器标示(可以在里边做简单的迭代操作,如数组下标,根据内容选值等)
| [,] 支持迭代器中做多选。
[] ?() 支持过滤操作.
n/a () 支持表达式计算
() n/a 分组,JsonPath不支持

示例:

我们以拉勾网城市JSON文件 http://www.lagou.com/lbs/getAllCitySearchLabels.json 为例,获取所有城市。

# jsonpath_lagou.py

import urllib2
import jsonpath
import json
import chardet

url = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
request =urllib2.Request(url)
response = urllib2.urlopen(request)
html = response.read()

# 把json格式字符串转换成python对象
jsonobj = json.loads(html)

# 从根节点开始,匹配name节点
citylist = jsonpath.jsonpath(jsonobj,'$..name')

print citylist
print type(citylist)
fp = open('city.json','w')

content = json.dumps(citylist, ensure_ascii=False)
print content

fp.write(content.encode('utf-8'))
fp.close()

5.xpath

有同学说,我正则用的不好,处理HTML文档很累,有没有其他的方法?

有!那就是XPath,我们可以先将 HTML文件 转换成 XML文档,然后用 XPath 查找 HTML 节点或元素。

5.1 什么是XML

· XML 指可扩展标记语言(EXtensible Markup Language)

· XML 是一种标记语言,很类似 HTML

· XML 的设计宗旨是传输数据,而非显示数据

· XML 的标签需要我们自行定义。

· XML 被设计为具有自我描述性。

· XML 是 W3C 的推荐标准

W3School官方文档:http://www.w3school.com.cn/xml/index.asp

5.1.1 XML和HTML 的区别

数据格式 描述 设计目标
XML Extensible Markup Language (可扩展标记语言) 被设计为传输和存储数据,其焦点是数据的内容。
HTML HyperText Markup Language (超文本标记语言) 显示数据以及如何更好显示数据。
HTML DOM Document Object Model for HTML (文档对象模型) 通过 HTML DOM,可以访问所有的 HTML 元素,连同它们所包含的文本和属性。可以对其中的内容进行修改和删除,同时也可以创建新的元素。

5.1.2 XML文档示例



<bookstore> 

  <book category="cooking"> 
    <title lang="en">Everyday Italiantitle>  
    <author>Giada De Laurentiisauthor>  
    <year>2005year>  
    <price>30.00price> 
  book>  

  <book category="children"> 
    <title lang="en">Harry Pottertitle>  
    <author>J K. Rowlingauthor>  
    <year>2005year>  
    <price>29.99price> 
  book>  

  <book category="web"> 
    <title lang="en">XQuery Kick Starttitle>  
    <author>James McGovernauthor>  
    <author>Per Bothnerauthor>  
    <author>Kurt Cagleauthor>  
    <author>James Linnauthor>  
    <author>Vaidyanathan Nagarajanauthor>  
    <year>2003year>  
    <price>49.99price> 
  book> 

  <book category="web" cover="paperback"> 
    <title lang="en">Learning XMLtitle>  
    <author>Erik T. Rayauthor>  
    <year>2003year>  
    <price>39.95price> 
  book> 

bookstore>

5.1.3 HTML DOM模型示例

HTML DOM 定义了访问和操作 HTML 文档的标准方法,以树结构方式表达 HTML 文档。

5.1.4 XML的节点关系

1)父(Parent)

每个元素以及属性都有一个父。

下面是一个简单的XML例子中,book 元素是 title、author、year 以及 price 元素的父:



<book>
  <title>Harry Pottertitle>
  <author>J K. Rowlingauthor>
  <year>2005year>
  <price>29.99price>
book>

2)子(Children)

元素节点可有零个、一个或多个子。

在下面的例子中,title、author、year 以及 price 元素都是 book 元素的子:



<book>
  <title>Harry Pottertitle>
  <author>J K. Rowlingauthor>
  <year>2005year>
  <price>29.99price>
book>

3)同胞(Sibling)

拥有相同的父的节点

在下面的例子中,title、author、year 以及 price 元素都是同胞:



<book>
  <title>Harry Pottertitle>
  <author>J K. Rowlingauthor>
  <year>2005year>
  <price>29.99price>
book>

4)先辈(Ancestor)

某节点的父、父的父,等等。

在下面的例子中,title 元素的先辈是 book 元素和 bookstore 元素:



<bookstore>

<book>
  <title>Harry Pottertitle>
  <author>J K. Rowlingauthor>
  <year>2005year>
  <price>29.99price>
book>

bookstore>

5)后代(Descendant)

某个节点的子,子的子,等等。

在下面的例子中,bookstore 的后代是 book、title、author、year 以及 price 元素:



<bookstore>

<book>
  <title>Harry Pottertitle>
  <author>J K. Rowlingauthor>
  <year>2005year>
  <price>29.99price>
book>

bookstore>

5.2 什么是XPath?

XPath (XML Path Language) 是一门在 XML 文档中查找信息的语言,可用来在 XML 文档中对元素和属性进行遍历。

W3School官方文档:http://www.w3school.com.cn/xpath/index.asp

5.2.1 XPath 开发工具

  1. 开源的XPath表达式编辑工具:XMLQuire(XML格式文件可用)

  2. Chrome插件 XPath Helper

  3. Firefox插件 XPath Checker

5.2.2 选取节点

XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。

下面列出了最常用的路径表达式:

表达式 描述
Nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。

在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

路径表达式 结果
bookstore 选取 bookstore 元素的所有子节点。
/bookstore 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!
bookstore/book 选取属于 bookstore 的子元素的所有 book 元素。
//book 选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang 选取名为 lang 的所有属性。

5.2.3 谓语(Predicates)

谓语用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中。

在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:

路径表达式 结果
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang=’eng’] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

5.2.4 选取未知节点

XPath 通配符可用来选取未知的 XML 元素。

通配符 描述
* 匹配任何元素节点。
@* 匹配任何属性节点。
node() 匹配任何类型的节点。

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式 结果
/bookstore/* 选取 bookstore 元素的所有子元素。
//* 选取文档中的所有元素。
//title[@*] 选取所有带有属性的 title 元素。

5.2.5 选取若干路径

通过在路径表达式中使用“|”运算符,您可以选取若干个路径。

实例

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式 结果
//book/title | //book/price 选取 book 元素的所有 title 和 price 元素。
//title | //price 选取文档中的所有 title 和 price 元素。
/bookstore/book/title | //price 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。

5.2.6 XPath的运算符

下面列出了可用在 XPath 表达式中的运算符:

img

这些就是XPath的语法内容,在运用到Python抓取时要先转换为xml。

1)lxml库**

lxml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。

lxml和正则一样,也是用 C 实现的,是一款高性能的 Python HTML/XML 解析器,我们可以利用之前学习的XPath语法,来快速的定位特定元素以及节点信息。

lxml python 官方文档:http://lxml.de/index.html

需要安装C语言库,可使用 pip 安装:pip install lxml (或通过wheel方式安装)

2)初步使用

我们利用它来解析 HTML 代码,简单示例:

# lxml_test.py

# 使用 lxml 的 etree 库
from lxml import etree 

text = '''
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first itema>li>
         <li class="item-1"><a href="link2.html">second itema>li>
         <li class="item-inactive"><a href="link3.html">third itema>li>
         <li class="item-1"><a href="link4.html">fourth itema>li>
         <li class="item-0"><a href="link5.html">fifth itema> # 注意,此处缺少一个 li> 闭合标签
     ul>
 div>
'''

#利用etree.HTML,将字符串解析为HTML文档
html = etree.HTML(text) 

# 按字符串序列化HTML文档
result = etree.tostring(html) 

print(result)
输出结果:
<html><body>
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first itema>li>
         <li class="item-1"><a href="link2.html">second itema>li>
         <li class="item-inactive"><a href="link3.html">third itema>li>
         <li class="item-1"><a href="link4.html">fourth itema>li>
         <li class="item-0"><a href="link5.html">fifth itema>li>
ul>
 div>
body>html>

lxml 可以自动修正 html 代码,例子里不仅补全了 li 标签,还添加了 body,html 标签。

3)文件读取:

除了直接读取字符串,lxml还支持从文件里读取内容。我们新建一个hello.html文件:



<div>
    <ul>
         <li class="item-0"><a href="link1.html">first itema>li>
         <li class="item-1"><a href="link2.html">second itema>li>
         <li class="item-inactive"><a href="link3.html"><span class="bold">third itemspan>a>li>
         <li class="item-1"><a href="link4.html">fourth itema>li>
         <li class="item-0"><a href="link5.html">fifth itema>li>
     ul>
 div>

再利用 etree.parse() 方法来读取文件。

# lxml_parse.py

from lxml import etree

# 读取外部文件 hello.html
html = etree.parse('./hello.html')
result = etree.tostring(html, pretty_print=True)

print(result)

输出结果与之前相同:

<html><body>
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first itema>li>
         <li class="item-1"><a href="link2.html">second itema>li>
         <li class="item-inactive"><a href="link3.html">third itema>li>
         <li class="item-1"><a href="link4.html">fourth itema>li>
         <li class="item-0"><a href="link5.html">fifth itema>li>
ul>
 div>
body>html>

5.2.7 XPath实例测试

1. 获取所有的

  • 标签
  • # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    print type(html)  # 显示etree.parse() 返回类型
    
    result = html.xpath('//li')
    
    print result  # 打印
  • 标签的元素集合 print len(result) print type(result) print type(result[0])
  • 输出结果:

    <type 'lxml.etree._ElementTree'>
    [<Element li at 0x1014e0e18>, <Element li at 0x1014e0ef0>, <Element li at 0x1014e0f38>, <Element li at 0x1014e0f80>, <Element li at 0x1014e0fc8>]
    5
    <type 'list'>
    <type 'lxml.etree._Element'>

    2. 继续获取

  • 标签的所有 class属性
  • # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    result = html.xpath('//li/@class')
    
    print result

    运行结果

    [‘item-0’, ‘item-1’, ‘item-inactive’, ‘item-1’, ‘item-0’]

    1. 继续获取
    2. 标签下hre为link1.html的标签
    # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    result = html.xpath('//li/a[@href="link1.html"]')
    
    print result

    运行结果

    []

    **4. 获取

  • 标签下的所有标签
  • # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    
    #result = html.xpath('//li/span')
    #注意这么写是不对的:
    #因为 / 是用来获取子元素的,而  并不是 
  • 的子元素,所以,要用双斜杠 result = html.xpath('//li//span') print result
  • 运行结果

    []

    **5. 获取

  • 标签下的标签里的所有 class
  • # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    result = html.xpath('//li/a//@class')
    
    print result

    运行结果

    [‘blod’]

    6. 获取最后一个

  • 的的href
  • # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    
    result = html.xpath('//li[last()]/a/@href')
    # 谓语 [last()] 可以找到最后一个元素
    
    print result

    运行结果

    [‘link5.html’]

    7. 获取倒数第二个元素的内容

    # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    result = html.xpath('//li[last()-1]/a')
    
    # text 方法可以获取元素内容
    print result[0].text

    运行结果

    fourth item

    8. 获取class值为bold的标签名

    # xpath_li.py
    
    from lxml import etree
    
    html = etree.parse('hello.html')
    
    result = html.xpath('//*[@class="bold"]')
    
    # tag方法可以获取标签名
    print result[0].tag

    运行结果

    span

    6.多线程

    6.1**多线程糗事百科案例**

    案例要求参考上一个糗事百科单进程案例

    Queue(队列对象)

    Queue是python中的标准库,可以直接import Queue引用;队列是线程间最常用的交换数据的形式

    python下多线程的思考

    对于资源,加锁是个重要的环节。因为python原生的list,dict等,都是not thread safe的。而Queue,是线程安全的,因此在满足使用条件下,建议使用队列

    1. 初始化: class Queue.Queue(maxsize) FIFO 先进先出

    2. 包中的常用方法:

    o Queue.qsize() 返回队列的大小

    o Queue.empty() 如果队列为空,返回True,反之False

    o Queue.full() 如果队列满了,返回True,反之False

    o Queue.full 与 maxsize 大小对应

    o Queue.get([block[, timeout]])获取队列,timeout等待时间

    1. 创建一个“队列”对象

    o import Queue

    o myqueue = Queue.Queue(maxsize = 10)

    1. 将一个值放入队列中

    o myqueue.put(10)

    1. 将一个值从队列中取出

    o myqueue.get()

    6.2 多线程示意图

    img

    # 7. phantomjs

    爬虫(Spider),反爬虫(Anti-Spider),反反爬虫(Anti-Anti-Spider) 之间恢宏壮阔的斗争…

    7.1 Day 1

    · 小莫想要某站上所有的电影,写了标准的爬虫(基于HttpClient库),不断地遍历某站的电影列表页面,根据 Html 分析电影名字存进自己的数据库。

    · 这个站点的运维小黎发现某个时间段请求量陡增,分析日志发现都是 IP(xxx.xxx.xxx.xxx)这个用户,并且 user-agent 还是 Python-urllib/2.7 ,基于这两点判断非人类后直接在服务器上封杀。

    7.2 Day 2

    · 小莫电影只爬了一半,于是也针对性的变换了下策略:1. user-agent 模仿百度(“Baiduspider…”),2. IP每爬半个小时就换一个IP代理。

    · 小黎也发现了对应的变化,于是在服务器上设置了一个频率限制,每分钟超过120次请求的再屏蔽IP。 同时考虑到百度家的爬虫有可能会被误伤,想想市场部门每月几十万的投放,于是写了个脚本,通过 hostname 检查下这个 ip 是不是真的百度家的,对这些 ip 设置一个白名单。

    7.3 Day 3

    · 小莫发现了新的限制后,想着我也不急着要这些数据,留给服务器慢慢爬吧,于是修改了代码,随机1-3秒爬一次,爬10次休息10秒,每天只在8-12,18-20点爬,隔几天还休息一下。

    · 小黎看着新的日志头都大了,再设定规则不小心会误伤真实用户,于是准备换了一个思路,当3个小时的总请求超过50次的时候弹出一个验证码弹框,没有准确正确输入的话就把 IP 记录进黑名单。

    7.4 Day 4

    · 小莫看到验证码有些傻脸了,不过也不是没有办法,先去学习了图像识别(关键词 PIL,tesseract),再对验证码进行了二值化,分词,模式训练之后,总之最后识别了小黎的验证码(关于验证码,验证码的识别,验证码的反识别也是一个恢弘壮丽的斗争史…),之后爬虫又跑了起来。

    · 小黎是个不折不挠的好同学,看到验证码被攻破后,和开发同学商量了变化下开发模式,数据并不再直接渲染,而是由前端同学异步获取,并且通过 JavaScript 的加密库生成动态的 token,同时加密库再进行混淆(比较重要的步骤的确有网站这样做,参见淘宝和微博的登陆流程)。

    7.5 Day 5

    · 混淆过的加密库就没有办法了么?当然不是,可以慢慢调试,找到加密原理,不过小莫不准备用这么耗时耗力的方法,他放弃了基于 HttpClient的爬虫,选择了内置浏览器引擎的爬虫(关键词:PhantomJS,Selenium),在浏览器引擎运行页面,直接获取了正确的结果,又一次拿到了对方的数据。

    · 小黎:…..

    7.6 爬虫与发爬虫的斗争还在继续…

    通常情况下,在爬虫与反爬虫的对弈中,爬虫一定会胜利。

    换言之,只要人类能够正常访问的网页,爬虫在具备同等资源的情况下就一定可以抓取到。

    7.6.1 关于爬虫部分一些建议:

    1. 尽量减少请求次数,能抓列表页就不抓详情页,减轻服务器压力,程序员都是混口饭吃不容易。

    2. 不要只看 Web 网站,还有手机 App 和 H5,这样的反爬虫措施一般比较少。

    3. 实际应用时候,一般防守方做到根据 IP 限制频次就结束了,除非很核心的数据,不会再进行更多的验证,毕竟成本的问题会考虑到。

    4. 如果真的对性能要求很高,可以考虑多线程(一些成熟的框架如 Scrapy都已支持),甚至分布式…

    7.7 Selenium

    Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动操作,不同是Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器)。

    Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。

    Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所以我们可以用一个叫 PhantomJS 的工具代替真实的浏览器。

    可以从 PyPI 网站下载 Selenium库https://pypi.python.org/simple/selenium ,也可以用 第三方管理器 pip用命令安装:pip install selenium

    Selenium 官方参考文档:http://selenium-python.readthedocs.io/index.html

    7.8 PhantomJS

    PhantomJS 是一个基于Webkit的“无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器要高效。

    如果我们把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理 JavaScrip、Cookie、headers,以及任何我们真实用户需要做的事情。

    注意:PhantomJS 只能从它的官方网站http://phantomjs.org/download.html) 下载。 因为 PhantomJS 是一个功能完善(虽然无界面)的浏览器而非一个 Python 库,所以它不需要像 Python 的其他库一样安装,但我们可以通过Selenium调用PhantomJS来直接使用。

    PhantomJS 官方参考文档:http://phantomjs.org/documentation

    7.8.1 快速入门

    Selenium 库里有个叫 WebDriver 的 API。WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 或者其他 Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。

    # IPython2 测试代码
    
    # 导入 webdriver
    from selenium import webdriver
    
    # 要想调用键盘按键操作需要引入keys包
    from selenium.webdriver.common.keys import Keys
    
    # 调用环境变量指定的PhantomJS浏览器创建浏览器对象
    driver = webdriver.PhantomJS()
    
    # 如果没有在环境变量指定PhantomJS位置
    # driver = webdriver.PhantomJS(executable_path="./phantomjs"))
    
    # get方法会一直等到页面被完全加载,然后才会继续程序,通常测试会在这里选择 time.sleep(2)
    driver.get("http://www.baidu.com/")
    
    # 获取页面名为 wrapper的id标签的文本内容
    data = driver.find_element_by_id("wrapper").text
    
    # 打印数据内容
    print data
    
    # 打印页面标题 "百度一下,你就知道"
    print driver.title
    
    # 生成当前页面快照并保存
    driver.save_screenshot("baidu.png")
    
    # id="kw"是百度搜索输入框,输入字符串"长城"
    driver.find_element_by_id("kw").send_keys(u"缝纫机乐队")
    
    # id="su"是百度搜索按钮,click() 是模拟点击
    driver.find_element_by_id("su").click()
    
    # 获取新的页面快照
    driver.save_screenshot("缝纫机乐队.png")
    
    # 打印网页渲染后的源代码
    print driver.page_source
    
    # 获取当前页面Cookie
    print driver.get_cookies()
    
    # ctrl+a 全选输入框内容
    driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'a')
    
    # ctrl+x 剪切输入框内容
    driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'x')
    
    # 输入框重新输入内容
    driver.find_element_by_id("kw").send_keys("itxdl")
    
    # 模拟Enter回车键
    driver.find_element_by_id("su").send_keys(Keys.RETURN)
    
    # 清除输入框内容
    driver.find_element_by_id("kw").clear()
    
    # 生成新的页面快照
    driver.save_screenshot("itxdl.png")
    
    # 获取当前url
    print driver.current_url
    
    # 关闭当前页面,如果只有一个页面,会关闭浏览器
    # driver.close()
    
    # 关闭浏览器
    driver.quit()

    7.8.2 页面操作

    Selenium 的 WebDriver提供了各种方法来寻找元素,假设下面有一个表单输入框:

    你可能感兴趣的:(爬虫-从入坑到脱坑)