python爬虫学习笔记5爬虫类结构优化

前言

打算全部以cookie来登陆,而不依赖于session(因为听组长说session没cookie快,而且我想学些新东西而不是翻来覆去地在舒适区鼓捣)。弄了几天终于弄出来个代码不那么混乱的爬虫类了,更新一下博文来总结一下。代码在我github的spider库里面。

代码库传送门

前文传送门:

本文传送门(个人博客):

python爬虫学习笔记5爬虫类结构优化

初步思路

既然要封装成爬虫类,那么就以面向对象的思维来思考一下结构。

从通用的爬虫开始,先不考虑如何爬取特定的网站。

以下只是刚开始的思路,并不是最终思路。

爬虫的行为步骤并不复杂,分为以下几步:

  1. 请求并获取网页(往往需要模拟登录)
  2. 解析网页提取内容(还需要先获取需要爬取的url)
  3. 保存内容(保存到数据库)

爬虫类方法(初步设计):

方法说明
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: <int> or <str> 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)

封装之后用起来比较方便。

save函数

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)
    
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值