Python教程

文盲的Python入门日记:第二十八天,封装一个自定义爬虫类,用来执行日常的采集(二)

本文主要是介绍文盲的Python入门日记:第二十八天,封装一个自定义爬虫类,用来执行日常的采集(二),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1、实例化采集类后,自带一些header信息,类似user-agent、accept之类的,能不手动添加就不手动添加(已实现)

2、在执行了采集后,获取采集到的响应头,解析其中的数据,该记录的记录该执行的执行,在下次调用采集方法时继承获取到的信息(已实现)

3、可以采集纯文本内容,也可以采集二进制流,方便采集页面和下载相关文档(已实现)

4、支持不同的字符编码,响应编码,比如gbk、utf8等,比如gzip、deflate等(已实现)

5、支持不同的请求方法,比如get、put、post、delete、head等(已实现)

6、不管是否采集异常,都能返回状态码(已实现)

7、可以伪造、添加各种头信息,伪造、添加cookie之类的信息,类似 Oauth:xxxx,Signture:xxx等

8、支持301、302之类的自动跳转采集,支持meta自动跳转采集

9、自动完成网址补全,不需要我们在根据采集目标,提取到链接后自己再计算

10、如果可以,尽量支持异步采集

11、如果可以,尽量支持事件委托

12、如果可以,尽量支持proxy

13、如果可以,尽量支持断点续传下载和上传

前一片文章我们建立了一个自定义的爬虫类,已经实现了部分需求了,本文将继续实现剩余的需求。在继续修改我们的类之前,先谈谈spyder。

-----------------------------

为什么要先聊聊spyder呢,主要是因为,老顾找不到一个完整的python包的使用手册,资料都是太零碎的东西,整理起来很麻烦,没有像php手册,或者msdn命名空间介绍一样的东西。就像之前几篇文章一样,每次想做的什么,都要先百度半天,结果本系列文章极度难产。那么spyder有什么好聊的呢?我们来看看。。。

打开spyder,如界面所示,他分成了几个不同的区域,通常,我们都在左边输入py代码,右下可以看输出结果,右上。。。没好好利用起来啊

比如左侧,我们输入一段代码

[n for n in range(1,100)]

选中并执行这段代码(F9),右下控制台就可以出现反馈信息

这是一个很方便的调试方法了,当代码中存在交互指令,例如input的时候,右下控制台可以进行交互操作,录入一些信息,这个就不多说了,大家都会用

然后,来想办法利用上右上区域,来辅助我们的学习和工作

在代码区域输入代码

import requests
req = requests()

然后将鼠标指向 requests()这个方法,界面发生变动了

当我们在浮动的这个提示上点一下后,右上区域就发生了内容改变,Help页里出现了好多信息

嗯。。。这就很方便了,可以查看方法的具体使用方式了,可惜的是,他并没有像vs里一样,将类的方法全部列出,只能靠类.,然后等待spyder响应后,出现可以使用的属性或方法,然后去选择我们可能需要的内容

现在,我们把指令修改下

import requests
req = requests.Request(url='https://www.baidu.com',method='GET')

运行这些代码,将右上的区域切换到变量资源管理器里(Variable Explorer)

我们在左侧运行的代码所产生的变量,都会在这里列出,只要没有清除,关闭,一直就保留在这里了,比如刚才的req,我们就可以看到的他描述,用鼠标双击这个变量看看

这个就是一个比较完整的实例属性和方法了,他里面将这个实例所有可用的方法和属性列出,并可展开查看,这样,就能避免之前我们找不到方法的问题了,直接给一个变量赋值成该类,然后到这边查看,是不是可以代替了vs里的对象浏览器了。

嗯,就简单的说两句spyder,毕竟老顾学python完全是自己摸索,而且岁数大了,看其他视频感觉很不适应,所以就慢慢来吧。至于其他的python 相关的IDE有没有提供这样的查阅方式,老顾也不了解,刚转行的同学们可以自行摸索。然后,我们回归主题,继续搞我们的爬虫类。

