记一次网络爬虫

HTTP嗅探

在学校官网上http://IP:8080/opac/publicNotice/bookOverdue,发现图书馆会定期更新借阅信息,一般一天更新一次。这些数据包括学生学号、借阅图书时间、图书名字和图书id等。可以用网络爬虫把这些数据收集下来,做数据分析。

首先打开Fiddler4进行HTTP嗅探。发现HTTP的请求Headers如下:

1
2
3
4
5
6
7
8
9
10
11
headers = {
'Host': 'IP',
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; OS_Linux x86_64; rv:48.0) Gecko/20100101 Firefox/48.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Referer': 'http://IP:8080/opac/publicNotice/bookOverdue',
'Cookie': 'JSESSIONID=***; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=zh',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded'
}

'Cookie'字段保存服务器和客户端的会话信息,从内容我们也知道服务器端的部分信息(谁会在Cookie字段保存这些信息?)
***出于安全考虑隐藏了。'Content-Type'字段表明HTTP上传的表单编码方案。其上传的表单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{'batchno': '',
'classNoName': '',
'classno': '',
'endtime': '',
'hasNextPage': 'true',
'language': '',
'libcode': '',
'limitDays': '',
'orderBy': '',
'page': '1',
'prevPage': '1',
'rows': '10',
'searchType': 'rdid',
'searchValue': '',
'specificClassno': '',
'specificLibcode': '',
'starttime': '',
'state': '',
'type': ''}

从表单可以看到,'page'字段就是浏览器翻页。其他的字段也不难理解,但这里可以不用处理。有了HTTP headers和上传表单,可以通过Python构造网络请求模仿浏览器的浏览行为。'User-Agent'字段表明所处的系统环境和浏览器类型,这个正好。

编写网络请求

编写网络请求脚本。不为学校服务器添加压力,采用单线程访问。

1
2
3
4
5
6
7
8
9
10
@time_delay(delay=seconds, with_net_delay=False)
@timethis
def loop(url, page, headers, data):
global_fd.write(str(page))
data.update(page=page)
r = requests.post(url, headers=headers, data=data)
print(len(r.content))
html = bs4.BeautifulSoup(r.content, 'lxml')
temp = list(html.table.find_all('tr'))
return temp

这两个装饰器分别是设置请求延时和计算请求时间。具体实现如下。

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
def timethis(func):
loop = 0
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal loop
start = time.time()
result = func(*args, **kwargs)
end = time.time()
loop += 1
global elapsed
elapsed = end - start
print('loop: {}, network delay time {:.2f}'.format(loop, elapsed))
return result
return wrapper

# 特殊情况下设置网络访问延迟的装饰器
def time_delay(delay=0, with_net_delay=False):

def make_delay(func):
if with_net_delay:
@functools.wraps(func)
def wrapper(*args, **kwargs):
number = delay-elapsed
number = number if number > 0 else 0
time.sleep(number)
result = func(*args, **kwargs)
return result
else:
@functools.wraps(func)
def wrapper(*args, **kwargs):
number = delay if delay >= 0 else 0
time.sleep(delay)
result = func(*args, **kwargs)
return result
return wrapper

return make_delay

处理HTML

获取的html需要处理DOM,从中提出需要的数据。

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
def refine(cell):
check = cell[0]
loop = []
for item in cell:
if item != check:
loop.append(item)
data = []

for item in loop:
person_id = item.contents[1].contents[0]
book_id = item.contents[3].contents[0]
try:
book_name = item.contents[5].contents[0].contents[0]
except Exception:
book_name = None
book_url = item.contents[5].contents[0].attrs['href']
borrow_time = [int(i) for i in item.contents[7].contents[0].split('-')]
return_time = [int(i) for i in item.contents[9].contents[0].split('-')]
temp = {'personid': person_id,
'bookid': book_id,
'bookname': book_name,
'bookurl': book_url,
'borrowtime': datetime.datetime(*borrow_time),
'returntime': datetime.datetime(*return_time)}
data.append(temp)
return data

但这里只有图书信息,没有学生的信息,通过一个简单的csv脚本采集学生的信息,包括班级、学号、姓名、学号,用于和图书借阅信息进行关联。

MySQL数据库表定义

获取完数据后,需要保存到数据库,方便管理和维护、查询。下面定义不同的表。

