目录
前言
iNaturalist 是世界上最受欢迎的大自然观察网站之一。 iNaturalist 为动植物学家提供了大量的由志愿者拍摄提供的动植物图片或记录,这些数据对于学者进行研究非常重要。他们设置了一个网页供用户下载数据,但由于网站数据导出的限制,每次只能下载不超过20w条记录,而且每次导出都需要很长一段时间,学者需要频繁查看是否下载完成,下载数据后重复操作继续下载。这个过程十分耗费时间,因此提供一个脚本实现自动化数据下载。
目前项目已经提交在GitHub上,需要的同学可以自行clone,顺便给个小小的star!
实现思路
- 获取INaturalist网站的用户cookies与authentication token;
- 查询目标的数据量,如果超过20w条记录,则需要缩小查询范围;
- 提交数据打包任务;
- 查询任务是否完成;
- 下载数据包并记录数据包的基本信息;
- 删除任务,避免占用太多后台空间(为己为人,保护网络资源)
实现过程
【注意:我的需求是下载INaturalist上全年全球的动植物数据,打包数据的字段除了Taxon Extras不包含,其他全部都需要】
1、用户cookies与authentication token的获取
这一步有两种实现方案:一种是用户手动获取,一种是通过模拟登陆获取。
由于获取cookies与authentication token只需要用户在浏览器登录网站就能得到,而且只需要获取一次,因此没有采用模拟登陆获取的方案。以下讲述用户如何手动获取这两个用户标识。
- 注册并登录数据导出网页,按下F12,并点击Network,刷新页面,按照下图步骤寻找关键字,记录cookies中的内容。
- 通过网页提供的查询栏,随便填写选项后发出查询,此时可以看到用户的authentication token,记录token的内容。
# 设置用户的cookie,可以从浏览器中获取
def get_cookies():
# str需要替换成自己账号的cookies,获取步骤详看图片指引
str = r''
return str
# 设置用户的权限token,可以从浏览器中获取
def get_authenticity_token():
# str需要替换成自己账号的authenticity_token,获取步骤详看图片指引
str = r''
return str
2、查询目标的数据量
通过观察网站的网络请求服务列表,可以发现有一个固定的接口(get 类型)来完成数据的查询,其中在返回信息的header中包含该数据查询的数据量,因此可以通过该接口进行判断。
涉及到网络请求,可以使用requests库来简便地实现。但是我们需要自己构建请求头信息,这就需要我们上面获取到的cookies和authentication token了。
- 先构建一个get方法和一个post方法,需要注意的是由于该网站服务器位于国外,有时会无法访问,因此需要在get与post方法中实现轮询。
# 通过get方法进行查询,并返回response
# 输入参数:requests创建的session(网络会话)、访问的url
def get_method(session, url):
# 添加请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies()
}
resp = None
# 如果查询引发异常,则跳过,睡眠5秒后再次查询
session.headers = headers
while(resp == None):
try:
resp = session.get(url)
except:
resp = None
time.sleep(5) #避免频繁请求对后台造成巨大压力
resp.close()
return resp
# 通过post方法进行提交,并返回response
# 输入参数:requests创建的session(网络会话)、访问的url、表单数据(目前为固定内容)
def post_method(session, url, headers, data):
resp = None
session.headers = headers
while (resp == None):
try:
resp = session.post(url, data=data)
except:
resp = None
time.sleep(5) #避免频繁请求对后台造成巨大压力
resp.close()
return resp
- 构建查询的url,发出请求,并获取查询的数据量。网站提供的查询选项很多,开发者可以根据自己的需要先构建一条目标url,然后根据这个url设计自己的构建方法。由于这个部分有很的组合类型,博主就不一一列出,仅使用最简单的日期作为示例。
# 用于查询指定日期内有多少条记录
# 输入参数:requests创建的session(网络会话),d1(起始日期),d2(终止日期)
# 返回内容:时间段中的记录条数
def check_data_count_during_date(session, d1, d2):
url_str = r'https://www.inaturalist.org/observations?quality_grade=any&identifications=any&d1=' + d1 + '&d2=' + d2 + '&partial=cached_component'
resp = get_method(session, url_str)
# 从response的头文件中查找记录条数总数,每次下载最大量不超过20w
return int(resp.headers.get('X-Total-Entries'))
3、提交数据打包任务
如果数据量没有超过20w,则可以向后台提交打包任务了。需要注意的时候post方法的Header与get方法的Header并不一样,因此需要独立构建,需要用到cookies和token。
- 提交的打包任务需要告诉后台你需要什么样的字段。后台通过表单的形式接收这些字段参数。
# 设置创建的时间段、需要包含的字段信息,1代表选择,0代表不选择
# 输入参数:d1(起始日期),d2(终止日期)
def get_form_data(d1, d2):
data = {
'utf8': '✓',
'observations_export_flow_task[inputs_attributes][0][extra][query]': 'quality_grade=any&identifications=any&d1='+ d1 + '&d2=' + d2,
'observations_export_flow_task[options][columns][id]': '1',
'observations_export_flow_task[options][columns][observed_on_string]': '1',
'observations_export_flow_task[options][columns][observed_on]': '1',
'observations_export_flow_task[options][columns][time_observed_at]': '1',
'observations_export_flow_task[options][columns][time_zone]': '1',
'observations_export_flow_task[options][columns][user_id]': '1',
'observations_export_flow_task[options][columns][user_login]': '1',
'observations_export_flow_task[options][columns][created_at]': '1',
'observations_export_flow_task[options][columns][updated_at]': '1',
'observations_export_flow_task[options][columns][quality_grade]': '1',
'observations_export_flow_task[options][columns][license]': '1',
'observations_export_flow_task[options][columns][url]': '1',
'observations_export_flow_task[options][columns][image_url]': '1',
'observations_export_flow_task[options][columns][sound_url]': '1',
'observations_export_flow_task[options][columns][tag_list]': '1',
'observations_export_flow_task[options][columns][description]': '1',
'observations_export_flow_task[options][columns][num_identification_agreements]': '1',
'observations_export_flow_task[options][columns][num_identification_disagreements]': '1',
'observations_export_flow_task[options][columns][captive_cultivated]': '1',
'observations_export_flow_task[options][columns][oauth_application_id]': '1',
'observations_export_flow_task[options][columns][place_guess]': '1',
'observations_export_flow_task[options][columns][latitude]': '1',
'observations_export_flow_task[options][columns][longitude]': '1',
'observations_export_flow_task[options][columns][positional_accuracy]': '1',
'observations_export_flow_task[options][columns][private_place_guess]': '1',
'observations_export_flow_task[options][columns][private_latitude]': '1',
'observations_export_flow_task[options][columns][private_longitude]': '1',
'observations_export_flow_task[options][columns][private_positional_accuracy]': '1',
'observations_export_flow_task[options][columns][geoprivacy]': '1',
'observations_export_flow_task[options][columns][taxon_geoprivacy]': '1',
'observations_export_flow_task[options][columns][coordinates_obscured]': '1',
'observations_export_flow_task[options][columns][positioning_method]': '1',
'observations_export_flow_task[options][columns][positioning_device]': '1',
'observations_export_flow_task[options][columns][place_town_name]': '1',
'observations_export_flow_task[options][columns][place_county_name]': '1',
'observations_export_flow_task[options][columns][place_state_name]': '1',
'observations_export_flow_task[options][columns][place_country_name]': '1',
'observations_export_flow_task[options][columns][place_admin1_name]': '1',
'observations_export_flow_task[options][columns][place_admin2_name]': '1',
'observations_export_flow_task[options][columns][species_guess]': '1',
'observations_export_flow_task[options][columns][scientific_name]': '1',
'observations_export_flow_task[options][columns][common_name]': '1',
'observations_export_flow_task[options][columns][iconic_taxon_name]': '1',
'observations_export_flow_task[options][columns][taxon_id]': '1',
'observations_export_flow_task[options][columns][taxon_kingdom_name]': '0',
'observations_export_flow_task[options][columns][taxon_phylum_name]': '0',
'observations_export_flow_task[options][columns][taxon_subphylum_name]': '0',
'observations_export_flow_task[options][columns][taxon_superclass_name]': '0',
'observations_export_flow_task[options][columns][taxon_class_name]': '0',
'observations_export_flow_task[options][columns][taxon_subclass_name]': '0',
'observations_export_flow_task[options][columns][taxon_superorder_name]': '0',
'observations_export_flow_task[options][columns][taxon_order_name]': '0',
'observations_export_flow_task[options][columns][taxon_suborder_name]': '0',
'observations_export_flow_task[options][columns][taxon_superfamily_name]': '0',
'observations_export_flow_task[options][columns][taxon_family_name]': '0',
'observations_export_flow_task[options][columns][taxon_subfamily_name]': '0',
'observations_export_flow_task[options][columns][taxon_supertribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_tribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_subtribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_genus_name]': '0',
'observations_export_flow_task[options][columns][taxon_genushybrid_name]': '0',
'observations_export_flow_task[options][columns][taxon_species_name]': '0',
'observations_export_flow_task[options][columns][taxon_hybrid_name]': '0',
'observations_export_flow_task[options][columns][taxon_subspecies_name]': '0',
'observations_export_flow_task[options][columns][taxon_variety_name]': '0',
'observations_export_flow_task[options][columns][taxon_form_name]': '0',
'commit': '创建导出'
}
return data
- 用户cookies和token、字段表单都准备好后,就能向后台发送任务了。此时需要记录这次打包任务的id号,用户查询该任务是否已经完成,为下载数据包做准备。
# 发送打包数据的任务
# 输入参数:d1(起始日期),d2(终止日期)
# 返回内容:任务的id号
def send_pack_data_proceess(session, d1, d2):
# 添加请求头
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '8917',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies(),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'www.inaturalist.org',
'Origin': 'https://www.inaturalist.org',
'Referer': 'https://www.inaturalist.org/observations/export',
'X-CSRF-Token': get_authenticity_token(),
'X-Requested-With': 'XMLHttpRequest'
}
url_str = r'https://www.inaturalist.org/flow_tasks'
resp = post_method(session, url_str, headers, get_form_data(d1, d2))
# 解析并返回任务的id号
user_dict = json.loads(resp.text)
return str(user_dict['id'])
4、查询任务是否完成
要查询任务是否完成,就需要上面我们记录的id号,通过这个id号查询项目的进度,如果进度显示完成,则可以进行下载步骤。需要注意的是,查询返回的json数据,可以通过json库自动解析为字典,再通过获取字典的信息判断任务进度。
# 获取创建进度查询的url
# 输入参数:requests创建的session(网络会话),任务的的id号
# 返回内容:output中的id和file_file_name
def check_proceess(session, projectId):
url_str = r'https://www.inaturalist.org/flow_tasks/' + projectId + r'/run.json'
print('开始检查任务进度...')
resp = get_method(session, url_str)
print('结束检查任务进度...')
# 如果打包完成,则output中存在相应的数据
user_dict = json.loads(resp.text)
if len(user_dict['outputs']) > 0:
return {'id': str(user_dict['outputs'][0]['id']), 'file_file_name': user_dict['outputs'][0]['file_file_name']}
else:
return {'id': 'null', 'file_file_name': 'null'}
5、下载数据包
查询到任务进度完成后,则可以进行自动下载。指定自己想要的名字进行保存后,设置任务id向后台发起下载请求(该数据为zip格式),因此需要使用到流下载模式和块写入模式。
# 下载数据,需要提供requests创建的session(网络会话),下载id号,文件名称和保存路径
def download_zip(session, id, file_file_name, save_path, chunk_size=128) -> bool:
str_url = r'https://www.inaturalist.org/attachments/flow_task_outputs/' + id + '/' + file_file_name
resp = None
while(resp == None):
try:
resp = session.get(str_url, stream=True)
except:
resp = None
time.sleep(5) #避免频繁请求对后台造成巨大压力
with open(save_path, 'wb') as fd:
for chunk in resp.iter_content(chunk_size=chunk_size):
fd.write(chunk)
resp.close()
6、删除任务
这一步在博主看来十分的重要,人家网站为大众提供了这么好的数据,如果我们下载完后擦擦屁股走人,放任占用人家后台的硬盘资源,那真的是很不该,所以啊,下载完之后一定要把人家的资源释放掉!!!
# 删除任务
def delete_proceess(session, id) -> bool:
# 设置请求头
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '128',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies(),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'www.inaturalist.org',
'Origin': 'https://www.inaturalist.org',
'Referer': 'https://www.inaturalist.org/observations/export'
}
# 设置请求url
url_str = r'https://www.inaturalist.org/flow_tasks/' + id
# 构建请求表单数据
data = {
'_method': 'delete',
'authenticity_token': get_authenticity_token()
}
resp = post_method(session, url_str, headers, data)
if (resp != None):
print('删除任务成功...')
return True
else:
return False
完整代码
import requests
import json
import datetime
import time
# 设置用户的cookie,可以从浏览器中获取
def get_cookies():
# str需要替换成自己账号的cookies,获取步骤详看图片指引
str = r''
return str
# 设置用户的权限token,可以从浏览器中获取
def get_authenticity_token():
# str需要替换成自己账号的authenticity_token,获取步骤详看图片指引
str = r''
return str
# 模拟登陆INaturalist,用于获取token与cookie
# 待定
# 设置创建的时间段、需要包含的字段信息,1代表选择,0代表不选择
# 输入参数:requests创建的session(网络会话),d1(起始日期),d2(终止日期)
def get_form_data(d1, d2):
data = {
'utf8': '✓',
'observations_export_flow_task[inputs_attributes][0][extra][query]': 'quality_grade=any&identifications=any&d1='+ d1 + '&d2=' + d2,
'observations_export_flow_task[options][columns][id]': '1',
'observations_export_flow_task[options][columns][observed_on_string]': '1',
'observations_export_flow_task[options][columns][observed_on]': '1',
'observations_export_flow_task[options][columns][time_observed_at]': '1',
'observations_export_flow_task[options][columns][time_zone]': '1',
'observations_export_flow_task[options][columns][user_id]': '1',
'observations_export_flow_task[options][columns][user_login]': '1',
'observations_export_flow_task[options][columns][created_at]': '1',
'observations_export_flow_task[options][columns][updated_at]': '1',
'observations_export_flow_task[options][columns][quality_grade]': '1',
'observations_export_flow_task[options][columns][license]': '1',
'observations_export_flow_task[options][columns][url]': '1',
'observations_export_flow_task[options][columns][image_url]': '1',
'observations_export_flow_task[options][columns][sound_url]': '1',
'observations_export_flow_task[options][columns][tag_list]': '1',
'observations_export_flow_task[options][columns][description]': '1',
'observations_export_flow_task[options][columns][num_identification_agreements]': '1',
'observations_export_flow_task[options][columns][num_identification_disagreements]': '1',
'observations_export_flow_task[options][columns][captive_cultivated]': '1',
'observations_export_flow_task[options][columns][oauth_application_id]': '1',
'observations_export_flow_task[options][columns][place_guess]': '1',
'observations_export_flow_task[options][columns][latitude]': '1',
'observations_export_flow_task[options][columns][longitude]': '1',
'observations_export_flow_task[options][columns][positional_accuracy]': '1',
'observations_export_flow_task[options][columns][private_place_guess]': '1',
'observations_export_flow_task[options][columns][private_latitude]': '1',
'observations_export_flow_task[options][columns][private_longitude]': '1',
'observations_export_flow_task[options][columns][private_positional_accuracy]': '1',
'observations_export_flow_task[options][columns][geoprivacy]': '1',
'observations_export_flow_task[options][columns][taxon_geoprivacy]': '1',
'observations_export_flow_task[options][columns][coordinates_obscured]': '1',
'observations_export_flow_task[options][columns][positioning_method]': '1',
'observations_export_flow_task[options][columns][positioning_device]': '1',
'observations_export_flow_task[options][columns][place_town_name]': '1',
'observations_export_flow_task[options][columns][place_county_name]': '1',
'observations_export_flow_task[options][columns][place_state_name]': '1',
'observations_export_flow_task[options][columns][place_country_name]': '1',
'observations_export_flow_task[options][columns][place_admin1_name]': '1',
'observations_export_flow_task[options][columns][place_admin2_name]': '1',
'observations_export_flow_task[options][columns][species_guess]': '1',
'observations_export_flow_task[options][columns][scientific_name]': '1',
'observations_export_flow_task[options][columns][common_name]': '1',
'observations_export_flow_task[options][columns][iconic_taxon_name]': '1',
'observations_export_flow_task[options][columns][taxon_id]': '1',
'observations_export_flow_task[options][columns][taxon_kingdom_name]': '0',
'observations_export_flow_task[options][columns][taxon_phylum_name]': '0',
'observations_export_flow_task[options][columns][taxon_subphylum_name]': '0',
'observations_export_flow_task[options][columns][taxon_superclass_name]': '0',
'observations_export_flow_task[options][columns][taxon_class_name]': '0',
'observations_export_flow_task[options][columns][taxon_subclass_name]': '0',
'observations_export_flow_task[options][columns][taxon_superorder_name]': '0',
'observations_export_flow_task[options][columns][taxon_order_name]': '0',
'observations_export_flow_task[options][columns][taxon_suborder_name]': '0',
'observations_export_flow_task[options][columns][taxon_superfamily_name]': '0',
'observations_export_flow_task[options][columns][taxon_family_name]': '0',
'observations_export_flow_task[options][columns][taxon_subfamily_name]': '0',
'observations_export_flow_task[options][columns][taxon_supertribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_tribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_subtribe_name]': '0',
'observations_export_flow_task[options][columns][taxon_genus_name]': '0',
'observations_export_flow_task[options][columns][taxon_genushybrid_name]': '0',
'observations_export_flow_task[options][columns][taxon_species_name]': '0',
'observations_export_flow_task[options][columns][taxon_hybrid_name]': '0',
'observations_export_flow_task[options][columns][taxon_subspecies_name]': '0',
'observations_export_flow_task[options][columns][taxon_variety_name]': '0',
'observations_export_flow_task[options][columns][taxon_form_name]': '0',
'commit': '创建导出'
}
return data
# 通过get方法进行查询,并返回response
# 输入参数:requests创建的session(网络会话),访问的url
def get_method(session, url):
# 添加请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies()
}
resp = None
# 如果查询引发异常,则跳过,睡眠5秒后再次查询
session.headers = headers
while(resp == None):
try:
resp = session.get(url)
except:
resp = None
time.sleep(5)
resp.close()
return resp
# 通过post方法进行提交,并返回response
# 输入参数:requests创建的session(网络会话),访问的url、表单数据(目前为固定内容)
def post_method(session, url, headers, data):
resp = None
session.headers = headers
while (resp == None):
try:
resp = session.post(url, data=data)
except:
resp = None
time.sleep(5)
resp.close()
return resp
# 用于查询指定日期内有多少条记录
# 输入参数:requests创建的session(网络会话),d1(起始日期),d2(终止日期)
# 返回内容:时间段中的记录条数
def check_data_count_during_date(session, d1, d2):
url_str = r'https://www.inaturalist.org/observations?quality_grade=any&identifications=any&d1=' + d1 + '&d2=' + d2 + '&partial=cached_component'
resp = get_method(session, url_str)
# 从response的头文件中查找记录条数总数,每次下载最大量不超过20w
return int(resp.headers.get('X-Total-Entries'))
# 发送打包数据的任务
# 输入参数:requests创建的session(网络会话),d1(起始日期),d2(终止日期)
# 返回内容:任务的id号
def send_pack_data_proceess(session, d1, d2):
# 添加请求头
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '8917',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies(),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'www.inaturalist.org',
'Origin': 'https://www.inaturalist.org',
'Referer': 'https://www.inaturalist.org/observations/export',
'X-CSRF-Token': get_authenticity_token(),
'X-Requested-With': 'XMLHttpRequest'
}
url_str = r'https://www.inaturalist.org/flow_tasks'
resp = post_method(session, url_str, headers, get_form_data(d1, d2))
# 解析并返回任务的id号
user_dict = json.loads(resp.text)
return str(user_dict['id'])
# 获取创建进度查询的url
# 输入参数:requests创建的session(网络会话),任务的的id号
# 返回内容:output中的id和file_file_name
def check_proceess(session, projectId):
url_str = r'https://www.inaturalist.org/flow_tasks/' + projectId + r'/run.json'
print('开始检查任务进度...')
resp = get_method(session, url_str)
print('结束检查任务进度...')
# 如果打包完成,则output中存在相应的数据
user_dict = json.loads(resp.text)
if len(user_dict['outputs']) > 0:
return {'id': str(user_dict['outputs'][0]['id']), 'file_file_name': user_dict['outputs'][0]['file_file_name']}
else:
return {'id': 'null', 'file_file_name': 'null'}
# 下载数据,需要requests创建的session(网络会话),提供下载id号和文件名称
def download_zip(session, id, file_file_name, save_path, chunk_size=128) -> bool:
str_url = r'https://www.inaturalist.org/attachments/flow_task_outputs/' + id + '/' + file_file_name
resp = None
while(resp == None):
try:
resp = session.get(str_url, stream=True)
except:
resp = None
time.sleep(5)
with open(save_path, 'wb') as fd:
for chunk in resp.iter_content(chunk_size=chunk_size):
fd.write(chunk)
resp.close()
# 删除任务
def delete_proceess(session, id) -> bool:
# 设置请求头
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '128',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Connection': 'keep-alive',
'Cookie': get_cookies(),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'www.inaturalist.org',
'Origin': 'https://www.inaturalist.org',
'Referer': 'https://www.inaturalist.org/observations/export'
}
# 设置请求url
url_str = r'https://www.inaturalist.org/flow_tasks/' + id
# 构建请求表单数据
data = {
'_method': 'delete',
'authenticity_token': get_authenticity_token()
}
resp = post_method(session, url_str, headers, data)
if (resp != None):
print('删除任务成功...')
return True
else:
return False
# 计算第n天后的日期,一般为6天后,总共是7天
# 输入参数:起始日期
# 返回内容:第n天后的日期
def get_date(date_str, n = 6):
the_date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
result_date = the_date + datetime.timedelta(days = n) # 第n天后是几号
d = result_date.strftime('%Y-%m-%d')
return d
# 比较两个真实日期之间的大小,如果截止日期小于终止日期,则返回True
def date_compare(date, end_date) -> bool:
satrt_day = datetime.datetime.strptime(date, '%Y-%m-%d')
end_day = datetime.datetime.strptime(end_date, '%Y-%m-%d')
return satrt_day <= end_day
# 执行函数
if __name__ == '__main__':
# 流程步骤
# 0、在浏览器登录INaturalist网站,按F12,点击Network,在Name列表中找到export请求,点击Headers->Request Headers->Cookies,复制粘贴到get_cookies函数中;
# 0、点击查找数据范围,在Network的Name列表中找到observations?quality_grade=any&identifications=any请求,点击Headers->Request Headers->X-CSRF-Token,复制粘贴到get_csrf函数中;
# 0、点击Elements左侧的第二个图标,变为蓝色后选择任务列表的删除按钮,右侧菜单栏自动索引到按钮的DOM位置,找到authenticity_token对应的value,复制粘贴到get_authenticity_token函数中;
# 1、设置存储路径和总的起始日期、终止日期
# 2、设置一定的时间段,比如6天或7天,得到截止日期,查询后如果返回的记录超过20w,则减少一天,直至记录在20w之内
# 3、提交打包任务
# 4、设置一定时间后检查任务完成进度,如果完成,则获取任务的输出结果id和file名称
# 5、下载输出结果
# 6、下载完建议删除任务结果,防止占用内存
# 7、再次开始执行步骤2,直到终止日期
store_path = r'' # 存储的路径,如果不设置,则默认与py文件同路径
start_day = '2019-01-01' # 起始日期
end_day = '2019-12-31' # 终止日期
init_data_interval = 6 # 天数间隔
day_interval = init_data_interval
stop_day = get_date(start_day, day_interval) # 截止日期
m_session = requests.session() # 创建网络会话连接
# 如果截止日期小于终止日期,则允许执行
while(date_compare(stop_day, end_day)):
# 如果返回的数目超过20w,则将日期减少一天,直至满足条件
while(check_data_count_during_date(m_session, start_day, stop_day) > 200000):
day_interval = day_interval - 1
if (day_interval < 0):
with open("download_record.txt", "a") as f:
f.write(start_day + ' can not download automatically!' + '\n')
break
stop_day = get_date(start_day, day_interval)
time.sleep(5) # 5秒之后再访问
# 如果单天的数据量超过20w,则跳过当天数据的下载
if (day_interval < 0):
# 更新时间段
day_interval = init_data_interval
start_day = get_date(stop_day, 1) # 起始日期为上次结束日期的下一天
stop_day = get_date(start_day, day_interval)
continue
# 某个时间段中的总记录数
record_count = check_data_count_during_date(m_session, start_day, stop_day)
# 满足打包条件后,发送打包任务
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' 发送任务...')
project_id = send_pack_data_proceess(m_session, start_day, stop_day)
# 发送打包任务后,先发两次查询,相隔一段时间后再查询一次
file_result = check_proceess(m_session, project_id)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' ' + file_result['id'] + ' ' + file_result['file_file_name'])
minute_45 = 2700 #45分钟,下载近20w数据量的轮询时间
minute_20 = 1200 #20分钟,下载单天数据量的轮询时间
while(file_result['id'] == 'null'):
time.sleep(minute_45)
file_result = check_proceess(m_session, project_id) # 更新检查
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' ' + file_result['id'] + ' ' + file_result['file_file_name'])
# 下载文件
file_name = 'observations-' + start_day + '-' + stop_day + '.csv.zip'
download_zip(m_session, file_result['id'], file_result['file_file_name'], store_path + file_name)
# 将文件基本信息进行记录
with open("download_record.txt", "a") as f:
f.write(file_name + ' ' + start_day + ' ' + stop_day + ' ' + str(record_count) + '\n')
time.sleep(5) # 停止操作5秒后执行删除任务
delete_proceess(m_session, project_id) # 下载完成后,删除任务数据,防止占用后台空间
time.sleep(30) # 删除操作后,停顿30秒后继续下一个任务
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' 下载结束,开始新任务... ')
# 更新时间段
day_interval = init_data_interval
start_day = get_date(stop_day, 1) #起始日期为上次结束日期的下一天
stop_day = get_date(start_day, day_interval)
m_session.close() # 结束网络会话连接
运行结果
下一步计划
上面的项目代码只是为了满足博主个人的需要,其实还是有很多增加的部分。尤其是在查询数据量阶段,可能存在以下几种可能。
- 单天全球的数据量超过20w,此时需要分为南半球下载和北半球下载,或者东西半球下载;
- 不需要全球那么多的数据,只需要某个地区的数据,此时需要构建新的查询url,同时在发送任务的数据表单修改对应的url;
- 不想有太多的zip包,而是下载完能够将一个时期内的数据都合并为一个完整的表格;
- …