Python 通过 xpath、CSS 解析 HTML / XML、scrapy 内置 ( xpath、re、css )、LinkExtractor

本文介绍Python中使用XPath解析HTML和XML文档的方法,包括lxml库的安装与使用、XPath的基础语法及实战案例。同时,文章对比了XPath与CSS选择器的不同应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

爬虫必备知识:https://blog.51cto.com/u_11555417/5204144

1、标签 语言

XML是可扩展标记语言(Extensible Markup Language)的缩写

标签语言是一类用于标记和描述文档结构、数据或内容的计算机语言。标签语言通常由标签(tags)组成,用于标识文本或数据的不同部分,并定义这些部分之间的关系或格式。常见的标签语言:

  • HTML (HyperText Markup Language):用途:HTML 是用于创建和设计网页的标签语言。它定义了网页的结构,使用标签来描述网页元素(如标题、段落、链接、图像等)
  • XML (Extensible Markup Language):XML是可扩展标记语言,是一种存储和传输数据的通用标签语言。它不预定义任何标签,允许用户自定义标签来描述数据的结构。
  • XHTML (Extensible HyperText Markup Language):用途:XHTML 是 HTML 的一种严格化版本,结合了 XML 的规则。XHTML 保持了 HTML 的语义,但遵循更严格的语法规则(如标签必须关闭)。
  • SGML (Standard Generalized Markup Language):用途:SGML 是一种元语言,HTML 和 XML 都基于 SGML。它用于定义文档结构和标签,适用于各种文档类型。
  • Markdown:用途:Markdown 是一种轻量级标记语言,常用于编写文档和格式化文本,广泛应用于博客、GitHub 等平台。它使用简洁的语法来表示格式(如标题、列表、加粗等)。
  • LaTeX:是一种用于排版和出版的标记语言,广泛用于学术论文和书籍的编写。它非常适合处理复杂的数学公式和图表。
  • MathML (Mathematical Markup Language);用途:MathML 是一种基于 XML 的语言,专门用于表示数学符号和公式,常用于科学和工程领域。
  • YAML (YAML Ain't Markup Language):用途:虽然 YAML 的名字包含“标记语言”,但它更常用于配置文件和数据序列化。它通过缩进和冒号语法来表示数据结构。

XPath 是 XML 的路径语言,主要查找 XML(标准通用标记语言的子集)文档中某部分位置。
XPath 基于 XML 的树状结构,提供在数据结构树中找寻节点的能力。 XPath 同样也支持HTML。
XPath 是一门小型的查询语言。python 中 lxml库 使用的是 Xpath 语法,是效率比较高的解析方法。

2、Python 解析 XML / HTMl

lxml 库

lxml 用法 官方文档:http://lxml.de/index.html

lxml 是 Python 中用于处理 XML 和 HTML 的库,并且功能丰富易于使用。

步骤1:安装 lxml 库 :pip3 install lxml
步骤2:from lxml import etree  # etree 表示 ElementTree(元素树),etree会自动修复HTML文本节点
步骤3:lxml 提供如下方式输入文本:
        selector = etree.fromstring():解析字符串
        selector = etree.HTML():解析HTML对象
        selector = etree.XML():解析XML对象
        selector = etree.parse():解析文件类型对象
步骤4:selector.xpath("xpath表达式")

etree.fromstring()

from lxml import etree

# 示例 XML 字符串
xml_data = '''
<bookstore>
    <book category="fiction">
        <title lang="en">Harry Potter</title>
        <author>J.K. Rowling</author>
        <year>2005</year>
        <price>29.99</price>
    </book>
    <book category="cooking">
        <title lang="en">Everyday Italian</title>
        <author>Giada De Laurentiis</author>
        <year>2005</year>
        <price>30.00</price>
    </book>
</bookstore>
'''

# 解析 XML 字符串
root = etree.fromstring(xml_data)

# 打印根节点的标签
print(root.tag)  # 输出: bookstore

# 使用 XPath 查找所有书的标题
titles = root.xpath("//book/title/text()")
print(titles)  # 输出: ['Harry Potter', 'Everyday Italian']

etree.HTML()

import re
import requests
import lxml.html
from lxml import etree
from pprint import pprint


def main():
    html = '''
    <div id="content">   
       <ul id="useful">
          <li>有效信息1</li>
          <li>有效信息2</li>
       </ul>
       <ul id="useless">
          <li>无效信息1</li>
          <li>无效信息2</li>
       </ul>
    </div>
    <div id="url">
       <a href="https://www.bing.com">点击打开bing</a>
       <a href="https://www.google.com" title="google搜索">点击打开google</a>
    </div>
    '''
    selector = etree.HTML(text=html)

    # 提取 li 中的有效信息
    content = selector.xpath('//ul[@id="useful"]/li/text()')
    for each in content:
        print(each)

    # 提取 a 中的属性
    link = selector.xpath('//a/@href')
    for each in link:
        print(each)

    title = selector.xpath('//a/@title')
    for each in title:
        print(each)


if __name__ == '__main__':
    main()

etree.XML

from lxml import etree

root_node = etree.XML('<root><a><b/></a></root>')
print(etree.tostring(root_node))
print(etree.tostring(root_node, xml_declaration=True))
print(etree.tostring(root_node, xml_declaration=True, encoding='utf-8'))

etree.parse()

from lxml import etree

# 解析 XML 文件
tree = etree.parse('books.xml')

# 获取根元素
root = tree.getroot()

# 打印根节点的标签
print(root.tag)

# 使用 XPath 查找特定元素
for book in root.xpath("//book"):
    title = book.find('title').text
    author = book.find('author').text
    print(f"Title: {title}, Author: {author}")

处理 非标准的 html

lxml 自动修复标签属性两侧缺失的引号,并闭合标签。

from lxml import etree
# 指定解析器HTMLParser会根据文件修复HTML文件中缺失的如声明信息
html = etree.parse('test.html', etree.HTMLParser())
result = etree.tostring(html) # 解析成字节
result_list = etree.tostringlist(html) # 解析成列表

import lxml.html

test_html = "<ul class=country> <li>Area <li id=aa>Population</ul>"
tree = lxml.html.fromstring(test_html)
print(f'type(tree) ---> {type(tree)}')
# pretty_print: 优化输出,输出换行符
# fixed_html = lxml.html.tostring(tree, pretty_print=True)
fixed_html = lxml.html.tostring(tree)  # 转换为字节
print(f'type(fixed_html) ---> {type(fixed_html)}')
print(fixed_html)

使用 CSS 选择器

选择所有标签 :*
选择<a>标签:a
选择所有 class="link"的元素: .link
选择 class="link" 的 <a>标签: a.link
选择 id= "home" 的 <a>标签: a#home
选择父元素为 <a> 标签的所有 <span>子标签: a > span
选择<a>标签内部的所有 <span>标签: a span
选择 title 属性为 "Home" 的所有<a>标签: a[title=Home]

import lxml.html
import requests
from fake_useragent import UserAgent
from pprint import pprint

ua = UserAgent()
header = {
    "User-Agent": ua.random
}
html = requests.get("https://www.baidu.com/", headers=header)
html = html.content.decode("utf8")
# pprint(html)
tree = lxml.html.fromstring(html)
tag_div_list = tree.cssselect('div#head .mnav')

# 输出文本内容
# area = td.text_content()
for tag_div in tag_div_list:
    pprint(tag_div.text)

编辑 HTML / XML

也可以使用 lxml 来生成新的 XML 文档或修改现有的 XML 数据:

from lxml import etree
import lxml.html as HTML

"""
Element是 XML处理的核心类,
Element对象可以直观的理解为 XML的节点,大部分 XML节点的处理都是围绕该类进行的。
这部分包括三个内容:节点的操作、节点属性的操作、节点内文本的操作。
"""

# 1.创建 element
root = etree.Element('root')
print(root, root.tag)

# 2.添加子元素,并为子节点添加属性
body = etree.SubElement(root, 'body')
body.text = 'body_text'          # 设置属性和值
body.set('class', 'body_class')  # 设置属性和值
etree.SubElement(root, 'child0').text = 'child0'
child1 = etree.SubElement(root, 'child1')
child2 = etree.SubElement(root, 'child2')
book = etree.SubElement(root, "book", category="fiction")
title = etree.SubElement(book, "title", lang="en")
title.text = "The Great Gatsby"
root.append(etree.Element('child3', interesting='sss'))
root.append(etree.Entity('#234'))
root.append(etree.Comment('some comment'))  # 添加注释
# 为第三个节点添加一个br
br = etree.SubElement(root.getchildren()[2], 'br')
br.tail = 'TAIL'
for element in root.iter():  # 也可以指定只遍历是Element的,root.iter(tag=etree.Element)
    if isinstance(element.tag, str):
        print('%s - %s' % (element.tag, element.text))
    else:
        print('SPECIAL: %s - %s' % (element, element.text))
# 输出整个节点
xml_string = etree.tostring(root, encoding='UTF-8', pretty_print=True).decode()
print(xml_string)

# 3.删除子节点
# root.remove(child2)

# 4.删除所有子节点
# root.clear()

# 5.以列表的方式操作子节点
print(len(root))
print(root.index(child1))  # 索引号
root.insert(0, etree.Element('child3'))  # 按位置插入
root.append(etree.Element('child4'))  # 尾部添加

# 6.获取父节点
print(child1.getparent().tag)
# print root[0].getparent().tag   #用列表获取子节点,再获取父节点
'''以上都是节点操作'''

# 7.创建属性
# root.set('hello', 'dahu')   #set(属性名,属性值)
# root.set('hi', 'qing')

# 8.获取属性
# print(root.get('hello'))    #get方法
# print root.keys(),root.values(),root.items()    #参考字典的操作
# print root.attrib           #直接拿到属性存放的字典,节点的attrib,就是该节点的属性
'''以上是属性的操作'''

# 9.text和tail属性
# root.text = 'Hello, World!'
# print root.text

# 10.test,tail 和 text 的结合
html = etree.Element('html')
html.text = 'html.text'
body = etree.SubElement(html, 'body')
body.text = 'wo ai ni'
child = etree.SubElement(body, 'child')
child.text = 'child.text'  # 一般情况下,如果一个节点的text没有内容,就只有</>符号,如果有内容,才会<>,</>都有
child.tail = 'tails'  # tail是在标签后面追加文本
print(etree.tostring(html))
# print(etree.tostring(html, method='text'))  # 只输出text和tail这种文本文档,输出的内容连在一起,不实用

# 11.Xpath方式
# print(html.xpath('string()'))   #这个和上面的方法一样,只返回文本的text和tail
print(html.xpath('//text()'))  # 这个比较好,按各个文本值存放在列表里面
tt = html.xpath('//text()')
print(tt[0].getparent().tag)  # 这个可以,首先我可以找到存放每个节点的text的列表,然后我再根据text找相应的节点
# for i in tt:
#     print i,i.getparent().tag,'\t',

# 12.判断文本类型
print(tt[0].is_text, tt[-1].is_tail)  # 判断是普通text文本,还是tail文本
'''以上都是文本的操作'''

# 13.字符串解析,fromstring方式
xml_data = '<html>html.text<body>wo ai ni<child>child.text</child>tails</body></html>'
root1 = etree.fromstring(xml_data)  # fromstring,字面意思,直接来源字符串
# print root1.tag
# print etree.tostring(root1)

# 14.xml方式
root2 = etree.XML(xml_data)  # 和fromstring基本一样,
print(etree.tostring(root2))

# 15.文件类型解析
'''
- a file name/path
- a file object
- a file-like object
- a URL using the HTTP or FTP protocol
'''
tree = etree.parse('text.html')  # 文件解析成元素树
root3 = tree.getroot()  # 获取元素树的根节点
print(etree.tostring(root3, pretty_print=True))

parser = etree.XMLParser(remove_blank_text=True)  # 去除xml文件里的空行
root = etree.XML("<root>  <a/>   <b>  </b>     </root>", parser)
print(etree.tostring(root))

# 16.html方式
xml_data1 = '<root>data</root>'
root4 = etree.HTML(xml_data1)
print(etree.tostring(root4))  # HTML方法,如果没有<html>和<body>标签,会自动补上
# 注意,如果是需要补全的html格式:这样处理哦
with open("quotes-1.html", 'r', encoding='utf-8') as f:
    a = HTML.document_fromstring(f.read())

for i in a.xpath('//div[@class="quote"]/span[@class="text"]/text()'):
    print(i)

# 17.输出内容,输出xml格式
print(etree.tostring(root))
print(etree.tostring(root, xml_declaration=True, pretty_print=True, encoding='utf-8')) 
'''以上是文件IO操作'''

# 18.findall方法
root = etree.XML("<root><a x='123'>aText<b/><c/><b/></a></root>")
print(root.findall('a')[0].text)  # findall操作返回列表
print(root.find('.//a').text)  # find操作就相当与找到了这个元素节点,返回匹配到的第一个元素
print(root.find('a').text)
print([b.text for b in root.findall('.//a')])  # 配合列表解析,相当帅气!
print(root.findall('.//a[@x]')[0].tag)  # 根据属性查询
'''以上是搜索和定位操作'''
print(etree.iselement(root))
print(root[0] is root[1].getprevious())  # 子节点之间的顺序
print(root[1] is root[0].getnext())
'''其他技能'''
# 遍历元素数
root = etree.Element("root")
etree.SubElement(root, "child").text = "Child 1"
etree.SubElement(root, "child").text = "Child 2"
etree.SubElement(root, "another").text = "Child 3"
etree.SubElement(root[0], "childson").text = "son 1"
# for i in root.iter():   #深度遍历
# for i in root.iter('child'):    #只迭代目标值
#     print i.tag,i.text
# print etree.tostring(root,pretty_print=True)

节点、属性、文本

Element类 是 XML 处理的核心类,Element 对象可以直观的理解为XML的节点,大部分XML节点的处理都是围绕该类进行的。这部分包括三个内容:

  • 节点的操作
    from lxml import etree
    
    root = etree.Element('root')  # 创建Element对象
    print(root.tag)  # 使用tag属性,获取节点的名称
    print(etree.tostring(root))  # 使用tostring方法输出XML内容
    
    # 添加子节点。第一个参数为父节点(Element对象),第二个参数为子节点名称。
    child1 = etree.SubElement(root, 'child1')
    child2 = etree.SubElement(root, 'child2')
    child3 = etree.SubElement(root, 'child3')
    
    # 使用remove方法删除指定节点,clear方法清空所有节点。
    root.remove(child1)  # 删除指定子节点
    print(etree.tostring(root))
    root.clear()
    print(etree.tostring(root))
    
    # 以列表的方式操作子节点
    child = root[0]  # 下标访问
    print(child.tag)
    print(len(root))  # 子节点数量
    root.index(child2)  # 获取索引号
    for child in root:  # 遍历
        print(child.tag)
    
    root.insert(0, etree.Element('child0'))  # 插入
    start = root[:1]  # 切片
    end = root[-1:]
    print(start[0].tag)
    print(end[0].tag)
    root.append(etree.Element('child4') )  # 尾部添加
    print(etree.tostring(root))
    # 获取父节点。使用getparent方法可以获取父节点。
    print(child1.getparent().tag)
    
  • 节点的属性。属性 是以 key-value 的方式存储的,就像字典一样
    from lxml import etree
    
    root = etree.Element('root', interesting='totally')  # 创建Element对象
    root.set('hello', 'haha')  # 设置属性和值
    print(etree.tostring(root))
    print(root.get('interesting'))  # 获取属性值
    print(sorted(root.keys()))  # keys方法 获取所有的属性名
    # items方法获取所有的键值对
    for name, value in sorted(root.items()):
        print(f'{name} ---> {value}')
    # 也可以用attrib属性一次拿到所有的属性及属性值存于字典
    attributes = root.attrib
    print(attributes)
    attributes['good'] = 'Bye' # 字典的修改影响节点
    print(root.get('good'))
  • 节点内的文本。可以使用 text 和 tail 属性、或 XPath 的方式来访问文本内容。
    from lxml import etree
    
    root = etree.Element('root')
    root.text = 'Hello, World!'
    print(root.text)
    print(etree.tostring(root))
    html = etree.Element('html')
    body = etree.SubElement(html, 'body')
    body.text = 'Text'
    print(etree.tostring(html))
    br = etree.SubElement(body, 'br')
    print(etree.tostring(html))
    br.tail = 'Tail'
    print(etree.tostring(br))
    print(etree.tostring(html))
    print(etree.tostring(html, method='text'))
    
    # xpath
    print(html.xpath('string()'))
    print(html.xpath('//text()'))
    print(html.xpath('//text()')[0])
    print(html.xpath('//text()')[0].getparent().tag)
    print(html.xpath('//text()')[0].is_text)
    print(html.xpath('//text()')[0].is_tail)
    
    

scrapy 库:xpath、css,re

使用 xpath 选择器

import scrapy
 
 
class JobboleSpider(scrapy.Spider):
    name = 'jobbole'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/']
 
    def parse(self, response):
        re_selector = response.xpath('//*[@id="post-110287"]/div[1]/h1/text()')

使用 CSS 选择器

​response.css(".entry-header h1").extract()
response.css(".entry-header h1::text").extract()[0]

response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace(" ·","")

response.css(".vote-post-up h10::text").extract()[0]

使用 re

scrapy 的 response 对象不能直接调用 re 和 re_first 方法。可以先调用 xpath 或者css方法,再使用正则进行匹配

response.css(".bookmark-btn::text").re('.*?(\d+).*')[0]

response.xpath('.').re_first('正则表达式')

使用 Selector

Selector 不一定非要在 Scrapy 中使用,也可以独立使用。使用 Selector 这个类来构建一个选择器对象,然后调用它的相关方法(如xpath css等)来提取数据。示例:构建 Selector 对象来提取数据:

from scrapy import Selector

body = '<html><head><title>hello world</title></head></html>'
selector = Selector(text=body)
title = selector.xpath('//title/text()').extract_first()
print(title)

普通的请求变成 scrapy 的 response

scrapy 的 response 有一个属性 selector,调用 response.selector 返回的内容就相当于用 response 的 text 构造了一个 Selectr 对象。通过这个 Selector 对象就可以调用 xpath、css 等解析方法提取数据。

Scrapy 提供了两个实用的快捷方法:response.xpath 和 response.css,二者的功能完全等同于 response.selector.xpath 和 response.selector.css。为了省事可以统一直接调用 response.xpath 和 response.css

TextResponse:向基类 Response 添加编码能力 ,这意味着仅用于二进制数据,例如图像,声音或任何媒体文件。

  • encoding(string) - 是一个字符串,包含用于此响应的编码。如果你创建一个TextResponse具有unicode主体的对象,它将使用这个编码进行编码(记住body属性总是一个字符串)。如果encoding是None(默认值),则将在响应标头和正文中查找编码。 TextResponse除了标准对象之外,对象还支持以下属性Response
  • text - 响应体,如unicode。
  • body_as_unicode() - 同样text,但可用作方法。保留此方法以实现向后兼容; 请喜欢response.text

其他的 xxxRespose 都最终继承 Respose,可以通过查看源码了解对应 xxxResponse的使用。

import requests
from scrapy.http import HtmlResponse


def func_1():
    url = 'https://www.gushiwen.cn/'
    resp = requests.get(url)
    resp.encoding = 'utf-8'
    response = HtmlResponse(url=resp.url, body=resp.text, encoding=resp.encoding)

    tag_a_list = response.xpath('//div[@class="right"]//div[@class="sons"]//a')
    list(map(lambda x=None: print(x), tag_a_list.extract()))

    for tag_a in tag_a_list:
        text = tag_a.xpath("./text()").extract_first()
        href = tag_a.xpath("./@href").extract_first()
        print(f'{text} ---> {href}')

    tag_a_list = response.css('div[class="right"] div[class="sons"] a')
    list(map(lambda x=None: print(x), tag_a_list.extract()))
    # print(response.css('').re())


if __name__ == '__main__':
    func_1()

LinkExtractor 提取链接

Link Extractors 文档

链接提取器( Link Extractors ) 的目的就是从 scrapy.http.Response 中提取链接。
导入包:scrapy.linkextractors import LinkExtractor  

类定义:class scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=False, unique=True, process_value=None, strip=True)

