☰
关于python简单的爬虫操作(requests和etree)
2026/10/17 3:58:45 网站建设 项目流程

前言


很多初学者一上来就学 Scrapy,结果被框架的目录结构、中间件、管道劝退。其实抓一个公开页面并把字段抠出来这件事,只需要两个东西:一个负责发 HTTP 请求的库,一个负责解析 HTML 的库。前者用requests,后者用lxml.etree,加起来不到五十行就能跑起来。


本文只讲这套最小组合:怎么装、怎么写、每一行在干什么,以及怎样从「抓一个标题」扩展到「抓一列记录」。刻意不引入任何框架、不引入异步,目的是让你先看见完整数据流,再决定要不要上更重的工具。


三点前置说明:其一,爬虫必须合规——遵守 robots.txt、带可识别的 User-Agent、请求间限速、只取公开页面、不绕登录墙与验证码、不并发压站。其二,本文示例的请求目标是 RFC 2606 保留的example.com(专门留给文档演示用,不会打扰任何真实站点),解析用自己构造的 HTML 字符串。其三,本机没有 Python 解释器,也不允许安装第三方库,代码均为逐行推演,未经运行。


还有一个版本前提:本文用 Python 3 的写法。Python 2.7 已于 2020 年 1 月 1 日停止维护,它那套东西(print语句、urllib2、xrange)都已过时——xrange在 Python 3 里被移除,range本身就已经是惰性的。


一、两个库各管一段


先摆清楚分工,后面看代码就不会混:




库层级职责输入输出



requests网络层发请求、拿响应、处理编码URLResponse对象

lxml.etree解析层把 HTML 文本变成树、查询节点str/bytesElement / 列表



安装(在能联网、有 pip 的环境里):


pip install requests lxml

requests是纯 Python 包,装起来通常很顺。lxml依赖 C 库 libxml2,部分平台需要预编译轮子;如果pip install lxml报编译错误,请查阅其官方文档提供的安装说明(以该库官方文档为准)。


二、最小可用脚本


下面是本文的主角,一个完整的最小抓取脚本:


# 适用于 Python 3.8+
import time
import requests
from lxml import etree

# 可识别的 User-Agent,注明身份,便于站点管理员联系
HEADERS = {"User-Agent": "TinyBot/1.0 (+contact: you@example.com)"}

def get_html(url, timeout=10):
"""发一次 GET 请求,返回解析好编码的文本。"""
resp = requests.get(url, headers=HEADERS, timeout=timeout)
resp.raise_for_status() # 非 2xx 一律抛异常
resp.encoding = resp.apparent_encoding # 修正可能的编码猜测
return resp.text

# 本地演示 HTML:真实项目里这行换成 get_html(url) 的返回值
HTML = """
<html><body>
<h1 class="page-title">演示页面</h1>
<ul>
<li><a href="/item/1">条目一</a></li>
<li><a href="/item/2">条目二</a></li>
</ul>
</body></html>
"""

root = etree.HTML(HTML) # 解析成树
title = root.xpath('//h1/text()')[0]
items = root.xpath('//ul/li/a/text()')
print(title) # 演示页面
print(items) # ['条目一', '条目二']

time.sleep(1) # 礼貌:请求之间留间隔

五十行不到,这就是爬虫的完整骨架:请求、解析、取值、限速。


三、逐段拆解


3.1requests.get的四个关注点



  • headers:带一个真实可识别的 User-Agent。默认的python-requests/x.y很容易被直接拒绝,而且也不礼貌——站点管理员不知道该联系谁。注释里写上联系方式是好习惯。

  • timeout:必须给。不写超时的话,对方迟迟不响应,你的脚本会无限期挂住。

  • raise_for_status():把 4xx / 5xx 变成异常。否则你会拿着一个 404 页面当正常内容去解析,出来一堆空值还查不出原因。

  • resp.encoding:requests优先按 HTTP 头里的 charset 解码;头缺失时会退化成 ISO-8859-1,中文网页就会乱码。把它改成resp.apparent_encoding(基于内容的猜测)能救回大部分情况。


apparent_encoding只对文本响应有意义,且只是猜测。真正权威的顺序是:HTTP 头 > HTML 里的 meta 声明 > 内容探测。


3.2etree.HTML干了什么


