见过的最全的网络爬虫干货总结!

·  阅读 41156

这可能是你见过的最全的网络爬虫干货总结!

昨天的时候我参加了掘金组织的一场 Python 网络爬虫主题的分享活动,主要以直播的形式分享了我从事网络爬虫相关研究以来的一些经验总结,整个直播从昨天下午 1 点一直持续到下午 5 点,整整四个小时。

整个分享分为三个阶段,第一阶段先介绍了自己从大学以来从事编程开发以来的相关历程,第二阶段是正式的网络爬虫分享流程,详细总结了网络爬虫开发的一些要点,第三阶段是解答一些提问,并抽奖送出一些礼品。所以在这里我会对我昨天分享的主要内容做下总结,另外还会附上视频回放、PPT,另外还会为大家送上一些福利,希望大家可以支持!

总括

整个分享的主题叫做《健壮高效的网络爬虫》,本次分享从抓取、解析、存储、反爬、加速五个方面介绍了利用 Python 进行网络爬虫开发的相关知识点和技巧,介绍了不同场景下如何采取不同措施高效地进行数据抓取的方法,包括 Web 抓取、App 抓取、数据存储、代理选购、验证码破解、分布式抓取及管理、智能解析等多方面的内容,另外还结合了不同场景介绍了常用的一些工具包,全部内容是我在从事网络爬虫研究过程以来的经验精华总结。

爬取

对于爬取来说,我们需要学会使用不同的方法来应对不同情景下的数据抓取任务。

爬取的目标绝大多数情况下要么是网页,要么是 App,所以这里就分为这两个大类别来进行了介绍。

对于网页来说,我又将其划分为了两种类别,即服务端渲染和客户端渲染,对于 App 来说,我又针对接口的形式进行了四种类别的划分——普通接口、加密参数接口、加密内容接口、非常规协议接口。

所以整个大纲是这样子的:

  • 网页爬取
    • 服务端渲染
    • 客户端渲染
  • App 爬取
    • 普通接口
    • 加密参数接口
    • 加密内容接口
    • 非常规协议接口

爬取 / 网页爬取

服务端渲染的意思就是页面的结果是由服务器渲染后返回的,有效信息包含在请求的 HTML 页面里面,比如猫眼电影这个站点。客户端渲染的意思就是页面的主要内容由 JavaScript 渲染而成,真实的数据是通过 Ajax 接口等形式获取的,比如淘宝、微博手机版等等站点。

服务端渲染的情况就比较简单了,用一些基本的 HTTP 请求库就可以实现爬取,如 urllib、urllib3、pycurl、hyper、requests、grab 等框架,其中应用最多的可能就是 requests 了。

对于客户端渲染,这里我又划分了四个处理方法:

  • 寻找 Ajax 接口,此种情形可以直接使用 Chrome/Firefox 的开发者工具直接查看 Ajax 具体的请求方式、参数等内容,然后用 HTTP 请求库模拟即可,另外还可以通过设置代理抓包来查看接口,如 Fiddler/Charles。
  • 模拟浏览器执行,此种情形适用于网页接口和逻辑较为复杂的情况,可以直接以可见即可爬的方式进行爬取,如可以使用 Selenium、Splinter、Spynner、pyppeteer、PhantomJS、Splash、requests-html 等来实现。
  • 直接提取 JavaScript 数据,此种情形适用于真实数据没有经过 Ajax 接口获取,而是直接包含在 HTML 结果的某个变量中,直接使用正则表达式将其提取即可。
  • 模拟执行 JavaScript,某些情况下直接模拟浏览器执行效率会偏低,如果我们把 JavaScript 的某些执行和加密逻辑摸清楚了,可以直接执行相关的 JavaScript 来完成逻辑处理和接口请求,比如使用 Selenium、PyExecJS、PyV8、js2py 等库来完成即可。

爬取 / App 爬取

对于 App 的爬取,这里分了四个处理情况:

  • 对于普通无加密接口,这种直接抓包拿到接口的具体请求形式就好了,可用的抓包工具有 Charles、Fiddler、mitmproxy。
  • 对于加密参数的接口,一种方法可以实时处理,例如 Fiddler、mitmdump、Xposed 等,另一种方法是将加密逻辑破解,直接模拟构造即可,可能需要一些反编译的技巧。
  • 对于加密内容的接口,即接口返回结果完全看不懂是什么东西,可以使用可见即可爬的工具 Appium,也可以使用 Xposed 来 hook 获取渲染结果,也可以通过反编译和改写手机底层来实现破解。
  • 对于非常规协议,可以使用 Wireshark 来抓取所有协议的包,或者使用 Tcpdump 来进行 TCP 数据包截获。

以上便是爬取流程的相关分类和对应的处理方法。

解析

对于解析来说,对于 HTML 类型的页面来说,常用的解析方法其实无非那么几种,正则、XPath、CSS Selector,另外对于某些接口,常见的可能就是 JSON、XML 类型,使用对应的库进行处理即可。

这些规则和解析方法其实写起来是很繁琐的,如果我们要爬上万个网站,如果每个网站都去写对应的规则,那么不就太累了吗?所以智能解析便是一个需求。

智能解析意思就是说,如果能提供一个页面,算法可以自动来提取页面的标题、正文、日期等内容,同时把无用的信息给刨除,例如上图,这是 Safari 中自带的阅读模式自动解析的结果。

对于智能解析,下面分为四个方法进行了划分:

  • readability 算法,这个算法定义了不同区块的不同标注集合,通过权重计算来得到最可能的区块位置。
  • 疏密度判断,计算单位个数区块内的平均文本内容长度,根据疏密程度来大致区分。
  • Scrapyly 自学习,是 Scrapy 开发的组件,指定⻚页⾯面和提取结果样例例,其可⾃自学习提取规则,提取其他同类⻚页⾯面。
  • 深度学习,使⽤用深度学习来对解析位置进⾏行行有监督学习,需要⼤大量量标注数据。

如果能够容忍一定的错误率,可以使用智能解析来大大节省时间。

目前这部分内容我也还在探索中,准确率有待继续提高。

存储

存储,即选用合适的存储媒介来存储爬取到的结果,这里还是分为四种存储方式来进行介绍。

  • 文件,如 JSON、CSV、TXT、图⽚、视频、⾳频等,常用的一些库有 csv、xlwt、json、pandas、pickle、python-docx 等。
  • 数据库,分为关系型数据库、非关系型数据库,如 MySQL、MongoDB、HBase 等,常用的库有 pymysql、pymssql、redis-py、pymongo、py2neo、thrift。
  • 搜索引擎,如 Solr、ElasticSearch 等,便于检索和实现⽂本匹配,常用的库有 elasticsearch、pysolr 等。
  • 云存储,某些媒体文件可以存到如七⽜牛云、又拍云、阿里云、腾讯云、Amazon S3 等,常用的库有 qiniu、upyun、boto、azure-storage、google-cloud-storage 等。

这部分的关键在于和实际业务相结合,看看选用哪种方式更可以应对业务需求。

反爬

反爬这部分是个重点,爬虫现在已经越来越难了,非常多的网站已经添加了各种反爬措施,在这里可以分为非浏览器检测、封 IP、验证码、封账号、字体反爬等。

下面主要从封 IP、验证码、封账号三个方面来阐述反爬的处理手段。

反爬 / 封 IP

对于封 IP 的情况,可以分为几种情况来处理:

  • 首先寻找手机站点、App 站点,如果存在此类站点,反爬会相对较弱。
  • 使用代理,如抓取免费代理、购买付费代理、使用 Tor 代理、Socks 代理等。
  • 在代理的基础上维护自己的代理池,防止代理浪费,保证实时可用。
  • 搭建 ADSL 拨号代理,稳定高效。

反爬 / 验证码

验证码分为非常多种,如普通图形验证码、算术题验证码、滑动验证码、点触验证码、手机验证码、扫二维码等。

  • 对于普通图形验证码,如果非常规整且没有变形或干扰,可以使用 OCR 识别,也可以使用机器学习、深度学习来进行模型训练,当然打码平台是最方便的方式。
  • 对于算术题验证码,推荐直接使用打码平台。
  • 对于滑动验证码,可以使用破解算法,也可以模拟滑动。后者的关键在于缺口的找寻,可以使用图片比对,也可以写基本的图形识别算法,也可以对接打码平台,也可以使用深度学习训练识别接口。
  • 对于点触验证码,推荐使用打码平台。
  • 对于手机验证码,可以使用验证码分发平台,也可以购买专门的收码设备,也可以人工验证。
  • 对于扫二维码,可以人工扫码,也可以对接打码平台。

反爬 / 封账号

某些网站需要登录才能爬取,但是一个账号登录之后请求过于频繁会被封号,为了避免封号,可以采取如下措施:

  • 寻找手机站点或 App 站点,此种类别通常是接口形式,校验较弱。
  • 寻找无登录接口,尽可能寻找⽆无需登录即可爬取的接口。
  • 维护 Cookies 池,使⽤用批量账号模拟登录,使⽤时随机挑选可用 Cookies 使⽤即可,实现:https://github.com/Python3WebSpider/CookiesPool。

加速

当爬取的数据量非常大时,如何高效快速地进行数据抓取是关键。

常见的措施有多线程、多进程、异步、分布式、细节优化等。

加速 / 多线程、多进程

爬虫是网络请求密集型任务,所以使用多进程和多线程可以大大提高抓取效率,如使用 threading、multiprocessing 等。

加速 / 异步

将爬取过程改成非阻塞形式,当有响应式再进行处理,否则在等待时间内可以运行其他任务,如使用 asyncio、aiohttp、Tornado、Twisted、gevent、grequests、pyppeteer、pyspider、Scrapy 等。

加速 / 分布式

分布式的关键在于共享爬取队列,可以使用 celery、huey、rq、rabbitmq、kafka 等来实现任务队列的对接,也可以使用现成的框架 pyspider、Scrapy-Redis、Scrapy-Cluster 等。

加速 / 优化

可以采取某些优化措施来实现爬取的加速,如:

  • DNS 缓存
  • 使用更快的解析方法
  • 使用更高效的去重方法
  • 模块分离化管控

加速 / 架构

如果搭建了分布式,要实现高效的爬取和管理调度、监控等操作,我们可以使用两种架构来维护我们的爬虫项目。

  • 将 Scrapy 项目打包为 Docker 镜像,使用 K8S 控制调度过程。
  • 将 Scrapy 项目部署到 Scrapyd,使用专用的管理工具如 SpiderKeeper、Gerapy 等管理。

以上便是我分享的全部内容,所有的内容几乎都展开说了,一共讲了一个半小时。

上面的文字版的总结可能比较简略,非常建议大家如有时间的话观看原版视频分享,里面还能看到我本人的真面目哦,现在已经上传到了 Bilibili,链接为:www.bilibili.com/video/av343…,欢迎大家观看学习和收藏~

另外对于这部分内容,其实还有我制作的更丰富的思维导图,预览图如下:

点击图片,获取爬虫思维导图高清图片

另外还有原演讲稿我也一并分享给大家,都放在我的公众号了。

获取方式如下:

  • 回复”爬虫讲稿“,获取本次分享的 PPT 内容。

暗网系列之:利用Python + OnionScan 打造自己的安全威胁情报平台(一)

2017-08-02 16:40

 

MottoIN暗网系列专题的目标,是让大家更加全面、更深入的了解暗网,这当中自然涉及安全威胁情报分析平台的话题。

OnionScan 是一个用来在暗网中寻找潜在的泄露数据的利器,最新版本中还加入了一些炫酷的可视化效果,尽管项目的管理团队并没有公布实现深度扫描的技术细节,也没有说明那些看起来非常酷的可视化效果是如何生成的。

不过,没关系,接下来,就让我们一起尝试使用 Python + OnionScan完成安全威胁情报平台的搭建,探讨如何在暗网中进行深度探索、寻找感兴趣的信息、分析有用的情报,顺便也提高一下数据可视化的技能。

通常让我们感到恐怖的,不是看不到的东西,而是看到了却不明白的东西”,深入暗网深处,看看能找到那些可怕的/宝贵的信息。

第一部分(Part  1)

流程简介

初始工作的简单流程,如下所述:

  1. 拥有一台可以7*24小时运行的服务器,扫描工作需要花费大量的时间(自己有服务器的话最好,练手的话也可以使用云服务器);
  2. 在服务器上运行Tor 软件;
  3. 安装 OnionScan;
  4. 编写Python脚本,处理扫描工作,并将扫描结果进行数据管理;
  5. Python进阶,优化数据分析结果的可视化展示效果

搭建合适的服务器及运行环境

第一步,创建服务器环境。推荐使用Ubuntu16.4(下文中也将以此作为演示环境)。根据自己的主机环境,选择合适的连接到服务器的方式。

  • windows系统:Putty;
  • Mac OS X或Linux系统:直接使用ssh命令(sshroot@IPADDRESS)

第二步,配置服务器的环境。连接到Ubuntu服务器上,运行如下命令:

apt-get update

apt-get install tor git bison libexif-dev

apt-get install python-pip

pip install stem

第三步:安装Go语言的运行环境。OnionScan是用Go语言编写的,参考Ryan Frankel 《http://www.hostingadvice.com/how-to/install-golang-on-ubuntu/》 的教程,在服务器上执行下列命令:

bash<<(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)

[[ -s “$HOME/.gvm/scripts/gvm” ]] && source “$HOME/.gvm/scripts/gvm”

source /root/.gvm/scripts/gvm

gvm install go1.8–binary

gvm use go1.8

也可以通过离线下载的方式进行安装:https://golang.org/dl/

第四步,安装OnionScan。命令如下:

go get github.com/s-rah/onionscan

go install github.com/s-rah/onionscan

完成上述步骤后,在终端输入:

onionscan

如果返回“onionscan”的命令行使用信息,恭喜你,onionscan已经安装成功了;

关闭终端后,如果发现不能运行onionscan的话,可以在终端输入下列命令解决该问题:

gvm use go1.8

第五步,修改Tor的配置

为了便于我们编写的Python脚本可以请求一个新的标识(一个新的IP地址),在这里需要对Tor的配置做一些小小的改动。后边在扫描的过程中如果遇到障碍,会用到这部分的内容。

在终端运行如下命令:

tor —hash-passwordYourPasswd

可以根据自己的喜好,设置“YourPasswd”的内容。这时你会接收到一串输出,把它们复制下来,然后在终端执行下来命令:

vim -w /etc/tor/torrc

此时会打开 torrc文件的编辑视图,跳转到文本底部,将之前复制的那串字符粘贴过来,效果类似下图所示:

 

完成之后,保存退出,在终端输入下来命令用于重启Tor服务:

service tor restart

至此,服务器环境搭建工作基本上就已经完成了。(不算太难,不是吗?)

在正式开始编写Python代码之前,还需要添加感兴趣的.onion地址清单,这样才能在暗网中顺利的执行扫描任务。例如:

wget https://raw.githubusercontent.com/automatingosint/osint_public/master/onionrunner/onion_master_list.txt

Tips:使用SSH连接到远程服务器后,执行一些扫描任务比较耗时,这种情况下,可以使用 Screen 命令,创建多个虚拟终端,在虚拟终端里执行命令,断开之前先把 Screen 挂起,重新启用时可以直接连接到之前的Screen上。

编写Python脚本,封装OnionScan

OnionScan是一款伟大的工具,为了能够更好的利用它为我们服务,需要对其进行封装,何况Tor的连接是出了名的不稳定,我们需要能够杀掉一个卡住的扫描进程,也需要能够从Tor网络中抓取新的IP地址。所以,是事后运用你的Python编码能力了。打开一个新的Python文件,并将其命名为onionrunner.py(完整代码参见文章末尾的更多资料部分)

 

第1-12行:载入脚本中所需的模块;

第14-15行:初始化两空链表,分别保存我们完整的 onion列表和正在进行的会话的onion扫描列表。

第17-18行:利用一个事件对象协调两个将被执行的线程。必须首先设置 Event对象,以便默认情况下主线程可以执行。。

接下来还需要构建一些辅助函数,这些函数将处理 main onions列表,并确保能够继续将新发现的onions添加到列表:

 

第23行:定义get_onion_list函数,主要用于加载我们的扫描清单。

第26-33行:一个判断,如果onion_master_list.txt文件存在(26),那么打开它,并祖杭阅读,将结果添加到stored_onions(30);如果文件不存在,则输出一条错误消息(32)并退出脚本(33)。

第35-37行:简单地输出加载的onions的总数(35),Return。

第41行:定义store_onion函数,它接受一个参数(onion),这是我们希望添加到master列表中的隐藏服务(onion网址)。

第45-46行:打开onion_master_list.txt列表文件(45),然后写入隐藏服务的onion地址(46)。

现在我们将实施与运行onionscan二进制做实际的扫描工作处理功能。保持你的编辑器添加代码:

接下来,将处理与运行onionscan二进制文件执行扫描任务相关的代码。

 

更多源代码的分析,感兴趣的话,可以自行阅读。

终端下,运行一下殒命,就可以开始扫描工作了:

python onionrunner.py

此时,你会看到如下所示的输出:

# python onionrunner.py

[*] Total onions for scanning: 7182

[*] Running 0 of 7182.

[*] Onionscanningnfokjthabqzfndmj.onion

[*] Running 1 of 7182.

[*] Onionscanning gmts3xxfrbfxdm3a.onion