参数 解释:

  • allow (str or list): 一个正则表达式(或正则表达式列表),(绝对)urls 必须匹配才能提取。如果没有给出(或为空),它将匹配所有链接。
  • deny (str or list): 一个正则表达式(或正则表达式列表),(绝对)urls 必须匹配才能排除(即不提取)。它优先于 allow 参数。如果没有给出(或为空),它不会排除任何链接。,可以 和 allow 配合一起用,前后夹击,参数和 allow 一样。
  • allow_domains (str or list):允许 的 域名 或者 域名列表。即 会被提取的链接的 domains。其实这个和 spider 类里的 allowdomains 是一个作用,即 抓取哪个域名下的网站。
  • deny_domains (str or list):拒绝 的 域名 或者 域名列表。即 不会被提取链接的 domains。和 allowdomains 相反,即 拒绝哪个域名下的网站。
  • deny_extensions( list ):包含在提取链接时应该忽略的扩展的单个值或字符串列表。即不允许的扩展名。如果没有给出(默认 是 None),它将默认为 IGNORED_EXTENSIONS 在 scrapy.linkextractors 包中定义的 列表 。(参考: http://www.xuebuyuan.com/296698.html
  • restrict_xpaths (str or list):一个XPath(或XPath的列表),它定义了从Response哪些区域中来提取链接。即 在网页哪个区域里提取链接,可以用 xpath 表达式和 css 表达式这个功能是划定提取链接的位置,让提取更加精准。如果给出,只有那些XPath选择的文本将被扫描链接。参见下面的例子。即 使用 xpath表达式,和allow共同作用过滤链接。
  • restrict_css (str or list):一个CSS选择器(或选择器列表),用于定义响应中应提取链接的区域。同 restrict_xpaths。
  • tags( str 或 list ):提取链接时要考虑的 标签标签列表。默认为('a', 'area')。即 默认提取a标签和area标签中的链接
  • attrs( list ):在查找要提取的链接时应该考虑的属性或属性列表(仅适用于参数中指定的那些标签tags )。默认为('href',)。即 默认提取 tags 里的 href 属性,也就是 url 链接。
  • canonicalize( boolean ):规范化每个提取的 url ( 使用w3lib.url.canonicalize_url)。默认为False。注意,canonicalize_url用于重复检查;它可以更改服务器端可见的URL,因此对于具有规范化URL和原始URL的请求,响应可能不同。如果使用LinkExtractor跟踪链接,则保持默认的canonicalize=False更为可靠。
  • unique( boolean ):是否对提取的链接进行过滤。即 这个地址是否要唯一,默认true,重复收集相同的地址没有意义。
  • process_value ( callable ) – 它接收来自扫描标签和属性提取每个值,可以修改该值,并返回一个新的,或返回 None 完全忽略链接的功能。如果没有给出,process_value 默认是 lambda x: x。其实就是:接受一个函数,可以立即对提取到的地址做加工,这个作用比较强大。比如,提取用 js 写的链接:
    例如,从这段代码中提取链接: <a href="javascript:goToPage('../other/page.html'); return false">Link text</a>

    你可以使用下面的这个 process_value 函数:

    def process_value(value):
        m = re.search("javascript:goToPage\('(.*?)'", value)
        if m:
            return m.group(1)
  • strip :把 提取 的 地址 前后多余的空格删除,很有必要。whether to strip whitespaces from extracted attributes. According to HTML5 standard, leading and trailing whitespaces must be stripped from hrefattributes of <a><area> and many other elements, src attribute of <img><iframe> elements, etc., so LinkExtractor strips space chars by default. Set strip=False to turn it off (e.g. if you’re extracting urls from elements or attributes which allow leading/trailing whitespaces).

示例:

import requests
from scrapy.http import HtmlResponse
from scrapy.linkextractors import LinkExtractor


def main():
    __resp = requests.get('https://so.gushiwen.cn/shiwens/')
    if 200 == __resp.status_code:
        resp = HtmlResponse(url=__resp.url, body=__resp.text, encoding=__resp.encoding)
        """
            LinkExtractor 初始化函数 __init__ 的参数
            def __init__(
                self,
                allow=(),
                deny=(),
                allow_domains=(),
                deny_domains=(),
                restrict_xpaths=(),
                tags=("a", "area"),
                attrs=("href",),
                canonicalize=False,
                unique=True,
                process_value=None,
                deny_extensions=None,
                restrict_css=(),
                strip=True,
                restrict_text=None,
            ):
        """
        le = LinkExtractor(
            restrict_xpaths=('//div[@id="right1"]', '//div[@id="right3"]')
        )
        links = le.extract_links(resp)
        list(map(lambda x=None: print(x), links))
        list(map(lambda x=None: print(f'{x.text.strip()} ---> {x.url}'), links))
    pass


if __name__ == '__main__':
    main()
    pass

scrapy 的常用 ImagesPipeline 重写实现:https://www.jianshu.com/p/cd05763d49e8
使用 Scrapy 自带的 ImagesPipeline下载图片,并对其进行分类:https://www.cnblogs.com/Kctrina/p/9523553.html
使用 FilesPipeline ImagesPipelinehttps://www.jianshu.com/p/a412c0277f8a

Rule 和 Link Extractors 多用于全站的爬取

爬取规则(Crawling rules):http://doc.scrapy.org/en/latest/topics/spiders.html#crawling-rules

Rule 是在定义抽取链接的规则:class scrapy.contrib.spiders.Rule(link_extractor,callback=None,cb_kwargs=None,follow=None,process_links=None,process_request=None)

参数解释:

  1. link_extractor:是一个 Link Extractor 对象。其定义了如何从爬取到的 页面(即 response) 提取链接的方式。
  2. callback:是一个 callable 或 string(该Spider中同名的函数将会被调用)。从 link_extractor 中每获取到链接时将会调用该函数。该回调函数接收一个 response 作为其第一个参数,并返回一个包含 Item 以及 Request 对象(或者这两者的子类)的列表。
  3. cb_kwargs:包含传递给回调函数的参数(keyword argument)的字典。
  4. follow:是一个 boolean 值,指定了根据该规则从 response 提取的链接 是否 需要跟进。如果 callback 为 None,follow 默认设置 True,否则默认 False。当 follow 为 True 时:爬虫会从获取的 response 中 取出符合规则的 url,再次进行爬取,如果这次爬取的 response 中还存在符合规则的 url,则再次爬取,无限循环,直到不存在符合规则的 url。 当 follow 为 False 时爬虫只从 start_urls 的 response 中取出符合规则的 url,并请求。
  5. process_links:是一个callable或string(该Spider中同名的函数将会被调用)。从link_extrator中获取到链接列表时将会调用该函数。该方法主要是用来过滤。
  6. process_request:是一个callable或string(该spider中同名的函数都将会被调用)。该规则提取到的每个request时都会调用该函数。该函数必须返回一个request或者None。用来过滤request。

官网示例:

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com']

    rules = (
        # Extract links matching 'category.php' (but not matching 'subsection.php')
        # and follow links from them (since no callback means follow=True by default).
        Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

        # Extract links matching 'item.php' and parse them with the spider's method parse_item
        Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
    )

    def parse_item(self, response):
        self.logger.info('Hi, this is an item page! %s', response.url)
        item = scrapy.Item()
        item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
        item['name'] = response.xpath('//td[@id="item_name"]/text()').get()
        item['description'] = response.xpath('//td[@id="item_description"]/text()').get()
        item['link_text'] = response.meta['link_text']
        url = response.xpath('//td[@id="additional_data"]/@href').get()
        return response.follow(url, self.parse_additional_page, cb_kwargs=dict(item=item))

    def parse_additional_page(self, response, item):
        item['additional_data'] = response.xpath('//p[@id="additional_data"]/text()').get()
        return item

示例:

# -*- coding: utf-8 -*-
 
import os
import requests
from pathlib import Path
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
 
 
class MMSpider(CrawlSpider):
    name = 'mm_spider'
    allowed_domains = ['www.521609.com']
    start_urls = ['http://www.521609.com']
    # start_urls = ['http://www.521609.com/xiaoyuanmeinv/10464_5.html']
 
    # 自定义配置。自定义配置会覆盖 setting.py 中配置,即 优先级 大于 setting.py 中配置
    custom_settings = {
        'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36',
        # 'LOG_FILE': 'MMSpider.log',
        # 'LOG_LEVEL': 'DEBUG'
        'LOG_ENABLED': False,  # 关闭 scrapy 中的 debug 打印
        'LOG_LEVEL': 'INFO'
    }
 
    rules = (
        # 匹配 包含 xiaohua/ 和 meinv/ 所有 URL
        Rule(LinkExtractor(allow=(r'xiaohua/\d+_?\d+', r'meinv/\d+_?\d+')), callback='parse_img', follow=True),
        Rule(LinkExtractor(allow=(r'xiaohua/', 'meinv/')), follow=True)
    )
 
    # def parse(self, response):
    #     print(response.url)
    #     super(MMSpider, self).parse(response)
    #     pass
 
    def parse_img(self, response):
        spider_name = self.name
        current_url = response.url
        print(f'current_url:{current_url}')
 
        mm_name = response.xpath('//div[@class="title"]/h2/text()').extract_first()
        link_extractor = LinkExtractor(
            allow='uploads/allimg',  # 匹配的 URL
            deny=r'(lp\.jpg)$',      # 排除的 URL。这里排除掉 以 lp.jpg 结尾 的 缩略图 URL
            tags=('img',),           # 要提取链接的 标签
            attrs=('src',),          # 提取的连接
            restrict_xpaths='//div[@class="picbox"]',  # 在 xpath 指定区域 匹配
            deny_extensions=''  # 禁用扩展。默认会把 .jpg 扩展名的URL后缀过滤掉,这里禁用。
        )
        all_links = link_extractor.extract_links(response)
 
        # 可以使用自定义的 item,也可以直接使用 Python 的 dict,
        # 因为 item 本身就是一个 python类型的 dict
        # img_item = MMItem()
        img_item = dict()
        image_urls_list = list()
 
        for link in all_links:
            print(f'\t{mm_name}:{link.url}')
            image_urls_list.append(link.url)
        else:
            img_item['img_name'] = mm_name
            img_item['image_urls'] = image_urls_list
            # yield img_item
            print(f'\t{img_item}')
            for img_url in image_urls_list:
                file_name = img_url.split('/')[-1].split('.')[0]
                dir_path = f'./{mm_name}'
                if not Path(dir_path).is_dir():
                    os.mkdir(dir_path)
                file_path = f'./{mm_name}/{file_name}.jpg'
                r = requests.get(url=img_url)
                if 200 == r.status_code:
                    with open(file_path, 'wb') as f:
                        f.write(r.content)
                else:
                    print(f'status_code:{r.status_code}')
            pass
 
 
if __name__ == '__main__':
    from scrapy import cmdline
    cmdline.execute('scrapy crawl mm_spider'.split())
    pass
  • CrawlSpider 除了从 Spider类 继承过来的属性外,还提供了一个新的属性 rules 来提供跟进链接(link)的方便机制,这个机制更适合从爬取的网页中获取 link 并继续爬取的工作。
  • rules 包含一个或多个 Rule 对象的集合。每个 Rule 对爬取网站的动作定义了特定规则。如果多个 Rule 匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。所以:规则的顺序可以决定获取的数据,应该把 精确匹配的规则放在前面,越模糊的规则放在后面。
  • CrawlSpider 也提供了一个可复写的方法:parse_start_url(response)  当 start_url 的请求返回时,该方法被调用。该方法分析最初的返回值并必须返回一个 Item 对象或一个 Request 对象或者一个可迭代的包含二者的对象

注意:当编写 CrawlSpider 爬虫 的 规则 时,不要使用 parse 作为回调函数。 由于 CrawlSpider 使用 parse 方法来实现其逻辑,如果覆盖了 parse 方法,CrawlSpider 将会运行失败。

parsel 库

parsel 库可以解析HTML和XML,并支持使用Xpath和CSS选择器对内容进行提取和修改,同时还融合了正则表达式的提取功能。parsel灵活且强大,python 最流行的爬虫框架 Scrapy 的选择器就是基于 parsel 做了二次封装。安装:pip install parsel

from parsel import Selector

html = '''
<div>
    <ul>
         <li class="item-0">第1个item</li>
         <li class="item-1"><a href="link2.html">第2个item</a></li>
         <li class="item-0 active"><a href="link3.html"><span class="bold">第3个item</span></a></li>
         <li class="item-1 active"><a href="link4.html">第4个item</a></li>
         <li class="item-0"><a href="link5.html">第5个item</a></li>
     </ul>
 </div>
'''


selector = Selector(text=html)
items = selector.css('.item-0')
print(f'{len(items)}, {type(items)}, {items}')

# 用css选择器提取的节点,输出就是xpath属性而不是css属性,因为css是首先被转化成xpath,真正用于节点提取的是xpath。
# 其中css选择器转化为xpath的过程是由底层 csselect 这个库实现的。
items2 = selector.xpath('//li[contains(@class, "item-0")]')
print(f'{len(items2)}, {type(items)}, {items2}')

for item in items:
    text = item.xpath('.//text()').get()
    print(text)

# get方法只会提取第一个selector对象的结果
result = selector.xpath('//li[contains(@class,"item-0")]//text()').get()
print(result)
# getall方法提取所有selector对象的结果
result = selector.xpath('//li[contains(@class,"item-0")]//text()').getall()
print(result)

# 使用 css。
# *来提取所有子节点,提取文本需要加上::text
result = selector.css(".item-0 *::text").getall()
print(result)
result = selector.css(".item-0.active a::attr(href)").get()  # css方法
print(result)
result = selector.xpath("//li[contains(@class,'item-0') and contains(@class,'active')]/a/@href").get()
print(result)

# 如果在调用css方法时,已经提取进一步的结果了,例如提取了节点的文本值,那么re方法就只会针对节点文本值进行提取
result = selector.css(".item-0").re("link.*")  # css方法
print(result)
# 用re_first方法来提取第一个符合规则的结果:
result = selector.css(".item-0").re_first('<span class="bold">(.*?)</span>')
print(result)

pyquery 库

安装:pip install pyquery

PyQuery 可以对 XML 文档进行 jQuery 查询。 该 API 尽可能类似于 jquery。PyQuery 使用 lxml 库 实现快速 XML 和 HTML 操作。

3、XPath 语法

XPath 可以在 XML 文档中对元素和属性进行查找遍历。XPath 是 W3C XSLT 标准的主要元素,并且 XQuery 和 XPointer 都构建于 XPath 表达之上。在 XPath 中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释以及文档(根)节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。根节点在 xpath 中可以用 "//" 来表示

示例:读取文本并解析节点 (etree 会修复 HTML 文本节点)

from lxml import etree

html_text = '''
<div>
    <ul>
         <li class="item-0"><a href="link1.html">第0个item</a></li>
         <li class="item-1"><a href="link2.html">第1个item</a></li>
         <li class="item-0"><a href="link5.html">第2个item</a>
     </ul>
 </div>
'''

html = etree.HTML(html_text)  # 初始化生成一个XPath解析对象
result = etree.tostring(html, encoding='utf-8')  # 解析对象输出代码
print(type(html))
print(type(result))
print(result.decode('utf-8'))

with open('test.html', 'w', encoding='utf-8') as f:
    f.write(html_text)

# 指定解析器HTMLParser会根据文件修复HTML文件中缺失的如声明信息
html = etree.parse('test.html', etree.HTMLParser())
result = etree.tostring(html)  # 解析成字节
print(f'tostring ---> {result}')
result=etree.tostringlist(html) #解析成列表
print(f'tostringlist ---> {result}')
print(type(html))
print(type(result))

节点关系 ( 父、子、同胞、先辈、后代 )

示例 xml 如下

<bookstore>
    <book>
        <title>Harry Potter</title>
        <author>J K. Rowling</author>
        <year>2005</year>
        <price>29.99</price>
    </book>
</bookstore>
  • 父 (parent):标签 title、author、year、price 的父标签都是 book
  • 子 (children):bookstore的子标签是book,book 的子标签是 title、author、year、price。
  • 同胞 (sibling):拥有相同的父的节点。title、author、year 以及 price 元素都是同胞
  • 先辈 (ancestor):节点的父、父的父,等等。title 的先辈是 book 和 bookstore
  • 后代 (descendant):子子孙孙。bookstore 的后代是 book、title、author、year、price 

选取节点

XPath 使用路径表达式在 XML 文档中选取节点。

nodename    选取此节点的所有子节点
/            从当前节点选取直接子节点
//            从当前节点选取子孙节点
.            选取当前节点
..            选取当前节点的父节点
@            选取属性
*            通配符,选择所有元素节点与元素名
@*            选取所有属性
[@attrib]    选取具有给定属性的所有元素
[@attrib='value']    选取给定属性具有给定值的所有元素
[tag]                选取所有具有指定元素的直接子节点
[tag='text']        选取所有具有指定元素并且文本内容是text节点

示例:一些路径表达式以及表达式的结果:

路径表达式结果
bookstore选取 bookstore 元素的所有子节点。
/bookstore选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!
bookstore/book选取属于 bookstore 的子元素的所有 book 元素。
//book选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang选取名为 lang 的所有属性。

谓语(Predicates)

谓语用来查找某个特定的节点或者包含某个指定的值的节点。谓语被嵌在方括号中。

示例:列出了带有谓语的一些路径表达式,以及表达式的结果:

路径表达式结果
/bookstore/book[1]选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()]选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1]选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3]选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang]选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang=’eng’]选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00]选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

