从零开始的爬虫(二):使用CSS选择器进行HTML文本解析
·
使用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> - 语法:以井号(#)开头,后跟ID值,如
-
属性选择器:
- 语法:使用方括号([])来包含属性名和值(可选)。
- 作用:选择具有指定属性或属性值的元素。
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'] - 在 CSS 选择器中,
-
-
选取单个元素具有多个class
- 如果你需要选取同时具有多个class的元素,可以使用
.class1.class2的语法。这意味着元素需要同时具有这两个class。
- 如果你需要选取同时具有多个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表示选取同时具有class1和class2的元素。注意它们之间的空格区别。 - 大小写敏感:class 名称在 HTML 中是不区分大小写的,但在 CSS 选择器中,通常是大小写敏感的(取决于 HTML 文档的具体实现)。
- 空格问题:在选择器中,
2.2 使用 pyquery 进行网页识别
-
使用函数获取一个网页的 HTML 文本、将 URL 链接拼接等函数在上一篇文章中已经实现,故本篇文章仅仅考虑使用 CSS 选择器提取网页的信息
-
基础知识
- 元素(Element):
- 元素是 HTML 或 XML 文档的基本构建块,它们定义了文档的结构和内容。
- 每个元素由一个开始标签(如
<p>)、一个结束标签(如</p>)以及可能包含在这两个标签之间的内容组成。 - 元素可以包含文本、其他元素(子元素),或者两者的组合。
- 例如,在
<a href="https://example.com">Link</a>中,<a>是一个元素,它包含文本 “Link” 并且有一个属性href。
- 属性(Attribute):
- 属性是元素的修饰符,它们提供有关元素的额外信息。
- 属性以键值对的形式出现在元素的开始标签中,如
id="myId"或class="myClass"。 - 属性可以用于定义元素的样式、行为或与其他资源的链接。
- 每个元素可以有零个或多个属性,这些属性定义了元素的特性。
- 继续上面的例子,
href="https://example.com"是<a>元素的一个属性,它指定了链接的目标 URL。
- 元素(Element):
-
详情页链接识别
- 可见对应详情页链接再元素 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 - 可见对应详情页链接再元素 a 下的 href,故直接使用 class 选择器 doc(“a.name”),再使用 attr(‘href’)获取详情页链接即可
-
详情页识别
- 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 }
- attr(name):
-
完整代码实现
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()
更多推荐




所有评论(0)