BUG
soup = BeautifulSoup(rep,"lxml")
报错bs4.FeatureNotFound: Couldn’t find a tree builder with the features you requested: html_parser.
解决方案:安装lxml
第一步是建立BeautifulSoup对象,这个对象在bs4模块中。在建立对象的时候可以额外指定一个参数,作为实际的HTML解析器。解析器的值可以指定html.parser,这是内置的HTML解析器。更好的选择是使用lxml解析器,纯C语言实现,速度会更快。如果使用lxml需要安装lxml包,不然会报上面的错误。
selenium和requests爬虫
- Selenium,实际是一个自动化测试工具,现在已经被广泛用于网络爬虫中来应对 JavaScript 渲染的页面的抓取,但是需要安装浏览器驱动,通过浏览器驱动来定位网页元素
- requests,python的第三方包,可以发起post、get、put、delete等请求,可以用于自动化接口测试,但是使用requests的get方法只能获取静态网页,一些使用js代码渲染的页面无法抓取
Pyppeteer
鉴于上述两种方法的缺点,采用pyppeteer进行抓包,是 Puppeteer 的 Python 版本的实现,由一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。 在 Pyppetter 中,实际上它背后也是有一个类似 Chrome 浏览器的 Chromium 浏览器在执行一些动作进行网页渲染。Chromium 是谷歌为了研发 Chrome 而启动的项目,是完全开源的。二者基于相同的源代码构建,Chrome 所有的新功能都会先在 Chromium 上实现,待验证稳定后才会移植,因此 Chromium 的版本更新频率更高,也会包含很多新的功能,但作为一款独立的浏览器,Chromium 的用户群体要小众得多。两款浏览器 “同根同源”,它们有着同样的 Logo,但配色不同,Chrome 由蓝红绿黄四种颜色组成,而 Chromium 由不同深度的蓝色构成。参考链接
from pyppeteer import launch
def crawl(url):
browser = await launch(options={'args': ['--no-sandbox']}) # 创建浏览器对象
page = await browser.newPage() #新建标签页
await page.goto(url,{'timeout': 1000*60}) #根据url获取页面相应内容
rep = await page.content() # 爬取整个网页数据
print(rep) #打印出来的就是一个HTML
await browser.close()
Beautiful Soup4
使用Beautiful Soup4定位网页元素,方便我们获取想要的信息,后面简称bs4。
主要用到bs4的两个函数,一个find会找到出现的首个满足条件的元素;findAll找到所有满足条件的元素。在实际应用中可以灵活运用。这两个函数可以只传一个参数如soup.find(‘title’),即找到rep中的title标签。但是对于一些标签有多个,比如ul有两个的情况下,可以荣归class加以区分,即soup.find(‘ul’,class_=“list1”)即可定位到第一个ul。
- 对于具有唯一性且没有嵌套的标签,如title标签,可以直接通过
soup.title.string
直接获取文本“Testing” - 对于有嵌套的标签,如h2,可以通过
soup.h2.get_text()
获取文本内容“I am embbed"- 这里标签名是否唯一只会影响我们是否取到想要的信息,但是程序是可以正常运行输出的
- 如果标签名唯一就输出文本内容,如果不唯一,就输出第一个h2标签的文本内容
- 标签名不唯一文本内容获取,以li标签举例
- 在rep中,虽然li有多个,但是我们可以根据ul缩小范围,
soup.find('ul',class_="list1").findAll('li')
这样我们可以得到list1下的两个li标签。想进一步获取特定li标签的内容,如果有可以唯一标识的,就在find参数里加上唯一属性;如果没有,也可以按照在列表中的相对位置,通过索引获取文本内容。
- 在rep中,虽然li有多个,但是我们可以根据ul缩小范围,
from bs4 import BeautifulSoup
rep = '''<title>Testing</title>
<body>
<h2>I am embbed
<tr>...</tr>
</h2>
<ul class='list1'>
<li>列表1第1项</li>
<li>列表1第2项</li>
</ul>
<p class='first'>文字1</p>
<p class='second'>文字2</p>
<ul class='list2'>
<li>列表2第1项</li>
<li>列表2第2项</li>
</ul>
</body>'''
def bs4_use(url):
soup = BeautifulSoup(rep, "lxml") #创建一个beautiful soup对象,html解释器选择lxml,req为爬取的网页信息
#soup = BeautifulSoup(open("index.html"), "lxml") #打开本地html文件创建bs4对象
print(soup.title)
print(soup.find('title').get_text())
以支付宝SDK最新版本定位举例
查看元素标签、位置
首先我们通过检查,先查看需要定位内容在HTML中的标签属性,相对位置,可以看到
- 版本号是一个h3标签,这个id虽然是唯一的,但是如果SDK更新,那最新版本的id一定会发生改变,所以打消用id直接定位。
- 版本号下面跟着三个p标签,p标签的内容有更新日期,以及提升的内容,这里有两个一个是大体的描述那方便改进了,一个描述的是具体的更新信息
- 这里需要注意一下,是不是每个更新的版本信息里都会有两种描述,后面发现有的版本没有大致描述,只有一个详细描述,也就是在h3标签下只有两个p标签,所以后面取数据的时候需要注意。
无法ID定位时查看网页整体布局
上面分析,根据ID定位元素代码复用性几乎是0,所以我们需要看网页的整体布局,下图中左边是源码,右边是页面布局,可以看到不同版本其实是依次排序的,即一个h3后面2-3个p标签为一个版本,后面又是一个h3标签接2-3个p标签。。依次直到所有的版本列举完。这些不同的版本中有着异同点,可以根据异同点来定位。
- 相同点:每个版本的第一个p标签是更新日期,且标签的文本内容一定包含“更新日期:”这五个字符,注意冒号是中文冒号
- 不同点:每个版本号不同;更新信息描述不同,15.8.08更新信息只有详细信息
实现
安装包:bs4、pyppeteer
版本号:因为最新版本一定是在最上面,所以我们找到的第一个h3标签就是最新版本的版本号。(这里的前提是前面没有出现h3标签)
其他信息
- 都是在p标签,所以我们可以直接找到所有的p标签,保存在列表中。遍历所有p标签,出现的第一个包含“更新日期”的p标签即最新版本的更新日期时间,停止遍历,同时记录一下索引index
- 由于我们不确定后面的更新信息是只有一个还有两个,所以我们可以找到第二个版本的更新日期,第二个版本的更新日期的前一个p标签一定是更新的详细信息
- 从index+1开始遍历,最多遍历3个p标签,一定可以到第二新版本的更新日期,记录第二新版本更新日期的p标签索引i,那么最新版本的更新信息在列表中的索引即为i-1
- 至此既可获取最新版本号、更新日期、更新详细信息。
- 在获取更新日期时,可以使用字符串切片,使用中文冒号作为分隔符,即可获取2022-04-21
import asyncio
from pyppeteer import launch
from bs4 import BeautifulSoup
async def main(url):
browser = await launch(options={'args': ['--no-sandbox']}) # 使用pyppeteer静态/动态网页都可成功爬取
page = await browser.newPage()
await page.goto(url,{'timeout': 1000*60})
await asyncio.sleep(15)
rep = await page.content() # 爬取到整个网页数据,接下来可用 正则/BeautifulSoup 就可以抓取数据
soup = BeautifulSoup(rep, "lxml")
latest_version = soup.h3.get_text()
other_msg = soup.findAll('p')
#find the latest update date
for index,item in enumerate(other_msg):
if '更新日期' in item.get_text():
date = item.get_text().split(':')[-1]
break
#find the second update date and locate the uopdated meaasge of the latest version
for i in range(index+1,index+4):
if '更新日期' in other_msg[i].get_text():
break
update_msg = other_msg[i-1].get_text()
print(latest_version)
print(date)
print(update_msg)
await browser.close()
if __name__ == '__main__':
url = 'https://opendocs.alipay.com/open/02no45' # 爬取的网址
asyncio.get_event_loop().run_until_complete(main(url))