CSS选择器
CSS全称为Cascading Style Sheets(级联样式表),CSS的建立初始目的是快速便捷地定义如何显示HTML元素。
对于CSS的基础语法,其本质为:选择器+一条或多条声明:
- selector{declaration1;…;declarationN}
每条声明由一个属性和一个值组成:
- property:value
比如例子:
- h1{color:red; font-size:14px},其含义是选择标签为h1的元素,并设置其两个属性。
CSS包含不同类型的选择器。
- 元素选择器:元素选择器可以直接选择文档元素,比如head和p。
- 类选择器:类选择器建立在元素的class属性上,比如
<h1 class="important">
,其类名为important
,因此.important
就是选择所有具有这个类属性的元素,我们可以结合元素选择器进行操作,比如p.important
指的是选择所有标签为p
,class属性为important
的元素。 - ID选择器:ID选择器建立在元素的ID属性上,比如
<h1 id="intro">
,则#intro
就是选择所有具有这个ID属性的元素。如果结合元素选择器,则有p#intro
(注意类选择是.
前缀,ID选择是#
前缀)。ID与类不同,ID在一个文档中只能出现一次。 - 属性选择器:选择有某个属性的元素,不论属性值是什么,
*[title]
表示选择所有包含title属性的元素,a[href]
表示选择所有带有href属性的锚元素。也可以指定属性值的限定a[href="www.so.com"]
。实际上,类选择器和ID选择器是特殊的属性选择器。 - 后代(包含)选择器:假设有以下HTML,后代选择器不受层级限制,
h1 em
会选择h1元素的所有em元素。
<h1>
<em>...</em>
<body>
<em>...</em>
</body>
</h1>
- 子元素选择器:是特殊的后代选择器,范围限制在子元素(仅有父子关系),比如选择h1元素的子元素strong:
h1>strong
。
常用的是类选择器和ID选择器,通常,JS操作元素也是依靠CSS定位。
XPath
XPath也是一种选择器,XPath相比CSS,CSS更加简洁,XPath的选择功能更强大。XPath将整个文档结构视为目录树,使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。
关于其路径的表达式有以下规则:
-
nodename
:选取此节点的所有子节点; -
/
:从根节点选择; -
//
:从匹配的当前节点选择文档中的节点,而不考虑它们的位置; -
.
:选择当前节点; -
..
:选择当前节点的父节点; -
@
:选择属性。
举例如下,假设我们有一个XML文档:
<?xml version="1.0"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learn XML</title>
<price>75</price>
</book>
</bookstore>
因此有:
-
bookstore
选取bookstore元素的所有子节点; -
/bookstore
选取根元素bookstore; -
/bookstore/book
选取属于bookstore的子元素的所有book元素; -
//book
选取所有book元素,而不管它们在文档中的位置; -
bookstore//book
选择属于bookstore元素的后代的所有book元素; -
//@lang
选取名为lang的所有属性。
XPath支持谓语功能,嵌在[]
中用来查找某个特定节点或包含某个指定值的节点。比如:
-
/bookstore/book[1]
:选择属于bookstore下的第一个book元素; -
/bookstore/book[last()]
:选择属于bookstore下的最后一个book元素; -
/bookstore/book[position()<3]
:选择属于bookstore下的前2个book元素; -
//title[@lang]
:选择所有名为lang的属性的title元素; - XPath还支持属性的条件过滤:
/bookstore/book[price>35.00]
;
Json
Json的诞生是为了便于JS处理数据,Json类似XML,但更轻量级,更快,更容易解析。
json(JavaSkriptObjectNotation),本质是XML的简化,XML格式为<标签>值</标签>
(可嵌套表达),举例:
<Student>
<name>baijingyi</name>
<address>CDC</address>
<gender>male</gender>
</Student>
<Student>
<name>lakers</name>
<address>LA</address>
<gender>male</gender>
</Student>
json表达更加简洁,json有两种数据结构:Object和List,一个花括号{key:value,key:value...}
代表一个对象,举例:
{"name":"baijingyi","address":"CDC","gender":"male"}
注意:key是string类型,json的字符串必须用双引号,如果用单引号会解析错误;
列表就是[value,value...]
,举例:
[1,2,45,"你好"]
dump()和load()以及dumps()和loads()
对于python中的json,还需要掌握dump()和load():
- 1.dump()用于写json
- 2.load()用于读json
如果函数名后加了’s’代表json对象是字符串形式,否则json对象是文件形式;
比如dumps()就是将python格式的对象写到json对象,且这个json对象是字符串形式;dump()则是将json对象写入了具体的文件:
import json
data=['a','b',{'x':1,'y':[2,3]},['d','e']]
#将python格式转为json格式,dumps的s代表json用字符串表示,不带s的dump代表将json写入文件
j=json.dumps(data)
print(j)
#>["a", "b", {"y": [2, 3], "x": 1}, ["d", "e"]]
#读入json格式字符串,返回python格式
jdata=json.loads(j)
print(jdata)
with open('./sever.json') as f:
#load用于读入json格式文件,返回python格式
jdata=json.load(f)
print(jdata)
#w+:打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件
with open('./dump.json','w+') as f:
json.dump(jdata,f)
Python处理XML文档
Python处理XML通常有两种方法:
- 基于DOM模型:把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。
- 基于SAX:流模式,边读边解析,占用内存小,解析快,缺点是需要自己处理事件。
使用以下XML文档演示,文件名为book.xml
:
<?xml version="1.0"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learn XML</title>
<price>75</price>
</book>
</bookstore>
基于DOM模型的操作为:
from xml.dom import minidom
# 加载文档并解析
doc=minidom.parse('book.xml')
# 获取根节点
root=doc.documentElement
print(type(root)) # <class 'xml.dom.minidom.Element'>
print(root.nodeName) # bookstore
print(dir(root)) # dir()查看一个对象的属性和方法
# 按照标签名查找子元素
books=root.getElementsByTagName('book')
for book in books:
titles=book.getElementsByTagName('title')
prices=book.getElementsByTagName('price')
# print(titles[0],prices[0])
"""
<DOM Element: title at 0x1bb1666cf70> <DOM Element: price at 0x1bb16672040>
<DOM Element: title at 0x1bb16672160> <DOM Element: price at 0x1bb166721f0>
"""
# print(titles[0].childNodes,prices[0].childNodes)
"""
[<DOM Text node "'Harry Pott'...">] [<DOM Text node "'29.99'">]
[<DOM Text node "'Learn XML'">] [<DOM Text node "'75'">]
"""
print(titles[0].childNodes[0].nodeValue,prices[0].childNodes[0].nodeValue)
"""
Harry Potter 29.99
Learn XML 75
"""
基于SAX的处理方式为:
from xml.parsers.expat import ParserCreate
# SAX是灵活的流处理, 因此事件需要自己定义
class DefaultSaxHandler(object):
def start_element(self,name,attrs):
"""
:param name: 元素
:param attrs: 元素的属性
"""
self.name=name
print('start element:{},attrs:{}'.format(name,str(attrs)))
def end_element(self,name):
print('end element:{}'.format(name))
def char_data(self,text):
if text.strip():
print("{}'s text is{}".format(self.name,text))
handler=DefaultSaxHandler()
parser=ParserCreate()
parser.StartElementHandler=handler.start_element # 比如<book>
parser.EndElementHandler=handler.end_element # 比如</book>
parser.CharacterDataHandler=handler.char_data # 比如<title>character data</title>
with open('book.xml','r') as f:
# 由于文件小, 就read()整个读取
parser.Parse(f.read())
"""
start element:bookstore,attrs:{}
start element:book,attrs:{}
start element:title,attrs:{'lang': 'eng'}
title's text isHarry Potter
end element:title
start element:price,attrs:{}
price's text is29.99
end element:price
end element:book
start element:book,attrs:{}
start element:title,attrs:{'lang': 'eng'}
title's text isLearn XML
end element:title
start element:price,attrs:{}
price's text is75
end element:price
end element:book
end element:bookstore
"""
SAX对XML的层级关系需要自己维护,因此更多人还是使用DOM模型处理XML。
正则表达式基础
正则表达式有以下基本匹配规则:
-
[0-9]
:任意一个数字,等价\d
; -
[a-z]
: 任意一个小写字母; -
[A-Z]
:任意一个大写字母; -
[^0-9]
:匹配非数字,等价\D
; -
\w
等价[a-z0-9_]
,匹配字母数字以及下划线; -
\W
等价对\w
取非; -
.
:任意字符; -
[]
:匹配内部任意字符或子表达式; -
[^]
:对字符集合取非; -
|
:指明两项之间的一个选择;
以下是限定符的规则:
-
*
:匹配前面的字符或者子表达式0次或多次,要匹配*
字符,请使用\*
; -
+
:匹配前一个字符至少1次,要匹配+
字符,请使用\+
,减号匹配不需要转义; -
?
:匹配前一个字符0次或多次; -
^
:匹配字符串开头; -
$
:匹配字符串结束; -
{n}
:n 是一个非负整数。匹配确定的 n 次; -
{n,}
:n 是一个非负整数。至少匹配n 次; -
{n,m}
:m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次; -
()
:标记一个子表达式的开始和结束位置。
更详细规则参考:正则表达语法
实例如下:
import re
# match从最开始进行匹配,遇到不符合元素就退出
mr=re.match(r'[0-9]+','123abc456edf789')
print(mr) # <re.Match object; span=(0, 3), match='123'>
# 利用()+groups方法匹配子表达, 分组
m=re.match(r'(\d{3})-(\d{3,8})','010-123456')
print(m.groups()) # ('010', '123456')
# 用正则表示+split方法分割字符串
p=re.compile(r'\d+')
print(p.split('one1two22three333.')) # ['one', 'two', 'three', '.']
selenium收集京东燕麦信息
Selenium 是web自动化测试工具集,因此也可以成为爬虫的工具。由于selenium是代替人类在网页上进行自动化浏览,因此使用selenium需要下载对应浏览器的驱动。以Edge浏览器为例,驱动下载链接为:MicrosoftWebDriver
下载稳定版后并进行解压:
初始化Edge,其中,Service(r"C:\Users\lphs\MySoftware\EdgeDriver\msedgedriver.exe")
中的参数为解压后的可执行文件路径:
from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.common.by import By
import re
import time
# 使用Edge浏览器
s=Service(r"C:\Users\lphs\MySoftware\EdgeDriver\msedgedriver.exe")
browser = webdriver.Edge(service=s)
# 设置等待时间不超过30s
browser.set_page_load_timeout(30)
我们先打开京东,并搜索燕麦,复制url有:
https://search.jd.com/Search?keyword=%E7%87%95%E9%BA%A6&qrst=1&wq=%E7%87%95%E9%BA%A6&stock=1&pvid=4ef0796c66d5464bb541edbf6acacc3f&page=1&s=1&click=0
用selenium的驱动器打开网页:
# 打开网页
browser.get("https://search.jd.com/Search?keyword=%E7%87%95%E9%BA%A6&qrst=1&wq=%E7%87%95%E9%BA%A6&stock=1&pvid=4ef0796c66d5464bb541edbf6acacc3f&page=1&s=1&click=0")
先完成一个简单的任务,我们获取页数信息,下拉到网页末端:
选中"共12页",右键,选择"检查",在HTML区域选择高亮对应的元素,并右键复制其CSS Selector,以便于我们定位这个元素。该位置的CSS选择器为:
J_bottomPage > span.p-skip > em:nth-child(1)
因此,我们可以获取页数信息:
# 用css选择器获取页数信息
page_info=browser.find_element(By.CSS_SELECTOR,"#J_bottomPage > span.p-skip > em:nth-child(1)")
print(page_info.text) # 共12页 到第
然后我们利用空格拆分字符串,再用正则表达式获取到需要的数字,即12:
# 将text拆开, 基于空格
pages=page_info.text.split(' ')
print(pages) # ['共12页', '', '到第']
# 构造正则表达式获取数字部分
p=re.compile(r'\D+')
pages=p.split(pages[0])
print(pages) # ['', '12', '']
pages=int(pages[1]) # 12
print("商品有{}页".format(pages))
为了演示,我们抓取前3页的商品名称和价格信息,注意url中page参数的编码,在本次实例中,page顺序为1,3,5对应1,2,3页(也就是说这是奇数编码),因此,换页面的url应为以下格式:
url="https://search.jd.com/Search......&page="+str(page*2+1)+"......"
注意一些技巧
另外我们需要借助JS语句模拟页面的滚动,并让浏览器有时间加载信息:
# 使用js指令模拟页面的滚动
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 给时间加载页面, 不然加载不完整
time.sleep(3)
抓取前3页的过程如下,主要操作还是基于我们人工去找CSS选择器,再利用脚本去定位web元素:
# 为了演示, 只抓取前3页
for page in range(pages):
if page>2:
break
print("抓取第{}页的信息".format(str(page+1)))
# 注意京东的url构造, page顺序为1,3,5对应1,2,3页(奇数编码)
url="https://search.jd.com/Search?keyword=%E7%87%95%E9%BA%A6&qrst=1&wq=%E7%87%95%E9%BA%A6&stock=1&pvid=4ef0796c66d5464bb541edbf6acacc3f&page="\
+str(page*2+1)+"&s=1&click=0"
browser.get(url)
# 使用js指令模拟页面的滚动
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 给时间加载页面, 不然加载不完整
time.sleep(3)
# 抓取商品: 首先获取无序列表ul, 然后从ul中根据tag_name“li”去搜集商品
# 注意find_elements和find_element的区别
# find_element()只会查找页面中符合条件的第一个节点, 并返回
# 查找多个元素的时候: 只能用find_elements(), 返回一个列表, 列表里的元素全是WebElement对象
goods=browser.find_element(By.CSS_SELECTOR,"#J_goodsList > ul").find_elements(By.TAG_NAME,"li")
print("第{}页有{}件商品".format(str(page+1),len(goods)))
for good in goods:
try:
title=good.find_element(By.CSS_SELECTOR,"#J_goodsList > ul > li:nth-child(1) > div > div.p-name.p-name-type-2 > a > em").text
price=good.find_element(By.CSS_SELECTOR,"#J_goodsList > ul > li:nth-child(1) > div > div.p-price > strong > i").text
print(title,price)
except:
print(good.text)
补充内容
在商品抓取任务中,我们最好还是要知道HTML的两个元素,一个是ul
,一个是li
。
<ul>
标签定义无序列表,<ol>
标签定义有序列表。
<li>
标签定义列表项目。所有主流浏览器都支持 <li>
标签。
<li>
标签可用在有序列表 (<ol>
) 和无序列表 (<ul>
) 中。
比如:
<html>
<body>
<p>有序列表:</p>
<ol>
<li>打开冰箱门</li>
<li>把大象放进去</li>
<li>关上冰箱门</li>
</ol>
<p>无序列表:</p>
<ul>
<li>雪碧</li>
<li>可乐</li>
<li>凉茶</li>
</ul>
</body>
</html>
结果为: