INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!

INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!

目录

前言

iNaturalist 是世界上最受欢迎的大自然观察网站之一。 iNaturalist 为动植物学家提供了大量的由志愿者拍摄提供的动植物图片或记录,这些数据对于学者进行研究非常重要。他们设置了一个网页供用户下载数据,但由于网站数据导出的限制,每次只能下载不超过20w条记录,而且每次导出都需要很长一段时间,学者需要频繁查看是否下载完成,下载数据后重复操作继续下载。这个过程十分耗费时间,因此提供一个脚本实现自动化数据下载。

目前项目已经提交在GitHub上,需要的同学可以自行clone,顺便给个小小的star!

实现思路

  1. 获取INaturalist网站的用户cookies与authentication token;
  2. 查询目标的数据量,如果超过20w条记录,则需要缩小查询范围;
  3. 提交数据打包任务;
  4. 查询任务是否完成;
  5. 下载数据包并记录数据包的基本信息;
  6. 删除任务,避免占用太多后台空间(为己为人,保护网络资源)

实现过程

【注意:我的需求是下载INaturalist上全年全球的动植物数据,打包数据的字段除了Taxon Extras不包含,其他全部都需要】

1、用户cookies与authentication token的获取

这一步有两种实现方案:一种是用户手动获取,一种是通过模拟登陆获取。
由于获取cookies与authentication token只需要用户在浏览器登录网站就能得到,而且只需要获取一次,因此没有采用模拟登陆获取的方案。以下讲述用户如何手动获取这两个用户标识。

  • 注册并登录数据导出网页,按下F12,并点击Network,刷新页面,按照下图步骤寻找关键字,记录cookies中的内容。
    INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!
  • 通过网页提供的查询栏,随便填写选项后发出查询,此时可以看到用户的authentication token,记录token的内容。INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!
# 设置用户的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设计自己的构建方法。由于这个部分有很的组合类型,博主就不一一列出,仅使用最简单的日期作为示例。
    INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!
# 用于查询指定日期内有多少条记录
# 输入参数: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。

  • 提交的打包任务需要告诉后台你需要什么样的字段。后台通过表单的形式接收这些字段参数。
    INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!
# 设置创建的时间段、需要包含的字段信息,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() # 结束网络会话连接

运行结果

INaturalist爬虫:动植物研究学者的数据下载利器,别再为下载数据花时间发愁!

下一步计划

上面的项目代码只是为了满足博主个人的需要,其实还是有很多增加的部分。尤其是在查询数据量阶段,可能存在以下几种可能。

  • 单天全球的数据量超过20w,此时需要分为南半球下载和北半球下载,或者东西半球下载;
  • 不需要全球那么多的数据,只需要某个地区的数据,此时需要构建新的查询url,同时在发送任务的数据表单修改对应的url;
  • 不想有太多的zip包,而是下载完能够将一个时期内的数据都合并为一个完整的表格;
上一篇:【静态路由配置实验】某公司有一个总部和两个分部,分别都是一个独立的局域网。


下一篇:【Java EE 学习 35 下】【struts2】【struts2文件上传】【struts2自定义拦截器】【struts2手动验证】