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

 

利用BeautifulSoup的find_all()函数查找某个标签且该标签某属性不出现

 

HTML代码如下:

<ul class=”sf-r-list”>
<li>
<a href=”/book/77″ class=”sc-list-cover fl”>
<img class=”ba_page_prvimg” onload=”baImgCenter(this)” badt_outwidth=”” src=”https://wqxuetang.oss-cn-beijing.aliyuncs.com/cover/0/0/77/77.jpg!m”>
</a>
<div class=”sf-r-info”>
<h3 class=”sf-r-infotit”><a href=”/book/77″ class=”ellipsis-2″>Android多媒体开发高级编程——为智能手机和平板电脑开发图形、音乐、视频和富媒体应用</a>
</h3>
<p class=”sf-r-infoau text-truncate”> (美) 艾佛瑞 (Every,S.V.)…</p>
<p class=”sf-r-infotext ellipsis-2″>《Android多媒体开发高级编程》使用清晰、直观的示例介绍了Android SDK中丰富的多媒体功能,有助于您开发能够创建、播放和共享多媒体的优秀Android应用程序。许多Android设备本身就是照相机、相册、…</p>
<div class=”sf-r-infoprice”>
<span style=”color:#8E9AA6;”> 暂不销售</span>
</div>
</div>
</li>
…<!– 与前述<li> .. </li>类同 –>

<li style=”float:none;margin:0;display:block;clear:both;”></li>
</ul>
需要把<ul> …</ul直接的列表项中的信息抓取出来,其中每个列表项包含书籍的名称、作者、简介和价格信息。我最初使用的代码如下:

from bs4 import BeautifulSoup
from urllib.request import urlopen