选取未知节点

XPath 通配符可用来选取未知的 XML 元素。

通配符描述
*匹配任何元素节点。
@*匹配任何属性节点。
node()匹配任何类型的节点。

示例:

路径表达式结果
/bookstore/*选取 bookstore 元素的所有子元素。
//*选取文档中的所有元素。
//title[@*]选取所有带有属性的 title 元素。

选取若干路径

通过在路径表达式中使用“|”运算符,可以选取若干个路径。

路径表达式结果
//book/title | //book/price选取 book 元素的所有 title 和 price 元素。
//title | //price选取文档中的所有 title 和 price 元素。
/bookstore/book/title | //price选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。

XPath 运算符

可用在 XPath 表达式中的运算符:( 此表参考来源:XPath 运算符

运算符描述实例返回值
|计算两个节点集//book | //cd返回所有拥有 book 和 cd 元素的节点集
+加法6 + 410
减法6 – 42
*乘法6 * 424
div除法8 div 42
=等于price=9.80如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。
!=不等于price!=9.80如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
<小于price<9.80如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
<=小于或等于price<=9.80如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
>大于price>9.80如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
>=大于或等于price>=9.80如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。
orprice=9.80 or price=9.70如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。
andprice>9.00 and price<9.90如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。
mod计算除法的余数5 mod 21

XPath 使用

python 使用 xpath(超详细):https://www.cnblogs.com/mxjhaima/p/13775844.html
W3school Xpath 教程:https://www.w3school.com.cn/xpath/index.asp
简书:Xpath 高级用法:https://www.jianshu.com/p/1575db75670f

函数、高级用法

@属性

//@class                                含有 class 属性的标签
//div[@*]                                含有属性 的 div
//div[@lang]                           含有 lang 属性的 div
//div[@lang='eng']                  lang 属性等于 eng 的 div
//div[not(@id)]                        不包含id属性 的 div

//div[@class='class1 class2']//span
//div[contains(@class, "class1")]//span
/bookstore/book[price>35.00]
/bookstore/book[price>35.00]/title
/bookstore/*   
//a[text()='标签的直接文本']

与或非:and、or、not()

//span[@type='radio' and @value='99']          多条件 (与)

//span[@name='bruce' and text()='bruce1'][1]      多条件 (与)
//span[@name='bruce'][text()='bruce1'][1]      多条件 (与)
//span[@id='bruce1' or text()='bruce2']           找出多个 (或)

not() 函数通常与返回值为 true 或者 false 的函数组合,比如 contains()、starts-with() 等。

//div[@id='identity' and not(contains(@class, 'aaa'))]
//div[@class='seats']//span[contains(@class,'seat') and not(contains(@class,'empty'))]
//div[@class='seats']//span[not(contains(//span[contains(@class, 'seat')]/@class, 'empty'))]

包含:contains()

//div[contains(@class,'关键字')]
//div[contains(text(),'关键字')]

//div[contains(@class, "class中包含的关键字")]/a/text()        
//div[starts-with(@id,'res')]//table[1]//td[2]//a//span[contains(.,'test') and contains(.,'kpi')]
//li[contains(@class,"aaa")]/a/text()
//li[1][contains(@class,"aaa")]/a/text()
//li[contains(@class,"aaa") and @name="fore"]/a/text()
//li[last()][contains(@class,"aaa")]/a/text()

starts-with()

//div[starts-with(@id,'kw')]     查找以 kw 开头的 div
//div[starts-with(@id,'id123')] 
//div[starts-with(@id,'id456')]//table//td[2]//table//td//a//span[contains(.,'test')]

标签  (不)包含  标签

//*[img]              选择那些包含 <img> 标签作为子节点的父标签。
//span[not(a)]        选择那些不包含 <a> 标签的 <span> 标签。

通过 子节点 定位

//div[div[@id='navigation']]
//div[div[@name='listType']]
//div[p[@name='testname']]

//div[div[@name='listType']]//img
//td[a//font[contains(text(),'包含的关键字')]]//input[@type='checkbox']

排除 指定 标签

示例:查询 <div> 下的所有子孙节点,并且排除掉 <style> 标签。
//div[contains(@id, "example")]//node()[not(self::style)]
        //div[contains(@id, "example")]: 查找整个文档中id 属性包含 "example" 的 <div> 元素。
        //node(): 选择这些 <div> 元素的所有子孙节点(可以是文本节点、元素节点、注释等)

        /node()  如果是这个,则表示 直接子节点。
        [not(self::style)]: 筛选子节点,排除 <style> 元素,即只保留非 <style> 的节点。

示例:选择所有 <div> 标签且其中不包含任何 <style> 标签的后代。
//div[not(descendant::style)] 

索引

注意:xpath 的索引是从 1 开始的。

//div/input[2]
//div[@id='position']/span[3]
//div[@id='position']/span[position()=3]
//div[@id='position']/span[position()>3]
//div[@id='position']/span[position()<3]
//div[@id='position']/span[last()]
//div[@id='position']/span[last()-1]
//li[@class="movie-name"][1]/text()
//li[1]/a/text()             选取第一个li节点
//li[last()]/a/text()        选取最后一个li节点
//li[position()<3]/a/text()  选取位置小于3的li节点,也就是1和2的
//li[last()-2]/a/text()      选取倒数第三个节点
//li[position()>2 and position()<4][contains(@class,"aaa")]/a/text()
//li[last()-2][contains(@class,"aaa")]/a/text()

substring 截取判断

<div data-for="result" id="swfEveryCookieWrap"></div>
//*[substring(@id,4,5)='Every']/@id              截取该属性 定位3,取长度5的字符
//*[substring(@id,4)='EveryCookieWrap']          截取该属性从定位3 到最后的字符
//*[substring-before(@id,'C')='swfEvery']/@id    属性 'C'之前的字符匹配
//*[substring-after(@id,'C')='ookieWrap']/@id    属性'C之后的字符匹配

通配符 *

//*  选择文档中任意层级的所有标签。"//" 可以理解为从根节点开始递归查找,* 指任意标签
//span[@*='bruce']            选择 属性值中有等于 bruce 的所有 span
//*[@name='bruce']           选择 name属性值是 bruce 的 所有 标签

node()、element()、attribute()、text()、string()

//node()  选择所有类型的节点,包括 元素<element>文本Text属性<attribute> 节点。

如果只想选择特定类型的节点,可以使用其他的 XPath 轴,如 child(子节点)、descendant(后代节点)、attribute(属性节点)等。

//element()        选择所有的元素节点
//attribute()      选择所有的属性节点
//text()           选择所有的文本节点

root.xpath('node()')
root.xpath('//node()')
root.xpath('//book/title|//book/price')
root.xpath('//title|//price')
root.xpath('/bookstore/book/title|//price')

注意:节点的文本值不属于属性

text()、/text()、//text()

//a[text()='baidu']
//span[@id='span_id' and contains(text(),'google')]
//div[@class="cls1"]/text()    获取class为cls1的div属于这个节点的文本
//div[@class="cls2"]//text()   获取class为cls2的div节点的子子孙孙的文本

string() 函数用来获取多个标签下的所有文本
示例:string(.)
示例:string(//div[@id="id123"])
对于如下的代码提取所有文本:string(//div[@id="id123"])
<div id="id123">
    我左青龙,
    <span id="tiger">右白虎,
        <ul>上朱雀,
            <li>下玄武。</li>
        </ul>老牛在当中,
    </span>
    龙头在胸口。
<div>

轴:父 (parent)、子 (children)、兄弟 (sibling)、祖先 (ancestor)、子子孙孙 (descendant)

//li[1]/ancestor::*    返回第一个li节点的所有祖先节点
//li[1]/ancestor::div  返回第一个li节点的祖先div节点

.//h3/preceding-sibling::div//text()    当前节点的前一个兄弟div节点的所有文本
//li[1]/attribute::*   返回第一个li节点的所有属性值
//li[1]/child::*              # 获取所有直接子节点
//li[1]/descendant::a         # 获取所有子孙节点的a节点
//li[1]/following::*          # 获取当前子节之后的所有节点
//li[1]/following-sibling::*  # 获取当前节点的所有同级节点
//li[1]/child::a[@href="www"]    查找第1个li节点的所有子节点中 href 属性为www的a节点
//li[1]/descendant::span    返回第1个li节点的所有子孙节点中的span节点
        
下面两个等价:查找父节点中还有class属性的 节点
        //a[@href="link4.html"]/../@class
        //a[@href="link4.html"]/parent::*/@class
