如何切换到 Selenium 中的活动选项卡?

How do I switch to the active tab in Selenium?

提问人:Uri 提问时间:2/25/2015 最后编辑:Uri 更新时间:8/31/2023 访问量:148983

问:

我们开发了一个 Chrome 扩展程序,我想用 Selenium 测试我们的扩展程序。我创建了一个测试,但问题是我们的扩展在安装时会打开一个新选项卡,我想我从另一个选项卡中得到了异常。是否可以切换到我正在测试的活动选项卡?或者另一种选择是从禁用扩展程序开始,然后登录我们的网站,然后才启用扩展程序。可能吗?这是我的代码:

def login_to_webapp(self):
    self.driver.get(url='http://example.com/logout')
    self.driver.maximize_window()
    self.assertEqual(first="Web Editor", second=self.driver.title)
    action = webdriver.ActionChains(driver=self.driver)
    action.move_to_element(to_element=self.driver.find_element_by_xpath(xpath="//div[@id='header_floater']/div[@class='header_menu']/button[@class='btn_header signature_menu'][text()='My signature']"))
    action.perform()
    self.driver.find_element_by_xpath(xpath="//ul[@id='signature_menu_downlist'][@class='menu_downlist']/li[text()='Log In']").click()
    self.driver.find_element_by_xpath(xpath="//form[@id='atho-form']/div[@class='input']/input[@name='useremail']").send_keys("[email]")
    self.driver.find_element_by_xpath(xpath="//form[@id='atho-form']/div[@class='input']/input[@name='password']").send_keys("[password]")
    self.driver.find_element_by_xpath(xpath="//form[@id='atho-form']/button[@type='submit'][@class='atho-button signin_button'][text()='Sign in']").click()

测试失败,因为在新选项卡(由扩展程序打开)中,“登录”不可见(我认为新选项卡仅在命令之后打开)。ElementNotVisibleException: Message: element not visibleself.driver.get(url='http://example.com/logout')

更新:我发现异常与额外的选项卡无关,它来自我们的网站。但是根据@aberna的回答,我用这段代码关闭了额外的选项卡:

def close_last_tab(self):
    if (len(self.driver.window_handles) == 2):
        self.driver.switch_to.window(window_name=self.driver.window_handles[-1])
        self.driver.close()
        self.driver.switch_to.window(window_name=self.driver.window_handles[0])

关闭额外的选项卡后,我可以在视频中看到我的选项卡。

蟒蛇硒 google-chrome-extension

评论


答:

58赞 aberna 2/25/2015 #1

一些可能的方法:

1 - 使用 send_keys (CONTROL + TAB) 在选项卡之间切换

self.driver.find_element_by_tag_name('body').send_keys(Keys.CONTROL + Keys.TAB)

2 - 使用 using ActionsChains (CONTROL+TAB) 在选项卡之间切换

actions = ActionChains(self.driver)      
actions.key_down(Keys.CONTROL).key_down(Keys.TAB).key_up(Keys.TAB).key_up(Keys.CONTROL).perform()

3 - 另一种方法可以利用 Selenium 方法来检查当前窗口并移动到另一个窗口:

你可以使用

driver.window_handles

查找窗口句柄列表,然后尝试使用以下方法进行切换。

- driver.switch_to.active_element      
- driver.switch_to.default_content
- driver.switch_to.window

例如,要切换到上次打开的选项卡,您可以执行以下操作:

driver.switch_to.window(driver.window_handles[-1])

评论

