目标:通过调查我国高校图书馆图书荐购现状,包括是否开展图书荐购,如果有,是否有图书荐购政策、图书荐购的形式有哪些?图书荐购系统是什么?图书荐购的效果怎么样?最后总结分析现状及其存在的问题,尝试给出笔者的意见。
思路
爬取我国高校图书馆的网站信息并存入数据库中
清洗数据
分析数据
爬取高校图书馆网站数据
1 从教育部网站下载全国高等学校名单
2 手动整理下载的名单,删除一些无用信息,使其便于python读取。
整理后的excel如下图:
3 编写程序,读取excel文件,并保存到mongodb中。
这里需要做的额外工作是把高校所在的省或直辖市也写入到数据库中,因此,需要保存的字段有学校名称(name)、所在城市(city)、办学层次(level)、办学性质(nature)和所在省份(province),具体代码如下:
import pymongo
import openpyxl
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
school = mydb['school'] #连接数据库及创建数据库、数据集合
#清洗数据
def clean(string):
if string != None and '\n' in string:
string = ''.join(string.split('\n')) #删除字符串中的回车符
return string
else:
return string
def getExcel(file):
province = '' #定义空字符串,用来保存省份
wb = openpyxl.load_workbook(file)
sheet = wb['sheet1']
for row in range(2,sheet.max_row+1):
num = sheet.cell(row=row, column=1).value #序号(只取省份行)
name = sheet.cell(row=row, column=2).value #校名
city = sheet.cell(row=row, column=5).value #所在地
level = sheet.cell(row=row, column=6).value #办学层次
nature = sheet.cell(row=row, column=7).value #办学性质
if nature == None:
nature = '公办'
if name == None:
province = num #如果name为None,说明这行是省份行,把省份保存到变量province中
pass #忽略该行不插入数据库
else:
name = clean(name)
city = clean(city) #调用clean()函数删除字符中间的\n符
data = {
'name':name,
'nature':nature,
'level':level,
'city':city,
'province':province,
}
## print(data)
school.insert_one(data) #插入数据
if __name__ == '__main__':
getExcel('全国高校名单.xlsx')
4 编写程序,爬取各高校图书馆主页信息,并保存到mongodb。
(1)爬取图书馆url
由于各高校图书馆主页url没有共性,无法构建url列表,因此采取通过百度搜索引擎进入各高校图书馆主页。在百度搜索框中输入“北京大学图书馆”、“天津大学图书馆”、“苏州健雄职业技术学院图书馆”、“北京吉利学院图书馆”,可以发现目标均在搜索结果的第一条,因此,可以尝试爬取搜索结果第一条来获取各图书馆的主页url,如果没有搜索到的目标输出具体馆名,待后面手工抽样检查。
通过浏览器检查(F12)发现,第1条记录包含在id=1的div标签中,具体的路径是div > a > em。然后我们可以采用requests和bs4模块获取并解析出需要爬取的图书馆名及其url,最后,通过比对数据库中的图书馆名和爬取的图书馆名是否一样来判断id=1的标签中是否包含目标图书馆的url。
手动输入浏览图书馆网站,我们发现获取的<a>标签的文本并不是简单的“校名+图书馆”格式,如下:
- 校名+图书馆,比如:清华大学图书馆
- 图书馆校名,比如:图书馆南京体育学院
- 校名-图书馆,比如:陕西职业技术学院--图书馆
- 图书馆,只有图书馆三字,比如钦州学院图书馆、常治医学院图书馆
- 简称-图书馆-校名,比如常德职业技术学院图书馆
- 校名+图书馆+其他字,比如唐山师范学院图书馆网站
- 校名+图书馆百度百科,比如长春大学旅游学院图书馆百度百科
- 校名+图书馆_百度图片,比如济源职业技术学院图书馆
- 校名+图书馆_百度地图,比如河北轨道运输职业技术学院图书馆
还包括其他很多类似的格式,以上各种格式显然妨碍了我们提取到正确的图书馆url,不能通过简单的比对学院图书馆名称来匹配目标。鉴于以上干扰较多,笔者经过仔细考虑和充分试验,最终采用了中文分词技术,将从数据库中提取的校名图书馆和爬取的<a>标签文本通过jieba库转为词语的集合,比较前一个集合是否是后一个集合的子集,再增加一些禁用词,如百度、图吧、博客等,就能较为准确的匹配到目标图书馆网站了。例如,“图书馆南京体育学院”和“南京体育学院图书馆”之间如果通过直接比较,是不匹配的,而如果通过分词技术变为('图书馆','南京','体育','学院')和('南京','体育','学院','图书馆')这两个集合的比较,就能够匹配成功了。
具体代码如下:
import pymongo,time
import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool
import jieba,re,warnings
warnings.filterwarnings('ignore')
jieba.add_word('图吧')
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36\
(KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
}
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
school = mydb['school']
test = mydb['test']#连接数据库及创建数据库、数据集合
def checkWords(words):
'''
检查传入的参数是否包含禁止词,如果包含禁止词返回False,否则返回True
'''
check = True
stopwords = ['百度','图吧','概况','的','是','多少','吗','公告','地址',\
'高校','博客','搜狐','采购','交流','豆丁']
for stopword in stopwords:
if stopword in words:
check = False
break
return check
def clean(string):
#删除字符串中的标点符号
return re.sub(r"[0-9\s+\.\!\/_,$%^*()?;;:-【】+\"\']+|[+——!,;:。?、~@#¥%……&*()]+", "", string)
def getSchoolUrl(school_names):
'''
构造百度搜索图书馆的url,并保存进mongodb
'''
for name in school_names:
if name != None:
url = "https://www.baidu.com/s?wd={}图书馆".format(name) #构建百度搜索的url
school.update_one({"name":name},{"$set":{"search_url":url}}) #更新school_url到数据库的school集合中
def getLibraryUrl(url):
'''
通过百度搜索获取图书馆网站url
'''
r = requests.get(url,headers=headers) #请求页面
time.sleep(1)
soup = BeautifulSoup(r.text, 'lxml') #解析页面
name = url.split('=')[-1] #获取url里的图书馆名
name_ = clean(name) #去标点符号
names = set(jieba.lcut(name_,cut_all=True)) #采用分词技术,和lib_names比对
for num in range(1,4): #只查找搜索结果前3条记录
try:
lib_name = soup.find('div',id=num).a.text.strip() #获取页面中的文本
lib_name = clean(lib_name) #去标点符号
lib_names = set(jieba.lcut(lib_name,cut_all=True)) #采用分词技术,和names比对
#如果条件为真,大概率是图书馆的网站,获取该url
if names.issubset(lib_names) and checkWords(lib_names):
lib_url = soup.find('div',id=num).a.get('href')
break;
elif lib_name == '图书馆' or lib_name == '图书馆欢迎您':
lib_url = soup.find('div',id=num).a.get('href')
break;
else:
lib_url = '无'
except:
pass
school.update_one({"name":name[:-3]},{"$set":{"lib_url":lib_url}}) #更新lib_url到数据库的school集合中
test.insert_one({'search_url':url})
if __name__ == '__main__':
## school_names = [item['name'] for item in school.find()]
## getSchoolUrl(school_names) #调用该函数构造百度搜索url,运行一次注释掉
start_urls = [item['search_url'] for item in school.find()] #开始url
finish_urls = [item['search_url'] for item in test.find()] #已完成的url
rest_urls = set(start_urls) - set(finish_urls) #剩下未爬取的url,断点续爬
print(len(rest_urls))
pool = Pool(processes=8) #创建8个进程池
pool.map(getLibraryUrl, list(rest_urls)) #调用该函数获取图书馆主页url
pool.close()
pool.join() #结束进程池
上面的代码中,getSchoolUrl()函数是从数据库中读取校名,构建百度搜索中的url,并插入到school集合中,构建出的百度搜索的url应该是这样的:
url = 'https://www.baidu.com/s?wd=清华大学图书馆'
getLibraryUlr()函数的作用就是爬取图书馆首页的url,运行以上代码,等待爬取完成。
爬取完成后,通过以下代码提取school中lib_url值为'无'的记录数量有913条,输出值为‘无’的搜索url,手动搜索检查确认基本绝大部分是搜索不到图书馆网站的。
items = school.find({'lib_url':'无'})
print(school.find({'lib_url':'无'}).count())
for item in items:
print(item['search_url'],item['level'])
爬取到图书馆网站后,我们输出一下图书馆网址,发现全部是以"http://www.baidu.com"域名开头的,这是因为通过百度搜索得到的网址都是经过百度加密的,需要进一步解密获取真实url才能方便后期清洗数据。获取真实url的代码如下:
import pymongo
import requests
from multiprocessing import Pool
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
test = mydb['test']
school = mydb['school']#连接数据库及创建数据库、数据集合
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36\
(KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
}
def getRealUrl(url):
try:
r = requests.get(url,headers=headers)
real_url = r.url
except:
real_url = '无'
school.update_one({'lib_url':url}, {'$set':{'real_url':real_url}})
test.insert_one({'lib_url':url})
if __name__ == '__main__':
start_urls = [items['lib_url'] for items in school.find({'lib_url':{'$regex':'^http'}})]
finish_urls = [items['lib_url'] for items in test.find()]
rest_urls = set(start_urls) - set(finish_urls) #剩下未爬取的url,断点续爬
print(len(rest_urls))
pool = Pool(processes=8) #创建8个进程池
pool.map(getRealUrl, list(rest_urls)) #调用该函数获取图书馆主页url
pool.close()
pool.join() #结束进程池
保存运行,最终成功获取了真实url1643条,获取失败75条。通过输出获取失败的lib_url,手动随机尝试未能成功转换的加密url的图书馆,部分网站打不开,部分网站显示“该页面因站点更换网址或服务不稳定等原因可能无法正常访问!”这样的信息,网站不太稳定,也是打不开的。
接下来需要清洗数据了。清洗数据的方式如下:
第1步,导出数据到csv文件。
第2步,筛选lib_url值为‘无’的记录,删除913条。
第3步,筛选real_url值为'无'的记录,删除75条。
第4步,筛选并观察real_url的域名,重点检查有重复的域名记录,如http://map.baidu.com/开头的是百度地图,http://www.docin.com/开头的是豆丁文库,经过手工检查确认删除了90条。
第5步,选中real_url列,通过excel条件格式-突出显示重复值筛选出完全重复的url,手工检查错误的url。之所以会有完全重复的值,原因是部分没有图书馆网站的院校和名称近似的院校搜索结果过近导致的,比如成都学院图书馆和成都师范学院图书馆。经过手工检查,最后删除了13条重复记录。
第6步,观察real_url列,找出过长的或带有参数的url,一般并非是图书馆网站或是非网站首页,经过手工浏览检查,最后删除163条。
最终保存保留了1377条记录,也就是说获取到图书馆网站的院校数为1377所,将清洗完的数据,重新导回数据库新的集合library中。
下面简单的统计下已有的数据:
import pymongo
import warnings
import numpy as np
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
import matplotlib
from wordcloud import WordCloud
warnings.filterwarnings('ignore')
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
school = mydb['school']
library = mydb['library'] #连接数据库及创建数据库、数据集合
#输出院校总数量
total = school.count_documents({})
print('全国普通高等院校总数:{}'.format(total))
#输出全国院校公办和民办的数量
public = school.count_documents({'nature':'公办'})
people = school.count_documents({'nature':'民办'})
other = total - public - people
print('全国普通高校中公办院校数:{},民办院校数:{},其他院校数:{}'.\
format(public,people,other))
#输出全国院校本科和专科的数量
college = school.count_documents({'level':'本科'})
academy = school.count_documents({'level':'专科'})
print('全国普通高校中本科院校数:{},专科院校数:{}'.format(college, academy))
#输出有图书馆网站的图书馆总数量
web_total = library.count_documents({})
print('全国普通高校中有图书馆网站的院校总数:{}'.format(web_total))
#输出有图书馆网站的公办和民办院校数量
web_public = library.count_documents({'nature':'公办'})
web_people = library.count_documents({'nature':'民办'})
web_other = web_total - web_public - web_people
print('全国普通高校中没有图书馆网站的公办院校数:{},民办院校数:{},其他院校数:{}'.\
format(web_public,web_people,web_other))
#输出有图书馆网站的本科和专科院校数量
web_college = library.count_documents({'level':'本科'})
web_academy = library.count_documents({'level':'专科'})
print('全国普通高校中没有图书馆网站的本科院校数:{},专科院校数:{}'.\
format(web_college,web_academy))
保存运行,得到如下结果:
下面来图形化处理部分数据
由以上饼图可以发现:
- 在全国所有院校中,从办学性质来看,公办院校的比例约占71%,民办院校之间的比例约为28%;从办学层次来看,本科院校所占比例约为47%,专科院校的比例约为53%,专科比本科院校略多。
- 在全国有图书馆网站的院校中,从办学性质来看,公办院校的比例由71%上升到了约了80%,民办院校的比例则由28%下降到20%左右;从办学层次来看,本科院校的比例反超专科院校,从约47%上升到了约63%,专科比例则从约53%将到约37%。
- 从以上可以看到,在图书馆网站建设投入上,公办院校比民办院校,本科院校比专科院校投入的要更多,尤其是本科院校和专科院校之间差距较大。这也算是从侧面印证了图书馆在本科院校比专科院校更受重视,能够获得更多的资源投入。
我们再来看下全国范围内高校按省份的统计情况:
由上图可以看到,江苏、广东、山东不愧是我国的教育大省,高校数量遥遥领先,而西藏、青海、宁夏和海南的高校数量垫底。
再看下有图书馆网站的普通高校按省份统计情况:
名次基本没有重大变化,这说明从各省份来看,高校图书馆网站建设情况和高校数量比例成正相关关系,即高校数量越多的省份,有图书馆网站的高校数量也越多。
然后再看下按城市的全国高校统计情况,由于全国城市较多,全部数据都做成图表的话就太密集了,因此只选高校数量排在前10的城市。
由上图可以看见,高校数量最多的5个城市中有3个是直辖市,北京高校数量最多,武汉和广州数量较为接近。
再看按城市的全国有图书馆网站的高校统计情况,同样只选数量前10的城市。
名词基本没有变化,但重庆市从第4名降到第10名,天津从第7名降到前10开外,说明了从城市整体比例来看,这两个城市的高校在图书馆网站建设方面较其他城市落后。
(2)根据图书馆url爬取图书馆主页
在爬取图书馆主页时,由于我们只需要图书荐购相关的信息,因此采用定向爬取技术。
爬取的目标:提取图书馆网站首页上关于图书荐购的信息和链接url。
内容采集规则:即通过该规则过滤爬取到网站信息,只保留需要的信息。
首先,通过手动浏览各家图书馆的主页,找到图书荐购的栏目,各家的图书荐购名称可能都有些许差别,需要手工统计荐购名称,再观察分析找出共同点,最后设置采集规则。笔者手工翻阅了20多家图书馆,发现图书荐购的栏目名称比想象中要乱啊,此处列举了一些:
- 推荐购买
- 荐购
- 图书荐购
- 互动-电子资源荐购
- 资源推荐
- 荐购与捐赠-读者荐购
- 荐购资源
- 书刊推荐
- 咨询推荐-资源推荐
- 读者荐购
- 推荐购书
- 资源荐购
- 荐购与捐赠
- 荐书平台
把所有这些荐购栏目名称放进一个正则模式中,后面通过比对关键词来找到是否在首页有图书荐购栏目。
regex = "推荐购买|购买推荐|推荐购书|购书推荐|推荐图书|图书推荐|推荐书刊|书刊推荐|\
荐购|书刊荐购|荐购书刊|资源推荐|荐购资源|图书荐购|荐购图书|荐购资源|资源荐购|\
荐购电子资源|电子资源荐购|荐购与捐赠|捐赠与荐购|\
读者荐购|读者荐书|读者推荐|我要荐购|网上荐购|荐书平台"
爬取图书图书馆首页中关于图书荐购的相关数据的代码如下:
import pymongo
import requests,csv,re
from bs4 import BeautifulSoup
from multiprocessing import Pool
from selenium import webdriver
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
library = mydb['library']
test = mydb['test']
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
}
regex = "推荐购买|购买推荐|推荐购书|购书推荐|推荐图书|图书推荐|推荐书刊|书刊推荐|\
荐购|书刊荐购|荐购书刊|资源推荐|荐购资源|图书荐购|荐购图书|荐购资源|资源荐购|\
荐购电子资源|电子资源荐购|荐购与捐赠|捐赠与荐购|\
读者荐购|读者荐书|读者推荐|我要荐购|网上荐购|荐书平台"
def getHtml(url):
'''
获取网页内容
'''
try:
r = requests.get(url,headers=headers,timeout=20)
print(r.headers['content-type'])
print(r.encoding)
print(r.apparent_encoding)
print(requests.utils.get_encodings_from_content(r.text))
if r.status_code == 200:
#解决中文乱码问题
if r.encoding == 'ISO-8859-1':
if requests.utils.get_encodings_from_content(r.text) == []:
encoding = r.apparent_encoding
else:
encoding = requests.utils.get_encodings_from_content(r.text)[0]
if re.match('^utf-8',encoding.lower()):
content = r.text.encode('ISO-8859-1').decode('utf-8','ignore')
elif re.match('^utf-16',encoding.lower()):
content = r.text.encode('ISO-8859-1').decode('utf-16','ignore')
elif encoding.lower() == 'gb2312':
content = r.text.encode('ISO-8859-1').decode('gb2312','ignore')
elif encoding.lower() == 'gbk':
content = r.text.encode('ISO-8859-1').decode('gbk','ignore')
else:
pass
else:
content = r.text
return content
except:
pass
def convertUrl(domain,ralativr_url):
'''
将相对url转换为绝对url
'''
if ralativr_url[:1] == '/':
href = domain + ralativr_url[1:] #如果相对url第一个字符是'/'则删除
elif ralativr_url[:4] != 'http':
href = domain + ralativr_url
else:
href = ralativr_url
return href
def getInfo(url):
'''
解析页面内容,获取数据
'''
content = getHtml(url)
try:
soup = BeautifulSoup(content,'lxml')
a_list = soup.find_all('a') #找到所有的超链接a标签
rec_name = ''
for a in list(set(a_list)):
recomment_tag = a.find(text=re.compile(regex)) #遍历a标签,通过正则匹配出图书荐购类目的标签
if recomment_tag:
print(recomment_tag)
rec_name = a.get_text().strip() #荐购栏目的名称
rec_href = convertUrl(url,a['href']) #荐购栏目的url
break;
if rec_name == '':
rec_name = '无'
rec_href = '无' #没有荐购栏目的名称和href都为无
data = {
'rec_name':rec_name,
'rec_href':rec_href,
}
library.update_many({'real_url':url}, {'$set':data})
test.insert_one({'url':url}) #爬取成功的将url保存到test集合,用于断点续爬
except:
pass
if __name__ == '__main__':
pool = Pool(processes=8) #创建进程池
start_urls = [items['real_url'] for items in library.find()]
end_urls = [items['url'] for items in test.find()]
rest_urls = set(start_urls) - set(end_urls)
print("待爬取总记录条数:{}".format(len(start_urls)))
print("已爬取的记录条数:{}".format(len(end_urls)))
print("剩余未爬取的记录条数:{}".format(len(rest_urls)))
pool.map(getInfo, list(rest_urls))
pool.close()
pool.join() #结束进程池
保存文件,运行程序,等待结果...
由于部分网站服务器不稳定,程序要多运行几遍,直到连续多次爬取不再增加为止。
主要目的是检查明显非图书荐购栏目的rec_name,如果没有荐购栏目,就将rec_name的值修改为“无”,如果有荐购栏目,则更改为正确的栏目名称和url,清洗完数据重新导入回数据库中。
有了以上的数据,我们可以大体上知道在全国范围内图书馆在网站建设及荐购服务方面的整体情况了。
由上图可知,在全国2631所高校中,没有图书馆网站的高校占比46%左右,接近一半的比例,这说明我国高校图书馆在网站建设方面还有较大的上升空间。图书馆网站是图书馆对外服务的在线窗口,没有这个窗口,读者无法方便的知道图书馆提供的服务和资源有哪些,图书馆也无法及时将自己的资源和服务推送给读者,图书馆的工作效率和服务水平将会大打折扣。
既有网站又在主页有荐购栏目的图书馆仅占到约21%左右的比例,实际数据应该比21%略高一点,因为有6%左右的网站可能是关闭了外网访问等原因,是打不开的。而有网站但在主页没有荐购栏目的图书馆占比约26%左右,这说明从总体上来看,国内高校图书馆对于图书荐购服务方面的仍然重视不足。
再从院校的性质来看,全国有网站且能正常打开的高校中,有网站无荐购的公办院校是民办院校的近3倍,鉴于全国范围内公办校本身就是民办院校数量的近3倍,该比例亦在正常范围之内。但是,在有网站有荐购的高校中,公办院校数量是民办院校的5倍左右,这说明公办院校的图书馆在荐购服务方面相比民办院校较有优势。
而在公办院校中有网站有荐购和有网站无荐购的院校数量基本相当,但是在民办院校中有网站无荐购的院校数量要比有网站有荐购的院校多了近2倍的数量。这说明无论是公办院校还是民办院校,在荐购服务的开展上都不甚理想,民办院校和公办院校图书馆在荐购服务上有较大的差距。
从院校级别来看,有网站无荐购的本科院校略多于专科院校,但是有网站有荐购的本科院校要远多于专科院校,比例大致在7:3左右。这说明在荐购服务方面本科院校图书馆比专科院校有优势;而在本科院校中有网站无荐购和有网站有荐购的比例较为接近,但在专科院校中有网站有荐购的比例仅占到专科院校的3成左右。这说明无论是本科院校还是专科院校,在荐购服务的开展上均不甚理想,专科院校相比本科院校图书馆在荐购服务上尚存在着较大的差距。
总的来说,图书馆的荐购服务开展情况跟院校性质和院校级别呈较为明显的相关性,即公办院校强于民办院校的图书馆,本科院校优于专科院校的图书馆。
接下来,我们开始提取有荐购服务的图书馆中荐购栏目的名称,统计荐购栏目名称的出现的频次情况。主要代码如下:
'''
***图书荐购栏目名称统计***
'''
data = {} #定义空字典统计图书荐购栏目名称的频次
rec_names = [items['rec_name'] for items in library.find({'rec_href':{'$regex':'^http'}})]
for rec_name in rec_names:
data[rec_name] = data.get(rec_name, 0) + 1
data = sorted(data.items(), key=lambda x:x[1], reverse=True) #转为列表倒序排列
print('荐购栏目名称频次前20的为:')
pprint.pprint(data[:20])
print('荐购栏目名称的总数为:{}'.format(len(data)))
#词云
data = dict(data) #重新转为字典
wc = WordCloud(
width = 600,
height = 600,
max_words = 20,
background_color = 'gray',
font_path='C:\Windows\Fonts\msyh.ttf')
wc.fit_words(data)
#初始化画布
fig = plt.figure()
plt.imshow(wc)
plt.axis('off')
plt.rcParams['font.sans-serif']=['SimHei'] #解决中文乱码
plt.show()
运行后得到如下结果:
由上图可知,在有荐购栏目的图书馆中,荐购栏目的名称的总数多达百个。荐购名称可谓五花八门,其中‘读者荐购’、‘图书荐购’、‘资源荐购’、‘书刊荐购’、‘图书推荐’这5个荐购名称出现的最为频繁,而‘读者荐购’和‘图书荐购’的出现频次均在百次以上,说明这两个荐购名称的认可度较高。最后附上一张荐购名称的词云。