------------------------------------------------

7、可以伪造、添加各种头信息,伪造、添加cookie之类的信息,类似 Oauth:xxxx,Signture:xxx等

一般来说,同一个站点很少有需要频繁改动头信息,cookie信息的地方,反爬的网站咱们另说,等进入采集实战的时候再进行应对,咱先说普通的网站。所以,实例化一次,并设置好这些信息,基本上这个实例就可以进行整站采集了,线程问题以后再说。

那么,我们这次把目光瞄准到天眼查这个站,试着根据他的响应,来调整我们的代码,使之能伪造cookie,接收header

先来次没调整前的采集

from spider import Ajax

ajax = Ajax()
html = ajax.Http('https://www.tianyancha.com',Ajax.Method.GET)
print(ajax.status,html)

很好,可以正常采集天眼查首页,但我们知道,天眼查会返回一些cookie信息,我们现在并没有接收,所以,进行下查找,看看cookie到底存放在哪里

因为对各种包都不熟悉,所以先在爬虫类里面添加两个属性

	@property
	def ResposeHeader(self):
		return self.__headers
	
	@property
	def Session(self):
		return self.__session

一个是用来返回响应头信息的,一个是用来返回会话信息的,让我们看看

好家伙。。。响应头居然有这么多数据么?548个?哦,展开一看,没那么多,548字节啊,吓我一大跳。

把响应头信息粘贴出来,整理一下看看

{
'Date': 'Wed, 30 Jun 2021 02:25:43 GMT', 
'Content-Type': 'text/html; charset=utf-8', 
'Transfer-Encoding': 'chunked', 
'Connection': 'keep-alive', 
'Set-Cookie': 'aliyungf_tc=8e44b1cb0fc5f37d29864918aa197ec6ed802b989655bf8732efdcd291861558; Path=/; HttpOnly, acw_tc=76b20f8c16250199433016427e4b75bd21ba7840934a083d22e010ebf1aedd;path=/;HttpOnly;Max-Age=1800, csrfToken=s4i4TN-WIKaLgWAXW3qHwbK5; path=/; secure, TYCID=784de430d94a11eb8216f7b2b73bb5b3; path=/; expires=Fri, 30 Jun 2023 02:25:43 GMT; domain=.tianyancha.com', 
'Content-Encoding': 'gzip'
}

哦吼,发现第一个关键信息,Set-Cookie,这个就是服务器发给浏览器的cookie信息了,Set-Cookie是其中的一种方式,记下,一会处理

然后,再看看会话里都有什么

很显然,会话里也保持了cookies的信息,里面有4个cookie了

print(ajax.Session.cookies)

<RequestsCookieJar[<Cookie TYCID=784de430d94a11eb8216f7b2b73bb5b3 for .tianyancha.com/>, <Cookie acw_tc=76b20f8c16250199433016427e4b75bd21ba7840934a083d22e010ebf1aedd for www.tianyancha.com/>, <Cookie aliyungf_tc=8e44b1cb0fc5f37d29864918aa197ec6ed802b989655bf8732efdcd291861558 for www.tianyancha.com/>, <Cookie csrfToken=s4i4TN-WIKaLgWAXW3qHwbK5 for www.tianyancha.com/>]>

得到了一个不知道什么对象的列表,先不管,总之,这里也有cookies,所以,现在我们需要自己定义一个cookies变量,存储这些信息,并在下次采集时继承进来,嗯,也得支持从外边添加cookie。另外,老顾注意到了这个cookie里的domain问题,伪造cookie,domain信息也很重要哦,参考老顾的另一个文章https://blog.csdn.net/superwfei/article/details/9198283?spm=1001.2014.3001.5502,部分网站对cookie验证时,会更严格一些,对不再特定domian的cookie是不予承认的,期待python的cookie处理。。。。

在__init__里,追加一个属性

self.cookies = requests.utils.cookiejar_from_dict({})

