[Python]网络爬虫(一):抓取网页的含义和URL基本构成css
1、网络爬虫的定义html
网络爬虫,即Web Spider,是一个很形象的名字。node
把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛。
网络蜘蛛是经过网页的连接地址来寻找网页的。python
从网站某一个页面(一般是首页)开始,读取网页的内容,找到在网页中的其它连接地址,git
而后经过这些连接地址寻找下一个网页,这样一直循环下去,直到把这个网站全部的网页都抓取完为止。web
若是把整个互联网当成一个网站,那么网络蜘蛛就能够用这个原理把互联网上全部的网页都抓取下来。正则表达式
这样看来,网络爬虫就是一个爬行程序,一个抓取网页的程序。算法
网络爬虫的基本操做是抓取网页。shell
那么如何才能为所欲为地得到本身想要的页面?编程
咱们先从URL开始。
2、浏览网页的过程
抓取网页的过程其实和读者平时使用IE浏览器浏览网页的道理是同样的。
好比说你在浏览器的地址栏中输入 www.baidu.com 这个地址。
打开网页的过程其实就是浏览器做为一个浏览的“客户端”,向服务器端发送了 一次请求,把服务器端的文件“抓”到本地,再进行解释、展示。
HTML是一种标记语言,用标签标记内容并加以解析和区分。
3、URI和URL的概念和举例
简单的来说,URL就是在浏览器端输入的 http://www.baidu.com 这个字符串。
在理解URL以前,首先要理解URI的概念。
什么是URI?
Web上每种可用的资源,如 HTML文档、图像、视频片断、程序等都由一个通用资源标志符(Universal Resource Identifier, URI)进行定位。
URI一般由三部分组成:
①访问资源的命名机制;
②存放资源的主机名;
③资源自身 的名称,由路径表示。
以下面的URI:
http://www.why.com.cn/myhtml/html1223/
咱们能够这样解释它:
①这是一个能够经过HTTP协议访问的资源,
②位于主机 www.webmonkey.com.cn上,
③经过路径“/html/html40”访问。
4、URL的理解和举例
URL是URI的一个子集。它是Uniform Resource Locator的缩写,译为“统一资源定位 符”。
通俗地说,URL是Internet上描述信息资源的字符串,主要用在各类WWW客户程序和服务器程序上。
采用URL能够用一种统一的格式来描述各类信息资源,包括文件、服务器的地址和目录等。
URL的通常格式为(带方括号[]的为可选项):
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
URL的格式由三部分组成:
①第一部分是协议(或称为服务方式)。
②第二部分是存有该资源的主机IP地址(有时也包括端口号)。
③第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,
第二部分和第三部分用“/”符号隔开。
第一部分和第二部分是不可缺乏的,第三部分有时能够省略。
5、URL和URI简单比较
URI属于URL更低层次的抽象,一种字符串文本标准。
换句话说,URI属于父类,而URL属于URI的子类。URL是URI的一个子集。
URI的定义是:统一资源标识符;
URL的定义是:统一资源定位符。
两者的区别在于,URI表示请求服务器的路径,定义这么一个资源。
而URL同时说明要如何访问这个资源(http://)。
下面来看看两个URL的小例子。
1.HTTP协议的URL示例:
使用超级文本传输协议HTTP,提供超级文本信息服务的资源。
例:http://www.peopledaily.com.cn/channel/welcome.htm
其计算机域名为www.peopledaily.com.cn。
超级文本文件(文件类型为.html)是在目录 /channel下的welcome.htm。
这是中国人民日报的一台计算机。
例:http://www.rol.cn.Net/talk/talk1.htm
其计算机域名为www.rol.cn.net。
超级文本文件(文件类型为.html)是在目录/talk下的talk1.htm。
这是瑞得聊天室的地址,可由此进入瑞得聊天室的第1室。
2.文件的URL
用URL表示文件时,服务器方式用file表示,后面要有主机IP地址、文件的存取路 径(即目录)和文件名等信息。
有时能够省略目录和文件名,但“/”符号不能省略。
例:file://ftp.yoyodyne.com/pub/files/foobar.txt
上面这个URL表明存放在主机ftp.yoyodyne.com上的pub/files/目录下的一个文件,文件名是foobar.txt。
例:file://ftp.yoyodyne.com/pub
表明主机ftp.yoyodyne.com上的目录/pub。
例:file://ftp.yoyodyne.com/
表明主机ftp.yoyodyne.com的根目录。
爬虫最主要的处理对象就是URL,它根据URL地址取得所须要的文件内容,而后对它 进行进一步的处理。
所以,准确地理解URL对理解网络爬虫相当重要。
[Python]网络爬虫(二):利用urllib2经过指定的URL抓取网页内容
版本号:Python2.7.5,Python3改动较大,各位另寻教程。
所谓网页抓取,就是把URL地址中指定的网络资源从网络流中读取出来,保存到本地。
相似于使用程序模拟IE浏览器的功能,把URL做为HTTP请求的内容发送到服务器端, 而后读取服务器端的响应资源。
在Python中,咱们使用urllib2这个组件来抓取网页。
urllib2是Python的一个获取URLs(Uniform Resource Locators)的组件。
它以urlopen函数的形式提供了一个很是简单的接口。
最简单的urllib2的应用代码只须要四行。
咱们新建一个文件urllib2_test01.py来感觉一下urllib2的做用:
- import urllib2
- response = urllib2.urlopen('http://www.baidu.com/')
- html = response.read()
- print html
按下F5能够看到运行的结果:
咱们能够打开百度主页,右击,选择查看源代码(火狐OR谷歌浏览器都可),会发现也是彻底同样的内容。
也就是说,上面这四行代码将咱们访问百度时浏览器收到的代码们所有打印了出来。
这就是一个最简单的urllib2的例子。
除了"http:",URL一样可使用"ftp:","file:"等等来替代。
HTTP是基于请求和应答机制的:
客户端提出请求,服务端提供应答。
urllib2用一个Request对象来映射你提出的HTTP请求。
在它最简单的使用形式中你将用你要请求的地址建立一个Request对象,
经过调用urlopen并传入Request对象,将返回一个相关请求response对象,
这个应答对象如同一个文件对象,因此你能够在Response中调用.read()。
咱们新建一个文件urllib2_test02.py来感觉一下:
- import urllib2
- req = urllib2.Request('http://www.baidu.com')
- response = urllib2.urlopen(req)
- the_page = response.read()
- print the_page
能够看到输出的内容和test01是同样的。
urllib2使用相同的接口处理全部的URL头。例如你能够像下面那样建立一个ftp请求。
1.发送data表单数据
这个内容相信作过Web端的都不会陌生,
有时候你但愿发送一些数据到URL(一般URL与CGI[通用网关接口]脚本,或其余WEB应用程序挂接)。
在HTTP中,这个常用熟知的POST请求发送。
这个一般在你提交一个HTML表单时由你的浏览器来作。
并非全部的POSTs都来源于表单,你可以使用POST提交任意的数据到你本身的程序。
通常的HTML表单,data须要编码成标准形式。而后作为data参数传到Request对象。
编码工做使用urllib的函数而非urllib2。
咱们新建一个文件urllib2_test03.py来感觉一下:
- import urllib
- import urllib2
- url = 'http://www.someserver.com/register.cgi'
- values = {'name' : 'WHY',
- 'location' : 'SDU',
- 'language' : 'Python' }
- data = urllib.urlencode(values) # 编码工做
- req = urllib2.Request(url, data) # 发送请求同时传data表单
- response = urllib2.urlopen(req) #接受反馈的信息
- the_page = response.read() #读取反馈的内容
若是没有传送data参数,urllib2使用GET方式的请求。
GET和POST请求的不一样之处是POST请求一般有"反作用",
它们会因为某种途径改变系统状态(例如提交成堆垃圾到你的门口)。
Data一样能够经过在Get请求的URL自己上面编码来传送。
- import urllib2
- import urllib
- data = {}
- data['name'] = 'WHY'
- data['location'] = 'SDU'
- data['language'] = 'Python'
- url_values = urllib.urlencode(data)
- print url_values
- name=Somebody+Here&language=Python&location=Northampton
- url = 'http://www.example.com/example.cgi'
- full_url = url + '?' + url_values
- data = urllib2.open(full_url)
这样就实现了Data数据的Get传送。
2.设置Headers到http请求
有一些站点不喜欢被程序(非人为访问)访问,或者发送不一样版本的内容到不一样的浏览器。
默认的urllib2把本身做为“Python-urllib/x.y”(x和y是Python主版本和次版本号,例如Python-urllib/2.7),
这个身份可能会让站点迷惑,或者干脆不工做。
浏览器确认本身身份是经过User-Agent头,当你建立了一个请求对象,你能够给他一个包含头数据的字典。
下面的例子发送跟上面同样的内容,但把自身模拟成Internet Explorer。
(多谢你们的提醒,如今这个Demo已经不可用了,不过原理仍是那样的)。
- import urllib
- import urllib2
- url = 'http://www.someserver.com/cgi-bin/register.cgi'
- user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
- values = {'name' : 'WHY',
- 'location' : 'SDU',
- 'language' : 'Python' }
- headers = { 'User-Agent' : user_agent }
- data = urllib.urlencode(values)
- req = urllib2.Request(url, data, headers)
- response = urllib2.urlopen(req)
- the_page = response.read()
先来讲一说HTTP的异常处理问题。
当urlopen不可以处理一个response时,产生urlError。
不过一般的Python APIs异常如ValueError,TypeError等也会同时产生。
HTTPError是urlError的子类,一般在特定HTTP URLs中产生。
1.URLError
一般,URLError在没有网络链接(没有路由到特定服务器),或者服务器不存在的状况下产生。
这种状况下,异常一样会带有"reason"属性,它是一个tuple(能够理解为不可变的数组),
包含了一个错误号和一个错误信息。
咱们建一个urllib2_test06.py来感觉一下异常的处理:
- import urllib2
- req = urllib2.Request('http://www.baibai.com')
- try: urllib2.urlopen(req)
- except urllib2.URLError, e:
- print e.reason
按下F5,能够看到打印出来的内容是:
[Errno 11001] getaddrinfo failed
也就是说,错误号是11001,内容是getaddrinfo failed
2.HTTPError
服务器上每个HTTP 应答对象response包含一个数字"状态码"。
有时状态码指出服务器没法完成请求。默认的处理器会为你处理一部分这种应答。
例如:假如response是一个"重定向",须要客户端从别的地址获取文档,urllib2将为你处理。
其余不能处理的,urlopen会产生一个HTTPError。
典型的错误包含"404"(页面没法找到),"403"(请求禁止),和"401"(带验证请求)。
HTTP状态码表示HTTP协议所返回的响应的状态。
好比客户端向服务器发送请求,若是成功地得到请求的资源,则返回的状态码为200,表示响应成功。
若是请求的资源不存在, 则一般返回404错误。
HTTP状态码一般分为5种类型,分别以1~5五个数字开头,由3位整数组成:
------------------------------------------------------------------------------------------------
200:请求成功 处理方式:得到响应的内容,进行处理
201:请求完成,结果是建立了新资源。新建立资源的URI可在响应的实体中获得 处理方式:爬虫中不会遇到
202:请求被接受,但处理还没有完成 处理方式:阻塞等待
204:服务器端已经实现了请求,可是没有返回新的信 息。若是客户是用户代理,则无须为此更新自身的文档视图。 处理方式:丢弃
300:该状态码不被HTTP/1.0的应用程序直接使用, 只是做为3XX类型回应的默认解释。存在多个可用的被请求资源。 处理方式:若程序中可以处理,则进行进一步处理,若是程序中不能处理,则丢弃
301:请求到的资源都会分配一个永久的URL,这样就能够在未来经过该URL来访问此资源 处理方式:重定向到分配的URL
302:请求到的资源在一个不一样的URL处临时保存 处理方式:重定向到临时的URL
304 请求的资源未更新 处理方式:丢弃
400 非法请求 处理方式:丢弃
401 未受权 处理方式:丢弃
403 禁止 处理方式:丢弃
404 没有找到 处理方式:丢弃
5XX 回应代码以“5”开头的状态码表示服务器端发现本身出现错误,不能继续执行请求 处理方式:丢弃
------------------------------------------------------------------------------------------------
Error Codes错误码
由于默认的处理器处理了重定向(300之外号码),而且100-299范围的号码指示成功,因此你只能看到400-599的错误号码。
BaseHTTPServer.BaseHTTPRequestHandler.response是一个颇有用的应答号码字典,显示了HTTP协议使用的全部的应答号。
当一个错误号产生后,服务器返回一个HTTP错误号,和一个错误页面。
你可使用HTTPError实例做为页面返回的应答对象response。
这表示和错误属性同样,它一样包含了read,geturl,和info方法。
咱们建一个urllib2_test07.py来感觉一下:
- import urllib2
- req = urllib2.Request('http://bbs.csdn.net/callmewhy')
- try:
- urllib2.urlopen(req)
- except urllib2.URLError, e:
- print e.code
- #print e.read()
按下F5能够看见输出了404的错误码,也就说没有找到这个页面。
3.Wrapping
因此若是你想为HTTPError或URLError作准备,将有两个基本的办法。推荐使用第二种。
咱们建一个urllib2_test08.py来示范一下第一种异常处理的方案:
- from urllib2 import Request, urlopen, URLError, HTTPError
- req = Request('http://bbs.csdn.net/callmewhy')
- try:
- response = urlopen(req)
- except HTTPError, e:
- print 'The server couldn\'t fulfill the request.'
- print 'Error code: ', e.code
- except URLError, e:
- print 'We failed to reach a server.'
- print 'Reason: ', e.reason
- else:
- print 'No exception was raised.'
- # everything is fine
和其余语言类似,try以后捕获异常而且将其内容打印出来。
由于HTTPError是URLError的子类,若是URLError在前面它会捕捉到全部的URLError(包括HTTPError )。
咱们建一个urllib2_test09.py来示范一下第二种异常处理的方案:
- from urllib2 import Request, urlopen, URLError, HTTPError
- req = Request('http://bbs.csdn.net/callmewhy')
- try:
- response = urlopen(req)
- except URLError, e:
- if hasattr(e, 'reason'):
- print 'We failed to reach a server.'
- print 'Reason: ', e.reason
- elif hasattr(e, 'code'):
- print 'The server couldn\'t fulfill the request.'
- print 'Error code: ', e.code
- else:
- print 'No exception was raised.'
- # everything is fine
在开始后面的内容以前,先来解释一下urllib2中的两个个方法:info and geturl
urlopen返回的应答对象response(或者HTTPError实例)有两个颇有用的方法info()和geturl()1.geturl():
这个返回获取的真实的URL,这个颇有用,由于urlopen(或者opener对象使用的)或许会有重定向。获取的URL或许跟请求URL不一样。
以人人中的一个超级连接为例,
咱们建一个urllib2_test10.py来比较一下原始URL和重定向的连接:
- from urllib2 import Request, urlopen, URLError, HTTPError
- old_url = 'http://rrurl.cn/b1UZuP'
- req = Request(old_url)
- response = urlopen(req)
- print 'Old url :' + old_url
- print 'Real url :' + response.geturl()
2.info():
这个返回对象的字典对象,该字典描述了获取的页面状况。一般是服务器发送的特定头headers。目前是httplib.HTTPMessage 实例。
经典的headers包含"Content-length","Content-type",和其余内容。
咱们建一个urllib2_test11.py来测试一下info的应用:
- from urllib2 import Request, urlopen, URLError, HTTPError
- old_url = 'http://www.baidu.com'
- req = Request(old_url)
- response = urlopen(req)
- print 'Info():'
- print response.info()
下面来讲一说urllib2中的两个重要概念:Openers和Handlers。
1.Openers:
当你获取一个URL你使用一个opener(一个urllib2.OpenerDirector的实例)。
正常状况下,咱们使用默认opener:经过urlopen。
但你可以建立个性的openers。
2.Handles:
Openers使用处理器handlers,全部的“繁重”工做由handlers处理。
每一个handlers知道如何经过特定协议打开URLs,或者如何处理URL打开时的各个方面。
例如HTTP重定向或者HTTP cookies。
若是你但愿用特定处理器获取URLs你会想建立一个openers,例如获取一个能处理cookie的opener,或者获取一个不重定向的opener。
要建立一个 opener,能够实例化一个OpenerDirector,
而后调用.add_handler(some_handler_instance)。
一样,可使用build_opener,这是一个更加方便的函数,用来建立opener对象,他只须要一次函数调用。build_opener默认添加几个处理器,但提供快捷的方法来添加或更新默认处理器。
其余的处理器handlers你或许会但愿处理代理,验证,和其余经常使用但有点特殊的状况。
Opener对象有一个open方法。
该方法能够像urlopen函数那样直接用来获取urls:一般没必要调用install_opener,除了为了方便。
说完了上面两个内容,下面咱们来看一下基本认证的内容,这里会用到上面说起的Opener和Handler。
Basic Authentication 基本验证为了展现建立和安装一个handler,咱们将使用HTTPBasicAuthHandler。
当须要基础验证时,服务器发送一个header(401错误码) 请求验证。这个指定了scheme 和一个‘realm’,看起来像这样:Www-authenticate: SCHEME realm="REALM".
例如Www-authenticate: Basic realm="cPanel Users"
客户端必须使用新的请求,并在请求头里包含正确的姓名和密码。
这是“基础验证”,为了简化这个过程,咱们能够建立一个HTTPBasicAuthHandler的实例,并让opener使用这个handler就能够啦。
HTTPBasicAuthHandler使用一个密码管理的对象来处理URLs和realms来映射用户名和密码。
若是你知道realm(从服务器发送来的头里)是什么,你就能使用HTTPPasswordMgr。
一般人们不关心realm是什么。那样的话,就能用方便的HTTPPasswordMgrWithDefaultRealm。
这个将在你为URL指定一个默认的用户名和密码。
这将在你为特定realm提供一个其余组合时获得提供。
咱们经过给realm参数指定None提供给add_password来指示这种状况。
最高层次的URL是第一个要求验证的URL。你传给.add_password()更深层次的URLs将一样合适。
说了这么多废话,下面来用一个例子演示一下上面说到的内容。
咱们建一个urllib2_test12.py来测试一下info的应用:
- # -*- coding: utf-8 -*-
- import urllib2
- # 建立一个密码管理者
- password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
- # 添加用户名和密码
- top_level_url = "http://example.com/foo/"
- # 若是知道 realm, 咱们可使用他代替 ``None``.
- # password_mgr.add_password(None, top_level_url, username, password)
- password_mgr.add_password(None, top_level_url,'why', '1223')
- # 建立了一个新的handler
- handler = urllib2.HTTPBasicAuthHandler(password_mgr)
- # 建立 "opener" (OpenerDirector 实例)
- opener = urllib2.build_opener(handler)
- a_url = 'http://www.baidu.com/'
- # 使用 opener 获取一个URL
- opener.open(a_url)
- # 安装 opener.
- # 如今全部调用 urllib2.urlopen 将用咱们的 opener.
- urllib2.install_opener(opener)
注意:以上的例子咱们仅仅提供咱们的HHTPBasicAuthHandler给build_opener。
默认的openers有正常情况的handlers:ProxyHandler,UnknownHandler,HTTPHandler,HTTPDefaultErrorHandler, HTTPRedirectHandler,FTPHandler, FileHandler, HTTPErrorProcessor。
代码中的top_level_url 实际上能够是完整URL(包含"http:",以及主机名及可选的端口号)。
例如:http://example.com/。
也能够是一个“authority”(即主机名和可选的包含端口号)。
例如:“example.com” or “example.com:8080”。
后者包含了端口号。
[Python]网络爬虫(五):urllib2的使用细节与抓站技巧
前面说到了urllib2的简单入门,下面整理了一部分urllib2的使用细节。
1.Proxy 的设置
urllib2 默认会使用环境变量 http_proxy 来设置 HTTP Proxy。
若是想在程序中明确控制 Proxy 而不受环境变量的影响,可使用代理。
新建test14来实现一个简单的代理Demo:
- import urllib2
- enable_proxy = True
- proxy_handler = urllib2.ProxyHandler({"http" : 'http://some-proxy.com:8080'})
- null_proxy_handler = urllib2.ProxyHandler({})
- if enable_proxy:
- opener = urllib2.build_opener(proxy_handler)
- else:
- opener = urllib2.build_opener(null_proxy_handler)
- urllib2.install_opener(opener)
这里要注意的一个细节,使用 urllib2.install_opener() 会设置 urllib2 的全局 opener 。
这样后面的使用会很方便,但不能作更细致的控制,好比想在程序中使用两个不一样的 Proxy 设置等。
比较好的作法是不使用 install_opener 去更改全局的设置,而只是直接调用 opener 的 open 方法代替全局的 urlopen 方法。
2.Timeout 设置
在老版 Python 中(Python2.6前),urllib2 的 API 并无暴露 Timeout 的设置,要设置 Timeout 值,只能更改 Socket 的全局 Timeout 值。
- import urllib2
- import socket
- socket.setdefaulttimeout(10) # 10 秒钟后超时
- urllib2.socket.setdefaulttimeout(10) # 另外一种方式
在 Python 2.6 之后,超时能够经过 urllib2.urlopen() 的 timeout 参数直接设置。
3.在 HTTP Request 中加入特定的 Header
要加入 header,须要使用 Request 对象:- import urllib2
- request = urllib2.Request('http://www.baidu.com/')
- request.add_header('User-Agent', 'fake-client')
- response = urllib2.urlopen(request)
- print response.read()
对有些 header 要特别留意,服务器会针对这些 header 作检查
User-Agent : 有些服务器或 Proxy 会经过该值来判断是不是浏览器发出的请求
Content-Type : 在使用 REST 接口时,服务器会检查该值,用来肯定 HTTP Body 中的内容该怎样解析。常见的取值有:
application/xml : 在 XML RPC,如 RESTful/SOAP 调用时使用
application/json : 在 JSON RPC 调用时使用
application/x-www-form-urlencoded : 浏览器提交 Web 表单时使用
在使用服务器提供的 RESTful 或 SOAP 服务时, Content-Type 设置错误会致使服务器拒绝服务
urllib2 默认状况下会针对 HTTP 3XX 返回码自动进行 redirect 动做,无需人工配置。要检测是否发生了 redirect 动做,只要检查一下 Response 的 URL 和 Request 的 URL 是否一致就能够了。
- import urllib2
- my_url = 'http://www.google.cn'
- response = urllib2.urlopen(my_url)
- redirected = response.geturl() == my_url
- print redirected
- my_url = 'http://rrurl.cn/b1UZuP'
- response = urllib2.urlopen(my_url)
- redirected = response.geturl() == my_url
- print redirected
若是不想自动 redirect,除了使用更低层次的 httplib 库以外,还能够自定义HTTPRedirectHandler 类。
- import urllib2
- class RedirectHandler(urllib2.HTTPRedirectHandler):
- def http_error_301(self, req, fp, code, msg, headers):
- print "301"
- pass
- def http_error_302(self, req, fp, code, msg, headers):
- print "303"
- pass
- opener = urllib2.build_opener(RedirectHandler)
- opener.open('http://rrurl.cn/b1UZuP')
5.Cookie
urllib2 对 Cookie 的处理也是自动的。若是须要获得某个 Cookie 项的值,能够这么作:- import urllib2
- import cookielib
- cookie = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- response = opener.open('http://www.baidu.com')
- for item in cookie:
- print 'Name = '+item.name
- print 'Value = '+item.value
运行以后就会输出访问百度的Cookie值:
6.使用 HTTP 的 PUT 和 DELETE 方法
urllib2 只支持 HTTP 的 GET 和 POST 方法,若是要使用 HTTP PUT 和 DELETE ,只能使用比较低层的 httplib 库。虽然如此,咱们仍是能经过下面的方式,使 urllib2 可以发出 PUT 或DELETE 的请求:- import urllib2
- request = urllib2.Request(uri, data=data)
- request.get_method = lambda: 'PUT' # or 'DELETE'
- response = urllib2.urlopen(request)
7.获得 HTTP 的返回码
对于 200 OK 来讲,只要使用 urlopen 返回的 response 对象的 getcode() 方法就能够获得 HTTP 的返回码。但对其它返回码来讲,urlopen 会抛出异常。这时候,就要检查异常对象的 code 属性了:- import urllib2
- try:
- response = urllib2.urlopen('http://bbs.csdn.net/why')
- except urllib2.HTTPError, e:
- print e.code
8.Debug Log
使用 urllib2 时,能够经过下面的方法把 debug Log 打开,这样收发包的内容就会在屏幕上打印出来,方便调试,有时能够省去抓包的工做- import urllib2
- httpHandler = urllib2.HTTPHandler(debuglevel=1)
- httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
- opener = urllib2.build_opener(httpHandler, httpsHandler)
- urllib2.install_opener(opener)
- response = urllib2.urlopen('http://www.google.com')
这样就能够看到传输的数据包内容了:
9.表单的处理
登陆必要填表,表单怎么填?
首先利用工具截取所要填表的内容。
好比我通常用firefox+httpfox插件来看看本身到底发送了些什么包。
以verycd为例,先找到本身发的POST请求,以及POST表单项。
能够看到verycd的话须要填username,password,continueURI,fk,login_submit这几项,其中fk是随机生成的(其实不太随机,看上去像是把epoch时间通过简单的编码生成的),须要从网页获取,也就是说得先访问一次网页,用正则表达式等工具截取返回数据中的fk项。continueURI顾名思义能够随便写,login_submit是固定的,这从源码能够看出。还有username,password那就很显然了:
- # -*- coding: utf-8 -*-
- import urllib
- import urllib2
- postdata=urllib.urlencode({
- 'username':'汪小光',
- 'password':'why888',
- 'continueURI':'http://www.verycd.com/',
- 'fk':'',
- 'login_submit':'登陆'
- })
- req = urllib2.Request(
- url = 'http://secure.verycd.com/signin',
- data = postdata
- )
- result = urllib2.urlopen(req)
- print result.read()
10.假装成浏览器访问
某些网站反感爬虫的到访,因而对爬虫一概拒绝请求
这时候咱们须要假装成浏览器,这能够经过修改http包中的header来实现
- #…
- headers = {
- 'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
- }
- req = urllib2.Request(
- url = 'http://secure.verycd.com/signin/*/http://www.verycd.com/',
- data = postdata,
- headers = headers
- )
- #...
11.对付"反盗链"
某些站点有所谓的反盗链设置,其实说穿了很简单,
就是检查你发送请求的header里面,referer站点是否是他本身,
因此咱们只须要像把headers的referer改为该网站便可,以cnbeta为例:
#... headers = { 'Referer':'http://www.cnbeta.com/articles' } #...
headers是一个dict数据结构,你能够放入任何想要的header,来作一些假装。
例如,有些网站喜欢读取header中的X-Forwarded-For来看看人家的真实IP,能够直接把X-Forwarde-For改了。
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:百度贴吧爬虫
- # 版本:0.1
- # 做者:why
- # 日期:2013-05-14
- # 语言:Python 2.7
- # 操做:输入带分页的地址,去掉最后面的数字,设置一下起始页数和终点页数。
- # 功能:下载对应页码内的全部页面并存储为html文件。
- #---------------------------------------
- import string, urllib2
- #定义百度函数
- def baidu_tieba(url,begin_page,end_page):
- for i in range(begin_page, end_page+1):
- sName = string.zfill(i,5) + '.html'#自动填充成六位的文件名
- print '正在下载第' + str(i) + '个网页,并将其存储为' + sName + '......'
- f = open(sName,'w+')
- m = urllib2.urlopen(url + str(i)).read()
- f.write(m)
- f.close()
- #-------- 在这里输入参数 ------------------
- # 这个是山东大学的百度贴吧中某一个帖子的地址
- #bdurl = 'http://tieba.baidu.com/p/2296017831?pn='
- #iPostBegin = 1
- #iPostEnd = 10
- bdurl = str(raw_input(u'请输入贴吧的地址,去掉pn=后面的数字:\n'))
- begin_page = int(raw_input(u'请输入开始的页数:\n'))
- end_page = int(raw_input(u'请输入终点的页数:\n'))
- #-------- 在这里输入参数 ------------------
- #调用
- baidu_tieba(bdurl,begin_page,end_page)
接下来准备用糗百作一个爬虫的小例子。
可是在这以前,先详细的整理一下Python中的正则表达式的相关内容。
正则表达式在Python爬虫中的做用就像是老师点名时用的花名册同样,是必不可少的神兵利器。
如下内容转自CNBLOG:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html
整理时没有注意,实在抱歉。
1、 正则表达式基础
1.1.概念介绍正则表达式是用于处理字符串的强大工具,它并非Python的一部分。
其余编程语言中也有正则表达式的概念,区别只在于不一样的编程语言实现支持的语法数量不一样。
它拥有本身独特的语法以及一个独立的处理引擎,在提供了正则表达式的语言里,正则表达式的语法都是同样的。
下图展现了使用正则表达式进行匹配的流程:
正则表达式的大体匹配过程是:
1.依次拿出表达式和文本中的字符比较,
2.若是每个字符都能匹配,则匹配成功;一旦有匹配不成功的字符则匹配失败。
3.若是表达式中有量词或边界,这个过程会稍微有一些不一样。
下图列出了Python支持的正则表达式元字符和语法:
1.2. 数量词的贪婪模式与非贪婪模式
正则表达式一般用于在文本中查找匹配的字符串。
贪婪模式,老是尝试匹配尽量多的字符;
非贪婪模式则相反,老是尝试匹配尽量少的字符。
Python里数量词默认是贪婪的。
例如:正则表达式"ab*"若是用于查找"abbbc",将找到"abbb"。
而若是使用非贪婪的数量词"ab*?",将找到"a"。
1.3. 反斜杠的问题
与大多数编程语言相同,正则表达式里使用"\"做为转义字符,这就可能形成反斜杠困扰。
假如你须要匹配文本中的字符"\",那么使用编程语言表示的正则表达式里将须要4个反斜杠"\\\\":
第一个和第三个用于在编程语言里将第二个和第四个转义成反斜杠,
转换成两个反斜杠\\后再在正则表达式里转义成一个反斜杠用来匹配反斜杠\。
这样显然是很是麻烦的。
Python里的原生字符串很好地解决了这个问题,这个例子中的正则表达式可使用r"\\"表示。
一样,匹配一个数字的"\\d"能够写成r"\d"。
有了原生字符串,妈妈不再用担忧个人反斜杠问题~
2、 介绍re模块
2.1. Compile
Python经过re模块提供对正则表达式的支持。
使用re的通常步骤是:
Step1:先将正则表达式的字符串形式编译为Pattern实例。
Step2:而后使用Pattern实例处理文本并得到匹配结果(一个Match实例)。
Step3:最后使用Match实例得到信息,进行其余的操做。
咱们新建一个re01.py来试验一下re的应用:
- # -*- coding: utf-8 -*-
- #一个简单的re实例,匹配字符串中的hello字符串
- #导入re模块
- import re
- # 将正则表达式编译成Pattern对象,注意hello前面的r的意思是“原生字符串”
- pattern = re.compile(r'hello')
- # 使用Pattern匹配文本,得到匹配结果,没法匹配时将返回None
- match1 = pattern.match('hello world!')
- match2 = pattern.match('helloo world!')
- match3 = pattern.match('helllo world!')
- #若是match1匹配成功
- if match1:
- # 使用Match得到分组信息
- print match1.group()
- else:
- print 'match1匹配失败!'
- #若是match2匹配成功
- if match2:
- # 使用Match得到分组信息
- print match2.group()
- else:
- print 'match2匹配失败!'
- #若是match3匹配成功
- if match3:
- # 使用Match得到分组信息
- print match3.group()
- else:
- print 'match3匹配失败!'
能够看到控制台输出了匹配的三个结果:
下面来具体看看代码中的关键方法。
★ re.compile(strPattern[, flag]):
这个方法是Pattern类的工厂方法,用于将字符串形式的正则表达式编译为Pattern对象。
第二个参数flag是匹配模式,取值可使用按位或运算符'|'表示同时生效,好比re.I | re.M。
另外,你也能够在regex字符串中指定模式,
好比re.compile('pattern', re.I | re.M)与re.compile('(?im)pattern')是等价的。
可选值有:
- re.I(全拼:IGNORECASE): 忽略大小写(括号内是完整写法,下同)
- re.M(全拼:MULTILINE): 多行模式,改变'^'和'$'的行为(参见上图)
- re.S(全拼:DOTALL): 点任意匹配模式,改变'.'的行为
- re.L(全拼:LOCALE): 使预约字符类 \w \W \b \B \s \S 取决于当前区域设定
- re.U(全拼:UNICODE): 使预约字符类 \w \W \b \B \s \S \d \D 取决于unicode定义的字符属性
- re.X(全拼:VERBOSE): 详细模式。这个模式下正则表达式能够是多行,忽略空白字符,并能够加入注释。
如下两个正则表达式是等价的:
- # -*- coding: utf-8 -*-
- #两个等价的re匹配,匹配一个小数
- import re
- a = re.compile(r"""\d + # the integral part
- \. # the decimal point
- \d * # some fractional digits""", re.X)
- b = re.compile(r"\d+\.\d*")
- match11 = a.match('3.1415')
- match12 = a.match('33')
- match21 = b.match('3.1415')
- match22 = b.match('33')
- if match11:
- # 使用Match得到分组信息
- print match11.group()
- else:
- print u'match11不是小数'
- if match12:
- # 使用Match得到分组信息
- print match12.group()
- else:
- print u'match12不是小数'
- if match21:
- # 使用Match得到分组信息
- print match21.group()
- else:
- print u'match21不是小数'
- if match22:
- # 使用Match得到分组信息
- print match22.group()
- else:
- print u'match22不是小数'
re提供了众多模块方法用于完成正则表达式的功能。
这些方法可使用Pattern实例的相应方法替代,惟一的好处是少写一行re.compile()代码,
但同时也没法复用编译后的Pattern对象。
这些方法将在Pattern类的实例方法部分一块儿介绍。
如一开始的hello实例能够简写为:
- # -*- coding: utf-8 -*-
- #一个简单的re实例,匹配字符串中的hello字符串
- import re
- m = re.match(r'hello', 'hello world!')
- print m.group()
re模块还提供了一个方法escape(string),用于将string中的正则表达式元字符如*/+/?等以前加上转义符再返回
2.2. Match
Match对象是一次匹配的结果,包含了不少关于这次匹配的信息,可使用Match提供的可读属性或方法来获取这些信息。
属性:
- string: 匹配时使用的文本。
- re: 匹配时使用的Pattern对象。
- pos: 文本中正则表达式开始搜索的索引。值与Pattern.match()和Pattern.seach()方法的同名参数相同。
- endpos: 文本中正则表达式结束搜索的索引。值与Pattern.match()和Pattern.seach()方法的同名参数相同。
- lastindex: 最后一个被捕获的分组在文本中的索引。若是没有被捕获的分组,将为None。
- lastgroup: 最后一个被捕获的分组的别名。若是这个分组没有别名或者没有被捕获的分组,将为None。
方法:
- group([group1, …]):
得到一个或多个分组截获的字符串;指定多个参数时将以元组形式返回。group1可使用编号也可使用别名;编号0表明整个匹配的子串;不填写参数时,返回group(0);没有截获字符串的组返回None;截获了屡次的组返回最后一次截获的子串。 - groups([default]):
以元组形式返回所有分组截获的字符串。至关于调用group(1,2,…last)。default表示没有截获字符串的组以这个值替代,默认为None。 - groupdict([default]):
返回以有别名的组的别名为键、以该组截获的子串为值的字典,没有别名的组不包含在内。default含义同上。 - start([group]):
返回指定的组截获的子串在string中的起始索引(子串第一个字符的索引)。group默认值为0。 - end([group]):
返回指定的组截获的子串在string中的结束索引(子串最后一个字符的索引+1)。group默认值为0。 - span([group]):
返回(start(group), end(group))。 - expand(template):
将匹配到的分组代入template中而后返回。template中可使用\id或\g<id>、\g<name>引用分组,但不能使用编号0。\id与\g<id>是等价的;但\10将被认为是第10个分组,若是你想表达\1以后是字符'0',只能使用\g<1>0。
- # -*- coding: utf-8 -*-
- #一个简单的match实例
- import re
- # 匹配以下内容:单词+空格+单词+任意字符
- m = re.match(r'(\w+) (\w+)(?P<sign>.*)', 'hello world!')
- print "m.string:", m.string
- print "m.re:", m.re
- print "m.pos:", m.pos
- print "m.endpos:", m.endpos
- print "m.lastindex:", m.lastindex
- print "m.lastgroup:", m.lastgroup
- print "m.group():", m.group()
- print "m.group(1,2):", m.group(1, 2)
- print "m.groups():", m.groups()
- print "m.groupdict():", m.groupdict()
- print "m.start(2):", m.start(2)
- print "m.end(2):", m.end(2)
- print "m.span(2):", m.span(2)
- print r"m.expand(r'\g<2> \g<1>\g<3>'):", m.expand(r'\2 \1\3')
- ### output ###
- # m.string: hello world!
- # m.re: <_sre.SRE_Pattern object at 0x016E1A38>
- # m.pos: 0
- # m.endpos: 12
- # m.lastindex: 3
- # m.lastgroup: sign
- # m.group(1,2): ('hello', 'world')
- # m.groups(): ('hello', 'world', '!')
- # m.groupdict(): {'sign': '!'}
- # m.start(2): 6
- # m.end(2): 11
- # m.span(2): (6, 11)
- # m.expand(r'\2 \1\3'): world hello!
2.3. Pattern
Pattern对象是一个编译好的正则表达式,经过Pattern提供的一系列方法能够对文本进行匹配查找。
Pattern不能直接实例化,必须使用re.compile()进行构造,也就是re.compile()返回的对象。
Pattern提供了几个可读属性用于获取表达式的相关信息:
- pattern: 编译时用的表达式字符串。
- flags: 编译时用的匹配模式。数字形式。
- groups: 表达式中分组的数量。
- groupindex: 以表达式中有别名的组的别名为键、以该组对应的编号为值的字典,没有别名的组不包含在内。
- # -*- coding: utf-8 -*-
- #一个简单的pattern实例
- import re
- p = re.compile(r'(\w+) (\w+)(?P<sign>.*)', re.DOTALL)
- print "p.pattern:", p.pattern
- print "p.flags:", p.flags
- print "p.groups:", p.groups
- print "p.groupindex:", p.groupindex
- ### output ###
- # p.pattern: (\w+) (\w+)(?P<sign>.*)
- # p.flags: 16
- # p.groups: 3
- # p.groupindex: {'sign': 3}
下面重点介绍一下pattern的实例方法及其使用。
1.match
match(string[, pos[, endpos]]) | re.match(pattern, string[, flags]):
这个方法将从string的pos下标处起尝试匹配pattern;
若是pattern结束时仍可匹配,则返回一个Match对象;
若是匹配过程当中pattern没法匹配,或者匹配未结束就已到达endpos,则返回None。
pos和endpos的默认值分别为0和len(string);
re.match()没法指定这两个参数,参数flags用于编译pattern时指定匹配模式。
注意:这个方法并非彻底匹配。
当pattern结束时若string还有剩余字符,仍然视为成功。
想要彻底匹配,能够在表达式末尾加上边界匹配符'$'。
下面来看一个Match的简单案例:
- # encoding: UTF-8
- import re
- # 将正则表达式编译成Pattern对象
- pattern = re.compile(r'hello')
- # 使用Pattern匹配文本,得到匹配结果,没法匹配时将返回None
- match = pattern.match('hello world!')
- if match:
- # 使用Match得到分组信息
- print match.group()
- ### 输出 ###
- # hello
2.search
search(string[, pos[, endpos]]) | re.search(pattern, string[, flags]):
这个方法用于查找字符串中能够匹配成功的子串。
从string的pos下标处起尝试匹配pattern,
若是pattern结束时仍可匹配,则返回一个Match对象;
若没法匹配,则将pos加1后从新尝试匹配;
直到pos=endpos时仍没法匹配则返回None。
pos和endpos的默认值分别为0和len(string));
re.search()没法指定这两个参数,参数flags用于编译pattern时指定匹配模式。
那么它和match有什么区别呢?
match()函数只检测re是否是在string的开始位置匹配,
search()会扫描整个string查找匹配,
match()只有在0位置匹配成功的话才有返回,若是不是开始位置匹配成功的话,match()就返回none
例如:
print(re.match(‘super’, ‘superstition’).span())
会返回(0, 5)
print(re.match(‘super’, ‘insuperable’))
则返回None
search()会扫描整个字符串并返回第一个成功的匹配
例如:
print(re.search(‘super’, ‘superstition’).span())
返回(0, 5)
print(re.search(‘super’, ‘insuperable’).span())
返回(2, 7)
看一个search的实例:
- # -*- coding: utf-8 -*-
- #一个简单的search实例
- import re
- # 将正则表达式编译成Pattern对象
- pattern = re.compile(r'world')
- # 使用search()查找匹配的子串,不存在能匹配的子串时将返回None
- # 这个例子中使用match()没法成功匹配
- match = pattern.search('hello world!')
- if match:
- # 使用Match得到分组信息
- print match.group()
- ### 输出 ###
- # world
3.split
split(string[, maxsplit]) | re.split(pattern, string[, maxsplit]):
按照可以匹配的子串将string分割后返回列表。
maxsplit用于指定最大分割次数,不指定将所有分割。
- import re
- p = re.compile(r'\d+')
- print p.split('one1two2three3four4')
- ### output ###
- # ['one', 'two', 'three', 'four', '']
4.findall
findall(string[, pos[, endpos]]) | re.findall(pattern, string[, flags]):
搜索string,以列表形式返回所有能匹配的子串。
- import re
- p = re.compile(r'\d+')
- print p.findall('one1two2three3four4')
- ### output ###
- # ['1', '2', '3', '4']
5.finditer
finditer(string[, pos[, endpos]]) | re.finditer(pattern, string[, flags]):
搜索string,返回一个顺序访问每个匹配结果(Match对象)的迭代器。
- import re
- p = re.compile(r'\d+')
- for m in p.finditer('one1two2three3four4'):
- print m.group(),
- ### output ###
- # 1 2 3 4
6.sub
sub(repl, string[, count]) | re.sub(pattern, repl, string[, count]):
使用repl替换string中每个匹配的子串后返回替换后的字符串。
当repl是一个字符串时,可使用\id或\g<id>、\g<name>引用分组,但不能使用编号0。
当repl是一个方法时,这个方法应当只接受一个参数(Match对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。
count用于指定最多替换次数,不指定时所有替换。
- import re
- p = re.compile(r'(\w+) (\w+)')
- s = 'i say, hello world!'
- print p.sub(r'\2 \1', s)
- def func(m):
- return m.group(1).title() + ' ' + m.group(2).title()
- print p.sub(func, s)
- ### output ###
- # say i, world hello!
- # I Say, Hello World!
7.subn
subn(repl, string[, count]) |re.sub(pattern, repl, string[, count]):
返回 (sub(repl, string[, count]), 替换次数)。
- import re
- p = re.compile(r'(\w+) (\w+)')
- s = 'i say, hello world!'
- print p.subn(r'\2 \1', s)
- def func(m):
- return m.group(1).title() + ' ' + m.group(2).title()
- print p.subn(func, s)
- ### output ###
- # ('say i, world hello!', 2)
- # ('I Say, Hello World!', 2)
至此,Python的正则表达式基本介绍就算是完成了^_^
[Python]网络爬虫(八):糗事百科的网络爬虫(v0.3)源码及解析(简化更新)
Q&A:
1.为何有段时间显示糗事百科不可用?
答:前段时间由于糗事百科添加了Header的检验,致使没法爬取,须要在代码中模拟Header。如今代码已经做了修改,能够正常使用。
2.为何须要单独新建个线程?
答:基本流程是这样的:爬虫在后台新起一个线程,一直爬取两页的糗事百科,若是剩余不足两页,则再爬一页。用户按下回车只是从库存中获取最新的内容,而不是上网获取,因此浏览更顺畅。也能够把加载放在主线程,不过这样会致使爬取过程当中等待时间过长的问题。
项目内容:
用Python写的糗事百科的网络爬虫。
使用方法:
新建一个Bug.py文件,而后将代码复制到里面后,双击运行。
程序功能:
在命令提示行中浏览糗事百科。
原理解释:
首先,先浏览一下糗事百科的主页:http://www.qiushibaike.com/hot/page/1
能够看出来,连接中page/后面的数字就是对应的页码,记住这一点为之后的编写作准备。
而后,右击查看页面源码:
观察发现,每个段子都用div标记,其中class必为content,title是发帖时间,咱们只须要用正则表达式将其“扣”出来就能够了。
明白了原理以后,剩下的就是正则表达式的内容了,能够参照这篇博文:
http://blog.csdn.net/wxg694175346/article/details/8929576
运行效果:
- <pre code_snippet_id="189704" snippet_file_name="blog_20140215_1_8153875" name="code" class="python"># -*- coding: utf-8 -*-
- import urllib2
- import urllib
- import re
- import thread
- import time
- #----------- 加载处理糗事百科 -----------
- class Spider_Model:
- def __init__(self):
- self.page = 1
- self.pages = []
- self.enable = False
- # 将全部的段子都扣出来,添加到列表中而且返回列表
- def GetPage(self,page):
- myUrl = "http://m.qiushibaike.com/hot/page/" + page
- user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
- headers = { 'User-Agent' : user_agent }
- req = urllib2.Request(myUrl, headers = headers)
- myResponse = urllib2.urlopen(req)
- myPage = myResponse.read()
- #encode的做用是将unicode编码转换成其余编码的字符串
- #decode的做用是将其余编码的字符串转换成unicode编码
- unicodePage = myPage.decode("utf-8")
- # 找出全部class="content"的div标记
- #re.S是任意匹配模式,也就是.能够匹配换行符
- myItems = re.findall('<div.*?class="content".*?title="(.*?)">(.*?)</div>',unicodePage,re.S)
- items = []
- for item in myItems:
- # item 中第一个是div的标题,也就是时间
- # item 中第二个是div的内容,也就是内容
- items.append([item[0].replace("\n",""),item[1].replace("\n","")])
- return items
- # 用于加载新的段子
- def LoadPage(self):
- # 若是用户未输入quit则一直运行
- while self.enable:
- # 若是pages数组中的内容小于2个
- if len(self.pages) < 2:
- try:
- # 获取新的页面中的段子们
- myPage = self.GetPage(str(self.page))
- self.page += 1
- self.pages.append(myPage)
- except:
- print '没法连接糗事百科!'
- else:
- time.sleep(1)
- def ShowPage(self,nowPage,page):
- for items in nowPage:
- print u'第%d页' % page , items[0] , items[1]
- myInput = raw_input()
- if myInput == "quit":
- self.enable = False
- break
- def Start(self):
- self.enable = True
- page = self.page
- print u'正在加载中请稍候......'
- # 新建一个线程在后台加载段子并存储
- thread.start_new_thread(self.LoadPage,())
- #----------- 加载处理糗事百科 -----------
- while self.enable:
- # 若是self的page数组中存有元素
- if self.pages:
- nowPage = self.pages[0]
- del self.pages[0]
- self.ShowPage(nowPage,page)
- page += 1
- #----------- 程序的入口处 -----------
- print u"""
- ---------------------------------------
- 程序:糗百爬虫
- 版本:0.3
- 做者:why
- 日期:2014-06-03
- 语言:Python 2.7
- 操做:输入quit退出阅读糗事百科
- 功能:按下回车依次浏览今日的糗百热点
- ---------------------------------------
- """
- print u'请按下回车浏览今日的糗百内容:'
- raw_input(' ')
- myModel = Spider_Model()
- myModel.Start()
- </pre><br><br>
[Python]网络爬虫(九):百度贴吧的网络爬虫(v0.4)源码及解析
百度贴吧的爬虫制做和糗百的爬虫制做原理基本相同,都是经过查看源码扣出关键数据,而后将其存储到本地txt文件。
源码下载:
http://download.csdn.net/detail/wxg694175346/6925583
用Python写的百度贴吧的网络爬虫。
使用方法:
新建一个BugBaidu.py文件,而后将代码复制到里面后,双击运行。
程序功能:
将贴吧中楼主发布的内容打包txt存储到本地。
原理解释:
首先,先浏览一下某一条贴吧,点击只看楼主并点击第二页以后url发生了一点变化,变成了:
http://tieba.baidu.com/p/2296712428?see_lz=1&pn=1
能够看出来,see_lz=1是只看楼主,pn=1是对应的页码,记住这一点为之后的编写作准备。
这就是咱们须要利用的url。接下来就是查看页面源码。
首先把题目抠出来存储文件的时候会用到。
能够看到百度使用gbk编码,标题使用h1标记:
一样,正文部分用div和class综合标记,接下来要作的只是用正则表达式来匹配便可。
运行截图:
生成的txt文件:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:百度贴吧爬虫
- # 版本:0.5
- # 做者:why
- # 日期:2013-05-16
- # 语言:Python 2.7
- # 操做:输入网址后自动只看楼主并保存到本地文件
- # 功能:将楼主发布的内容打包txt存储到本地。
- #---------------------------------------
- import string
- import urllib2
- import re
- #----------- 处理页面上的各类标签 -----------
- class HTML_Tool:
- # 用非 贪婪模式 匹配 \t 或者 \n 或者 空格 或者 超连接 或者 图片
- BgnCharToNoneRex = re.compile("(\t|\n| |<a.*?>|<img.*?>)")
- # 用非 贪婪模式 匹配 任意<>标签
- EndCharToNoneRex = re.compile("<.*?>")
- # 用非 贪婪模式 匹配 任意<p>标签
- BgnPartRex = re.compile("<p.*?>")
- CharToNewLineRex = re.compile("(<br/>|</p>|<tr>|<div>|</div>)")
- CharToNextTabRex = re.compile("<td>")
- # 将一些html的符号实体转变为原始符号
- replaceTab = [("<","<"),(">",">"),("&","&"),("&","\""),(" "," ")]
- def Replace_Char(self,x):
- x = self.BgnCharToNoneRex.sub("",x)
- x = self.BgnPartRex.sub("\n ",x)
- x = self.CharToNewLineRex.sub("\n",x)
- x = self.CharToNextTabRex.sub("\t",x)
- x = self.EndCharToNoneRex.sub("",x)
- for t in self.replaceTab:
- x = x.replace(t[0],t[1])
- return x
- class Baidu_Spider:
- # 申明相关的属性
- def __init__(self,url):
- self.myUrl = url + '?see_lz=1'
- self.datas = []
- self.myTool = HTML_Tool()
- print u'已经启动百度贴吧爬虫,咔嚓咔嚓'
- # 初始化加载页面并将其转码储存
- def baidu_tieba(self):
- # 读取页面的原始信息并将其从gbk转码
- myPage = urllib2.urlopen(self.myUrl).read().decode("gbk")
- # 计算楼主发布内容一共有多少页
- endPage = self.page_counter(myPage)
- # 获取该帖的标题
- title = self.find_title(myPage)
- print u'文章名称:' + title
- # 获取最终的数据
- self.save_data(self.myUrl,title,endPage)
- #用来计算一共有多少页
- def page_counter(self,myPage):
- # 匹配 "共有<span class="red">12</span>页" 来获取一共有多少页
- myMatch = re.search(r'class="red">(\d+?)</span>', myPage, re.S)
- if myMatch:
- endPage = int(myMatch.group(1))
- print u'爬虫报告:发现楼主共有%d页的原创内容' % endPage
- else:
- endPage = 0
- print u'爬虫报告:没法计算楼主发布内容有多少页!'
- return endPage
- # 用来寻找该帖的标题
- def find_title(self,myPage):
- # 匹配 <h1 class="core_title_txt" title="">xxxxxxxxxx</h1> 找出标题
- myMatch = re.search(r'<h1.*?>(.*?)</h1>', myPage, re.S)
- title = u'暂无标题'
- if myMatch:
- title = myMatch.group(1)
- else:
- print u'爬虫报告:没法加载文章标题!'
- # 文件名不能包含如下字符: \ / : * ? " < > |
- title = title.replace('\\','').replace('/','').replace(':','').replace('*','').replace('?','').replace('"','').replace('>','').replace('<','').replace('|','')
- return title
- # 用来存储楼主发布的内容
- def save_data(self,url,title,endPage):
- # 加载页面数据到数组中
- self.get_data(url,endPage)
- # 打开本地文件
- f = open(title+'.txt','w+')
- f.writelines(self.datas)
- f.close()
- print u'爬虫报告:文件已下载到本地并打包成txt文件'
- print u'请按任意键退出...'
- raw_input();
- # 获取页面源码并将其存储到数组中
- def get_data(self,url,endPage):
- url = url + '&pn='
- for i in range(1,endPage+1):
- print u'爬虫报告:爬虫%d号正在加载中...' % i
- myPage = urllib2.urlopen(url + str(i)).read()
- # 将myPage中的html代码处理并存储到datas里面
- self.deal_data(myPage.decode('gbk'))
- # 将内容从页面代码中抠出来
- def deal_data(self,myPage):
- myItems = re.findall('id="post_content.*?>(.*?)</div>',myPage,re.S)
- for item in myItems:
- data = self.myTool.Replace_Char(item.replace("\n","").encode('gbk'))
- self.datas.append(data+'\n')
- #-------- 程序入口处 ------------------
- print u"""#---------------------------------------
- # 程序:百度贴吧爬虫
- # 版本:0.5
- # 做者:why
- # 日期:2013-05-16
- # 语言:Python 2.7
- # 操做:输入网址后自动只看楼主并保存到本地文件
- # 功能:将楼主发布的内容打包txt存储到本地。
- #---------------------------------------
- """
- # 以某小说贴吧为例子
- # bdurl = 'http://tieba.baidu.com/p/2296712428?see_lz=1&pn=1'
- print u'请输入贴吧的地址最后的数字串:'
- bdurl = 'http://tieba.baidu.com/p/' + str(raw_input(u'http://tieba.baidu.com/p/'))
- #调用
- mySpider = Baidu_Spider(bdurl)
- mySpider.baidu_tieba()
[Python]网络爬虫(十):一个爬虫的诞生全过程(以山东大学绩点运算为例)
先来讲一下咱们学校的网站:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/zhxt_bks.html
查询成绩须要登陆,而后显示各学科成绩,可是只显示成绩而没有绩点,也就是加权平均分。
显然这样手动计算绩点是一件很是麻烦的事情。因此咱们能够用python作一个爬虫来解决这个问题。
1.决战前夜
先来准备一下工具:HttpFox插件。
这是一款http协议分析插件,分析页面请求和响应的时间、内容、以及浏览器用到的COOKIE等。
以我为例,安装在火狐上便可,效果如图:
能够很是直观的查看相应的信息。
点击start是开始检测,点击stop暂停检测,点击clear清除内容。
通常在使用以前,点击stop暂停,而后点击clear清屏,确保看到的是访问当前页面得到的数据。
2.深刻敌后
下面就去山东大学的成绩查询网站,看一看在登陆的时候,到底发送了那些信息。
先来到登陆页面,把httpfox打开,clear以后,点击start开启检测:
输入完了我的信息,确保httpfox处于开启状态,而后点击肯定提交信息,实现登陆。
这个时候能够看到,httpfox检测到了三条信息:
这时点击stop键,确保捕获到的是访问该页面以后反馈的数据,以便咱们作爬虫的时候模拟登录使用。
3.庖丁解牛
乍一看咱们拿到了三个数据,两个是GET的一个是POST的,可是它们究竟是什么,应该怎么用,咱们还一无所知。
因此,咱们须要挨个查看一下捕获到的内容。
先看POST的信息:
既然是POST的信息,咱们就直接看PostData便可。
能够看到一共POST两个数据,stuid和pwd。
而且从Type的Redirect to能够看出,POST完毕以后跳转到了bks_login2.loginmessage页面。
由此看出,这个数据是点击肯定以后提交的表单数据。
点击cookie标签,看看cookie信息:
没错,收到了一个ACCOUNT的cookie,而且在session结束以后自动销毁。
那么提交以后收到了哪些信息呢?
咱们来看看后面的两个GET数据。
先看第一个,咱们点击content标签能够查看收到的内容,是否是有一种生吞活剥的快感-。-HTML源码暴露无疑了:
看来这个只是显示页面的html源码而已,点击cookie,查看cookie的相关信息:
啊哈,原来html页面的内容是发送了cookie信息以后才接受到的。
再来看看最后一个接收到的信息:
大体看了一下应该只是一个叫作style.css的css文件,对咱们没有太大的做用。
4.冷静应战
既然已经知道了咱们向服务器发送了什么数据,也知道了咱们接收到了什么数据,基本的流程以下:
- 首先,咱们POST学号和密码--->而后返回cookie的值
- 而后发送cookie给服务器--->返回页面信息。
- 获取到成绩页面的数据,用正则表达式将成绩和学分单独取出并计算加权平均数。
OK,看上去好像很简单的样纸。那下面咱们就来试试看吧。
可是在实验以前,还有一个问题没有解决,就是POST的数据到底发送到了哪里?
再来看一下当初的页面:
很明显是用一个html框架来实现的,也就是说,咱们在地址栏看到的地址并非右边提交表单的地址。
那么怎样才能得到真正的地址-。-右击查看页面源代码:
嗯没错,那个name="w_right"的就是咱们要的登陆页面。
网站的原来的地址是:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/zhxt_bks.html
因此,真正的表单提交的地址应该是:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/xk_login.html
输入一看,果不其然:
靠竟然是清华大学的选课系统。。。目测是我校懒得作页面了就直接借了。。结果连标题都不改一下。。。
可是这个页面依旧不是咱们须要的页面,由于咱们的POST数据提交到的页面,应该是表单form的ACTION中提交到的页面。
也就是说,咱们须要查看源码,来知道POST数据到底发送到了哪里:
嗯,目测这个才是提交POST数据的地址。
整理到地址栏中,完整的地址应该以下:
http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login
(获取的方式很简单,在火狐浏览器中直接点击那个连接就能看到这个连接的地址了)
5.小试牛刀
接下来的任务就是:用python模拟发送一个POST的数据并取到返回的cookie值。
关于cookie的操做能够看看这篇博文:
http://blog.csdn.net/wxg694175346/article/details/8925978
咱们先准备一个POST的数据,再准备一个cookie的接收,而后写出源码以下:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山东大学爬虫
- # 版本:0.1
- # 做者:why
- # 日期:2013-07-12
- # 语言:Python 2.7
- # 操做:输入学号和密码
- # 功能:输出成绩的加权平均值也就是绩点
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- cookie = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- #须要POST的数据#
- postdata=urllib.urlencode({
- 'stuid':'201100300428',
- 'pwd':'921030'
- })
- #自定义一个请求#
- req = urllib2.Request(
- url = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login',
- data = postdata
- )
- #访问该连接#
- result = opener.open(req)
- #打印返回的内容#
- print result.read()
如此这般以后,再看看运行的效果:
ok,如此这般,咱们就算模拟登录成功了。
6.偷天换日
接下来的任务就是用爬虫获取到学生的成绩。
再来看看源网站。
开启HTTPFOX以后,点击查当作绩,发现捕获到了以下的数据:
点击第一个GET的数据,查看内容能够发现Content就是获取到的成绩的内容。
而获取到的页面连接,从页面源代码中右击查看元素,能够看到点击连接以后跳转的页面(火狐浏览器只须要右击,“查看此框架”,便可):
从而能够获得查当作绩的连接以下:
http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre
7.万事俱备
如今万事俱备啦,因此只须要把连接应用到爬虫里面,看看可否查看到成绩的页面。
从httpfox能够看到,咱们发送了一个cookie才能返回成绩的信息,因此咱们就用python模拟一个cookie的发送,以此来请求成绩的信息:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山东大学爬虫
- # 版本:0.1
- # 做者:why
- # 日期:2013-07-12
- # 语言:Python 2.7
- # 操做:输入学号和密码
- # 功能:输出成绩的加权平均值也就是绩点
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- #初始化一个CookieJar来处理Cookie的信息#
- cookie = cookielib.CookieJar()
- #建立一个新的opener来使用咱们的CookieJar#
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- #须要POST的数据#
- postdata=urllib.urlencode({
- 'stuid':'201100300428',
- 'pwd':'921030'
- })
- #自定义一个请求#
- req = urllib2.Request(
- url = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login',
- data = postdata
- )
- #访问该连接#
- result = opener.open(req)
- #打印返回的内容#
- print result.read()
- #打印cookie的值
- for item in cookie:
- print 'Cookie:Name = '+item.name
- print 'Cookie:Value = '+item.value
- #访问该连接#
- result = opener.open('http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre')
- #打印返回的内容#
- print result.read()
按下F5运行便可,看看捕获到的数据吧:
既然这样就没有什么问题了吧,用正则表达式将数据稍稍处理一下,取出学分和相应的分数就能够了。
8.手到擒来
这么一大堆html源码显然是不利于咱们处理的,下面要用正则表达式来抠出必须的数据。
关于正则表达式的教程能够看看这个博文:
http://blog.csdn.net/wxg694175346/article/details/8929576
咱们来看当作绩的源码:
既然如此,用正则表达式就易如反掌了。
咱们将代码稍稍整理一下,而后用正则来取出数据:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山东大学爬虫
- # 版本:0.1
- # 做者:why
- # 日期:2013-07-12
- # 语言:Python 2.7
- # 操做:输入学号和密码
- # 功能:输出成绩的加权平均值也就是绩点
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- import re
- class SDU_Spider:
- # 申明相关的属性
- def __init__(self):
- self.loginUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login' # 登陆的url
- self.resultUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre' # 显示成绩的url
- self.cookieJar = cookielib.CookieJar() # 初始化一个CookieJar来处理Cookie的信息
- self.postdata=urllib.urlencode({'stuid':'201100300428','pwd':'921030'}) # POST的数据
- self.weights = [] #存储权重,也就是学分
- self.points = [] #存储分数,也就是成绩
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
- def sdu_init(self):
- # 初始化连接而且获取cookie
- myRequest = urllib2.Request(url = self.loginUrl,data = self.postdata) # 自定义一个请求
- result = self.opener.open(myRequest) # 访问登陆页面,获取到必须的cookie的值
- result = self.opener.open(self.resultUrl) # 访问成绩页面,得到成绩的数据
- # 打印返回的内容
- # print result.read()
- self.deal_data(result.read().decode('gbk'))
- self.print_data(self.weights);
- self.print_data(self.points);
- # 将内容从页面代码中抠出来
- def deal_data(self,myPage):
- myItems = re.findall('<TR>.*?<p.*?<p.*?<p.*?<p.*?<p.*?>(.*?)</p>.*?<p.*?<p.*?>(.*?)</p>.*?</TR>',myPage,re.S) #获取到学分
- for item in myItems:
- self.weights.append(item[0].encode('gbk'))
- self.points.append(item[1].encode('gbk'))
- # 将内容从页面代码中抠出来
- def print_data(self,items):
- for item in items:
- print item
- #调用
- mySpider = SDU_Spider()
- mySpider.sdu_init()
水平有限,,正则是有点丑,。运行的效果如图:
ok,接下来的只是数据的处理问题了。。
9.凯旋而归
完整的代码以下,至此一个完整的爬虫项目便完工了。
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山东大学爬虫
- # 版本:0.1
- # 做者:why
- # 日期:2013-07-12
- # 语言:Python 2.7
- # 操做:输入学号和密码
- # 功能:输出成绩的加权平均值也就是绩点
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- import re
- import string
- class SDU_Spider:
- # 申明相关的属性
- def __init__(self):
- self.loginUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login' # 登陆的url
- self.resultUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre' # 显示成绩的url
- self.cookieJar = cookielib.CookieJar() # 初始化一个CookieJar来处理Cookie的信息
- self.postdata=urllib.urlencode({'stuid':'201100300428','pwd':'921030'}) # POST的数据
- self.weights = [] #存储权重,也就是学分
- self.points = [] #存储分数,也就是成绩
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
- def sdu_init(self):
- # 初始化连接而且获取cookie
- myRequest = urllib2.Request(url = self.loginUrl,data = self.postdata) # 自定义一个请求
- result = self.opener.open(myRequest) # 访问登陆页面,获取到必须的cookie的值
- result = self.opener.open(self.resultUrl) # 访问成绩页面,得到成绩的数据
- # 打印返回的内容
- # print result.read()
- self.deal_data(result.read().decode('gbk'))
- self.calculate_date();
- # 将内容从页面代码中抠出来
- def deal_data(self,myPage):
- myItems = re.findall('<TR>.*?<p.*?<p.*?<p.*?<p.*?<p.*?>(.*?)</p>.*?<p.*?<p.*?>(.*?)</p>.*?</TR>',myPage,re.S) #获取到学分
- for item in myItems:
- self.weights.append(item[0].encode('gbk'))
- self.points.append(item[1].encode('gbk'))
- #计算绩点,若是成绩还没出来,或者成绩是优秀良好,就不运算该成绩
- def calculate_date(self):
- point = 0.0
- weight = 0.0
- for i in range(len(self.points)):
- if(self.points[i].isdigit()):
- point += string.atof(self.points[i])*string.atof(self.weights[i])
- weight += string.atof(self.weights[i])
- print point/weight
- #调用
- mySpider = SDU_Spider()
- mySpider.sdu_init()
[Python]网络爬虫(11):亮剑!爬虫框架小抓抓Scrapy闪亮登场!
前面十章爬虫笔记陆陆续续记录了一些简单的Python爬虫知识,
用来解决简单的贴吧下载,绩点运算天然不在话下。
不过要想批量下载大量的内容,好比知乎的全部的问答,那便显得游刃不有余了点。
因而乎,爬虫框架Scrapy就这样出场了!
Scrapy = Scrach+Python,Scrach这个单词是抓取的意思,
暂且能够叫它:小抓抓吧。
小抓抓的官网地址:点我点我。
那么下面来简单的演示一下小抓抓Scrapy的安装流程。
具体流程参照:官网教程
友情提醒:必定要按照Python的版本下载,要否则安装的时候会提醒找不到Python。建议你们安装32位是由于有些版本的必备软件64位很差找。
1.安装Python(建议32位)
建议安装Python2.7.x,3.x貌似还不支持。
安装完了记得配置环境,将python目录和python目录下的Scripts目录添加到系统环境变量的Path里。
在cmd中输入python若是出现版本信息说明配置完毕。
2.安装lxml
lxml是一种使用 Python 编写的库,能够迅速、灵活地处理 XML。点击这里选择对应的Python版本安装。
3.安装setuptools
用来安装egg文件,点击这里下载python2.7的对应版本的setuptools。
4.安装zope.interface
可使用第三步下载的setuptools来安装egg文件,如今也有exe版本,点击这里下载。
5.安装Twisted
Twisted是用Python实现的基于事件驱动的网络引擎框架,点击这里下载。
6.安装pyOpenSSL
pyOpenSSL是Python的OpenSSL接口,点击这里下载。
7.安装win32py
提供win32api,点击这里下载
8.安装Scrapy
终于到了激动人心的时候了!安装了那么多小部件以后终于轮到主角登场。
直接在cmd中输入easy_install scrapy回车便可。
9.检查安装
打开一个cmd窗口,在任意位置执行scrapy命令,获得下列页面,表示环境配置成功。
[Python]网络爬虫(12):爬虫框架Scrapy的第一个爬虫示例入门教程
(建议你们多看看官网教程:教程地址)
咱们使用dmoz.org这个网站来做为小抓抓一展身手的对象。
首先先要回答一个问题。
问:把网站装进爬虫里,总共分几步?
答案很简单,四步:
- 新建项目 (Project):新建一个新的爬虫项目
- 明确目标(Items):明确你想要抓取的目标
- 制做爬虫(Spider):制做爬虫开始爬取网页
- 存储内容(Pipeline):设计管道存储爬取内容
好的,基本流程既然肯定了,那接下来就一步一步的完成就能够了。
1.新建项目(Project)
在空目录下按住Shift键右击,选择“在此处打开命令窗口”,输入一下命令:
其中,tutorial为项目名称。
能够看到将会建立一个tutorial文件夹,目录结构以下:
- tutorial/
- scrapy.cfg
- tutorial/
- __init__.py
- items.py
- pipelines.py
- settings.py
- spiders/
- __init__.py
- ...
下面来简单介绍一下各个文件的做用:
- scrapy.cfg:项目的配置文件
- tutorial/:项目的Python模块,将会从这里引用代码
- tutorial/items.py:项目的items文件
- tutorial/pipelines.py:项目的pipelines文件
- tutorial/settings.py:项目的设置文件
- tutorial/spiders/:存储爬虫的目录
2.明确目标(Item)
在Scrapy中,items是用来加载抓取内容的容器,有点像Python中的Dic,也就是字典,可是提供了一些额外的保护减小错误。
通常来讲,item能够用scrapy.item.Item类来建立,而且用scrapy.item.Field对象来定义属性(能够理解成相似于ORM的映射关系)。
接下来,咱们开始来构建item模型(model)。
首先,咱们想要的内容有:
- 名称(name)
- 连接(url)
- 描述(description)
修改tutorial目录下的items.py文件,在本来的class后面添加咱们本身的class。
由于要抓dmoz.org网站的内容,因此咱们能够将其命名为DmozItem:
- # Define here the models for your scraped items
- #
- # See documentation in:
- # http://doc.scrapy.org/en/latest/topics/items.html
- from scrapy.item import Item, Field
- class TutorialItem(Item):
- # define the fields for your item here like:
- # name = Field()
- pass
- class DmozItem(Item):
- title = Field()
- link = Field()
- desc = Field()
刚开始看起来可能会有些看不懂,可是定义这些item能让你用其余组件的时候知道你的 items究竟是什么。
能够把Item简单的理解成封装好的类对象。
3.制做爬虫(Spider)
制做爬虫,整体分两步:先爬再取。
也就是说,首先你要获取整个网页的全部内容,而后再取出其中对你有用的部分。
3.1爬
Spider是用户本身编写的类,用来从一个域(或域组)中抓取信息。
他们定义了用于下载的URL列表、跟踪连接的方案、解析网页内容的方式,以此来提取items。
要创建一个Spider,你必须用scrapy.spider.BaseSpider建立一个子类,并肯定三个强制的属性:
- name:爬虫的识别名称,必须是惟一的,在不一样的爬虫中你必须定义不一样的名字。
- start_urls:爬取的URL列表。爬虫从这里开始抓取数据,因此,第一次下载的数据将会从这些urls开始。其余子URL将会从这些起始URL中继承性生成。
- parse():解析的方法,调用的时候传入从每个URL传回的Response对象做为惟一参数,负责解析并匹配抓取的数据(解析为item),跟踪更多的URL。
这里能够参考宽度爬虫教程中说起的思想来帮助理解,教程传送:[Java] 知乎下巴第5集:使用HttpClient工具包和宽度爬虫。
也就是把Url存储下来并依此为起点逐步扩散开去,抓取全部符合条件的网页Url存储起来继续爬取。
下面咱们来写第一只爬虫,命名为dmoz_spider.py,保存在tutorial\spiders目录下。
dmoz_spider.py代码以下:
- from scrapy.spider import Spider
- class DmozSpider(Spider):
- name = "dmoz"
- allowed_domains = ["dmoz.org"]
- start_urls = [
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
- ]
- def parse(self, response):
- filename = response.url.split("/")[-2]
- open(filename, 'wb').write(response.body)
从parse函数能够看出,将连接的最后两个地址取出做为文件名进行存储。
而后运行一下看看,在tutorial目录下按住shift右击,在此处打开命令窗口,输入:
运行结果如图:
报错了:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xb0 in position 1: ordinal not in range(128)
运行第一个Scrapy项目就报错,真是命运多舛。
应该是出了编码问题,谷歌了一下找到了解决方案:
在python的Lib\site-packages文件夹下新建一个sitecustomize.py:
再次运行,OK,问题解决了,看一下结果:
最后一句INFO: Closing spider (finished)代表爬虫已经成功运行而且自行关闭了。
包含 [dmoz]的行 ,那对应着咱们的爬虫运行的结果。
能够看到start_urls中定义的每一个URL都有日志行。
还记得咱们的start_urls吗?
http://www.dmoz.org/Computers/Programming/Languages/Python/Books
http://www.dmoz.org/Computers/Programming/Languages/Python/Resources
由于这些URL是起始页面,因此他们没有引用(referrers),因此在它们的每行末尾你会看到 (referer: <None>)。
在parse 方法的做用下,两个文件被建立:分别是 Books 和 Resources,这两个文件中有URL的页面内容。
那么在刚刚的电闪雷鸣之中到底发生了什么呢?
首先,Scrapy为爬虫的 start_urls属性中的每一个URL建立了一个 scrapy.http.Request 对象 ,并将爬虫的parse 方法指定为回调函数。
而后,这些 Request被调度并执行,以后经过parse()方法返回scrapy.http.Response对象,并反馈给爬虫。
3.2取
爬取整个网页完毕,接下来的就是的取过程了。
光存储一整个网页仍是不够用的。
在基础的爬虫里,这一步能够用正则表达式来抓。
在Scrapy里,使用一种叫作 XPath selectors的机制,它基于 XPath表达式。
若是你想了解更多selectors和其余机制你能够查阅资料:点我点我
这是一些XPath表达式的例子和他们的含义
- /html/head/title: 选择HTML文档<head>元素下面的<title> 标签。
- /html/head/title/text(): 选择前面提到的<title> 元素下面的文本内容
- //td: 选择全部 <td> 元素
- //div[@class="mine"]: 选择全部包含 class="mine" 属性的div 标签元素
以上只是几个使用XPath的简单例子,可是实际上XPath很是强大。
能够参照W3C教程:点我点我。
必须经过一个 Response 对象对他们进行实例化操做。
你会发现Selector对象展现了文档的节点结构。所以,第一个实例化的selector必与根节点或者是整个目录有关 。
在Scrapy里面,Selectors 有四种基础的方法(点击查看API文档):
- xpath():返回一系列的selectors,每个select表示一个xpath参数表达式选择的节点
- css():返回一系列的selectors,每个select表示一个css参数表达式选择的节点
- extract():返回一个unicode字符串,为选中的数据
- re():返回一串一个unicode字符串,为使用正则表达式抓取出来的内容
3.3xpath实验
下面咱们在Shell里面尝试一下Selector的用法。
实验的网址:http://www.dmoz.org/Computers/Programming/Languages/Python/Books/
熟悉完了实验的小白鼠,接下来就是用Shell爬取网页了。
进入到项目的顶层目录,也就是第一层tutorial文件夹下,在cmd中输入:
回车后能够看到以下的内容:
在Shell载入后,你将得到response回应,存储在本地变量 response中。
因此若是你输入response.body,你将会看到response的body部分,也就是抓取到的页面内容:
或者输入response.headers 来查看它的 header部分:
如今就像是一大堆沙子握在手里,里面藏着咱们想要的金子,因此下一步,就是用筛子摇两下,把杂质出去,选出关键的内容。
selector就是这样一个筛子。
在旧的版本中,Shell实例化两种selectors,一个是解析HTML的 hxs 变量,一个是解析XML 的 xxs 变量。
而如今的Shell为咱们准备好的selector对象,sel,能够根据返回的数据类型自动选择最佳的解析方案(XML or HTML)。
而后咱们来捣弄一下!~
要完全搞清楚这个问题,首先先要知道,抓到的页面究竟是个什么样子。
好比,咱们要抓取网页的标题,也就是<title>这个标签:
能够输入:
结果就是:
这样就能把这个标签取出来了,用extract()和text()还能够进一步作处理。
备注:简单的罗列一下有用的xpath路径表达式:
表达式 | 描述 |
---|---|
nodename | 选取此节点的全部子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
所有的实验结果以下,In[i]表示第i次实验的输入,Out[i]表示第i次结果的输出(建议你们参照:W3C教程):
- In [1]: sel.xpath('//title')
- Out[1]: [<Selector xpath='//title' data=u'<title>Open Directory - Computers: Progr'>]
- In [2]: sel.xpath('//title').extract()
- Out[2]: [u'<title>Open Directory - Computers: Programming: Languages: Python: Books</title>']
- In [3]: sel.xpath('//title/text()')
- Out[3]: [<Selector xpath='//title/text()' data=u'Open Directory - Computers: Programming:'>]
- In [4]: sel.xpath('//title/text()').extract()
- Out[4]: [u'Open Directory - Computers: Programming: Languages: Python: Books']
- In [5]: sel.xpath('//title/text()').re('(\w+):')
- Out[5]: [u'Computers', u'Programming', u'Languages', u'Python']
固然title这个标签对咱们来讲没有太多的价值,下面咱们就来真正抓取一些有意义的东西。
使用火狐的审查元素咱们能够清楚地看到,咱们须要的东西以下:
咱们能够用以下代码来抓取这个<li>标签:
从<li>标签中,能够这样获取网站的描述:
能够这样获取网站的标题:
能够这样获取网站的超连接: