打算全部以cookie来登陆,而不依赖于session(因为听组长说session没cookie快,而且我想学些新东西而不是翻来覆去地在舒适区鼓捣)。弄了几天终于弄出来个代码不那么混乱的爬虫类了,更新一下博文来总结一下。代码在我github的spider库里面。
代码库传送门
前文传送门:
python爬虫学习笔记1一个简单的爬虫
python爬虫学习笔记2模拟登录与数据库
python爬虫学习笔记3封装爬虫类
python爬虫学习笔记4模拟登录函数的优化
本文传送门(个人博客):
python爬虫学习笔记5爬虫类结构优化
既然要封装成爬虫类,那么就以面向对象的思维来思考一下结构。
从通用的爬虫开始,先不考虑如何爬取特定的网站。
以下只是刚开始的思路,并不是最终思路。
爬虫的行为步骤并不复杂,分为以下几步:
爬虫类方法(初步设计):
方法 | 说明 |
---|---|
login | 登录 |
parse | 解析 |
save | 保存 |
crawl | 爬取(外部调用者只需调用这个方法即可) |
爬虫类属性(初步设计):
属性 | 说明 |
---|---|
headers | 请求的头部信息,用于伪装成浏览器 |
cookies | 保存登录后得到的cookies |
db_data | 数据库的信息,用于连接数据库 |
我想将这个爬虫类设计得更为通用,也就是只修改解析的部分就能爬取不同的网站。组长说我这是打算写一个爬虫框架,我可没那么厉害,只是觉得把逻辑写死不能通用的类根本不能叫做类罢了。
我看了一下组长给出的参考代码,大致结构是这样的:
首先一个Parse
解析类(为了关注结构,具体内容省略):
class Parse():
def parse_index(self,text):
'''
用于解析首页
:param text: 抓取到的文本
:return: cpatcha_url, 一个由元组构成的列表(元组由两个元素组成 (代号,学校名称))
'''
pass
def parse_captcha(self, content, client):
'''
解析验证码
:return: or a code
'''
pass
def parse_info(self, text):
'''
解析出基本信息
:param text:
:return:
'''
pass
def parse_current_record(self, text):
'''
解析消费记录
:param text:
:return:
'''
return self.parse_info(text)
def parse_history_record(self, text):
'''
解析历史消费记录
:param text:
:return:
'''
return self.parse_info(text)
这个思路不错,将解析部分独立形成一个类,不过这样要如何与爬虫类进行逻辑上的关联呢?解析类的对象,是什么?是解析器吗?解析器与爬虫应该是什么关系呢?
我继续往下看:
class Prepare():
def login_data(self,username, password, captcha, schoolcode, signtype):
'''
构造登陆使用的参数
:return:data
'''
pass#省略代码,下同
def history_record_data(self, beginTime, endTime):
'''
历史消费记录data
:param beginTime:
:param endTime:
:return: data
'''
pass
这是一个Prepare
类,准备类?准备登录用的数据。说起来似乎比解析类更难以让我接受。解析器还可以说是装在爬虫身上,但是,但是“准备”这件事情分明是一个动作啊喂!
好吧,“一类动作”倒能说得过去吧。我看看怎么和爬虫类联系起来:
class Spider(Parse, Prepare):#???
pass
等会儿等会儿……
继承关系?
让我捋捋。
为了让爬虫能解析和能准备还真是不按套路出牌啊……
子类应该是父类的特化吧不是吗,就像猫类继承动物类,汽车类继承车类一样,猫是动物,汽车也是车。
算了不继续了,毕竟我不是为了故意和我组长作对。只是将其作为一个例子来说明我的思路。
参考代码虽然不太能让我接受,但是它的结构仍然带给了我一定启发。就是解析函数不一定要作为爬虫的方法。
解析这个步骤如果真的只写在一个函数里面真的非常非常乱,因为解析不只一个函数。比如解析表单的隐藏域,解析页面的url,解析页面内容等。
单独写一个解析类也可以。至于它和爬虫类的关系,我觉得组合关系更为合适(想象出了一只蜘蛛身上背着一个红外透视仪的样子),spider的解析器可以更换,这样子我觉着更符合逻辑一些。
关于更换解析器的方式,我打算先写一个通用的解析器类作为基类,而后派生出子解析器类,子解析器根据不同的网站采取不同的解析行为。
然后新建my_parser.py
文件,写了一个MyParser
类。解析方式是xpath和beautifulsoup。这里面的代码是我把已经用于爬取学校网站的特定代码通用化之后的示例代码,实际上并不会被调用,只是统一接口,用的时候会新写一个类继承它,并覆盖里面的函数。
class MyParser(object):
def login_data_parser(self,login_url):
'''
This parser is for chd
:param url: the url you want to login
:return (a dict with login data,cookies)
'''
response=requests.get(login_url)
html=response.text
# parse the html
soup=BeautifulSoup(html,'lxml')
#insert parser,following is an example
example_data=soup.find('input',{'name': 'example_data'})['value']
login_data={
'example_data':example_data
}
return login_data,response.cookies
def uni_parser(self,url,xpath,**kwargs):
response=requests.post(url,**kwargs)
html=response.text
tree=etree.HTML(html)
result_list=tree.xpath(xpath)
return result_list
def get_urls(self,catalogue_url,**kwargs):
'''
get all urls that needs to crawl.
'''
#prepare
base_url='http://example.cn/'
cata_base_url=catalogue_url.split('?')[0]
para = {
'pageIndex': 1
}
#get the number of pages
xpath='//*[@id="page_num"]/text()'
page_num=int(self.uni_parser(cata_base_url,xpath,params=para,**kwargs))
#repeat get single catalogue's urls
xpath='//a/@href'#link tag's xpath
url_list=[]
for i in range(1,page_num+1):
para['pageIndex'] = i
#get single catalogue's urls
urls=self.uni_parser(cata_base_url,xpath,params=para,**kwargs)
for url in urls:
url_list.append(base_url+str(url))
return url_list
def get_content(self,url,**kwargs):
'''
get content from the parameter "url"
'''
html=requests.post(url,**kwargs).text
soup=BeautifulSoup(html,'lxml')
content=soup.find('div',id='content')
content=str(content)
return content
我把构造登录信息的部分放在了解析器中。并在登录中调用。
登录之后得到的cookies就在参数中传递。
由于只打算存到数据库,所以并没有写一个“存档宝石类“,或许之后会写。
目前我只写了一个保存函数,以及自己封装的一个数据库类。
这个数据库类是my_database.py
中的MyDatabase
(应该不会撞名吧),目前只封装了insert函数,传入的参数有三个:数据库名,表名,装有记录的字典。代码如下:
import pymysql
class MyDatabase(object):
def __init__(self,*args,**kwargs):
self.conn=pymysql.connect(*args,**kwargs)
self.cursor=self.conn.cursor()
def insert(self,db,table,record_dict):
'''
:param db:name of database that you want to use
:param table:name of table that you want to use
:param record_dict:key for column,value for value
'''
#1.use the database
sql='use {}'.format(db)
self.cursor.execute(sql)
self.conn.commit()
#2.connect the sql commend
sql='insert into {}('.format(table)
record_list=list(record_dict.items())
for r in record_list:
sql += str(r[0])
if r != record_list[-1]:
sql += ','
sql+=') values('
for r in record_list:
sql += '"'
sql += str(r[1])
sql += '"'
if r != record_list[-1]:
sql += ','
sql+=')'
#3.commit
self.cursor.execute(sql)
self.conn.commit()
def show(self):
pass
def __del__(self):
self.cursor.close()
self.conn.close()
if __name__ == "__main__":
db_data={
'host':'127.0.0.1',
'user':'root',
'passwd':'password',
'port':3306,
'charset':'utf8'
}
test_record={
'idnew_table':'233'
}
mydb=MyDatabase(**db_data)
mydb.insert('news','new_table',test_record)
封装之后用起来比较方便。
def save(content,**save_params):
mydb=MyDatabase(**save_params)
record={
'content':pymysql.escape_string(content)
}
mydb.insert('dbase','bulletin',record)
pymysql.escape_string()函数是用于将内容转义的,因为爬取的是html代码(就不解析那么细了,直接把那一块html代码全部存下来,打开的时候格式还不会乱),有些内容可能使组合成的sql语句无法执行。
给构造函数传入特定的解析器和保存函数,然后调用crawl方法就可以让spider背着特制的parser去爬取网站内容啦~
登录函数和上次不太一样,做了一些修改,不过主要功能仍然是获取登录之后的cookies的。
简单说一下修改:我们学校网站登录之后会从登陆页面开始,经过三四次跳转之后才到达首页,期间获取到的cookies都需要保留,这样才能利用这些cookies来进入新闻公告页面。于是禁止重定向,手动获取下一个url,得到这一站的cookies之后再手动跳转,直到跳转到首页。
import requests
class MySpider(object):
def __init__(self,parser,save,**save_params):
self.parser=parser#parser is a object of class
self.save=save#save is a function
self.save_params=save_params
self.cookies=None
self.headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"
}
def login(self,login_url,home_page_url):
'''
login
:param login_url: the url you want to login
:param login_data_parser: a callback function to get the login_data you need when you login,return (login_data,response.cookies)
:param target_url: Used to determine if you have logged in successfully
:return: response of login
'''
login_data=None
#get the login data
login_data,cookies=self.parser.login_data_parser(login_url)
#login without redirecting
response=requests.post(login_url,headers=self.headers,data=login_data,cookies=cookies,allow_redirects=False)
cookies_num=1
while(home_page_url!=None and response.url!=home_page_url):#if spider is not reach the target page
print('[spider]: I am at the "{}" now'.format(response.url))
print('[spider]: I have got a cookie!Its content is that \n"{}"'.format(response.cookies))
#merge the two cookies
cookies=dict(cookies,**response.cookies)
cookies=requests.utils.cookiejar_from_dict(cookies)
cookies_num+=1
print('[spider]: Now I have {} cookies!'.format(cookies_num))
next_station=response.headers['Location']
print('[spider]: Then I will go to the page whose url is "{}"'.format(next_station))
response=requests.post(next_station,headers=self.headers,cookies=cookies,allow_redirects=False)
cookies=dict(cookies,**response.cookies)
cookies=requests.utils.cookiejar_from_dict(cookies)
cookies_num+=1
if(home_page_url!=None and response.url==home_page_url):
print("login successfully")
self.cookies=cookies
return response
def crawl(self,login_url,home_page_url,catalogue_url):
self.login(login_url,home_page_url)
url_list=self.parser.get_urls(catalogue_url,cookies=self.cookies,headers=self.headers)
for url in url_list:
content=self.parser.get_content(url,cookies=self.cookies,headers=self.headers)
self.save(content,**self.save_params)
def __del__(self):
pass
为了更好地展示结构,大部分内容都pass省略掉。想看具体代码可以去我github的spider库
这个文件内首先创建了一个特定解析类,继承自通用解析类,再写了一个保存函数,准备好参数,最后爬取。
from my_spider import MySpider
from my_parser import MyParser
from my_database import MyDatabase
from bs4 import BeautifulSoup
import requests
import pymysql
class chdParser(MyParser):
def login_data_parser(self,login_url):
'''
This parser is for chd
:param url: the url you want to login
:return (a dict with login data,cookies)
'''
pass
return login_data,response.cookies
def get_urls(self,catalogue_url,**kwargs):
'''
get all urls that needs to crawl.
'''
#prepare
pass
#get page number
pass
#repeat get single catalogue's urls
pass
for i in range(1,page_num+1):
para['pageIndex'] = i
#get single catalogue's urls
pass
return url_list
def save(content,**save_params):
pass
if __name__ == '__main__':
login_url="pass"#省略
home_page_url="pass"
catalogue_url="pass"
parser=chdParser()
save_params={
'host':'127.0.0.1',
'user':'root',
'passwd':'password',
'port':3306,
'charset':'utf8'
}
sp=MySpider(parser,save,**save_params)
sp.crawl(login_url,home_page_url,catalogue_url)