然后,我们就可以通过实例添加cookie了

import re
from spider import Ajax

ajax = Ajax()
# 为了获取初始cookie,先访问下天眼查首页
ajax.Http('https://www.tianyancha.com')
ajax.cookies.set('tyc-user-info', '{***********}', domain='.tianyancha.com')
ajax.cookies.set('auth_token', '****************', domain='.tianyancha.com')
html = ajax.Http('https://www.tianyancha.com/company/2968548568',Ajax.Method.GET)
# 显示现在已有cookies
print(ajax.cookies)
print(re.findall(r'<div class="detail ">[\s\S]*?(?=<div class="card-tag company-header-card">)',html,re.I))

很好,cookie伪造成功了,对于域名带前缀点的问题直接就不是问题,证据就是,他没提示登录,其次,电话号码没有星号隐藏。

在这段代码中,我们使用了两次Http方法,第一次是获取初始cookie,如果没有初始cookie,那么我们就需要通过cookies.set方法,自己添加初始cookie,老顾懒得添加,让他自动获取好了,然后第二次采集前,我们追加了两个cookie,并继承了第一次采集的cookie,所以,正确得到了我们的期望结果,而cookie在同一实例内,只需要添加一次,我们这个ajax实例再次使用Http访问天眼查其他企业信息时,就不需要再关注cookie信息了。

剩下的就是伪造请求头了,之前,我们的Header定义,是一个固定的词典,现在要对他进行改装一下,变成动态的词典,同样,在__init__里追加两个赋值

		self.__requestHeaders = {}
		self.__refreshRequestHeaders()

调整Header属性的实现

	@property
	def Header(self):
		return self.__requestHeaders

然后,增加一个私有方法,用来初始化请求头信息

	def __refreshRequestHeaders(self):
		self.__requestHeaders.update({'refer':self.refer
			,'user-agent':self.agent
			,'accept':self.accept
			,'accept-encoding':self.encoding
			,'accept-language':self.lang
			,'cache-control':self.cache})

最后,我们给Ajax类增加一个公开的方法,AddHeader,用来将信息添加到请求头中

	def AddHeader(self,key:str = None,val:str = None,dic:dict = None):
		if dic != None and isinstance(dic,dict):
			self.__requestHeaders.update(dic)
		if key != None and val != None:
			self.__requestHeaders.update({key:val})
from spider import Ajax

ajax = Ajax()
ajax.AddHeader(dic={'oauth':'userinfo'})
ajax.AddHeader('pwd','***')
print(ajax.Header)

运行一下,看看结果

很好,请求头信息已更新了,虽然有时候需要删除一些请求头,我这里就不去实现了,大家有需求的话,自行实现就可以了,那么第七个需求也告一段落了,下边开始处理跳转问题。

8、支持301、302之类的自动跳转采集,支持meta自动跳转采集

让我们找一个带跳转的网址,比如:http://m6z.cn/6uVNKg,一个用短链接生成器生成的地址

来,我们尝试下,这个请求会发生什么

from spider import Ajax
ajax = Ajax()
html = ajax.Http('http://m6z.cn/6uVNKg')
print(ajax.status,ajax.ResposeHeader)
print(html)

他自动跳过去了!返回的状态码也是200!中间301、302的过程省略掉了!https://blog.csdn.net/qq_36145663/article/details/101703090,原来,你不想自动301、302,还得设置这个参数allow_redirects=False,算了,让他自动跳吧,不过,我们还是加一个开关,可以用来关闭这个自动跳转,在__init__里追加一个属性

self.redirect = True

在发送请求位置修改 send 参数

res = self.__session.send(request=pre,allow_redirects=self.redirect)

然后就可以成功的禁止自动301,302了

然后,自动跳转,还需要支持meta的跳转,这个稍后再说吧,因为不管是meta跳转,还是js跳转,都涉及到一个网址补全的问题,咱们先解决了这个,再回来支持meta跳转和js跳转