下面两个等价:
        //div[span[text()='cur_note']]/descendant::div/span[text()='123']
        //div[span[text()='cur_note']]//div/span[text()='123']
        
//div[span[text()='cur_node']]/parent::div      找父节点是 div 的标签
//div[span[text()='cur_node']]/ancestor::div    找祖先节点是 div 的标签

示例:   
    //input[@id='123']/following-sibling::input    查找下一个兄弟节点
    //input[@id='123']/preceding-sibling::span     查找上一个兄弟节点
    //span[not(contains(text(),'关键字'))]        查找不包含 "关键字" 字段的span


示例:
//div[starts-with(@id,'res')]//table[1]//td[2]//a//span[contains(.,'test')]/ancestor::div[starts-with(@id,'res')]//table[2]//descendant::a[2]
//img/ancestor::*[2]
        /ancestor::*[2]:表示 <img> 标签的第二层祖先(即爷爷标签)。而 [2] 限定了选择第 2 层祖先(爷爷)。也就是说,如果一个 <img> 标签的父标签是 <div>,其祖父标签也是 <div>,则这个表达式会返回祖父标签。
        ancestor::* 会找到所有的祖先标签,
//img/ancestor::div[2]  查找文档中所有的 <img> 元素。从每个 <img> 元素开始,向上查找其第二个祖先 <div> 元素。
示例:
        //img/preceding-sibling::*//text():获取当前节点之前的所有兄弟其子元素的文本。
        //img/preceding-sibling::text():   获取当前节点之前的兄弟节点中的直接文本。
