前言

好久不更了,工作太忙- -入职不到两三个月,编程语言从Java跳C/C++,后来又需要爬虫,又去学了python,最近几日还在学vue...怎么说呢,往好听了说叫博学多识,往坏了说广而不专- -。 接下来我就对最近的爬虫工作做一个总结。

我要爬取的网站不方便贴出来,不过这是个老网站了,没有什么验证码那一类的反爬措施。要说麻烦在哪了的话,那就是小日本的编程逻辑了吧...请求参数命名随心所欲(从这一点上来看,是个外包公司没错了,听说日本好多来中国找外包,这个应该不是,因为我在JS里看到的注释是日文的),着实让人摸不着头脑。这些参数大多是纯英文,但是有那么一部分关键的参数命名是用的日文发音加英文写的。。。在他们的请求参数里,还有一个时间戳参数,你每一次请求都会更新时间戳,下一次的请求必须时间戳跟这个时间戳一摸一样。这个操作可以的,直接让我没办法用同一个账号的cookie多个服务器同时爬。

最坑的是,它只能通过IE访问,因为它是ActiveX做的,这是微软提供的一个IE插件,我爬虫的过程中好多问题都让我怀疑人生了,动不动页面就卡住动不了了。所以,我程序里日志和异常捕获做的特别多。。。

selenium的execute_script()

我一开始使用webdriver获取元素然后.click()做的点击事件,不知道是不是ActiveX的问题...所以,通通换成了用Js操作:

document.getElementById('Login').click()

而输入账号密码,用send_keys()仿佛又太慢了,干脆换成:

document.getElementById('XXX').value = 'SSM'

由于页面是动态加载的,所以在通过F12观察并拼接参数后,就开始访问对应的URL获取JSON(通过Python中的requests插件发送的请求),不过对有一部分URL的访问遇到了问题,没有报错,但是返回的JSON中缺少了我所需要的数据。于是突发奇想,干脆在页面上执行JS发送XMLHttpRequest请求获取数据好了,结果一试还真的就拿到了。但是XMLHttpRequest是异步的,获取后台数据后,会执行回调函数。所以突发奇想,用JS把数据都放到body标签的一个自定义的属性里,后面再获取这个属性就好了。以下,是我封装的方法:

def get_response_by_xml_request(driver, params, url, wait_time=1.5):
    """
    通过XMLHttpRequest发送请求,获取响应的JSON
    :param driver:
    :param params:发送请求时的参数(map类型)
    :param url:Post请求访问的URL
    :param wait_time: 本次发送请求,等待响应回复的时间
    :return:
    """

    # 记录下当前的timestamp,后面比较,如果发现从JSON中取到的和当前的一样
    old_timestamp = driver.find_element_by_xpath("//input[@name='_TIMESTAMP']").get_attribute("value")

    # 在页面上发送XMLHttpRequest请求的基本JS语句
    js_str = """
                    var shen_obj = '%s';
                    var shen_url = '%s';
                    var shen_httpRequest = new XMLHttpRequest();
                    const callback = arguments[arguments.length - 1];
                    shen_httpRequest.open('POST', shen_url, true);
                    shen_httpRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
                    shen_httpRequest.send(shen_obj);

                    shen_httpRequest.onreadystatechange = function () {
                        if (shen_httpRequest.readyState == 4 && shen_httpRequest.status == 200) {
                            var json_str = shen_httpRequest.responseText;
                            var json_attr = document.createAttribute('json');
                            json_attr.nodeValue = json_str;
                            //给body标签添加自定义属性
                            document.getElementsByTagName('body')[0].setAttributeNode(json_attr);
                            //设置新的_TIMESTAMP
                            var json = JSON.parse(json_str);
                            document.getElementsByName('_TIMESTAMP')[0].value = json['_UCT']['_TIMESTAMP'];
                        }else if(shen_httpRequest.status != 200){
                            //如果获取的响应不是正确回应,则设置body为None值
                            var json = 'None';
                            var json_attr = document.createAttribute('json');
                            json_attr.nodeValue = json;
                            document.getElementsByTagName('body')[0].setAttributeNode(json_attr);
                        }
                    };
             """ % (get_request_param(params), url)

    try:
        # 执行Js给body标签添加属性
        result = driver.execute_script(js_str)
        print("result: %s" % result)
        time.sleep(1000)
    except selenium.common.exceptions.JavascriptException as js_exception:
        logger.error("执行的JS语句出错,该JS为 %s" % js_str)
        raise js_exception

    time.sleep(wait_time)
    result_str = driver.find_element_by_tag_name("body").get_attribute("json")

    try:
        # 以30秒为期限等待,TimeStamp改变
        wait_timestamp_change(driver, old_timestamp)
    except TimeStampError:
        logger.warning("_TIMESTAMP设置失败,尝试重新设置")

        if result_str == "None":
            raise NoResponseError("收到的回应为空")

        # 先比较看是不是获取的JSON有问题
        result_json = json.loads(result_str)
        new_timestamp = result_json['_UCT']['_TIMESTAMP']

        # 判断新的JSON是否被放入到body的json属性中了
        if new_timestamp != old_timestamp:

            # 不相同,则将新获取到的new_timestamp设置到form表单中
            set_timestamp_last_frame(driver, result_str)
        else:
            # 相同则说明json获取失败,重新从网页上获取
            logger.warning("新获得的JSON与上一次请求的JSON相同")
            time.sleep(2)

            result_str = driver.find_element_by_tag_name("body").get_attribute("json")
            result_json = json.loads(result_str)
            new_timestamp = result_json['_UCT']['_TIMESTAMP']

            if new_timestamp == old_timestamp:
                # 获取新的JSON失败
                logger.error("页面上的JSON没有变化")
                raise TimeStampError
            else:
                set_timestamp_last_frame(driver, result_str)

    return result_str

