2021春项目需求记录 python实现模拟登录+爬取NASA Modis 上的产品数据

python实现模拟登录+爬取Nasa Modis 上的产品数据

概述

3月的中旬时候参与了学校的一个大创项目,作为本科生,本人只是摸鱼打杂,负责了其中的一个功能模块:爬取NASA Modis数据。

整个过程也只是慢慢修改增添方案,主体功能于四月中旬基本完成。因为后续并未真正用到,所以自己还没进行更多优化,请各位谅解。如个人使用参考,可以试试这个爬虫。(2021年3-5月期间的,最近没留意网站更新)

整个过程中,可以收获对Cookie、python的selenium库以及基本爬虫知识的一定了解(也是我自己学到的一些东西)。

基本思路

2021春项目需求记录 python实现模拟登录+爬取NASA Modis 上的产品数据
2021春项目需求记录 python实现模拟登录+爬取NASA Modis 上的产品数据

代码

nasa_moids.py:

import requests
import json
import os
import random
import time
import datetime
from dateutil.relativedelta import relativedelta
import autoClick

__author__ = 'Ray'

'''
Task:输入产品信息,附加筛选条件,自动爬取Modis遥感数据

观察/记录:
    1.数据一般延时更新,且所选区域在部分日期下没有数据
    2.仅hdf所在URL需要cookies,所以应模拟登录获取Cookies(其实短期内Cookies不变,如果不要求过高,可以手动输入Cookies)
    3.要用到中间URL,找规律

Tips:
    1.网络事先需要连接好,并保持稳定,否则不只会影响模拟点击。(看到一种说法是借助浏览器厂商差异,换FireFox,还未尝试)
    2.Modis网站的服务器运行状况良好
    3.解决访问时间过长,模拟点击报错问题!(循环尝试?),下载URL卡死问题
    4.目前的访问和下载速度较慢,考虑异步,多线程下载或其他方式
    5.ban,代理池,访问频率(考虑要不要定期清除Cookie)
    6.符合使用需求
    7.后续维护问题,观察网站变化,也要靠考虑浏览器版本升级与驱动的兼容
'''


