Pypputeer

Puppeteer 是 Google 基于 Node.js 开发的一个工具,而 Pyppeteer 又是什么呢?它实际上是 Puppeteer 的 Python 版本的实现,但它不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。

在 Pyppetter 中,实际上它背后也是有一个类似 Chrome 浏览器的 Chromium 浏览器在执行一些动作进行网页渲染,首先说下 Chrome 浏览器和 Chromium 浏览器的渊源。

总的来说,两款浏览器的内核是一样的,实现方式也是一样的,可以认为是开发版和正式版的区别,功能上基本是没有太大区别的。

环境安装

pip install pyppeteer

注意: 支持异步需要3.5以上的解释器

import pyppeteer
print(pyppeteer.__chromium_revision__) # 查看版本号
print(pyppeteer.executablePath()) # 查看 Chromium 存放路径 # pyppeteer-install 帮助你去安装谷歌

如果无法启动,需要手动改文件路径

'linux': 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/575458/chrome-linux.zip'

'mac': 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/575458/chrome-mac.zip'

'win32': https://registry.npmmirror.com/binary.html?path=chromium-browser-snapshots/

官方网站:https://miyakogi.github.io/pyppeteer/reference.html

常见API: http://events.jianshu.io/p/0fcf8bdf74a9

常见问题处理: https://www.jianshu.com/p/f1a8fb7037d7

1 测试样例

from pyppeteer import launch
import asyncio
import time
async def main():
# 启动一个浏览器
browser = await launch(headless=False,args=['--disable-infobars','--window-size=1920,1080'])
# 创建一个页面
page = await browser.newPage()
# 跳转到百度
await page.goto("http://www.baidu.com/")
# 输入要查询的关键字,type 第一个参数是元素的selector,第二个是要输入的关键字
await page.type('#kw','pyppeteer')
# 点击提交按钮 click 通过selector点击指定的元素
await page.click('#su')
time.sleep(3)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())

接下来看看它的参数简介:

  • ignoreHTTPSErrors (bool):是否要忽略 HTTPS 的错误,默认是 False。

  • headless (bool):是否启用 Headless 模式,即无界面模式,如果 devtools 这个参数是 True 的话,那么该参数就会被设置为 False,否则为 True,即默认是开启无界面模式的。

  • executablePath (str):可执行文件的路径,如果指定之后就不需要使用默认的 Chromium 了,可以指定为已有的 Chrome 或 Chromium。

  • slowMo (int|float):通过传入指定的时间,可以减缓 Pyppeteer 的一些模拟操作。

  • args (List[str]):在执行过程中可以传入的额外参数。

  • 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):事件循环对象。

2 基本配置

2.0 基本参数

params={
# 关闭无头浏览器
"headless": False,
'dumpio':'True', # 防止浏览器卡住
r'userDataDir':'./cache-data', # 用户文件地址
"args": [
'--disable-infobars', # 关闭自动化提示框
'--window-size=1920,1080', # 窗口大小
'--log-level=30', # 日志保存等级, 建议设置越小越好,要不然生成的日志占用的空间会很大 30为warning级别
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
'--no-sandbox', # 关闭沙盒模式
'--start-maximized', # 窗口最大化模式
'--proxy-server=http://localhost:1080' # 代理
],
}

2.1 设置窗口

# UI模式  频闭警告
browser = await launch(headless=False, args=['--disable-infobars'])
page = await browser.newPage()
await page.setViewport({'width': 1200, 'height': 800})

2.2 添加头部

await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")

2.2 网页截图

page.screenshot(path='example.png')

2.3 伪装浏览器 绕过检测

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

# 伪装
await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
'{ webdriver:{ get: () => false } })}') await page.goto('https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')

2.4 案例演示 触发JS

async def run():
browser = await launch()
page = await browser.newPage()
await page.setViewport({'width': 1200, 'height': 800})
await page.goto('https://www.zhipin.com/job_detail/?query=%E8%85%BE%E8%AE%AF%E7%88%AC%E8%99%AB&city=101020100&industry=&position=')
dimensions = await page.evaluate('''() => {
       return {
           cookie: window.document.cookie,
       }
   }''')
print(dimensions,type(dimensions)) asyncio.get_event_loop().run_until_complete(run())

2.5 boss直聘cookie反爬绕过实践

import asyncio,requests
from pyppeteer import launch async def run():
browser = await launch()
page = await browser.newPage()
await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")
await page.setViewport(viewport={'width': 1536, 'height': 768})
await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
'{ webdriver:{ get: () => false } }) }')
await page.goto('https://www.zhipin.com/job_detail/?query=%E8%85%BE%E8%AE%AF%E7%88%AC%E8%99%AB&city=101020100&industry=&position=')
dimensions = await page.evaluate('''() => {
       return {
           cookie: window.document.cookie,
       }
   }''')
headets = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36',
'cookie': dimensions.get('cookie')
} res = requests.get(
'https://www.zhipin.com/job_detail/?query=%E8%85%BE%E8%AE%AF%E7%88%AC%E8%99%AB&city=101020100&industry=&position=',
headers=headets)
print(res.text)

:boss主要是cookie token反爬还有IP监测

  1. 6滚动到页面底部
await page.evaluate('window.scrollBy(0, document.body.scrollHeight)')

3常用函数

常见API:http://events.jianshu.io/p/0fcf8bdf74a9

  • page.goto(url) 请求url
  • page.waitfor(time) 设置页面等待时间,单位是毫秒,常用语设置操作间隔,让page能加载完成指定目标
  • page.waitForSelector(selector) / page.waitForXPath(xpath) 等待目标元素加载完成,默认timeout是30秒,可以辅助确定指定位置元素是否已经加载完成。
  • page.J(css selector)/page.querySelector(css selector) 通过css selector定位元素,前面是缩写函数
  • page.Jx(xpath)/page.xpath(xpath) 通过xpath定位元素,前面是缩写函数

    通过xpath方法定位后用getProperty("textContent").jsonValue()方法获取具体的值,但是很麻烦,还是推荐用原生XPATH进行对源码解析

    或者通过CSS选择器和js语法进行获取,详情见 https://blog.csdn.net/weixin_55399173/article/details/121300589

    commodity_name = await (await (await commodity.xpath("./a/@href"))[0].getProperty("textContent")).jsonValue()
  • page.content() 获取页面当前加载网页的document,用法
  • page.cookies() 获取页面当前的cookies,不是字典形式 而是字符串类型 name=key value=value
  • page.evaluate(jsstr) 执行js,js代码用字符串书写,注意引号的使用
  • page.evaluateOnNewDocument(jsstr) 在页面新打开一个document时才生效,上面的函数是当前document生效

    另外关于js的函数几种写法,箭头函数可以理解为匿名函数 () => {document.body.clientHeight}; 等于 function () {return document.body.clientWidth}

    上述2个执行js的方法 可以直接返回js的返回值 所以也可以这么写比如bro_height = await page.evaluate('document.body.clientHeight') 可以直接返回页面高度
  • page.frames 返回的是iframes列表,可以遍历元素的__ditc__, 里面的_url,_navigationURL,_name属性可以用来判断 也可以遍历iframes列表,对元素进行xpath判断
frames =  page.frames
for i in frames:
ret = await i.xpath(".//input[@id='phoneIpt']")
print(ret)
if ret:
await ret[0].type("aaaaa")
  • page.hover(selector) 指针移动到selector定位的元素位置
  • page.screenshot() 页面截屏
  • page.setCacheEnabled() 是否启用缓存,默认是True
  • page.setJavaScriptEnabled() 是否允许加载js,默认是True
  • page.setRequestInterception() 是否允许请求和返回注入,默认是False
  • page.setUserAgent() 设置UA
  • page.setViewport() 设置窗口大小 page.setViewport({'width':xx,'height':xx})
  • page.type(selector,str) 在指定位置中输入字符串

3 进阶使用

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://careers.tencent.com/search.html?keyword=python')
# 调用选折器
await page.waitForXPath('//div[@class="recruit-wrap recruit-margin"]/div')
# 获取网页源代码
doc = pq(await page.content())
# 提取数据
title = [item.text() for item in doc('.recruit-title').items()]
print('title:', title)
# 关闭浏览器
await browser.close()
# 启动异步方法
asyncio.get_event_loop().run_until_complete(main())

4 数据提取

# 在页面内执行 document.querySelector。如果没有元素匹配指定选择器,返回值是 None
J = querySelector
# 在页面内执行 document.querySelector,然后把匹配到的元素作为第一个参数传给 pageFunction
Jeval = querySelectorEval
# 在页面内执行 document.querySelectorAll。如果没有元素匹配指定选择器,返回值是 []
JJ = querySelectorAll
# 在页面内执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction
JJeval = querySelectorAllEval
# XPath表达式
Jx = xpath # Pyppeteer 三种解析方式
Page.querySelector() # 选择器 css 选择器
Page.querySelectorAll()
Page.xpath() # xpath 表达式
# 简写方式为:
Page.J(), Page.JJ(), and Page.Jx()

5 获取属性

提取目标地址:https://pic.netbian.com/4kmeinv/index.html 所有的图片资源

async def mains1():
browser = await launch(headless=False, args=['--disable-infobars'])
page = await browser.newPage()
await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")
await page.setViewport(viewport={'width': 1536, 'height': 768})
await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
'{ webdriver:{ get: () => false } }) }')
await page.goto('https://pic.netbian.com/4kmeinv/index.html')
elements = await page.querySelectorAll(".clearfix li a img")
for item in elements:
# 获取连接
title_link = await (await item.getProperty('src')).jsonValue()
print(title_link)
await browser.close()
asyncio.get_event_loop().run_until_complete(mains1())

6 登录案例

import asyncio
from pyppeteer import launch async def mains2():
browser = await launch({'headless': False, 'args': ['--disable-infobars', '--window-size=1920,1080']})
page = await browser.newPage()
await page.setViewport({'width': 1920, 'height': 1080})
await page.goto('https://www.captainbi.com/amz_login.html')
await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
'{ webdriver:{ get: () => false } }) }')
await page.type('#username', '13555553333') # 账号
await page.type('#password', '123456') # 密码
await asyncio.sleep(2)
await page.click('#submit',{'timeout': 3000})
import time
# await browser.close()
print('登录成功')
asyncio.get_event_loop().run_until_complete(mains2())

7 综合案例

# encoding: utf-8
"""
@site: https://www.tulingxueyuan.cn/
@file: 唯品会.py
"""
import requests
from lxml import etree
from loguru import logger
import pandas as pd
from utils import ua
import asyncio
from pyppeteer import launch class Wph(object): def __init__(self,url,name):
self.url = url self.name = name self.headers = {
'user-agent': ua.get_random_useragent()
} self.session = requests.session() self.hadlnone = lambda x:x[0] if x else '' async def main(self,url):
global browser
browser = await launch()
page = await browser.newPage()
await page.goto(url)
text = await page.content() # 返回页面html
return text def spider(self): df = pd.DataFrame(columns=['品牌', '标题', '原价', '现价', '折扣'])
# 发起HTTP请求
# https://category.vip.com/suggest.php?keyword=%E5%8F%A3%E7%BA%A2&brand_sn=10000359
res = self.session.get(self.url,params={'keyword':self.name},headers = self.headers,verify=False) html = etree.HTML(res.text) url_list = html.xpath('//div[@class="c-filter-group-content"]/div[contains(@class,"c-filter-group-scroll-brand")]/ul/li/a/@href') # 迭代品牌URL地址
for i in url_list: ua.wait_some_time()
# 驱动浏览器 请求
page_html = asyncio.get_event_loop().run_until_complete(self.main('http:' + i))
# 获取网页源代码 page = etree.HTML(page_html) htmls = page.xpath('//section[@id="J_searchCatList"]/div')
# 迭代商品URL列表
for h in htmls[1:]:
# 评判
pingpai = self.hadlnone(h.xpath('//div[contains(@class,"c-breadcrumbs-cell-title")]/span/text()'))
# 标题
title = self.hadlnone(h.xpath('.//div[contains(@class,"c-goods-item__name")]/text()'))
# 价格 原价
y_price = self.hadlnone(h.xpath('.//div[contains(@class,"c-goods-item__market-price")]/text()'))
# 卖价
x_price = self.hadlnone(h.xpath('.//div[contains(@class,"c-goods-item__sale-price")]/text()'))
# 折扣
zk = self.hadlnone(h.xpath('.//div[contains(@class,"c-goods-item__discount")]/text()')) logger.info(f'品牌{pingpai},标题{title},原价{y_price},现价{x_price},折扣{zk}')
# 构造字典
pro = {
'品牌':pingpai,
'标题':title,
'原价':y_price,
'现价':x_price,
'折扣':zk
}
df = df.append([pro]) df.to_excel('唯品会数据2.xlsx',index=False) return df def __del__(self):
browser.close() if __name__ == '__main__':
url = 'https://category.vip.com/suggest.php'
name = '香水'
w = Wph(url,name)
w.spider()

唯品会实例2.