检查 onionscan_results 目录,你会看到新增的一些JSON 文件,这些文件一扫描的隐藏服务的地址命名。让它继续运行吧,如果你真的想收集这些目标的信息的话。当然了,如果你使用了本文中所列的Onions列表,那么可以选择直接下载扫描结果:

https://github.com/automatingosint/osint_public/tree/master/onionrunner

利用Shodan搜索引擎扩大暗网 OSNIT的能力

Shodan 搜索引擎可以说是安全专业人员的最常用工具之一,它的搜索能力极其强大,所以,我们需要想办法把它集成到我们的威胁情报分析平台中。幸运的是,Shodan提供了Python接口,这便于我们利用其API进行一些低水平的交互。

首先,安装Shodan。

Windows: pip install shodan

Mac/Linux: sudo pip install shodan

一般情况下,人们不会将同一台服务器同时托管在“公共网络”和“暗网”中,但是也不能排除这种可能。接下来,我们就以检索暗网中的SSH密钥信息为例,进行测试,看看能否碰撞出一些奇妙的结果。

依照咱们的惯例,依然需要编写一个Python脚本来完成此项工作。这一次我们创建一个名为 “sshkeys.py”的脚本(下载链接参见文末更多资料部分):

sshkeys.py 脚本的代码如下图所示:

 

第3行:加载了shodan 模块;

第19-20行:如果扫描结果中包含一个 SSH 密钥,打印出一个友好的提示;

第22-25行:如果 key_to_hosts字典里已经包含了该SSH 密钥(22),添加当前隐藏的服务并存储在密钥列表里(23)。这里之所以使用一个密钥列表,是因为存在同一个SSH 密钥关联多个隐藏服务的情况,这确实很有趣。如果key_to_hosts 字典中没有该SSH 密钥,那么用一个新的列表初始化字典,并添加当前的隐藏服务(25);

第27-35行:当上边的循环代码遍历完所有的JSON文件,我们需要针对每个密钥做一个快速的分析,判断它是否被多个隐藏服务共享。

第37-50行:利用 Shodan 的API,检查密钥的指纹信息(公共网络中的IP地址)。

你可以使用下列命令,直接运行该脚本:

# python sshkeys.py

至此,你可以去检索一下这些公共网络中的IP地址与暗网中隐藏服务的潜在联系了。当然你也可以使用现成的威胁情报分析工具来自动化完成分析工作。

结论

关于暗网中情报的收集和分析,有很多有趣的工作可以去做。

本文只是一个开始,我们希望读者能更多的从信息安全和隐私保护的角度看待暗网,走进暗网,利用智能的情报分析,帮助我们更多地保护客户。

更多资料

http://www.hostingadvice.com/how-to/install-golang-on-ubuntu/

https://raw.githubusercontent.com/automatingosint/osint_public/master/onionrunner/onionrunner.py

https://github.com/automatingosint/osint_public/blob/master/onionrunner/analysis/sshkeys.py

https://www.hunch.ly

前端 onionscan + tor 实现暗网爬取

trainer-co · 2019年12月11日 · 22 次阅读

1、安装相关依赖

123456
yum update
yum install git bison libexif-devyum install toryum install epel-release
yum install python-pippip install stem

2、 安装 go 语言环境

12345678910111213141516
wget https://dl.google.com/go/go1.12.6.linux-amd64.tar.gz# 解压tar -C /usr/local -xzf go1.12.6.linux-amd64.tar.gz# 创建go项目路径mkdir -p /root/gopath# 添加环境变量vi /etc/profile# 添加内容GOROOT=/usr/local/goGOPATH=/root/gopath
export GOROOT
export GOPATH
export PATH=$PATH:$GOPATH/bin:$GOROOT/bin# 使环境变量生效
source /etc/profile

 

3、安装 onionscan

12345678910
# 由于golang.org被墙,不能直接下载文件,所以需要从镜像仓库手动下载相应的依赖文件
mkdir -p /root/gopath/src/golang.org/x

cd /root/gopath/src/golang.org/x/git clone https://github.com/golang/net.git git clone https://github.com/golang/crypto.gitgit clone https://github.com/golang/sys.git# 安装onionscan
go get github.com/s-rah/onionscan
go install github.com/s-rah/onionscan

在命令行输入onionscan返回相应的命令信息表示 onionscan 安装成功

4、Tor 的配置

由于 onionscan 是通过 tor 代理进行访问暗网,而 tor 在国内不能直接连接上,所以需要进行代理配置

1234567891011121314
# 编辑tor配置文件vi /etc/tor/torrc# 在文件最后添加Socks4Proxy 192.98.203.92:1088 #socks4代理地址,也支持http代理和sock5代理
# tor不能用root用户运行,创建一个用户来运行toruseradd tor# 创建tor运行所需的目录,以及赋予权限mkdir -p /run/tor
chown -R tor:tor /run/tor
# 用tor用户登录su tor# 启动tortor

出现如下图输出,表示 tor 启动成功

 

onionscan 的使用

123456
# 创建onionscan数据存储目录mkdir -p /opt/onionscandb
# 使用onionscan进行爬取onionscan -dbdir /opt/onionscandb  -torProxyAddress 127.0.0.1:9050 -depth 1 -jsonReport -verbose wallstyizjhkrvmj.onion

参数说明:

  1. -dbdir 指定爬取数据存储目录,未指定情况下会保存在当前路径下
  2. -torProxyAddress 使用 tor 代理
  3. -depth 指定扫描暗网的深度
  4. -jsonReport 扫描结果生成 json 报告
  5. -verbose 控制台输出详细信息

扫描结果效果图

 

参考资料

  1. 暗网系列之:利用 Python + OnionScan 打造自己的安全威胁情报平台
  2. 用好 OnionScan,自己动手制作暗网爬虫

Window 下安装Redis服务

下载地址:https://github.com/MSOpenTech/redis/releases

Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择,这里我们下载 Redis-x64-xxx.zip压缩包到 D:\Development_Tools\Redis

安装完成后,安装目录下大概会有以下几个文件:

redis-server.exe:服务端程序,提供redis服务

redis-cli.exe: 客户端程序,通过它连接redis服务并进行操作

redis-check-dump.exe:本地数据库检查

redis-check-aof.exe:更新日志检查

redis-benchmark.exe:性能测试,用以模拟同时由N个客户端发送M个 SETs/GETs 查询 (类似于 Apache 的ab 工具).

redis.windows.conf: 配置文件,将redis作为普通软件使用的配置,命令行关闭则redis关闭

redis.windows-service.conf:配置文件,将redis作为系统服务的配置,用以区别开两种不同的使用方式

查看及修改redis配置文件http://www.cnblogs.com/ningskyer/articles/5730611.html

打开cmd 窗口 使用cd命令切换目录到 D:\Development_Tools\Redis 运行

redis-server.exe redis.windows.conf

*注:可以把 redis 的路径加到系统的环境变量里,这样就省得再输路径了,后面的那个 redis.windows.conf 可以省略,如果省略,会启用默认的。

输入之后,会显示如下界面:

这时候另启一个cmd窗口,原来的不要关闭,不然就无法访问服务端了。

安装服务

redis-server --service-install redis.windows.conf

OK,大功告成,看看本地的服务,是不是加入了Redis了。

启动服务(安装服务之后,Redis并没有启动):

redis-server --service-start

停止服务

redis-server --service-stop

测试:

切换到redis目录下运行

 redis-cli.exe -h 127.0.0.1 -p 6379

设置键值对 set myKey abc

取出键值对 get myKey

 

介绍一款好用的redis可视化工具:Redis Desktop Manager

下载地址:https://redisdesktop.com/download

 

参考文章1:http://www.runoob.com/redis/redis-install.html

参考文章2:http://www.cnblogs.com/ningskyer/articles/5730611.html

python定时爬取网站更新_详解python定时简单爬取网页新闻存入数据库并发送邮件…

于 2020-12-21 11:08:32 发布

一、项目要求

1、程序可以从北京工业大学首页上爬取新闻内容:http://www.bjut.edu.cn

 

2、程序可以将爬取下来的数据写入本地MySQL数据库中。

3、程序可以将爬取下来的数据发送到邮箱。

4、程序可以定时执行。

二、项目分析

1、爬虫部分利用requests库爬取html文本,再利用bs4中的BeaultifulSoup库来解析html文本,提取需要的内容。

2、使用pymysql库连接MySQL数据库,实现建表和插入内容操作。

3、使用smtplib库建立邮箱连接,再使用email库将文本信息加工成邮件消息并发送。

4、使用schedule库实现定时执行该程序。

三、代码分析

1、导入需要的库:

# 爬虫相关模块

import requests

from bs4 import BeautifulSoup

import pymysql

# 发邮件相关模块

import smtplib

from email.mime.text import MIMEText

from email.header import Header

import time

# 定时模块

import schedule

2、获取html文件:

# 连接获取html文本

def getHTMLtext(url):

try:

headers={undefined

“user-agent”:”Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36″,

} # 浏览器请求头

r = requests.get(url, headers = headers, timeout = 30) # 获取连接

r.raise_for_status() # 测试连接是否成功,若失败则报异常

r.encoding = r.apparent_encoding # 解析编码

return r.text

except:

return “”

其中必须添加请求头headers否则get请求时会返回错误页面。

raise_for_status()可以根据状态码判断连接对象的状态,如果成功便继续执行,若连接失败则抛出异常,因此利用try-except捕获。

apparent_encoding()方法可以解析判断可能的编码方式。

3、解析html提取数据:

首先观察网页源码确定新闻标签位置:

 

# 解析html提取数据

def parseHTML(news, html):

soup = BeautifulSoup(html, “html.parser”) # 获取soup

for i in soup.find(attrs = {‘class’ : ‘list’}).find_all(‘li’): # 存放新闻的li标签

date = i.p.string + ‘-‘ + i.h2.string # 日期

href = i.a[‘href’] # 链接

title = i.find(‘h1’).string # 标题

content = i.find_all(‘p’)[1].string # 梗概

news.append([date, href, title, content]) # 添加到列表中

可见所有新闻内容全部存放在class为”list”的div标签中,而每条新闻又存放在li标签中,因此利用find和find_all方法遍历所有li标签。

每个li标签中a标签的href属性存放新闻链接,h1标签存放新闻标题,h2标签存放日期,第一个p标签存放年、月,第二个p标签存放新闻梗概。依次取出对应标签中的文本内容,并将年月日拼接后依次存入news列表中。

4、存入数据库

# 存入数据库

def toMysql(news):

conn = pymysql.connect(host = ‘localhost’, port = 3306, user = ‘root’, passwd = ‘数据库密码’, db = ‘数据库名称’,charset = ‘gbk’, connect_timeout = 1000)

cursor = conn.cursor()

sql = ”’

create table if not exists tb_news(

日期 date,

链接 varchar(400),

标题 varchar(400),

梗概 varchar(400))

”’

cursor.execute(sql) # 建表

for new in news: # 循环存入数据

sql = ‘insert into tb_news(日期, 链接, 标题, 梗概) values(%s, %s, %s, %s)’

date = new[0]

href = new[1]

title = new[2]

content = new[3]

cursor.execute(sql, (date, href, title, content))

conn.commit()

conn.close()

由于新闻字数较多,存取时可能会有乱码以及数据过长存储失败的问题,与数据库编码有关,可以在MySQL的my.ini配置文件中修改默认编码为gbk。

5、发送邮件

# 发送邮件

def sendMail(news):

from_addr = ‘发送邮箱’ # 发送邮箱

password = ’16位授权码’ # 邮箱授权码

to_addr = ‘接收邮箱’ # 接收邮箱

mailhost = ‘smtp.qq.com’ # qq邮箱的smtp地址

qqmail = smtplib.SMTP() # 建立SMTP对象

qqmail.connect(mailhost, 25) # 25为SMTP常用端口

qqmail.login(from_addr, password) # 登录邮箱

content = ”

for new in news: # 拼接邮件内容字符串

content += ‘新闻时间:’ + new[0] + ‘\n’ + ‘新闻链接:’ + new[1] + ‘\n’ + ‘新闻标题:’ + new[2] + ‘\n’ + ‘新闻梗概:’ + new[3] + ‘\n’

content += ‘======================================================================\n’

# 拼接题目字符串

subject = time.strftime(‘%Y-%m-%d %X’, time.localtime(time.time())) + ‘时爬取的北工大首页主要新闻\n’

# 加工邮件message格式

msg = MIMEText(content, ‘plain’, ‘utf-8’)

msg[‘subject’] = Header(subject, ‘utf-8’)

try:

qqmail.sendmail(from_addr, to_addr, msg.as_string())

print(‘发送成功’)

except:

print(‘发送失败’)

qqmail.quit()

注意其中的密码不是指邮箱的登录密码,而是指邮箱的smtp授权码,qq邮箱可以再设置中开启smtp服务,并获取授权码。

 

6、主函数

# 主函数

def main():

news = []

url = “http://www.bjut.edu.cn/”

html = getHTMLtext(url)

parseHTML(news, html)

toMysql(news)

print(news)

sendMail(news)

输入北京工业大学官网的url并新建一个列表news用来存放消息,然后依次调用函数爬取新闻存入数据库并发到邮箱。为了检验上述程序是否可以完成任务,先调用依次main()函数并print(news)看看结果:

main() #测试需要,之后会删除

结果如下:

 

 

 

由此可见程序执行正常。

7、定时执行

# 定时执行整个任务

schedule.every().monday.at(“08:00”).do(main) # 每周一早上八点执行main函数

while True:

schedule.run_pending()

time.sleep(1)

用死循环保证schedule一直运行。设定的是每周一早上8:00执行程序。

为了方便检查效果,先将运行时间改为每5s运行一次:

schedule.every(5).seconds.do(main)

 

每5s便可以收到一封邮件,由此可见满足定时需求。至此程序结束。

四、完整代码

# 爬虫相关模块

import requests

from bs4 import BeautifulSoup

import pymysql

# 发邮件相关模块

import smtplib

from email.mime.text import MIMEText

from email.header import Header

import time

# 定时模块

import schedule

# 连接获取html文本

def getHTMLtext(url):

try:

headers={undefined

“user-agent”:”Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36″,

} # 浏览器请求头

r = requests.get(url, headers = headers, timeout = 30) # 获取连接

r.raise_for_status() # 测试连接是否成功,若失败则报异常

r.encoding = r.apparent_encoding # 解析编码

return r.text

except:

return “”

# 解析html提取数据

def parseHTML(news, html):

soup = BeautifulSoup(html, “html.parser”) # 获取soup

for i in soup.find(attrs = {‘class’ : ‘list’}).find_all(‘li’): # 存放新闻的li标签

date = i.p.string + ‘-‘ + i.h2.string # 日期

href = i.a[‘href’] # 链接

title = i.find(‘h1’).string # 标题

content = i.find_all(‘p’)[1].string # 梗概

news.append([date, href, title, content]) # 添加到列表中

# 存入数据库

def toMysql(news):

conn = pymysql.connect(host = ‘localhost’, port = 3306, user = ‘root’, passwd = ‘数据库密码’, db = ‘数据库名称’,charset = ‘gbk’, connect_timeout = 1000)

cursor = conn.cursor()

sql = ”’

create table if not exists tb_news(

日期 date,

链接 varchar(400),

标题 varchar(400),

梗概 varchar(400))

”’

cursor.execute(sql) # 建表

for new in news: # 循环存入数据

sql = ‘insert into tb_news(日期, 链接, 标题, 梗概) values(%s, %s, %s, %s)’

date = new[0]

href = new[1]

title = new[2]

content = new[3]

cursor.execute(sql, (date, href, title, content))

conn.commit()

conn.close()

# 发送邮件

def sendMail(news):

from_addr = ‘发送邮箱’ # 发送邮箱

password = ’16位授权码’ # 邮箱授权码

to_addr = ‘接收邮箱’ # 接收邮箱

mailhost = ‘smtp.qq.com’ # qq邮箱的smtp地址

qqmail = smtplib.SMTP() # 建立SMTP对象

qqmail.connect(mailhost, 25) # 25为SMTP常用端口

qqmail.login(from_addr, password) # 登录邮箱

content = ”

for new in news: # 拼接邮件内容字符串

content += ‘新闻时间:’ + new[0] + ‘\n’ + ‘新闻链接:’ + new[1] + ‘\n’ + ‘新闻标题:’ + new[2] + ‘\n’ + ‘新闻梗概:’ + new[3] + ‘\n’

content += ‘======================================================================\n’

# 拼接题目字符串

subject = time.strftime(‘%Y-%m-%d %X’, time.localtime(time.time())) + ‘时爬取的北工大首页主要新闻\n’

# 加工邮件message格式

msg = MIMEText(content, ‘plain’, ‘utf-8’)

msg[‘subject’] = Header(subject, ‘utf-8’)

try:

qqmail.sendmail(from_addr, to_addr, msg.as_string())

print(‘发送成功’)

except:

print(‘发送失败’)

qqmail.quit()

# 主函数

def main():

news = []

url = “http://www.bjut.edu.cn/”

html = getHTMLtext(url)

parseHTML(news, html)

print(news)

sendMail(news)

# 定时执行整个任务

schedule.every().monday.at(“08:00”).do(main) # 每周一早上八点执行main函数

while True:

schedule.run_pending()

time.sleep(1)

相关文章
————————————————
版权声明:本文为CSDN博主「weixin_39997695」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39997695/article/details/111783667

python程序定时或隔时运行

python怎么设置每隔几秒执行脚本?

python设置每隔几秒执行脚本的方法:

1、利用python死循环实现每10s执行一次脚本

#!/usr/bin/env python
import os,time
#how to run it?
#nohup python -u example.py >> /data/logs/example.log 2>&1 &
while True:
        os.system('command')//执行系统命令
        time.sleep(10)//推迟执行、休眠

2、设置1-10s执行一次脚本

#!/usr/bin/env python
import os,time,random
#how to run it?
#nohup python -u example.py >> /data/logs/example.log 2>&1 &
while True:
        sleeptime=random.randint(0, 10)//1-10随机数
        os.system('command')
        time.sleep(sleeptime)

更多Python知识请关注Python自学网

给定需要的时间执行此函数,每隔5秒执行一次的完整代码如下

import time

def sleeptime(hour, min, sec):
return hour * 3600 + min * 60 + sec

second = sleeptime(0, 0, 5)
while 1 == 1:
time.sleep(second)
print(‘do action’)
# 这是隔5秒执行一次
————————————————
版权声明:本文为CSDN博主「夕闻」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43207144/article/details/110367052

python每天定时9点执行_Python实现定时任务,定时采集数据,定时执行脚本程序都可以…

写后端的同学们可能都知道,工作中可能需要周期性执行一些任务,俗称定时任务。Linux环境下,可以借助于系统自带的crontab完成定时任务。但是很多时候,开发的同学们可能并没有权限去操作crontab,所以就催生了一些不太好的解决方案,比如基于Python/Java里面的Timer去弄一个周期性执行的工具。

很多人学习python,不知道从何学起。

很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手。

很多已经做案例的人,却不知道如何去学习更加高深的知识。

那么针对这三类人,我给大家提供一个好的学习平台,免费领取视频教程,电子书籍,以及课程的源代码!

大佬们,醒醒,主角出场了。今天给大家分享一个Python轻量级定时任务解决方案,schedule,可以满足常用的周期性任务需求。

安装

pipinstall schedule

使用

schedule提供了多种方法去描述执行周期,比如:

#1,按照某个时间间隔

every(10).seconds.do(task),表示每隔10秒执行task任务。当然可以把间隔换成其他单位,比如minutes,hours,days

#2,按照一个固定的时间点

every().day.at(‘9:30’).do(task),表示每天9:30执行任务

#encoding=utf-8

fromdatetimeimportdatetime

importtime

importschedule

defjob1(*args):

withopen(‘tasks’,’a’)asf:

f.write(‘[{}]{}\n’.format(datetime.now().strftime(‘%Y-%m-%d %H:%M:%S.%f’),’msg’))

# 每隔5秒执行一次job1

schedule.every(5).seconds.do(job1)

# 每天9:30执行任务

schedule.every().day.at(’09:30′).do(job1)

if__name__ ==’__main__’:

whileTrue:

schedule.run_pending()

time.sleep(1)

效果

如何让任务只执行一次?

只需要在任务最后加上这句话就行,return schedule.CancelJob

#encoding=utf-8

fromdatetimeimportdatetime

importtime

importscheduledef job_run_once(*args):

print(‘Job run only once’)

#

returnschedule.CancelJob

# 只执行一次任务

schedule.every(3).seconds.do(job_run_once)

if__name__ ==’__main__’:

whileTrue:

schedule.run_pending()

time.sleep(1)

就是这么简单,你学会了吗?

  • 实现定时刷新
from time import sleep
from selenium import webdriver
driver= webdriver.Chrome() #  需要 下载 对应浏览器 驱动到 python 安装目录
driver.get("https://blog.csdn.net/qq_27061049/article/details/90577597") # 刷新网址
for i in range(10000): # 刷新次数
    driver.refresh()  # 刷新网页
    sleep(5) # 五秒一次

python中各种转义字符

Python 字符串

字符串是 Python 中最常用的数据类型。我们可以使用引号(‘或”)来创建字符串。

创建字符串很简单,只要为变量分配一个值即可。例如:

var1 = 'Hello World!'
var2 = "Python Runoob"

Python 访问字符串中的值

Python 不支持单字符类型,单字符在 Python 中也是作为一个字符串使用。

Python 访问子字符串,可以使用方括号来截取字符串,如下实例:

实例(Python 2.0+)

#!/usr/bin/python
var1 = Hello World!
var2 = Python Runoob
print var1[0]: , var1[0]
print var2[1:5]: , var2[1:5]

以上实例执行结果:

var1[0]:  H
var2[1:5]:  ytho

Python 字符串连接

我们可以对字符串进行截取并与其他字符串进行连接,如下实例:

实例(Python 2.0+)

#!/usr/bin/python
# -*- coding: UTF-8 -*-
var1 = Hello World!
print 输出 :- , var1[:6] + Runoob!

以上实例执行结果

输出 :-  Hello Runoob!

Python 转义字符

在需要在字符中使用特殊字符时,python 用反斜杠 \ 转义字符。如下表:

转义字符 描述
\(在行尾时) 续行符
\\ 反斜杠符号
\’ 单引号
\” 双引号
\a 响铃
\b 退格(Backspace)
\e 转义
\000
\n 换行
\v 纵向制表符
\t 横向制表符
\r 回车
\f 换页
\oyy 八进制数,y 代表 0~7 的字符,例如:\012 代表换行。
\xyy 十六进制数,以 \x 开头,yy代表的字符,例如:\x0a代表换行
\other 其它的字符以普通格式输出

Python字符串运算符

下表实例变量 a 值为字符串 “Hello”,b 变量值为 “Python”:

操作符 描述 实例
+ 字符串连接
>>>a + b HelloPython
* 重复输出字符串
>>>a * 2 HelloHello
[] 通过索引获取字符串中字符
>>>a[1] e
[ : ] 截取字符串中的一部分
>>>a[1:4] ell
in 成员运算符 – 如果字符串中包含给定的字符返回 True
>>>H in a True
not in 成员运算符 – 如果字符串中不包含给定的字符返回 True
>>>M not in a True
r/R 原始字符串 – 原始字符串:所有的字符串都是直接按照字面的意思来使用,没有转义特殊或不能打印的字符。 原始字符串除在字符串的第一个引号前加上字母”r”(可以大小写)以外,与普通字符串有着几乎完全相同的语法。
>>>print r\n \n >>> print R\n \n
% 格式字符串 请看下一章节

实例(Python 2.0+)

#!/usr/bin/python
# -*- coding: UTF-8 -*-
a = Hello
b = Python
print a + b 输出结果:, a + b
print a * 2 输出结果:, a * 2
print a[1] 输出结果:, a[1]
print a[1:4] 输出结果:, a[1:4] if( H in a) :
print H 在变量 a 中 else :
print H 不在变量 a 中 if( M not in a) :
print M 不在变量 a 中 else :
print M 在变量 a 中
print r\n print R\n

以上程序执行结果为:

a + b 输出结果: HelloPython
a * 2 输出结果: HelloHello
a[1] 输出结果: e
a[1:4] 输出结果: ell
H 在变量 a 
M 不在变量 a 
\n
\n

Python 字符串格式化

Python 支持格式化字符串的输出 。尽管这样可能会用到非常复杂的表达式,但最基本的用法是将一个值插入到一个有字符串格式符 %s 的字符串中。

在 Python 中,字符串格式化使用与 C 中 sprintf 函数一样的语法。

如下实例:

#!/usr/bin/python

print "My name is %s and weight is %d kg!" % ('Zara', 21)

以上实例输出结果:

My name is Zara and weight is 21 kg!

python 字符串格式化符号:

<tbody</tbody

    符   号 描述
      %c  格式化字符及其ASCII码
      %s  格式化字符串
      %d  格式化整数
      %u  格式化无符号整型
      %o  格式化无符号八进制数
      %x  格式化无符号十六进制数
      %X  格式化无符号十六进制数(大写)
      %f  格式化浮点数字,可指定小数点后的精度
      %e  用科学计数法格式化浮点数
      %E  作用同%e,用科学计数法格式化浮点数
      %g  %f和%e的简写
      %G  %F 和 %E 的简写
      %p  用十六进制数格式化变量的地址

格式化操作符辅助指令:

符号 功能
* 定义宽度或者小数点精度
用做左对齐
+ 在正数前面显示加号( + )
<sp> 在正数前面显示空格
# 在八进制数前面显示零(‘0′),在十六进制前面显示’0x’或者’0X'(取决于用的是’x’还是’X’)
0 显示的数字前面填充’0’而不是默认的空格
% ‘%%’输出一个单一的’%’
(var) 映射变量(字典参数)
m.n. m 是显示的最小总宽度,n 是小数点后的位数(如果可用的话)

Python2.6 开始,新增了一种格式化字符串的函数 str.format(),它增强了字符串格式化的功能。


Python 三引号

Python 中三引号可以将复杂的字符串进行赋值。

Python 三引号允许一个字符串跨多行,字符串中可以包含换行符、制表符以及其他特殊字符。

三引号的语法是一对连续的单引号或者双引号(通常都是成对的用)。

 >>> hi = '''hi 
there'''
>>> hi   # repr()
'hi\nthere'
>>> print hi  # str()
hi 
there  

三引号让程序员从引号和特殊字符串的泥潭里面解脱出来,自始至终保持一小块字符串的格式是所谓的WYSIWYG(所见即所得)格式的。

一个典型的用例是,当你需要一块HTML或者SQL时,这时当用三引号标记,使用传统的转义字符体系将十分费神。

 errHTML = '''
<HTML><HEAD><TITLE>
Friends CGI Demo</TITLE></HEAD>
<BODY><H3>ERROR</H3>
<B>%s</B><P>
<FORM><INPUT TYPE=button VALUE=Back
ONCLICK="window.history.back()"></FORM>
</BODY></HTML>
'''
cursor.execute('''
CREATE TABLE users (  
login VARCHAR(8), 
uid INTEGER,
prid INTEGER)
''')

Unicode 字符串

Python 中定义一个 Unicode 字符串和定义一个普通字符串一样简单:

>>> u'Hello World !'
u'Hello World !'

引号前小写的”u”表示这里创建的是一个 Unicode 字符串。如果你想加入一个特殊字符,可以使用 Python 的 Unicode-Escape 编码。如下例所示:

>>> u'Hello\u0020World !'
u'Hello World !'

被替换的 \u0020 标识表示在给定位置插入编码值为 0x0020 的 Unicode 字符(空格符)。


python的字符串内建函数

字符串方法是从python1.6到2.0慢慢加进来的——它们也被加到了Jython中。

这些方法实现了string模块的大部分方法,如下表所示列出了目前字符串内建支持的方法,所有的方法都包含了对Unicode的支持,有一些甚至是专门用于Unicode的。

方法 描述
string.capitalize() 把字符串的第一个字符大写
string.center(width) 返回一个原字符串居中,并使用空格填充至长度 width 的新字符串
string.count(str, beg=0, end=len(string)) 返回 str 在 string 里面出现的次数,如果 beg 或者 end 指定则返回指定范围内 str 出现的次数
string.decode(encoding=’UTF-8′, errors=’strict’) 以 encoding 指定的编码格式解码 string,如果出错默认报一个 ValueError 的 异 常 , 除非 errors 指 定 的 是 ‘ignore’ 或 者’replace’
string.encode(encoding=’UTF-8′, errors=’strict’) 以 encoding 指定的编码格式编码 string,如果出错默认报一个ValueError 的异常,除非 errors 指定的是’ignore’或者’replace’
string.endswith(obj, beg=0, end=len(string)) 检查字符串是否以 obj 结束,如果beg 或者 end 指定则检查指定的范围内是否以 obj 结束,如果是,返回 True,否则返回 False.
string.expandtabs(tabsize=8) 把字符串 string 中的 tab 符号转为空格,tab 符号默认的空格数是 8。
string.find(str, beg=0, end=len(string)) 检测 str 是否包含在 string 中,如果 beg 和 end 指定范围,则检查是否包含在指定范围内,如果是返回开始的索引值,否则返回-1
string.format() 格式化字符串
string.index(str, beg=0, end=len(string)) 跟find()方法一样,只不过如果str不在 string中会报一个异常.
string.isalnum() 如果 string 至少有一个字符并且所有字符都是字母或数字则返

回 True,否则返回 False

string.isalpha() 如果 string 至少有一个字符并且所有字符都是字母则返回 True,

否则返回 False

string.isdecimal() 如果 string 只包含十进制数字则返回 True 否则返回 False.
string.isdigit() 如果 string 只包含数字则返回 True 否则返回 False.
string.islower() 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分大小写的)字符都是小写,则返回 True,否则返回 False
string.isnumeric() 如果 string 中只包含数字字符,则返回 True,否则返回 False
string.isspace() 如果 string 中只包含空格,则返回 True,否则返回 False.
string.istitle() 如果 string 是标题化的(见 title())则返回 True,否则返回 False
string.isupper() 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分大小写的)字符都是大写,则返回 True,否则返回 False
string.join(seq) 以 string 作为分隔符,将 seq 中所有的元素(的字符串表示)合并为一个新的字符串
string.ljust(width) 返回一个原字符串左对齐,并使用空格填充至长度 width 的新字符串
string.lower() 转换 string 中所有大写字符为小写.
string.lstrip() 截掉 string 左边的空格
string.maketrans(intab, outtab]) maketrans() 方法用于创建字符映射的转换表,对于接受两个参数的最简单的调用方式,第一个参数是字符串,表示需要转换的字符,第二个参数也是字符串表示转换的目标。
max(str) 返回字符串 str 中最大的字母。
min(str) 返回字符串 str 中最小的字母。
string.partition(str) 有点像 find()和 split()的结合体,从 str 出现的第一个位置起,把 字 符 串 string 分 成 一 个 3 元 素 的 元 组 (string_pre_str,str,string_post_str),如果 string 中不包含str 则 string_pre_str == string.
string.replace(str1, str2,  num=string.count(str1)) 把 string 中的 str1 替换成 str2,如果 num 指定,则替换不超过 num 次.
string.rfind(str, beg=0,end=len(string) ) 类似于 find() 函数,返回字符串最后一次出现的位置,如果没有匹配项则返回 -1。
string.rindex( str, beg=0,end=len(string)) 类似于 index(),不过是返回最后一个匹配到的子字符串的索引号。
string.rjust(width) 返回一个原字符串右对齐,并使用空格填充至长度 width 的新字符串
string.rpartition(str) 类似于 partition()函数,不过是从右边开始查找
string.rstrip() 删除 string 字符串末尾的空格.
string.split(str=””, num=string.count(str)) 以 str 为分隔符切片 string,如果 num 有指定值,则仅分隔 num+1 个子字符串
string.splitlines([keepends]) 按照行(‘\r’, ‘\r\n’, \n’)分隔,返回一个包含各行作为元素的列表,如果参数 keepends 为 False,不包含换行符,如果为 True,则保留换行符。
string.startswith(obj, beg=0,end=len(string)) 检查字符串是否是以 obj 开头,是则返回 True,否则返回 False。如果beg 和 end 指定值,则在指定范围内检查.
string.strip([obj]) 在 string 上执行 lstrip()和 rstrip()
string.swapcase() 翻转 string 中的大小写
string.title() 返回”标题化”的 string,就是说所有单词都是以大写开始,其余字母均为小写(见 istitle())
string.translate(str, del=””) 根据 str 给出的表(包含 256 个字符)转换 string 的字符,

要过滤掉的字符放到 del 参数中

string.upper() 转换 string 中的小写字母为大写
string.zfill(width) 返回长度为 width 的字符串,原字符串 string 右对齐,前面填充0

 

python实现excel的覆盖写入和追加

python 读取excel方法(最大行数:1048576)


首先需要导入 import openpyxl

1、打开excel,并且获取sheet

1     inwb=openpyxl.load_workbook(Path_generate)
2     Sheetnames=inwb.get_sheet_names()
3     ws=inwb.get_sheet_by_name(Sheetnames[0])

2,最大行数,列数

1     rows=ws.max_row
2     cols=ws.max_column

3,输出指定数值

1 print(ws.cell(1,1).value)

4,写:创造空白空间及sheet

1 outwb=openpyxl.Workbook()
2 outws=outwb.create_sheet(index=0)

5,写:写入信息

1 outws.cell(row, col).value = row * 2  # 写文件

6,保存文件

1 saveExcel = "D:\work\Excel_txtProcesss\test.xlsx"
2 outwb.save(saveExcel)  # 一定要记得保存

汇总:

读取函数

复制代码
 1  def readExel(self):
 2         filename = r'D:workExcel_txtProcesss
ew-微博-合并58.xlsx'
 3         inwb = openpyxl.load_workbook(filename)  # 读文件
 4 
 5         sheetnames = inwb.get_sheet_names()  # 获取读文件中所有的sheet,通过名字的方式
 6         ws = inwb.get_sheet_by_name(sheetnames[0])  # 获取第一个sheet内容
 7 
 8         # 获取sheet的最大行数和列数
 9         rows = ws.max_row
10         cols = ws.max_column
11         for r in range(1, rows):
12             for c in range(1, cols):
13                 print(ws.cell(r, c).value)
14             if r == 10:
15                 break
复制代码

写函数