html = urlopen(‘https://wqbook.wqxuetang.com/category/tid_2004/pid_/pn_1014.html’)
bs = BeautifulSoup(html,’lxml’)
book_ul = bs.find(“ul”,class_=”sf-r-list”)
book_lis = book_ul.find_all(“li”)
for item in book_lis:
print(item.prettify())
但是抓取的列表项中总会包含:

<li style=”float:none;margin:0;display:block;clear:both;”></li>
1
当时在BeautifulSoup的函数find_all()中的参数上想了一些办法,因为我预感对标签的筛选肯定在参数中有所体现。可是都不成功。在我读到的爬虫书籍中,对此场景的技术方案都没讲解。无奈最后我采用了笨方法将<li style=”float:none;margin:0;display:block;clear:both;”></li>剔除出去,代码如下:

from bs4 import BeautifulSoup
from urllib.request import urlopen

html = urlopen(‘https://wqbook.wqxuetang.com/category/tid_2004/pid_/pn_1014.html’)
bs = BeautifulSoup(html,’lxml’)
book_ul = bs.find(“ul”,class_=”sf-r-list”)
book_lis = book_ul.find_all(“li”)
for item in book_lis:
if book_li.find(“a”):
print(item.prettify())
亦即,我是通过判断在标签<li></li>是否包含子标签<a></a>来完成的。当时也顺利完成了爬虫的功能。但这个问题在我脑中记录下来了。直到今天,读书籍《Web Scraping with Python》第二版1的85页代码时,发现:

downloadList = bs.findAll(src=True)
1
受启发,可以用到本文场景中。当时书上也没讲解src=True的含义。

优雅的解决方案
在BeautifulSoup的函数find_all()中的参数中设置某个属性值为False或True,允许我们在匹配时控制某个属性在标签中是否出现,以此来匹配查找。于是,本应用场景的优美解决方案为:

# scrapeBookInfoUnitTest.py
# 2020-08-20

from bs4 import BeautifulSoup
from urllib.request import urlopen

html = urlopen(‘https://wqbook.wqxuetang.com/category/tid_2004/pid_/pn_1014.html’)
bs = BeautifulSoup(html,’lxml’)
book_ul = bs.find(“ul”,class_=”sf-r-list”)
#book_lis = book_ul.find_all(“li”)
book_lis = book_ul.find_all(“li”,style=False)
for item in book_lis:

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

为了将comments下的所有用户评论选出来

每一个<li>标签代表一个用户的评论,就直接用find_all(‘li’)了

但发现这里有21项,但数了数发现只有20个用户,原来啊是这里出问题了:

这个li标签是用户评论下的一张图片。

但我们不想选到这个,观察一下,用户的li含有“data-id”和“id”属性,而图片没有,如下图:

 

于是就去百度找了一下“beautifulsoup find_all怎样把带有某种属性的标签选出而不含该属性的标签不选”但没找到结果,

先是试了试在find_all加入属性,想匹配出含有”data-id”属性的“li”标签,但试了几个都出问题,就感觉这个方法行不通。

 

最后通过翻阅beautifulsoup官方文档,发现一个find_all传方法:

于是自己也写了一个方法,正好把所有符合条件的都选了出来了

1 soup = BeautifulSoup(open(comment_file,encoding='utf-8'),'lxml')
2 comments = soup.select('div.comment-list')[0]
3 comments = comments.find_all(lambda tag:tag.has_attr('data-id') and tag.has_attr('id'))

如下

后来又阅读了一下官方文档,

 

发现这些用户的li都含有id属性,且“id”均含“rev_”开头,所以试了下正则表达式:

也正确的把他们都选出来了!

 

时隔半年再来写这个小爬虫,还是挺吃力啊!所以还是学了之后要多用,多巩固,才能迎刃而解!

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.  

GitHub上有哪些优秀的爬虫项目

0

1

写在前面的话

今天JAP君给大家安利一波福利!GitHub上优秀的爬虫项目大集合!!!大家赶快收藏一波!

2

福利开始喽!

1. Anti-Anti-Spider

地址:https://github.com/luyishisi/Anti-Anti-Spider

很全面的反爬虫项目大全:

1:验证码 {亚马逊验证码破解,knn,svm,Tensorflow自动生成验证码并大量训练从而破解–98%成功率}

2:代理 {抓取西刺代理,以及一个高可用的国外代理网站,并存入数据库,从而随时调用}

3:代码模板 {多线程优化,百度地图可视化采集,聚焦爬虫,selenium模拟登陆,域名爬虫}

5:爬虫项目源码 {优酷网,腾讯视频,推特,拉钩网,百度地图,妹子图网,百家号,百度百科,csdn,新浪微博, 淘宝采集}

6:ip更换技术 {代理,tor,adsl}

7:请求伪造 {phantomjs,requests,selenium}

8:phantomjs {伪造请求头,获取页面截图,获取页面源码,设置超时}

9:selenium {伪造请求头,支付宝模拟登陆}

10:UrlSpider {项目中常用的采集代码样本,经过多线程数据库操作优化,最高速度6kw/d}

2. awesome-spider

网址:https://github.com/facert/awesome-spide

这是一个id为facert的知乎工程师开源的项目,目前见过最详细最多的爬虫案例大全了,真的值得大家去学习一波!

Brigtdata,旧名Luminati 目前是海外最牛的全球真人住宅IP代理。除了有点儿小贵,没毛病,成功率99%。 现在在搞优惠活动,需要高质量的稳定代理的可以考虑一下,使用任何套餐的客户都可以送150-250美金. 具体点击链接注册后根据邮件联系中文客服哦。


收集各种爬虫 (默认爬虫语言为 python), 欢迎大家 提 pr 或 issue, 收集脚本见此项目 github-search

warning: 爬虫有时效性,如没法直接运行,请适当更改逻辑。

3. Nyspider

网址:https://github.com/Nyloner/Nyspider

这是ID为Nyloner的一个今日头条的工程师弄的,star1000+,风格与上面的项目大有不同。

可以看出,都是各类网址,和本人的工作有关。

4. awesome-python-login-model

网址:https://github.com/CriseLYJ/awesome-python-login-model

这是ID为CriseLYJ(职业不详)的用户,这个项目用于模拟各种网址登陆,也包含一些简单的爬虫,star6000+。

5. python-spider

网址:https://github.com/Jack-Cherish/python-spider

这是ID为Jack-Cherish的东北大学的一个学生整理的学习python爬虫的资料,star6000+,包含不少的实战项目,非常适合想学习的朋友。

6. Google,Baidu,Bing三大搜素引擎图片爬虫

网址:https://github.com/sczhengyabin/Image-Downloader

这个爬虫足够满足小型项目初始数据集的积累,结果命名也非常整齐规范,最大的优点是稳定。

3

END

github访问加速

  1. 使用镜像网站
  2. 使用代理网站下载
  3. cdn加速
  4. 转入gitee加速

概括:

如果是下载比较大的项目,比如耗时5min往上,大小30mb往上,十分推荐使用代理网站下载,或者转入gitee的方式下载.

如果仅仅是下载比较小的项目,类似代码性质,文档性质的项目,使用cdn加速,提升到100多KB/s也就够用了

一.使用镜像网站

一共有三种加速的方式

  1. 使用github的镜像网站cnpmjs.org

原地址:

github.com/xxx.git

替换为:

github.com.cnpmjs.org/x

示例:

git clone github.com.cnpmjs.org/x

说白了,就在github.com后面加个cnpmjs.org即可

2. 使用github的镜像网站 hub.fastgit.org/ 进行搜索

上面两种的缺点: 每次访问页面都需要手动添加一次cnpmjs.org,如果访问的时候出现了Whoa there!字样,一般刷新10几次页面就可以了.

二. 使用代理网站下载

对于github release中下载的大文件

使用toolwa.com/github/来下载,速度起飞,无需注册,亲测有效


三. cdn加速

通过修改系统hosts文件的办法,绕过国内dns解析,直接访问GitHub的CDN节点,从而达到github访问加速的目的。不需要海外的服务器辅助。

GitHub在国内访问速度慢的问题原因有很多,但最直接和最主要的原因是GitHub的分发加速网络的域名遭到dns污染,下载网站上任何东西的时候会下半天,有时还会失败需要从头再来,多失败了几次又因访问次数过多被做了ip限制,让人恼火

做到以上需要三步

  1. 获取GitHub官方CDN地址
  2. 修改系统Hosts文件
  3. 刷新系统DNS缓存

1. 获取GitHub官方CDN地址

首先,打开

查询以下三个链接的DNS解析地址

  1. github.com
  2. assets-cdn.github.com
  3. github.global.ssl.fastly.net

2. 修改系统Hosts文件

接着,打开系统hosts文件(需管理员权限)。
路径:C:\Windows\System32\drivers\etc

mac或者其他linux系统的话,是/etc下的hosts文件,需要切入到root用户修改

# Copyright (c) 1993-2009 Microsoft Corp. 
# 
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows. 
# 
# This file contains the mappings of IP addresses to host names. Each 
# entry should be kept on an individual line. The IP address should 
# be placed in the first column followed by the corresponding host name. 
# The IP address and the host name should be separated by at least one 
# space. 
# 
# Additionally, comments (such as these) may be inserted on individual 
# lines or following the machine name denoted by a '#' symbol. 
# 
# For example: 
# 
#      102.54.94.97     rhino.acme.com          # source server 
#       38.25.63.10     x.acme.com              # x client host 




# localhost name resolution is handled within DNS itself. 
#   127.0.0.1       localhost 
#   ::1             localhost 


140.82.113.3    github.com
185.199.108.153 assets-cdn.github.com
199.232.69.194  github.global.ssl.fastly.net

并在末尾添加三行记录并保存。(需管理员权限,注意IP地址与域名间需留有空格)

感谢评论区

的分享, 对于ubuntu系统,修改完hosts文件执行如下命令: sudo /etc/init.d/network-manager restart

3. 刷新系统DNS缓存

最后,Windows+X 打开系统命令行(管理员身份)或powershell

运行 ipconfig /flushdns 手动刷新系统DNS缓存。

mac系统修改完hosts文件,保存并退出就可以了.不要要多一步刷新操作.
centos系统执行/etc/init.d/network restart命令 使得hosts生效


四. 转入gitee加速

最终下载速度对比

github 42KB/s (加了github访问cdn)

github下载速度

gitee 1034KB/s 大约25倍与github的速度

gitee下载速度

例:我们要下载github.com/DoubleLabyri

先访问要下载的仓库的地址(在chrome中打

点击fork (fork会把这个仓库复制一份到你的github账号的名下,所以你需要有个githu账号,没有的注册一下,有了的记得登陆)

点完之后

注意到这个仓库已经到了我们的名下

好了 github这边的事我们暂时做完了

现在登陆gitee (没有账号的注册一个账号)

然后点击

gitee

接着会出现一个授权

然后可能会出现第一输入密码的地方

这儿输入mac的登陆密码 并点击始终允许

然后出现

输入 github账号的密码 之后出现

选择我们刚刚的项目 navicat-keygen -> 导入

gitee正在帮我们从github下载(gitee从github下载的速度一定是很快的,毕竟大网站)

一般来说30s内就处理好自动刷新了

然后我们复制这个网址

然后我们下载这个地址

可以看到速度

 

9种方法让你访问Github提速到2MB/s!

经常有不少粉丝问我,github 访问超级慢,有没有办法加快,我当初推荐的第 9 种方法。这种方法太过麻烦,直到最近我在网上看到有牛人总结的 GitHub 的 9 种加速方式,感觉还不错,小伙伴们可以试试!

1. GitHub 镜像访问

这里提供两个最常用的镜像地址:

  • https://github.com.cnpmjs.org
  • https://hub.fastgit.org

也就是说上面的镜像就是一个克隆版的 GitHub,你可以访问上面的镜像网站,网站的内容跟 GitHub 是完整同步的镜像,然后在这个网站里面进行下载克隆等操作。

2. GitHub 文件加速

利用 Cloudflare Workers 对 github release 、archive 以及项目文件进行加速,部署无需服务器且自带CDN.

  • https://gh.api.99988866.xyz
  • https://g.ioiox.com

以上网站为演示站点,如无法打开可以查看开源项目:gh-proxy-GitHub(https://hunsh.net/archives/23/) 文件加速自行部署。

3. Github 加速下载

只需要复制当前 GitHub 地址粘贴到输入框中就可以代理加速下载!

地址:http://toolwa.com/github/

4. 加速你的 Github

https://github.zhlh6.cn

输入 Github 仓库地址,使用生成的地址进行 git ssh 等操作

5. 谷歌浏览器 GitHub 加速插件(推荐)

如果小伙伴在线安装不便,可以加我微信号:codedq 我免费发给大家!

6. GitHub raw 加速

GitHub raw 域名并非 github.com 而是 raw.githubusercontent.com,上方的 GitHub 加速如果不能加速这个域名,那么可以使用 Static CDN 提供的反代服务。

将 raw.githubusercontent.com 替换为 raw.staticdn.net 即可加速。

7. GitHub + Jsdelivr

jsdelivr 唯一美中不足的就是它不能获取 exe 文件以及 Release 处附加的 exe 和 dmg 文件。

也就是说如果 exe 文件是附加在 Release 处但是没有在 code 里面的话是无法获取的。所以只能当作静态文件 cdn 用途,而不能作为 Release 加速下载的用途。

8. 通过 Gitee 中转 fork 仓库下载

网上有很多相关的教程,这里简要的说明下操作。

访问 gitee 网站:https://gitee.com/ 并登录,在顶部选择“从 GitHub/GitLab 导入仓库” 如下:

在导入页面中粘贴你的Github仓库地址,点击导入即可:

等待导入操作完成,然后在导入的仓库中下载浏览对应的该 GitHub 仓库代码,你也可以点击仓库顶部的“刷新”按钮进行 Github 代码仓库的同步。

9. 通过修改 HOSTS 文件进行加速

手动把cdn和ip地址绑定。

第一步:获取 github 的 global.ssl.fastly 地址 访问:http://github.global.ssl.fastly.net.ipaddress.com/#ipinfo 获取cdn和ip域名:

得到:199.232.69.194 https://github.global.ssl.fastly.net

第二步:获取github.com地址

访问:https://github.com.ipaddress.com/#ipinfo 获取cdn和ip:

得到:140.82.114.4 http://github.com

第三步:修改 host 文件映射上面查找到的 IP

windows系统:

1、修改C:\Windows\System32\drivers\etc\hosts文件的权限,指定可写入:右击->hosts->属性->安全->编辑->点击Users->在Users的权限“写入”后面打勾。如下:

然后点击确定。

2、右击->hosts->打开方式->选定记事本(或者你喜欢的编辑器)->在末尾处添加以下内容:

  1. 199.232.69.194 github.global.ssl.fastly.net
  2. 140.82.114.4 github.com

来源:http://39sd.cn/D8F48

自动化测试远程驱动静默方式(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

JUPYTER NOTEBOOK 中更换主题与折叠代码

得益于良好的交互性和简洁的界面, Deep Learning 或 Python 编程中经常使用 Jupyter Notebook 作为一个快速实现的环境。
Jupyter Notebook 的默认界面比较简朴,但其基于 Web 技术开发的特性让定制、修改界面变得不那么困难。
不过,直接操作如在 CSS 文件上修改,对不熟悉 web 开发的调包/调参侠们来说依然是个艰巨的任务。好在 Python 是个开放的社区,已经有人开发了一些应用,安装之后只需几条命令就可以实现上面的功能。下面就分别介绍两组插件:jupyterthemes 和 jupyter_contrib_nbextensions。

使用 JUPYTERTHEMES 定制界面

效果如图:

这里写图片描述

这里写图片描述

这里写图片描述

这些看起来非常酷炫的效果是 jupyterthemes 插件实现的:https://github.com/dunovank/jupyter-themes

安装插件需要:

  • Python 2.7, 3.4, 3.5, 3.6
  • Jupyter (Anaconda recommended)
  • matplotlib

安装非常简单:

# 安装 jupyterthemes
pip install jupyterthemes

# 升级到最新版本
pip install --upgrade jupyterthemes

安装完成后,jupyterthemes 以别名 jt 存在于 python 环境中。
有时页面 https://github.com/dunovank/jupyter-themes 上显示的主题可能已经不支持了,所以更换主题前首先

jt -l
  • 1

查看当前支持的主题。
比如我想换 grade3,那么输入

jt -t -grade3
  • 1

就把主题换掉了。不过默认字体都特别难看,所以需要指定字体。可以对照示例选择顺眼的字体:
https://github.com/dunovank/jupyter-themes#command-line-examples

其他的设置比较琐碎就不再赘述了,github 页面上给出了详细的说明。

代码折叠

Jupyter Notebook 另一个不太舒服的地方就是默认没有代码折叠功能。手动修改比较麻烦,所以也有人把功能集成到一部分插件里,可以直接在 notebook 界面上点击选项来设置。
插件安装和启用:

pip install jupyter_contrib_nbextensions
jupyter contrib nbextension install --user
pip install jupyter_nbextensions_configurator
jupyter nbextensions_configurator enable --user

其中 jupyter_contrib_nbextensions 是功能集成插件,nbextensions_configurator 让用户可以直接在 notebook 界面中修改这些选项。
成功配置的话,启动 notebook 会看到
这里写图片描述

多了一个菜单。打开
这里写图片描述
发现可选内容很多,这里我们只需要折叠代码的功能,所以直接在 Collapsible Headings 勾选就可以了。其他功能可以自己慢慢尝试。