使用CSS选择器进行 HTML 解析

在上一篇文章中,我们尝试了通过爬取网页的 HTML 文本并使用正则表达式来提取所需信息。然而,正则表达式在处理 HTML 时显得既复杂又局限。因此,在本篇文章中将转而采用更为高效且灵活的方法:利用 XPath 和 CSS 选择器进行信息提取。
XPath 和 CSS 选择器作为专门用于选择和提取 HTML 元素内容的工具,它们提供了更为直观和简洁的方式来定位所需数据。相较于正则表达式,这两种方法不仅降低了提取数据的复杂性,还显著提高了提取的准确性和灵活性。parsel 库在解析 HTML 文本方面表现出色,并且支持使用 XPath 和 CSS 选择器进行内容提取。

2.1 CSS 选择器

CSS 选择器中较为常用的选择器有 class 选择器、id 选择器、属性选择器、伪类选择器,下面我就介绍一下各个选择器的书写规范。同时下述代码都是使用 parsel 库中的 Selector 类和 css 方法进行实现

  • 常用的选择器介绍

    • class 选择器

      • class 选择器是最为常用的选择器之一
      • 语法:class 选择器由一个点(.)起始,后接类名。
      • 作用:class 选择器用于选择那些具有特定类名的元素。多个元素可以共享同一个类名,或是一个元素使用多个类名
        CSS:
      .highlight {
          background-color: yellow;
          color: red;
          font-weight: bold;
      }
      

      HTML:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Class Selector Example</title>
          <link rel="stylesheet" href="styles.css">
      </head>
      <body>
          <div class="highlight">这是一个被高亮显示的div元素。</div>
          <p class="highlight">这是一个被高亮显示的段落。</p>
          <span class="highlight">这是一个被高亮显示的span元素。</span>
      </body>
      </html>
      
    • id选择器:

      • 语法:以井号(#)开头,后跟ID值,如 #uniqueID
      • 作用:选择具有唯一ID的元素。由于ID在文档中应该是唯一的,因此ID选择器通常用于选中单个元素。
        CSS:
      /* 选择id为"header"的元素 */
      #header {
          background-color: lightblue;
          padding: 20px;
      }
      

      HTML

      <div id="header">这是一个标题</div>
      
    • 属性选择器:

      • 语法:使用方括号([])来包含属性名和值(可选)。
      • 作用:选择具有指定属性或属性值的元素。
        CSS:
      	/* 选择所有具有"type"属性的input元素 */
      		input[type] {
      		    border: 1px solid black;
      		}
      		
      		/* 选择 type 属性值为"text"的 input 元素 */
      		input[type="text"] {
      		background-color: lightyellow;
      		}```
      
      
      

      HTML:

      <input type="text" placeholder="这是一个文本输入框">
      <input type="password" placeholder="这是一个密码输入框">
      
    • 伪类选择器

      • 语法:以冒号(:)开头,后跟伪类名。
      • 作用:选择处于特定状态的元素或具有特定特征的元素。
        CSS:
      /* 选择鼠标悬停时的链接 */
      a:hover {
          color: red;
      }
      
      /* 选择获得焦点的input元素 */
      input:focus {
          border-color: blue;
      }
      
      /* 选择每个列表的第一个子项 */
      li:first-child {
          font-weight: bold;
      }
      

      HTML:

      
      <a href="#">这是一个链接</a>
      <input type="text" placeholder="焦点时会变蓝">
      <ul>
          <li>第一个列表项(会加粗)</li>
          <li>第二个列表项</li>
      </ul>
      
    • 子选择器

      • 在 CSS 选择器中,> 被称为子选择器(child combinator),它用于选择直接子元素。当在 CSS 选择器中使用 > 时,它表示“直接子代”,意味着只选择那些直接作为某个元素子元素的元素,而不包括更深层次的后代元素。
      • 提取直接子元素
      from parsel import Selector
      
      html_content = '''
      
      <div class="parent">
          <div class="child">Child 1</div>
          <div class="child">
              <div class="grandchild">Grandchild 1</div>
          </div>
      </div>
      '''
      selector = Selector(text=html_content)
      # 选择具有 "child" 类的直接子元素
      # 而不会提取更深层的元素,诸如div class="grandchild"的元素
      child_elements = selector.css('.parent > .child')
      
      # 提取文本
      
      child_text = child_elements.get().strip()
      print(child_text)
      
      # 输出: Child 1
      
      
      • 提取单个孙元素
      from parsel import Selector
      
      html_content = """
      <div class="parent">
          <div>
              <div class="grandchild">Grandchild 1</div>
          </div>
      </div>
      """
      
      selector = Selector(text=html_content)
      
      # 使用 > 选择器选择孙元素
      grandchild_elements = selector.css('div.parent > div > div.grandchild')
      
      # 提取文本
      grandchild_text = grandchild_elements.get().strip()
      print(grandchild_text)  # 输出: Grandchild 1
      
      
      • 叠加使用 > 选择器选择孙元素
      from parsel import Selector
      
      html_content = """
      <div class="parent">
          <div>
              <div class="child">孙元素 1</div>
              <div class="child">孙元素 2</div>
          </div>
          <div>
              <div class="child">孙元素 3</div>
          </div>
      </div>
      """
      
      selector = Selector(text=html_content)
      
      # 使用 CSS 选择器选择孙元素
      grandchildren = selector.css('div.parent > div > div.child')
      
      # 提取所有匹配的孙元素的文本
      grandchildren_texts = grandchildren.getall()
      
      # 打印结果
      print(grandchildren_texts)  # 输出: ['孙元素 1', '孙元素 2', '孙元素 3']
      
  • 选取单个元素具有多个class

    • 如果你需要选取同时具有多个class的元素,可以使用.class1.class2的语法。这意味着元素需要同时具有这两个class。
from bs4 import BeautifulSoup
# html
html = '<div class="class1 class2 class3">Content</div>'
soup = BeautifulSoup(html, 'html.parser')

# 选取具有 class1 和 class2 的元素
element = soup.select_one('.class1.class2')
print(element.text)  # 输出: Content
from parsel import Selector

html = '<div class="class1 class2 class3">Content</div>'

# 创建 Selector 对象
sel = Selector(text=html)

# 选取具有 class1 和 class2 的元素
element = sel.css('.class1.class2').get()
# 打印元素的文本内容
text = sel.css('.class1.class2::text').get()
print(text.strip())  

# 输出: Content
  • 选取具有任意一个 class
    • 如果需要选取具有任意一个 class 的元素,可以分别写多个选择器,并使用逗号分隔。
from parsel import Selector

html = '''
<div class="class1">Content 1</div>
<div class="class2">Content 2</div>
'''

# 创建 Selector 对象
sel = Selector(text=html)

# 选取具有 class1 或 class2 的元素
elements = sel.css('.class1, .class2')

# 遍历元素并打印文本内容
for element in elements:
    print(element.get())

# 输出:
# <div class="class1">Content 1</div>
# <div class="class2">Content 2</div>

# 若是只想要文本内容,而不是整个HTML标签,可以使用.getall()结合文本提取
# 同时若是仅仅想得到第一个文本,那使用get()即可
texts = elements.css('::text').getall()
for text in texts:
    print(text.strip())

# 输出:
# Content 1
# Content 2
  • 组合使用 class 和其他选择器
    • 同时还还可以将 class 选择器与其他选择器(如 id 选择器、属性选择器、伪类选择器等)组合使用。下述是关于 id 选择器、属性选择器、伪类选择器等常用选择器的解释和代码示例。
from parsel import Selector

html_text = """
<html>
<head>
    <title>示例页面</title>
</head>
<body>
    <div id="header">这是一个标题</div>
    <input type="text" class="input-field" placeholder="这是一个文本输入框">
    <input type="password" class="input-field" placeholder="这是一个密码输入框">
    <a href="#" class="link">这是一个链接</a>
    <ul>
        <li class="list-item first">第一个列表项</li>
        <li class="list-item">第二个列表项</li>
    </ul>
</body>
</html>
"""

# 创建 Selector 对象
sel = Selector(text=html_text)
# 根据上述提到的CSS代码书写对应的选择器
# 使用 id 选择器
header = sel.css('#header').get()
print(f"id选择器获取的内容: {header}")

# 使用属性选择器
text_input = sel.css('input[type="text"]').get()
print(f"属性选择器获取的内容: {text_input}")

# 使用伪类选择器(这里使用 :first-child)
first_list_item = sel.css('li:first-child').get()
print(f"伪类选择器获取的内容: {first_list_item}")

# 也可以结合使用,比如选择第一个具有特定类的列表项
first_list_item_with_class = sel.css('li.list-item:first-child').get()
print(f"结合使用的选择器获取的内容: {first_list_item_with_class}")
  • 注意事项
    • 空格问题:在选择器中,.class1 .class2 表示选取后代元素中具有 class2 的元素,而 .class1.class2 表示选取同时具有 class1class2 的元素。注意它们之间的空格区别。
    • 大小写敏感:class 名称在 HTML 中是不区分大小写的,但在 CSS 选择器中,通常是大小写敏感的(取决于 HTML 文档的具体实现)。

2.2 使用 pyquery 进行网页识别

  • 使用函数获取一个网页的 HTML 文本、将 URL 链接拼接等函数在上一篇文章中已经实现,故本篇文章仅仅考虑使用 CSS 选择器提取网页的信息

  • 基础知识

    1. 元素(Element):
      • 元素是 HTML 或 XML 文档的基本构建块,它们定义了文档的结构和内容。
      • 每个元素由一个开始标签(如 <p>)、一个结束标签(如 </p>)以及可能包含在这两个标签之间的内容组成。
      • 元素可以包含文本、其他元素(子元素),或者两者的组合。
      • 例如,在 <a href="https://example.com">Link</a> 中,<a> 是一个元素,它包含文本 “Link” 并且有一个属性 href
    2. 属性(Attribute):
      • 属性是元素的修饰符,它们提供有关元素的额外信息。
      • 属性以键值对的形式出现在元素的开始标签中,如 id="myId"class="myClass"
      • 属性可以用于定义元素的样式、行为或与其他资源的链接。
      • 每个元素可以有零个或多个属性,这些属性定义了元素的特性。
      • 继续上面的例子,href="https://example.com"<a> 元素的一个属性,它指定了链接的目标 URL。
  • 详情页链接识别

    • 可见对应详情页链接再元素 a 下的 href,故直接使用 class 选择器 doc(“a.name”),再使用 attr(‘href’)获取详情页链接即可
      在这里插入图片描述
    def parse_index(html):
      doc=pq(html)
      items=doc('.name').items()
      print(items)
      for item in items:
          # 从items中获得每一个详情页的URL,并返回details_url
          href=item.attr('href')
          details_url=urljoin(BASE_URL, href)
          logging.info('get item url %s', details_url)
          yield details_url
    
  • 详情页识别

    • attr(name):
      • attr 方法用于提取选中元素的指定属性的值。
      • 它接受一个参数 name,即你想要提取的属性的名称。
      • 如果元素有多个属性,你可以使用 attr 方法来获取特定的一个。
      • 如果指定的属性不存在,attr 方法将返回 None
    from parsel import Selector
    
    html_content = '<div id="myId" class="myClass">Hello World!</div>'
    selector = Selector(text=html_content)
    div_selector = selector.css('div')
    
    # 提取 id 属性
    div_id = div_selector.attr('id')
    print(div_id)  # 输出: myId
    
    # 提取不存在的属性
    non_existent_attr = div_selector.attr('non_existent')
    print(non_existent_attr)  # 输出: None
    
    • text:
      • text 方法用于提取选中元素的文本内容。
      • 它不接收任何参数,直接返回元素内部的文本。
      • 如果选中的元素包含多个子元素,text 方法将返回所有子元素的文本内容,合并成一个字符串。
      • 如果元素没有文本内容或者是一个空元素,text 方法将返回空字符串。
      def parse_detail(html):
      
          doc = pq(html)
          # 使用attr()方法获取属性值
          cover = doc('img.cover').attr('src')
          # 使用子选择器的方式悬着a元素的直接子元素h2的文本值
          name = doc('a > h2').text()
          # 使用items()方法获取所有匹配的元素,并返回一个迭代器
          categories = [item.text() for item in doc('.categories button span').items()]
          # 使用contains()方法获取包含指定文本的元素
          published_at = doc('.info:contains(上映)').text()
          # 使用正则表达式匹配日期
          published_at = re.search('(\d{4}-\d{2}-\d{2})', published_at).group(1) \
              if published_at and re.search('\d{4}-\d{2}-\d{2}', published_at) else None
          # 使用text()方法获取文本值
          drama = doc('.drama p').text()
          # 使用text()方法获取文本值
          score = doc('p.score').text()
          score = float(score) if score else None
          return {
              'cover': cover,
              'name': name,
              'categories': categories,
              'published_at': published_at,
              'drama': drama,
              'score': score
          }
      
  • 完整代码实现

import requests
import logging
import re
from pyquery import PyQuery as pq
from urllib.parse import urljoin
import multiprocessing


logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(levelname)s: %(message)s')

BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10
MONGO_CONNECTION_STRING = 'mongodb://localhost:27017'



def scrape_page(url):

    logging.info('scraping %s...', url)
    try:
        response = requests.get(url)
        if response.status_code == 200:
            return response.text
        logging.error('get invalid status code %s while scraping %s', response.status_code, url)
    except requests.RequestException:
        logging.error('error occurred while scraping %s', url, exc_info=True)


def scrape_index(page):

    index_url = f'{BASE_URL}/page/{page}'
    return scrape_page(index_url)


def parse_index(html):

    doc=pq(html)
    items=doc('.name').items()
    print(items)
    for item in items:
        # 从items中获得每一个详情页的URL,并返回details_url
        href=item.attr('href')
        details_url=urljoin(BASE_URL, href)
        logging.info('get item url %s', details_url)
        yield details_url

def scrape_detail(url):

    return scrape_page(url)


def parse_detail(html):
    doc = pq(html)
    # 使用attr()方法获取属性值
    cover = doc('img.cover').attr('src')
    # 使用text()方法获取文本值
    name = doc('a > h2').text()
    # 使用items()方法获取所有匹配的元素,并返回一个迭代器
    categories = [item.text() for item in doc('.categories button span').items()]
    # 使用contains()方法获取包含指定文本的元素
    published_at = doc('.info:contains(上映)').text()
    # 使用正则表达式匹配日期
    published_at = re.search('(\d{4}-\d{2}-\d{2})', published_at).group(1) \
        if published_at and re.search('\d{4}-\d{2}-\d{2}', published_at) else None
    # 使用text()方法获取文本值
    drama = doc('.drama p').text()
    # 使用text()方法获取文本值
    score = doc('p.score').text()
    score = float(score) if score else None
    return {
        'cover': cover,
        'name': name,
        'categories': categories,
        'published_at': published_at,
        'drama': drama,
        'score': score
    }


def main(page):

    index_html = scrape_index(page)
    detail_urls = parse_index(index_html)
    for detail_url in detail_urls:
        detail_html = scrape_detail(detail_url)
        data = parse_detail(detail_html)
        logging.info('get detail data %s', data)



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

加入社区!打开量化的大门,首批课程上线啦!

更多推荐