整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

新手学Python精选:Python如何爬取动态网页内容

析动态内容

根据权威机构发布的全球互联网可访问性审计报告,全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的,这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容,也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案,一是JavaScript逆向工程;另一种是渲染JavaScript获得渲染后的内容。

JavaScript逆向工程

下面我们以“360图片”网站为例,说明什么是JavaScript逆向工程。其实所谓的JavaScript逆向工程就是找到通过Ajax技术动态获取数据的接口。

但是当我们在浏览器中通过右键菜单“显示网页源代码”的时候,居然惊奇的发现页面的HTML代码中连一个<img>标签都没有,那么我们看到的图片是怎么显示出来的呢?原来所有的图片都是通过JavaScript动态加载的,而在浏览器的“开发人员工具”的“网络”中可以找到获取这些图片数据的网络API接口,

那么结论就很简单了,只要我们找到了这些网络API接口,那么就能通过这些接口获取到数据,当然实际开发的时候可能还要对这些接口的参数以及接口返回的数据进行分析,了解每个参数的意义以及返回的JSON数据的格式,这样才能在我们的爬虫中使用这些数据。

关于如何从网络API中获取JSON格式的数据并提取出我们需要的内容,在之前的《文件和异常》一文中已经讲解过了,这里不再进行赘述。

使用Selenium

尽管很多网站对自己的网络API接口进行了保护,增加了获取数据的难度,但是只要经过足够的努力,绝大多数还是可以被逆向工程的,但是在实际开发中,我们可以通过浏览器渲染引擎来避免这些繁琐的工作,WebKit就是一个利用的渲染引擎。

WebKit的代码始于1998年的KHTML项目,当时它是Konqueror浏览器的渲染引擎。2001年,苹果公司从这个项目的代码中衍生出了WebKit并应用于Safari浏览器,早期的Chrome浏览器也使用了该内核。在Python中,我们可以通过Qt框架获得WebKit引擎并使用它来渲染页面获得动态内容,关于这个内容请大家自行阅读《爬虫技术:动态页面抓取超级指南》一文。

如果没有打算用上面所说的方式来渲染页面并获得动态内容,其实还有一种替代方案就是使用自动化测试工具Selenium,它提供了浏览器自动化的API接口,这样就可以通过操控浏览器来获取动态内容。首先可以使用pip来安装Selenium。

pip3 install selenium

下面以“阿里V任务”的“直播服务”为例,来演示如何使用Selenium获取到动态内容并抓取主播图片。

import requests
from bs4 import BeautifulSoup
def main():
 resp = requests.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
 soup = BeautifulSoup(resp.text, 'lxml')
 for img_tag in soup.select('img[src]'):
 print(img_tag.attrs['src'])
if __name__ == '__main__':
 main()

运行上面的程序会发现没有任何的输出,因为页面的HTML代码上根本找不到<img>标签。接下来我们使用Selenium来获取到页面上的动态内容,再提取主播图片。

from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
def main():
 driver = webdriver.Chrome()
 driver.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
 soup = BeautifulSoup(driver.page_source, 'lxml')
 for img_tag in soup.body.select('img[src]'):
 print(img_tag.attrs['src'])
if __name__ == '__main__':
 main()

在上面的程序中,我们通过Selenium实现对Chrome浏览器的操控,如果要操控其他的浏览器,可以创对应的浏览器对象,例如Firefox、IE等。运行上面的程序,如果看到如下所示的错误提示,那是说明我们还没有将Chrome浏览器的驱动添加到PATH环境变量中,也没有在程序中指定Chrome浏览器驱动所在的位置。

selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

为了解决上面的问题,可以到Selenium的官方网站找到浏览器驱动的下载链接并下载需要的驱动,在Linux或macOS系统下可以通过下面的命令来设置PATH环境变量,Windows下配置环境变量也非常简单,不清楚的可以自行了解。

export PATH=$PATH:/Users/Hao/Downloads/Tools/chromedriver/

其中/Users/Hao/Downloads/Tools/chromedriver/就是chromedriver所在的路径。

用 Windows 自带的 MSAA 发现浏览器窗口,自动执行 JavaScript 很简单!

aardio 代码示例:

import winex;
import winex.accObject;
import winex.key;

