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

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

初步思路

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

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

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

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

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

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

方法 说明
login 登录
parse 解析
save 保存
crawl 爬取(外部调用者只需调用这个方法即可)

爬虫类属性(初步设计):

属性 说明
headers 请求的头部信息,用于伪装成浏览器
cookies 保存登录后得到的 cookies
db_data 数据库的信息,用于连接数据库

进一步设计

我想将这个爬虫类设计得更为通用,也就是只修改解析的部分就能爬取不同的网站。组长说我这是打算写一个爬虫框架,我可没那么厉害,只是觉得把逻辑写死不能通用的类根本不能叫做类罢了。

参考代码

我看了一下组长给出的参考代码,大致结构是这样的:

首先一个Parse解析类(为了关注结构,具体内容省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)

这个思路不错,将解析部分独立形成一个类,不过这样要如何与爬虫类进行逻辑上的关联呢?解析类的对象,是什么?是解析器吗?解析器与爬虫应该是什么关系呢?

我继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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类,准备类?准备登录用的数据。说起来似乎比解析类更难以让我接受。解析器还可以说是装在爬虫身上,但是,但是“准备”这件事情分明是一个动作啊喂!

好吧,“一类动作”倒能说得过去吧。我看看怎么和爬虫类联系起来:

1
2
class Spider(Parse, Prepare):#???
pass

等会儿等会儿……

继承关系?

让我捋捋。

为了让爬虫能解析和能准备还真是不按套路出牌啊……

子类应该是父类的特化吧不是吗,就像猫类继承动物类,汽车类继承车类一样,猫是动物,汽车也是车。

算了不继续了,毕竟我不是为了故意和我组长作对。只是将其作为一个例子来说明我的思路。

解析器类

参考代码虽然不太能让我接受,但是它的结构仍然带给了我一定启发。就是解析函数不一定要作为爬虫的方法。

解析这个步骤如果真的只写在一个函数里面真的非常非常乱,因为解析不只一个函数。比如解析表单的隐藏域,解析页面的 url,解析页面内容等。

单独写一个解析类也可以。至于它和爬虫类的关系,我觉得组合关系更为合适(想象出了一只蜘蛛身上背着一个红外透视仪的样子),spider 的解析器可以更换,这样子我觉着更符合逻辑一些。

关于更换解析器的方式,我打算先写一个通用的解析器类作为基类,而后派生出子解析器类,子解析器根据不同的网站采取不同的解析行为。

然后新建my_parser.py文件,写了一个MyParser类。解析方式是 xpath 和 beautifulsoup。这里面的代码是我把已经用于爬取学校网站的特定代码通用化之后的示例代码,实际上并不会被调用,只是统一接口,用的时候会新写一个类继承它,并覆盖里面的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 函数,传入的参数有三个:数据库名,表名,装有记录的字典。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 函数

1
2
3
4
5
6
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 之后再手动跳转,直到跳转到首页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 库

这个文件内首先创建了一个特定解析类,继承自通用解析类,再写了一个保存函数,准备好参数,最后爬取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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)
作者

憧憬少

发布于

2019-04-21

更新于

2019-04-21

许可协议