示例://div[starts-with(@id,'res')]//table[1]//td[2]//span[contains(.,'QuickStart')]/../../descendant::img

from lxml import etree

html_content = '''
<div>
    <div>
        <p>p1</p>
        <span>span1</span>
        <p id="target1">target1</p>
        <span>span2</span>
        <p>p2</p>
        <div>
            <p>p3</p>
            <span>span3</span>
            <p id="target2">target2</p>
            <span>span4</span>
            <p>p4</p>    
            <div>
                <span>span5</span>
                <p>p5</p>
            </div>    
        </div>
    </div>
</div>
'''

# 解析 HTML
tree = etree.HTML(html_content)

# 定位目标节点
target_node = tree.xpath('//p[@id="target2"]')[0]  # 选择具有 id="target2" 的 <p> 节点


# 获取最近的祖先 <div>
ancestor_div = target_node.xpath('ancestor::div[1]')
# 使用 XPath 获取目标节点到祖先之间的所有文本
texts_between = ancestor_div.xpath('.//text()')  # 获取所有文本

print(target_node.xpath('ancestor::div[1]/child::*//text()'))
print(target_node.xpath('ancestor::div[1]/child::*/text()'))

# 过滤出目标节点之前的文本
filtered_texts = []
found_target = False

for text in texts_between:
    if text.strip() == target_node.text.strip():
        found_target = True  # 找到目标节点后,开始收集文本
    if found_target:
        filtered_texts.append(text.strip())

