8月17日上午,国自然基金网站开放对外查询的接口,科研狗团队在中午12点钟左右已经获得所有数据,然后经过确认,在下午4点左右推送了排名前100数据,应该是最早发布前100的公众号。本文为您介绍本科研狗是如何从零基础一步一步到最终机器抓取数据成功。
在半个月之前,科研狗群里面的一位老师说到再过半个月就是国自然出结果的时候,领导每年要求罗列出兄弟学校的基金情况,每年都是手动一个一个搜索下载,问有没有更好的方法?
自然手动搜索非常耗费精力,据科研狗初步观察,总分类有3000种,查询一个学校的所有项目情况,至少需要输入3000次验证码以及3000次查询,一个学校何况如此,如果查询10多个学校,人力基本无法完成。
现在市面上肯定有很多自动抓取的公司,花费一定的费用肯定可以定制这套程序。但是我想自己亲自去搞定他,是的,一个医学科研狗要利用python近乎0基础搞定这项任务。
基础:本人可以说python零基础,只是学过基本语法。熟悉html网页结构。
时间:工作日平均每天花费2小时,周末平均每天花费4小时。
工具:python语言+firefox浏览器
最终效果:
第一天:确定是否可以利用python完成任务
因为本科研狗从未学过或者利用python进行过抓取的实际操作,所有首先第一要务是确定下python是否可以完成本项任务。打开国自然查询的网页,可以发现“资助类别”和“验证码”是必须填写的,当选择不同的“资助类别”时候,可能还要求选择“申请代码”。理论上说只要能够识别验证码,然后提交这些数据给服务器,便可以自动查询。
那么首要的任务就是如何自动识别验证码,对于一个还不会python的小白来说,百度是最好的工具(不得不说,我在学习计算机方面用到了非常多的百度)
花费了大概一个小时把第一页的所有内容大略看了一遍,有用到机器学习识别的,有用到特征值区分数字字母的,但是都是需要自己从头开发,短期内似乎不可行。随后发现一篇文章中提到了pytesseract,进一步搜索pytesseract,发它google的一个tesseract-OCR识别软件的python封装,意思就是允许python程序调用google的文字识别软件。有了google这个招牌的加持,相信识别上应该没多少问题。
结论:第一天初步确定了采用python+ pytesseract来解决本次问题。
第二天:学习python的初步语法
科研狗之前建立过一个python的学习小组,但是还未有具体的学习进度。我在之前在机器上已经安装好了python的运行环境。今天我需要学习python的基本语法。学习一门计算机语言最开始就是数据类型,比如数值int,浮点型float,字符型 string等,这些大家在大学C语言的时候已经学过。但是python多了一些特有的类型,比如元组tuple,列表list,字典dict,熟悉python的特有结构差不多花去一个小时。然后就是python的控制语句,if, else,循环while, for等,花费大约半个小时。接下来是python的函数,python的函数有点特点,一般语言都是function来定义函数,python采用def关键词来定义。
第三天:学习python的高级语法
Python有一个特性就是有很多外部包,就是别人已经写好了很多功能,我们只需要下载下来安装就可以用,采用pip方式来安装。然后学习了python的模块如何书写。
第四天:初步解析验证码
首先我们安装好pytesseract,具体怎么安装请百度,比较简单。然后从国自然网站上下载一个验证码图片用于做识别测试。
第一天查看的验证码识别内容今天用上了,对于上述验证码不经任何处理识别,效果不好。
验证码一般增加一些难于处理的元素,比如噪点、干扰线。本验证码的难点是第二个字符,国自然的网站增加了旋转模糊处理。在尝试了很多方法之后,今天没有解决该问题。
第五天:今天的目标就是解决验证码
经过一番测试,最后采用了如下的技术路线:图像二值化(非黑即白)-> 高斯模糊-> 再次二值化,然后直接调用pytesseract识别处理。
非常好,80%的一次性就能识别,这个识别率已经非常高了。识别关键代码如下:
#自适应阀值二值化
def_get_dynamic_binary_image(filedir,img_name):
filename='E:/python/validatecode/'+ img_name.split('.')[] +'-binary.jpg'
img_name = filedir +'/'+ img_name
print('.....'+ img_name)
im = cv2.imread(img_name)
im = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)#灰值化
#二值化
th1 = cv2.adaptiveThreshold(im,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,21,1)
#cv2.imwrite(filename,th1)
returnth1
def_get_static_binary_image(img,threshold =140):
'''手动二值化'''
img = Image.open(img)
img = img.convert('L')
pixdata = img.load()
w,h = img.size
foryinrange(h):
forxinrange(w):
ifpixdata[x,y]
pixdata[x,y] =
else:
pixdata[x,y] =255
returnimg
#干扰线降噪
definterference_line(img):
#filename = 'E:/python/validatecode/' + img_name.split('.')[0] + '-interferenceline.jpg'
h,w = img.shape[:2]
#!!!opencv矩阵点是反的
# img[1,2] 1:图片的高度,2:图片的宽度
foryinrange(1,w -1):
forxinrange(1,h -1):
count =
ifimg[x,y -1] >245:
count = count +1
ifimg[x,y +1] >245:
count = count +1
ifimg[x -1,y] >245:
count = count +1
ifimg[x +1,y] >245:
count = count +1
ifcount >2:
img[x,y] =255
#cv2.imwrite(filename,img)
returnimg
#点降噪
definterference_point(img,x =,y =):
"""
9邻域框,以当前点为中心的田字框,黑点个数
:paramx:
:paramy:
:return:
"""
#filename = 'E:/python/validatecode/' + img_name.split('.')[0] + '-interferencePoint.jpg'
#todo判断图片的长宽度下限
cur_pixel = img[x,y]#当前像素点的值
height,width = img.shape[:2]
foryinrange(,width -1):
forxinrange(,height -1):
ify ==:#第一行
ifx ==:#左上顶点,4邻域
#中心点旁边3个点
sum =int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x +1,y]) \
+int(img[x +1,y +1])
ifsum
img[x,y] =
elifx == height -1:#右上顶点
sum =int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x -1,y]) \
+int(img[x -1,y +1])
ifsum
img[x,y] =
else:#最上非顶点,6邻域
sum =int(img[x -1,y]) \
+int(img[x -1,y +1]) \
+int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x +1,y]) \
+int(img[x +1,y +1])
ifsum
img[x,y] =
elify == width -1:#最下面一行
ifx ==:#左下顶点
#中心点旁边3个点
sum =int(cur_pixel) \
+int(img[x +1,y]) \
+int(img[x +1,y -1]) \
+int(img[x,y -1])
ifsum
img[x,y] =
elifx == height -1:#右下顶点
sum =int(cur_pixel) \
+int(img[x,y -1]) \
+int(img[x -1,y]) \
+int(img[x -1,y -1])
ifsum
img[x,y] =
else:#最下非顶点,6邻域
sum =int(cur_pixel) \
+int(img[x -1,y]) \
+int(img[x +1,y]) \
+int(img[x,y -1]) \
+int(img[x -1,y -1]) \
+int(img[x +1,y -1])
ifsum
img[x,y] =
else:# y不在边界
ifx ==:#左边非顶点
sum =int(img[x,y -1]) \
+int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x +1,y -1]) \
+int(img[x +1,y]) \
+int(img[x +1,y +1])
ifsum
img[x,y] =
elifx == height -1:#右边非顶点
sum =int(img[x,y -1]) \
+int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x -1,y -1]) \
+int(img[x -1,y]) \
+int(img[x -1,y +1])
ifsum
img[x,y] =
else:#具备9领域条件的
sum =int(img[x -1,y -1]) \
+int(img[x -1,y]) \
+int(img[x -1,y +1]) \
+int(img[x,y -1]) \
+int(cur_pixel) \
+int(img[x,y +1]) \
+int(img[x +1,y -1]) \
+int(img[x +1,y]) \
+int(img[x +1,y +1])
ifsum
img[x,y] =
#cv2.imwrite(filename,img)
returnimg
第六天:研究nsfc查询页面的结构
用firefox打开nsfc查询页面,然后打开web(快捷键ctrl+shift+K)控制台查看页面cookies,认识到页面中所有操作的请求头,请求数据,响应头,响应数据等。
第七天:学习用python进行网络请求
当然还是百度学习下,然后采用python的urllib.request包请求,查看 urllib.request 包的所有方法。
print("模拟打开搜索页面")
# 1请求打开https://isisn.nsfc.gov.cn/egrantindex/funcindex/prjsearch-list
url ="https://isisn.nsfc.gov.cn/egrantindex/funcindex/prjsearch-list"
postData = urllib.parse.urlencode({}).encode('utf-8')
getHeader = {
"Host":"isisn.nsfc.gov.cn",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language":"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding":"gzip, deflate, br",
"Connection":"keep-alive",
"Upgrade-Insecure-Requests":1
}
req = urllib.request.Request(url,postData,getHeader)
#print(urllib.request.urlopen(req).read().decode('utf-8'))
#自动记住cookie
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
result = opener.open(req)
#print(result.read().decode('utf-8'))
headers = result.headers
第八天:请求验证码并保存在本地
现在已经是万事具备只欠东风,这一天我要解决保存验证码到本地,然后通过pytesseract识别得到验证码:
opener = urllib.request.build_opener()
validateCodeCookie = joinCookie(cookies,['sessionidindex','test','isisn'])
opener.addheaders=[
("Accept","*/*"),
("Accept-Encoding","gzip, deflate, br"),
("Accept-Language","zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"),
("Connection","keep-alive"),
("Cookie",validateCodeCookie),
("Host","isisn.nsfc.gov.cn"),
("Referer","https://isisn.nsfc.gov.cn/egrantindex/funcindex/prjsearch-list"),
("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0")
]
urllib.request.install_opener(opener)
url ="https://isisn.nsfc.gov.cn/egrantindex/validatecode.jpg?"+str(time.time())
filepath,headersTemp = urllib.request.urlretrieve(url,imgurl)
#重新设置cookies
#获得set-cookie
ifnoChangeCookie ==:
newSetCookie = headersTemp['Set-Cookie']
temp = parseCookie(newSetCookie)
cookies =dict(cookies,**temp)
第9天:尝试抓取一个面上项目分类
这个时候我们可以尝试识别一个验证码,然后发送到国自然服务器看下是否会返回结果。中间验证码识别可能不能一次就能通过,所有我选择循环查询验证,直到验证成功。
只要发送grantCode和验证码到服务器就会得到相应的数据,grantCode如下:
grantCodeList = {
'218':'面上项目',
'220':'重点项目',
'222':'重大项目',
'339':'重大研究计划',
'429':'国家杰出青年科学基金',
'432':'创新研究群体项目',
'433':'国际(地区)合作与交流项目',
'649':'专项基金项目',
'579':'联合基金项目',
'632':'海外及港澳学者研究基金',
'635':'国家基础科学人才培养基金',
'51':'国家重大科研仪器设备研制专项',
'52':'国家重大科研仪器研制项目',
'2699':'优秀青年科学基金项目',
'70':'应急管理项目',
'7161':'科学中心项目'
}
第10天:建立mysql数据库存储
因为数据很多,我们不可能一次性就抓取,如果中断了该如何继续?那么我们存储数据库,每抓取一个分类就存入数据库标记了该分类已经抓取,中间如果中断了,下次启动程序判断是否已经抓取,如果已经抓取就跳过:
#如果当前分类没有子类执行查询
forcateincategory:
#如果当前分类id,年份,以及grandcode已经处理成功了,则跳过
sql ="SELECT id FROM nsfc_flag WHERE id = '%s' AND grantCode = '%s' AND year = '%d'"% (cate[],grantCode,year)
cursor.execute(sql)
ifcursor.rowcount >:
print("已经处理过该类,跳过")
else:
try:
# print(cate) ('A010101', 'A010101.解析数论', 'A010101.解析数论', 'A0101')
# 0定义一些全局变量currentPage totalPage等
# 1先刷新验证码refreshcheckCode()
# 2解析验证码
# 3 ajax查询验证码是否正确
# 4如不正确,跳到1继续执行,直到正确
# 5发送查询数据resultDate, checkCode, currentPage=1 totalPage=1 perPage=10
# 6存储查询结果到xml
# 7解析xml结果并存储到mysql
# 8如果totalPage>currentPage表明还有下一页
# 9跳转到2,直到totalPage = currentPage
第11天:建立循环查询逻辑
前面已经调试好某一个类的查询,那么现在我们需要对3000多个分类循环查询。根据前期的研究发现,好像只有那些没有子类的分类才有项目,因此我之前就剔除了有子类的分类查询。但是发现抓取结束后数据严重不全。此时觉得有些分类即使有子类也会有下属的项目。然后今天删除所有数据,明天重新再来。
第12天:终于大功告成
经过重新调整之后,程序终于大功告成,能够完美获取2017年国自然基金的数据,获得的如下:
第13天:统计每个学校单位的数据
我已经抓取了2017年国自然基金,接下来需要解析每个单位的所有数据。这个很好办,循环所有的数据,然后找出学校单位(共1511个独立单位),最后根据不同的单位计算总和。
第14天:调整程序,测试稳定性
今天主要任务是调整程序,优化程序,因为抓取一个分类需要进行三次网络请求,基本需要3s;而总共3000多个分类,算下来至少需要3个小时,可以尝试多线程操作,但是为了避免对NSFC网站带来太高的压力,我最终设置了两个线程,查询nsfc网站的频率大约为1s一次请求,这基本不会对NSFC网站造成影响,2个小时左右完成的数据的获取。
第15天:国自然开放查询日期
最开始我想设置一个自动查询开放时间,原理是设置一个定时任务,每隔10分钟查询一次NSFC看是否开放。最后我发现操作失误关闭了定时任务,不过后来手动启动程序,在2个小时后已经获得所有数据。最终获得数据条目数42150条数据,这与NSFC发布的官方数目一致!
科研狗寻求合伙人,有想法、有技术、有资源可以请微信联系 widy-liu。
关注科研狗微信号,获取更多信息
加微信“widy-liu” 标明“科研狗”,加入科研狗综合讨论群。目前群一已满,群二已满员,群三开通中。
领取专属 10元无门槛券
私享最新 技术干货