JavaScript动态渲染页面爬取之Pyppeteer的使用
JavaScript动态渲染的页面不止Ajax一种。例如有些页面的分页部分由JavaScript生成,非原始HTML代码。
为了解决这些问题,我们可以直接模拟浏览器运行,然后爬取数据,这样就可以实现所见即所爬。
Python提供了许多模拟浏览器运行的库,例如Selenium、Splash、Pyppeteer、Playwright等。
一、Pyppeteer的使用
在很多情况下,Ajax请求的接口含有加密参数,例如token、sign等。由于请求Ajax接口时必须加上token参数,因此得深入分析并找到token参数的构造逻辑,难以模拟请求。
因此,模拟浏览器的运行,爬取数据即可解决。
1、Pyppeteer介绍
Pyppeteer依赖Chromium浏览器运行的。如果第一次运行Pyppeteer的时候,没有安装Chromium浏览器,程序会自动帮我们自动安装和配置好,另外,Pyppeteer是基于Python的新特性asnc实现的,所以它的一些操作执行也支持异步方式。
2、安装
pip3 install Pyppeteer
3、快速上手
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
browser = await launch() # 新建一个browser对象。相当于启动浏览器
page = await browser.newPage() #新建一个page对象并赋值给page变量,这相当于在浏览器中新建了一个选项卡,服务器托管但还未访问任何页面。
await page.goto('https://spa2.scrape.center/') # 调用page的goto方法,相当于在浏览器中输入page方法参数中的URL,浏览器加载对应的页面
await page.waitForSelector('.item .name') #调用page的waitForSelector方法,传入选择器,页面就会等待选择器对应的节点信息加载出来后立即返回,否则等待直到超时。
doc = pq(await page.content()) # 页面加载出来后,调用content方法,获取当前浏览器的源代码,这就是JavaScript渲染后的结果
names = [item.text() for item in doc('.item .name').items()] # 使用pyquery解析页面,提取信息
print('Names:',names)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
import asyncio
from pyppeteer import launch
width, height = 1366,768
async def main():
browser = await launch()
page = await browser.newPage()
await page.setViewport({'width':width,'height':height}) # 设置页面窗口的大小
await page.goto('https://spa2.scrape.center/')
await page.waitForSelector('.item .name')
await asyncio.sleep(2)
await page.screenshot(path='example.png') # 保存页面截图
dimensions = await page.evaluate('''() => { # 执行JavaScript语句并返回对应的数据
return {
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
deviceScaleFactor:window.devicePixelRatio,
}
}''')
print(dimensions)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
在screenshot方法里,通过path参数用于传入页面截图的保存路径,另外还可以指定截图的格式type、清晰度quality、是否全屏fullPage和裁切clip等参数。
总之、利用pyppeteer可以控制浏览器执行几乎所有想实现的操作和功能。
4、launch方法
使用pyppeteer的第一步就是启动浏览器。调用launch方法即可。
launch方法的API:
pyppeteer,launcher.launch(options:dic = None,**kwargs) -> pyppeteer.browser.Browser
# 观察源码可以发现,这是一个async修饰的方法,所以在调用的时候要加await
launch方法的参数:
- ignoreHTTPSErrors(bool):是否忽略HTTPS的错误,默认是False。
- headless(bool):是否启用无头模式,即无界面模式。如果devtools参数是True,该参数会被设置为False,否则为True,即默认开启无界面模式。
- executablePath(str):可执行文件的路径。指定该参数之后就不需要使用默认的Chromium浏览器了,可以指定已有的Chrome或Chromium。
- slowMo(int|float):通过传入指定的时间,可以减缓Pyppeteer的一些模拟操作。
- args(List|float):在执行过程中可以传入额外参数。
- ignoreDefaultArgs(bool):是否忽略Pyppeteer的默认参数。如果使用这个参数,那么最好通过args设置一些参数,否则可能会出现一些意想不到的问题。这个参数相对比较危险。
- handleSIGINT(bool):是否响应SIGINT信号,也就是是否可以使用Ctrl+C终止浏览器程序,默认为True。
- handleSIGTERM(bool):是否响应SIGTERM信号(一般是KILL命令),默认是True。
- handleSIGHUP(bool):是否响应SIGHUP信号,即挂起信号,例如终端退出操作,默认是True。
- dumpio(bool):是否将Pyppeteer的输出内容传给process,stdout对象和process,stderr对象,默认是False。
- userDataDir(str):用户数据文件夹,可以保留一些个性化配置和操作记录。
- env(dict):环境变量,可以传入字典形式的数据。
- devtools(bool):是否自动为每一个页面开启调试工具默认是False。如果这个参数设置为True,那么headless参数就会无效,会被强制设置为False。
- logLevel(int|str):日志级别,默认和root logger对象的级别相同。
- autoClose(bool):当一些命令执行完之后,是否自动关闭浏览器,默认是True。
- loop(asyncio.AbstractEventLoop):事件循环对象。
5、无头模式
import asyncio
from pyppeteer import launch
async def main():
await launch(headless=False) # 设为False,启动时就能看见界面了
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
6、调试模式
在写爬虫的时候会经常需要分析网页结构和网络请求,所以开启调试模式是非常有必要的。
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(de服务器托管vtools=True)
page = await browser.newPage()
await page.goto('https://www.baidu.com')
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
刚才说过,如果devtools参数设置为True,无头模式就会关闭,界面始终会显示出来。
7、禁用提示条
可以看到在第5点上有一个提示“Chrome正受到自动测试软件的控制”,用args参数去除。
browser = await launch(headless=False,args=['--disable-infobars'])
8、防止检测
刚刚只是提示关闭了,有些网站还是能检测到Webdriver属性。
Pyppeteer的Page对象有一个叫做evaluateOnNewDocument方法,意思是在每次加载网页的时候执行某条语句,这里可以利用它执行隐藏Webdriver属性的命令:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=False,args=['--disable-infobars'])
page = await browser.newPage()
await page.evaluateOnNewDocument('Object.defineProperty(navigator,"webdriver",{get:() => undefined})')
await page.goto('https://antispider1.scrape.center/')
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
可以看到整个页面成功加载出来了,绕过了对Webdriver属性的检测
9、页面大小的设置
在上述,可以发现页面的显示BUG,整个浏览器的窗口要比显示内容的窗口大,这个情况并非每个页面都会出现。调用Page对象的setViewport方法可以设置窗口大小:
import asyncio
from pyppeteer import launch
width,height = 1366,768
async def main():
browser = await launch(headless=False,args=['--disable-infobars',f'--window-size={width},{height}'])
page = await browser.newPage()
await page.evaluateOnNewDocument('Object.defineProperty(navigator,"webdriver",{get:() => undefined})')
await page.goto('https://antispider1.scrape.center/')
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
10、用户数据持久化
我们看到,每次打开pyppeteer的时候,都是一个新的空白浏览器。如果网页需要登录,那得反复登录!!
设置用户目录即可解决:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=False,userDataDir='./userdata',args=['--disable-infobars'])
page = await browser.newPage()
await page.goto('https://taobao.com')
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
这里将userData属性的值设置为了./userdata,即当前目录的userdata文件夹。关于这个文件夹,具体看https://chromium.googlesource.com/chromium/sec/+/master/docs/user_data_dir.md
以上是launch方法及其对应参数的配置。
11、Browser
我们了解launch方法,它的返回值是一个Browser对象,即浏览器对象,我们通常会赋值给browser变量(其实就是Browser类的一个实例)
Browser类的定义:
class pyppeteer.browser.Browser(connection:pyppeteer.connection.Connection,contextIds:List[str],ignoreHTTPSErrors:bool,setDefaultViewport:bool,process:Optional[subprocess.Popen] = None,closeCallback:Callable[[],Awaitable[None]] = None.**kwargs)
12、开启无痕模式
可以通过createIncognitoBrowserConText方法开启无痕模式:
import asyncio
from pyppeteer import launch
width,height = 1200,768
async def main():
browser = await launch(headless=False,args=['--disable-infobars',f'--window-size={width},{height}'])
context = await browser.createIncognitoBrowserContext()
page = await context.newPage()
await page.setViewport({'width':width,'height':height})
await page.goto('https://www.baidu.com')
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
13、关闭
close方法关闭浏览器:
async def main():
browser = await launch()
page = await browser.newPage()
await page.goto('https://spa2.scrape.center/')
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14、Page
Page即页面,对应一个网页、一个选项卡。
14.1、选择器
Page对象内置了很多用于选取节点的选择器方法,例如J方法,给它传入一个选择器,就能返回匹配到的第一个节点,等价于querySelector方法;又如JJ方法,给它传入选择器,会返回符合选择器的所有节点组成的列表,等价于queySelectorAll方法。
import asyncio
from pyppeteer import launch
async def main():
browser = await launch()
page = await browser.newPage()
await page.goto('https://spa2.scrape.center/')
await page.waitForSelector('.item .name')
j_result1 = await page.J('.item .name')
j_result2 = await page.querySelector('.item .name')
jj_result1 = await page.JJ('.item .name')
jj_result2 = await page.querySelectorAll('.item .name')
print('J Result1:',j_result1)
print('J Result2:',j_result2)
print('JJ Result1:',jj_result1)
print('JJ Result2:',jj_result2)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14.2、选项卡操作
新建选项卡后,先调用pages方法获取所有打开的页面,然后选择一个页面调用其bringToFront方法即可切换页面。
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=False)
page = await browser.newPage()
await page.goto('https://www.baidu.com')
page = await browser.newPage()
await page.goto('https://www.bing.com')
pages = await browser.pages()
print('Pages:',pages)
page1 = pages[1]
await page1.bringToFront()
await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())
这里先启动了Pyppeteer,然后调用了newPage方法新建了两个选项卡,并访问了两个网站。
14.3、页面操作
一定要有对应的方法来控制一个页面的加载、前进、后退、关闭和保存等行为:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=False)
page = await browser.newPage()
await page.goto('https://dynamic1.scrape.cuiqingcai.com/')
await page.goto('https://spa2.scrape.cemter/')
# 后退
await page.goBack()
# 前进
await page.goForward()
# 刷新
await page.reload()
# 保存PDF
await page.pdf()
# 截图
await page.screenshot()
# 设置页面HTML
await page.setContent()
# 设置User-Agent
await page.setUserAgent()
# 设置Headers
await page.setExtraHTTPHeaders(headers={})
# 关闭
await page.close()
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14.4、点击
Pyppeteer同样可以模拟点击,调用click方法即可。
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
browser = await launch()
page = await browser.newPage()
await page.goto('https://spa2.scrape.center/')
await page.waitForSelector('.item .name')
await page.click('.item .name',options={
'button':'right',
'clickCount':1,
'delay':3000,
})
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
- button:鼠标按钮,取值有left、middle、right。
- clickCount:点击次数,取值有left,right,middle。
- delay:延迟点击。
14.5、输入文本
使用type方法可以输入文本
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=False)
page = await browser.newPage()
await page.goto('https://www.taobao.com')
await page.type('#q','iPad')
await asyncio.sleep(10)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14.6、获取信息
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
browser = await launch(headless=False)
page = await browser.newPage()
await page.goto('https://spa2.scrape.center/')
print('HTML:',await page.content())
print('Cookies:',await page.cookies())
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14.7、执行
可以用evaluate执行JavaScript语句
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
width, height = 1366,768
async def main():
browser = await launch()
page = await browser.newPage()
await page.setViewport({'width':width,'height':height})
await page.goto('https://spa2.scrape.center/')
await page.waitForSelector('.item .name')
await asyncio.sleep(2)
await page.screenshot(path='example.png')
dimensions = await page.evaluate('''() => {
return {
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
deviceScaleFactor:window.devicePixelRatio,
}
}''')
print(dimensions)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
14.8、延迟等待
在本节开始的时候,我们演示了waitForSelector的用法,它可以让页面等待某些符合条件的节点加载出来再返回结果。还有其他等待方法:
- waitForFunction:等待某个JavaScript方法执行完毕或返回结果
- waitForNavigation:等待页面跳转,如果没加载出来就报错
- waitForRequest:等待某个特定的请求发出
- waitForResponse:等待某个特定请求对应的响应
- waitFor:通用的等待方法
- waitForXPath:等待符合XPath的节点加载出来。
二、pyppeteer爬取实战
1、爬取目标
电影网站:https://spa2.scrape.center/
2、工作
- 遍历每一页列表页,获取每部电影详情页的URL
- 爬取每部电影的详情页,提取电影的名称、评分、类别、封面、简介等信息。
- 将爬取的数据存储至数据库
3、准备工作
- Python与pyppeteer库
4、爬取列表页
准备工作:
# 准备工作
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s : %(message)s') # 定义日志配置
INDEX_URL = 'https://spa2.scrape.center/page/{page}'
TIEMEOUT = 10
TOTAL_PAGE = 10
WINDOW_WIDTH, WINDOW_HEIGHT = 1366, 768 # 浏览器的宽和高
HEADLESS = False # 指定是否启用无头模式,False代表会弹窗
定义初始化pyppeteer的方法:
# 初始化pyppeteer方法
from pyppeteer import launch
browser, tab = None, None # 声明变量,前者代表浏览器对象,后者代表新建的页面选项卡。
async def init():
global browser, tab # 设置为全局变量,能够在其它方法里调用
browser = await launch(headless=HEADLESS,
args=['--disabled-infobars', f'--window-size={WINDOW_WIDTH},{WINDOW_HEIGHT}']) # args参数指定隐藏提示条和设置浏览器窗口的宽高
tab = await browser.newPage()
await tab.setViewport({'width': WINDOW_WIDTH, 'height': WINDOW_HEIGHT})
定义一个通用的爬取方法:
# 定义一个通用的爬取方法
from pyppeteer.errors import TimeoutError
async def scrape_page(url,selector): # 定义两个参数,url代表要爬取的页面的URL,使用goto方法调用此参数即可访问对应页面;另一个是selector,即等待渲染出的节点对应的CSS选择器。
logging.info('scraping %s',url)
try:
await tab.goto(url)
await tab.waitForSelector(selector,options={
'timeout':TIEMEOUT * 1000
}) # waitForSelector方法等待selector选择器匹配的节点加载出来,通过option指定最长等待时间
except TimeoutError: # 超时则报出异常
logging.error('error occurred while scraping %s',url,exc_info=True)
实现爬取列表页的方法:
# 列表页的爬取
async def scrape_index(page): # 接受参数page,代表要爬取的页面的页码
url = INDEX_URL.format(page=page) # 通过format方法构造出列表页的URL
await scrape_page(url,'.item .name') # 同时传入选择器,.item .name是列表页中电影的名称
在定义一个解析列表页的方法,用来提取每部电影的详情页URL:
# 解析列表页
async def parse_index():
return await tab.querySelectorAllEval('.item .name','nodes => nodes.map(node => node.href)')
# 这里调用了querySelectorAllEval方法,接受两个参数:一是selector,代表选择器;另一个是pageFunction,代表要执行的JavaScript方法。这个方法的作用是找出和选择器匹配的节点,然后根据pageFunction定义的逻辑从这些节点中抽取对应的结果并返回。
# 我们给参数selector传入了电影名称。由于和选择器相匹配的节点有多个,所以给pageFunction参数输入的JavaScript方法就是nodes,其返回值是调用map方法得到node,然后调用node的href属性得到的超链接。这样querSelectorAllEval的返回结果就是当前列表页中的所有电影的详情页的URL组成的列表。
串联调用刚刚实现的方法:
import asyncio
async def main():
await init() # 首先调用init方法
try:
for page in range(1,TOTAL_PAGE + 1): # 遍历所有页码
await scrape_index(page) # 爬取每一个列表页
detail_urls = await parse_index() # 从列表页提取每个URL
logging.info('detail_urls %s',detail_data) # 输出
finally:
await browser.close()
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
5、爬取详情页
定义爬取详情页的方法:
async def scrape_detail(url):
await scrape_page(url,'h2') # 直接调用scrape_page方法,传入详情页url和选择器即可,这里h2代表电影名称。
提取详情页里的信息的方法:
# 提取详情页里面的信息
async def parse_detail():
url = tab.url
name = await tab.querySelectorEval('h2','node => node.innerText')
categories = await tab.querySelectorAllEval('.categories button span','nodes => nodes.map(node => node.innerText)')
cover = await tab.querySelectorEval('.cover','node => node.src')
score = await tab.querySelectorEval('.score','node => node.innerText')
drama = await tab.querySelectorEval('.drama p','node => node.innerText')
return {
'url':url,
'name':name,
'categories':categories,
'cover':cover,
'score':score,
'drama':drama
} # 将提取结果作为一个字典返回
# URL:直接调用tab对象的url属性即可获取当前页面的URL
# 名称:由于名称只涉及一个节点,因此我们调用querySelectorEval方法,第一个参数h2代表根据电影名称提取对应的节点;第二个参数pageFunction,这里调用node的innerText属性,提取了文本值,即电影名称。
# 类别:类别有多个,因此调用querySelectorAllEval方法。其CSS选择器.categories button span,可以选中多个类别节点;第二个参数与上相似。
# 封面:同上
# 分数:同上
# 简介:同上
在main方法里面添加对其的调用即可:
import asyncio
async def main():
await init()
try:
for page in range(1,TOTAL_PAGE + 1):
await scrape_index(page)
detail_urls = await parse_index()
for detail_url in detail_urls:
await scrape_detail(detail_url)
detail_data = await parse_detail()
await save_data(detail_data)
logging.info('detail_urls %s',detail_data)
finally:
await browser.close()
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
6、数据存储
定义一个数据存储的方法,将爬取下来的数据保存为JSON格式:
# 存储数据
import json
from os import makedirs
from os.path import exists
RESULTS_DIR = 'results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
async def save_data(data):
name = data.get('name')
data_path = f'{RESULTS_DIR}/{name}.json'
json.dump(data,open(data_path,'a',encoding='utf-8'),ensure_ascii=False,indent=2)
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
知识点总结: 1.通过模块来寻找漏洞 2.m服务器托管sf查找漏洞 3.通过网站源代码,查看模块信息 环境准备 攻击机:kali2023 靶机:pWnOS v2.0 安装地址:pWnOS: 2.0 (Pre-Release) ~ VulnHub 在安装网址中看…