前言
好久不更了,工作太忙- -入职不到两三个月,编程语言从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为什么叫异步,是因为它有回调函数。浏览器把数据发过去之后不会干坐着等待接收端的回复,而是等数据到来的时候,去调用回调函数处理。在并发里也是这个道理,你通过信号量通信控制并发,或者怎么样。肯定都没有直接方法调用要快,内存消耗更少。