# 打印结果
print("Text from target node to its ancestor:")
for text in filtered_texts:
    print(text)

向后(following) / 向前(preceding)

向 后(前) 查找所有节点

following-sibling 可获取当前节点之后的所有同级(兄弟)节点
preceding-sibling 可获取当前节点之前的所有同级(兄弟)节点
    //span[@class="fk_cur"]/../following::a    往下的所有a
    //span[@class="fk_cur"]/../preceding::a    往上的所有a
    //li[1]/following::*[2]                    查找第1个<li>节点后面所有节点的第2个节点
    //li[1]/following-sibling::*
    //li[1]/preceding-sibling::*

示例:豆瓣 爬虫

import re
import json
import datetime
import requests
from lxml import etree

requests.packages.urllib3.disable_warnings()

class DBSpider(object):

    def __init__(self):
        self.custom_headers = {
            'Host': 'movie.douban.com',
            'Connection': 'keep-alive',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                          '(KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,'
                      'image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        },

        # self.proxies = {
        #     'http': '127.0.0.1:8080',
        #     'https': '127.0.0.1:8080'
        # }

        self.s = requests.session()
        self.s.verify = False
        self.s.headers = self.custom_headers
        # self.s.proxies = self.proxies

    def __del__(self):
        pass

    def api_artists_info(self, artists_id=None):
        ret_val = None
        if artists_id:
            url = f'https://movie.douban.com/celebrity/{artists_id}/'
            try:
                r = self.s.get(url)
                if 200 == r.status_code:
                    response = etree.HTML(r.text)

                    artists_name = response.xpath('//h1/text()')
                    artists_name = artists_name[0] if len(artists_name) else ''

                    chinese_name = re.findall(r'([\u4e00-\u9fa5·]+)', artists_name)
                    chinese_name = chinese_name[0] if len(chinese_name) else ''

                    english_name = artists_name.replace(chinese_name, '').strip()

                    pic = response.xpath('//div[@id="headline"]//div[@class="pic"]//img/@src')
                    pic = pic[0] if len(pic) else ''

                    sex = response.xpath('//div[@class="info"]//span[contains(text(), "性别")]/../text()')
                    sex = ''.join(sex).replace('\n', '').replace(':', '').strip() if len(sex) else ''

                    constellation = response.xpath('//div[@class="info"]//span[contains(text(), "星座")]/../text()')
                    constellation = ''.join(constellation).replace('\n', '').replace(':', '').strip() if len(constellation) else ''

                    birthday = response.xpath('//div[@class="info"]//span[contains(text(), "日期")]/../text()')
                    birthday = ''.join(birthday).replace('\n', '').replace(':', '').strip() if len(birthday) else ''

                    place = response.xpath('//div[@class="info"]//span[contains(text(), "出生地")]/../text()')
                    place = ''.join(place).replace('\n', '').replace(':', '').strip() if len(place) else ''

                    occupation = response.xpath('//div[@class="info"]//span[contains(text(), "职业")]/../text()')
                    occupation = ''.join(occupation).replace('\n', '').replace(':', '').strip() if len(occupation) else ''

                    desc = ''.join([x for x in response.xpath('//span[@class="all hidden"]/text()')])

                    artists_info = dict(
                        artistsId=artists_id,
                        homePage=f'https://movie.douban.com/celebrity/{artists_id}',
                        sex=sex,
                        constellation=constellation,
                        chineseName=chinese_name,
                        foreignName=english_name,
                        posterAddre=pic,
                        # posterAddreOSS=Images.imgages_data(pic, 'movie/douban'),
                        birthDate=birthday,
                        birthAddre=place,
                        desc=desc,
                        occupation=occupation,
                        showCount='',
                        fetchTime=str(datetime.datetime.now()),
                    )
                    # print(json.dumps(artists_info, ensure_ascii=False, indent=4))
                    ret_val = artists_info
                else:
                    print(f'status code : {r.status_code}')
            except BaseException as e:
                print(e)
        return ret_val

    def test(self):
        pass


if __name__ == '__main__':
    douban = DBSpider()
    temp_uid = '1044707'
    # temp_uid = '1386515'
    # temp_uid = '1052358'
    # temp_uid = '1052357'
    user_info = douban.api_artists_info(temp_uid)
    print(json.dumps(user_info, ensure_ascii=False, indent=4))
    pass

示例:解析 古文网

打印 诗经 所对应的 URL

import json
import traceback
import requests
from lxml import etree


def parse():
    url = 'https://www.gushiwen.org/guwen/shijing.aspx'
    r = requests.get(url)
    if r.status_code == 200:
        selector = etree.HTML(r.text)
        s_all_type_content = selector.xpath('//div[@class="sons"]/div[@class="typecont"]')
        print(len(s_all_type_content))

        article_list = list()
        for s_type_content in s_all_type_content:
            book_m1 = s_type_content.xpath('.//strong/text()')[0].encode('utf-8').decode('utf-8')
            s_all_links = s_type_content.xpath('.//span/a')
            article_dict = dict()
            for s_link in s_all_links:
                link_name = s_link.xpath('./text()')[0].encode('utf-8').decode('utf-8')
                try:
                    link_href = s_link.xpath('./@href')[0].encode('utf-8').decode('utf-8')
                except BaseException as e:
                    link_href = None
                article_dict[link_name] = link_href
            temp = dict()
            temp[book_m1] = article_dict
            article_list.append(temp)
        print(json.dumps(article_list, ensure_ascii=False, indent=4))

    else:
        print(r.status_code)


if __name__ == '__main__':
    parse()
    pass

4、CSS 选择器

CSS 语法

CSS 选择器 参考手册:http://www.w3school.com.cn/cssref/css_selectors.asp
CSS 选择器 :http://www.runoob.com/cssref/css-selectors.html

选择器示例示例说明CSS
.class.intro选择所有class="intro"的元素1
#id#firstname选择所有id="firstname"的元素1
**选择所有元素2
elementp选择所有<p>元素1
element,elementdiv,p选择所有<div>元素和<p>元素1
element elementdiv p选择<div>元素内的所有<p>元素1
element>elementdiv>p选择所有父级是 <div> 元素的 <p> 元素2
element+elementdiv+p选择所有紧接着<div>元素之后的<p>元素2
[attribute][target]选择所有带有target属性元素2
[attribute=value][target=-blank]选择所有使用target="-blank"的元素2
[attribute~=value][title~=flower]选择标题属性包含单词"flower"的所有元素2
[attribute|=language][lang|=en]选择 lang 属性以 en 为开头的所有元素2
:linka:link选择所有未访问链接1
:visiteda:visited选择所有访问过的链接1
:activea:active选择活动链接1
:hovera:hover选择鼠标在链接上面时1
:focusinput:focus选择具有焦点的输入元素2
:first-letterp:first-letter选择每一个<P>元素的第一个字母1
:first-linep:first-line选择每一个<P>元素的第一行1
:first-childp:first-child指定只有当<p>元素是其父级的第一个子级的样式。2
:beforep:before在每个<p>元素之前插入内容2
:afterp:after在每个<p>元素之后插入内容2
:lang(language)p:lang(it)选择一个lang属性的起始值="it"的所有<p>元素2
element1~element2p~ul选择p元素之后的每一个ul元素3
[attribute^=value]a[src^="https"]选择每一个src属性的值以"https"开头的元素3
[attribute$=value]a[src$=".pdf"]选择每一个src属性的值以".pdf"结尾的元素3
[attribute*=value]a[src*="runoob"]选择每一个src属性的值包含子字符串"runoob"的元素3
:first-of-typep:first-of-type选择每个p元素是其父级的第一个p元素3
:last-of-typep:last-of-type选择每个p元素是其父级的最后一个p元素3
:only-of-typep:only-of-type选择每个p元素是其父级的唯一p元素3
:only-childp:only-child选择每个p元素是其父级的唯一子元素3
:nth-child(n)p:nth-child(2)选择每个p元素是其父级的第二个子元素3
:nth-last-child(n)p:nth-last-child(2)选择每个p元素的是其父级的第二个子元素,从最后一个子项计数3
:nth-of-type(n)p:nth-of-type(2)选择每个p元素是其父级的第二个p元素3
:nth-last-of-type(n)p:nth-last-of-type(2)选择每个p元素的是其父级的第二个p元素,从最后一个子项计数3
:last-childp:last-child选择每个p元素是其父级的最后一个子级。3
:root:root选择文档的根元素3
:emptyp:empty选择每个没有任何子级的p元素(包括文本节点)3
:target#news:target选择当前活动的#news元素(包含该锚名称的点击的URL)3
:enabledinput:enabled选择每一个已启用的输入元素3
:disabledinput:disabled选择每一个禁用的输入元素3
:checkedinput:checked选择每个选中的输入元素3
:not(selector):not(p)选择每个并非p元素的元素3
::selection::selection匹配元素中被用户选中或处于高亮状态的部分3
:out-of-range:out-of-range匹配值在指定区间之外的input元素3
:in-range:in-range匹配值在指定区间之内的input元素3
:read-write:read-write用于匹配可读及可写的元素3
:read-only:read-only用于匹配设置 "readonly"(只读) 属性的元素3
:optional:optional用于匹配可选的输入元素3
:required:required用于匹配设置了 "required" 属性的元素3
:valid:valid用于匹配输入值为合法的元素3
:invalid:invalid用于匹配输入值为非法的元素3

基本 css 选择器

CSS 选择器中,最常用的选择器 如下:

选择器描述举例
*通配选择器,选择所有的元素*
<type>选择特定类型的元素,支持基本HTML标签h1
.<class>选择具有特定class的元素。.class1
<type>.<class>特定类型和特定class的交集。(直接将多个选择器连着一起表示交集)h1.class1
#<id>选择具有特定id属性值的元素#id1

属性选择器

除了最基本的核心选择器外,还有可以 基于属性 的 属性选择器

选择器描述举例
[attr]选取定义attr属性的元素,即使该属性没有值[placeholder]
[attr="val"]选取attr属性等于val的元素[placeholder="请输入关键词"]
[attr^="val"]选取attr属性开头为val的元素[placeholder^="请输入"]
[attr$="val"]选取attr属性结尾为val的元素[placeholder$="关键词"]
[attr*="val"]选取attr属性包含val的元素[placeholder*="入关"]
[attr~="val"]选取attr属性包含多个空格分隔的属性,其中一个等于val的元素[placeholder~="关键词"]
[attr|="val"]选取attr属性等于val的元素或第一个属性值等于val的元素[placeholder|="关键词"]

        <p class="important warning">This paragraph is a very important warning.</p>
        selenium举例: (By.CSS_SELECTOR,'p[class="import warning"]') 
        属性与属性的值需要完全匹配,如上面用p[class='impprtant']就定位不到; 
        部分属性匹配:(By.CSS_SELECTOR,'p[class~="import warning"]'); 
        子串匹配&特定属性匹配: 
        [class^="def"]:选择 class 属性值以 "def" 开头的所有元素 
        [class$="def"]:选择 class 属性值以 "def" 结尾的所有元素 
        [class*="def"]:选择class 属性值中包含子串 "def" 的所有元素 
        [class|="def"]:选择class 属性值等于"def"或以"def-"开头的元素(这个是特定属性匹配)

关系选择器

有一些选择器是基于层级之间的关系,这类选择器称之为关系选择器

选择器描述举例
<selector> <selector>第二个选择器为第一个选择器的后代元素,选取第二个选择器匹配结果.class1 h1
<selector> > <selector>第二个选择器为第一个选择器的直接子元素,选取第二个选择器匹配结果.class1 > *
<selector> + <selector>第二个选择器为第一个选择器的兄弟元素,选取第二个选择器的下一兄弟元素.class1 + [lang]
<selector> ~ <selector>第二个选择器为第一个选择器的兄弟元素,选取第二个选择器的全部兄弟元素.class1 ~ [lang]

        选择 某个元素 的 后代的元素: 
        selenium举例:(By.CSS_SELECTOR,‘div button’)
        div元素的所有的后代元素中标签为button元素,不管嵌套有多深

        选择 某个元素 的 子代元素: 
        selenium举例:(By.CSS_SELECTOR,‘div > button’)
        div元素的所有的子代元素中标签为button元素(>符号前后的空格可有可无)

        一个元素不好定位时,它的兄长元素很起眼,可以借助兄长来扬名,因此不妨称之为 "弟弟选择器".
        即选择某个元素的弟弟元素(先为兄,后为弟): 
        selenium举例: (By.CSS_SELECTOR,'button + li')
        button与li属于同一父元素,且button与li相邻,选择button下标签为li的元素

联合选择器与反选择器

利用 联合选择器与反选择器,可以实现 与和或 的关系。

选择器描述举例
<selector>,<selector>属于第一个选择器的元素或者是属于第二个选择器的元素h1, h2
:not(<selector>)不属于选择器选中的元素:not(html)

伪元素和伪类选择器

CSS选择器支持了 伪元素和伪类选择器。

:active鼠标点击的元素
:checked处于选中状态的元素
:default选取默认值的元素
:disabled选取处于禁用状态的元素
:empty选取没有任何内容的元素
:enabled选取处于可用状态的元素
:first-child选取元素的第一个子元素
:first-letter选取文本的第一个字母
:first-line选取文本的第一行
:focus选取得到焦点的元素
:hover选取鼠标悬停的元素
:in-range选取范围之内的元素
:out-of-range选取范围之外的元素
:lang(<language>)选取lang属性为language的元素
:last-child选取元素的最后一个子元素

CSS选择器的常见语法

高阶:

Selenium 中使用 CSS Selector 

https://www.bbsmax.com/A/MyJxLGE1Jn/

1.  根据 标签 定位 tagName (定位的是一组,多个元素)
        find_element_by_css_selector("div")

2. 根据 id属性 定位 ( 注意:id 使用 # 表示
        find_element_by_css_selector("#eleid")
        find_element_by_css_selector("div#eleid")
        
3. 根据 className 属性 定位(注意:class 属性 使用 . )

        两种方式:前面加上 tag 名称。也可以不加。如果不加 tag 名称时,点不能省略。
        find_element_by_css_selector('.class_value')       
        find_element_by_css_selector("div.eleclass")
        find_element_by_css_selector('tag_name.class_value')

        有的 class_value 比较长,而且中间有空格时,不能把空格原样写进去,那样不能识别。
        这时,空格用点代替,前面要加上 tag_name。
        driver.find_element_by_css_selector('div.panel.panel-email').click()
        # <p class="important warning">This paragraph is a very important warning.</p>
        driver.find_element_by_css_selector('.important')
        driver.find_element_by_css_selector('.important.warning')
        
4. 根据 标签 属性 定位
        两种方式,可以在前面加上 tag 名称,也可以不加。
        find_element_by_css_selector("[attri_name='attri_value']")
        find_element_by_css_selector("input[type='password']").send_keys('密码')
        find_element_by_css_selector("[type='password']").send_keys('密码')
    4.1 精确 匹配
        find_element_by_css_selector("div[name=elename]")  #属性名=属性值,精确值匹配
        find_element_by_css_selector("a[href]") #是否存在该属性,判断a元素是否存在href属性

    注意:如果 class属性值 里带空格,用.来代替空格
    4.2 模糊 匹配
        find_element_by_css_selector("div[name^=elename]") #从起始位置开始匹配
        find_element_by_css_selector("div[name$=name2]") #从结尾匹配
        find_element_by_css_selector("div[name*=name1]") #从中间匹配,包含
    4.3 多属性 匹配
        find_element_by_css_selector("div[type='eletype][value='elevalue']") #同时有多属性
        find_element_by_css_selector("div.eleclsss[name='namevalue'] #选择class属性为eleclass并且name为namevalue的div节点
        find_element_by_css_selector("div[name='elename'][type='eletype']:nth-of-type(1) #选择name为elename并且type为eletype的第1个div节点

5. 定位  子元素 (A>B)
        find_element_by_css_selector("div#eleid>input") #选择id为eleid的div下的所有input节点
        find_element_by_css_selector("div#eleid>input:nth-of-type(4) #选择id为eleid的div下的第4个input节点
        find_element_by_css_selector("div#eleid>nth-child(1)") #选择id为eleid的div下的第一个子节点

6. 定位  后代元素 (A空格B)
        find_element_by_css_selector("div#eleid input") #选择id为eleid的div下的所有的子孙后代的 input 节点
        find_element_by_css_selector("div#eleid>input:nth-of-type(4)+label #选择id为eleid的div下的第4个input节点的相邻的label节点
        find_element_by_css_selector("div#eleid>input:nth-of-type(4)~label #选择id为eleid的div下的第4个input节点之后中的所有label节点

7. 不是 ( 否 )
        find_element_by_css_selector("div#eleid>*.not(input)") #选择id为eleid的div下的子节点中不为input 的所有子节点
        find_element_by_css_selector("div:not([type='eletype'])") #选择div节点中type不为eletype的所有节点

8. 包含 
        find_element_by_css_selector("li:contains('Goa')") # <li>Goat</li>
        find_element_by_css_selector("li:not(contains('Goa'))) # <li>Cat</li>

9. by index
        find_element_by_css_selector("li:nth(5)")

10. 路径  法
        两种方式,可以在前面加上 tag 名称,也可以不加。注意它的层级关系使用大于号">"。
        find_element_by_css_selector("form#loginForm>ul>input[type='password']").send_keys('密码')

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值