//遍历浏览器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") { 
	
	//获取 MSAA 接口对象
	var accObject = winex.accObject.fromWindow(hwnd);	
	
	//查找文本框
	var edit = accObject.find(
		role = 0x2A;
		name = "<Address and search bar>|<地址和搜索栏>";
	)
	
	if(edit){ 
		//获取浏览器地址栏内容
		var url = edit.value();
		
		//修改浏览器地址栏内容
		edit.setValue("javascript:alert(document.location.href)")
		edit.takeFocus();

		//后台发送按键消息
		winex.key.click(hwnd,"ENTER");
		thread.delay(1000);
	} 
}

运行一下看看效果:

所有 Windows 系统都自带 MSAA,接口简单,易于使用,生成的 EXE 程序体积也会很小。aardio 标准库 winex.accObject 则对 MSAA 做了进一步封装,用法就更简单了。

窗口基础知识

1、什么是窗口

「窗口」是应用程序在屏幕上创建的一个显示区域,通常用于接收并处理用户操作,并显示要输出的内容。窗口上的文本框、按钮、菜单这些也都是窗口。

我们一般将顶层独立窗口称为「窗体」,而窗体上的子窗口称为「控件」。

2、什么是窗口句柄

窗口句柄是一个用于唯一标准窗口的整数值。

其实很多系统资源,例如位图、进程、线程都有唯一标准资源的句柄。

在 aardio 中所有句柄都存为指针类型,唯有窗口句柄是普通的数值类型。

3、无句柄窗口

无句柄窗口是指该窗口上的控件没有创建子窗口,典型的例如网页上的按钮、文本框都没有窗口句柄。MSAA 可用于操作无句柄窗口。

使用窗口探测器

请在 aardio 中打开 『工具 > 探测器 > 窗口探测器』:

拖动『窗口探测器』左下角的十字图标到目标窗口上,就会显示窗口信息。

使用窗口探测器我们可以发现 Chrome, Edge 等浏览器的网页窗口类名都是 "Chrome_WidgetWin_1", 所以我们可以用下面的 aadio 代码获取所有打开的浏览器窗口:

import winex;

for hwnd,title in winex.each("Chrome_WidgetWin_1") { 
	
	
}

aardio 中 winex 名字空间的所有库、函数都是用于操作外部程序窗口的。

winex.each() 用于遍历所有符合条件的窗口, winex.each() 的第一个参数可以指定窗口类名,这个类名支持模式匹配语法( 类正则表达式,但更简单,用法请参考语法文档 )。

自窗口句柄获取 MSAA 对象

自窗口句柄获取 MSAA 对象,代码很简单:

import winex;
import winex.accObject;

//遍历浏览器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") { 
	
	//获取 MSAA 接口对象
	var accObject = winex.accObject.fromWindow(hwnd);	

}

使用 MSAA 探测工具

请在 aardio 中打开 winex.accObject 的文档或源码,搜索“ACC对象浏览工具” 并下载该工具( inspect.exe )。

运行 inspect.exe ,点选下图的『 Watch Cursor 』图标:

也就是允许探测鼠标指向的窗口。

然后将鼠标移向浏览器的地址栏,Inspect 找到了地址栏所在的 ACC 对象,并显示了一堆信息,我们重点关注这几行:

Name:	"Address and search bar"
Role:	editable text (0x2A)

Name 是 ACC 对象的名称。
Role 是 ACC 对象的角色,其实就是控件类型。

根据上面的信息,我们修改代码获取浏览器地址栏:

import winex;
import winex.accObject;
import console;

//遍历浏览器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") { 
	
	//获取 MSAA 接口对象
	var accObject = winex.accObject.fromWindow(hwnd);	
	
	//查找地址栏
	var edit = accObject.find(
		role = 0x2A;
		name = "<Address and search bar>|<地址和搜索栏>";
	)
	
	//显示地址栏的内容
	if(edit) console.log( edit.value() )

}

console.pause();

在 aardio 中运行上面的代码,我们干净利索地拿到了浏览器地址栏的网址。

拿到一个 accObject 对象以后,可以调用 accObject.find() 函数继续查找符合条件的子节点。而查找参数就是我们用 Inspect.exe 探测到的参数。

查找参数中,role, state 可以是文本,也可以是数值,一般建议用数值( 速度更快 )。

上面的 name 参数用到了模式匹配:

name = "<Address and search bar>|<地址和搜索栏>";