图书信息表的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE `exbooks` (
`idx` INT(11) NOT NULL AUTO_INCREMENT,
`personid` VARCHAR(30) NULL DEFAULT NULL,
`bookurl` VARCHAR(50) NULL DEFAULT NULL,
`bookid` VARCHAR(20) NULL DEFAULT NULL,
`bookname` VARCHAR(200) NULL DEFAULT NULL,
`borrowtime` DATETIME NULL DEFAULT NULL,
`returntime` DATETIME NULL DEFAULT NULL,
`hash_` VARCHAR(40) NULL DEFAULT NULL,
PRIMARY KEY (`idx`),
UNIQUE INDEX `hash_` (`hash_`),
INDEX `personid_index` (`personid`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=81418
;

学生信息表的创建。

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `students` (
`academy` VARCHAR(30) NULL DEFAULT NULL,
`class` VARCHAR(40) NULL DEFAULT NULL,
`personid` VARCHAR(40) NOT NULL,
`personname` VARCHAR(10) NULL DEFAULT NULL,
PRIMARY KEY (`personid`),
UNIQUE INDEX `personid_idx` (`personid`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

我们希望图书借阅信息和学生的借阅情况对应起来,公共视图建立学生-图书的关系。

1
2
3
4
5
6
7
8
CREATE VIEW AS SELECT `e`.`bookid` AS `bookid`,`e`.`bookurl` AS `bookurl`,
`e`.`bookname` AS `bookname`,`e`.`borrowtime` AS `borrowtime`,
`e`.`returntime` AS `returntime`,`e`.`personid` AS `personid`,
`s`.`personname` AS `personname`,`s`.`class` AS `class`,
`s`.`academy` AS `academy`
FROM (`exbooks` `e`
LEFT JOIN `students` `s` ON((`s`.`personid` = `e`.`personid`)))
ORDER BY `e`.`personid` DESC

把数据写入MySQL

把数据写入到MySQL数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def insert_into_exbooks(item, cursor):
item = item.copy()
hash_ = sha1(item)
item.update({'hash_': hash_})
item = parse_data(item)
sql = create_insertion('exbooks', item)
status = cursor.execute(sql)

def parse_data(data):
data['borrowtime'] = str(data['borrowtime'])
data['returntime'] = str(data['returntime'])
if data['bookname'] is None:
data['bookname'] = 'unknow'
return data

网络爬虫完毕后,可以通过SQL查询获取需要的信息。例如我想查看我最近借阅的信息。

数据库查询

1
2
3
4
5
SELECT *
FROM personbooks
WHERE personid = '201411611413'
ORDER BY borrowtime DESC
LIMIT 20

结果如下,只显示部分字段的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
B0586611 /opac/book/461067 垃圾回收的算法与实现 2017-06-22 00:00:00 2017-07-22 00:00:00
B0542784 /opac/book/458656 深入理解Flask 2017-06-22 00:00:00 2017-07-22 00:00:00
B0588314 /opac/book/461787 设计模式精解及面试攻略 2017-06-22 00:00:00 2017-07-22 00:00:00
B0500025 /opac/book/399203 MySQL技术内幕 2017-06-22 00:00:00 2017-07-22 00:00:00
B0555166 /opac/book/476903 算法新解 2017-06-22 00:00:00 2017-07-22 00:00:00
B0571626 /opac/book/463880 Python参考手册 2017-06-22 00:00:00 2017-07-22 00:00:00
B0542785 /opac/book/458659 Python Web开发实战 2017-06-22 00:00:00 2017-07-22 00:00:00
B0541765 /opac/book/403913 算法之美:隐匿在数据结构背后的原理 2017-06-22 00:00:00 2017-07-22 00:00:00
B0597453 /opac/book/406761 数据结构与算法:Python语言描述 2017-04-01 00:00:00 2017-05-01 00:00:00
B0450349 /opac/book/367335 NoSQL数据库技术实战 2017-03-29 00:00:00 2017-04-28 00:00:00
B0493726 /opac/book/390730 RabbitMQ实战:高效部署分布式消息队列:distrbuted messaging for everyone 2017-03-29 00:00:00 2017-04-28 00:00:00
B0586611 /opac/book/461067 垃圾回收的算法与实现 2017-03-29 00:00:00 2017-04-28 00:00:00
B0571626 /opac/book/463880 Python参考手册 2017-03-17 00:00:00 2017-04-24 00:00:00
B0569142 /opac/book/459875 Python面向对象编程指南 2017-03-08 00:00:00 2017-04-24 00:00:00
B0542785 /opac/book/458659 Python Web开发实战 2017-02-26 00:00:00 2017-04-24 00:00:00
B0500025 /opac/book/399203 MySQL技术内幕 2017-02-26 00:00:00 2017-04-24 00:00:00
B0346781 /opac/book/340866 HTTP权威指南 2017-02-26 00:00:00 2017-04-24 00:00:00
B0517201 /opac/book/402756 超实用的JavaScript代码段 2017-02-26 00:00:00 2017-04-24 00:00:00
B0516474 /opac/book/402699 超实用的jQuery代码段 2017-02-26 00:00:00 2017-04-24 00:00:00
B0482313 /opac/book/381087 Python黑帽子:黑客与渗透测试编程之道:python programming for hackers and pentesters 2017-02-26 00:00:00 2017-04-24 00:00:00

现在这些数据一共有四万多条,可以做很多有意义的数据分析。例如挖掘阅读学习兴趣相似的人等。以后有空分享。

转载请包括本文地址:https://allenwind.github.io/blog/2350
更多文章请参考:https://allenwind.github.io/blog/archives/