3.爬虫基础知识与简易爬虫实现

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

下载稳定版后并进行解压:
fig1
初始化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")

先完成一个简单的任务,我们获取页数信息,下拉到网页末端:
fig2
选中"共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>

结果为:
fig3


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值