from pyppeteer import launch
import asyncio
from faker import Faker
from lxml import etree async def start():
bro = await launch({"executablePath": "D:\sofrware\chromium\chrome.exe", "headless": False,
"userDataDir": r"C:\Users\admin\Desktop\get_response\user_data",
}, args=["--start-maximized", "--disable-infobars"])
page_list = await bro.pages()
page = page_list[0] # bro_width = await page.evaluate('function () {return (window.screen.width) * (window.devicePixelRatio)}')
bro_width = await page.evaluate('window.devicePixelRatio')
bro_height = await page.evaluate('() => {return (window.screen.height) * (window.devicePixelRatio)}')
await page.evaluate('() =>{ Object.defineProperties(navigator,'
'{ webdriver:{ get: () => false } })}')
await page.setUserAgent(ua)
await page.setViewport(viewport={'width': int(bro_width), 'height': int(bro_height)})
await page.goto(
"https://list.vip.com/autolist.html?rule_id=52403837&title=%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91&refer_url=https%3A%2F%2Fcategory.vip.com%2Fhome",
{"waitUntil": "load"})
flag = 1
while flag:
web_total_height = await page.evaluate("function () {return document.body.clientHeight}")
print("当前页面滚轮总高度为%s" % web_total_height)
web_scroll_height = await page.evaluate("function () {return document.documentElement.scrollTop}")
print("当前页面滚轮高度为%s" % web_scroll_height)
if web_total_height - web_scroll_height > 900:
await page.evaluate("function () {document.documentElement.scrollTop=%s}" % web_total_height)
else:
print("到底了")
flag = 0
break await asyncio.sleep(1)
print("跳出循环")
html = await page.content() # commodity_lists = await page.xpath("//div[@id='J_wrap_pro_add']//div")
# for commodity in commodity_lists:
# name = await commodity.xpath(".//div[contains(@class,'c-goods-item__name--two-line')]/text()")
# price = await commodity.xpath(".//div[@class='c-goods-item__sale-price J-goods-item__sale-price']/span/text()")
# if name:
# commodity_name = await (await name[0].getProperty('textContent')).jsonValue()
# else:
# # commodity_name = await (await (await commodity.xpath("./a/@href"))[0].getProperty("textContent")).jsonValue()
# commodity_name = await commodity.xpath("./a/@href")
# print(commodity_name) commodity_xpath = etree.HTML(html)
commodity_lists = commodity_xpath.xpath("//div[@id='J_wrap_pro_add']/div")
for commodity in commodity_lists:
name = commodity.xpath(".//div[contains(@class,'c-goods-item__name')]/text()")
price = commodity.xpath(".//div[contains(@class,'c-goods-item__sale-price')]/text()") print(name, price)
await asyncio.sleep(60)
await bro.close() fake = Faker(locale='zh_CN')
ua = fake.chrome()
loop = asyncio.get_event_loop().run_until_complete(start())

pypeeter 自动化的更多相关文章

  1. 构建一个基本的前端自动化开发环境 —— 基于 Gulp 的前端集成解决方案(四)

    通过前面几节的准备工作,对于 npm / node / gulp 应该已经有了基本的认识,本节主要介绍如何构建一个基本的前端自动化开发环境. 下面将逐步构建一个可以自动编译 sass 文件.压缩 ja ...

  2. 细说前端自动化打包工具--webpack

    背景 记得2004年的时候,互联网开发就是做网页,那时也没有前端和后端的区分,有时一个网站就是一些纯静态的html,通过链接组织在一起.用过Dreamweaver的都知道,做网页就像用word编辑文档 ...

  3. python自动化测试(2)-自动化基本技术原理

    python自动化测试(2) 自动化基本技术原理 1   概述 在之前的文章里面提到过:做自动化的首要本领就是要会 透过现象看本质 ,落实到实际的IT工作中就是 透过界面看数据. 掌握上面的这样的本领 ...

  4. Appium移动自动化框架

    引言:Appium 是一个移动端自动化测试开源工具,可以针对不同的平台用一套API来编写测试用例.本文对Appium自动化测试框架的功能进行了概括. 本文选自<软件自动化测试开发>. Ap ...

  5. 前端自动化构建工具gulp记录

    一.安装 1)安装nodejs 通过nodejs的npm安装gulp,插件也可以通过npm安装.windows系统是个.msi工具,只要一直下一步即可,软件会自动在写入环境变量中,这样就能在cmd命令 ...

  6. CYQ.Data 从入门到放弃ORM系列:开篇:自动化框架编程思维

    前言: 随着CYQ.Data 开始回归免费使用之后,发现用户的情绪越来越激动,为了保持这持续的激动性,让我有了开源的念头. 同时,由于框架经过这5-6年来的不断演进,以前发的早期教程已经太落后了,包括 ...

  7. CYQ.Data V5 分布式自动化缓存设计介绍

    前方: 其实完成这个功能之前,我就在思考:是先把想法写了来,和大伙讨论讨论后再实现,还是实现后再写文论述自己的思维. 忽然脑后传来一个声音说:你发文后会进入发呆阶段. 所以还是静下心,让我轻轻地把代码 ...

  8. 在CentOS上构建.net自动化编译环境

             我们知道在Windows上我们很容易构建于MSBuild的自动化编译环境,那么在CentOS也是可以的,主要是需要Mono. 在这儿我们选择Jenkins+Gitlab+Mono在C ...

  9. 感悟 GNU C 以及将 Vim 打造成 C/C++ 的半自动化 IDE

    C 语言在 Linux 系统中的重要性自然是无与伦比.不可替代,所以我写 Linux 江湖系列不可能不提 C 语言.C 语言是我的启蒙语言,感谢 C 语言带领我进入了程序世界.虽然现在不靠它吃饭,但是 ...

  10. 应用Grunt自动化地优化你的项目前端

    在不久前我曾写了一篇 应用r.js来优化你的前端 的文章,为大家介绍了r.js这个实用工具,它可以很好地压缩.合并前端文件并打包整个项目.但是如果将r.js放到项目中,我们不得不顾及到一个问题——项目 ...