etree.HTML(text)把字符串按 HTML 规则解析:



  • 它会自动补全未闭合的标签(<li>不写</li>也没事),这是 HTML 解析器与 XML 解析器的关键区别。

  • 返回一个根 Element,可以直接对它调用.xpath()。

  • 如果传入的是bytes,它会看 HTML 里的 meta 编码声明;传str时就按已解码的文本处理。


3.3.xpath()的返回值


.xpath()总是返回列表,哪怕命中的只有一个节点。所以root.xpath('//h1/text()')[0]才是单个字符串。空结果返回[],下标访问会抛IndexError——这是这套组合最常见的运行时错误。


不同表达式返回不同元素类型,务必分清:




表达式返回



//liElement 列表

//li/@class字符串列表(属性值)

//li/text()字符串列表(直接文本)

//li//text()字符串列表(含后代文本)

string(//li)单个str(不是列表)



四、从「抓一个」到「抓一列」


真实需求几乎都是抓一列记录。做法是先把行收敛出来,再在每行内取字段:


# 适用于 Python 3.8+
rows = root.xpath('//li') # 每一行
records = []
for row in rows:
link = row.xpath('./a') # 相对当前行
if not link:
continue
a = link[0]
records.append({
"text": (a.text or "").strip(), # text 可能是 None
"href": a.get("href", ""), # 属性取值,带默认值
})
print(records)

两个细节值得强调:a.text在元素没有直接文本时是None,所以用a.text or ""兜底;a.get("href", "")是 Element 的属性读取方法,属性不存在时返回第二个参数,比a.attrib["href"]安全(后者会抛KeyError)。


如果要抓很多页,把限速写进循环,并遵守 robots.txt 里声明的抓取间隔:


# 适用于 Python 3.8+
import time

def crawl(urls, delay=1.0):
results = []
for u in urls:
try:
html = get_html(u)
except requests.RequestException as exc:
print("跳过:", u, exc) # 单页失败不拖垮整批
continue
root = etree.HTML(html)
results.extend(root.xpath('//h1/text()'))
time.sleep(delay) # 限速:不要并发压站
return results

requests.RequestException是requests异常体系的基类,捕获它可以把连接错误、超时、HTTP 错误统一处理掉,并且不会顺手捕获KeyboardInterrupt这类不该吞的异常。


常见坑点



  1. 不带 User-Agent


❌ 直接用默认 UA 发请求,很多站点直接返回 403。


✅ 设置一个可识别身份的User-Agent,并在注释里留下联系方式。



  1. 不设timeout


❌requests.get(url)—— 对方不响应时脚本永久挂起。


✅requests.get(url, timeout=10)。



  1. 忽略状态码


❌ 拿着 404 页面解析,结果全空。


✅ 先resp.raise_for_status()。



  1. 中文乱码不当回事


❌ 直接resp.text,服务端没声明 charset 时按 ISO-8859-1 解码,中文全乱。


✅ 显式修正resp.encoding,优先信任响应头。



  1. xpath结果直接下标


❌root.xpath('//h1/text()')[0],页面没有 h1 时抛IndexError。


✅ 先取列表判断是否为空,或用next(iter(...), default)。



  1. 用etree.fromstring解析 HTML


❌ 遇到<br>、未闭合标签抛XMLSyntaxError。


✅ HTML 一律用etree.HTML。



  1. 不留请求间隔


❌for u in urls: requests.get(u)一秒几十次。


✅ 每轮time.sleep(delay),尊重 robots.txt。



  1. 裸except吞掉一切


❌except:会把KeyboardInterrupt、SystemExit一起吞掉,Ctrl+C 都停不下来。


✅ 只捕获requests.RequestException这类具体异常。


总结




环节最小写法



请求requests.get(url, headers=..., timeout=...)

校验resp.raise_for_status()

编码resp.encoding = resp.apparent_encoding

解析etree.HTML(resp.text)

取值root.xpath(...),注意总是返回列表

限速循环里time.sleep(delay)



requests加lxml.etree这套最小组合,把「发请求」和「解结构」两件事拆得干干净净,二十来行就能跑。先把它写熟,理解每一步为什么这么写,再考虑要不要上框架——那时你判断框架里每个组件的作用,会清楚得多。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询