复制代码
1     def writeExcel(self):
2         outwb = openpyxl.Workbook()  # 打开一个将写的文件
3         outws = outwb.create_sheet(index=0)  # 在将写的文件创建sheet
4         for row in range(1, 70000):
5             for col in range(1, 4):
6                 outws.cell(row, col).value = row * 2  # 写文件
7             print(row)
8         saveExcel = "D:\work\Excel_txtProcesss\test.xlsx"
9         outwb.save(saveExcel)  # 一定要记得保存
复制代码

背景:需要生成类似自动化case格式的那种excel表格

覆盖式写入:
献上代码:

import xlrd
import openpyxl
from xlutils.copy import copy

#覆盖式写入,定义覆盖写入excel函数
def WriteExcel(path, sheet_name):
”’
# :param sheet_name: 需要改写的sheet_name
# :param path: 工作薄的路径
# :return:
# ”’
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.title = sheet_name
k_list = []
v_list = []
for k,v in value_dict.items():
k_list.append(k)
v_list.append(v)
for i in range(0, len(k_list)):
sheet.cell(row=1, column=i + 1, value=k_list[i])
for j in range(0, len(v_list)):
sheet.cell(row=i + 2, column=j + 1, value=v_list[j])

workbook.save(path)
print(“xlsx格式表格【覆盖】写入数据成功!”)

if __name__ == ‘__main__’:

#定义工作薄的路径
path = ‘/Users/dongyue/Desktop/code/leecode/xlsx格式demo.xlsx’
#定义要写入的行和列的值
value_dict = {
“请求方法”:”post”,
“请求参数”:”测试参数”,
“返回参数”:”测试参数”,
“预期结果”:”successful”,
“实际结果”:”successful”,
}
#定义excel的sheet_name “xlsx格式测试表 ”
sheet_name = “xlsx格式测试表”

WriteExcel(path,sheet_name)
执行结果是:

excel中的显示是:

 

这个在第二次执行的时候会覆盖原来的内容,所以这种是全部覆盖式的写入。

 

追加式写入:
献上代码:

import xlrd
import openpyxl
from xlutils.copy import copy

#append写入,定义追加写入excel函数
def WriteExcelAppend(path):
”’
:param line_number: 行数
:param path: 工作薄的路径
:return:
”’
workbook = xlrd.open_workbook(path) #打开工作薄
sheets_name = workbook.sheet_names() #获取工作薄的所有的sheet名称
worksheet = workbook.sheet_by_name(sheets_name[0]) #获取工作薄中的第一个sheet
rows_exists = worksheet.nrows #获取已经存在的数据行数
new_workbook = copy(workbook) #将xlrd对象拷贝转化为xlwt对象
new_worksheet = new_workbook.get_sheet(0) #获取转化后的第一个表格
v_list = []
for k, v in value_dict.items():
v_list.append(v)
for j in range(0, len(v_list)):
new_worksheet.write(rows_exists, j, v_list[j])
new_workbook.save(path) # 保存工作簿
print(“xls格式表格【追加】写入数据成功!”)

if __name__ == ‘__main__’:

#定义工作薄的路径
path = ‘/Users/dongyue/Desktop/code/leecode/xlsx格式demo.xlsx’
#定义要写入的行和列的值
value_dict = {
“请求方法”:”post”,
“请求参数”:”测试参数”,
“返回参数”:”测试参数”,
“预期结果”:”successful”,
“实际结果”:”successful”,
}
#定义excel的sheet_name “xlsx格式测试表 ”
sheet_name = “xlsx格式测试表”

WriteExcelAppend(path)

在执行之前我在excel中备注下:

希望达到的预期是在这行之后进行追加

执行结果为:

excel中的展示

 

我在追加的时候只追加了一条数据,如果小伙伴们想追加多条,可以进行循环。这种的数据是我们自行写死的,在测试过程中的话,是需要拉取各种参数,然后进行写入的,我们可以获取各种参数后,给他处理成dict形式。

还有就是如果想自行的让代码看这个操作是执行追加还是覆盖的话。可以先获取下,已经存在的行数(上面的代码中有),如果值等于0,就说明该表中没有数据,可以执行覆盖式的写入,如果值大于0,就需要进行追加式写入。

现在的excel表格是这样的:

 

将下方代码进行更改

if __name__ == ‘__main__’:

#定义工作薄的路径
path = ‘/Users/dongyue/Desktop/code/leecode/xlsx格式demo.xlsx’
#定义要写入的行和列的值
value_dict = {
“请求方法”:”post”,
“请求参数”:”测试参数”,
“返回参数”:”测试参数”,
“预期结果”:”successful”,
“实际结果”:”successful”,
}
#定义excel的sheet_name “xlsx格式测试表 ”
sheet_name = “xlsx格式测试表”

workbook = xlrd.open_workbook(path)
sheets_name = workbook.sheet_names()
worksheet = workbook.sheet_by_name(sheets_name[0])
rows_exists = worksheet.nrows
if rows_exists == 0:
WriteExcel(path,sheet_name)
else:
WriteExcelAppend(path)
其中取出的是第一个sheet,此时是有数据,这种情况下执行应该是进行追加。执行结果如下:

 

excel中展示:

 

如果将worksheet = workbook.sheet_by_name(sheets_name[0]) 后面的sheets_name[0]改为sheets_name[1],此时获取的就是sheet_name为ceshi的那个表格,这个表格是没有数据的。所以我们此时执行就是覆盖写入,并且之前的sheet是没有了的。

执行结果如下:

excel展示:

——————

 



====================================

——————————
版权声明:本文为CSDN博主「Mojitoice」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Mojitoice/article/details/107020961

您的位置: 网站首页openpyxl教程当前文章

openpyxl删除单行删除多行

删除行与列:

woeksheet.delete_rows(row)#这里填的行数从1开始,1是第一行,所以上面的0是无效的

删除列也是同样可以

woeksheet.delete_cols(row)

点击领取>>python全套教程
点击领取>>js逆向_app逆向_安卓群控

  delete_rows可以指定删除一行也可以删除多行,默认删除一行。官方文档如下:

  openpyxl.worksheet.worksheet.Worksheet.delete_rows()
       delete_rows(idx, amount=1)
       Delete row or rows from row==idx

下面的代码演示openpyxl在excel中追加一行、删除第1行、删除第1到3行(删除行和清空行数据不同,删除行后下面的行会往上移)。

  1. # -*- coding: utf-8 -*-
  2. from openpyxl import Workbook
  3. wb = Workbook() # 默认生成一个名为Sheet的sheet
  4.  
  5. # 创建sheet
  6. for name in [‘a’,‘b’]:
  7. ws = wb.create_sheet(name)
  8.  
  9. # 追加行
  10. for sheet in wb:
  11. for i in range(1,5):
  12. sheet.append([‘a’+str(i),‘b’+str(i)])
  13.  
  14. # 删除第一行
  15. for sheet in wb:
  16. sheet.delete_rows(1)
  17.  
  18. # 删除从1到3行
  19. for sheet in wb:
  20. sheet.delete_rows(1,3)
  21.  
  22. wb.save(‘test.xlsx’)
  23.  

自动化测试远程驱动静默方式(Jenkins+Selenium+Chrome+Docker)

于 2017-08-11 18:39:04 发布

由于之前一直使用PhantomJS作为线上静默方案,并且此方案PhantomJS驱动的Binary和测试代码都在同一台服务器上,导致有时候本地做好的Case经常无法完整的正常运作,并且大多数情况下,测试人员本地Case的编写都是采用Chrome作为首选项,所以我们希望能够统一使用Chrome来进行统一驱动,所以本文记载了工作中遇到的一些坑,以免忘记。

注意:此文档所涉及的内容不太适合新同学,新同学可以打开自动化测试入门教程进行学习,大牛可以直接跳过哈。