0赞 Uri 2/25/2015
它不起作用,我认为它不会切换选项卡,因为在视频中 [ app.crossbrowsertesting.com/public/id7ed71371078e09/selenium/... ]我看到活动选项卡是相同的。
0赞 Learner 12/23/2016
我认为如果第二个选项卡有文本输入并且 selenium 输入文本在那里,这种方法将不起作用——至少在我的情况下是这样。
4赞 11/3/2017
我在使用新标签时遇到了麻烦,这种方法挽救了这一天driver.switch_to_window(driver.window_handles[1])
0赞 winklerrr 9/19/2018
请注意,和实际上是方法,而不是像 !default_content()window()active_element
0赞 Zhivko.Kostadinov 2/2/2021
实际上,在不同的操作系统上,使用 kyes 和键盘玩游戏可能会产生意想不到的结果。我建议只使用内置功能,如下所示:Where, will landing on the next tab will landing on the current tabnext_tab = context.session.driver.window_handles[1] context.session.driver.switch_to.window(next_tab)driver.window_handles[1]driver.window_handles[0]
97赞 Avaricious_vulture 9/30/2017 #2

这实际上在 3.x 中对我有用:

driver.switch_to.window(driver.window_handles[1])

将附加窗口句柄,因此这将选择列表中的第二个选项卡

要继续使用第一个选项卡:

driver.switch_to.window(driver.window_handles[0])

评论

3赞 11/3/2017
它看起来很骇人听闻,但如果您知道您的测试站点应该在一个新选项卡中打开一个链接而不是其他任何内容,那么它就会很好用。非常适合快速和肮脏。
2赞 Gabriel Devillers 1/30/2019
@sdkks对不起,我不明白你的意思。如果我的评论是愚蠢的或冒犯的,请道歉,这不是故意的。我使用了这段代码,它工作正常。你能详细说明一下为什么它看起来很骇人听闻吗?我所说的“你”这个词并不是指“你,用户sdkks”,而是测试站点应该在一个新选项卡中打开一个链接”之外的另一个原因,例如,如果开发人员明确要求 webdriver 打开一个新选项卡。
0赞 Frankie 6/24/2020
不能再欣赏你了。你是男人,非常感谢!
24赞 Pedro Lobito 4/22/2018 #3

不幸的是,接受的答案对我不起作用。
要打开一个新选项卡并让 selenium 切换到它,我使用了:

driver.execute_script('''window.open("https://some.site/", "_blank");''')
sleep(1) # you can also try without it, just playing safe
driver.switch_to.window(driver.window_handles[-1]) # last opened tab handle  
# driver.switch_to_window(driver.window_handles[-1]) # for older versions

如果需要切换回主选项卡,请使用:

driver.switch_to.window(driver.window_handles[0])

总结:

包含打开的列表,将其用作选项卡之间的参数。window_handleshandlestabsswitch_to.window()

评论

4赞 winklerrr 9/19/2018
请注意,该名称已弃用并替换为switch_to_window()switch_to.window()
0赞 Dirk 3/11/2020
可能有用的补充 - 要连续循环浏览所有打开的选项卡,请使用import itertools, time; for tab in itertools.cycle(reversed(driver.window_handles)): driver.switch_to.window(tab); time.sleep(2)
0赞 Pedro Lobito 3/24/2020
@winklerrr答案更新了更新的 tks!switch_to.window()
7赞 Talmtikisan 9/25/2019 #4

按 + 或选择假定启动时只打开一个选项卡。ctrltwindow_handles[0]

如果您打开了多个选项卡,那么它可能会变得不可靠。

这是我所做的:

old_tabs=self.driver.window_handles
#Perform action that opens new window here
new_tabs=self.driver.window_handles
for tab in new_tabs:
     if tab in old tabs:
          pass
     else:
          new_tab=tab
driver.switch_to.window(new_tab)

这将在切换到新选项卡之前积极识别它,并将活动窗口设置为所需的新选项卡。

仅仅告诉浏览器发送 + 是行不通的,因为它不会告诉 Web 驱动程序实际切换到新选项卡。ctrltab

-3赞 Gus Bustillos 11/5/2019 #5

找到了一种使用 ahk 库的方法。对于我们这些需要解决这个问题的非程序员来说非常容易。使用的 Python 3.7.3

安装 ahk。pip 安装 ahk

import ahk
from ahk import AHK
import selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