9、自动完成网址补全,不需要我们在根据采集目标,提取到链接后自己再计算

在日常采集过程中,我们经常会碰到页面内链接地址缺少域名,有的则是有域名但没有协议。。。还有其他各种不应该出现的协议。。。。HMMMMMMM,反正经历的多了,自然就知道

这次,我们用政采网ccgp.gov.cn的首页来实验一下

from spider import Ajax
ajax = Ajax()
#html = ajax.Http('http://news.baidu.com/ns?word=school&ie=gb2312&cl=2&rn=20&ct=0&tn=newsrss&class=0')
html = ajax.Http('http://www.ccgp.gov.cn/')
print(html)

可以看到,页面内N多的链接都是没有网址的,所以,我们需要在采集的时候就处理掉,得到的完整的链接地址,方便我们后续处理。这个时候,该正则大显身手了。对了,在做这个补全之前,我们看看scrapy有没有补全,好像用scrapy的人很多。

行吧,咱也来次最简单的 scrapy 采集,不管那么多,只采集各首页看看

先在命令行运行几个指令

d:\>pip install scrapy

d:\>scrapy startproject ccgp

d:\>cd ccgp

d:\ccgp>scrapy genspider ccgp_gather www.ccgp.gov.cn

的确是很简单就建立了一个针对 ccgp 的采集

然后修改其中一些文件

找到 settings.py,修改 robotstxt_obey,不验证robots.txt

找到 middlewares.py,修改 process_request 方法,在这里追加上 user-agent 信息

找到 ccgp_gather.py,修改 parse 方法,保存我们采集到的首页内容

然后回到命令行,运行采集

d:\ccgp>scrapy crawl ccgp_gather

很好,这个页面被采集下来了,我们来瞅瞅看

得嘞,他也没有进行url补全,顺带一说,感觉用 scrapy 做采集相对来说更麻烦了一点,我以前已经建立了n多的xml,针对自己的采集规则有完整的内容了,什么翻页采集,什么时间范围内采集,什么标题过滤,我们做采集,很少有整站采集,也很少有无脑采集,所以这个scrapy如果要实现以上这些需求,感觉还是挺麻烦的,每个站点都这么搞一次。。。我不如把所有站点信息放到一个xml里,用统一的规则,使用自己的爬虫解析器来一次采集多个站点。总而言之,scrapy和老顾是有缘无分了。但是,如果使用scrapy也并不影响阅读本文哦,大家可不要放弃继续阅读哦。

回到我们自己的url补全上来,再在 Ajax 类里追加一个私有方法 __url_complemented,在 http方法,return html 前,用这个方法修正一下再返回

嗯。。。。。分析下,哪些地方是url?同学们自己列举下哦

有 href ,很常见的,a标签,link 标签

有 src ,也很常见的,script标签,img标签,embed等

还有一些容易被忽略的,url,存在于style中,样式文件中,meta中。。。。

以及更不容易发现的,location,open,存在于 js 之中。。。。action,存在于 form 标签

好了好了,越说越复杂了。。。我这里仅仅先实现前两个啊,再加一个meta,其他的我不考虑哦

做url补全其实也很简单,用正则把 url 提取出来,验证 url 是否是合法的 url,当然特例要排除,什么 about:blank 啦,什么 file:/// 这种本地文件啦,什么 base64 数据(图片src可能会有这个情况)啦。。。总之,只对需要进行补全的 url 进行计算,人本身已经是带协议的就不再操作了

哪些需要补全呢?

1、没有带协议的,比如 //blog.csdn.net,鬼知道他是 http还是https。。。其实这个是由当前页面协议决定的,你在 http域名页面点这个链接,这个结果就是 http://blog.csdn.net,你在https域名页面点开这个链接,结果就是 https://blog.csdn.net