踏坑过程
所以,在最开始的时候我设想的方案以为是Chrome与PhantomJS一样,只需要简单的下载一个Driver就可以了,后来查阅相关文档才得知Chrome Driver必须要在本机上安装一个Google Chrome浏览器,并且要下载最新的,否则可能不支持Headless,并且支持Headless的ChromeDriver版本也必须要大于等于2.31;然后,我就开始在Centos7上进行折腾安装Google Chrome与ChromeDriver 2.31,2.31版本的Driver总报一个错:./chromedriver: /lib64/libc.so.6: version `GLIBC_2.18’ not found (required by ./chromedriver, 查阅了帖子才知道尼玛2.31在Centos上存在Bug,具体Bug的帖子:链接1-Stackoverflow、链接2-Google Chrome问题、链接3-Chrome Driver问题, 后来打算换2.30搭配Xvfb进行测试,结果还是不行,果断放弃了;后来想竟然Centos7不行,那就到Jenkins Slave的Docker容器中跑的看看吧,因为我这个Slave镜像是用的Debian 8,结果Chrome Driver 2.31是可以运行起来的,我以为一切都OK了,我就上代码开始跑,结果总是不行,我就不信这个邪,继续查阅资料,让我找到了某人做的一个Google Chrome Headless的Docker镜像,我用他的方式运行,并进入到容器中我自己安装Driver 2.31,结果整体测试下来,初步达到我的需求了,尼玛终于弄好了。

实施步骤
方案一(不推荐)
由于我找到的那个镜像并不直接支持ChromeDriver,所以我对他的镜像进行了二次打包,具体可以查看我的Docker Hub地址,或我的Github Dockerfile源码库,都有说明,如果你有任何更好的方案也可以提交PR到我的仓库,我会Merge的,谢谢!

注意:由于此方案是我封装的镜像,非官方的selenium-standalone的方式,会存在一些扩展上的问题,比如没有截图的功能等,在此基础上我推荐使用方案二,当然也可以选择第三方案。

Docker 运行
进入到镜像仓库中,根据文档的说明或直接执行以下在Docker环境运行就可以跑起来了,默认情况下会把ChromeDriver的 –verbose 进行开启。

docker run -d –name selenium-chrome -p 9515:9515 –cap-add=SYS_ADMIN caryyu/selenium-chrome:latest
1
Java代码Selenium使用
URL url = null;
try {
url = new URL(“http://172.16.1.12:9515”);
} catch (MalformedURLException e) {
e.printStackTrace();
}

ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments(“–headless”,”–disable-gpu”,”–window-size=1290,1080″);
DesiredCapabilities capabilities = DesiredCapabilities.chrome();
capabilities.setCapability(“chromeOptions”, chromeOptions);
WebDriver wdriver = new RemoteWebDriver(url,capabilities);
方案二(推荐方案)
此方案比第一方案好一些,基于selenium-standalone的方式封装的,通过HTTP地址能够打开并截图监控运行状况,记住Docker运行下列镜像的时候一定要加 –cap-add=SYS_ADMIN 权限(更多查看下面 Docker容器权限 )。

Docker 运行
docker run -it –rm –name chrome –shm-size=1024m –cap-add=SYS_ADMIN -p 4444:4444 yukinying/chrome-headless-browser-selenium
1
Java代码Selenium使用
我只截取了关键的代码片段,主要的核心是以下连接地址。

URL url = null;
try {
url = new URL(“http://172.16.1.12:4444/wd/hub”);
} catch (MalformedURLException e) {
e.printStackTrace();
}
// 初始化WebDriver代码同上

参数说明
–headless 必须使用静默模式,无GUI界面;
–disable-gpu 必须要禁用掉gpu,因为服务器没有图形显示相关支持;
–window-size 自定义窗口大小,因为浏览器的Window大小会决定获取到元素的可能性。
更多配置信息你参考:Google Chrome 命令选项大全
方案三(可选)
此方案是第二方案的升级,同样也是属于standalone的方式,由开源社区进行针对Docker版本的开发与维护,支持很多新特性,如视频录制与回放,VNC直接通过Web实时监控Case的运行情况,更此项目细节请点击这里: 点击查看。

注意:此方案经我测试发现在多线程情况下跑用例对内存与CPU的资源消耗特大,需要仔细研究相关的设置后按优配置方可,切记!切记!

docker run -d –name zalenium -p 4444:4444 \
-e TZ=”Asia/Shanghai” \
-e SCREEN_WIDTH=1920 -e SCREEN_HEIGHT=1080 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v zalenium-videos:/home/seluser/videos \
–privileged dosel/zalenium start

Docker容器权限
上述的例子当中,我们创建运行容器的时候强调了 –cap-add=SYS_ADMIN 选项,并表示此选项必须要加上,否则会出现各种问题;其实还有一个与之对应的选项 –privileged,所以在这里我根据我所查阅到的资料:资料一、资料二,再结合我个人的理解进行简要解释,如下:

–privileged 赋予容器最高权限,基本上宿主机能干的事情,容器里都能干。
–cap-add=SYS_ADMIN 具体指定容器拥有的权限(可多次指定不同权限),更多选项查看这个:点击查看。
Jenkins 推荐两款插件
TestNG & Report
此插件主要针对生成Report对结果进行查看,主要原理就是解析testng.xml文件生成图表视图,具体可以:点击文档。

TestNG In Progress
此插件就是在多线程或单线运行时可以实时监控testcase运行进度状态,可以进行很快定位出错的用例,不用每次针对结果进行查看,具体文档:点击这里。

题外话
题外话与本篇核心无关,你没有兴趣的话直接忽略就行了,我只是把一些杂项记录一下,方便之后的查阅。

Centos 7 安装谷歌浏览器
我们利用yum工具进行安装,因为有一个好处就是更新方便,更多具体细节:点击这里

软件源
我们先创建软件源(/etc/yum.repos.d/google-chrome.repo)。

[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub

查看版本信息
yum info google-chrome-stable

安装命令
yum install google-chrome-stable

下载ChromeDriver
wget https://chromedriver.storage.googleapis.com/2.30/chromedriver_linux64.zip

Xvfb虚拟帧缓冲图形X Window服务器
我看有些人利用Xvfb做虚拟图形输出,所以我也把相关的内容记录一下,但我没有尝试成功。Xvfb官网地址, 这里有一个其他人做的Case:点击查看.

安装
yum search xvfb
yum install xorg-x11-server-Xvfb

启动XVFB组件
Xvfb :2 -screen 0 1024x768x16 +extension RANDR &

测试试试
export DISPLAY=:2 && google-chrome http://www.baidu.com

参考资料
https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver
https://sites.google.com/a/chromium.org/chromedriver/
————————————————
版权声明:本文为CSDN博主「littlebrain4solving」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/littlebrain4solving/article/details/77102084

使用slenium+chromedriver实现无敌爬虫

 

摘要

@概述 通常各大网站的后台都会有一定的反爬机制,既为了数据安全,也为了减小服务器压力 通常反爬的手段的方向,都 […]

@概述

  • 通常各大网站的后台都会有一定的反爬机制,既为了数据安全,也为了减小服务器压力
  • 通常反爬的手段的方向,都是识别非浏览器客户端,而selenium所做的事情,恰恰是驱动真正的浏览器去执行请求和操作,只不过信号不是来源于鼠标,而是来源于selenium的API(selenium本是一个自动化的测试工具)
  • 自然人用户能做的一切,selenium几乎都驱动浏览器取做,无论是否有界面,包括输入、点击、滑动,等等
  • 然而到底是鼠标操作的浏览器发起的请求还是API,对于服务端来说,是没有任何差别的
  • 所以说:做人难,做男人难,做一个后台开发的男人难上加难,让我们开始对其实施蹂躏吧

@一些掌故

  • 早些的时候流行的组合并不是selenium+chrome浏览器驱动,而是selenium+phantomjs
  • phantomjs是一款没有界面的浏览器,业界称作无头浏览器(headless),由于没有界面和渲染,其运行速度要大大优于有界面的浏览器,这恰恰是爬虫喜欢的,因此红极一时
  • 后来chrome和火狐推出了无头模式,且运行速度很流畅,phantomjs已然寿终正寝,因此我们表过不提@开发环境的搭建(基于ubuntu)
  • 安装selenium:sudo pip install selenium
  • 如果没有则安装chrome浏览器(尽量更新到58以上):http://www.linuxidc.com/Linux/2016-05/131096.htm
  • 安装chrome浏览器驱动(注意最新版本尾号是29而非9):https://www.cnblogs.com/Lin-Yi/p/7658001.html

@导包

  1. 您需要选择一个短代码# 导入selenium的浏览器驱动接口
  2. from selenium import webdriver
  3. # 要想调用键盘按键操作需要引入keys包
  4. from selenium.webdriver.common.keys import Keys
  5. # 导入chrome选项
  6. from selenium.webdriver.chrome.options import Options

@第一个程序:抓取页面内容,生成页面快照

  1. # 创建chrome浏览器驱动,无头模式(超爽)
  2. chrome_options = Options()
  3. chrome_options.add_argument('--headless')
  4. driver = webdriver.Chrome(chrome_options=chrome_options)
  5. # 加载百度页面
  6. driver.get("http://www.baidu.com/")
  7. # time.sleep(3)
  8. # 获取页面名为wrapper的id标签的文本内容
  9. data = driver.find_element_by_id("wrapper").text
  10. print(data)
  11. # 打印页面标题 "百度一下,你就知道"
  12. print(driver.title)
  13. # 生成当前页面快照并保存
  14. driver.save_screenshot("baidu.png")
  15. # 关闭浏览器
  16. driver.quit()

@模拟用户输入和点击搜索,跟真人操作一样!

  1. # get方法会一直等到页面被完全加载,然后才会继续程序,通常测试会在这里选择 time.sleep(2)
  2. driver.get("http://www.baidu.com/")
  3. # id="kw"是百度搜索输入框,输入字符串"程序猿"
  4. driver.find_element_by_id("kw").send_keys(u"程序猿")
  5. # id="su"是百度搜索按钮,click() 是模拟点击
  6. driver.find_element_by_id("su").click()
  7. time.sleep(3)
  8. # 获取新的页面快照
  9. driver.save_screenshot("程序猿.png")
  10. # 打印网页渲染后的源代码
  11. print(driver.page_source)
  12. # 获取当前页面Cookie
  13. print(driver.get_cookies())
  14. # ctrl+a 全选输入框内容
  15. driver.find_element_by_id("kw").send_keys(Keys.CONTROL, 'a')
  16. # ctrl+x 剪切输入框内容
  17. driver.find_element_by_id("kw").send_keys(Keys.CONTROL, 'x')
  18. # 输入框重新输入内容
  19. driver.find_element_by_id("kw").send_keys("美女")
  20. # 模拟Enter回车键
  21. driver.find_element_by_id("su").send_keys(Keys.RETURN)
  22. time.sleep(3)
  23. # 清除输入框内容
  24. driver.find_element_by_id("kw").clear()
  25. # 生成新的页面快照
  26. driver.save_screenshot("美女.png")
  27. # 获取当前url
  28. print(driver.current_url)
  29. # 关闭浏览器
  30. driver.quit()

@模拟用户登录

  1. # 加载微博登录页
  2. driver.get("http://passport.weibo.cn/signin/login?entry=mweibo&r=http%3A%2F%2Fweibo.cn%2F&backTitle=%CE%A2%B2%A9&vt=")
  3. time.sleep(3)
  4. # 找到输入框,键入用户名和密码
  5. driver.find_element_by_id('loginName').send_keys("worio.hainan@163.com")
  6. driver.find_element_by_id('loginPassword').send_keys("Qq94313805")
  7. # 点击登录按钮
  8. driver.find_element_by_id('loginAction').click()
  9. time.sleep(3)
  10. # 快照显示已经成功登录
  11. print(driver.save_screenshot('jietu.png'))
  12. driver.quit()

@使用cookies登录

  1. # 加载知乎主页,查看快照知此时处于未登录状态
  2. driver.get("https://www.zhihu.com")
  3. time.sleep(1)
  4. print(driver.save_screenshot("zhihu_nocookies.png"))
  5. # 操作浏览器登录知乎并抓包cookies
  6. zhihu_cookies = {
  7. # 'aliyungf_tc' : 'AQAAAAR4YFOeswAAnLFJcVRd4MKOTTXu',
  8. 'l_n_c': '1',
  9. 'q_c1': '8572377703ba49138d30d4b9beb30aed|1514626811000|1514626811000',
  10. 'r_cap_id': 'MTc5M2Y0ODUzMjc0NDMzNmFkNTAzZDBjZTQ4N2EyMTc=|1514626811|a97b2ab0453d6f77c6cdefe903fd649ee8531807',
  11. 'cap_id': 'YjQyZTEwOWM4ODlkNGE1MzkwZTk3NmI5ZGU0ZTY2YzM=|1514626811|d423a17b8d165c8d1b570d64bc98c185d5264b9a',
  12. 'l_cap_id': 'MGE0NjFjM2QxMzZiNGE1ZWFjNjhhZmVkZWQwYzBkZjY=|1514626811|a1eb9f2b9910285350ba979681ca804bd47f12ca',
  13. 'n_c': '1',
  14. 'd_c0': 'AKChpGzG6QyPThyDpmyPhXaV-B9_IYyFspc=|1514626811',
  15. '_xsrf': 'ed7cbc18-03dd-47e9-9885-bbc1c634d10f',
  16. 'capsion_ticket': '2|1:0|10:1514626813|14:capsion_ticket|44:NWY5Y2M0ZGJiZjFlNDdmMzlkYWE0YmNjNjA4MTRhMzY=|6cf7562d6b36288e86afaea5339b31f1dab2921d869ee45fa06d155ea3504fe1',
  17. '_zap': '3290e12b-64dc-4dae-a910-a32cc6e26590',
  18. 'z_c0': '2|1:0|10:1514626827|4:z_c0|92:Mi4xYm4wY0FRQUFBQUFBb0tHa2JNYnBEQ1lBQUFCZ0FsVk5DNjAwV3dCb2xMbEhxc1FTcEJPenpPLWlqSS1qNm5KVEFR|d89c27ab659ba979a977e612803c2c886ab802adadcf70bcb95dc1951bdfaea5',
  19. '__utma': '51854390.2087017282.1514626889.1514626889.1514626889.1',
  20. '__utmb': '51854390.0.10.1514626889',
  21. '__utmc': '51854390',
  22. '__utmz': '51854390.1514626889.1.1.utmcsr=zhihu.com|utmccn=(referral)|utmcmd=referral|utmcct=/',
  23. '__utmv': "51854390.100--|2=registration_date=20150408=1'3=entry_date=20150408=1",
  24. }
  25. # 将用户登录产生的cookies全部添加到当前会话
  26. for k, v in zhihu_cookies.items():
  27. driver.add_cookie({'domain': '.zhihu.com', 'name': k, 'value': v})
  28. # 再次访问知乎主页并拍照,此时已经是登录状态了
  29. driver.get("https://www.zhihu.com")
  30. time.sleep(3)
  31. print(driver.save_screenshot("zhihu_cookies.png"))
  32. # 退出浏览器
  33. driver.quit()

@模拟滚动条的滚动(这个用常规的爬虫很难实现)

  1. # 加载知乎主页
  2. driver.get("https://www.zhihu.com")
  3. time.sleep(1)
  4. # 加载本地cookies实现登录
  5. for k, v in zhihu_cookies.items():
  6. driver.add_cookie({'domain': '.zhihu.com', 'name': k, 'value': v})
  7. # 以登录状态再次发起访问
  8. driver.get("https://www.zhihu.com")
  9. time.sleep(3)
  10. # 将页面滚动到最后,执行多次
  11. for i in range(3):
  12. js = "var q=document.documentElement.scrollTop=10000"
  13. driver.execute_script(js)
  14. time.sleep(3)
  15. # 截图并退出,页面侧边滚动条已经下滑了许多像素
  16. print(driver.save_screenshot("zhihu_scroll.png"))
  17. driver.quit()

@一边滚动一边加载

  • 唯品会首页的女装图片,是一边滚动一边进行ajax异步加载的
  • 这个靠常规的抓包实现起来很麻烦
  • 使用selenium我们只需模拟用户多次下拉滚动条,一段时间之后再重新拿取渲染好的页面源码,就可以像爬取静态页面那样去爬取图片了
  • 类似这种操作,其实质就是开挂,是几乎无法防守的
  1. # 唯品会女装图片链接无法直接获得
  2. # 请求唯品会页面
  3. driver.get("https://category.vip.com/search-3-0-1.html?q=3|30036||&rp=30074|30063&ff=women|0|2|2&adidx=1&f=ad&adp=65001&adid=326630")
  4. time.sleep(3)
  5. # 逐渐滚动浏览器窗口,令ajax逐渐加载
  6. for i in range(1, 10):
  7. js = "var q=document.body.scrollTop=" + str(500 * i) # PhantomJS
  8. js = "var q=document.documentElement.scrollTop=" + str(500 * i) # 谷歌 和 火狐
  9. driver.execute_script(js)
  10. print('=====================================')
  11. time.sleep(3)
  12. # 拿到页面源码
  13. html = etree.HTML(driver.page_source)
  14. all_img_list = []
  15. # 得到所有图片
  16. img_group_list = html.xpath("//img[contains(@id,'J_pic')]")
  17. # img_group_list = html.xpath("//img[starts-with(@id,'J_pic')]")
  18. # 正则表达式匹配
  19. # img_group_list = html.xpath(r'//img[re:match(@id, "J_pic*")]',namespaces={"re": "http://exslt.org/regular-expressions"})
  20. # 收集所有图片链接到列表
  21. for img_group in img_group_list:
  22. img_of_group = img_group.xpath(".//@data-original | .//@data-img-back | .//@data-img-side")
  23. print(img_of_group)
  24. all_img_list.append('\n'.join(img_of_group) + '\n')
  25. # 将收集到的数据写入文件
  26. with open('vip.txt', 'w', encoding='utf-8') as f:
  27. f.write('\n'.join(all_img_list))
  28. # 退出浏览器
  29. driver.quit()

Beautiful Soup 4.2.0 文档

Beautiful Soup 4.2.0 文档

_static/cover.jpgBeautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间.

这篇文档介绍了BeautifulSoup4中所有主要特性,并且有小例子.让我来向你展示它适合做什么,如何工作,怎样使用,如何达到你想要的效果,和处理异常情况.

文档中出现的例子在Python2.7和Python3.2中的执行结果相同

你可能在寻找 Beautiful Soup3 的文档,Beautiful Soup 3 目前已经停止开发,我们推荐在现在的项目中使用Beautiful Soup 4, 移植到BS4

寻求帮助

如果你有关于BeautifulSoup的问题,可以发送邮件到 讨论组 .如果你的问题包含了一段需要转换的HTML代码,那么确保你提的问题描述中附带这段HTML文档的 代码诊断 [1]

快速开始

下面的一段HTML代码将作为例子被多次用到.这是 爱丽丝梦游仙境的 的一段内容(以后内容中简称为 爱丽丝 的文档):

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

使用BeautifulSoup解析这段代码,能够得到一个 BeautifulSoup 的对象,并能按照标准的缩进格式的结构输出:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

print(soup.prettify())
# <html>
#  <head>
#   <title>
#    The Dormouse's story
#   </title>
#  </head>
#  <body>
#   <p class="title">
#    <b>
#     The Dormouse's story
#    </b>
#   </p>
#   <p class="story">
#    Once upon a time there were three little sisters; and their names were
#    <a class="sister" href="http://example.com/elsie" id="link1">
#     Elsie
#    </a>
#    ,
#    <a class="sister" href="http://example.com/lacie" id="link2">
#     Lacie
#    </a>
#    and
#    <a class="sister" href="http://example.com/tillie" id="link2">
#     Tillie
#    </a>
#    ; and they lived at the bottom of a well.
#   </p>
#   <p class="story">
#    ...
#   </p>
#  </body>
# </html>

几个简单的浏览结构化数据的方法:

soup.title
# <title>The Dormouse's story</title>

soup.title.name
# u'title'

soup.title.string
# u'The Dormouse's story'

soup.title.parent.name
# u'head'

soup.p
# <p class="title"><b>The Dormouse's story</b></p>

soup.p['class']
# u'title'

soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find(id="link3")
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

从文档中找到所有<a>标签的链接:

for link in soup.find_all('a'):
    print(link.get('href'))
    # http://example.com/elsie
    # http://example.com/lacie
    # http://example.com/tillie

从文档中获取所有文字内容:

print(soup.get_text())
# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...

这是你想要的吗?别着急,还有更好用的

安装 Beautiful Soup

如果你用的是新版的Debain或ubuntu,那么可以通过系统的软件包管理来安装:

$ apt-get install Python-bs4

Beautiful Soup 4 通过PyPi发布,所以如果你无法使用系统包管理安装,那么也可以通过 easy_install 或 pip 来安装.包的名字是 beautifulsoup4 ,这个包兼容Python2和Python3.

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

(在PyPi中还有一个名字是 BeautifulSoup 的包,但那可能不是你想要的,那是 Beautiful Soup3 的发布版本,因为很多项目还在使用BS3, 所以 BeautifulSoup 包依然有效.但是如果你在编写新项目,那么你应该安装的 beautifulsoup4 )

如果你没有安装 easy_install 或 pip ,那你也可以 下载BS4的源码 ,然后通过setup.py来安装.

$ Python setup.py install

如果上述安装方法都行不通,Beautiful Soup的发布协议允许你将BS4的代码打包在你的项目中,这样无须安装即可使用.

作者在Python2.7和Python3.2的版本下开发Beautiful Soup, 理论上Beautiful Soup应该在所有当前的Python版本中正常工作

安装完成后的问题

Beautiful Soup发布时打包成Python2版本的代码,在Python3环境下安装时,会自动转换成Python3的代码,如果没有一个安装的过程,那么代码就不会被转换.

如果代码抛出了 ImportError 的异常: “No module named HTMLParser”, 这是因为你在Python3版本中执行Python2版本的代码.

如果代码抛出了 ImportError 的异常: “No module named html.parser”, 这是因为你在Python2版本中执行Python3版本的代码.

如果遇到上述2种情况,最好的解决方法是重新安装BeautifulSoup4.

如果在ROOT_TAG_NAME = u’[document]’代码处遇到 SyntaxError “Invalid syntax”错误,需要将把BS4的Python代码版本从Python2转换到Python3. 可以重新安装BS4:

$ Python3 setup.py install

或在bs4的目录中执行Python代码版本转换脚本

$ 2to3-3.2 -w bs4

安装解析器

Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,其中一个是 lxml .根据操作系统不同,可以选择下列方法来安装lxml:

$ apt-get install Python-lxml

$ easy_install lxml

$ pip install lxml

另一个可供选择的解析器是纯Python实现的 html5lib , html5lib的解析方式与浏览器相同,可以选择下列方法来安装html5lib:

$ apt-get install Python-html5lib

$ easy_install html5lib

$ pip install html5lib

下表列出了主要的解析器,以及它们的优缺点:

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, "html.parser")
  • Python的内置标准库
  • 执行速度适中
  • 文档容错能力强
  • Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
lxml HTML 解析器 BeautifulSoup(markup, "lxml")
  • 速度快
  • 文档容错能力强
  • 需要安装C语言库
lxml XML 解析器

BeautifulSoup(markup, ["lxml", "xml"])

BeautifulSoup(markup, "xml")

  • 速度快
  • 唯一支持XML的解析器
  • 需要安装C语言库
html5lib BeautifulSoup(markup, "html5lib")
  • 最好的容错性
  • 以浏览器的方式解析文档
  • 生成HTML5格式的文档
  • 速度慢
  • 不依赖外部扩展

推荐使用lxml作为解析器,因为效率更高. 在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必须安装lxml或html5lib, 因为那些Python版本的标准库中内置的HTML解析方法不够稳定.

提示: 如果一段HTML或XML文档格式不正确的话,那么在不同的解析器中返回的结果可能是不一样的,查看 解析器之间的区别 了解更多细节

如何使用

将一段文档传入BeautifulSoup 的构造方法,就能得到一个文档的对象, 可以传入一段字符串或一个文件句柄.

from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))

soup = BeautifulSoup("<html>data</html>")

首先,文档被转换成Unicode,并且HTML的实例都被转换成Unicode编码

BeautifulSoup("Sacr&eacute; bleu!")
<html><head></head><body>Sacré bleu!</body></html>

然后,Beautiful Soup选择最合适的解析器来解析这段文档,如果手动指定解析器那么Beautiful Soup会选择指定的解析器来解析文档.(参考 解析成XML ).

对象的种类

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种: Tag , NavigableString , BeautifulSoup , Comment .

Tag

Tag 对象与XML或HTML原生文档中的tag相同:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>

Tag有很多方法和属性,在 遍历文档树 和 搜索文档树 中有详细解释.现在介绍一下tag中最重要的属性: name和attributes

Name

每个tag都有自己的名字,通过 .name 来获取:

tag.name
# u'b'

如果改变了tag的name,那将影响所有通过当前Beautiful Soup对象生成的HTML文档:

tag.name = "blockquote"
tag
# <blockquote class="boldest">Extremely bold</blockquote>

Attributes

一个tag可能有很多个属性. tag <b class="boldest"> 有一个 “class” 的属性,值为 “boldest” . tag的属性的操作方法与字典相同:

tag['class']
# u'boldest'

也可以直接”点”取属性, 比如: .attrs :

tag.attrs
# {u'class': u'boldest'}

tag的属性可以被添加,删除或修改. 再说一次, tag的属性操作方法与字典一样

tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

tag['class']
# KeyError: 'class'
print(tag.get('class'))
# None

多值属性

HTML 4定义了一系列可以包含多个值的属性.在HTML5中移除了一些,却增加更多.最常见的多值的属性是 class (一个tag可以有多个CSS的class). 还有一些属性 rel , rev , accept-charset , headers , accesskey . 在Beautiful Soup中多值属性的返回类型是list:

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class']
# ["body", "strikeout"]

css_soup = BeautifulSoup('<p class="body"></p>')
css_soup.p['class']
# ["body"]

如果某个属性看起来好像有多个值,但在任何版本的HTML定义中都没有被定义为多值属性,那么Beautiful Soup会将这个属性作为字符串返回

id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'

将tag转换成字符串时,多值属性会合并为一个值

rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']
rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>

如果转换的文档是XML格式,那么tag中不包含多值属性

xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
xml_soup.p['class']
# u'body strikeout'

可以遍历的字符串

字符串常被包含在tag内.Beautiful Soup用 NavigableString 类来包装tag中的字符串:

tag.string
# u'Extremely bold'
type(tag.string)
# <class 'bs4.element.NavigableString'>

一个 NavigableString 字符串与Python中的Unicode字符串相同,并且还支持包含在 遍历文档树 和 搜索文档树 中的一些特性. 通过 unicode() 方法可以直接将 NavigableString 对象转换成Unicode字符串:

unicode_string = unicode(tag.string)
unicode_string
# u'Extremely bold'
type(unicode_string)
# <type 'unicode'>

tag中包含的字符串不能编辑,但是可以被替换成其它的字符串,用 replace_with() 方法:

tag.string.replace_with("No longer bold")
tag
# <blockquote>No longer bold</blockquote>

NavigableString 对象支持 遍历文档树 和 搜索文档树 中定义的大部分属性, 并非全部.尤其是,一个字符串不能包含其它内容(tag能够包含字符串或是其它tag),字符串不支持 .contents 或 .string 属性或 find() 方法.

如果想在Beautiful Soup之外使用 NavigableString 对象,需要调用 unicode() 方法,将该对象转换成普通的Unicode字符串,否则就算Beautiful Soup已方法已经执行结束,该对象的输出也会带有对象的引用地址.这样会浪费内存.

BeautifulSoup

BeautifulSoup 对象表示的是一个文档的全部内容.大部分时候,可以把它当作 Tag 对象,它支持 遍历文档树 和 搜索文档树 中描述的大部分的方法.

因为 BeautifulSoup 对象并不是真正的HTML或XML的tag,所以它没有name和attribute属性.但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个值为 “[document]” 的特殊属性 .name

soup.name
# u'[document]'

注释及特殊字符串

Tag , NavigableString , BeautifulSoup 几乎覆盖了html和xml中的所有内容,但是还有一些特殊对象.容易让人担心的内容是文档的注释部分:

markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>

Comment 对象是一个特殊类型的 NavigableString 对象:

comment
# u'Hey, buddy. Want to buy a used parser'

但是当它出现在HTML文档中时, Comment 对象会使用特殊的格式输出:

print(soup.b.prettify())
# <b>
#  <!--Hey, buddy. Want to buy a used parser?-->
# </b>

Beautiful Soup中定义的其它类型都可能会出现在XML的文档中: CData , ProcessingInstruction , Declaration , Doctype .与 Comment 对象类似,这些类都是 NavigableString 的子类,只是添加了一些额外的方法的字符串独享.下面是用CDATA来替代注释的例子:

from bs4 import CData
cdata = CData("A CDATA block")
comment.replace_with(cdata)

print(soup.b.prettify())
# <b>
#  <![CDATA[A CDATA block]]>
# </b>

遍历文档树

还拿”爱丽丝梦游仙境”的文档来做例子:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

通过这段例子来演示怎样从文档的一段内容找到另一段内容

子节点

一个Tag可能包含多个字符串或其它的Tag,这些都是这个Tag的子节点.Beautiful Soup提供了许多操作和遍历子节点的属性.

注意: Beautiful Soup中字符串节点不支持这些属性,因为字符串没有子节点

tag的名字

操作文档树最简单的方法就是告诉它你想获取的tag的name.如果想获取 <head> 标签,只要用 soup.head :

soup.head
# <head><title>The Dormouse's story</title></head>

soup.title
# <title>The Dormouse's story</title>

这是个获取tag的小窍门,可以在文档树的tag中多次调用这个方法.下面的代码可以获取<body>标签中的第一个<b>标签:

soup.body.b
# <b>The Dormouse's story</b>

通过点取属性的方式只能获得当前名字的第一个tag:

soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

如果想要得到所有的<a>标签,或是通过名字得到比一个tag更多的内容的时候,就需要用到 Searching the tree 中描述的方法,比如: find_all()

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

.contents 和 .children

tag的 .contents 属性可以将tag的子节点以列表的方式输出:

head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>

head_tag.contents
[<title>The Dormouse's story</title>]

title_tag = head_tag.contents[0]
title_tag
# <title>The Dormouse's story</title>
title_tag.contents
# [u'The Dormouse's story']

BeautifulSoup 对象本身一定会包含子节点,也就是说<html>标签也是 BeautifulSoup 对象的子节点:

len(soup.contents)
# 1
soup.contents[0].name
# u'html'

字符串没有 .contents 属性,因为字符串没有子节点:

text = title_tag.contents[0]
text.contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

通过tag的 .children 生成器,可以对tag的子节点进行循环:

for child in title_tag.children:
    print(child)
    # The Dormouse's story

.descendants

.contents 和 .children 属性仅包含tag的直接子节点.例如,<head>标签只有一个直接子节点<title>

head_tag.contents
# [<title>The Dormouse's story</title>]

但是<title>标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串 “The Dormouse’s story”也属于<head>标签的子孙节点. .descendants 属性可以对所有tag的子孙节点进行递归循环 [5] :

for child in head_tag.descendants:
    print(child)
    # <title>The Dormouse's story</title>
    # The Dormouse's story

上面的例子中, <head>标签只有一个子节点,但是有2个子孙节点:<head>节点和<head>的子节点, BeautifulSoup 有一个直接子节点(<html>节点),却有很多子孙节点:

len(list(soup.children))
# 1
len(list(soup.descendants))
# 25

.string

如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点:

title_tag.string
# u'The Dormouse's story'

如果一个tag仅有一个子节点,那么这个tag也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同:

head_tag.contents
# [<title>The Dormouse's story</title>]

head_tag.string
# u'The Dormouse's story'

如果tag包含了多个子节点,tag就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None :

print(soup.html.string)
# None

.strings 和 stripped_strings

如果tag中包含多个字符串 [2] ,可以使用 .strings 来循环获取:

for string in soup.strings:
    print(repr(string))
    # u"The Dormouse's story"
    # u'\n\n'
    # u"The Dormouse's story"
    # u'\n\n'
    # u'Once upon a time there were three little sisters; and their names were\n'
    # u'Elsie'
    # u',\n'
    # u'Lacie'
    # u' and\n'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'\n\n'
    # u'...'
    # u'\n'

输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容:

for string in soup.stripped_strings:
    print(repr(string))
    # u"The Dormouse's story"
    # u"The Dormouse's story"
    # u'Once upon a time there were three little sisters; and their names were'
    # u'Elsie'
    # u','
    # u'Lacie'
    # u'and'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'...'

全部是空格的行会被忽略掉,段首和段末的空白会被删除

父节点

继续分析文档树,每个tag或字符串都有父节点:被包含在某个tag中

.parent

通过 .parent 属性来获取某个元素的父节点.在例子“爱丽丝”的文档中,<head>标签是<title>标签的父节点:

title_tag = soup.title
title_tag
# <title>The Dormouse's story</title>
title_tag.parent
# <head><title>The Dormouse's story</title></head>

文档title的字符串也有父节点:<title>标签

title_tag.string.parent
# <title>The Dormouse's story</title>

文档的顶层节点比如<html>的父节点是 BeautifulSoup 对象:

html_tag = soup.html
type(html_tag.parent)
# <class 'bs4.BeautifulSoup'>

BeautifulSoup 对象的 .parent 是None:

print(soup.parent)
# None

.parents

通过元素的 .parents 属性可以递归得到元素的所有父辈节点,下面的例子使用了 .parents 方法遍历了<a>标签到根节点的所有节点.

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
for parent in link.parents:
    if parent is None:
        print(parent)
    else:
        print(parent.name)
# p
# body
# html
# [document]
# None

兄弟节点

看一段简单的例子:

sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
print(sibling_soup.prettify())
# <html>
#  <body>
#   <a>
#    <b>
#     text1
#    </b>
#    <c>
#     text2
#    </c>
#   </a>
#  </body>
# </html>

因为<b>标签和<c>标签是同一层:他们是同一个元素的子节点,所以<b>和<c>可以被称为兄弟节点.一段文档以标准格式输出时,兄弟节点有相同的缩进级别.在代码中也可以使用这种关系.

.next_sibling 和 .previous_sibling

在文档树中,使用 .next_sibling 和 .previous_sibling 属性来查询兄弟节点:

sibling_soup.b.next_sibling
# <c>text2</c>

sibling_soup.c.previous_sibling
# <b>text1</b>

<b>标签有 .next_sibling 属性,但是没有 .previous_sibling 属性,因为<b>标签在同级节点中是第一个.同理,<c>标签有 .previous_sibling 属性,却没有 .next_sibling 属性:

print(sibling_soup.b.previous_sibling)
# None
print(sibling_soup.c.next_sibling)
# None

例子中的字符串“text1”和“text2”不是兄弟节点,因为它们的父节点不同:

sibling_soup.b.string
# u'text1'

print(sibling_soup.b.string.next_sibling)
# None

实际文档中的tag的 .next_sibling 和 .previous_sibling 属性通常是字符串或空白. 看看“爱丽丝”文档:

<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>

如果以为第一个<a>标签的 .next_sibling 结果是第二个<a>标签,那就错了,真实结果是第一个<a>标签和第二个<a>标签之间的顿号和换行符:

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

link.next_sibling
# u',\n'

第二个<a>标签是顿号的 .next_sibling 属性:

link.next_sibling.next_sibling
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>

.next_siblings 和 .previous_siblings

通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出:

for sibling in soup.a.next_siblings:
    print(repr(sibling))
    # u',\n'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # u' and\n'
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
    # u'; and they lived at the bottom of a well.'
    # None

for sibling in soup.find(id="link3").previous_siblings:
    print(repr(sibling))
    # ' and\n'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # u',\n'
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
    # u'Once upon a time there were three little sisters; and their names were\n'
    # None

回退和前进

看一下“爱丽丝” 文档:

<html><head><title>The Dormouse's story</title></head>
<p class="title"><b>The Dormouse's story</b></p>

HTML解析器把这段字符串转换成一连串的事件: “打开<html>标签”,”打开一个<head>标签”,”打开一个<title>标签”,”添加一段字符串”,”关闭<title>标签”,”打开<p>标签”,等等.Beautiful Soup提供了重现解析器初始化过程的方法.

.next_element 和 .previous_element

.next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的.

这是“爱丽丝”文档中最后一个<a>标签,它的 .next_sibling 结果是一个字符串,因为当前的解析过程 [2] 因为当前的解析过程因为遇到了<a>标签而中断了:

last_a_tag = soup.find("a", id="link3")
last_a_tag
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_a_tag.next_sibling
# '; and they lived at the bottom of a well.'

但这个<a>标签的 .next_element 属性结果是在<a>标签被解析之后的解析内容,不是<a>标签后的句子部分,应该是字符串”Tillie”:

last_a_tag.next_element
# u'Tillie'

这是因为在原始文档中,字符串“Tillie” 在分号前出现,解析器先进入<a>标签,然后是字符串“Tillie”,然后关闭</a>标签,然后是分号和剩余部分.分号与<a>标签在同一层级,但是字符串“Tillie”会被先解析.

.previous_element 属性刚好与 .next_element 相反,它指向当前被解析的对象的前一个解析对象:

last_a_tag.previous_element
# u' and\n'
last_a_tag.previous_element.next_element
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

.next_elements 和 .previous_elements

通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样:

for element in last_a_tag.next_elements:
    print(repr(element))
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# <p class="story">...</p>
# u'...'
# u'\n'
# None

搜索文档树

Beautiful Soup定义了很多搜索方法,这里着重介绍2个: find() 和 find_all() .其它方法的参数和用法类似,请读者举一反三.

再以“爱丽丝”文档作为例子:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

使用 find_all() 类似的方法可以查找到想要查找的文档内容

过滤器

介绍 find_all() 方法前,先介绍一下过滤器的类型 [3] ,这些过滤器贯穿整个搜索的API.过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中.

字符串

最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的<b>标签:

soup.find_all('b')
# [<b>The Dormouse's story</b>]

如果传入字节码参数,Beautiful Soup会当作UTF-8编码,可以传入一段Unicode 编码来避免Beautiful Soup解析编码出错

正则表达式

如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容.下面例子中找出所有以b开头的标签,这表示<body>和<b>标签都应该被找到:

import re
for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
# body
# b

下面代码找出所有名字中包含”t”的标签:

for tag in soup.find_all(re.compile("t")):
    print(tag.name)
# html
# title

列表

如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有<a>标签和<b>标签:

soup.find_all(["a", "b"])
# [<b>The Dormouse's story</b>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

True

True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点

for tag in soup.find_all(True):
    print(tag.name)
# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

方法

如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 [4] ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False

下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True:

def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')

将这个方法作为参数传入 find_all() 方法,将得到所有<p>标签:

soup.find_all(has_class_but_no_id)
# [<p class="title"><b>The Dormouse's story</b></p>,
#  <p class="story">Once upon a time there were...</p>,
#  <p class="story">...</p>]

返回结果中只有<p>标签没有<a>标签,因为<a>标签还定义了”id”,没有返回<html>和<head>,因为<html>和<head>中没有定义”class”属性.

下面代码找到所有被文字包含的节点内容:

from bs4 import NavigableString
def surrounded_by_strings(tag):
    return (isinstance(tag.next_element, NavigableString)
            and isinstance(tag.previous_element, NavigableString))

for tag in soup.find_all(surrounded_by_strings):
    print tag.name
# p
# a
# a
# a
# p

现在来了解一下搜索方法的细节

find_all()

find_all( name , attrs , recursive , text , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.这里有几个例子:

soup.find_all("title")
# [<title>The Dormouse's story</title>]

soup.find_all("p", "title")
# [<p class="title"><b>The Dormouse's story</b></p>]

soup.find_all("a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find_all(id="link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

import re
soup.find(text=re.compile("sisters"))
# u'Once upon a time there were three little sisters; and their names were\n'

有几个方法很相似,还有几个方法是新的,参数中的 text 和 id 是什么含义? 为什么 find_all("p", "title") 返回的是CSS Class为”title”的<p>标签? 我们来仔细看一下 find_all() 的参数

name 参数

name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉.

简单的用法如下:

soup.find_all("title")
# [<title>The Dormouse's story</title>]

重申: 搜索 name 参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True .

keyword 参数

如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 id 的参数,Beautiful Soup会搜索每个tag的”id”属性.

soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

如果传入 href 参数,Beautiful Soup会搜索每个tag的”href”属性:

soup.find_all(href=re.compile("elsie"))
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

搜索指定名字的属性时可以使用的参数值包括 字符串 , 正则表达式 , 列表True .

下面的例子在文档树中查找所有包含 id 属性的tag,无论 id 的值是什么:

soup.find_all(id=True)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

使用多个指定名字的参数可以同时过滤tag的多个属性:

soup.find_all(href=re.compile("elsie"), id='link1')
# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:

data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
data_soup.find_all(data-foo="value")
# SyntaxError: keyword can't be an expression

但是可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的tag:

data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]

按CSS搜索

按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字 class 在Python中是保留字,使用 class 做参数会导致语法错误.从Beautiful Soup的4.1.1版本开始,可以通过 class_ 参数搜索有指定CSS类名的tag:

soup.find_all("a", class_="sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

class_ 参数同样接受不同类型的 过滤器 ,字符串,正则表达式,方法或 True :

soup.find_all(class_=re.compile("itl"))
# [<p class="title"><b>The Dormouse's story</b></p>]

def has_six_characters(css_class):
    return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

tag的 class 属性是 多值属性 .按照CSS类名搜索tag时,可以分别搜索tag中的每个CSS类名:

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.find_all("p", class_="strikeout")
# [<p class="body strikeout"></p>]

css_soup.find_all("p", class_="body")
# [<p class="body strikeout"></p>]

搜索 class 属性时也可以通过CSS值完全匹配:

css_soup.find_all("p", class_="body strikeout")
# [<p class="body strikeout"></p>]

完全匹配 class 的值时,如果CSS类名的顺序与实际不符,将搜索不到结果:

soup.find_all("a", attrs={"class": "sister"})
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

text 参数

通过 text 参数可以搜搜文档中的字符串内容.与 name 参数的可选值一样, text 参数接受 字符串 , 正则表达式 , 列表True . 看例子:

soup.find_all(text="Elsie")
# [u'Elsie']

soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):
    ""Return True if this string is the only child of its parent tag.""
    return (s == s.parent.string)

soup.find_all(text=is_the_only_string_within_a_tag)
# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

虽然 text 参数用于搜索字符串,还可以与其它参数混合使用来过滤tag.Beautiful Soup会找到 .string 方法与 text 参数值相符的tag.下面代码用来搜索内容里面包含“Elsie”的<a>标签:

soup.find_all("a", text="Elsie")
# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

limit 参数

find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢.如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量.效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果.

文档树中有3个tag符合搜索条件,但结果只返回了2个,因为我们限制了返回数量:

soup.find_all("a", limit=2)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

recursive 参数

调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False .

一段简单的文档:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...

是否使用 recursive 参数的搜索结果:

soup.html.find_all("title")
# [<title>The Dormouse's story</title>]

soup.html.find_all("title", recursive=False)
# []

像调用 find_all() 一样调用tag

find_all() 几乎是Beautiful Soup中最常用的搜索方法,所以我们定义了它的简写方法. BeautifulSoup 对象和 tag 对象可以被当作一个方法来使用,这个方法的执行结果与调用这个对象的 find_all() 方法相同,下面两行代码是等价的:

soup.find_all("a")
soup("a")

这两行代码也是等价的:

soup.title.find_all(text=True)
soup.title(text=True)

find()

find( name , attrs , recursive , text , **kwargs )

find_all() 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个<body>标签,那么使用 find_all() 方法来查找<body>标签就不太合适, 使用 find_all 方法并设置 limit=1 参数不如直接使用 find() 方法.下面两行代码是等价的:

soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]

soup.find('title')
# <title>The Dormouse's story</title>

唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果.

find_all() 方法没有找到目标是返回空列表, find() 方法找不到目标时,返回 None .

print(soup.find("nosuchtag"))
# None

soup.head.title 是 tag的名字 方法的简写.这个简写的原理就是多次调用当前tag的 find() 方法:

soup.head.title
# <title>The Dormouse's story</title>

soup.find("head").find("title")
# <title>The Dormouse's story</title>

find_parents() 和 find_parent()

find_parents( name , attrs , recursive , text , **kwargs )

find_parent( name , attrs , recursive , text , **kwargs )

我们已经用了很大篇幅来介绍 find_all() 和 find() 方法,Beautiful Soup中还有10个用于搜索的API.它们中的五个用的是与 find_all() 相同的搜索参数,另外5个与 find() 方法的搜索参数类似.区别仅是它们搜索文档的不同部分.

记住: find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等. find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容. 我们从一个文档中的一个叶子节点开始:

a_string = soup.find(text="Lacie")
a_string
# u'Lacie'

a_string.find_parents("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

a_string.find_parent("p")
# <p class="story">Once upon a time there were three little sisters; and their names were
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
#  and they lived at the bottom of a well.</p>

a_string.find_parents("p", class="title")
# []

文档中的一个<a>标签是是当前叶子节点的直接父节点,所以可以被找到.还有一个<p>标签,是目标叶子节点的间接父辈节点,所以也可以被找到.包含class值为”title”的<p>标签不是不是目标叶子节点的父辈节点,所以通过 find_parents() 方法搜索不到.

find_parent() 和 find_parents() 方法会让人联想到 .parent 和 .parents 属性.它们之间的联系非常紧密.搜索父辈节点的方法实际上就是对 .parents 属性的迭代搜索.

find_next_siblings() 合 find_next_sibling()

find_next_siblings( name , attrs , recursive , text , **kwargs )

find_next_sibling( name , attrs , recursive , text , **kwargs )

这2个方法通过 .next_siblings 属性对当tag的所有后面解析 [5] 的兄弟tag节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点, find_next_sibling() 只返回符合条件的后面的第一个tag节点.

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_next_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_next_sibling("p")
# <p class="story">...</p>

find_previous_siblings() 和 find_previous_sibling()

find_previous_siblings( name , attrs , recursive , text , **kwargs )

find_previous_sibling( name , attrs , recursive , text , **kwargs )

这2个方法通过 .previous_siblings 属性对当前tag的前面解析 [5] 的兄弟tag节点进行迭代, find_previous_siblings() 方法返回所有符合条件的前面的兄弟节点, find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点:

last_link = soup.find("a", id="link3")
last_link
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_link.find_previous_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# <p class="title"><b>The Dormouse's story</b></p>

find_all_next() 和 find_next()

find_all_next( name , attrs , recursive , text , **kwargs )

find_next( name , attrs , recursive , text , **kwargs )

这2个方法通过 .next_elements 属性对当前tag的之后的 [5] tag和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点:

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_next(text=True)
# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
#  u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")
# <p class="story">...</p>

第一个例子中,字符串 “Elsie”也被显示出来,尽管它被包含在我们开始查找的<a>标签的里面.第二个例子中,最后一个<p>标签也被显示出来,尽管它与我们开始查找位置的<a>标签不属于同一部分.例子中,搜索的重点是要匹配过滤器的条件,并且在文档中出现的顺序而不是开始查找的元素的位置.

find_all_previous() 和 find_previous()

find_all_previous( name , attrs , recursive , text , **kwargs )

find_previous( name , attrs , recursive , text , **kwargs )

这2个方法通过 .previous_elements 属性对当前节点前面 [5] 的tag和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous() 方法返回第一个符合条件的节点.

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_previous("p")
# [<p class="story">Once upon a time there were three little sisters; ...</p>,
#  <p class="title"><b>The Dormouse's story</b></p>]

first_link.find_previous("title")
# <title>The Dormouse's story</title>

find_all_previous("p") 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,<p>标签包含了我们开始查找的<a>标签.不要惊讶,这段代码的功能是查找所有出现在指定<a>标签之前的<p>标签,因为这个<p>标签包含了开始的<a>标签,所以<p>标签一定是在<a>之前出现的.

CSS选择器

Beautiful Soup支持大部分的CSS选择器 [6] ,在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数,即可使用CSS选择器的语法找到tag:

soup.select("title")
# [<title>The Dormouse's story</title>]

soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]

通过tag标签逐层查找:

soup.select("body a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("html head title")
# [<title>The Dormouse's story</title>]

找到某个tag标签下的直接子标签 [6] :

soup.select("head > title")
# [<title>The Dormouse's story</title>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("body > a")
# []

找到兄弟节点标签:

soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie"  id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过CSS的类名查找:

soup.select(".sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("[class~=sister]")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过tag的id查找:

soup.select("#link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("a#link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过是否存在某个属性来查找:

soup.select('a[href]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过属性的值来查找:

soup.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select('a[href^="http://example.com/"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href*=".com/el"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

通过语言设置来查找:

multilingual_markup = """
 <p lang="en">Hello</p>
 <p lang="en-us">Howdy, y'all</p>
 <p lang="en-gb">Pip-pip, old fruit</p>
 <p lang="fr">Bonjour mes amis</p>
"""
multilingual_soup = BeautifulSoup(multilingual_markup)
multilingual_soup.select('p[lang|=en]')
# [<p lang="en">Hello</p>,
#  <p lang="en-us">Howdy, y'all</p>,
#  <p lang="en-gb">Pip-pip, old fruit</p>]

对于熟悉CSS选择器语法的人来说这是个非常方便的方法.Beautiful Soup也支持CSS选择器API,如果你仅仅需要CSS选择器的功能,那么直接使用 lxml 也可以,而且速度更快,支持更多的CSS选择器语法,但Beautiful Soup整合了CSS选择器的语法和自身方便使用API.

修改文档树

Beautiful Soup的强项是文档树的搜索,但同时也可以方便的修改文档树

修改tag的名称和属性

在 Attributes 的章节中已经介绍过这个功能,但是再看一遍也无妨. 重命名一个tag,改变属性的值,添加或删除属性:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b

tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

修改 .string

给tag的 .string 属性赋值,就相当于用当前的内容替代了原来的内容:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)

tag = soup.a
tag.string = "New link text."
tag
# <a href="http://example.com/">New link text.</a>

注意: 如果当前的tag包含了其它tag,那么给它的 .string 属性赋值会覆盖掉原有的所有内容包括子tag

append()

Tag.append() 方法想tag中添加内容,就好像Python的列表的 .append() 方法:

soup = BeautifulSoup("<a>Foo</a>")
soup.a.append("Bar")

soup
# <html><head></head><body><a>FooBar</a></body></html>
soup.a.contents
# [u'Foo', u'Bar']

BeautifulSoup.new_string() 和 .new_tag()

如果想添加一段文本内容到文档中也没问题,可以调用Python的 append() 方法或调用工厂方法 BeautifulSoup.new_string() :

soup = BeautifulSoup("<b></b>")
tag = soup.b
tag.append("Hello")
new_string = soup.new_string(" there")
tag.append(new_string)
tag
# <b>Hello there.</b>
tag.contents
# [u'Hello', u' there']

如果想要创建一段注释,或 NavigableString 的任何子类,将子类作为 new_string() 方法的第二个参数传入:

from bs4 import Comment
new_comment = soup.new_string("Nice to see you.", Comment)
tag.append(new_comment)
tag
# <b>Hello there<!--Nice to see you.--></b>
tag.contents
# [u'Hello', u' there', u'Nice to see you.']

# 这是Beautiful Soup 4.2.1 中新增的方法

创建一个tag最好的方法是调用工厂方法 BeautifulSoup.new_tag() :

soup = BeautifulSoup("<b></b>")
original_tag = soup.b

new_tag = soup.new_tag("a", href="http://www.example.com")
original_tag.append(new_tag)
original_tag
# <b><a href="http://www.example.com"></a></b>

new_tag.string = "Link text."
original_tag
# <b><a href="http://www.example.com">Link text.</a></b>

第一个参数作为tag的name,是必填,其它参数选填

insert()

Tag.insert() 方法与 Tag.append() 方法类似,区别是不会把新元素添加到父节点 .contents 属性的最后,而是把元素插入到指定的位置.与Python列表总的 .insert() 方法的用法下同:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.insert(1, "but did not endorse ")
tag
# <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
tag.contents
# [u'I linked to ', u'but did not endorse', <i>example.com</i>]

insert_before() 和 insert_after()

insert_before() 方法在当前tag或文本节点前插入内容:

soup = BeautifulSoup("<b>stop</b>")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# <b><i>Don't</i>stop</b>

insert_after() 方法在当前tag或文本节点后插入内容:

soup.b.i.insert_after(soup.new_string(" ever "))
soup.b
# <b><i>Don't</i> ever stop</b>
soup.b.contents
# [<i>Don't</i>, u' ever ', u'stop']

clear()

Tag.clear() 方法移除当前tag的内容:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.clear()
tag
# <a href="http://example.com/"></a>

extract()

PageElement.extract() 方法将当前tag移除文档树,并作为方法结果返回:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

i_tag = soup.i.extract()

a_tag
# <a href="http://example.com/">I linked to</a>

i_tag
# <i>example.com</i>

print(i_tag.parent)
None

这个方法实际上产生了2个文档树: 一个是用来解析原始文档的 BeautifulSoup 对象,另一个是被移除并且返回的tag.被移除并返回的tag可以继续调用 extract 方法:

my_string = i_tag.string.extract()
my_string
# u'example.com'

print(my_string.parent)
# None
i_tag
# <i></i>

decompose()

Tag.decompose() 方法将当前节点移除文档树并完全销毁:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

soup.i.decompose()

a_tag
# <a href="http://example.com/">I linked to</a>

replace_with()

PageElement.replace_with() 方法移除文档树中的某段内容,并用新tag或文本节点替代它:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

new_tag = soup.new_tag("b")
new_tag.string = "example.net"
a_tag.i.replace_with(new_tag)

a_tag
# <a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 方法返回被替代的tag或文本节点,可以用来浏览或添加到文档树其它地方

wrap()

PageElement.wrap() 方法可以对指定的tag元素进行包装 [8] ,并返回包装后的结果:

soup = BeautifulSoup("<p>I wish I was bold.</p>")
soup.p.string.wrap(soup.new_tag("b"))
# <b>I wish I was bold.</b>

soup.p.wrap(soup.new_tag("div"))
# <div><p><b>I wish I was bold.</b></p></div>

该方法在 Beautiful Soup 4.0.5 中添加

unwrap()

Tag.unwrap() 方法与 wrap() 方法相反.将移除tag内的所有tag标签,该方法常被用来进行标记的解包:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

a_tag.i.unwrap()
a_tag
# <a href="http://example.com/">I linked to example.com</a>

与 replace_with() 方法相同, unwrap() 方法返回被移除的tag

输出

格式化输出

prettify() 方法将Beautiful Soup的文档树格式化后以Unicode编码输出,每个XML/HTML标签都独占一行

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n  <a href="http://example.com/">\n...'

print(soup.prettify())
# <html>
#  <head>
#  </head>
#  <body>
#   <a href="http://example.com/">
#    I linked to
#    <i>
#     example.com
#    </i>
#   </a>
#  </body>
# </html>

BeautifulSoup 对象和它的tag节点都可以调用 prettify() 方法:

print(soup.a.prettify())
# <a href="http://example.com/">
#  I linked to
#  <i>
#   example.com
#  </i>
# </a>

压缩输出

如果只想得到结果字符串,不重视格式,那么可以对一个 BeautifulSoup 对象或 Tag 对象使用Python的 unicode() 或 str() 方法:

str(soup)
# '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

unicode(soup.a)
# u'<a href="http://example.com/">I linked to <i>example.com</i></a>'

str() 方法返回UTF-8编码的字符串,可以指定 编码 的设置.

还可以调用 encode() 方法获得字节码或调用 decode() 方法获得Unicode.

输出格式

Beautiful Soup输出是会将HTML中的特殊字符转换成Unicode,比如“&lquot;”:

soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'

如果将文档转换成字符串,Unicode编码会被编码成UTF-8.这样就无法正确显示HTML特殊字符了:

str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'

get_text()

如果只想得到tag中包含的文本内容,那么可以嗲用 get_text() 方法,这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回:

markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)

soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'

可以通过参数指定tag的文本内容的分隔符:

# soup.get_text("|")
u'\nI linked to |example.com|\n'

还可以去除获得文本内容的前后空白:

# soup.get_text("|", strip=True)
u'I linked to|example.com'

或者使用 .stripped_strings 生成器,获得文本列表后手动处理列表:

[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']

指定文档解析器

如果仅是想要解析HTML文档,只要用文档创建 BeautifulSoup 对象就可以了.Beautiful Soup会自动选择一个解析器来解析文档.但是还可以通过参数指定使用那种解析器来解析当前文档.

BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄,第二个参数用来标识怎样解析文档.如果第二个参数为空,那么Beautiful Soup根据当前系统安装的库自动选择解析器,解析器的优先数序: lxml, html5lib, Python标准库.在下面两种条件下解析器优先顺序会变化:

  • 要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
  • 指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”

安装解析器 章节介绍了可以使用哪种解析器,以及如何安装.

如果指定的解析器没有安装,Beautiful Soup会自动选择其它方案.目前只有 lxml 解析器支持XML文档的解析,在没有安装lxml库的情况下,创建 beautifulsoup 对象时无论是否指定使用lxml,都无法得到解析后的对象

解析器之间的区别

Beautiful Soup为不同的解析器提供了相同的接口,但解析器本身时有区别的.同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档.区别最大的是HTML解析器和XML解析器,看下面片段被解析成HTML结构:

BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>

因为空标签<b />不符合HTML标准,所以解析器把它解析成<b></b>

同样的文档使用XML解析如下(解析XML需要安装lxml库).注意,空标签<b />依然被保留,并且文档前添加了XML头,而不是被包含在<html>标签内:

BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>

HTML解析器之间也有区别,如果被解析的HTML文档是标准格式,那么解析器之间没有任何差别,只是解析速度不同,结果都会返回正确的文档树.

但是如果被解析文档不是标准格式,那么不同的解析器返回结果可能不同.下面例子中,使用lxml解析错误格式的文档,结果</p>标签被直接忽略掉了:

BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>

使用html5lib库解析相同文档会得到不同的结果:

BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>

html5lib库没有忽略掉</p>标签,而是自动补全了标签,还给文档树添加了<head>标签.

使用pyhton内置库解析结果如下:

BeautifulSoup("<a></p>", "html.parser")
# <a></a>

与lxml [7] 库类似的,Python内置库忽略掉了</p>标签,与html5lib库不同的是标准库没有尝试创建符合标准的文档格式或将文档片段包含在<body>标签内,与lxml不同的是标准库甚至连<html>标签都没有尝试去添加.

因为文档片段“<a></p>”是错误格式,所以以上解析方式都能算作”正确”,html5lib库使用的是HTML5的部分标准,所以最接近”正确”.不过所有解析器的结构都能够被认为是”正常”的.

不同的解析器可能影响代码执行结果,如果在分发给别人的代码中使用了 BeautifulSoup ,那么最好注明使用了哪种解析器,以减少不必要的麻烦.

编码

任何HTML或XML文档都有自己的编码方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文档都被转换成了Unicode:

markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'

这不是魔术(但很神奇),Beautiful Soup用了 编码自动检测 子库来识别当前文档编码并转换成Unicode编码. BeautifulSoup 对象的 .original_encoding 属性记录了自动识别编码的结果:

soup.original_encoding
'utf-8'

编码自动检测 功能大部分时候都能猜对编码格式,但有时候也会出错.有时候即使猜测正确,也是在逐个字节的遍历整个文档后才猜对的,这样很慢.如果预先知道文档编码,可以设置编码参数来减少自动检查编码出错的概率并且提高文档解析速度.在创建 BeautifulSoup 对象的时候设置 from_encoding 参数.

下面一段文档用了ISO-8859-8编码方式,这段文档太短,结果Beautiful Soup以为文档是用ISO-8859-7编码:

markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
'ISO-8859-7'

通过传入 from_encoding 参数来指定编码方式:

soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>םולש</h1>
soup.original_encoding
'iso8859-8'

少数情况下(通常是UTF-8编码的文档中包含了其它编码格式的文件),想获得正确的Unicode编码就不得不将文档中少数特殊编码字符替换成特殊Unicode编码,“REPLACEMENT CHARACTER” (U+FFFD, �) [9] . 如果Beautifu Soup猜测文档编码时作了特殊字符的替换,那么Beautiful Soup会把 UnicodeDammit 或 BeautifulSoup 对象的 .contains_replacement_characters 属性标记为 True .这样就可以知道当前文档进行Unicode编码后丢失了一部分特殊内容字符.如果文档中包含�而 .contains_replacement_characters 属性是 False ,则表示�就是文档中原来的字符,不是转码失败.

输出编码

通过Beautiful Soup输出文档时,不管输入文档是什么编码方式,输出编码均为UTF-8编码,下面例子输入文档是Latin-1编码:

markup = b'''
<html>
  <head>
    <meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
  </head>
  <body>
    <p>Sacr\xe9 bleu!</p>
  </body>
</html>
'''

soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
#  <head>
#   <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
#  </head>
#  <body>
#   <p>
#    Sacré bleu!
#   </p>
#  </body>
# </html>

注意,输出文档中的<meta>标签的编码设置已经修改成了与输出编码一致的UTF-8.

如果不想用UTF-8编码输出,可以将编码方式传入 prettify() 方法:

print(soup.prettify("latin-1"))
# <html>
#  <head>
#   <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...

还可以调用 BeautifulSoup 对象或任意节点的 encode() 方法,就像Python的字符串调用 encode() 方法一样:

soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'

soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'

如果文档中包含当前编码不支持的字符,那么这些字符将呗转换成一系列XML特殊字符引用,下面例子中包含了Unicode编码字符SNOWMAN:

markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b

SNOWMAN字符在UTF-8编码中可以正常显示(看上去像是☃),但有些编码不支持SNOWMAN字符,比如ISO-Latin-1或ASCII,那么在这些编码中SNOWMAN字符会被转换成“&#9731”:

print(tag.encode("utf-8"))
# <b>☃</b>

print tag.encode("latin-1")
# <b>&#9731;</b>

print tag.encode("ascii")
# <b>&#9731;</b>

Unicode, dammit! (靠!)

编码自动检测 功能可以在Beautiful Soup以外使用,检测某段未知编码时,可以使用这个方法:

from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'

如果Python中安装了 chardet 或 cchardet 那么编码检测功能的准确率将大大提高.输入的字符越多,检测结果越精确,如果事先猜测到一些可能编码,那么可以将猜测的编码作为参数,这样将优先检测这些编码:

dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'

编码自动检测 功能中有2项功能是Beautiful Soup库中用不到的

智能引号

使用Unicode时,Beautiful Soup还会智能的把引号 [10] 转换成HTML或XML中的特殊字符:

markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

也可以把引号转换为ASCII码:

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'

很有用的功能,但是Beautiful Soup没有使用这种方式.默认情况下,Beautiful Soup把引号转换成Unicode:

UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'

矛盾的编码

有时文档的大部分都是用UTF-8,但同时还包含了Windows-1252编码的字符,就像微软的智能引号 [10] 一样.一些包含多个信息的来源网站容易出现这种情况. UnicodeDammit.detwingle() 方法可以把这类文档转换成纯UTF-8编码格式,看个简单的例子:

snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")

这段文档很杂乱,snowmen是UTF-8编码,引号是Windows-1252编码,直接输出时不能同时显示snowmen和引号,因为它们编码不同:

print(doc)
# ☃☃☃�I like snowmen!�

print(doc.decode("windows-1252"))
# ☃☃☃“I like snowmen!”

如果对这段文档用UTF-8解码就会得到 UnicodeDecodeError 异常,如果用Windows-1252解码就回得到一堆乱码.幸好, UnicodeDammit.detwingle() 方法会吧这段字符串转换成UTF-8编码,允许我们同时显示出文档中的snowmen和引号:

new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”

UnicodeDammit.detwingle() 方法只能解码包含在UTF-8编码中的Windows-1252编码内容,但这解决了最常见的一类问题.

在创建 BeautifulSoup 或 UnicodeDammit 对象前一定要先对文档调用 UnicodeDammit.detwingle() 确保文档的编码方式正确.如果尝试去解析一段包含Windows-1252编码的UTF-8文档,就会得到一堆乱码,比如: ☃☃☃“I like snowmen!”.

UnicodeDammit.detwingle() 方法在Beautiful Soup 4.1.0版本中新增

解析部分文档

如果仅仅因为想要查找文档中的<a>标签而将整片文档进行解析,实在是浪费内存和时间.最快的方法是从一开始就把<a>标签以外的东西都忽略掉. SoupStrainer 类可以定义文档的某段内容,这样搜索文档时就不必先解析整篇文档,只会解析在 SoupStrainer 中定义过的文档. 创建一个 SoupStrainer 对象并作为 parse_only 参数给 BeautifulSoup 的构造方法即可.

SoupStrainer

SoupStrainer 类接受与典型搜索方法相同的参数:name , attrs , recursive , text , **kwargs 。下面举例说明三种 SoupStrainer 对象:

from bs4 import SoupStrainer

only_a_tags = SoupStrainer("a")

only_tags_with_id_link2 = SoupStrainer(id="link2")

def is_short_string(string):
    return len(string) < 10

only_short_strings = SoupStrainer(text=is_short_string)

再拿“爱丽丝”文档来举例,来看看使用三种 SoupStrainer 对象做参数会有什么不同:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
# <a class="sister" href="http://example.com/elsie" id="link1">
#  Elsie
# </a>
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>
# <a class="sister" href="http://example.com/tillie" id="link3">
#  Tillie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
# Elsie
# ,
# Lacie
# and
# Tillie
# ...
#

还可以将 SoupStrainer 作为参数传入 搜索文档树 中提到的方法.这可能不是个常用用法,所以还是提一下:

soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
#  u'\n\n', u'...', u'\n']

常见问题

代码诊断

如果想知道Beautiful Soup到底怎样处理一份文档,可以将文档传入 diagnose() 方法(Beautiful Soup 4.2.0中新增),Beautiful Soup会输出一份报告,说明不同的解析器会怎样处理这段文档,并标出当前的解析过程会使用哪种解析器:

from bs4.diagnose import diagnose
data = open("bad.html").read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug  1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...

diagnose() 方法的输出结果可能帮助你找到问题的原因,如果不行,还可以把结果复制出来以便寻求他人的帮助

文档解析错误

文档解析错误有两种.一种是崩溃,Beautiful Soup尝试解析一段文档结果却抛除了异常,通常是 HTMLParser.HTMLParseError .还有一种异常情况,是Beautiful Soup解析后的文档树看起来与原来的内容相差很多.

这些错误几乎都不是Beautiful Soup的原因,这不会是因为Beautiful Soup得代码写的太优秀,而是因为Beautiful Soup没有包含任何文档解析代码.异常产生自被依赖的解析器,如果解析器不能很好的解析出当前的文档,那么最好的办法是换一个解析器.更多细节查看 安装解析器 章节.

最常见的解析错误是 HTMLParser.HTMLParseError: malformed start tag 和 HTMLParser.HTMLParseError: bad end tag .这都是由Python内置的解析器引起的,解决方法是 安装lxml或html5lib

最常见的异常现象是当前文档找不到指定的Tag,而这个Tag光是用眼睛就足够发现的了. find_all() 方法返回 [] ,而 find() 方法返回 None .这是Python内置解析器的又一个问题: 解析器会跳过那些它不知道的tag.解决方法还是 安装lxml或html5lib

版本错误

  • SyntaxError: Invalid syntax (异常位置在代码行: ROOT_TAG_NAME = u'[document]' ),因为Python2版本的代码没有经过迁移就在Python3中窒息感
  • ImportError: No module named HTMLParser 因为在Python3中执行Python2版本的Beautiful Soup
  • ImportError: No module named html.parser 因为在Python2中执行Python3版本的Beautiful Soup
  • ImportError: No module named BeautifulSoup 因为在没有安装BeautifulSoup3库的Python环境下执行代码,或忘记了BeautifulSoup4的代码需要从 bs4 包中引入
  • ImportError: No module named bs4 因为当前Python环境下还没有安装BeautifulSoup4

解析成XML

默认情况下,Beautiful Soup会将当前文档作为HTML格式解析,如果要解析XML文档,要在 BeautifulSoup 构造方法中加入第二个参数 “xml”:

soup = BeautifulSoup(markup, "xml")

当然,还需要 安装lxml

解析器的错误

  • 如果同样的代码在不同环境下结果不同,可能是因为两个环境下使用不同的解析器造成的.例如这个环境中安装了lxml,而另一个环境中只有html5lib, 解析器之间的区别 中说明了原因.修复方法是在 BeautifulSoup 的构造方法中中指定解析器
  • 因为HTML标签是 大小写敏感 的,所以3种解析器再出来文档时都将tag和属性转换成小写.例如文档中的 <TAG></TAG> 会被转换为 <tag></tag> .如果想要保留tag的大写的话,那么应该将文档 解析成XML .

杂项错误

  • UnicodeEncodeError: 'charmap' codec can't encode character u'\xfoo' in position bar (或其它类型的 UnicodeEncodeError )的错误,主要是两方面的错误(都不是Beautiful Soup的原因),第一种是正在使用的终端(console)无法显示部分Unicode,参考 Python wiki ,第二种是向文件写入时,被写入文件不支持部分Unicode,这时只要用 u.encode("utf8") 方法将编码转换为UTF-8.
  • KeyError: [attr] 因为调用 tag['attr'] 方法而引起,因为这个tag没有定义该属性.出错最多的是 KeyError: 'href' 和 KeyError: 'class' .如果不确定某个属性是否存在时,用 tag.get('attr') 方法去获取它,跟获取Python字典的key一样
  • AttributeError: 'ResultSet' object has no attribute 'foo' 错误通常是因为把 find_all() 的返回结果当作一个tag或文本节点使用,实际上返回结果是一个列表或 ResultSet 对象的字符串,需要对结果进行循环才能得到每个节点的 .foo 属性.或者使用 find() 方法仅获取到一个节点
  • AttributeError: 'NoneType' object has no attribute 'foo' 这个错误通常是在调用了 find() 方法后直节点取某个属性 .foo 但是 find() 方法并没有找到任何结果,所以它的返回值是 None .需要找出为什么 find() 的返回值是 None .

如何提高效率

Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml .

换句话说,还有提高Beautiful Soup效率的办法,使用lxml作为解析器.Beautiful Soup用lxml做解析器比用html5lib或Python内置解析器速度快很多.

安装 cchardet 后文档的解码的编码检测会速度更快

解析部分文档 不会节省多少解析时间,但是会节省很多内存,并且搜索时也会变得更快.

Beautiful Soup 3

Beautiful Soup 3是上一个发布版本,目前已经停止维护.Beautiful Soup 3库目前已经被几个主要的linux平台添加到源里:

$ apt-get install Python-beautifulsoup

在PyPi中分发的包名字是 BeautifulSoup :

$ easy_install BeautifulSoup

$ pip install BeautifulSoup

或通过 Beautiful Soup 3.2.0源码包 安装

Beautiful Soup 3的在线文档查看 这里 ,当然还有 中文版 ,然后再读本片文档,来对比Beautiful Soup 4中有什新变化.

迁移到BS4

只要一个小变动就能让大部分的Beautiful Soup 3代码使用Beautiful Soup 4的库和方法—-修改 BeautifulSoup 对象的引入方式:

from BeautifulSoup import BeautifulSoup

修改为:

from bs4 import BeautifulSoup
  • 如果代码抛出 ImportError 异常“No module named BeautifulSoup”,原因可能是尝试执行Beautiful Soup 3,但环境中只安装了Beautiful Soup 4库
  • 如果代码跑出 ImportError 异常“No module named bs4”,原因可能是尝试运行Beautiful Soup 4的代码,但环境中只安装了Beautiful Soup 3.

虽然BS4兼容绝大部分BS3的功能,但BS3中的大部分方法已经不推荐使用了,就方法按照 PEP8标准 重新定义了方法名.很多方法都重新定义了方法名,但只有少数几个方法没有向下兼容.

上述内容就是BS3迁移到BS4的注意事项

需要的解析器

Beautiful Soup 3曾使用Python的 SGMLParser 解析器,这个模块在Python3中已经被移除了.Beautiful Soup 4默认使用系统的 html.parser ,也可以使用lxml或html5lib扩展库代替.查看 安装解析器 章节

因为 html.parser 解析器与 SGMLParser 解析器不同,它们在处理格式不正确的文档时也会产生不同结果.通常 html.parser 解析器会抛出异常.所以推荐安装扩展库作为解析器.有时 html.parser 解析出的文档树结构与 SGMLParser 的不同.如果发生这种情况,那么需要升级BS3来处理新的文档树.

方法名的变化

  • renderContents -> encode_contents
  • replaceWith -> replace_with
  • replaceWithChildren -> unwrap
  • findAll -> find_all
  • findAllNext -> find_all_next
  • findAllPrevious -> find_all_previous
  • findNext -> find_next
  • findNextSibling -> find_next_sibling
  • findNextSiblings -> find_next_siblings
  • findParent -> find_parent
  • findParents -> find_parents
  • findPrevious -> find_previous
  • findPreviousSibling -> find_previous_sibling
  • findPreviousSiblings -> find_previous_siblings
  • nextSibling -> next_sibling
  • previousSibling -> previous_sibling

Beautiful Soup构造方法的参数部分也有名字变化:

  • BeautifulSoup(parseOnlyThese=...) -> BeautifulSoup(parse_only=...)
  • BeautifulSoup(fromEncoding=...) -> BeautifulSoup(from_encoding=...)

为了适配Python3,修改了一个方法名:

  • Tag.has_key() -> Tag.has_attr()

修改了一个属性名,让它看起来更专业点:

  • Tag.isSelfClosing -> Tag.is_empty_element

修改了下面3个属性的名字,以免雨Python保留字冲突.这些变动不是向下兼容的,如果在BS3中使用了这些属性,那么在BS4中这些代码无法执行.

  • UnicodeDammit.Unicode -> UnicodeDammit.Unicode_markup“
  • Tag.next -> Tag.next_element
  • Tag.previous -> Tag.previous_element

生成器

将下列生成器按照PEP8标准重新命名,并转换成对象的属性:

  • childGenerator() -> children
  • nextGenerator() -> next_elements
  • nextSiblingGenerator() -> next_siblings
  • previousGenerator() -> previous_elements
  • previousSiblingGenerator() -> previous_siblings
  • recursiveChildGenerator() -> descendants
  • parentGenerator() -> parents

所以迁移到BS4版本时要替换这些代码:

for parent in tag.parentGenerator():
    ...

替换为:

for parent in tag.parents:
    ...

(两种调用方法现在都能使用)

BS3中有的生成器循环结束后会返回 None 然后结束.这是个bug.新版生成器不再返回 None .

BS4中增加了2个新的生成器, .strings 和 stripped_strings . .strings 生成器返回NavigableString对象, .stripped_strings 方法返回去除前后空白的Python的string对象.

XML

BS4中移除了解析XML的 BeautifulStoneSoup 类.如果要解析一段XML文档,使用 BeautifulSoup 构造方法并在第二个参数设置为“xml”.同时 BeautifulSoup 构造方法也不再识别 isHTML 参数.

Beautiful Soup处理XML空标签的方法升级了.旧版本中解析XML时必须指明哪个标签是空标签. 构造方法的 selfClosingTags 参数已经不再使用.新版Beautiful Soup将所有空标签解析为空元素,如果向空元素中添加子节点,那么这个元素就不再是空元素了.

实体

HTML或XML实体都会被解析成Unicode字符,Beautiful Soup 3版本中有很多处理实体的方法,在新版中都被移除了. BeautifulSoup 构造方法也不再接受 smartQuotesTo 或 convertEntities 参数. 编码自动检测 方法依然有 smart_quotes_to 参数,但是默认会将引号转换成Unicode.内容配置项 HTML_ENTITIES , XML_ENTITIES 和 XHTML_ENTITIES 在新版中被移除.因为它们代表的特性已经不再被支持.

如果在输出文档时想把Unicode字符转换成HTML实体,而不是输出成UTF-8编码,那就需要用到 输出格式 的方法.

迁移杂项

Tag.string 属性现在是一个递归操作.如果A标签只包含了一个B标签,那么A标签的.string属性值与B标签的.string属性值相同.

多值属性 比如 class 属性包含一个他们的值的列表,而不是一个字符串.这可能会影响到如何按照CSS类名哦搜索tag.

如果使用 find* 方法时同时传入了 text 参数 和 name 参数 .Beautiful Soup会搜索指定name的tag,并且这个tag的 Tag.string 属性包含text参数的内容.结果中不会包含字符串本身.旧版本中Beautiful Soup会忽略掉tag参数,只搜索text参数.

BeautifulSoup 构造方法不再支持 markupMassage 参数.现在由解析器负责文档的解析正确性.

很少被用到的几个解析器方法在新版中被移除,比如 ICantBelieveItsBeautifulSoup 和 BeautifulSOAP .现在由解析器完全负责如何解释模糊不清的文档标记.

prettify() 方法在新版中返回Unicode字符串,不再返回字节流.

BeautifulSoup3 文档

[1] BeautifulSoup的google讨论组不是很活跃,可能是因为库已经比较完善了吧,但是作者还是会很热心的尽量帮你解决问题的.
[2] (12) 文档被解析成树形结构,所以下一步解析过程应该是当前节点的子节点
[3] 过滤器只能作为搜索文档的参数,或者说应该叫参数类型更为贴切,原文中用了 filter 因此翻译为过滤器
[4] 元素参数,HTML文档中的一个tag节点,不能是文本节点
[5] (12345) 采用先序遍历方式
[6] (12) CSS选择器是一种单独的文档搜索语法, 参考 http://www.w3school.com.cn/css/css_selector_type.asp
[7] 原文写的是 html5lib, 译者觉得这是愿文档的一个笔误
[8] wrap含有包装,打包的意思,但是这里的包装不是在外部包装而是将当前tag的内部内容包装在一个tag里.包装原来内容的新tag依然在执行 wrap() 方法的tag内
[9] 文档中特殊编码字符被替换成特殊字符(通常是�)的过程是Beautful Soup自动实现的,如果想要多种编码格式的文档被完全转换正确,那么,只好,预先手动处理,统一编码格式
[10] (12) 智能引号,常出现在microsoft的word软件中,即在某一段落中按引号出现的顺序每个引号都被自动转换为左引号,或右引号.

Table Of Contents

This Page