教程:文盲的Python入门日记:第二十八天,封装一个自定义爬虫类
优采云 发布时间: 2022-10-20 16:29教程:文盲的Python入门日记:第二十八天,封装一个自定义爬虫类
1.实例化采集类后,自带一些头信息,类似于user-agent,accept等,可以手动添加,不用手动添加(已经实现)
2、执行采集后,获取采集收到的响应头,解析里面的数据,记录执行的执行情况,下次调用采集方法时继承. 信息(已实施)
3.可以采集纯文本内容或采集二进制流,方便采集页面和下载相关文档(已经实现)
4.支持不同的字符编码、响应编码,如gbk、utf8等,如gzip、deflate等(已实现)
5.支持不同的请求方式,如get、put、post、delete、head等(已实现)
6、无论采集是否异常,都可以返回状态码(实现)
7.可以伪造添加各种头信息,伪造添加cookies等信息,类似于Oauth:xxxx, Signature:xxx等。
8.支持301、302采集等自动跳转,支持元自动跳转采集
9.自动完成URL补全,我们根据采集目标提取链接后不需要自己计算
10.如果可能,尽量支持异步采集
11. 如果可能,尽量支持事件委托
12.如果可能,尽量支持代理
13.如果可能,尽量支持断点续传下载
在上一节文章我们构建了一个自定义爬虫类,已经实现了一些需求。本文将继续执行其余要求。在我们继续修改我们的类之前,让我们谈谈 spyder。
-----------------------------------------
为什么先说spyder,主要是老谷找不到python包的完整使用手册。资料过于零散,整理起来很麻烦。它不像 php 手册或 msdn 命名空间介绍。的东西。就像前面几篇文章一样,每次想做点什么,都要百度很久。结果,这个系列文章是极其难产的。那么与 spyder 有什么可谈的呢?让我们来看看。. .
打开spyder,如界面所示,分为几个不同的区域,通常情况下,我们在左侧输入py代码,右下角可以看到输出结果,右上角可以看到输出结果。. . 没有好好利用
比如在左边,我们输入一段代码
[n for n in range(1,100)]
选择并执行这段代码(F9),右下控制台可以出现反馈信息
这是一种非常方便的调试方法。当代码中有交互指令时,比如输入,右下控制台可以进行交互操作,输入一些信息。这个我就不多说了,大家都会用。
那么,快来想办法利用右上方的区域来辅助我们的学习和工作吧
在代码区输入代码
import requests
req = requests()
然后鼠标指向requests()方法,界面变了
当我们点击悬浮提示时,右上方区域的内容发生变化,帮助页面出现大量信息
嗯。. . 这个很方便,可以查看方法的具体用法,可惜他并没有像vs中那样列出类的所有方法,只依赖类。然后在等待spyder响应之后,看来可以使用属性或者方法了,然后去选择我们可能需要的
现在,让我们修改命令
import requests
req = requests.Request(url='https://www.baidu.com',method='GET')
运行这些代码,将右上方区域切换到变量资源管理器(Variable Explorer)
我们在左侧运行的代码生成的变量将在此处列出。只要他们没有被清除或关闭,他们就会留在这里。比如刚才的req中,我们可以看到他的描述。用鼠标双击该变量。看一看
这是一个比较完整的实例属性和方法。它列出了该实例的所有可用方法和属性,并且可以展开查看。这样就可以避免之前找不到方法的问题,直接给一个变量赋值给这个类,然后去这里看看能不能代替vs中的对象浏览器。
好吧,简单说一下spyder。毕竟,老谷完全是靠自己摸索学会了蟒蛇,而且年纪大了,看其他视频都觉得不舒服,所以慢慢来。至于其他python相关的IDE有没有提供这样的查询方式,老顾不知道,刚转行的同学可以自行探索。然后,我们回到主题并继续研究我们的爬行动物。
------------------------------------------------
7.可以伪造添加各种头信息,伪造添加cookies等信息,类似于Oauth:xxxx, Signature:xxx等。
一般来说,同一个站点上需要频繁更改header信息和cookie信息的地方很少。再说说反爬网站,等我们进入采集实战的时候再去处理。让我们先谈谈它。常规 网站。所以,实例化一次,设置好这个信息,基本上这个实例全站都可以用采集,后面再讲线程问题。
所以,这一次,我们把目光投向了天眼查站,并尝试根据他的响应来调整我们的代码,让它可以伪造cookies和接收headers
先到先无调整采集
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就是其中一种方式,写下来,以后再处理
然后,查看会话中的内容
显然cookie的信息也保存在session中,里面有4个cookie。
print(ajax.Session.cookies)
我有一个我不知道的对象列表,让我们不要管它。总之,这里有cookies,所以现在我们需要自己定义一个cookies变量来存储这些信息,并在下次继承采集,好吧,我们必须支持从外部添加cookies。另外,老谷注意到了这个cookie中的域问题。伪造cookies和域信息也很重要。请参考老谷的另一个文章,有的网站在校验cookies的时候会比较严格。,不再是特定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'[\s\S]*?(?=)',html,re.I))
很好,cookie伪造成功,域名带前缀点的问题不成问题。证据是他没有提示登录,其次电话号码没有被星号隐藏。
在这段代码中,我们使用了两次 Http 方法。第一次是获取初始cookie。如果没有初始cookie,那么我们需要自己通过cookies.set方法添加初始cookie。老顾懒得加了,让他自动获取,然后在第二个采集之前,我们追加两个cookie,继承第一个采集的cookie,这样就正确得到了我们预期的结果,并且cookies在同一个实例中,只需要添加一次。当我们的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)
运行它并查看结果
很好,请求头信息已经更新了,虽然有时候需要删除一些请求头,这里我就不实现了。有需求可以自己实现,那么第七个需求就告一段落了,下一部分开始Handle jump issues。
8.支持301、302采集等自动跳转,支持元自动跳转采集
让我们找到一个带有重定向的 URL,例如: ,一个用短链接*敏*感*词*生成的地址
来,我们试试,这个请求会发生什么
from spider import Ajax
ajax = Ajax()
html = ajax.Http('http://m6z.cn/6uVNKg')
print(ajax.status,ajax.ResposeHeader)
print(html)
他自动跳了过去!返回的状态码也是200!中间的301和302的过程就省略了!, 原来你不想自动 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跳转,都涉及到一个url补全问题,我们先解决这个,再来支持meta跳转和js跳转
9.自动完成URL补全,我们根据采集目标提取链接后不需要自己计算
在日常的采集过程中,我们经常会遇到页面中的链接地址缺少域名,有的有域名但没有协议的情况。. . 还有其他各种不应该存在的协议。. . . HMMMMMMM,反正经历了很多,自然知道了
这次我们来试试政财网首页
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)
可以看出页面中没有URL的链接有N多条,所以我们需要在采集的时候进行处理,得到完整的链接地址,方便我们后面处理。这个时候,就是常客大显身手的时候了。顺便说一下,在做这个补全之前,先看看scrapy有没有补全,好像很多人用scrapy。
好,我们上最简单的scrapy 采集,不管多少,就采集每个首页
先在命令行运行几条命令
d:\>pip install scrapy
<p>
d:\>scrapy startproject ccgp
d:\>cd ccgp
d:\ccgp>scrapy genspider ccgp_gather www.ccgp.gov.cn
</p>
为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方法之前,返回html,用这个方法改正后返回
嗯。. . . . 分析,url在哪里?同学们,列出来
.
.
.
.
.
.
有href,很常见,a标签,链接标签
还有src,也很常见,script标签,img标签,embed等。
还有一些容易被忽略的 URL,它们存在于样式、样式文件和元数据中。. . .
而且更难找,定位,开放,存在于js中。. . . 动作,存在于表单标签中
好吧,越来越复杂了。. . 我这里只实现前两个,添加一个元,其他的我不考虑。
补全url其实很简单。使用正则表达式提取url,验证url是否为合法url。当然,特殊情况要除外,what about:blank,什么file:///,什么base64数据(图片src可能有这种情况)。. . 总之,只计算需要补全的url,人家自己已经有了协议,不再操作。
哪些需要填写?
1、没有协议,比如//,鬼知道是http还是https。. . 其实这是由当前页面协议决定的。你在http域名页面点击这个链接,结果是你在https域名页面点击这个链接,结果是
2、如果链接地址路径不完整,比如/superwefei,需要填写域名才能获取/superwefei。这种情况比较复杂,可能在需要计算路径的时候遇到,比如../../../image/xxx.shtml,也可能遇到非标准路径../image/xxx.shtml
这两种情况基本都是老顾遇到的。如果还有其他情况,可以通知老顾,继续学习。
下面是实现代码
<p> 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)