这个模式表达式中的 | 线是 “或” 的意思,而 < > 括号用于包含子串。如果目标 ACC 对象的 name 包含 "Address and search bar" 或者 "地址和搜索栏" 都符合条件。

后台按键

aardio 标准库 key,mouse 用于对前台窗口模拟按键鼠标。

例如:

key.press("ENTER")

作用就是模拟按下回车键。

如果我们改用 winex.key, winex.mouse 就可以直接向后台窗口发送按键或鼠标消息。这样的好处是不会干扰用户操作。

例如向浏览器窗口发送回车键消息:

import winex;
import winex.key;

//遍历浏览器窗口(兼容 Chrome,Edge 等)
for hwnd,title in winex.each("Chrome_WidgetWin_1") { 

	//后台发送按键消息
	winex.key.click(hwnd,"ENTER");
	thread.delay(1000);
}

键名

操作按键的函数都需要用到键名。

我们还可以直接运行 aardio 『 工具 > 鼠标按键 > 按键指令生成器』

在『按键指令生成器』窗口内我们任意按键,就可以显示对应的键名了。

听说 UIA 更先进

有时候先进也是一种负担,飞机比自行车先进,这不等于任何时候都要用飞机替代自行车。

当然,在 aardio 中调用 UIA 也是很简单的,示例( 可独立运行 ):

import process;
process.executeWaitInput("notepad.exe",io.getSpecial(0x25/*_CSIDL_SYSTEM*/,"drivers\etc\HOSTS"));
 
//导入 .Net 类
import System.Windows.Automation;
TreeScope = ::UIAutomationTypes.import("System.Windows.Automation.TreeScope"); 

//访问 .Net 类的静态成员
Automation = System.Windows.Automation;
AutomationElement = Automation.AutomationElement;
RootElement = AutomationElement.RootElement;

//查找记事本窗口
var condNotepadClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"Notepad")
var notepad = RootElement.FindFirst( TreeScope.Children, condNotepadClass) 

//查找记事本的编辑框
var condEditClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"Edit");
var editBox = notepad.FindFirst( TreeScope.Descendants, condEditClass); 

if(!editBox){
	//Windows 11
	condEditClass = Automation.PropertyCondition(AutomationElement.ClassNameProperty,"RichEditD2DPT");
	editBox = notepad.FindFirst( TreeScope.Descendants, condEditClass); 	
}

//获取记事本内的文本
var textPattern =  editBox.GetCurrentPattern(Automation.TextPattern.Pattern);
var text = textPattern.DocumentRange.GetText(50)

import win.dlg.message;
win.dlg.message().info(text + " ……")

然并卵,WebDriver 不香么?

aardio 调用 WebDriver 就更简单了,示例:

我们看一下通过Python Selenium WebDriver执行JavaScript语句的几种不同方法。

在本教程中,让我们分析Selenium WebDriver中使用最少但功能最强大的功能。是的,我将讨论JavaScript执行器,并向您展示通过Python Selenium WebDriver执行JavaScript语句的几种不同方法。

可能会发生这种情况,在某些实时项目中,Selenium WebDriver无法对特定的Web元素执行操作。例如,由于WebDriver模拟最终用户交互,因此很自然地会拒绝单击最终用户看不到的元素(有时即使Web元素在页面上可见,也会发生这种情况)。可能有其他几个类似的原因或情况。

在这些情况下,我们可以依靠JavaScript来单击或对该Web元素执行操作,并且可以通过WebDriver执行这些JavaScript语句。

您可以使用JavaScript执行WebElement界面所做的所有操作。

什么是JavaScript?

JavaScript是一种脚本语言,它在客户端运行,即在浏览器上运行,并且当您浏览网页时会做一些神奇的事情。有关更多详细信息,请在DZone上搜索关键字“JavaScript”。

我们如何在WebDriver中使用JavaScript?

Python Selenium WebDriver提供了一个内置方法:

driver.execute_script("some javascript code here");

我们可以通过两种方式在浏览器中执行JavaScript。

方法1:在文档根级别执行JavaScript

在这种情况下,我们使用JavaScript提供的方法捕获我们想要使用的元素,然后在其上声明一些操作并使用WebDriver执行此JavaScript。

例:

javaScript = "document.getElementsByName('username')[0].click();"

driver.execute_script(javaScript)

我们在这里做什么?