class Nasa:  # 面向对象编程,创建类
    def __init__(self):
        self.headers1 = {
            'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36",
            'Referer': 'https://ladsweb.modaps.eosdis.nasa.gov/search/order/4/MOD11A1--6/2021-02-22..2021-03-08/DB/97,30.1,107.1,21.2',
            'Host': 'ladsweb.modaps.eosdis.nasa.gov',
            'Accept': '*/*',
            'Connection': 'keep-alive',
            'X-Requested-With': 'XMLHttpRequest'}
        # 还未修改Referer!!
        # 留意下headers会不会变化
        # 当遇到403时,headers的内容越全越有希望,除了在get()方法中添加cookies参数,也可以在headers字典中添加cookies
        self.headers2 = {
            'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"}

    # 获取当前日期范围
    def getDate(self, if_range='last month', range=20):
        # example: '2021-02-22..2021-03-08'
        date = time.strftime('%Y-%m-%d', time.localtime())

        # 如果是范围筛选,还要考虑年份和月份的灵活变化
        if if_range == 'all':
            date = '2020-01-01..' + date
        else:
            date = str(datetime.date.today() + relativedelta(days=-30)) + '..' + str(date)

        print('Date range:' + str(date) + '\n')
        return date

    # 获取页面
    def getPage(self, url, cookie=None):
        try:
            re = requests.get(url, headers=self.headers1, timeout=(9.05, 6.05), cookies=cookie)
            print('已完成get指令')
            print(re.status_code)
            re.raise_for_status()
            re.encoding = re.apparent_encoding
            print('获取页面成功!准备return')
            # time.sleep(1)
            return re
        except requests.exceptions.RequestException as e:
            print(e)
            print('请求失败!您的网络状况可能存在一点问题,需重试')

    # 将开发者工具界面中,Headers里显示的cookie转换为字典
    def cookieToDict(self, cookie):
        cookie = cookie.split(';')
        cok = dict()
        for i in cookie:
            part = i.split('=')
            part[0] = part[0].strip(' ')
            cok[part[0]] = part[1]
        return cok

    # 通过间接爬取,下载数据文件
    def getHdf(self, data_kind, date_ranges, area, cookie):
        for kind in data_kind:
            url1 = 'https://ladsweb.modaps.eosdis.nasa.gov/api/v1/files/product={}&collection={}&dateRanges={}&areaOfInterest={}&dayCoverage=true&dnboundCoverage=true'.format(
                data_kind[kind][0], data_kind[kind][1], date_ranges, area)
            url2_head = 'https://ladsweb.modaps.eosdis.nasa.gov'

            # 获取hdf的URL字段
            while True:
                try:
                    re = nasa.getPage(url1)
                    # print('str:\n' + re.text[:500] + '\n')  # 测试而已
                    text = json.loads(re.text, strict=False)
                    # text = re.json()
                    print(text)
                    print('\n')
                    file_url = []  # 存储fileURL的列表,每一个元素包括fileURL和对应日期

                    for i in text:
                        j = text[i]
                        f_url = j.get('fileURL')  # 神奇的地方,这里一开始用正常键值对访问,只有fileURL会显示KeyError
                        f_date = j.get('start').split(' ')[0]
                        file_url.append((f_url, f_date))

                    # 浏览器页面最多只能显示两千条数据,但是url1里会包含所有数据的url2
                    print(file_url)
                    file_length = len(file_url)
                    print('Length of hdf files:' + str(file_length) + '\n')
                    break

                except:
                    print('尝试爬取file URL失败,可能原因如下:1.url已更新;(2.cookies有误或失效;)3.网页json已更新')

            # 保持下载状态,这里指下载全部文件的一个周期
            while True:
                count = 1
                downloaded = 0
                skip = 0
                root = 'upload/'

                # 解决新的防卡死问题,考虑headers问题
                for i in file_url:
                    try:
                        path = root + i[1] + '/' + kind
                        if not os.path.exists(path):  # 如果文件目录不存在
                            os.makedirs(path)  # os.mkdir()只能创建一级目录,而os.makedirs()可以创建多级目录

                        url2 = url2_head + i[0]
                        filepath = path + url2.split("/")[-1]
                        if os.path.exists(filepath):
                            print(str(count) + '.文件已存在\n')
                            count += 1
                            skip += 1
                            continue

                        re = nasa.getPage(url2, cookie)
                        hdf = re.content
                        print(str(count) + '.已获取下载目标,准备下载')
                        with open(filepath, 'wb') as f:
                            f.write(hdf)
                            print('下载完成!\n')
                            f.close()
                            downloaded += 1
                    except:
                        print("下载第" + str(count) + "个文件失败,原因可能是连接出错\n")

                    count += 1
                    time.sleep(random.uniform(3, 4.5))

                if downloaded + skip >= file_length:
                    break
                # 遍历完一遍下载页面URL,长时间休眠一次
                time.sleep(random.uniform(480, 720))


# 获取访问下载URL时Request Headers中的Cookie
def getCookie(driver_path, date_range, area_of_interest, username, password):
    auto = autoClick.GetCookie(driver_path, date_range, area_of_interest, username, password)
    cookie = auto.autoClick()
    print('已获取cookie')

    # 整合得到Request Headers中的Cookie
    request_cookie = dict()
    for i in cookie:
        request_cookie[i['name']] = i['value']
    assert (request_cookie != {})
    print(request_cookie)
    return request_cookie


if __name__ == '__main__':
    # nasa = Nasa()
    # date = nasa.getDate()
    # area1 = '97,30,107,21'
    # path = "chromedriver.exe"
    # user = '......'  # 个人账户及密码信息!
    # code = '......'
    # kind = {'upload/Surface temperature/': ['MOD11A1', 6],
    #         'upload/Terra surface reflectance/': ['MOD09A1', 61]}
    # area2 = 'x97y30,x107y21'  # 文件url信息所在的url中,area信息格式与产品交互页面不同
    # cookie = ''
    #
    # # 暂定每天爬取1次Cookie,3次遥感数据
    # while True:
    #     if (datetime.datetime.now().hour == 0) and (datetime.datetime.now().minute == 00):
    #         cookie = getCookie(path, date, area1, user, code)
    #         time.sleep(3)
    #         nasa.getHdf(kind, date, area2, cookie)
    #         time.sleep(300)
    #         # 考虑计时任务
    #
    #     if datetime.datetime.now().hour == 4 or datetime.datetime.now().hour == 8:
    #         if datetime.datetime.now().minute == 00:
    #             nasa.getHdf(kind, date, area2, cookie)
    #             time.sleep(600)

    nasa = Nasa()
    date = nasa.getDate()
    area = '97,30,107,21'

    path = "chromedriver.exe"
    user = '......'  # 个人账户及密码信息!
    code = '......'

    cookie = getCookie(path, date, area, user, code)
    time.sleep(3)

    kind = {'Surface temperature/': ['MOD11A1', 6], 'Terra surface reflectance/': ['MOD09A1', 61]}  # 后续维护问题,观察网站变化
    area = 'x97y30,x107y21'  # 文件url信息所在的url中,area信息格式与产品交互页面不同
    nasa.getHdf(kind, date, area, cookie)


'''
知识笔记:
在爬虫中,有时候处理需要登录才能访问的页面时,我们一般会直接将登录成功后获得的Cookies
放在请求头里面直接请求,一般不必重新登录(如果Cookies稳定不变)。

当浏览器下一次请求该网站时,浏览器会把此Cookies放到请求头一起提交给服务器,
Cookies携带了会话ID信息,服务器检查它即可找到对应的会话,再对其判断以辨认用户状态

如果传给服务器的Cookies无效,或者会话过期,则不能继续访问页面。

因此,Cookies需与对话配合,一个处于客户端,一个处于服务端。

每次通过HTTP协议访问一个资源的时候,浏览器都会自动在请求的header中加入你在这个域名下的所有cookies。
服务器在收到请求后可以根据是否有cookies以及cookies的值来判断你的身份,从而返回不同的资源。
(在登陆页面登陆后,服务器会会给你分配一个cookie,并放入返回的response的header中。
你的浏览器会储存这个cookie并在每次访问这个网站时都加入request的header中。
同时,服务器也会对给你分配的cookie的值进行配对存储,这样就能通过你的cookie过得你的身份了。)

当遇到403时,headers的内容越全越有希望,除了在get()方法中添加cookies参数,也可以在headers字典中添加cookies。

- 模拟登录:
    (requests.Session()可以维持一个会话,并且自动处理Cookies)
    1.利用网页开发工具,勾选preserve log;

    2.清空Cookies,初次登陆,一般是查看Network里的第一个请求,观察Request Headers 
    和 Form Data里的独特信息,他们就是模拟登录需要的信息;
    同时,也可以看到登陆后Response Headers中会有一个Set-Cookie,一般会与其他Cookie有关;

    3.退出登录,清空Cookies,回到登录页面,
    在Elements里找token。其他信息一般也可以通过这两项获得;

    4.终极:selenium模拟浏览器登录后(能否先seesion登录),先尝试获取Cookies。

- 模拟点击:
    需用代码精确模拟使用浏览器时的一系列操作。

'''

autoClick.py:

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
#from selenium.webdriver.common.keys import Keys
import time
import random

__author__ = 'Ray'


class GetCookie:
    def __init__(self, driver_path, date_range, area_of_interest, username, password):
        self.driver_path = driver_path
        self.date_range = date_range
        self.area_of_interest = area_of_interest
        self.username = username
        self.password = password

    def autoClick(self):
        count = 1
        while True:
            # 1.创建Chrome浏览器对象,这会在电脑上在打开一个浏览器窗口

            # 开启开发者工具(F12)
            # option = webdriver.ChromeOptions()
            # option.add_argument("--auto-open-devtools-for-tabs")
            # browser = webdriver.Chrome(chrome_options=option,
            #                           executable_path=self.driver_path)

            # 为给webdriver的options增加参数excludeSwitches
            option = webdriver.ChromeOptions()
            option.add_experimental_option('excludeSwitches', ['enable-automation'])
            option.add_experimental_option('useAutomationExtension', False)
            browser = webdriver.Chrome(executable_path=self.driver_path, options=option)
            #browser = webdriver.Chrome(executable_path=self.driver_path)
            # browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            #     "source": """
            #     Object.defineProperty(navigator, 'webdriver', {
            #       get: () => undefined
            #     })
            #   """
            # })

            try:
                # 2.通过浏览器向服务器发送URL请求
                url1 = 'https://ladsweb.modaps.eosdis.nasa.gov/search/order/4/MOD11A1--6/{}/DB/{}'.format(
                    self.date_range, self.area_of_interest)  # 后续维护问题,观察网站变化,不过该模块目的只是获取cookie
                # browser.get(
                #     'https://ladsweb.modaps.eosdis.nasa.gov/search/order/4/MOD11A1--6/2021-02-22..2021-03-08/DB/97,30.1,107.1,21.2')
                browser.get(url1)
                time.sleep(1)

                # 3.刷新浏览器
                # browser.refresh()

                # 4.设置浏览器的大小
                browser.set_window_size(1400, 800)

                # 5.登陆页面之前
                profile = browser.find_element_by_id("profile-menu-option")
                ActionChains(browser).move_to_element(profile).perform()
                profile.click()
                time.sleep(4.5)  # !注意对手动使用浏览器时的精准模拟,这里会有近1秒的Loading,才会显示login按钮,前提是网络条件良好
                login = browser.find_element_by_xpath("/html/body/nav/div/div/ul/li[6]/ul/li/a")  # 后续维护问题,观察网站变化,暂时未找到更好的定位方式
                login.click()

                # 6.模拟登录
                # 输入账号
                input_account = browser.find_element_by_id('username')
                input_account.send_keys(self.username)
                # 输入密码
                input_password = browser.find_element_by_id('password')
                input_password.send_keys(self.password)
                # 取消勾选保持登陆状态的选项
                cancel_stay_in = browser.find_element_by_id('stay_in')
                cancel_stay_in.click()
                # 点击登录按钮
                time.sleep(2)
                login_button = browser.find_element_by_name('commit')

                # builder = ActionChains(browser)
                # builder.key_down(Keys.F12).perform()

                login_button.click()
                # print('=====================================')
                # print("middle page url: " + str(browser.current_url))
                # print("middle page title: " + str(browser.title))
                # print("cookie信息为:" + str(browser.get_cookies()))  # 多个Cookie
                # print(browser.get_cookies)

                # 7.回到产品页面,发现该页面下Cookie 各name-value组成的对能满足我们所需的request headers中的Cookie
                time.sleep(5)
                print('\n=====================================')
                print("current url: " + str(browser.current_url))
                print("current title: " + str(browser.title))
                cookie = browser.get_cookies()
                print('\n')
                print(type(cookie))
                print("cookie信息为:" + str(cookie))  # 多个Cookie

                time.sleep(4)
                # browser.quit()  # 关闭所有窗口
                return cookie

            except:
                browser.quit()
                print('第' + str(count) + '次模拟点击过程中遇到问题\n')
                time.sleep(random.uniform(5, 6))
                count += 1


# 用于测试的主程序,实际投入时应运行nasa_modis.py中的主程序
if __name__ == '__main__':
    path = "chromedriver.exe"
    ranges = '2021-02-22..2021-03-08'
    interest = '97,30.1,107.1,21.2'
    user = '......'  # 个人账户信息及密码!
    code = '......'
    auto_click = GetCookie(path, ranges, interest, user, code)  # 可以def __init__(self, parameters):
    cookie = auto_click.autoClick()

login.py(未使用selenium的模拟登录,项目未用上,仅供参考):

import requests
from requests import utils
from lxml import etree
import json
import time
'''
1.每一次访问登录页面,token都会更新
2.如果先在earthdata正页登录,进入产品页面还需走确认登录流程
'''


class Login:
    def __init__(self):
        self.headers1 = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"}
        self.headers2 = {
            'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36",
            'Referer': 'https://ladsweb.modaps.eosdis.nasa.gov/search/order/4/MOD11A1--6/2021-02-22..2021-03-08/DB/97,30.1,107.1,21.2',
            'Host': 'ladsweb.modaps.eosdis.nasa.gov',
            'Accept': '*/*',
            'Connection': 'keep-alive',
            'X-Requested-With': 'XMLHttpRequest'}
        self.login_url = 'https://urs.earthdata.nasa.gov'
        self.post_url = 'https://urs.earthdata.nasa.gov/login'
        # 测试用的logined_url
        self.logined_url = 'https://ladsweb.modaps.eosdis.nasa.gov/api/v1/files/product=MOD11A1&collection=6&dateRanges=2021-02-22..2021-03-08&areaOfInterest=x97y30,x107y21&dayCoverage=true&dnboundCoverage=true'
        self.session = requests.Session()  # 神奇,会话处理Cookie
        self.username = '......'  # 个人账户信息及密码
        self.password = '......'

    def token(self):
        response = self.session.get(self.login_url, headers=self.headers1, timeout=30)  # 可能会被封ip
        print('准备获取token')
        selector = etree.HTML(response.text)
        token = selector.xpath('//*[@id="login"]/input[2]/@value')[0]  # 日了,获取属性不用text()
        print('token: ' + str(token) + '\n')
        return token

    def getCookie(self):
        pass

    def login(self, cookie={}):
        post_data = {
            'commit': 'Log in',
            'utf8': '✓',
            'authenticity_token': self.token(),
            'username': self.username,
            'password': self.password,
        }    # 'redirect_uri': self.logined_url

        response = self.session.post(self.post_url, data=post_data, headers=self.headers1)  # 模拟登录
        print('模拟登录成功!\n')

        cookie = response.cookies  # 里面有首次登录得到的Set-Cookie
        cookie = utils.dict_from_cookiejar(cookie)
        print(cookie)
        print(type(cookie))
        print('\n')
        time.sleep(1)

        response = self.session.get(self.logined_url, headers=self.headers2, timeout=30, cookies=cookie)
        time.sleep(1)
        # if response.status_code == 200:
        #     # 这里response headers里没看到cookie
        #     self.getHdf(response, cookie)


if __name__ == '__main__':
    log = Login()
    log.login()
上一篇:ENVI+IDL遥感图像处理


下一篇:MTK Modis 使用简介