options = webdriver.ChromeOptions()
options.add_experimental_option("excludeSwitches", ['enable-automation']); #to disable infobar about chrome controlled by automation. 
chrome_options.add_argument('--start-maximized') 
chromeDriver = webdriver.Chrome('C:\\new_software\\chromedriver.exe', chrome_options = options) #specify your chromedriver location

chromeDriver.get('https://www.autohotkey.com/')#launch a tab

#launch some other random tabs for testing. 
chromeDriver.execute_script("window.open('https://developers.google.com/edu/python/introduction', 'tab2');")

chromeDriver.execute_script("window.open('https://www.facebook.com/', 'tab3');")

chromeDriver.execute_script("window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open', 'tab4');"`)
seleniumwindow = ahk.active_window #as soon as you open you Selenium session, get a handle of the window frame with AHK. 
seleniumwindow.activate() #will activate whatever tab you have active in the Selenium browser as AHK is activating the window frame 
#To activate specific tabs I would use chromeDriver.switchTo()
#chromeDriver.switch_to_window(chromeDriver.window_handles[-1]) This takes you to the last opened tab in Selenium and chromeDriver.switch_to_window(chromeDriver.window_handles[1])to the second tab, etc.. 

评论

0赞 Coddy 10/22/2022
当它只能在 Selelium 中完成时,为什么你想要另一个依赖项?
-1赞 Gus Bustillos 11/5/2019 #6

这是完整的脚本。

注意:删除下面小 URL 的两行中的空格。Stack Overflow 不允许此处包含微小的链接。

import ahk
import win32clipboard
import traceback
import appJar
import requests
import sys
import urllib
import selenium
import getpass
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import socket
import time 
import urllib.request
from ahk import AHK, Hotkey, ActionChain # You want to play with AHK. 
from appJar import gui

try:                                                                                                                                                                         
    ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")    

except:                                                                                                                                                                         
    ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")    

finally:                                                                                                                                                                         
    pass 

chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--start-maximized')
chrome_options.add_experimental_option("excludeSwitches", ['enable-automation']);
chromeDriver = webdriver.Chrome('C:\\new_software\\chromedriver.exe', chrome_options = chrome_options)

def  ahk_disabledevmodescript():
    try:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")                                                                                                                                                                           
    except:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")                                                                                                                                                                         
    finally:                                                                                                                                                                         
        pass   
    ahk_disabledevmodescriptt= [
    str('WinActivate,ahk_exe chrome.exe'),
    str('Send {esc}'),
    ]
    #Run-Script
    for snipet in  ahk_disabledevmodescriptt:
        ahk.run_script(snipet, blocking=True )
    return 

def launchtabsagain():

    chromeDriver.execute_script("window.open('https://developers.google.com/edu/python/introduction', 'tab2');")
    chromeDriver.execute_script("window.open('https://www.facebook.com/', 'tab3');")
    chromeDriver.execute_script("window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open', 'tab4');")
    chromeDriver.execute_script("window.open('https://www.easyespanol.org/', 'tab5');")
    chromeDriver.execute_script("window.open('https://www.google.com/search?source=hp&ei=EPO2Xf3EMLPc9AO07b2gAw&q=programming+is+not+difficult&oq=programming+is+not+difficult&gs_l=psy-ab.3..0i22i30.3497.22282..22555...9.0..0.219.3981.21j16j1......0....1..gws-wiz.....6..0i362i308i154i357j0j0i131j0i10j33i22i29i30..10001%3A0%2C154.h1w5MmbFx7c&ved=0ahUKEwj9jIyzjb_lAhUzLn0KHbR2DzQQ4dUDCAg&uact=5', 'tab6');")
    chromeDriver.execute_script("window.open('https://www.google.com/search?source=hp&ei=NvO2XdCrIMHg9APduYzQDA&q=dinner+recipes&oq=&gs_l=psy-ab.1.0.0i362i308i154i357l6.0.0..3736...0.0..0.179.179.0j1......0......gws-wiz.....6....10001%3A0%2C154.gsoCDxw8cyU', 'tab7');")

    return  
chromeDriver.get('https://ebc.cybersource.com/ebc2/')
compoanionWindow = ahk.active_window

launchtabs = launchtabsagain()
disabledevexetmessage = ahk_disabledevmodescript()



def copyUrl(): 
    try:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")                                                                                                                                                                           
    except:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")                                                                                                                                                                         
    finally:                                                                                                             

        pass 
    snipet = str('WinActivate,ahk_exe chrome.exe')
    ahk.run_script(snipet, blocking=True )  
    compoanionWindow.activate() 
    ahk_TinyChromeCopyURLScript=[

        str('WinActivate,ahk_exe chrome.exe'),
        str('send ^l'),
        str('sleep 10'),
        str('send ^c'),
        str('BlockInput, MouseMoveoff'),
        str('clipwait'),
    ] 

    #Run-AHK Script
    if ahk:
        for snipet in  ahk_TinyChromeCopyURLScript:
            ahk.run_script(snipet, blocking=True )  
    win32clipboard.OpenClipboard()
    urlToShorten = win32clipboard.GetClipboardData()
    win32clipboard.CloseClipboard()   


    return(urlToShorten)


def tiny_url(url):
    try:

        apiurl = "https: // tinyurl. com / api - create. php? url= " #remove spaces here
        tinyp = requests.Session()
        tinyp.proxies = {"https" : "https://USER:PASSWORD." + "@userproxy.visa.com:443", "http" : "http://USER:PASSWORD." + "@userproxy.visa.com:8080"}
        tinyUrl = tinyp.get(apiurl+url).text
        returnedresponse = tinyp.get(apiurl+url)
        if returnedresponse.status_code == 200: 
            print('Success! response code =' + str(returnedresponse))
        else:
            print('Code returned = ' + str(returnedresponse))
            print('From IP Address =' +IPadd)


    except:
        apiurl = "https: // tinyurl. com / api - create. php? url= " #remove spaces here
        tinyp = requests.Session()
        tinyUrl = tinyp.get(apiurl+url).text
        returnedresponse = tinyp.get(apiurl+url)
        if returnedresponse.status_code == 200: 
            print('Success! response code =' + str(returnedresponse))
            print('From IP Address =' +IPadd)
        else:
            print('Code returned = ' + str(returnedresponse))

    return tinyUrl

def tinyUrlButton():

    longUrl = copyUrl()
    try:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")                                                                                                                                                                           
    except:                                                                                                                                                                         
        ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")                                                                                                                                                                         
    finally:                                                                                                                                                                         
        pass
    try:
        shortUrl = tiny_url(longUrl)
        win32clipboard.OpenClipboard()
        win32clipboard.EmptyClipboard()
        win32clipboard.SetClipboardText(shortUrl)
        win32clipboard.CloseClipboard()
        if ahk:
            try:
                if str(shortUrl) == 'Error':
                    ahk.run_script("Msgbox,262144 ,Done.,"+ shortUrl + "`rPlease make sure there is a link to copy and that the page is fully loaded., 5.5" )
                else:
                    ahk.run_script("Msgbox,262144 ,Done.,"+ shortUrl + " is in your clipboard., 1.5" )
                # ahk.run_script("WinActivate, tinyUrl" )
            except:
                traceback.print_exc()
                print('error during ahk script')

                pass

    except:
        print('Error getting tinyURl')
        traceback.print_exc()

def closeChromeTabs(): 
        try: 
            try:                                                                                                                                                                          
                ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")                                                                                                                                                                          
            except:                                                                                                                                                                          
                ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")                                                                                                                                                                  
            finally:                                                                                                                                                                          
                    pass   
            compoanionWindow.activate()  
            ahk_CloseChromeOtherTabsScript = [ 

                str('WinActivate,ahk_exe chrome.exe'), 
                str('Mouseclick, Right, 30, 25,,1'), 
                str('Send {UP 3} {enter}'), 
                str('BlockInput, MouseMoveOff'), 
                ] 
                #Run-Script 
            if ahk: 
                for snipet in  ahk_CloseChromeOtherTabsScript: 
                        ahk.run_script(snipet, blocking=True ) 
            return(True) 
        except: 
            traceback.print_exc() 
            print("Failed to run closeTabs function.") 
            ahk.run_script('Msgbox,262144,,Failed to run closeTabs function.,2') 
            return(False)         



        # create a GUI and testing this library.

window = gui("tinyUrl and close Tabs test ", "200x160")
window.setFont(9)
window.setBg("blue")
window.removeToolbar(hide=True)
window.addLabel("description", "Testing AHK Library.")
window.addLabel("title", "tinyURL")
window.setLabelBg("title", "blue")
window.setLabelFg("title", "white")
window.addButtons(["T"], tinyUrlButton)
window.addLabel("title1", "Close tabs")
window.setLabelBg("title1", "blue")
window.setLabelFg("title1", "white")
window.addButtons(["C"], closeChromeTabs)
window.addLabel("title2", "Launch tabs")
window.setLabelBg("title2", "blue")
window.setLabelFg("title2", "white")
window.addButtons(["L"], launchtabsagain)
window.go()

if window.exitFullscreen():
    chromeDriver.quit()


def closeTabs():
    try:
        try:                                                                                                                                                                         
            ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU64.exe")                                                                                                                                                                           
        except:                                                                                                                                                                         
            ahk = AHK(executable_path="C:\\Program Files\\AutoHotkey\\AutoHotkeyU32.exe")                                                                                                                                                                         
        finally:                                                                                                                                                                         
                pass   

        compoanionWindow.activate()     
        ahk_CloseChromeOtherTabsScript = [

            str('WinActivate,ahk_exe chrome.exe'),
            str('Mouseclick, Right, 30, 25,,1'),
            str('Send {UP 3} {enter}'),
            str('BlockInput, MouseMoveOff'),
            ]
            #Run-Script
        if ahk:
            for snipet in  ahk_CloseChromeOtherTabsScript:
                    ahk.run_script(snipet, blocking=True )
        return(True)
    except:
        traceback.print_exc()
        print("Failed to run closeTabs function.")
        ahk.run_script('Msgbox,262144,Failed,Failed to run closeTabs function.,2')
        return(False)

评论

0赞 Xammax 12/21/2021
不完全确定为什么这会收到负面评价。
0赞 Ajjax 1/31/2020 #7

用户“aberna”的提示通过以下方式为我工作:

首先,我得到了一个选项卡列表:

  tab_list = driver.window_handles

然后我选择选项卡:

   driver.switch_to.window(test[1])

返回上一个选项卡:

    driver.switch_to.window(test[0])

评论

0赞 Nikolay Hüttenberend 1/31/2020
在你的答案中,变量从何而来?与已接受的答案或一些投票最多的答案有什么区别?test
0赞 Ajjax 2/2/2020
test=tab_list 正如帖子中提到的,解决方案的想法已经在公认的答案中,第 3 项,我只是认为写下来可以帮助像我这样的用户,没有接受过正规 IT 教育的菜鸟。直到现在我才意识到“Avaricious_vulture”在之前的帖子中也解释说......
3赞 Amar Kumar 3/10/2020 #8

如果您只想关闭活动选项卡并需要保持浏览器窗口打开,则可以使用 switch_to.Window 方法,该方法将输入参数为 Window handle-ID。以下示例演示如何实现此自动化:

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get('https://www.google.com')

driver.execute_script("window.open('');")
time.sleep(5)

driver.switch_to.window(driver.window_handles[1])
driver.get("https://facebook.com")
time.sleep(5)

driver.close()
time.sleep(5)

driver.switch_to.window(driver.window_handles[0])
driver.get("https://www.yahoo.com")
time.sleep(5)

#driver.close()
0赞 Ashark 4/13/2022 #9

TLDR:有一种变通解决方案,但有一些限制。

我正在使用已经打开的浏览器,如下所示。问题是每次我启动脚本时,selenium 都会在内部选择一个随机选项卡。官方文档说:

单击在新窗口中打开的链接将聚焦新窗口 或选项卡,但 WebDriver 不会知道哪个窗口 操作系统认为处于活动状态。

这对我来说听起来很奇怪。因为这不是 selenium 处理和自动化浏览器交互的第一个任务吗?更重要的是,切换到任何选项卡实际上切换 gui 中的活动选项卡。似乎这是一个错误。在撰写本文时,python-selenium 版本为 4.1.0。driver.switch_to.window(...)

让我们看看我们可以使用哪些方法。

使用硒window_handles[0]方法

上面答案的方法并不可靠。它并不总是有效。例如,当您在不同的标签页之间切换时,chromium/vivaldi 可能会开始返回的不是当前标签页。

print("Current driver tab:", driver.title)  # <- the random tab title
driver.switch_to.window(chromium_driver.window_handles[0])
print("Current driver tab:", driver.title)  # <-- the currently opened tab title. But not always reliable.

所以跳过这个方法。

使用远程调试方法

与以前的方法相比,没有提供任何额外的 selenium 驱动程序。

通过远程调试协议获取选项卡列表,例如

r = requests.get("http://127.0.0.1:9222/json")
j = r.json()
found_tab = False
for el in j:
    if el["type"] == "page":  # Do this check, because if that is background-page, it represents one of installed extensions
        found_tab = el
        break
if not found_tab:
    print("Could not find tab", file=sys.stderr)

real_opened_tab_handle = "CDwindow-" + found_tab["id"]

driver.switch_to(real_opened_tab_handle)

实际上返回的内容与 中的内容相同。所以也跳过这个方法。driver.window_handles

X11 的解决方法

from wmctrl import Window

all_x11_windows = Window.list()
chromium_windows = [ el for el in all_x11_windows if el.wm_class == 'chromium.Chromium' ]
if len(chromium_windows) != 1:
    print("unexpected numbner of chromium windows")
    exit(1)
real_active_tab_name = chromium_windows[0].wm_name.rstrip(" – Chromium")

chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")

# https://stackoverflow.com/a/70088095/7869636 - Selenium connect to existing browser.
# Need to start chromium as: chromium --remote-debugging-port=9222

driver = webdriver.Chrome(service=Service(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()), options=chrome_options)

tabs = driver.window_handles
found_active_tab = False
for tab in tabs:
    driver.switch_to.window(tab)
    if driver.title != real_active_tab_name:
        continue
    else:
        found_active_tab = True
        break

if not found_active_tab:
    print("Cannot switch to needed tab, something went wrong")
    exit(1)
else:
    print("Successfully switched to opened tab")

print("Working with tab called:", driver.title)

这个想法是从 wmctrl 获取窗口标题,这将让您知道活动的选项卡名称。

Wayland 的变通解决方案

以前的解决方案有一个限制,wmctrl 仅适用于 x11 窗口。

我目前发现了如何获取您单击的窗口的标题。

print("Please click on the browser window")
opened_tab = subprocess.run("qdbus org.kde.KWin /KWin queryWindowInfo | grep caption", shell=True, capture_output=True).stdout.decode("utf-8")
opened_tab_title = opened_tab.rstrip(" - Vivaldi\n").lstrip("caption: ")

然后,可以使用上一个解决方案中的脚本。

该解决方案可以在 wayland 上使用 kwin 窗口列表查询进行改进。如果有人帮助改善这一点,我会很高兴。不幸的是,我目前不知道如何获取 wayland 窗口列表。

0赞 podoshva 8/31/2023 #10

如果您突然需要 Java 解决方案,您可以这样做:

driver.switchTo().window((String) driver.getWindowHandles().toArray()[index of tab]);

评论

0赞 Ben A. 9/4/2023
这并不能提供问题的答案。一旦你有足够的声誉,你就可以对任何帖子发表评论;相反,提供不需要提问者澄清的答案。- 从评论