<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>TraceCloud 学习中心</title>
    <link>https://tc.xfei.tech/learn/</link>
    <description>TraceCloud 学习中心沉淀爬虫实战方法与靶场解题思路</description>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 12 Jun 2026 00:23:22 GMT</lastBuildDate>
    <atom:link href="https://tc.xfei.tech/learn/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title><![CDATA[XPath 教程：爬虫如何用 XPath 定位网页元素]]></title>
      <link>https://tc.xfei.tech/learn/xpath-tutorial-for-crawler/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/xpath-tutorial-for-crawler/</guid>
      <pubDate>Thu, 11 Jun 2026 22:23:10 GMT</pubDate>
      <description><![CDATA[XPath 是爬虫里精准定位网页元素的核心技能，本文从节点选取、谓语、轴运算到 Python lxml 实战，全面讲解 XPath 语法与避坑指南。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/xpath-tutorial-for-crawler.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/xpath-tutorial-for-crawler.webp" alt="XPath 教程：爬虫如何用 XPath 定位网页元素" /></p>
<h2 id="-xpath">为什么爬虫要学 XPath？</h2>
<p>CSS 选择器你会了，但遇到这类情况就不够用了：</p>
<ul>
<li>要选&quot;父节点下第二个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">li</code> 里的链接&quot;</li>
<li>要选&quot;所有带 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 属性且包含 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/detail/</code> 的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 标签&quot;</li>
<li>要选&quot;当前节点的爷爷节点里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">span</code> 文本&quot;</li>
</ul>
<p>这些用 CSS 选择器能做到吗？能，但会很绕，甚至做不到。</p>
<p>XPath 就是来解决这个问题的——它能精准定位 DOM 树里<strong>任意位置</strong>的元素，不依赖层级关系的 CSS 类名，而是用路径表达式一层层找过去。</p>
<p>CSS 选择器是&quot;猜&quot;，XPath 是&quot;指&quot;。</p>
<p>学完这篇，你能做到：</p>
<ol>
<li>用路径表达式选任意节点</li>
<li>用谓语精确找到第 N 个元素或特定属性</li>
<li>用轴（axis）向上找父节点、向下找子孙节点</li>
<li>在 Python 的 lxml 库里写 XPath 表达式</li>
</ol>
<hr>
<h2 id="xpath--css-">XPath 是什么？和 CSS 选择器有什么区别？</h2>
<p>XPath 全称是 XML Path Language，最初是用来在 XML 文档里定位节点的语言。
后来被广泛引入了 HTML 解析领域，因为它本质上是一套&quot;在树状结构里找节点&quot;的语法。</p>
<p><strong>简单对比：</strong></p>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;"></th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">CSS 选择器</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">XPath</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">起源</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">为样式设计</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">为 XML 解析设计</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选父节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ul &gt; li</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li/parent::ul</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选第 N 个</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">:nth-child(2)</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[2]</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选属性</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a[href]</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//a[@href]</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选文本</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">不直接支持</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span/text()</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选祖先</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">不支持</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span/ancestor::div</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">上手难度</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">低</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">中</td>
</tr>
</tbody>
</table></div>
<p>CSS 选择器适合简单快速的定位，XPath 适合复杂精准的定位。
两者不是替代关系，而是互补关系——爬虫老手通常两个都会，需要哪个用哪个。</p>
<hr>
<h2 id="xpath-">XPath 基础语法：节点选取怎么写？</h2>
<p>XPath 用&quot;路径表达式&quot;来描述节点位置，就像文件系统里的路径。</p>
<h3 id="heading">最基本的五种表达式</h3>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">表达式</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">含义</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">示例</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">nodename</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选取该名字的所有子节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code> → 所有 div</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">从根节点选取（绝对路径）</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/html/body</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">从匹配位置的当前节点往下选（相对路径）</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div//a</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">当前节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">..</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">当前节点的父节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">@</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">选取属性</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">@href</code></td>
</tr>
</tbody>
</table></div>
<p>看一个 HTML 结构：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;html&gt;
  &lt;body&gt;
    &lt;div class=&quot;container&quot;&gt;
      &lt;ul class=&quot;movie-list&quot;&gt;
        &lt;li class=&quot;movie-item&quot;&gt;
          &lt;a href=&quot;/movie/1&quot;&gt;电影标题 A&lt;/a&gt;
        &lt;/li&gt;
        &lt;li class=&quot;movie-item&quot;&gt;
          &lt;a href=&quot;/movie/2&quot;&gt;电影标题 B&lt;/a&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>对应 XPath：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//ul                   → 选取所有 ul
//body/div              → 选取 body 下的 div
//div//a               → 选取 div 内任意层级的所有 a
//a/@href              → 选取所有 a 标签的 href 属性值
//li[1]                → 选取第一个 li
</code></pre>
<hr>
<h2 id="-n-">谓语怎么用？如何精确找到第 N 个元素或特定属性？</h2>
<p>谓语就是写在方括号里的条件 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[条件]</code>，可以精确筛选节点。</p>
<h3 id="heading-1">按索引定位：第一个、第二个、最后一个</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[1]                → 第一个 li（XPath 从 1 开始，不是 0）
//li[last()]           → 最后一个 li
//li[last()-1]         → 倒数第二个 li
//li[position() &lt; 3]   → 前两个 li
</code></pre>
<h3 id="classhrefid">按属性值定位：class、href、id</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div[@class=&quot;container&quot;]          → class 等于 container 的 div
//a[@href=&quot;/movie/1&quot;]             → href 等于指定值的 a
//div[@id]                         → 有 id 属性的所有 div
//div[not(@id)]                   → 没有 id 属性的所有 div
</code></pre>
<h3 id="heading-2">按文本内容定位</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span[text()=&quot;下一页&quot;]              → 文本等于&quot;下一页&quot;的 span
//span[contains(text(),&quot;价格&quot;)]      → 文本包含&quot;价格&quot;的 span
//a[contains(@href,&quot;/detail&quot;)]       → href 包含 &quot;/detail&quot; 的 a
</code></pre>
<h3 id="heading-3">组合条件：多个谓语叠加</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[@class=&quot;movie-item&quot;][1]         → class 为 movie-item 的第一个 li
//div[@id=&quot;main&quot;]//a[@href][2]       → id 为 main 的 div 下，第二个带 href 的 a
</code></pre>
<hr>
<h2 id="xpath--1">XPath 轴怎么用？父子兄弟节点定位</h2>
<p>轴（Axis）是 XPath 独有的能力——可以往当前节点的上方或下方任意方向查找，这是 CSS 选择器做不到的。</p>
<p>语法：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//轴::节点测试[谓语]</code></p>
<h3 id="heading-4">常用轴</h3>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">轴</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">含义</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">示例</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">parent::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">父节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span/parent::div</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">child::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">子节点（默认，可省略）</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div/child::a</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">descendant::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">所有后代节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div/descendant::span</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ancestor::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">所有祖先节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span/ancestor::div</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">following-sibling::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">后续兄弟节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//h2/following-sibling::p</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">preceding-sibling::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">前置兄弟节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//td/preceding-sibling::th</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">self::</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">当前节点自身</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span[self::span[@class=&quot;price&quot;]]</code></td>
</tr>
</tbody>
</table></div>
<h3 id="heading-5">实战例子</h3>
<p><strong>找到当前节点的父节点：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span[@class=&quot;price&quot;]/parent::div
</code></pre>
<p><strong>找到所有兄弟节点：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[@class=&quot;active&quot;]/following-sibling::li
</code></pre>
<p><strong>找到爷爷节点里的某个属性：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span/ancestor::div[@id=&quot;article&quot;]//h1
</code></pre>
<p><strong>选中当前节点的文本再往父节点走：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//text()[.=&quot;登录&quot;]/parent::button
</code></pre>
<hr>
<h2 id="python--xpathlxml--etree-">Python 爬虫里怎么用 XPath？lxml + etree 示例</h2>
<p>Python 里用 XPath，主要通过 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">lxml</code> 库解析 HTML 并提取数据：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from lxml import etree

html = &quot;&quot;&quot;
&lt;html&gt;
  &lt;body&gt;
    &lt;div class=&quot;movie-list&quot;&gt;
      &lt;li class=&quot;movie&quot;&gt;
        &lt;span class=&quot;title&quot;&gt;肖申克的救赎&lt;/span&gt;
        &lt;span class=&quot;score&quot;&gt;9.7&lt;/span&gt;
      &lt;/li&gt;
      &lt;li class=&quot;movie&quot;&gt;
        &lt;span class=&quot;title&quot;&gt;霸王别姬&lt;/span&gt;
        &lt;span class=&quot;score&quot;&gt;9.6&lt;/span&gt;
      &lt;/li&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

selector = etree.HTML(html)

# 选所有电影标题
titles = selector.xpath('//span[@class=&quot;title&quot;]/text()')
print(titles)  # ['肖申克的救赎', '霸王别姬']

# 选第一部电影的分数
score = selector.xpath('//li[1]//span[@class=&quot;score&quot;]/text()')
print(score)  # ['9.7']

# 选包含&quot;救赎&quot;的标题
title = selector.xpath('//span[contains(text(),&quot;救赎&quot;)]/text()')
print(title)  # ['肖申克的救赎']

# 获取 href 属性
links = selector.xpath('//a/@href')
print(links)  # ['/movie/1', '/movie/2']

# 选中所有 a 的文本和对应 href
for a in selector.xpath('//a'):
    text = a.xpath('text()')[0] if a.xpath('text()') else ''
    href = a.xpath('@href')[0] if a.xpath('@href') else ''
    print(text, href)
</code></pre>
<h3 id="-lxml--xpath-">常用 lxml + XPath 方法</h3>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">方法</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('//xpath')</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">返回列表，所有匹配结果</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('//xpath')[0]</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">取第一个匹配结果</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('string(//xpath)')</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">取拼接后的完整文本</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('//xpath/@href')</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">取属性值列表</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('count(//li)')</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">统计匹配数量</td>
</tr>
</tbody>
</table></div>
<hr>
<h2 id="xpath--css--1">XPath 和 CSS 选择器怎么选？哪个更好用？</h2>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">场景</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">推荐</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">原因</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">页面结构简单，类名清晰</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS 选择器</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">写起来更短，更直观</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">需要定位第 N 个元素</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">XPath</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">:nth-child</code> 语法繁琐</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">需要取文本内容</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">XPath</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS 无法直接取文本</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">需要向上找父节点/祖先</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">XPath</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS 不支持向上查找</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">需要定位带特定属性的元素</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">两者皆可</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a[href]</code> 和 XPath <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//a[@href]</code> 等价</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">需要统计节点数量</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">XPath</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">count()</code> 函数比 CSS 方便</td>
</tr>
</tbody>
</table></div>
<p><strong>建议：</strong> 先用 CSS 选择器搞定简单的，遇到复杂定位再上 XPath。两种工具配合使用，爬虫老手都是这样干的。</p>
<hr>
<h2 id="xpath--2">XPath 常见报错与处理</h2>
<h3 id="1-">1. 空列表：匹配不到元素</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">results = selector.xpath('//div[@class=&quot;movie&quot;]//a/text()')
# results = []  ← 空列表
</code></pre>
<p>检查：</p>
<ol>
<li>类名拼写是否正确（大小写敏感）</li>
<li>页面是否是动态加载（需要用 Selenium/Playwright 先生成 HTML）</li>
<li>用浏览器开发者工具复制实际 XPath 检查</li>
</ol>
<h3 id="2-">2. 下标越界</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">title = selector.xpath('//li[1]//span/text()')[0]
# IndexError: list index out of range
</code></pre>
<p>XPath 从 1 开始，不是 0。如果页面结构不确定，先判断：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">titles = selector.xpath('//li//span/text()')
if titles:
    print(titles[0])
</code></pre>
<h3 id="3-">3. 属性名拼错</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//img[@src]       ✅ 正确
//img[@scr]       ❌ 错误（scr 是常见拼写错误）
</code></pre>
<h3 id="4-">4. 文本含空格或换行导致匹配失败</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;span&gt;   价格   &lt;/span&gt;
</code></pre>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span[text()=&quot;价格&quot;]              ❌ 失败（文本有空格）
//span[contains(text(),&quot;价格&quot;)]     ✅ 成功
</code></pre>
<hr>
<h2 id="xpath--3">XPath 优化技巧：写得更精准、更稳定</h2>
<p><strong>技巧 1：用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">@class</code> 代替没有语义的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code></strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div/div/div/span                ❌ 脆弱，层级一改就失效
//div[@class=&quot;movie-card&quot;]//span  ✅ 语义稳定，不依赖具体层级
</code></pre>
<p><strong>技巧 2：用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">contains()</code> 处理动态生成的类名</strong></p>
<p>有些页面类名是动态生成的，比如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">class=&quot;item-83271&quot;</code>：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[contains(@class,&quot;item-&quot;)]//a   → 只要 class 里含 &quot;item-&quot; 就能匹配
</code></pre>
<p><strong>技巧 3：用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">string()</code> 取完整文本（避免空白节点干扰）</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">string(//div[@class=&quot;content&quot;])   → 拼接所有子节点文本，自动去除空白
text()                              → 只取当前节点直接文本，可能遗漏子节点内容
</code></pre>
<p><strong>技巧 4：用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">normalize-space()</code> 处理首尾空格</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span[normalize-space(text())=&quot;价格&quot;]   → 自动去除首尾空格再匹配
</code></pre>
<p><strong>技巧 5：避免使用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">@id</code> 做定位（ID 常动态生成）</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div[@id=&quot;product-price&quot;]        ❌ ID 常动态变化，维护成本高
//span[contains(@class,&quot;price&quot;)]  ✅ 用类名或属性值组合更稳定
</code></pre>
<hr>
<h2 id="heading-6">常见问题</h2>
<h3 id="xpath--css--2">XPath 和 CSS 选择器哪个更快？</h3>
<p>在 lxml 里，CSS 选择器会被转成 XPath 执行，速度差异可以忽略。选你能写得更准的，而不是更快的。</p>
<h3 id="-javascript--xpath-">页面是 JavaScript 动态生成的，能用 XPath 吗？</h3>
<p>能，但要先拿到渲染后的 HTML。用 Selenium 或 Playwright 打开页面，等内容加载完成，再提取 HTML 给 lxml 解析：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(&quot;https://example.com&quot;)
    page.wait_for_selector(&quot;.movie-item&quot;)  # 等内容加载完成
    html = page.content()
    browser.close()

from lxml import etree
selector = etree.HTML(html)
titles = selector.xpath('//span[@class=&quot;title&quot;]/text()')
</code></pre>
<h3 id="-xpath--lxml-">为什么 XPath 在浏览器开发者工具里能选中，但 lxml 解析不出来？</h3>
<p>浏览器里的 XPath 基于渲染后的 DOM，有可能有隐藏的命名空间（namespace）导致解析失败。解决方法是使用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">etree.HTML()</code> 而不是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">etree.XML()</code>，或者用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">lxml.html.fromstring()</code> 解析：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from lxml import html
selector = html.fromstring(html_source)
</code></pre>
<h3 id="xpath--json-">XPath 能处理 JSON 数据吗？</h3>
<p>XPath 只处理 XML/HTML，不能直接处理 JSON。如果目标数据在 API 返回的 JSON 里，requests + json() 更直接，不需要 XPath。</p>
<h3 id="xpath--4">XPath 中的通配符怎么用？</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//*                    → 选取所有元素
//div/*                → 选取 div 下所有直接子元素
//li[@class=&quot;item-*&quot;]  → class 以 &quot;item-&quot; 开头的所有 li
</code></pre>
<h3 id="xpath--5">XPath 支持逻辑运算吗？</h3>
<p>支持，用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">and</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">or</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">not()</code>：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-xpath" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div[@class=&quot;main&quot; and @id=&quot;content&quot;]   → 同时满足两个条件
//div[@class=&quot;main&quot; or @class=&quot;sidebar&quot;] → 满足任一条件
//div[not(@class=&quot;hidden&quot;)]               → 不包含某类名
</code></pre>
<hr>
<h2 id="heading-7">总结</h2>
<p>XPath 的核心能力就三点：</p>
<ol>
<li><strong>路径表达式</strong> — 精准定位树中任意节点</li>
<li><strong>谓语</strong> — 用条件过滤，找到第 N 个或满足特定属性的元素</li>
<li><strong>轴</strong> — 往上找父/祖先，往下找子孙，不用受 CSS 只能&quot;向下找&quot;的限制</li>
</ol>
<p>CSS 选择器和 XPath 不是谁替代谁，而是各有优势。CSS 快而直观，XPath 强而精准。</p>
<p>学完这篇，你应该能应对爬虫里 80% 的元素定位场景。剩下 20% 的极端情况（比如跨 iframe、Shadow DOM），后面再单独研究。</p>
<hr>
<h2 id="heading-8">相关知识</h2>
<p>如果还没学 CSS 选择器，可以先看 <a href="https://tc.xfei.tech/learn/css-selectors-crawler-data-extraction-basics">CSS 选择器入门：从改样式到爬虫数据定位</a>，XPath 和 CSS 选择器是互补关系，两者都需要掌握。</p>
<p>想深入理解网页结构，推荐阅读：</p>
<ul>
<li><a href="https://tc.xfei.tech/learn/dom-tree-nodes-parent-child-sibling">DOM 树是什么？节点、父子关系与兄弟关系详解</a></li>
<li><a href="https://tc.xfei.tech/learn/http-protocol-request-response-status-codes">HTTP 协议入门：请求、响应、状态码（爬虫视角）</a></li>
<li><a href="https://tc.xfei.tech/learn/requests-exception-handling">Python 爬虫 requests 异常处理完全指南</a></li>
</ul>
<hr>
<h2 id="faq">FAQ</h2>
<p><strong>Q：XPath 从 1 开始数还是从 0 开始？</strong></p>
<p>A：XPath 从 1 开始数，和编程语言的数组下标从 0 开始不同。<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//li[1]</code> 选的是第一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">li</code>，不是第二个。</p>
<p><strong>Q：CSS 选择器和 XPath 能同时用吗？</strong></p>
<p>A：能。Python 的 lxml 库支持用 CSS 选择器写法，自动转成 XPath 执行。写复杂的节点定位时，XPath 更强大；写简单的样式类名定位时，CSS 选择器更直观。</p>
<p><strong>Q：为什么 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div</code> 能匹配所有 div，但 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//div[@class=&quot;container&quot;]</code> 却匹配不到？</strong></p>
<p>A：检查页面 HTML 源码里实际的 class 属性值。有些页面类名是动态的，比如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">class=&quot;container-abc123&quot;</code>，需要用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">contains()</code> 模糊匹配，而不是精确匹配。</p>
<p><strong>Q：XPath 能跨 iframe 查找元素吗？</strong></p>
<p>A：不能直接跨 iframe。用 XPath 只能查到当前文档树的节点。如果目标元素在 iframe 里，需要先切换到那个 iframe 的 document，再继续用 XPath 查找。</p>
<p><strong>Q：XPath 的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 有什么区别？</strong></p>
<p>A：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 是从根节点开始的绝对路径，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code> 是从当前节点开始往下任意层级的相对路径。爬虫里最常用的是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code>，因为往往不知道根节点是什么，直接从文档任意位置往下找更实用。</p>
<p><strong>Q：XPath 能统计某个元素出现了多少次吗？</strong></p>
<p>A：能。用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">count()</code> 函数：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('count(//div[@class=&quot;item&quot;]')</code>，返回匹配到的 div 数量（浮点数），转为整数的用法是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">int(selector.xpath('count(//li)')</code>。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[Python Parsel 教程：用 CSS、XPath 提取网页数据]]></title>
      <link>https://tc.xfei.tech/learn/python-parsel-tutorial/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/python-parsel-tutorial/</guid>
      <pubDate>Thu, 11 Jun 2026 12:44:38 GMT</pubDate>
      <description><![CDATA[Python Parsel 怎么用？本文通过可运行示例讲清 Selector、CSS、XPath、正则提取、列表页解析及空结果排查，并给出 Parsel 配合 requests 的完整写法。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/python-parsel-tutorial.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/python-parsel-tutorial.webp" alt="Python Parsel 教程：用 CSS、XPath 提取网页数据" /></p>
<h2 id="-parsel">安装 Parsel</h2>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-bash" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">pip install parsel
</code></pre>
<p>如果示例还要发送 HTTP 请求，再安装 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests</code>：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-bash" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">pip install requests
</code></pre>
<h2 id="-selector-">从 Selector 开始</h2>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Selector</code> 用来包装待解析的文本。调用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.css()</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.xpath()</code> 后，会得到一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">SelectorList</code>，它可以继续调用选择器方法，也可以直接遍历。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from parsel import Selector

html = &quot;&quot;&quot;
&lt;html&gt;
  &lt;body&gt;
    &lt;div class=&quot;article&quot;&gt;
      &lt;h1&gt;Python 爬虫入门&lt;/h1&gt;
      &lt;p class=&quot;summary&quot;&gt;学习网页请求与数据提取。&lt;/p&gt;
      &lt;a href=&quot;/start&quot;&gt;开始阅读&lt;/a&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

selector = Selector(text=html)
</code></pre>
<p>后面的 CSS、XPath 和正则示例都使用这个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector</code>。</p>
<h2 id="-css-">用 CSS 选择器提取数据</h2>
<p>CSS 语法短，页面结构不复杂时很好读。</p>
<p>如果你对类选择器、属性选择器和后代选择器还不熟，可以先看 <a href="https://tc.xfei.tech/learn/css-selectors-crawler-data-extraction-basics/">CSS 选择器入门：从改样式到爬虫数据定位</a>。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 提取文本
title = selector.css(&quot;h1::text&quot;).get()
print(title)  # Python 爬虫入门

# 提取属性
link = selector.css(&quot;a::attr(href)&quot;).get()
print(link)  # /start

# 提取全部匹配结果
paragraphs = selector.css(&quot;p::text&quot;).getall()
print(paragraphs)  # ['学习网页请求与数据提取。']
</code></pre>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::text</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::attr(name)</code> 是 Parsel/Scrapy 扩展的伪元素：</p>
<ul>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::text</code> 获取当前元素的直接文本节点</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::attr(href)</code> 获取元素的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 属性</li>
</ul>
<p>它们不是浏览器原生 CSS 语法。在浏览器控制台测试定位规则时，先测试 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 这部分，回到 Parsel 后再补上 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::text</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::attr()</code>。</p>
<p>还有一个容易忽略的细节：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">::text</code> 只取直接文本节点。遇到下面这种结构：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;p class=&quot;desc&quot;&gt;价格 &lt;strong&gt;299&lt;/strong&gt; 元&lt;/p&gt;
</code></pre>
<p>如果想取出元素内部的全部文本，可以使用 XPath：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">texts = selector.css(&quot;p.desc&quot;).xpath(&quot;.//text()&quot;).getall()
description = &quot;&quot;.join(texts).strip()
</code></pre>
<h2 id="-xpath-">用 XPath 提取数据</h2>
<p>XPath 更适合按属性、层级关系或文本条件筛选节点。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 提取文本
title = selector.xpath(&quot;//h1/text()&quot;).get()

# 提取属性
link = selector.xpath(&quot;//a/@href&quot;).get()

# 按 class 属性筛选
summary = selector.xpath('//p[@class=&quot;summary&quot;]/text()').get()
</code></pre>
<p>CSS 和 XPath 可以混用。例如先用 CSS 找到文章卡片，再用 XPath 提取卡片中的全部文本。</p>
<h3 id="-xpath">循环内要使用相对 XPath</h3>
<p>这可能是 Parsel 新手最容易踩的坑。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">for item in selector.css(&quot;div.item&quot;):
    # 正确：从当前 item 内部继续查找
    name = item.xpath(&quot;.//span[@class='name']/text()&quot;).get()
</code></pre>
<p>这里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.//</code> 表示“从当前节点向下查找”。如果写成 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//span</code>，查询会回到整份文档，每次循环都可能拿到同一批结果。</p>
<h2 id="get--getall-"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.get()</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.getall()</code> 有什么区别</h2>
<p>两者的区别不复杂：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">first = selector.css(&quot;li::text&quot;).get()
items = selector.css(&quot;li::text&quot;).getall()
</code></pre>
<ul>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.get()</code> 返回第一个匹配值；没有匹配时返回 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">None</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.getall()</code> 返回所有匹配值；没有匹配时返回空列表 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[]</code></li>
</ul>
<p>可以给 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.get()</code> 设置默认值，避免字段缺失后调用字符串方法时报错：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">price = selector.css(&quot;.price::text&quot;).get(default=&quot;&quot;).strip()
</code></pre>
<p>旧代码中常见的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.extract()</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.extract_first()</code> 仍可使用，但新代码更建议写 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.getall()</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.get()</code>。</p>
<h2 id="heading">用正则表达式做二次提取</h2>
<p>正则适合处理已经定位好的短文本，不建议直接拿它解析整页 HTML。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from parsel import Selector

price_html = &quot;&lt;span class='price'&gt;价格：¥128.00&lt;/span&gt;&quot;
price_selector = Selector(text=price_html)

price = price_selector.css(&quot;span.price::text&quot;).re_first(r&quot;\d+(?:\.\d+)?&quot;)
print(price)  # 128.00
</code></pre>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.re()</code> 返回所有匹配结果，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.re_first()</code> 只返回第一个结果。调用正则后得到的是字符串，不再是可以继续调用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.css()</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.xpath()</code> 的选择器。</p>
<h2 id="heading-1">提取列表页数据</h2>
<p>列表页不要分别提取全部标题和全部链接后再 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">zip()</code>。只要某条记录缺少一个字段，数据就可能错位。更稳妥的写法是先遍历每个卡片，再在卡片内部取字段。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from parsel import Selector

html = &quot;&quot;&quot;
&lt;div class=&quot;product-list&quot;&gt;
  &lt;div class=&quot;item&quot;&gt;
    &lt;a class=&quot;name&quot; href=&quot;/product/1&quot;&gt;机械键盘&lt;/a&gt;
    &lt;span class=&quot;price&quot;&gt;299&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;item&quot;&gt;
    &lt;a class=&quot;name&quot; href=&quot;/product/2&quot;&gt;无线鼠标&lt;/a&gt;
    &lt;span class=&quot;price&quot;&gt;159&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&quot;&quot;&quot;

selector = Selector(text=html)
products = []

for item in selector.css(&quot;div.item&quot;):
    name = item.css(&quot;a.name::text&quot;).get(default=&quot;&quot;).strip()
    link = item.css(&quot;a.name::attr(href)&quot;).get(default=&quot;&quot;)
    price = item.css(&quot;span.price::text&quot;).get(default=&quot;&quot;).strip()

    if not name or not link:
        continue

    products.append({
        &quot;name&quot;: name,
        &quot;link&quot;: link,
        &quot;price&quot;: price,
    })

print(products)
</code></pre>
<p>这个写法有两个好处：字段属于哪条记录一目了然，个别卡片缺字段时也不会影响后面的数据。</p>
<h2 id="parsel--requests">Parsel 配合 requests</h2>
<p>下面是一个更接近实际项目的例子：请求新闻列表，检查响应，再提取标题和链接。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

import requests
from parsel import Selector


def scrape_news(url: str) -&gt; list[dict[str, str]]:
    headers = {
        &quot;User-Agent&quot;: (
            &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &quot;
            &quot;AppleWebKit/537.36 (KHTML, like Gecko) &quot;
            &quot;Chrome/124.0 Safari/537.36&quot;
        )
    }

    try:
        response = requests.get(url, headers=headers, timeout=(5, 15))
        response.raise_for_status()
    except requests.RequestException as exc:
        print(f&quot;请求失败：{exc}&quot;)
        return []

    selector = Selector(text=response.text)
    news_list = []

    for article in selector.css(&quot;article.news-item&quot;):
        title = article.css(&quot;h2 a::text&quot;).get(default=&quot;&quot;).strip()
        href = article.css(&quot;h2 a::attr(href)&quot;).get(default=&quot;&quot;)
        date = article.css(&quot;time::attr(datetime)&quot;).get(default=&quot;&quot;)
        summary = &quot; &quot;.join(
            text.strip()
            for text in article.css(&quot;p.summary&quot;).xpath(&quot;.//text()&quot;).getall()
            if text.strip()
        )

        if not title or not href:
            continue

        news_list.append({
            &quot;title&quot;: title,
            &quot;link&quot;: urljoin(response.url, href),
            &quot;date&quot;: date,
            &quot;summary&quot;: summary,
        })

    return news_list


if __name__ == &quot;__main__&quot;:
    for news in scrape_news(&quot;https://example.com/news&quot;):
        print(news[&quot;title&quot;], &quot;-&gt;&quot;, news[&quot;link&quot;])
</code></pre>
<p>这里统一捕获了 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">RequestException</code>。需要区分连接失败、超时和 HTTP 状态码异常时，可以继续看 <a href="https://tc.xfei.tech/learn/requests-exception-handling/">Python 爬虫 requests 异常处理完全指南</a>。</p>
<p>示例中有几个值得保留的习惯：</p>
<ul>
<li>请求必须设置超时，避免程序一直等下去</li>
<li>使用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">raise_for_status()</code> 处理 4xx、5xx 响应</li>
<li>在每个卡片内部提取字段，避免列表错位</li>
<li>使用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin()</code> 把相对链接补成绝对链接</li>
<li>对可能缺失的字段设置默认值</li>
</ul>
<h2 id="-xml-">解析 XML 和命名空间</h2>
<p>解析 XML 时显式设置 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">type=&quot;xml&quot;</code>：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from parsel import Selector

xml_data = &quot;&quot;&quot;
&lt;root&gt;
  &lt;item id=&quot;1&quot;&gt;&lt;name&gt;商品 A&lt;/name&gt;&lt;stock&gt;50&lt;/stock&gt;&lt;/item&gt;
  &lt;item id=&quot;2&quot;&gt;&lt;name&gt;商品 B&lt;/name&gt;&lt;stock&gt;0&lt;/stock&gt;&lt;/item&gt;
&lt;/root&gt;
&quot;&quot;&quot;

selector = Selector(text=xml_data, type=&quot;xml&quot;)

for item in selector.xpath(&quot;//item&quot;):
    item_id = item.xpath(&quot;@id&quot;).get(default=&quot;&quot;)
    name = item.xpath(&quot;name/text()&quot;).get(default=&quot;&quot;)
    stock = item.xpath(&quot;stock/text()&quot;).get(default=&quot;&quot;)
    print(item_id, name, stock)
</code></pre>
<p>带命名空间的 XML 不能总靠普通标签名匹配。数据结构简单、也不需要区分同名命名空间时，可以先移除命名空间：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector = Selector(text=xml_with_namespace, type=&quot;xml&quot;)
selector.remove_namespaces()
items = selector.xpath(&quot;//item&quot;).getall()
</code></pre>
<p>如果文档里存在多个同名标签，移除命名空间可能让它们混在一起。这时应保留命名空间，并在 XPath 中注册、使用对应前缀。</p>
<h2 id="heading-2">为什么选择器明明正确，却取不到数据</h2>
<p>遇到空结果时，先别急着换 CSS 或 XPath。按下面的顺序检查，通常更快。</p>
<h3 id="1-">1. 响应里根本没有目标内容</h3>
<p>先检查服务端返回的原始 HTML：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(response.status_code)
print(response.url)
print(response.text[:1000])
</code></pre>
<p>浏览器里看得到、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">response.text</code> 里没有，常见原因是页面由 JavaScript 渲染。Parsel 不会执行 JavaScript，需要找到页面背后的数据接口，或者使用 Playwright 等浏览器自动化工具拿到渲染后的 HTML。</p>
<h3 id="2-">2. 请求被重定向或返回了验证页</h3>
<p>登录页、验证码页和风控提示也可能返回 200。只看状态码不够，还要检查最终 URL、页面标题和正文片段。</p>
<h3 id="3-">3. 页面编码判断错误</h3>
<p>如果内容存在但中文乱码，可以先检查响应头和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">response.apparent_encoding</code>，确认后再指定编码：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">response.encoding = &quot;gb18030&quot;
selector = Selector(text=response.text)
</code></pre>
<p>不要见到中文站点就固定写 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">gbk</code>。编码应根据响应头、页面声明或实际字节内容判断。</p>
<h3 id="4-xpath-">4. XPath 查询范围写错</h3>
<p>在子节点中继续查询时，优先检查有没有把 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.//</code> 写成 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code>。后者会从整份文档开始匹配。</p>
<h3 id="5-">5. 文本藏在子标签里</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">p::text</code> 取不到 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;p&gt;文字 &lt;strong&gt;重点&lt;/strong&gt;&lt;/p&gt;</code> 中的全部内容。需要改用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.//text()</code>，再把文本片段清洗、拼接。</p>
<h2 id="css--xpath">CSS 还是 XPath</h2>
<p>没有必要二选一。</p>
<p>页面结构简单时，CSS 通常更短：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.css(&quot;article.news-item h2 a::text&quot;).getall()
</code></pre>
<p>需要按文本、父子关系或复杂条件筛选时，XPath 更顺手：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">selector.xpath('//article[contains(@class, &quot;news-item&quot;)]//a/text()').getall()
</code></pre>
<p>实际项目里常见的做法是：CSS 负责定位重复卡片，XPath 处理卡片内部不规则的文本结构。哪种写法更清楚，就用哪种。</p>
<h2 id="heading-3">常用方法速查</h2>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">需求</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">写法</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">创建 HTML 解析器</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Selector(text=html)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">创建 XML 解析器</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Selector(text=xml, type=&quot;xml&quot;)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">CSS 选择节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.css(&quot;div.item&quot;)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">XPath 选择节点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.xpath(&quot;//div&quot;)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">获取第一个结果</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.get()</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">获取全部结果</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.getall()</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">获取文本</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.css(&quot;h1::text&quot;)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">获取属性</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.css(&quot;a::attr(href)&quot;)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">正则提取首个结果</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.re_first(pattern)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">正则提取全部结果</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.re(pattern)</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">移除 XML 命名空间</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.remove_namespaces()</code></td>
</tr>
</tbody>
</table></div>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[Python urljoin 教程：URL 路径拼接与相对路径处理完整指南]]></title>
      <link>https://tc.xfei.tech/learn/python-urljoin-tutorial-url-parse/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/python-urljoin-tutorial-url-parse/</guid>
      <pubDate>Mon, 01 Jun 2026 15:44:07 GMT</pubDate>
      <description><![CDATA[urljoin 是 Python 爬虫处理相对路径拼接的核心工具。本文从 URL 结构讲起，覆盖相对路径、绝对路径、./、../、urlparse、urlunparse 在爬虫中的实战用法，附 6 个实战案例与常见报错解决方案。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/python-urljoin-tutorial-url-parse.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/python-urljoin-tutorial-url-parse.webp" alt="Python urljoin 教程：URL 路径拼接与相对路径处理完整指南" /></p>
<h2 id="-url-">为什么爬虫要处理 URL 路径拼接？</h2>
<p>写爬虫时，你会经常遇到这类问题：</p>
<ul>
<li>从页面 HTML 里拿到相对路径 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/img/logo.png</code>，不知道怎样拼成完整 URL</li>
<li>用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code> 拼出来结果和想的不一样，不知道哪里出了问题</li>
<li>base URL 带了查询参数 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">?x=1&amp;b=2</code> 和锚点 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">#hash</code>，担心会被一起带进结果里</li>
</ul>
<p>如果直接用字符串拼接：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">base = &quot;https://example.com/a/b/c?x=1&amp;b=2#hash&quot;
result = base + &quot;/img/logo.png&quot;
# ❌ https://example.com/a/b/c?x=1&amp;b=2#hash/img/logo.png
</code></pre>
<p>这显然不对。正确做法是使用 Python 标准库的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code>，它<strong>按 URL 结构规则</strong>来处理路径拼接，不是简单的字符串连接。</p>
<p><strong>学完这篇，你能够：</strong></p>
<ol>
<li>理解 URL 各组成部分（scheme、netloc、path、query、fragment）</li>
<li>掌握 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code> 的路径拼接规则（相对路径、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">../</code>、绝对路径）</li>
<li>区分 urljoin 和字符串拼接的本质差异</li>
<li>在爬虫里正确拼接相对路径，提取完整资源 URL</li>
<li>避开 urljoin 的常见误区</li>
</ol>
<hr>
<h2 id="url-urljoin-">URL 结构：urljoin 到底在处理什么？</h2>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>urljoin 拼的不是字符串，而是 <strong>URL 结构</strong>。</p>
</blockquote>
<p>先把 base URL 拆解成 5 个组成部分：</p>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">组成部分</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">含义</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">示例值</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">scheme</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">协议</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">netloc</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">域名</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">example.com</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">path</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">路径</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/b/c</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">query</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">查询参数</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">x=1&amp;b=2</code></td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">fragment</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">锚点</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">hash</code></td>
</tr>
</tbody>
</table></div>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urlparse

base = &quot;https://example.com/a/b/c?x=1&amp;b=2#hash&quot;
parsed = urlparse(base)

print(parsed.scheme)    # https
print(parsed.netloc)    # example.com
print(parsed.path)      # /a/b/c
print(parsed.query)     # x=1&amp;b=2
print(parsed.fragment)  # hash
</code></pre>
<p><strong>关键提醒：</strong></p>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>urljoin 只处理 <strong>path</strong>，base URL 的 query（<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">?</code> 后面的部分）和 fragment（<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">#</code> 后面的部分）<strong>会被直接丢弃</strong>。</p>
<p>也就是说，无论 base 带了多么复杂的查询参数，拼接结果都不会包含它们。只有 path 会被用来参与路径计算。</p>
</blockquote>
<hr>
<h2 id="urljoin-">urljoin 基础：相对路径怎么拼？</h2>
<h3 id="heading">相对路径：以「当前目录」为基准</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

base = &quot;https://example.com/a/b/c?x=1&amp;b=2#hash&quot;

result = urljoin(base, &quot;img/1.jpg&quot;)
print(result)
# https://example.com/a/b/img/1.jpg
</code></pre>
<p>为什么会是这个结果？</p>
<ol>
<li>base 的 path 是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/b/c</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">c</code> 被识别为<strong>文件名</strong>，而非目录</li>
<li>当前目录 = <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/b/</code></li>
<li>拼接 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img/1.jpg</code> → <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/b/img/1.jpg</code></li>
</ol>
<p>这就是 urljoin 的核心规则：<strong>以 base path 的「当前目录」为基准，往下拼接相对路径</strong>。</p>
<h3 id="heading-1">./：明确表示&quot;当前目录&quot;</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(base, &quot;./path&quot;))
# https://example.com/a/b/path
</code></pre>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code> 的效果和上面一样，只是更明确地告诉你：<strong>以当前目录为起点</strong>。</p>
<h3 id="heading-2">../：向上回退一层目录</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(base, &quot;../css/main.css&quot;))
# https://example.com/a/css/main.css
</code></pre>
<p>计算过程：</p>
<ol>
<li>当前目录 = <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/b/</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">..</code> 退一层 → <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/</code></li>
<li>再拼上 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">css/main.css</code> → <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/a/css/main.css</code></li>
</ol>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>有几个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">..</code>，就退几层目录。</p>
</blockquote>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 退两层
print(urljoin(base, &quot;../../css/main.css&quot;))
# https://example.com/css/main.css
</code></pre>
<hr>
<h2 id="urljoin--1">urljoin 进阶：绝对路径怎么处理？</h2>
<h3 id="-">/ 开头：从网站根目录算</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(base, &quot;/static/app.js&quot;))
# https://example.com/static/app.js
</code></pre>
<p><strong>关键区别：</strong></p>
<ul>
<li>相对路径（无 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 开头）：以 base 的当前目录为基准</li>
<li>绝对路径（有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 开头）：<strong>从域名根目录开始</strong>，忽略 base 的路径</li>
</ul>
<h3 id="-url">完整 URL：直接覆盖</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 第二个参数是完整 URL，直接返回它
print(urljoin(base, &quot;https://other.com/path&quot;))
# https://other.com/path
</code></pre>
<hr>
<h2 id="urljoin-vs-">urljoin vs 字符串拼接：本质区别在哪里？</h2>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">对比项</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">字符串拼接</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">urljoin</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">处理方式</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">字符连接</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">URL 结构解析</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">遇到 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 时的行为</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">产生双斜杠 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">自动规整</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">处理 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">../</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">当普通字符</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">正确回退目录</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">处理 query/fragment</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">原样保留</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">丢弃（除非相对路径本身含）</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">跨协议/域名</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">全部拼接</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">正确替换</td>
</tr>
</tbody>
</table></div>
<p><strong>字符串拼接的坑：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">base = &quot;https://example.com/a/b/&quot;
path = &quot;/img/logo.png&quot;

print(base + path)
# ❌ https://example.com/a/b//img/logo.png（双斜杠）

print(base + path.lstrip('/'))
# ⚠️ 可以，但你自己处理了，urljoin 更安全
</code></pre>
<p><strong>urljoin 的优势：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

base = &quot;https://example.com/a/b/&quot;
path = &quot;/img/logo.png&quot;

print(urljoin(base, path))
# ✅ https://example.com/img/logo.png（自动处理）
</code></pre>
<hr>
<h2 id="-urljoin-">实战：爬虫里怎么用 urljoin 拼接图片链接？</h2>
<h3 id="-html--url">场景：从 HTML 里提取相对路径，拼接完整 URL</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin
from lxml import etree
import requests

# 目标页面
url = &quot;https://example.com/article/python-intro&quot;
html = requests.get(url, headers={&quot;User-Agent&quot;: &quot;Mozilla/5.0&quot;}).text

selector = etree.HTML(html)

# 提取所有图片的 src（相对路径）
img_srcs = selector.xpath('//img[@src]/@src')

for src in img_srcs:
    full_url = urljoin(url, src)
    print(full_url)
</code></pre>
<p>运行结果示例：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://example.com/article/python-intro
https://example.com/article/img/cover.jpg          # 相对路径 → 完整 URL
https://example.com/static/logo.png               # / 开头 → 根目录
https://other.com/banner.png                      # 完整 URL → 直接保留
</code></pre>
<h3 id="-url--1">场景：批量拼接资源 URL 并下载</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin
from lxml import etree
import requests

def download_resources(page_url, css_selector):
    &quot;&quot;&quot;下载页面指定选择器对应的所有资源&quot;&quot;&quot;
    html = requests.get(page_url, headers={&quot;User-Agent&quot;: &quot;Mozilla/5.0&quot;}).text
    selector = etree.HTML(html)
    resources = selector.xpath(f'{css_selector}/@href | {css_selector}/@src')
    
    for resource in resources:
        # 拼接完整 URL
        resource_url = urljoin(page_url, resource)
        # 下载（省略异常处理）
        r = requests.get(resource_url)
        filename = resource_url.split('/')[-1]
        with open(filename, 'wb') as f:
            f.write(r.content)
        print(f&quot;Downloaded: {resource_url}&quot;)
</code></pre>
<h3 id="-url-1">场景：拼接分页 URL</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

base = &quot;https://example.com/list/page1&quot;
page_count = 5

for i in range(1, page_count + 1):
    # 方法 1：用 urljoin 拼接
    page_url = urljoin(base, f&quot;page{i}&quot;)
    print(page_url)
    # https://example.com/list/page1 → https://example.com/list/page2 → ...

# 方法 2：用 format 直接生成（更直观）
for i in range(1, page_count + 1):
    print(f&quot;https://example.com/list/page{i}&quot;)
</code></pre>
<hr>
<h2 id="urljoin--2">urljoin 常见报错与处理</h2>
<h3 id="-1-query-">报错 1：拼接结果包含了旧的 query 参数</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

base = &quot;https://example.com/a/b?keyword=python&quot;
result = urljoin(base, &quot;img/logo.png&quot;)

print(result)
# https://example.com/a/img/logo.png
# ✅ query 参数被正确丢弃了（这不是报错，是 urljoin 的标准行为）
</code></pre>
<p><strong>如果你的本意是要保留 query 参数</strong>，需要手动处理：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urlparse, urlunparse

base = &quot;https://example.com/a/b?keyword=python&quot;
new_path = &quot;img/logo.png&quot;

parsed = urlparse(base)
new_url = urlunparse((
    parsed.scheme,
    parsed.netloc,
    new_path,
    '',  # params（很少用）
    parsed.query,  # 保留 query
    ''   # fragment
))
print(new_url)
# https://example.com/img/logo.png?keyword=python
</code></pre>
<h3 id="-2">报错 2：相对路径带空格导致拼接失败</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 相对路径里带空格，必须编码
src = &quot;/img/my photo.jpg&quot;
# ❌ urljoin 会把空格当成路径分隔符，导致奇怪结果

from urllib.parse import quote
safe_src = quote(src, safe='/')
full_url = urljoin(base, safe_src)
# ✅ https://example.com/img/my%20photo.jpg
</code></pre>
<h3 id="-3base-url---">报错 3：base URL 末尾有 / 跟没有，结果不一样</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

base_with_slash = &quot;https://example.com/a/b/&quot;
base_no_slash = &quot;https://example.com/a/b&quot;

print(urljoin(base_with_slash, &quot;c&quot;))
# https://example.com/a/b/c

print(urljoin(base_no_slash, &quot;c&quot;))
# https://example.com/a/c  ← 注意这里！b 被当成文件名替换了
</code></pre>
<p><strong>处理方法：</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 保证 base URL 末尾有 /
import urllib.parse

def ensure_trailing_slash(url):
    parsed = urllib.parse.urlparse(url)
    if not parsed.path.endswith('/'):
        return url.rstrip('/') + '/'
    return url

base = ensure_trailing_slash(&quot;https://example.com/a/b&quot;)
print(urljoin(base, &quot;c&quot;))
# https://example.com/a/b/c  ✅
</code></pre>
<h3 id="-4-url">报错 4：协议相对 URL（//开头）</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(&quot;https://example.com/&quot;, &quot;//cdn.com/logo.png&quot;))
# ✅ https://cdn.com/logo.png（自动补全 https:）
</code></pre>
<p>这其实是 urljoin 的正常行为，但有时会引发问题，需要注意。</p>
<hr>
<h2 id="urljoin--urlparseurlunparse-">urljoin 与 urlparse、urlunparse 的关系</h2>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urllib.parse</code> 模块里三个函数配合使用：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin, urlparse, urlunparse

url = &quot;https://example.com/a/b/c?x=1#section&quot;

# 1. 解析：拆解 URL 结构
parsed = urlparse(url)
print(parsed)
# ParseResult(scheme='https', netloc='example.com', path='/a/b/c', query='x=1', fragment='section')

# 2. 修改：替换某个组成部分
modified = parsed._replace(path=&quot;/new/path&quot;)
print(urlunparse(modified))
# https://example.com/new/path?x=1#section

# 3. 拼接：用 urljoin
result = urljoin(url, &quot;../css/style.css&quot;)
print(result)
# https://example.com/a/css/style.css
</code></pre>
<p><strong>三者的分工：</strong></p>
<div style="overflow-x:auto; margin:16px 0;"><table style="width:100%; border-collapse:collapse; margin:16px 0; table-layout:auto; border:1px solid #d0d7de;">
<thead>
<tr>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">函数</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">作用</th>
<th style="border:1px solid #d0d7de; padding:8px 10px; background:#f6f8fa; text-align:left; vertical-align:top;">场景</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlparse</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">拆分 URL 结构</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">分析、修改 URL 某部分</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">拼接相对路径</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">爬虫提取资源、生成完整 URL</td>
</tr>
<tr>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlunparse</code></td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">组合成新 URL</td>
<td style="border:1px solid #d0d7de; padding:8px 10px; vertical-align:top; background:#ffffff;">修改后重新组装</td>
</tr>
</tbody>
</table></div>
<hr>
<h2 id="heading-3">常见问题</h2>
<h3 id="urljoin--ospathjoin-">urljoin 和 os.path.join 有什么区别？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">os.path.join</code> 是<strong>文件系统路径</strong>拼接，不管 URL 结构。<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code> 是<strong>URL 路径</strong>拼接，理解 URL 结构规则。两者不能混用：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">import os
from urllib.parse import urljoin

# os.path.join 用在文件系统路径上
print(os.path.join(&quot;/a/b&quot;, &quot;c&quot;))
# ✅ /a/b/c

# urljoin 用在 URL 上
print(urljoin(&quot;https://example.com/a/b&quot;, &quot;c&quot;))
# ✅ https://example.com/a/c
</code></pre>
<h3 id="base-url---">base URL 末尾的 / 重要吗？</h3>
<p><strong>非常重要。</strong></p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import urljoin

# 有 /
urljoin(&quot;https://example.com/a/b/&quot;, &quot;c&quot;)
# https://example.com/a/b/c

# 没有 /
urljoin(&quot;https://example.com/a/b&quot;, &quot;c&quot;)
# https://example.com/a/c  ← 完全不同的结果！
</code></pre>
<p>爬虫里处理相对路径时，建议先对 base URL 做规范化处理：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">def normalize_base_url(url):
    from urllib.parse import urlparse
    p = urlparse(url)
    if not p.path.endswith('/'):
        return f&quot;{p.scheme}://{p.netloc}{p.path}/&quot;
    return url
</code></pre>
<h3 id="urljoin-hash">urljoin 能处理锚点（#hash）吗？</h3>
<p><strong>不能直接保留。</strong> urljoin 会丢弃 base 的 fragment：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(&quot;https://example.com/a#section&quot;, &quot;b&quot;))
# https://example.com/a  ← #section 丢了
</code></pre>
<p>如果需要保留锚点到新页面：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">def join_with_fragment(base, path):
    from urllib.parse import urlparse
    frag = urlparse(base).fragment
    joined = urljoin(base, path)
    return f&quot;{joined}#{frag}&quot; if frag else joined
</code></pre>
<h3 id="-url--">协议相对 URL（以 // 开头）怎么处理？</h3>
<p>有些资源链接写成 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//cdn.example.com/logo.png</code>，这是协议相对 URL：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">print(urljoin(&quot;https://example.com/&quot;, &quot;//cdn.example.com/logo.png&quot;))
# ✅ https://cdn.example.com/logo.png
</code></pre>
<p>urljoin 会自动补全 scheme，但如果需要在没有协议的上下文中处理它，需要注意这一点。</p>
<hr>
<h2 id="heading-4">总结</h2>
<p>urljoin 的核心逻辑其实很简单：</p>
<ol>
<li><strong>相对路径</strong>（无 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 开头）：以 base 的「当前目录」（即去掉文件名后的路径）为基准，往下拼接</li>
<li><strong><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code></strong>：明确指向当前目录</li>
<li><strong><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">../</code></strong>：向上回退一层目录</li>
<li><strong>绝对路径</strong>（<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 开头）：从网站根目录开始，忽略 base 的路径</li>
<li><strong>完整 URL</strong>：直接替换 base，不做任何拼接</li>
</ol>
<p>爬虫里用 urljoin 的最佳实践：</p>
<ul>
<li>提取页面资源时，先用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin(base, relative_path)</code> 拼接成完整 URL 再请求</li>
<li>用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlparse</code> 分析 URL 结构，用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin</code> 拼接相对路径，用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlunparse</code> 重组修改后的 URL</li>
<li>拼接前先规范化 base URL（检查末尾 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code>），避免目录层级判断错误</li>
<li>用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">quote()</code> 编码含空格或特殊字符的路径</li>
</ul>
<hr>
<h2 id="heading-5">相关知识</h2>
<p>想深入理解 URL 处理，推荐阅读：</p>
<ul>
<li><a href="https://tc.xfei.tech/learn/absolute-relative-path-browser-resource-resolution">绝对路径和相对路径是什么？浏览器如何补全资源地址</a></li>
<li><a href="https://tc.xfei.tech/learn/http-protocol-request-response-status-codes">HTTP 协议入门：请求、响应、状态码（爬虫视角）</a></li>
<li><a href="https://tc.xfei.tech/learn/requests-exception-handling">Python 爬虫 requests 异常处理完全指南</a></li>
</ul>
<hr>
<h2 id="faq">FAQ</h2>
<p><strong>Q：urljoin 和字符串拼接 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">+</code> 哪个更好？</strong></p>
<p>A：urljoin 更好。它按 URL 结构规则处理路径拼接，能自动处理 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">../</code> 等特殊符号，而字符串拼接会产生双斜杠或错误的路径。Python 的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urllib.parse</code> 就是为这个设计的，不要自己写字符串拼接。</p>
<p><strong>Q：base URL 带查询参数，urljoin 会保留吗？</strong></p>
<p>A：不会。urljoin 只处理 path，base 的 query 参数和 fragment 会被丢弃。如果需要保留 query 参数，需要用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlparse</code> + <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urlunparse</code> 手动处理。</p>
<p><strong>Q：相对路径开头有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code> 和没有有区别吗？</strong></p>
<p>A：在 urljoin 里基本没有区别。<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin(base, &quot;img/1.jpg&quot;)</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">urljoin(base, &quot;./img/1.jpg&quot;)</code> 结果完全一样。<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code> 的作用是明确告诉阅读者这是当前目录下的路径，在 urljoin 解析层面是一样的。</p>
<p><strong>Q：urljoin 会不会产生双斜杠 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code>？</strong></p>
<p>A：不会。urljoin 会自动规整路径，消除多余的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code>。但字符串拼接 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">base + &quot;/path&quot;</code> 会产生 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//</code>（虽然大多数服务器能处理，但不符合规范）。</p>
<p><strong>Q：爬虫里提取到相对路径，什么时候用 urljoin，什么时候不需要？</strong></p>
<p>A：如果相对路径是完整的 URL（以 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http://</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://</code> 开头），不需要 urljoin。如果是以 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">../</code> 开头，或者没有任何 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 的纯路径，就必须用 urljoin 拼接成完整 URL。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[Python 爬虫 requests 异常处理完全指南：超时、ConnectionError、HTTPError 怎么办]]></title>
      <link>https://tc.xfei.tech/learn/requests-exception-handling/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/requests-exception-handling/</guid>
      <pubDate>Sat, 30 May 2026 03:21:50 GMT</pubDate>
      <description><![CDATA[梳理 requests 异常的完整体系，按请求阶段逐一说明 URL 错误、连接失败、超时、状态码异常和重定向异常，帮你写出不容易崩的爬虫代码。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/requests-exception-handling.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/requests-exception-handling.webp" alt="Python 爬虫 requests 异常处理完全指南：超时、ConnectionError、HTTPError 怎么办" /></p>
<h2 id="heading">为什么你的爬虫请求总是失败？</h2>
<p>requests 发出去的请求，一定会成功吗？</p>
<p>不一定。</p>
<p>服务器可能崩了，网络可能波动，代理可能失效，证书可能过期。
这些事情不在你代码的控制范围内。</p>
<p>不处理异常，程序就会在某个你没想到的时候直接崩掉。</p>
<h2 id="requests-">requests 异常有哪些？一图梳理完整体系</h2>
<p>Python 异常最顶层是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">BaseException</code>，日常能捕获到的运行时异常基本都在它的子类 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Exception</code> 下面。</p>
<p>requests 在 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Exception</code> 下面定义了一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">RequestException</code>，它是所有 requests 异常的基类。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">BaseException
 └─ Exception
     └─ RequestException        # 所有 requests 异常基类
         ├─ HTTPError
         ├─ ConnectionError
         │   ├─ ProxyError
         │   └─ SSLError
         ├─ Timeout
         │   ├─ ConnectTimeout
         │   └─ ReadTimeout
         ├─ TooManyRedirects
         ├─ MissingSchema
         ├─ InvalidSchema
         └─ InvalidURL
</code></pre>
<p>只要捕获 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">RequestException</code>，requests 的全部异常都能兜住。
当然，你也可以细分捕获，不同的异常用不同的方式处理。</p>
<h2 id="heading-1">异常按阶段来理解更清楚</h2>
<p>一次请求从发出到拿到结果，是分阶段的。
异常基本上就是某个阶段失败了。</p>
<h3 id="url--missingschemainvalidurl-">URL 报错 MissingSchema/InvalidURL 怎么处理？</h3>
<p>请求还没发出去就报错了，多半是 URL 写错了。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">import requests

requests.get(&quot;example.com&quot;)        # MissingSchema，缺少 http://
requests.get(&quot;ftp://example.com&quot;)  # InvalidSchema，不支持 ftp
requests.get(&quot;https://example .com&quot;) # InvalidURL，格式不合法
</code></pre>
<p>这类问题如果 URL 是手写的，检查一遍就能解决。
如果 URL 来自上游爬虫，就要在拼接时做好校验。</p>
<h3 id="-connectionerrorproxyerror-">连接失败 ConnectionError/ProxyError 怎么处理？</h3>
<p>URL 没问题，进入连接阶段，可能遇到 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ConnectionError</code>。</p>
<p>常见场景：断网了、服务器 IP 不存在、端口没开、代理配置错、证书问题。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests.get(&quot;https://not-exist-domain-xyz.com&quot;)  # ConnectionError
</code></pre>
<p>两个常见子类：</p>
<ul>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ProxyError</code>：代理连接失败</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">SSLError</code>：HTTPS 证书验证失败，前面讲过，可以传 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">verify=False</code> 临时跳过</li>
</ul>
<h3 id="requests-timeout-">requests 超时怎么办？timeout 这样设置才正确</h3>
<p>连上了，但服务器迟迟不响应。</p>
<p>这时候一定要设置 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">timeout</code>，否则代码会一直卡着，永远等下去。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;"># 连接和读取共享 5 秒超时
requests.get(url, timeout=5)

# 分开设置：连接 3 秒，读取 10 秒
requests.get(url, timeout=(3, 10))
</code></pre>
<p>超时会抛 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Timeout</code>，子类是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ConnectTimeout</code>（连接超时）和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ReadTimeout</code>（读取超时）。</p>
<p><strong>timeout 一定要带上，别省。</strong></p>
<h3 id="-httperror-raise-for-status-">状态码报错 HTTPError 怎么处理？raise_for_status 用法</h3>
<p>这种情况 requests 默认不抛异常。
状态码 404、500，你都能正常拿到 response，requests 不认为这是错误。</p>
<p>如果你希望状态码非 2xx 时抛异常，就调用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">raise_for_status()</code>：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">response = requests.get(url)
response.raise_for_status()  # 非 2xx 抛 HTTPError
</code></pre>
<h3 id="-toomanyredirects-">重定向过多 TooManyRedirects 怎么处理？</h3>
<p>requests 默认会自动跟随重定向，比如 http 跳 https 就是这么处理的。</p>
<p>但如果重定向形成了死循环，超过限制（默认 30 次）就会抛 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">TooManyRedirects</code>。</p>
<h2 id="requests--1">requests 异常处理代码怎么写？（完整示例）</h2>
<p>不是每个异常都要单独捕获，判断标准只有一个：<strong>这个异常能不能影响我的下一步行为？</strong></p>
<p>能影响就单独捕获，不知道怎么处理就交给父类统一兜底。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">import requests

try:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
except requests.Timeout:
    # 超时可能要重试，或切换代理
    print(&quot;请求超时&quot;)
except requests.HTTPError:
    # 状态码不对，业务层处理
    print(f&quot;HTTP 错误：{response.status_code}&quot;)
except requests.RequestException:
    # 其他所有 requests 异常统一兜底
    print(&quot;请求失败&quot;)
else:
    # 没有异常，正常处理结果
    print(response.text)
</code></pre>
<p>注意：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">RequestException</code> 一定要写在最后，写在前面会直接拦截所有异常，子类捕获永远不会生效。</p>
<h2 id="heading-2">一句话记住异常处理精髓</h2>
<p><strong>异常按阶段理解，捕获按需细分，RequestException 最后兜底。</strong></p>
<p>这套思路不只适用于 requests，写任何网络请求的代码都用得上。</p>
<h2 id="heading-3">常见问题</h2>
<h3 id="requests--2">requests 超时了怎么办？</h3>
<p>设置 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">timeout</code> 参数。推荐分开设置连接超时和读取超时：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests.get(url, timeout=(3, 10))</code>，连接超时 3 秒，读取超时 10 秒。如果没有设置 timeout，请求会无限等待直到服务器响应。</p>
<h3 id="connectionerror--timeout-">ConnectionError 和 Timeout 哪个先处理？</h3>
<p>建议优先捕获 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Timeout</code>，因为超时更常见，而且可能是临时性网络问题，可以考虑重试。<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">ConnectionError</code> 通常意味着网络本身不通或者服务器不可达，重试的意义不大。但具体顺序可以根据业务需求调整。</p>
<h3 id="requests--ssl-">requests 报 SSL 错误怎么解决？</h3>
<p>如果是证书验证失败，可以传 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">verify=False</code> 临时跳过验证（仅限开发环境）：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests.get(url, verify=False)
</code></pre>
<p>如果是证书过期或者域名不匹配，应该检查 URL 是否正确，或者更新本地的 CA 证书 bundle。生产环境不建议关闭证书验证。</p>
<h3 id="-404-">为什么状态码 404 不会抛异常？</h3>
<p>requests 默认不会对任何状态码抛异常，2xx 和 4xx/5xx 都会正常返回响应。如果希望在非 2xx 时自动抛异常，记得加上 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">response.raise_for_status()</code>。</p>
<h3 id="-proxyerror-">代理设置后报 ProxyError 怎么办？</h3>
<p>先检查代理地址和端口是否写对（格式如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http://ip:port</code>），确认代理本身是否可用。也可以先在本地终端用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">curl</code> 测试代理是否正常工作。如果代理需要认证，记得在 URL 里带上用户名和密码：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http://user:pass@ip:port</code>。</p>
<h3 id="heading-4">捕获所有异常用哪个？</h3>
<p>用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests.RequestException</code>，它是所有 requests 异常的父类，捕获它就能兜住全部情况。注意一定要放在子类异常（Timeout、HTTPError 等）之后，否则子类永远不会被命中。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[网页源代码和 Elements 有什么区别？静态页面与动态页面怎么判断]]></title>
      <link>https://tc.xfei.tech/learn/source-code-vs-elements-static-dynamic-page/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/source-code-vs-elements-static-dynamic-page/</guid>
      <pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[讲清网页源代码、Elements 面板和实时 DOM 的区别，说明为什么页面上看得到的数据，源代码里不一定有，以及如何判断静态页面和动态页面。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/element.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/element.webp" alt="网页源代码和 Elements 有什么区别？静态页面与动态页面怎么判断" /></p>
<h2 id="heading">先说结论</h2>
<p>网页源代码和 Elements 不是一回事。</p>
<ol>
<li><strong>网页源代码</strong>：服务器最初返回的 HTML 文档。</li>
<li><strong>Elements 面板</strong>：浏览器当前实时渲染出来的 DOM 结构。</li>
</ol>
<p>这两个内容经常很像，但它们的来源、用途和变化方式都不一样。</p>
<h2 id="heading-1">网页源代码是什么</h2>
<p>网页源代码是浏览器从服务器拿回来的原始 HTML。</p>
<p>它的特点是：</p>
<ol>
<li>来自网络响应。</li>
<li>通常是静态文本。</li>
<li>反映的是页面最初的返回结果。</li>
</ol>
<p>你可以把它理解成“施工图纸”。
它告诉你浏览器最开始收到了什么，而不是页面最后变成了什么。</p>
<h2 id="elements-">Elements 面板是什么</h2>
<p>Elements 面板展示的是浏览器当前正在使用的 DOM。</p>
<p>它的特点是：</p>
<ol>
<li>会随着 JavaScript 改变而变化。</li>
<li>会反映当前页面真实结构。</li>
<li>可以看到动态插入、删除或修改后的内容。</li>
</ol>
<p>这也是为什么你在页面上改一个元素的文本，Elements 会立刻更新，但网页源代码通常不会变。</p>
<h2 id="heading-2">为什么两者会不一样</h2>
<p>原因很简单：<strong>JavaScript 会在页面加载后继续修改 DOM。</strong></p>
<p>可能的情况有三种：</p>
<ol>
<li>HTML 一开始就把数据写好了。</li>
<li>JavaScript 后面请求接口，再把数据插入页面。</li>
<li>JavaScript 修改了原有节点的内容或结构。</li>
</ol>
<p>所以你在页面上看到的内容，不一定是服务器最初返回的 HTML 里就有的。</p>
<h2 id="0-"><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">$0</code> 是什么</h2>
<p>在开发者工具里，你选中的那个元素，控制台里可以用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">$0</code> 直接引用。</p>
<p>这在调试时非常方便，比如：</p>
<ol>
<li>快速查看当前选中元素。</li>
<li>修改元素文本。</li>
<li>临时测试样式和结构。</li>
</ol>
<p>它本质上就是浏览器给你的一个便捷入口，帮助你更快操作当前选中的节点。</p>
<h2 id="heading-3">静态页面和动态页面怎么区分</h2>
<h3 id="heading-4">静态页面</h3>
<p>静态页面指的是：目标数据直接写在 HTML 里，浏览器拿到源代码就能看到。</p>
<p>这种页面的特点是：</p>
<ol>
<li>数据在源代码里能找到。</li>
<li>不依赖复杂的 JavaScript 渲染。</li>
<li>直接请求 HTML 往往就能拿到主要内容。</li>
</ol>
<h3 id="heading-5">动态页面</h3>
<p>动态页面指的是：页面内容由 JavaScript 后续生成或填充。</p>
<p>这种页面的特点是：</p>
<ol>
<li>源代码里可能看不到目标数据。</li>
<li>Elements 里通常能看到最终结果。</li>
<li>需要进一步分析 JavaScript 或 Network 请求。</li>
</ol>
<h2 id="heading-6">怎么快速判断一个页面属于哪一种</h2>
<p>你可以按下面这几个步骤看：</p>
<ol>
<li>先在页面上找到目标数据。</li>
<li>打开网页源代码，看数据是否已经存在。</li>
<li>如果源代码没有，再去 Elements 面板找。</li>
<li>如果 Elements 里有，说明内容大概率是动态生成的。</li>
<li>再切到 Network 面板，刷新页面，看是不是接口返回了这些数据。</li>
</ol>
<p>这套方法对爬虫分析非常实用，因为它直接决定你下一步该怎么抓数据。</p>
<h2 id="heading-7">为什么爬虫一定要分清这两个视图</h2>
<p>很多爬虫问题，根源不是“代码写错了”，而是<strong>找错了数据来源</strong>。</p>
<p>如果你把源代码当成最终页面，就可能：</p>
<ol>
<li>误以为数据不存在。</li>
<li>错过真正返回数据的接口。</li>
<li>在错误的 HTML 结构上浪费时间。</li>
</ol>
<p>所以，先分清“源代码”和“实时 DOM”，你后面的分析效率会高很多。</p>
<h2 id="heading-8">一个非常实用的判断口诀</h2>
<p>你可以记成这样：</p>
<p><strong>源代码看起点，Elements 看结果。</strong></p>
<p>如果页面上有数据，源代码没有，那就优先考虑 JavaScript 动态渲染。
如果页面上和源代码一致，那它更像静态页面。</p>
<h2 id="heading-9">和浏览器加载流程的关系</h2>
<p>这篇内容其实是上一节的延伸。</p>
<p>浏览器先拿到 HTML，再加载资源，再执行 JavaScript。
而 Elements 面板展示的，就是这一整套过程结束后的最终状态。</p>
<p>所以你在学完页面加载流程之后，再回头看源代码和 Elements，就会突然清楚很多。</p>
<h2 id="heading-10">总结</h2>
<p>网页源代码、Elements、静态页面、动态页面，这四个概念一定要一起理解：</p>
<ol>
<li>源代码是服务器返回的原始 HTML。</li>
<li>Elements 是浏览器当前实时 DOM。</li>
<li>静态页面的数据通常在 HTML 里。</li>
<li>动态页面的数据通常由 JavaScript 后续生成。</li>
</ol>
<p>这篇文章读完后，你在分析网页时，应该能更快判断：
<strong>是直接看 HTML，还是要去找 JavaScript 和接口。</strong></p>
<p>配合前两篇一起看，会更完整：</p>
<ol>
<li><a href="https://tc.xfei.tech/learn/browser-page-load-flow-url-request-rendering">浏览器输入 URL 后发生了什么？页面加载流程全解析</a></li>
<li><a href="https://tc.xfei.tech/learn/absolute-relative-path-browser-resource-resolution">绝对路径和相对路径是什么？浏览器如何补全资源地址</a></li>
</ol>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[浏览器输入 URL 后发生了什么？页面加载流程全解析]]></title>
      <link>https://tc.xfei.tech/learn/browser-page-load-flow-url-request-rendering/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/browser-page-load-flow-url-request-rendering/</guid>
      <pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[从地址栏输入 URL 开始，系统讲清浏览器如何补全地址、发起请求、解析 HTML、加载资源、执行 JavaScript，直到页面完成渲染。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/page-load-flow.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/page-load-flow.webp" alt="浏览器输入 URL 后发生了什么？页面加载流程全解析" /></p>
<h2 id="-url-">浏览器输入 URL 后要经历哪些步骤？</h2>
<p>很多人以为&quot;输入 URL 回车&quot;只是打开了一个网页，其实浏览器在背后做了很多事。</p>
<p>如果把整个过程拆开，它大致会经历这几个阶段：</p>
<ol>
<li>处理 URL。</li>
<li>发起网络请求。</li>
<li>拿到 HTML 文档。</li>
<li>继续加载 CSS、图片、JavaScript 等资源。</li>
<li>执行 JavaScript，生成或修改页面内容。</li>
<li>页面渲染完成。</li>
</ol>
<p>理解这条流水线，对爬虫学习特别重要。
因为你后面判断&quot;数据到底来自哪里&quot;，本质上就是在判断它落在这条流水线的哪个环节。</p>
<h2 id="-url">第一步：浏览器怎么处理 URL？</h2>
<p>当你在地址栏输入一个网址后，浏览器并不是立刻发请求，它会先做一轮标准化处理。</p>
<p>常见处理包括：</p>
<ol>
<li>补全协议，比如自动加上 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http://</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://</code>。</li>
<li>对非 ASCII 字符做 URL 编码。</li>
<li>把最终地址整理成可以真正发请求的完整 URL。</li>
</ol>
<p>这里最关键的一点是：<strong>浏览器真正发送请求时，用的一定是完整 URL。</strong></p>
<p>也就是说，浏览器输入框里看起来&quot;像地址&quot;的东西，和真正能发出去的地址，不一定是同一个形态。</p>
<h2 id="heading">第二步：浏览器怎么向服务器发起请求？</h2>
<p>URL 处理完成后，浏览器会向服务器发起 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">GET</code> 请求。</p>
<p>如果服务器正常返回，浏览器会收到一个响应。
这个响应里最核心的是响应头和响应体：</p>
<ol>
<li>响应头告诉浏览器&quot;这是什么类型的内容&quot;。</li>
<li>响应体里装着真正的资源内容。</li>
</ol>
<p>对于你在地址栏输入的网页来说，服务器通常先返回的是 HTML 文档。</p>
<h2 id="-content-type-">第三步：浏览器怎么根据 Content-Type 处理内容？</h2>
<p>浏览器不会只看文件后缀，它更在意响应头里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code>。</p>
<p>如果响应头告诉它这是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">text/html</code>，浏览器就会把返回内容当作 HTML 来解析。
如果是 CSS，就按 CSS 处理。
如果是 JavaScript，就交给 JS 引擎执行。</p>
<p>这就是为什么同样是一个地址，返回不同 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code> 时，浏览器会表现出完全不同的行为。</p>
<h2 id="html-">第四步：HTML 是怎么被解析的？</h2>
<p>浏览器拿到 HTML 后，会从上到下顺序解析。</p>
<p>解析过程中，它会遇到不同类型的标签：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">link</code>：通常引用 CSS。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code>：通常引用图片。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">script</code>：通常引用 JavaScript。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code>：默认不会自动发请求，要等用户点击。</li>
</ol>
<p>这一步很像浏览器边读边做决定。
看到一个资源，就根据资源类型继续往下处理。</p>
<h2 id="cssjs-">第五步：CSS、图片、JS 等资源是怎么加载的？</h2>
<p>当浏览器解析到 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">link</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">script</code> 这类资源引用时，它会继续发请求去拿这些内容。</p>
<p>这里经常会遇到一个细节：路径不一定写的是完整 URL。
这时候就要先分清 <strong>绝对路径</strong> 和 <strong>相对路径</strong>。</p>
<p>比如：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;link rel=&quot;stylesheet&quot; href=&quot;/css/main.css&quot; /&gt;
&lt;img src=&quot;./logo.svg&quot; /&gt;
&lt;script src=&quot;js/app.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>这些路径如果不是完整 URL，浏览器就会先补全，再去请求。</p>
<p>想把这块理解得更扎实，可以继续看 <a href="https://tc.xfei.tech/learn/absolute-relative-path-browser-resource-resolution">绝对路径和相对路径是什么？浏览器如何补全资源地址</a>。</p>
<h2 id="javascript-">第六步：JavaScript 怎么改变页面内容？</h2>
<p>当浏览器拿到 JavaScript 文件后，会把它交给 JS 引擎执行。</p>
<p>JavaScript 执行后，页面可能发生这些变化：</p>
<ol>
<li>新增元素。</li>
<li>删除元素。</li>
<li>修改文本。</li>
<li>请求接口并把接口结果渲染到页面上。</li>
</ol>
<p>所以你在页面上看到的内容，并不一定都来自最初返回的 HTML。</p>
<p>这点对爬虫特别关键，因为它直接决定了你是要：</p>
<ol>
<li>直接解析 HTML。</li>
<li>还是去分析 JavaScript 发起的接口请求。</li>
</ol>
<h2 id="heading-1">第七步：为什么源代码和页面内容不一样？</h2>
<p>很多初学者第一次看浏览器开发者工具时，都会有这个疑问：
&quot;页面上明明有内容，为什么查看网页源代码却找不到？&quot;</p>
<p>原因很简单：</p>
<ol>
<li>网页源代码看到的是服务器最初返回的 HTML。</li>
<li>页面上看到的是浏览器执行完 JavaScript 之后的最终结果。</li>
</ol>
<p>也就是说，源代码是&quot;起点&quot;，页面上的内容是&quot;结果&quot;。</p>
<p>如果某些数据是 JS 动态生成的，源代码里可能完全没有，但 Elements 面板里已经出现了。</p>
<h2 id="elements-">第八步：Elements 面板展示的是什么？</h2>
<p>浏览器里的 Elements 面板看到的，不是静态文本，而是当前实时的 DOM 结构。</p>
<p>你可以在控制台里选中一个元素，然后通过 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">$0</code> 快速引用它。</p>
<p>比如你修改了 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">$0</code> 的文本内容，Elements 面板会立刻变化。
但这并不意味着网页源代码也变了，因为源代码还是服务器返回的原始 HTML。</p>
<p>这就是&quot;源代码&quot;和&quot;当前页面 DOM&quot;之间最重要的区别。</p>
<h2 id="heading-2">第九步：怎么判断一个页面是不是动态页面？</h2>
<p>如果你想快速判断一个页面是不是动态页面，可以按这个顺序观察：</p>
<ol>
<li>先看页面上有没有目标数据。</li>
<li>再看网页源代码里有没有。</li>
<li>如果源代码没有，但页面上有，去 Elements 面板看。</li>
<li>如果 Elements 里有，大概率是 JavaScript 生成或渲染的。</li>
<li>再去 Network 面板找对应接口。</li>
</ol>
<p>这个判断顺序，基本能覆盖大多数网页数据源分析场景。</p>
<p>如果你想继续分清&quot;源代码&quot;和&quot;Elements&quot;到底差在哪，可以接着看 <a href="https://tc.xfei.tech/learn/source-code-vs-elements-static-dynamic-page">网页源代码和 Elements 有什么区别？静态页面与动态页面怎么判断</a>。</p>
<h2 id="heading-3">浏览器页面加载流程怎么记最实用？</h2>
<p>你可以把页面加载流程记成一句话：</p>
<p><strong>URL 先被浏览器整理，HTML 先被拉回来，资源继续补齐，JavaScript 再把页面&quot;活&quot;起来。</strong></p>
<p>只要这条主线清楚了，你后面学抓包、接口复现、静态页和动态页判断，都会顺很多。</p>
<h2 id="heading-4">总结</h2>
<p>浏览器输入 URL 后发生的事，不是&quot;打开一个页面&quot;这么简单，而是一整套连续动作：</p>
<ol>
<li>处理 URL。</li>
<li>请求 HTML。</li>
<li>解析资源。</li>
<li>执行 JavaScript。</li>
<li>渲染最终页面。</li>
</ol>
<p>这篇文章最重要的价值，是帮你建立一个判断框架。
当你以后面对任何网页时，都可以先问自己：<strong>这个数据是在 HTML 里，还是在 JavaScript 之后才出现的？</strong></p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[绝对路径和相对路径是什么？浏览器如何补全资源地址]]></title>
      <link>https://tc.xfei.tech/learn/absolute-relative-path-browser-resource-resolution/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/absolute-relative-path-browser-resource-resolution/</guid>
      <pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[讲清绝对路径、相对路径、协议相对路径和根路径的区别，并结合浏览器解析规则说明资源地址是怎么被补全成完整 URL 的。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/absolute-relative-path.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/absolute-relative-path.webp" alt="绝对路径和相对路径是什么？浏览器如何补全资源地址" /></p>
<h2 id="heading">先记住一个前提</h2>
<p><strong>只有完整 URL 才能真正发起网络请求。</strong></p>
<p>浏览器看到的路径写法可以很多，但真正发给服务器的，必须是它自己补全后的完整地址。</p>
<p>所以你在页面里看到的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./css/main.css</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/css/main.css</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//example.com/main.css</code>，只是写法不同。
浏览器最终都会把它们整理成完整 URL，再去请求资源。</p>
<h2 id="heading-1">什么是绝对路径</h2>
<p>绝对路径的特点是：<strong>它本身就能定位资源</strong>。</p>
<p>常见写法有三种：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://tc.xfei.tech/web/css/styles.css
//tc.xfei.tech/web/css/styles.css
/web/css/styles.css
</code></pre>
<p>这三种写法的共同点是：浏览器都有办法把它们补成完整 URL。</p>
<h3 id="1-url">1）完整 URL</h3>
<p>最完整的写法就是协议、域名、路径都齐全。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://tc.xfei.tech/web/css/styles.css
</code></pre>
<p>这类地址可以直接请求，几乎不会产生歧义。</p>
<h3 id="2">2）协议相对写法</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//tc.xfei.tech/web/css/styles.css
</code></pre>
<p>这种写法省略了协议，浏览器会自动继承当前页面使用的协议。
如果当前页面是 HTTPS，最终就会补成 HTTPS。</p>
<h3 id="3">3）根路径写法</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/web/css/styles.css
</code></pre>
<p>这种写法从站点根目录开始找资源，和当前页面所在的子目录无关。</p>
<h2 id="heading-2">什么是相对路径</h2>
<p>相对路径的特点是：<strong>它必须依赖当前页面地址来补全。</strong></p>
<p>常见写法有：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">css/styles.css
./css/styles.css
js/app.js
./logo.svg
</code></pre>
<p>它们本身不是完整地址，单独拿出来不能直接请求。
浏览器必须先知道“当前页面在哪个目录下”，然后才能把它补成完整 URL。</p>
<h2 id="heading-3">浏览器到底是相对谁</h2>
<p>相对路径相对的不是“整个网站”，而是<strong>当前页面的 URL 所在目录</strong>。</p>
<p>例如当前页面是：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://tc.xfei.tech/web/index.html
</code></pre>
<p>那么：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">css/styles.css
./css/styles.css
</code></pre>
<p>最终都会被补成：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https://tc.xfei.tech/web/css/styles.css
</code></pre>
<p>这也是为什么很多人第一次写路径会出错。
他以为路径是相对网站根目录，实际上它是相对当前页面目录。</p>
<h2 id="heading-4">浏览器如何补全路径</h2>
<p>浏览器补全路径时，通常会按这几个步骤理解：</p>
<ol>
<li>先拿到当前页面的完整地址。</li>
<li>找到当前页面所在目录。</li>
<li>把相对路径拼到这个目录后面。</li>
<li>得到最终可请求的完整 URL。</li>
</ol>
<p>如果你把这个机制想明白了，很多“资源 404”问题就会立刻变得清晰。</p>
<h2 id="heading-5">在页面里最常见的几种资源引用</h2>
<h3 id="css">CSS</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;link rel=&quot;stylesheet&quot; href=&quot;./css/main.css&quot; /&gt;
</code></pre>
<h3 id="heading-6">图片</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;img src=&quot;./images/logo.svg&quot; /&gt;
</code></pre>
<h3 id="javascript">JavaScript</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;script src=&quot;js/app.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>这些写法都很常见，而且都依赖浏览器补全。</p>
<p>如果路径写错了，最直接的结果就是资源加载失败。
所以分析页面时，除了看标签本身，还要看它引用资源的路径是否正确。</p>
<h2 id="heading-7">为什么绝对路径在分析页面时很省心</h2>
<p>绝对路径最大的优点是稳定。</p>
<p>不管当前页面在哪个目录，只要路径是绝对的，浏览器请求的目标都不会变。
这对于静态站点、后台管理页、以及很多爬虫脚本里的资源定位都很有帮助。</p>
<p>相对路径虽然写起来短，但当页面目录结构变化时，出错概率更高。</p>
<h2 id="heading-8">路径分析时最容易混淆的点</h2>
<h3 id="1-">1）<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 开头不等于相对路径</h3>
<p>很多初学者会把 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/css/main.css</code> 看成“相对当前文件”的路径，其实不是。
它是从站点根目录开始的绝对路径。</p>
<h3 id="2---">2）<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code> 和不写 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">./</code> 很接近</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">css/styles.css
./css/styles.css
</code></pre>
<p>这两种在多数场景下效果几乎一样，都是相对路径。</p>
<h3 id="3-1">3）协议相对写法会继承当前协议</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">//tc.xfei.tech/web/css/styles.css
</code></pre>
<p>这个写法会直接继承页面协议，所以调试时要特别注意它最终是 http 还是 https。</p>
<h2 id="heading-9">一个很实用的排查方法</h2>
<p>当你发现资源加载失败，可以按这个顺序排查：</p>
<ol>
<li>先看路径是不是完整 URL。</li>
<li>如果不是，判断它是绝对路径还是相对路径。</li>
<li>如果是相对路径，确认当前页面目录是什么。</li>
<li>把路径手动补成完整 URL。</li>
<li>直接在浏览器里打开，看资源是否真的存在。</li>
</ol>
<p>这套方法不仅适用于网页资源，也适用于你后面做爬虫时分析接口路径。</p>
<h2 id="heading-10">一句话总结</h2>
<p>你可以这样记：</p>
<p><strong>绝对路径自己就能指路，相对路径要靠当前页面补全。</strong></p>
<p>只要你能把这个规则记牢，浏览器为什么能加载 CSS、图片和脚本，基本就能讲通了。</p>
<p>这篇内容和 <a href="https://tc.xfei.tech/learn/browser-page-load-flow-url-request-rendering">浏览器输入 URL 后发生了什么？页面加载流程全解析</a> 是一组，建议顺着一起看。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3 差异详解（含队头阻塞）]]></title>
      <link>https://tc.xfei.tech/learn/http-versions-1-0-1-1-2-3-differences/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/http-versions-1-0-1-1-2-3-differences/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[系统梳理 HTTP 各版本演进逻辑：从短连接到长连接，从文本到二进制分帧，从应用层队头阻塞到传输层优化。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/http-12.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/http-12.webp" alt="HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3 差异详解（含队头阻塞）" /></p>
<h2 id="-http-">为什么要理解 HTTP 版本演进</h2>
<p>很多网络问题看起来像“代码问题”，本质却是协议机制差异导致的。
尤其在抓包和性能分析场景里，搞清 HTTP/1.0 到 HTTP/3 的演进，会让你更快判断瓶颈在哪里。</p>
<h2 id="http10">HTTP/1.0：一次请求一次连接</h2>
<p>HTTP/1.0 的典型特征是短连接：</p>
<ol>
<li>发一个请求，建立一次 TCP 连接。</li>
<li>请求完成后，连接立即关闭。</li>
</ol>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-03.webp" alt="HTTP/1.0 短连接示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>问题在于 TCP 建连和断连都有成本。页面资源一多，这个成本会被不断放大。</p>
<h2 id="http11">HTTP/1.1：长连接提升复用效率</h2>
<p>HTTP/1.1 引入 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Keep-Alive</code>，让多个请求复用同一个 TCP 连接，减少反复握手与挥手开销。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-04.webp" alt="HTTP/1.1 长连接示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>这是非常关键的一步优化，但它并没有彻底解决并发效率问题。</p>
<h2 id="http11-">HTTP/1.1 的核心痛点：队头阻塞</h2>
<p>在同一个连接内，请求与响应的处理顺序强相关。
前面的请求慢，后面的请求就会被拖住，这就是 HTTP 层面的队头阻塞。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-05.webp" alt="HTTP/1.1 队头阻塞示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>浏览器常见绕法是开多个 TCP 连接并行请求，但这也会带来额外连接成本。</p>
<h2 id="http2--">HTTP/2：二进制分帧 + 多路复用</h2>
<p>HTTP/2 的关键变化是把传输单位从“整段文本报文”改为“二进制帧（Frame）”。</p>
<ol>
<li>一个完整请求/响应被组织为一个流（Stream）。</li>
<li>每个帧标记所属流编号。</li>
<li>多个流可在同一 TCP 连接上交错传输。</li>
</ol>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-06.webp" alt="HTTP/2 分帧示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-08.webp" alt="HTTP/2 多路复用示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>这让 HTTP 层面的队头阻塞得到明显缓解。</p>
<h2 id="http2-">HTTP/2 另一个关键优化：头部压缩</h2>
<p>HTTP/2 通过静态表 + 动态表压缩头字段，减少重复传输开销。
高频字段可用索引表达，不必每次完整传输。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-09.webp" alt="HTTP/2 头部压缩示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>参考规范： <a href="https://httpwg.org/specs/rfc7541.html#static.table.definition">RFC 7541 静态表定义</a></p>
<h2 id="http2-tcp-">HTTP/2 仍然存在的问题：TCP 队头阻塞</h2>
<p>HTTP/2 虽然解决了应用层请求乱序匹配的问题，但底层仍依赖 TCP。
一旦 TCP 某个分段丢包，后续数据即使已到达，也要等待重传完成。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-07.webp" alt="TCP 层队头阻塞示意图 1" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-10.webp" alt="TCP 层队头阻塞示意图 2" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>这就是传输层队头阻塞。</p>
<h2 id="http3-quic">HTTP/3：基于 QUIC，面向传输层问题优化</h2>
<p>HTTP/3 将底层从 TCP 切换到 UDP，并通过 QUIC 提供可靠传输能力。
目标是把连接管理、重传、多路机制做得更适合现代网络环境。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-11.webp" alt="HTTP/3 与 QUIC 示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>常见收益包括：</p>
<ol>
<li>握手更快。</li>
<li>连接 ID 支持网络切换时更平滑地延续会话。</li>
<li>更好地缓解传输层阻塞影响。</li>
</ol>
<h2 id="heading">版本差异速览</h2>
<ol>
<li>HTTP/1.0：短连接，连接成本高。</li>
<li>HTTP/1.1：长连接复用，但同连接内易队头阻塞。</li>
<li>HTTP/2：二进制分帧、多路复用、头部压缩，解决应用层阻塞痛点。</li>
<li>HTTP/3：基于 QUIC，重点改善传输层阻塞与连接体验。</li>
</ol>
<h2 id="heading-1">对爬虫和接口调试的实际价值</h2>
<p>理解版本差异后，你在抓包里看到“慢”时会更清楚该看哪里：</p>
<ol>
<li>如果是大量短连接，先怀疑连接复用不足。</li>
<li>如果同连接串行等待明显，关注 HTTP/1.1 队头阻塞。</li>
<li>如果上层并发正常但仍卡，考虑 TCP 丢包和传输层影响。</li>
<li>协议版本不同，开发者工具可见信息也不同，分析方式要跟着调整。</li>
</ol>
<h2 id="heading-2">总结</h2>
<p>HTTP 版本演进，本质是在不断降低网络通信中的结构性开销和阻塞成本。
当你把“问题发生在应用层还是传输层”区分清楚，调试效率会明显提升。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[HTTP 协议入门：请求、响应、状态码（爬虫视角）]]></title>
      <link>https://tc.xfei.tech/learn/http-protocol-request-response-status-codes/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/http-protocol-request-response-status-codes/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[从爬虫初学者最常见场景出发，讲清 HTTP 请求与响应的结构、状态码的判断方法，以及抓包时应该优先看哪些字段。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/http-protocol.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/http-protocol.webp" alt="HTTP 协议入门：请求、响应、状态码（爬虫视角）" /></p>
<h2 id="-http">为什么爬虫一定要先学 HTTP</h2>
<p>爬虫本质不是“神秘技术”，而是一个自动发请求、收响应的客户端程序。
你能不能把请求发对、把响应看懂，直接决定了爬虫项目能不能跑起来。</p>
<p>所以在学代码之前，先吃透 HTTP 的通信格式，后面抓包、接口复现、反爬排查都会轻松很多。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-01.webp" alt="HTTP 请求格式示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<h2 id="http-">HTTP 的两个核心特性</h2>
<ol>
<li>无状态：每次请求默认独立，服务端不会天然记住你上一次做过什么。</li>
<li>文本协议（HTTP/1.x 视角）：请求与响应按规范组织成可读文本结构。</li>
</ol>
<p>无状态意味着什么？
你上一次请求成功，不代表下一次自动成功。Cookie、Token、会话参数，都需要你在后续请求里明确带上。</p>
<h2 id="heading">请求报文怎么读：三段式结构</h2>
<p>一个完整 HTTP 请求可分为三部分：</p>
<ol>
<li>请求行</li>
<li>请求头</li>
<li>请求体</li>
</ol>
<p>其中，请求头和请求体之间必须有一个空行，这是协议格式要求。</p>
<h3 id="1">1）请求行：先看“你要做什么”</h3>
<p>请求行通常包含：请求方法 + 路径 + 协议版本。
爬虫里最常见的方法是：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">GET</code>：拿数据</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">POST</code>：提交数据</li>
</ol>
<p>很多登录、注册、搜索提交会用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">POST</code>，并把参数放进请求体。</p>
<h3 id="2">2）请求头：再看“你是谁，从哪来”</h3>
<p>请求头是键值对集合，用来补充请求上下文。初学爬虫先重点看这几个：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code>：声明客户端环境（浏览器/系统）。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code>：说明当前请求来源页面。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Cookie</code>：携带会话态与业务标识。</li>
</ol>
<p>请求失败时，先别急着改代码，先对比浏览器和脚本这三个头是否一致，往往就能快速定位问题。</p>
<h3 id="3">3）请求体：业务数据放这里</h3>
<p>请求体是提交给服务端的业务内容。
<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">GET</code> 通常没有请求体，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">POST</code> 更常见有请求体。</p>
<h2 id="heading-1">响应报文怎么读：同样三段式</h2>
<p>响应结构也分三部分：</p>
<ol>
<li>响应行</li>
<li>响应头</li>
<li>响应体</li>
</ol>
<p>响应头和响应体之间同样有空行分隔。</p>
<p><img src="https://static.xfei.tech/learn/images/http-basics/http-02.webp" alt="HTTP 响应格式示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<h3 id="heading-2">响应行：状态判断的第一现场</h3>
<p>响应行包含：协议版本 + 状态码 + 状态描述。
在爬虫排错里，状态码是第一优先级信息。</p>
<h2 id="heading-3">常见状态码速查（爬虫高频）</h2>
<h3 id="2xx">2xx：成功</h3>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">200 OK</code>：请求成功并返回结果。</li>
</ol>
<h3 id="3xx">3xx：重定向</h3>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">301 Moved Permanently</code>：永久重定向，浏览器通常会缓存。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">302 Found</code>：临时重定向，通常每次都要重新请求旧地址。</li>
</ol>
<h3 id="4xx">4xx：客户端请求问题</h3>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">400 Bad Request</code>：请求格式或参数错误。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">401 Unauthorized</code>：未授权，常见于未登录或登录失效。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">403 Forbidden</code>：请求被拒绝，常见于权限不足或风控拦截。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">404 Not Found</code>：路径错误或资源不存在。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">429 Too Many Requests</code>：请求过快，被限流。</li>
</ol>
<h3 id="5xx">5xx：服务端错误</h3>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">500</code> 系列通常表示服务端内部处理失败。</li>
</ol>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>更多状态码参考： <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status">MDN HTTP 状态码文档</a></p>
</blockquote>
<h2 id="heading-4">一个实战中常见的坑</h2>
<p>状态码 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">200</code> 不等于业务成功。
很多站点会返回 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">200</code>，但在响应体里给出“未登录”“签名错误”“访问频繁”等业务错误信息。</p>
<p>所以排错顺序建议固定为：</p>
<ol>
<li>先看状态码</li>
<li>再看响应头（例如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code>）</li>
<li>最后看响应体的真实业务内容</li>
</ol>
<h2 id="heading-5">给初学者的请求排查流程</h2>
<p>遇到“脚本拿不到数据”时，按这个流程走：</p>
<ol>
<li>在浏览器开发者工具定位同一请求。</li>
<li>对比 URL、方法、参数是否一致。</li>
<li>对比关键请求头：<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Cookie</code>。</li>
<li>对比状态码与响应体错误提示。</li>
<li>观察是否出现 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">301/302</code> 跳转或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">429</code> 限流。</li>
</ol>
<p>这个流程跑通后，你会发现大多数“玄学问题”都能落到明确字段差异上。</p>
<h2 id="heading-6">总结</h2>
<p>学 HTTP 不是背概念，而是为了把请求发送和响应解读变成可重复的工程动作。
把请求结构、响应结构、状态码这三块打牢，爬虫入门就完成了一半。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[HTTP 请求头与响应头实战：User-Agent、Referer、Cookie、Content-Type]]></title>
      <link>https://tc.xfei.tech/learn/http-headers-user-agent-referer-cookie-content-type/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/http-headers-user-agent-referer-cookie-content-type/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[聚焦爬虫开发最常用的 HTTP 头字段，讲清它们的作用、常见问题与排查顺序，帮你快速提升接口复现成功率。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/http-headers.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/http-headers.webp" alt="HTTP 请求头与响应头实战：User-Agent、Referer、Cookie、Content-Type" /></p>
<h2 id="-http-">为什么 HTTP 头字段决定了爬虫成败？</h2>
<p>很多同学会遇到这种情况：浏览器访问正常，脚本请求却失败。
最常见原因不是 URL 写错，而是 HTTP 头信息不完整或不一致。</p>
<p>在爬虫场景里，头字段可以理解为&quot;请求的上下文身份信息&quot;。服务端经常根据这些信息做鉴权、风控和内容分发。</p>
<h2 id="http--3-">HTTP 请求头要看哪 3 个？</h2>
<h3 id="user-agent-">User-Agent 是什么？声明客户端身份有什么用？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code> 用于告诉服务端&quot;我是哪个浏览器/设备发起的请求&quot;。
许多站点会根据它做基础策略判断，缺失或异常可能直接触发拦截。</p>
<p>实战建议：</p>
<ol>
<li>先复用浏览器真实 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code>。</li>
<li>批量采集时保持同一会话内稳定，避免频繁切换。</li>
<li>如果返回异常页，优先检查 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code> 是否丢失。</li>
</ol>
<h3 id="referer-">Referer 是什么？为什么重要？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code> 表示当前请求来自哪个页面。
部分接口会要求&quot;必须从特定页面跳转而来&quot;，否则直接拒绝。</p>
<p>实战建议：</p>
<ol>
<li>先在浏览器抓包确认真实 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code>。</li>
<li>请求链路中涉及详情页、播放页、下载页时重点检查。</li>
<li>出现 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">403</code> 时，把 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code> 放到首批排查项。</li>
</ol>
<h3 id="cookie-">Cookie 是什么？为什么要携带登录态？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Cookie</code> 是会话状态的核心载体。
登录后拿到的数据，脚本如果不带对应 Cookie，大概率会变成未登录视图或直接报错。</p>
<p>实战建议：</p>
<ol>
<li>把 Cookie 视为&quot;会话钥匙&quot;，注意时效和刷新。</li>
<li>优先使用会话对象自动管理 Cookie，而不是手写拼接。</li>
<li>当响应提示未授权时，先检查 Cookie 是否过期或缺字段。</li>
</ol>
<h2 id="-content-type">响应头为什么要先看 Content-Type？</h2>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code> 告诉你响应体是什么格式。
例如：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">text/html; charset=utf-8</code>：HTML 页面</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">application/json</code>：JSON 数据</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">image/webp</code>：图片资源</li>
</ol>
<p>它对爬虫的价值主要有两点：</p>
<ol>
<li>判断当前拿到的是&quot;真实业务数据&quot;还是&quot;跳转页/错误页&quot;。</li>
<li>判断解码方式，避免乱码或解析失败。</li>
</ol>
<h2 id="heading">为什么不能只看状态码，还要看响应头？</h2>
<p>很多请求返回 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">200</code>，但实际上拿到的是登录页 HTML，而不是目标 JSON。
这时候如果不看 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code>，你会误以为接口成功，后续解析才报错。</p>
<p>正确做法是：</p>
<ol>
<li>先看状态码</li>
<li>再看 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code></li>
<li>最后根据类型选择解析策略（HTML 解析或 JSON 解析）</li>
</ol>
<h2 id="heading-1">如何用请求头与响应头联动排错？</h2>
<p>当你遇到&quot;浏览器有数据，脚本没数据&quot;时，按顺序检查：</p>
<ol>
<li>URL、请求方法是否一致。</li>
<li>请求参数（query/body）是否一致。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code> 是否缺失或异常。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code> 是否符合来源要求。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Cookie</code> 是否有效且完整。</li>
<li>响应状态码是否异常（尤其 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">401/403/429</code>）。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code> 是否符合预期。</li>
</ol>
<p>这 7 步能覆盖大部分接口复现失败场景。</p>
<h2 id="heading-2">浏览器开发者工具怎么用来排查爬虫请求？</h2>
<p>建议固定在 Network 面板做三件事：</p>
<ol>
<li>找到目标请求（先看 document，再定位 xhr/fetch）。</li>
<li>查看 Headers，记录关键请求头和响应头。</li>
<li>对照脚本请求逐项比对，不一致就先改一致。</li>
</ol>
<p>只要你养成&quot;先抓包，再写代码，再比对&quot;的习惯，排错成本会明显下降。</p>
<h2 id="heading-3">总结</h2>
<p>HTTP 头字段不是细枝末节，而是爬虫请求能否通过服务端校验的基础。
把 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">User-Agent</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Referer</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Cookie</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Content-Type</code> 这四项吃透，接口复现成功率会有非常明显的提升。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[网页由什么组成？HTML、CSS、JavaScript 入门（爬虫必学）]]></title>
      <link>https://tc.xfei.tech/learn/html-css-javascript-webpage-basics/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/html-css-javascript-webpage-basics/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[从爬虫初学者视角讲清网页三件套：HTML 负责结构，CSS 负责样式，JavaScript 负责交互，以及为什么看懂网页结构是抓取数据的第一步。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/html-css-javascript.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/html-css-javascript.webp" alt="网页由什么组成？HTML、CSS、JavaScript 入门（爬虫必学）" /></p>
<h2 id="heading">为什么爬虫要先学网页基础？</h2>
<p>很多同学学爬虫时，会急着上手 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">requests</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">BeautifulSoup</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">Scrapy</code>，但一打开网页源代码就懵了：
标签、属性、样式、脚本混在一起，到底哪个才是我要抓的数据？</p>
<p>所以在写爬虫之前，先搞懂网页由什么组成非常重要。
你不需要一开始就成为前端工程师，但至少要知道网页里哪些内容负责结构、哪些内容负责样式、哪些内容负责交互。</p>
<p>一个普通网页，核心由三部分组成：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">HTML</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">CSS</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">JavaScript</code></li>
</ol>
<p>可以把它们理解成一套房子：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">HTML</code> 是毛坯房，负责基础结构。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">CSS</code> 是装修方案，负责页面长相。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">JavaScript</code> 是智能控制系统，负责交互和逻辑。</li>
</ol>
<h2 id="html-">HTML 是什么？网页的骨架是怎么组成的？</h2>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">HTML</code> 全称是 HyperText Markup Language，中文叫超文本标记语言。
注意，它是&quot;标记语言&quot;，不是&quot;编程语言&quot;。</p>
<p>这意味着 HTML 本身不负责复杂逻辑判断，它主要负责描述网页结构。
例如页面上的标题、段落、图片、链接、按钮，都可以用 HTML 标签表示。</p>
<p>一个最基础的 HTML 页面大概长这样：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;title&gt;我的第一个网页&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;爬虫你好&lt;/h1&gt;
    &lt;p&gt;这是一个简单的网页。&lt;/p&gt;
    &lt;a href=&quot;https://www.xfei.tech&quot;&gt;访问文档&lt;/a&gt;
    &lt;button&gt;点我一下&lt;/button&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这里面有几个关键部分：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;!doctype html&gt;</code>：声明这是一个 HTML 文档。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;html&gt;</code>：整个网页的根元素。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;head&gt;</code>：网页配置区，例如编码、标题、样式引用。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;body&gt;</code>：网页内容区，用户真正能看到的内容通常在这里。</li>
</ol>
<p>从爬虫角度看，HTML 是最重要的基础。
因为很多页面数据，最终都会以 HTML 标签和文本的形式出现在页面结构里。</p>
<h2 id="css-">CSS 是什么？网页装修是怎么实现的？</h2>
<p>只有 HTML 的页面通常很朴素，就像只有墙体和门窗的毛坯房。
如果想让网页变好看，就需要 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">CSS</code>。</p>
<p>CSS 全称是 Cascading Style Sheets，中文叫层叠样式表。它负责控制网页的视觉表现，例如：</p>
<ol>
<li>字体大小</li>
<li>文字颜色</li>
<li>背景颜色</li>
<li>图片尺寸</li>
<li>按钮圆角</li>
<li>页面布局</li>
<li>鼠标悬停效果</li>
</ol>
<p>例如：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1 {
  color: #1f2937;
  text-align: center;
}

button {
  padding: 10px 16px;
  border-radius: 999px;
  background: #111827;
  color: white;
}
</code></pre>
<p>CSS 本身不改变网页&quot;有什么内容&quot;，它改变的是这些内容&quot;怎么显示&quot;。
同一份 HTML，换一套 CSS，视觉效果可能完全不同。</p>
<p>对爬虫来说，CSS 还有一个额外价值：它会用到选择器。
选择器不仅能选中元素来改样式，以后我们也会用类似写法在 Python 或浏览器里定位要抓取的数据。</p>
<h2 id="javascript-">JavaScript 是什么？网页交互和逻辑是怎么实现的？</h2>
<p>如果说 HTML 让网页&quot;能看&quot;，CSS 让网页&quot;好看&quot;，那么 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">JavaScript</code> 就是让网页&quot;能动起来&quot;。</p>
<p>JavaScript 是一门真正的编程语言。它可以处理：</p>
<ol>
<li>点击按钮后弹出提示。</li>
<li>鼠标划过后展开菜单。</li>
<li>输入框实时校验格式。</li>
<li>页面滚动时加载更多数据。</li>
<li>请求接口并把结果渲染到页面上。</li>
</ol>
<p>例如：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;button id=&quot;btn&quot;&gt;添加一行文字&lt;/button&gt;
&lt;div class=&quot;container&quot;&gt;&lt;/div&gt;

&lt;script&gt;
  const btn = document.querySelector(&quot;#btn&quot;)
  const container = document.querySelector(&quot;.container&quot;)

  btn.addEventListener(&quot;click&quot;, () =&gt; {
    const p = document.createElement(&quot;p&quot;)
    p.textContent = &quot;hello&quot;
    container.appendChild(p)
  })
&lt;/script&gt;
</code></pre>
<p>这段代码做了三件事：</p>
<ol>
<li>用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">querySelector</code> 选中按钮。</li>
<li>给按钮注册点击事件。</li>
<li>点击后创建一个新的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">p</code> 元素并插入页面。</li>
</ol>
<p>这就是 JavaScript 的作用：处理交互，操作页面结构。</p>
<h2 id="heading-1">为什么浏览器内容和网页源代码不一样？</h2>
<p>初学爬虫时，一个常见困惑是：
浏览器里明明有数据，但右键查看网页源代码却找不到。</p>
<p>原因往往是：这些数据不是 HTML 初始返回的，而是 JavaScript 后续请求接口再渲染出来的。</p>
<p>也就是说，网页内容可能有两种来源：</p>
<ol>
<li>服务端直接返回在 HTML 里。</li>
<li>JavaScript 在浏览器运行后，再请求接口并动态插入页面。</li>
</ol>
<p>这会影响爬虫策略：</p>
<ol>
<li>如果数据在 HTML 里，可以直接请求页面并解析 HTML。</li>
<li>如果数据由 JavaScript 动态加载，需要去 Network 面板找接口。</li>
<li>如果接口有复杂参数，可能还要分析请求头、Cookie、签名逻辑。</li>
</ol>
<p>所以网页基础不是&quot;前端知识点&quot;，而是爬虫判断数据来源的前置能力。</p>
<h2 id="heading-2">爬虫怎么观察网页？有什么标准顺序？</h2>
<p>打开一个网页后，建议按这个顺序观察：</p>
<ol>
<li>先看页面上有没有目标数据。</li>
<li>右键查看网页源代码，确认数据是否在原始 HTML 中。</li>
<li>打开开发者工具 Elements 面板，观察元素结构。</li>
<li>打开 Network 面板，刷新页面，看是否有接口返回目标数据。</li>
<li>如果页面点击后才出现数据，重点观察对应点击触发了哪些请求。</li>
</ol>
<p>这个顺序能帮你快速判断：应该解析 HTML，还是应该复现接口。</p>
<h2 id="htmlcssjavascript-">HTML、CSS、JavaScript 和爬虫有什么关系？</h2>
<p>把三者放到爬虫语境里，可以这样理解：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">HTML</code>：最常见的数据承载结构。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">CSS</code>：提供定位元素的选择器思路。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">JavaScript</code>：解释动态加载、交互行为和接口请求来源。</li>
</ol>
<p>很多爬虫问题看似是代码问题，其实是网页认知问题。
当你知道一个页面由结构、样式和交互三层组成，就不会只盯着页面表面，而会开始追问：数据到底是从哪里来的？</p>
<h2 id="heading-3">总结</h2>
<p>网页基础可以先抓住一句话：</p>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">HTML</code> 搭结构，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">CSS</code> 改样式，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">JavaScript</code> 做交互。</p>
<p>对爬虫学习者来说，最重要的不是把所有前端语法背下来，而是建立判断力：
看到一个页面，能分清内容在哪里、样式怎么选中元素、交互背后可能触发了什么请求。</p>
<p>这个判断力一旦建立，后面学 HTML 解析、CSS 选择器、接口抓包和 JavaScript 逆向都会顺很多。</p>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>延伸阅读：如果你还没建立客户端和服务器的概念，可以先看 <a href="https://tc.xfei.tech/learn/url-client-server-crawler-basics">URL 是什么？从客户端与服务器讲透爬虫第一步</a>。</p>
</blockquote>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[DOM 树是什么？节点、父子关系与兄弟关系详解]]></title>
      <link>https://tc.xfei.tech/learn/dom-tree-nodes-parent-child-sibling/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/dom-tree-nodes-parent-child-sibling/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[用爬虫初学者能理解的方式讲清 DOM 树、节点、父节点、子节点和兄弟节点，帮助你看懂网页结构并准确定位数据。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/dom-tree-nodes.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/dom-tree-nodes.webp" alt="DOM 树是什么？节点、父子关系与兄弟关系详解" /></p>
<h2 id="-dom-">为什么要理解 DOM 树</h2>
<p>学爬虫时，你迟早会遇到这样的问题：
页面上有很多相似的标题、图片、按钮和链接，怎么才能准确选中自己想要的那一个？</p>
<p>答案通常不只是“看标签名”，而是要看它在整个页面结构里的位置。
这就需要理解 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">DOM 树</code>。</p>
<p>DOM 树能帮你回答这些问题：</p>
<ol>
<li>某个元素在页面结构的哪一层？</li>
<li>它的外层容器是谁？</li>
<li>它旁边有哪些同级元素？</li>
<li>它内部还有没有更深层的子元素？</li>
<li>选择器为什么能选中它，或者为什么选不中它？</li>
</ol>
<h2 id="dom-">DOM 是什么</h2>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">DOM</code> 全称是 Document Object Model，中文通常叫文档对象模型。
你可以把它理解为：浏览器把 HTML 文档解析后，生成的一棵结构树。</p>
<p>HTML 源代码是文本，浏览器不能只把它当普通字符串处理。
浏览器会把标签、属性、文本、注释等内容解析成一个个节点，再按嵌套关系组织起来。</p>
<p>这棵树，就是 DOM 树。</p>
<p><img src="https://static.xfei.tech/learn/images/web-basics/dom-tree.webp" alt="DOM 树节点关系示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<h2 id="heading">什么是节点</h2>
<p>在 HTML 中，很多东西都可以被看作节点（Node），例如：</p>
<ol>
<li>文档本身是节点。</li>
<li>元素是节点，例如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">html</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">head</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">p</code>。</li>
<li>属性也可以被理解为节点信息，例如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">id</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">class</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code>。</li>
<li>文本是节点，例如标题里的文字。</li>
<li>注释也是节点。</li>
</ol>
<p>例如下面这段 HTML：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;div class=&quot;container&quot;&gt;
  &lt;h1 id=&quot;title&quot;&gt;爬虫你好&lt;/h1&gt;
  &lt;p&gt;这是一个段落。&lt;/p&gt;
  &lt;a href=&quot;https://www.xfei.tech&quot;&gt;访问文档&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>可以简单理解为：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code> 是一个元素节点。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">p</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code> 里面的子元素节点。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">class=&quot;container&quot;</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">id=&quot;title&quot;</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href=&quot;...&quot;</code> 是节点上的属性信息。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">爬虫你好</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">这是一个段落。</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">访问文档</code> 是文本内容。</li>
</ol>
<h2 id="heading-1">父节点、子节点和兄弟节点</h2>
<p>DOM 树最重要的是节点关系。
新手先掌握三种关系就够了：</p>
<ol>
<li>父节点</li>
<li>子节点</li>
<li>兄弟节点</li>
</ol>
<h3 id="heading-2">父节点：包住当前节点的上一层</h3>
<p>如果一个元素在另一个元素里面，那么外层元素就是它的父节点。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;body&gt;
  &lt;h1&gt;爬虫你好&lt;/h1&gt;
&lt;/body&gt;
</code></pre>
<p>在这段结构里，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code> 是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code> 的父节点。</p>
<h3 id="heading-3">子节点：被当前节点直接包住的下一层</h3>
<p>反过来看，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code> 是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code> 的子节点。
如果一个节点直接出现在另一个节点内部，它就是直接子节点。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;div class=&quot;card&quot;&gt;
  &lt;img src=&quot;/movie.webp&quot; alt=&quot;电影封面&quot; /&gt;
  &lt;h2&gt;电影标题&lt;/h2&gt;
&lt;/div&gt;
</code></pre>
<p>这里 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h2</code> 都是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div.card</code> 的子节点。</p>
<h3 id="heading-4">兄弟节点：拥有同一个父节点的同级节点</h3>
<p>如果多个元素被同一个父元素包住，它们就是兄弟节点。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;div class=&quot;card&quot;&gt;
  &lt;h2&gt;电影标题&lt;/h2&gt;
  &lt;p&gt;电影简介&lt;/p&gt;
  &lt;a href=&quot;/detail&quot;&gt;查看详情&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>这里 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h2</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">p</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 都在同一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div.card</code> 里面，所以它们是兄弟节点。</p>
<h2 id="dom--1">DOM 树为什么是一棵树</h2>
<p>HTML 是层层嵌套的结构。
一个文档里有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">html</code>，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">html</code> 里有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">head</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code>，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code> 里又有各种标题、段落、图片、按钮。</p>
<p>它看起来就像一棵倒过来的树：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">document</code> 是整棵树的入口。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">html</code> 是页面的根元素。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">head</code> 和 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code> 是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">html</code> 的子节点。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body</code> 下面继续分出更多内容节点。</li>
</ol>
<p>除了根节点外，每个节点通常都有自己的父节点，也可能拥有多个子节点或兄弟节点。</p>
<h2 id="dom--2">DOM 树和选择器有什么关系</h2>
<p>选择器的本质，就是在 DOM 树里找到目标节点。</p>
<p>例如：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container img {
  border-radius: 12px;
}
</code></pre>
<p>这句 CSS 的意思不是“随便找一张图片”，而是：</p>
<ol>
<li>先找到类名为 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">container</code> 的元素。</li>
<li>再去它内部找所有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code> 后代元素。</li>
</ol>
<p>这就是基于 DOM 层级关系的查找。</p>
<p>再看一个例子：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.card &gt; img {
  width: 160px;
}
</code></pre>
<p>这里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&gt;</code> 表示只找直接子元素。
如果图片藏在更深层的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code> 里面，就不会被选中。</p>
<p>所以，想写准选择器，就必须先看懂 DOM 树。</p>
<h2 id="-dom--1">爬虫里怎么利用 DOM 树</h2>
<p>假设页面结构是这样的：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;div class=&quot;movie-card&quot;&gt;
  &lt;img src=&quot;/cover.webp&quot; alt=&quot;电影封面&quot; /&gt;
  &lt;h2 class=&quot;title&quot;&gt;星际穿越&lt;/h2&gt;
  &lt;p class=&quot;score&quot;&gt;9.4&lt;/p&gt;
  &lt;a class=&quot;detail&quot; href=&quot;/movie/1&quot;&gt;查看详情&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>如果你要抓电影标题，不能只想“抓 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h2</code>”，因为页面上可能有很多 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h2</code>。
更稳的思路是：</p>
<ol>
<li>先定位每一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie-card</code>。</li>
<li>再在每个卡片内部找 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.title</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.score</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.detail</code>。</li>
<li>把同一个卡片里的标题、评分、链接组合成一条数据。</li>
</ol>
<p>这就是爬虫里非常常见的“先定位列表项，再提取字段”的思路。</p>
<h2 id="heading-5">常见误区：只看页面，不看结构</h2>
<p>浏览器展示出来的是视觉结果，DOM 树展示的是结构关系。
视觉上挨得很近的两个元素，不一定在 DOM 中就是兄弟节点；视觉上隔得很远的元素，也可能在同一个父容器里。</p>
<p>所以爬虫调试时，不要只凭页面肉眼判断。
更可靠的做法是打开开发者工具，在 Elements 面板里观察真实结构。</p>
<p>建议养成这个习惯：</p>
<ol>
<li>先选中页面上的目标文字。</li>
<li>在 Elements 面板里查看它所在的标签。</li>
<li>向上找外层容器。</li>
<li>判断它和其他字段之间的父子、兄弟关系。</li>
<li>再决定用什么选择器。</li>
</ol>
<h2 id="heading-6">总结</h2>
<p>DOM 树是浏览器理解网页结构的方式，也是爬虫定位数据的地图。</p>
<p>你只要先掌握这三组关系：</p>
<ol>
<li>父节点：外层包住当前节点的节点。</li>
<li>子节点：当前节点直接包住的下一层节点。</li>
<li>兄弟节点：拥有同一个父节点的同级节点。</li>
</ol>
<p>再配合 CSS 选择器，就能从“看见页面”进一步走到“准确定位数据”。
这一步打牢后，后面学 HTML 解析、XPath、CSS Selector 和动态页面抓包都会更轻松。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[CSS 选择器入门：从改样式到爬虫数据定位]]></title>
      <link>https://tc.xfei.tech/learn/css-selectors-crawler-data-extraction-basics/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/css-selectors-crawler-data-extraction-basics/</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[系统讲解元素选择器、类选择器、ID 选择器、后代选择器、子元素选择器、兄弟选择器、属性选择器和伪类选择器，并说明它们在爬虫中的用法。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/css-selectors.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/css-selectors.webp" alt="CSS 选择器入门：从改样式到爬虫数据定位" /></p>
<h2 id="-css-">爬虫为什么要学 CSS 选择器？</h2>
<p>CSS 选择器最早是用来给网页元素加样式的。
例如选中标题改颜色，选中按钮加圆角，选中图片设置大小。</p>
<p>但对爬虫来说，选择器还有一个更重要的作用：定位数据。</p>
<p>当你要从网页里提取标题、图片、链接、价格、评分时，本质上都在做同一件事：
从 DOM 树里选中目标元素，再取出里面的文本或属性。</p>
<p>所以选择器不是前端专属知识，而是爬虫入门必须掌握的基础能力。</p>
<h2 id="css-">CSS 选择器有哪三种写法？</h2>
<p>在了解选择器之前，先看 CSS 代码可以写在哪里。</p>
<h3 id="heading">内联样式怎么写？</h3>
<p>内联样式直接写在元素的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">style</code> 属性里，只影响当前元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;h1 style=&quot;color: red;&quot;&gt;爬虫你好&lt;/h1&gt;
</code></pre>
<p>这种写法很直观，但不适合大量复用。</p>
<h3 id="heading-1">内部样式表怎么写？</h3>
<p>内部样式表写在 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">head</code> 里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">style</code> 标签中。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;style&gt;
  h1 {
    text-align: center;
  }
&lt;/style&gt;
</code></pre>
<p>这里的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code> 就是元素选择器，它会选中页面上所有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1</code> 元素。</p>
<h3 id="heading-2">外部样式表怎么引入？</h3>
<p>外部样式表写在独立的 CSS 文件里，再通过 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">link</code> 引入。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;link rel=&quot;stylesheet&quot; href=&quot;/style.css&quot; /&gt;
</code></pre>
<p>大型网站通常会使用外部样式表，方便维护和复用。</p>
<h2 id="css-id-">CSS 基础选择器有哪些？元素、类、ID 怎么用？</h2>
<p>基础选择器先掌握三类：</p>
<ol>
<li>元素选择器</li>
<li>类选择器</li>
<li>ID 选择器</li>
</ol>
<h3 id="heading-3">元素选择器怎么用？</h3>
<p>元素选择器直接写标签名，会选中页面上所有同类型元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">h1 {
  color: red;
}
</code></pre>
<p>如果要同时选中多个标签，可以用逗号分隔：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a,
img,
button {
  display: block;
}
</code></pre>
<p>在爬虫中，元素选择器适合粗略定位，例如先拿到所有链接：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a
</code></pre>
<p>但它通常不够精确，因为页面上可能有大量相同标签。</p>
<h3 id="heading-4">类选择器怎么用？</h3>
<p>类选择器用点号 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.</code> 开头，后面跟类名。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container {
  display: flex;
}
</code></pre>
<p>它会选中所有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">class</code> 包含 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">container</code> 的元素。</p>
<p>HTML 里可以这样写：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;div class=&quot;container&quot;&gt;内容区域&lt;/div&gt;
</code></pre>
<p>类选择器在爬虫里非常常用。
例如很多列表卡片会有稳定的类名：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie-card
</code></pre>
<p>如果你想批量提取列表数据，通常会先选中每个卡片容器。</p>
<h3 id="id-">ID 选择器怎么用？</h3>
<p>ID 选择器用井号 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">#</code> 开头。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">#btn {
  background: black;
  color: white;
}
</code></pre>
<p>HTML 中对应：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;button id=&quot;btn&quot;&gt;提交&lt;/button&gt;
</code></pre>
<p>规范上，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">id</code> 在同一个页面里应该唯一。
所以 ID 选择器通常只选中一个元素。</p>
<p>在爬虫中，如果目标元素有稳定唯一的 ID，定位会非常方便。
但实际网站里，很多 ID 可能由前端框架动态生成，不一定适合长期依赖。</p>
<h2 id="css--1">CSS 选择器怎么组合？后代、子元素、多个类名怎么用？</h2>
<p>真实网页结构通常不止一层，所以只会基础选择器还不够。
接下来要掌握组合选择器。</p>
<h3 id="heading-5">后代选择器怎么用？空格代表什么意思？</h3>
<p>后代选择器用空格表示&quot;在它里面找&quot;。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container img {
  border-radius: 50%;
}
</code></pre>
<p>意思是：选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container</code> 内部所有层级的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code> 元素。
不管图片是直接子元素，还是藏在更深层的容器里，只要在 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container</code> 里面，都算后代。</p>
<p>爬虫场景中，后代选择器很常见：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie-card .title
</code></pre>
<p>表示在每个电影卡片里找标题。</p>
<h3 id="heading-6">子元素选择器怎么用？大于号和空格有什么区别？</h3>
<p>子元素选择器用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&gt;</code> 表示，只选中直接子元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.container &gt; img {
  width: 160px;
}
</code></pre>
<p>如果 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code> 外面还包了一层 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code>，这条选择器就选不中它。</p>
<p>它适合结构比较明确、希望避免误选更深层元素的场景。</p>
<h3 id="heading-7">多个类名同时匹配怎么写？</h3>
<p>一个元素可以有多个类名：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-html" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&lt;img class=&quot;movie featured&quot; src=&quot;/cover.webp&quot; alt=&quot;电影封面&quot; /&gt;
</code></pre>
<p>如果要选中同时拥有这两个类名的元素，可以连着写：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie.featured
</code></pre>
<p>注意中间没有空格。
如果写成 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie .featured</code>，意思就变成了在 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.movie</code> 里面找 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.featured</code> 后代元素。</p>
<p>这个区别非常重要，少一个空格和多一个空格，选择结果完全不同。</p>
<h2 id="css--2">CSS 兄弟选择器怎么用？加号和波浪线有什么区别？</h2>
<p>兄弟选择器用于选择同一层级中，某个元素后面的元素。</p>
<h3 id="heading-8">相邻兄弟选择器怎么用？加号选的是什么？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">+</code> 只选中紧挨在后面的第一个兄弟元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a + img {
  border: 2px solid red;
}
</code></pre>
<p>它表示：选中紧跟在 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 后面的第一个 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code>。</p>
<h3 id="heading-9">通用兄弟选择器怎么用？波浪线选的是什么？</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">~</code> 会选中后面所有符合条件的同级元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a ~ img {
  border: 2px solid red;
}
</code></pre>
<p>它表示：选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 后面所有同级的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">img</code>。</p>
<p>兄弟选择器在爬虫里不如类选择器高频，但遇到&quot;标题后面的价格&quot;&quot;标签后面的值&quot;这类结构时，会很有用。</p>
<h2 id="css--3">CSS 属性选择器怎么用？能匹配链接和图片吗？</h2>
<p>属性选择器用于根据元素属性进行匹配。
它在爬虫里尤其重要，因为链接、图片、接口入口经常藏在属性里。</p>
<h3 id="heading-10">属性选择器有哪些常见写法？</h3>
<p>常见写法如下：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[href]
</code></pre>
<p>选中所有带 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 属性的元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[href=&quot;https://www.xfei.tech&quot;]
</code></pre>
<p>选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 完全等于指定地址的元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[href^=&quot;http&quot;]
</code></pre>
<p>选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 以 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http</code> 开头的元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[href$=&quot;.tech&quot;]
</code></pre>
<p>选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 以 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.tech</code> 结尾的元素。</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">[href*=&quot;xfei&quot;]
</code></pre>
<p>选中 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 中包含 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">xfei</code> 的元素。</p>
<h3 id="heading-11">属性选择器在爬虫里怎么用？</h3>
<p>爬虫中经常会用属性选择器筛选链接，例如：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a[href*=&quot;/detail/&quot;]
</code></pre>
<p>表示选中所有 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code> 中包含 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/detail/</code> 的详情页链接。</p>
<h2 id="css-not--empty-">CSS 伪类选择器怎么用？:not 和 :empty 能帮爬虫做什么？</h2>
<p>伪类选择器用冒号开头，用来描述元素的特殊状态或筛选规则。</p>
<h3 id="not-">:not(...) 怎么用？怎么排除某些元素？</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a:not(.doc) {
  font-size: 18px;
}
</code></pre>
<p>这表示选中所有不是 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.doc</code> 的 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a</code> 元素。</p>
<p>在数据提取中，如果你想排除某些广告链接、文档链接或空链接，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">:not(...)</code> 会很方便。</p>
<h3 id="empty-">:empty 怎么用？能识别空元素吗？</h3>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">a:empty {
  display: none;
}
</code></pre>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">:empty</code> 会选中没有子节点内容的元素。
它可以帮助你识别页面里没有文本的空标签。</p>
<h2 id="javascript--python--css-">JavaScript 和 Python 怎么用 CSS 选择器查找元素？</h2>
<p>选择器不只出现在 CSS 里。
JavaScript 也可以用选择器查找元素：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-js" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">const btn = document.querySelector(&quot;#btn&quot;)
const links = document.querySelectorAll(&quot;a[href]&quot;)
</code></pre>
<p>后面写爬虫时，很多解析库也支持 CSS 选择器。
例如在 Python 的 BeautifulSoup 里：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from bs4 import BeautifulSoup

soup = BeautifulSoup(html, &quot;html.parser&quot;)
titles = soup.select(&quot;.movie-card .title&quot;)
links = soup.select('a[href*=&quot;/detail/&quot;]')
</code></pre>
<p>你会发现，不管是 CSS、JavaScript 还是 Python，选择器的核心思想都是一样的：
从 DOM 树里选中你想操作的元素。</p>
<h2 id="-css--1">爬虫 CSS 选择器有哪些实战技巧？</h2>
<p>写选择器时，建议遵循这几个原则：</p>
<ol>
<li>优先从稳定的列表容器开始选。</li>
<li>不要只依赖过于宽泛的标签名，例如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">div</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">span</code>。</li>
<li>类名语义清晰时，优先使用类选择器。</li>
<li>ID 稳定且唯一时，可以使用 ID 选择器。</li>
<li>提取链接和图片时，多关注 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">href</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">src</code>、<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">alt</code> 等属性。</li>
<li>选择器越短越好，但前提是不会误选。</li>
<li>页面结构复杂时，先在开发者工具里验证选择结果。</li>
</ol>
<p>一个推荐模式是：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">.list-item .title
</code></pre>
<p>而不是：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-css" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">body div div div h2
</code></pre>
<p>后者太依赖页面层级，一旦前端结构稍微调整，就容易失效。</p>
<h2 id="heading-12">总结</h2>
<p>CSS 选择器的核心目的只有一个：选中你想操作的元素。</p>
<p>在前端里，选中元素是为了改样式；
在 JavaScript 里，选中元素是为了做交互；
在爬虫里，选中元素是为了提取数据。</p>
<p>先掌握元素、类、ID、后代、子元素、兄弟、属性和伪类选择器，就足够应对大多数入门级网页解析场景。
后面再学习 XPath、浏览器抓包和动态页面分析时，你会发现这些选择器思维仍然是通用的。</p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[URL 是什么？从客户端与服务器讲透爬虫第一步]]></title>
      <link>https://tc.xfei.tech/learn/url-client-server-crawler-basics/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/url-client-server-crawler-basics/</guid>
      <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[面向爬虫初学者，系统讲清客户端和服务器的通信关系、URL 的 6 个组成部分，以及 URL 编码在实战中的作用。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/client-server-url.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/client-server-url.webp" alt="URL 是什么？从客户端与服务器讲透爬虫第一步" /></p>
<h2 id="heading">先建立一个最重要的认知</h2>
<p>爬虫不是“黑科技”，它本质上只是一个<strong>模拟客户端发请求</strong>的程序。
你在浏览器里点开网页，和你在 Python 里请求网页，底层都在做同一件事：客户端向服务器要资源。</p>
<p>所以在写任何爬虫代码之前，先把这套通信模型吃透，会让后续学习快很多。</p>
<h2 id="heading-1">客户端和服务器，到底是谁在做什么</h2>
<p>在网络通信中，永远有两个角色：</p>
<ol>
<li>客户端（Client）：发起请求的一方。</li>
<li>服务器（Server）：接收请求并返回结果的一方。</li>
</ol>
<p>你常用的浏览器就是客户端，网站背后的应用服务就是服务器。
这也是为什么我们常说 B/S（Browser/Server）架构，本质上仍然是 C/S（Client/Server）架构。</p>
<p><img src="https://static.xfei.tech/learn/images/url-basics/client-server.webp" alt="客户端与服务器通信示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<p>把它放到爬虫语境里就更直观了：</p>
<ol>
<li>你写的爬虫程序 = 客户端。</li>
<li>目标网站 = 服务器。</li>
<li>你请求页面/接口 = 发起 Request。</li>
<li>网站返回 HTML/JSON/图片 = 返回 Response。</li>
</ol>
<p>这就是一轮完整的爬虫工作流。</p>
<h2 id="url-">URL 是什么，为什么它是爬虫定位资源的核心</h2>
<p>URL 全称是 Uniform Resource Locator（统一资源定位符），也就是我们日常说的网址。
它的作用是告诉客户端：<strong>去哪里、用什么方式、拿什么资源</strong>。</p>
<p>一个完整 URL 通常可以拆成 6 部分：</p>
<ol>
<li>协议（Protocol / Schema）</li>
<li>主机（Host）</li>
<li>端口（Port）</li>
<li>路径（Path）</li>
<li>参数（Query / Param）</li>
<li>hash（锚点）</li>
</ol>
<p><img src="https://static.xfei.tech/learn/images/url-basics/url-structure.webp" alt="URL 结构示意图" style="max-width:100%; height:auto; display:block; margin:12px 0; border-radius:8px;"></p>
<h3 id="1">1）协议：先约定通信规则</h3>
<p>协议就是通信格式，比如 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http</code> 或 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https</code>。
你可以把它理解为“双方说话前先统一语法规则”。</p>
<p>在爬虫里最常见的就是 HTTP(S) 协议，后续我们抓接口、带请求头、带 Cookie，都是在这套规则里工作。</p>
<h3 id="2">2）主机：找到目标机器</h3>
<p>主机就是你要访问的那台计算机，对应两种常见形式：</p>
<ol>
<li>IP 地址：网络中的机器编号。</li>
<li>域名：便于记忆的别名（会经 DNS 解析为 IP）。</li>
</ol>
<p>也就是说，你输入域名访问网站时，浏览器会先帮你“翻译”到对应 IP，再去连接服务器。</p>
<h3 id="3">3）端口：找到目标服务</h3>
<p>一台机器上可能同时跑很多服务，所以只知道 IP 还不够。
端口就像同一栋楼里的门牌号，帮你找到具体服务进程。</p>
<p>端口可以省略，客户端会按协议自动补默认值：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http</code> 默认 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">80</code></li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">https</code> 默认 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">443</code></li>
</ol>
<h3 id="4">4）路径：告诉服务器你要哪份资源</h3>
<p>路径用于标识具体资源或服务入口。
例如：</p>
<ol>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/</code> 通常表示首页资源。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/list</code> 可能是列表服务。</li>
<li><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">/api/v1/items</code> 可能是接口资源。</li>
</ol>
<p>同一个站点，不同路径往往对应不同业务能力。</p>
<h3 id="5">5）参数：传递额外条件</h3>
<p>参数用于补充请求条件，常见于搜索、分页、筛选：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-text" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">http://movie.com/list?page=3&amp;size=20
</code></pre>
<p>上面这个 URL 的参数表达了两件事：</p>
<ol>
<li>请求第 3 页</li>
<li>每页 20 条</li>
</ol>
<p>多个参数用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">&amp;</code> 连接，键值使用 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">key=value</code> 形式。</p>
<h3 id="6hash">6）hash：更多是浏览器侧定位</h3>
<p><code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">#</code> 后面的 hash 一般用于前端页面内部锚点跳转，通常不参与服务器资源定位。
在多数爬虫请求里，hash 不是关键点。</p>
<h2 id="url--1">URL 编码：为什么中文会“变成一串百分号”</h2>
<p>URL 中不能直接出现非 ASCII 字符（比如中文）。
当你搜索中文关键词时，浏览器会自动把中文转成编码形式，这就是 URL 编码（Percent-Encoding）。</p>
<p>例如中文关键词会被编码成 <code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">%E4%BA%91%E9%9F%B5</code> 这类字符串。
在 Python 中，你可以这样编码和解码：</p>
<pre style="margin:16px 0; padding:12px 14px; overflow-x:auto; background:#f6f8fa; border:1px solid #d0d7de; border-radius:8px; font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:13px; line-height:1.6; white-space:pre; word-break:normal;"><code class="language-python" style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">from urllib.parse import quote, unquote

keyword = &quot;云韵&quot;
encoded = quote(keyword)
decoded = unquote(encoded)

print(encoded)  # %E4%BA%91%E9%9F%B5
print(decoded)  # 云韵
</code></pre>
<p>实战里很多 HTTP 客户端库会帮你自动编码，但你要知道这个机制，否则排查请求失败时会非常被动。</p>
<h2 id="-url-">给爬虫初学者的一套 URL 分析步骤</h2>
<p>拿到任意一个网址，先按下面 5 步拆解：</p>
<ol>
<li>看协议：HTTP 还是 HTTPS。</li>
<li>看主机：目标站点是谁。</li>
<li>看路径：对应哪个页面或接口服务。</li>
<li>看参数：哪些是必传参数，哪些是可选参数。</li>
<li>做最小化实验：逐步删减参数，验证服务是否还能正常返回。</li>
</ol>
<p>这套步骤能帮你快速识别“请求最小闭环”，是后续接口分析、请求复现、反爬排查的基础能力。</p>
<h2 id="heading-2">总结</h2>
<p>如果把爬虫学习看成盖楼，<code style="font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; font-size:0.95em;">客户端/服务器 + URL</code> 就是地基。
理解了这篇里的核心点，你就能更稳地进入下一步：抓包、请求复现、参数分析和自动化采集。</p>
<p>一句话收尾：<strong>先学会读 URL，再学会发请求，最后再谈爬虫工程化。</strong></p>
]]></content:encoded>
      <category><![CDATA[实战教程]]></category>
    </item>
    <item>
      <title><![CDATA[爬虫合法吗？一文讲清爬虫法律边界与风险]]></title>
      <link>https://tc.xfei.tech/learn/crawler-legality-and-risk-boundary/</link>
      <guid isPermaLink="true">https://tc.xfei.tech/learn/crawler-legality-and-risk-boundary/</guid>
      <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[爬虫是不是违法？本文从法律框架、行为边界、常见高风险场景与合规清单出发，帮你快速判断爬虫项目是否踩线。]]></description>
      <media:content url="https://static.xfei.tech/learn/images/og/crawler-legality-and-risk-boundary.webp" medium="image" />
      <content:encoded><![CDATA[<p><img src="https://static.xfei.tech/learn/images/og/crawler-legality-and-risk-boundary.webp" alt="爬虫合法吗？一文讲清爬虫法律边界与风险" /></p>
<h2 id="heading">爬虫合法吗？先说结论</h2>
<p>爬虫技术本身并不违法，真正有风险的是“不当使用”。
判断一个爬虫行为是否可能违法，核心看三件事：你爬什么、你怎么爬、你爬完拿来做什么。</p>
<p>如果是个人学习、技术研究、教学演示，且只抓公开信息、不破坏对方系统、不传播敏感数据，通常风险较低。
如果是批量抓取后倒卖、泄露个人信息、造成网站故障，风险会急剧上升。</p>
<h2 id="heading-1">为什么“爬虫违法”这个说法会误导初学者</h2>
<p>很多人把“有人因爬虫被处罚”理解成“爬虫本身违法”。这个理解不准确。
和开车一样，车本身不是问题，超速、酒驾、肇事才是问题。</p>
<p>爬虫也是同一逻辑：</p>
<ol>
<li>技术中立：爬虫是自动获取网络信息的手段。</li>
<li>行为可分：合法用途和违法用途都可能存在。</li>
<li>后果导向：是否侵权、是否造成损害，是关键判断点。</li>
</ol>
<h2 id="heading-2">与爬虫密切相关的法律框架</h2>
<p>做爬虫时，常被讨论的法律主要有以下几类：</p>
<ol>
<li>《数据安全法》：关注数据处理活动的安全与责任边界。</li>
<li>《网络安全法》：关注网络运行安全、系统保护与秩序维护。</li>
<li>《个人信息保护法》：关注个人信息收集、处理、传输、共享等合规要求。</li>
<li>《反不正当竞争法》：关注是否通过不正当方式攫取商业利益。</li>
<li>《著作权法》：关注作品内容抓取后的复制、传播、商用风险。</li>
<li>《刑法》：针对严重后果行为可能涉及刑事责任。</li>
</ol>
<p>这些法律并不是“专门用来禁止爬虫”，而是防止数据滥用、系统破坏、隐私泄露与商业侵权。</p>
<h2 id="heading-3">判断爬虫风险的三重标准</h2>
<h3 id="1">1）动机：你为什么要爬</h3>
<ul>
<li>学习、研究、教学、模型验证：通常更容易落在合理范围内。</li>
<li>抓付费内容并转售、搬运牟利：高风险，可能构成侵权或不正当竞争。</li>
</ul>
<h3 id="2">2）行为：你怎么爬</h3>
<ul>
<li>控制频率、抓公开页面、遵守访问规则：风险相对可控。</li>
<li>高频并发冲击、绕过限制、导致对方服务异常：风险显著升高。</li>
</ul>
<h3 id="3">3）结果：你造成了什么影响</h3>
<ul>
<li>数据仅用于个人学习，不传播、不交易：风险较低。</li>
<li>导致隐私外泄、商业损失、系统故障：风险可能升级到民事、行政甚至刑事层面。</li>
</ul>
<h2 id="7-">7 个最常见的高风险雷区</h2>
<p>下面这些场景，是爬虫初学者最容易踩坑的地方：</p>
<ol>
<li>采集和流通敏感个人信息
如身份证号、账单、社保、公积金等，风险极高。</li>
<li>把目标站点“爬崩”
不限流、不降频、高并发扫站，容易被认定为恶意行为。</li>
<li>接黑灰产外包
如批量登录、批量注册、验证码打码、账号批量操作。</li>
<li>做内容搬运型商用
抓视频、小说、图书、资讯后改头换面二次分发变现。</li>
<li>忽视版权边界
“自己看”与“公开传播、售卖”在法律上不是一回事。</li>
<li>误判“非公开数据”
需要登录或付费才能看到的数据，不等于可随意采集和扩散。</li>
<li>公开传播破解/逆向细节
个人研究与公开扩散有本质差别，后者更容易引发法律争议。</li>
</ol>
<h2 id="heading-4">爬虫学习者的合规清单</h2>
<p>在每个项目开始前，先自检这 8 条：</p>
<ol>
<li>目标数据是否包含个人敏感信息？</li>
<li>数据是否属于登录后/付费后才能访问？</li>
<li>本次采集是否有明确、正当、可解释的用途？</li>
<li>是否设置了请求频率上限与超时重试上限？</li>
<li>是否避免对目标服务造成明显性能压力？</li>
<li>是否避免将抓取结果用于侵权传播或商业倒卖？</li>
<li>是否保留了最小化采集原则（只拿必要字段）？</li>
<li>是否准备了“发现异常立即停止”的机制？</li>
</ol>
<h2 id="faq">常见问题（FAQ）</h2>
<h3 id="q1">Q1：爬公开网页，一定合法吗？</h3>
<p>不一定。公开可访问不等于可任意采集、任意商用，仍要看用途、方式和影响。</p>
<h3 id="q2-demo-">Q2：只做学习 demo 会被追责吗？</h3>
<p>一般风险较低，但前提是数据和行为都在合理边界内，不触碰敏感信息与系统安全底线。</p>
<h3 id="q3">Q3：课程学习阶段最重要的原则是什么？</h3>
<p>一句话：用途不明不写，写到一半发现不对劲立刻停。</p>
<h2 id="heading-5">总结</h2>
<p>“爬虫合法吗”这个问题，答案从来不是简单的“是/否”。
真正决定风险的，不是工具，而是使用者的动机、行为与后果。</p>
<p>把边界先立住，再学技术，爬虫就能成为创造价值的能力；
越过边界，技术优势也可能变成法律风险。</p>
<blockquote style="margin:16px 0; padding:0 0 0 12px; border-left:4px solid #d0d7de; color:#57606a;">
<p>说明：本文用于技术学习与合规认知，不构成法律意见。涉及具体业务时，建议咨询专业律师。</p>
</blockquote>
]]></content:encoded>
      <category><![CDATA[实用指南]]></category>
    </item>
  </channel>
</rss>