WebView实现¶
WebView是所有WebControl的底层容器,WebView也是进行Web自动化的底层窗口基础,所有的Web控件操作最终都会体现在WebView的操作上,此外,WebView的封装还会用到底层的系统窗口的一些属性。WebView的封装都需要继承QT4W中定义的IWebView基类,在该基类中定义了一些列WebView需要实现的操作: 1.WebView的窗口属性; 2.常见操作:点击、拖拽、鼠标hover、滑动等。 3.辅助接口:Js执行、截屏、文件上传等。 IWebView中具体的接口定义详见*API文档*。
WebView示例¶
下面就以PC端Windows上的WebView封装为例来说明整个WebView封装的流程。首先,这里需要明确的是WebView本质上也是系统上的一个窗口,对于Windows系统来说,webView的底层实现是基于window窗口,所以实现webView需要封装windows的窗口及其他的系统操作。
封装系统窗口¶
在封装WebView前,需要封装好系统的控件实现,提供基本的能力支持,QT4X已经实现了系统原生窗口封装、鼠标操作及键盘输入输入等。QT4C提供了Windows原声控件的封装;QT4A提供Android系统原生控件操作;QT4I提供IOS控件操作。此处的具体实现可以参考QT4X的接口文档: QT4C: 接口文档链接(暂未开源,开源后更新); QT4A: 接口文档链接; QT4I: 接口文档链接;
实现WebView功能¶
整个WebView功能大致可以分为三个部分: 其一:WebView的属性及基本能力提供,这一部分提供的能力主要用于实现WebView自身的操控,包括获取自身的Rect属性、一些列点击操作以及输入操作.这部分实现和操作系统的窗口系统有着十分紧密的联系,故此处会用到操作系统的窗口操作API。下面给出QT4C端的windows系统上的这部分实现。
@property
def rect(self):
'''当前可见窗口的坐标信息
'''
return win32gui.GetClientRect(self._window.HWnd)
def activate(self, is_true=True):
'''激活当前窗口
:param is_true: 是否激活,默认为True
:type is_true: bool
'''
self._window.TopLevelWindow.bringForeground()
win32gui.BringWindowToTop(self._window.TopLevelWindow.HWnd)
def _inner_click(self, flag, click_type, x_offset, y_offset,):
self.activate()
new_x, new_y = win32gui.ClientToScreen(self._window.HWnd, (int(x_offset), int(y_offset)))
if self._offscreen_win:
new_x += self._window.BoundingRect.Left - self._offscreen_win.BoundingRect.Left
new_y += self._window.BoundingRect.Top - self._offscreen_win.BoundingRect.Top
Mouse.click(new_x, new_y, flag, click_type)
def click(self, x_offset, y_offset):
self._inner_click(MouseFlag.LeftButton, MouseClickType.SingleClick, x_offset, y_offset)
def double_click(self, x_offset, y_offset):
self._inner_click(MouseFlag.LeftButton, MouseClickType.DoubleClick, x_offset, y_offset)
def right_click(self, x_offset, y_offset):
self._inner_click(MouseFlag.RightButton, MouseClickType.SingleClick, x_offset, y_offset)
def long_click(self, x_offset, y_offset, duration=1):
raise NotImplementedError
def hover(self, x_offset, y_offset):
self.activate()
new_x, new_y = win32gui.ClientToScreen(self._window.HWnd, (int(x_offset), int(y_offset)))
if self._offscreen_win:
new_x += self._window.BoundingRect.Left - self._offscreen_win.BoundingRect.Left
new_y += self._window.BoundingRect.Top - self._offscreen_win.BoundingRect.Top
Mouse.move(new_x, new_y)
def scroll(self, backward=True):
self._window.scroll(backward)
def send_keys(self, keys):
self._window.sendKeys(keys)
说明:这里使用了经过Python封装的WIN32API来获取Windows窗口操作能力,WebView在WindowsPC上本质就是一个Window,在WebView初始化时会传入,一直指代其自身的窗口句柄self._window。这部分的接口实现就是使用经过Python封装的windowsAPI来对这个窗口进行一些列操作。此处封装较为简单,主要就是对系统API在封装。对于PC端来说,没有长按这个操作,故long_click()可以不用实现,此处可以不重写,但是如果重写该方法,就需要raise NotImplmentError,以便告知用户该功能未实现。 其二:实现eval_script()接口,提供注入JS脚本的能力,此处具体实现在不同的浏览器上有所差异,执行JS代码是浏览器内核所具备的能力,因此此处需要实现浏览器驱动来执行JS脚本。因此这里需要对不同浏览器内核通信方案比较熟悉,才能很好地实现该方法。下面给出一个封装IE浏览器内核驱动的示例,windows上是通过windows消息机制来建立和浏览器的通信机制,根据提frameID来获取指定的HTMLWindow,具体实现如下:
class IEDriver(object):
'''window['qt4w_driver_lib']
'''
def __init__(self, ie_server_hwnd):
self._hwnd = ie_server_hwnd
self._init_com_obj()
def _init_com_obj(self):
'''初始化com对象
'''
if hasattr(self, '_doc'): logging.debug('[IEDriver] re_init com_obj')
else: time.sleep(2) # 部分IE10上发现打开页面时不sleep会导致拒绝访问错误
msg = win32gui.RegisterWindowMessage('WM_HTML_GETOBJECT')
for _ in range(3):
try:
ret, result = win32gui.SendMessageTimeout(self._hwnd, msg, 0, 0, win32con.SMTO_ABORTIFHUNG, 2000)
ob = pythoncom.ObjectFromLresult(result, pythoncom.IID_IDispatch, 0)
self._doc = win32com.client.dynamic.Dispatch(ob)
self._win = self._doc.parentWindow
break
except AttributeError, e:
# 页面跳转时易发生此问题
logging.debug(str(e))
time.sleep(0.5)
else:
raise RuntimeError('初始化COM对象失败')
def _check_valid(self):
'''检查com对象的有效性
'''
try:
self._doc._oleobj_.GetIDsOfNames('readyState')
return True
except pywintypes.com_error, e:
if (e.args[0] % 0x100000000) == 0x80070005:
self._init_com_obj() # 重新初始化
return False
raise e
def eval_script(self, frame_win, script, use_eval=True):
'''
IE10以上异常对象才有stack属性
'''
logging.debug('[IEDriver] eval script: %s' % script[:200].strip())
if not isinstance(script, unicode):
script = script.decode('utf8') # 必须使用unicode编码
if use_eval:
script = script.replace('\\', r'\\')
script = script.replace('"', r'\"')
script = script.replace('\r', r'\r')
script = script.replace('\n', r'\n')
script = r'''document.script_result = (function(){
try{
var result = eval("%s");
if(result != undefined){
return 'S'+result.toString();
}else{
return 'Sundefined';
}
}catch(e){
var retVal = 'E['+e.name + ']' + e.message;//toString()
if(e.stack) retVal += '\n' + e.stack;
else{
var f = arguments.callee.caller;
while (f) {
retVal += f.name;
f = f.caller;
}
}
return retVal;
}
})();''' % script
self._check_valid()
if frame_win == None:
frame_win = self._win
frame_doc = self._doc
else:
frame_doc = frame_win.document
self._retry_for_access_denied(lambda: frame_win.execScript(script))
if not use_eval: return
if not self._check_valid(): return # 一般是页面发生跳转,此时无法获取到直接结果
name_id = frame_doc._oleobj_.GetIDsOfNames('script_result')
result = frame_doc._oleobj_.Invoke(name_id, 0, pythoncom.DISPATCH_PROPERTYGET, True)
if result == '': raise IEDriverError('JavaScript返回为空')
if isinstance(result, unicode):
result = result.encode('utf8')
logging.debug('[IEDriver] result: %s' % result[:200].strip())
return result
说明:IEDriver初始化时通过调用_init_com_obj函数获取到IHTMLDocument2的com对象,然后通过获取该对象的ParentWindow,调用IE浏览器的Window.exceScript()来执行JS脚本,并直返回JS代码的执行结果。各个浏览器的执行JS方法实现有所差别,因此这里应该根据浏览器的实现原理采用不同实现。
其三:获取页面内的属性及dom结构的操作能力的封装,在QT4W中这部分能力由WebDriver进行实现,但是在WebView中定义一个__getattr__方法来获取WebDriver的能力。WebDriver的具体能力详情见*WEbDriver封装*。
def __getattr__(self, attr):
'''转发给WebDriver实现
'''
return getattr(self._webdriver, attr)
此外,在WebView进行初始化会传递一些参数,下面看一个具体的WebView初始化实例:
class IEWebView(WebViewBase):
'''IE WebView实现
'''
def __init__(self, ie_window_or_hwnd):
'''初始化
:params ie_window_or_hwnd: ie窗口或句柄
:type ie_window_or_hwnd: Control or int
'''
if isinstance(ie_window_or_hwnd, int): # 句柄需要转化为对应的窗口,句柄是IEFrame的句柄
process_id = win32process.GetWindowThreadProcessId(ie_window_or_hwnd)[1]
from browser.ie import IEWindow_QT4W
ie_window = IEWindow_QT4W(process_id).ie_window
else:
ie_window = ie_window_or_hwnd
from qt4w.webdriver import iewebdriver
self._webdriver = iewebdriver.IEWebDriver(self)
self._browser_type = 'ie'
self._ie_window = ie_window
self._driver = IEDriver(self._ie_window.HWnd)
super(IEWebView, self).__init__(ie_window, self._webdriver)
class WebViewBase(IWebView):
'''PC端WebView基类
'''
def __init__(self, window, webdriver, offscreen_win=None):
self._window = window
self._offscreen_win = offscreen_win
self._webdriver = webdriver
self._parent_wnd = win32gui.GetParent(self._window.HWnd)
# 如果获取的父窗口是空
if self._parent_wnd == 0:
self._parent_wnd = self._window.HWnd
self._browser_type = 'not defined'
这里需要着重理解的就是self._driver和Self._webDriver这两者之间的区别,self._driver可以看成是浏览器执行JS内核的一个代理,负责执行传入的JS代码及返回结果,其具体实现和浏览器高度相关。self._webdriver是QT4W定义的负责解析dom结构的功能集合。此外,这里建议根据系统类型来实现对应的WebViewBase来封装窗口相关操作,然后再根据不同浏览器封装XX(browsername)WebView实现执行JS能力。