2、链接地址路径不全的,比如 /superwfei,需要补全上域名,得到 blog.csdn.net/superwfei。这种情况相对来说比较复杂,有可能会碰到需要路径计算的时候,比如 ../../../image/xxx.shtml,也有可能碰到不太规范的路径 ..../image/xxx.shtml

老顾碰到的基本就这两种情况,如果有其它情况的,可以告知老顾,再继续研究研究

下面是实现代码 

	def __url_complemented(self,html):
		html = re.sub('''(url|src|href)\\s*=\\s*(['"]?)([^'"]*)(\\2|[\\s\\r\\n\\t])''',self.__url_replace,html,count=0,flags=re.I)
		return html

	def __url_replace(self,m):
		url = m.group(3).strip()
		# about:setting、about:blank 类型的,javascript:void(0) 类型的,#类型的,原样返回
		if re.search('^(#.*|javascript:.*|[a-z_-]+:[a-z_-]+)$',url,re.I):
			return m.string[m.span()[0]:m.span()[1]]
		# 带有协议的,原样返回,例如 https://、ftp://、file://、tencent://等
		if re.search('^[a-z]+://',url,re.I):
			return m.string[m.span()[0]:m.span()[1]]
		# base64 格式,原样返回
		if re.search('^base64',url,re.I):
			return m.string[m.span()[0]:m.span()[1]]
		root = re.sub(r'^([a-z]+:/{2,}[^/]+).*','\\1/',self.current_url.strip(),re.I)
		if re.search('^/(?!/)',url,re.I):
			url = re.sub('^/',root,url,re.I)
		elif re.search('^//',url):
			url = re.sub('^([a-z]+:)//.*$','\\1',root,re.I) + url
		else:
			path = re.sub('/[^/]*$','',self.current_url) + '/'
			p = re.search(r'^[\./]+',url,re.I)
			if p:
				# 具有 ./ 计算路径
				# 获取开头的全部计算路径
				p = p.string[p.span()[0]:p.span()[1]]
				# 去掉路径中 ./ 后,剩余的点的数量,就是路径向上路径的层级
				p = re.sub(r'\./','',p)
				# 获得剩余点的数量,得到层级
				p = len(p)
				pth = path
				for i in range(p):
					pth = re.sub('[^/]+/','',pth,re.I)
				if len(pth)<len(root):
					pth = root
				url = pth + re.sub(r'^[\./]+','',url)
			else:
				# 无 ./ 计算路径,当前路径
				url = path + url
		return m.group(1) + '=' + m.group(2) + url + m.group(4)

在 Http 方法,return 前追加一行

		self.html = self.__url_complemented(self.html)

然后,我们再次对 ccgp 首页进行一下采集,看看链接都有什么

import re

from spider import Ajax
ajax = Ajax()
#html = ajax.Http('http://news.baidu.com/ns?word=school&ie=gb2312&cl=2&rn=20&ct=0&tn=newsrss&class=0')
html = ajax.Http('http://www.ccgp.gov.cn/')
urls = re.findall('''(url|src|href)\\s*=\\s*(['"]?)([^'"]*)(\\2|[\\s\\r\\n\\t])''',html,re.I)
print(len(urls))
for i in urls:
    print(i[0],'=',i[1],i[2],i[3])

嗯,本次采集,一共344个网址,除了 # 和 javascript:,其他的网址都有 http或https协议,是完整的url了

包括图片、下载地址、页面地址,这就方便很多了,需要注意的是,因为无法自动判定给出的采集地址到底是目录,还是文件,所以需要人为协助一下,路径的,传入的参数后边追加一个/哦,这样我们就方便计算路径了

------------------------------------------------------

那么,今天我们就先这样,本文完成了采集类的7、8、9三个需求,暂时日常采集就可以满足了,下一篇文章,我们做个实战,然后有机会了,再继续完善后边的需求。js自动跳转和meta自动跳转,也一起放到下次完善的时候再说吧。

这篇关于文盲的Python入门日记:第二十八天,封装一个自定义爬虫类,用来执行日常的采集(二)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!