第1步:我们正在使用JavaScript检查并通过属性“名称”获取元素。(另外,可以使用'id'和'class'属性。)

第2步:使用JavaScript声明并对元素执行单击操作。

第3步:调用execute_script()方法并将我们创建的JavaScript作为字符串值传递。

请注意 上面[0] 的 getElementsByName('username')[0] 声明。JavaScript函数 getElementsByName , getElementsByClassName 等返回所有匹配的元件的阵列。在我们的例子中,我们需要对可以通过的第一个匹配元素进行操作 index [0] 。如果您知道自己在做什么,即,如果您知道要操作的元素的索引,则可以直接使用索引,例如 getElementsByName('username')[2] 。

但是,如果您使用的是JavaScript函数' getElementById ',则不需要使用任何索引,因为它只返回一个元素('id'应该是唯一的)。

执行时,WebDriver会将JavaScript语句注入浏览器,脚本将执行该任务。在我们的示例中,它对目标元素执行单击操作。此JavaScript具有自己的命名空间,不会干扰实际网页中的JavaScript。

方法2:在元素级别执行JavaScript

在这种情况下,我们使用WebDriver捕获我们想要使用的元素,然后使用JavaScript在其上声明一些操作,并通过将web元素作为参数传递给JavaScript来使用WebDriver执行此JavaScript。

这令人困惑吗?让我们分解吧。

例如:

userName = driver.find_element_by_xpath("//button[@name='username']")

driver.execute_script("arguments[0].click();", userName)

我们在这里做什么?

第1步:使用WebDriver提供的方法检查和捕获元素,例如'find_element_by_xpath ':

userName = driver.find_element_by_xpath("//button[@name='username']")

第2步:使用JavaScript声明并对元素执行单击操作:

arguments[0].click()

第3步:execute_script() 使用我们创建的JavaScript语句作为字符串值调用 方法,并使用WebDriver作为参数捕获Web元素:

driver.execute_script("arguments[0].click();", userName)

上面两行代码可以缩短为下面的格式,我们使用WebDriver找到一个元素,声明一些JavaScript函数,并使用WebDriver执行JavaScript。

driver.execute_script("arguments[0].click();",

driver.find_element_by_xpath("//button[@name='username']"))

更频繁面临的另一个问题是需要滚动到网页的底部。您可以在一行代码中执行此操作:

driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

此外,您的语句中可以有多个JavaScript操作。

例如:

userName = driver.find_element_by_xpath("//button[@name='username']")

password = driver.find_element_by_xpath("//button[@name='password']")

driver.execute_script("arguments[0].click();arguments[1].click();", userName, password)

在这种情况下,web元素的顺序的使用很重要。访问 index 与 [0] 一个JavaScript语句中的任何位置将检索传递的第一个网页元素。

driver.execute_script("arguments[1].click();arguments[0].click();", userName, password)

如何返回值

JavaScript执行程序的另一个重要方面是它可用于从Web元素中获取值。这意味着该 execute_script() 方法可以返回值。

例如:

print driver.execute_script('return document.getElementById("fsr").innerText')

请注意,如果您想要JavaScript代码返回的内容,则需要使用return。此外,可以使用Selenium定位元素并将其传递到脚本中。

什么元素找不到会发生什么?

当JavaScript找不到要操作的元素时,它会抛出带有相应错误消息的WebDriver异常。

场景1:我们正在尝试使用' print driver.execute_script('return document.getElementById("fsr").innerText') ' 来读取属性, 但网页中没有这样的元素。我们在异常跟踪中收到以下消息:

selenium.common.exceptions.WebDriverException: Message: unknown error: Cannot read property 'innerText' of null

场景2:我们试图在JavaScript中使用无效的操作或错误函数名称,例如' print driver.execute_script('document.getElementById("fsr").clic();') '。(注意click() 方法名称中的拼写错误 。)

selenium.common.exceptions.WebDriverException: Message: unknown error: document.getElementById(...).clic is not a function

摘要

以下是可以使用JavaScript的一些潜在操作的摘要。

  • 获取元素文本或属性
  • 找到一个元素
  • 对元素做一些操作,比如 click()
  • 更改元素的属性
  • 滚动到网页上的元素或位置
  • 等到页面加载完毕

使用Selenium处理DOM时,JavaScript的基本知识有很大帮助。