由于往body里放属性,我也不知道是哪个JSON,甚至连时间戳更新没有都不清楚,所以在里面加上了对时间戳的获取和比较。

execute_async_script()执行异步JS

该方法是执行异步的JS,它对应的会让driver去等待一段时间,等待JS去调用它设定的回调函数,该回调函数直接用这样一句就好了:

const callback = arguments[arguments.length - 1];

const或者var,let都可以,具体代码如下:

# 在页面上发送XMLHttpRequest请求的基本JS语句
    js_str = """
                    var shen_obj = '%s';
                    var shen_url = '%s';
                    var shen_httpRequest = new XMLHttpRequest();
                    const callback = arguments[arguments.length - 1];
                    shen_httpRequest.open('POST', shen_url, true);
                    shen_httpRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
                    shen_httpRequest.send(shen_obj);

                    shen_httpRequest.onreadystatechange = function () {
                        if (shen_httpRequest.readyState == 4 && shen_httpRequest.status == 200) {
                            var json_str = shen_httpRequest.responseText;
                            
                            //设置新的_TIMESTAMP
                            var json = JSON.parse(json_str);
                            document.getElementsByName('_TIMESTAMP')[0].value = json['_UCT']['_TIMESTAMP'];
                            callback(json_str);
                        }else if(shen_httpRequest.status != 200){
                            //如果获取的响应不是正确回应,则设置body为None值
                            callback('None');
                        }
                    };
             """ % (get_request_param(params), url)
    try:
        # 执行Js给body标签添加属性
        result_str = driver.execute_async_script(js_str)
    except selenium.common.exceptions.JavascriptException as js_exception:
        logger.error("执行的JS语句出错,该JS为 %s" % js_str)
        raise js_exception

我JS变量命名奇葩是因为担心会和它程序中的全局变量重名。不过我并不了解selenium实现JS语句的底层原理,还请大佬赐教,也欢迎萌新一起讨论QAQ。

总结

这次爬虫主要就是学了个框架,以及为了程序健壮性和改BUG更快一些,你程序中的异常处理,日志打印等等。selenium爬虫框架是可以模拟人的行为的,所以只要是显示在页面上的数据,那都是可抓取的。顺带一提,selenium支持Python和Java等多种语言。在用之前,一定要先去官网看看相关的说明。比方说我这个Ie在使用的时候就需要设置一些东西。另外,对于在selenium的驱动程序路径加到path里我不是很喜欢,我直接写在xml里,在用webdriver.IE('path')方法的时候,把路径作为参数传进去就好啦。另外,驱动程序的版本也是要和selenium的版本对应的。

异步:异步,简单粗暴的理解就是剩下的事儿不需要你去做。不过这个理解并不准确,我目前的理解是,异步是针对于IO来讲的,就像ajax为什么叫异步,是因为它有回调函数。浏览器把数据发过去之后不会干坐着等待接收端的回复,而是等数据到来的时候,去调用回调函数处理。在并发里也是这个道理,你通过信号量通信控制并发,或者怎么样。肯定都没有直接方法调用要快,内存消耗更少。