随机推荐

  1. ffmpeg拉取rtsp视频流

    公司项目需要提供实时显示网络摄像头实时视频. void RTSPFFmpeg::rtsp_open(const char *url) { AVFormatContext* format_ctx = a ...

  2. 11月30日内容总结——前端简介、http协议概念、html协议概念及基础知识和部分标签的讲解

    目录 一.前端与后端的概念 什么是前端开发? 什么是后端? 学习前端的目的 前端三剑客 二.前端前戏 三.HTTP协议 1.四大特性 2.报文格式 3.响应状态码 四.HTML概览 1.HTML简介 ...

  3. 《Terraform 101 从入门到实践》 第二章 Providers插件管理

    <Terraform 101 从入门到实践>这本小册在南瓜慢说官方网站和GitHub两个地方同步更新,书中的示例代码也是放在GitHub上,方便大家参考查看. 不怕出身低,行行出状元. 插 ...

  4. 易语言 CS1.6单机开源

    一个绘制 一个修改器 垃圾玩意 https://kxd.lanzoul.com/iJiwf07ve61a https://kxd.lanzoul.com/iTT4n07w61tg

  5. BUUCTF-[强网杯2019]随便注

    强网杯2019随便注 它说随便注,它可不是随便注入的哈 首先测试闭合环境,因为有回显,所以很快即知道了是一个单引号闭合 接下来常规操作,得到列数大概为2 1';select 2; 返回了过滤信息 于是 ...

  6. 物语(monogatari)

    \(Description\) 某一天,少年邂逅了同病相连的IA.见面后,IA一把牵起少年的手,决定和他一起逃离部落,离开这个无法容身的是非之地. 要逃离部落,少年和IA就需要先选择一条耗时最少的路线 ...

  7. CF1250C Trip to Saint Petersburg

    题目传送门 思路 线段树入门题. 不妨固定一个右端点 \(r\),把所有右端点小于 \(r\) 的区间都在 \(1\) 至此区间的左端点处 update 一个 \(p\),然后每次都给区间 \(1\) ...

  8. vue2和vue3区别

    1. vue2和vue3双向数据绑定原理发生了改变 vue2的双向数据绑定是利用了es5 的一个API Object.definepropert() 对数据进行劫持 结合发布订阅模式来实现的.vue3 ...

  9. GPIO 和轮询控制 LED 的状态

    GPIO 概念 I/O 是输入(Input)和输出(Output)的意思,GPIO(General Purpose I/O)是基本输入输出,是 I/O 的最基本形式.STM32F103ZET6 大概有 ...

  10. 通过反射机制简化 JDBC ResultSet 实体类的注入

    提出问题 查询完某个表之后,一般都是把结果的每一个字段注入到一个实体类中.比如,数据库 users 表,查询出来的结果注入到 User 实体类中. 通过 while 遍历 ResultSet,把字段对 ...