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自动跳转,也一起放到下次完善的时候再说吧。