<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Felix 的余白]]></title><description><![CDATA[记录 Linux、Docker、Web 开发与部署排错，也把过程写成可复用的经验。]]></description><link>https://root.mom</link><image><url>https://root.mom/favicon.ico</url><title>Felix 的余白</title><link>https://root.mom</link></image><generator>Yohaku (https://github.com/Innei/Yohaku)</generator><lastBuildDate>Fri, 28 Aug 2026 19:55:26 GMT</lastBuildDate><atom:link href="https://root.mom/feed" rel="self" type="application/rss+xml"/><pubDate>Fri, 28 Aug 2026 19:55:26 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[记一次折腾的爬虫经历：被"假 DRM"忽悠的视频下载与 Cookie 污染避坑指南]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://root.mom/posts/experience/video-download-cookie-pollution-fake-drm-guide">https://root.mom/posts/experience/video-download-cookie-pollution-fake-drm-guide</a></blockquote><div><p>在近期的一个自动化项目中，我需要从某在线教育平台（基于保利威/BokeCC 视频云）批量下载 1000 多个课程视频。原本以为只是常规的 M3U8 解析与下载，却没想到陷入了一个耗时极长的“私有 DRM 加密”陷阱。</p><p>经过反复抓包、测试和重构，最终发现罪魁祸首竟然是一个极其基础的细节——<strong>Cookie 跨域污染</strong>。在此复盘整个踩坑与破局的过程，希望能帮到有类似需求的开发者。</p><hr/><h2 id="">踩坑实录：那些看似无解的报错</h2><p>在项目初期，我已经成功拿到了视频的 M3U8 播放列表，并且解析出了带有 <code>AES-128</code> 加密的 <code>.ts</code> 切片地址和 Key。然而在下载合成时，遭遇了以下诡异现象：</p><ol start="1"><li><strong>播放器报错 <code>0xC00D36C4</code></strong>：下载下来的 MP4 文件体积看起来正常，但双击打开直接提示文件损坏，毫无画面。</li><li><strong>FFmpeg 报 <code>Invalid data found when processing input</code></strong>：尝试用原生的 FFmpeg 命令行去合并和解密流媒体时，底层直接拒绝处理。</li></ol><p><strong>错误的排查方向：</strong>
这些现象在技术特征上，像极了流媒体的高级私有加密（如魔改了字节序、或者使用了类似 Widevine 的 DRM）。我顺着这个思路，耗费了大量时间去研究 CDN 的防御机制、尝试逆向播放器的 JS 解密算法，结果越陷越深。</p><hr/><h2 id="">真相大白：其实根本没有黑科技</h2><p>经过彻底的回溯，真相让人哭笑不得：<strong>根本没有什么魔改的私有 DRM，标准的 AES-128 加密完全可以直接解，导致视频损坏的唯一原因是被“毒 Cookie”污染了。</strong></p><h3 id="1--cookie-">1. 致命的 Cookie 跨域污染</h3><p>在早期的请求代码中，我把带有主站登录态的庞大 <code>Cookie</code>，连同请求头一起发送给了 BokeCC 的 CDN 视频切片服务器。
CDN 服务器有着严格的域名和凭证校验机制，当它收到非本域名的“非法 Cookie”时，直接触发了防御机制，下发了<strong>被污染、格式错乱的垃圾数据</strong>。下载工具把这些垃圾数据合并成了 MP4，自然连文件头都是错的。</p><h3 id="2-user-agent">2. 身份标识（User-Agent）精分</h3><p>为了绕过复杂的 PC 端风控，我在获取接口链接时伪装成了移动端（iPhone UA），但在发起下载请求时，却忘了统一 UA，默认使用了 PC 端的标识。服务器识别到同一会话的设备跳跃，导致部分切片直接返回 <code>403 Forbidden</code>。</p><hr/><h2 id="">破局方案：大道至简的最终架构</h2><p>最终的解决方案并不复杂，核心在于<strong>“信道隔离”与“工具代工”</strong>。</p><h3 id="api-cdn-">方案一：API 走鉴权，CDN 走纯净通道</h3><p>在调用 <code>yt-dlp</code> 进行实际的视频流拉取时，<strong>坚决不传递主站的 Cookie</strong>，只保留 <code>Referer</code>。CDN 发现没有干扰项，就会老老实实下发标准的加密切片。</p><pre class="language-python lang-python"><code class="language-python lang-python">cmd = [
    &#x27;yt-dlp&#x27;,
    &#x27;--add-header&#x27;, &#x27;Referer: &#x27;,
    &#x27;--add-header&#x27;, &#x27;User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0...)&#x27;, 
    # 🚫 绝不添加 Cookie 参数，防止 CDN 报错拦截
    &#x27;--concurrent-fragments&#x27;, &#x27;4&#x27;, # 开启多线程切片下载
    &#x27;-o&#x27;, output_filepath, url
]
</code></pre>
<h3 id="-ua">方案二：全局统一移动端 UA</h3><p>将移动端 UA 定义为全局常量，无论是向主站 API 申请播放凭证，还是后续拉取 M3U8 列表和 TS 切片，全程保持身份一致，彻底消除风控疑虑。</p><h3 id="">方案三：把脏活累活交给专业工具</h3><p>放弃手写复杂的 FFmpeg 管道或 Python 协程下载，直接调用 <code>yt-dlp</code>。它能够自动处理 M3U8 的解析、并发下载、AES-128 密钥获取、解密以及最终的 MP4 容器封装，极大提升了代码的稳定性和可维护性。</p><hr/><h2 id="">附赠技巧：如何防止浏览器自动化录制黑屏？</h2><p>如果你在做爬虫时，使用的是 Playwright/Selenium 等浏览器自动化工具，可能会遇到网页因为“切后台”或“窗口最小化”导致视频自动暂停或黑屏（触发了 <code>Page Visibility API</code>）。</p><p>解决这个问题的绝招不是用容易被风控识别的 <code>headless=True</code>，而是<strong>开启真实窗口，并把它“扔”到屏幕外</strong>：</p><pre class="language-python lang-python"><code class="language-python lang-python">browser = p.chromium.launch(
    headless=False, # 保持有头模式防检测
    args=[
        &quot;--disable-backgrounding-occluded-windows&quot;, # 🚫 禁止遮挡窗口黑屏
        &quot;--window-position=-2000,-2000&quot;,            # 🪄 物理障眼法：扔到屏幕外的超远左上角
    ]
)
</code></pre>
<hr/><h2 id="">经验总结</h2><ol start="1"><li><strong>不要总想着“总有刁民想害朕”</strong>：遇到乱码或无法解析的数据，先检查自己的请求头是否干净、协议是否标准，不要上来就怀疑是遇到世界级 DRM 难题。</li><li><strong>隔离变量思维</strong>：API 接口的鉴权逻辑和 CDN 资源分发节点的校验逻辑往往是两套系统，请求参数（尤其是 Cookie）千万不要一股脑全带上。</li><li><strong>不要重复造轮子</strong>：能用 <code>yt-dlp</code> 处理的标准流媒体，就不要自己去写 ts 切片下载和 AES 解密脚本。</li></ol></div><p style="text-align:right"><a href="https://root.mom/posts/experience/video-download-cookie-pollution-fake-drm-guide#comments">览毕，何不一言？</a></p></div>]]></description><link>https://root.mom/posts/experience/video-download-cookie-pollution-fake-drm-guide</link><guid isPermaLink="true">https://root.mom/posts/experience/video-download-cookie-pollution-fake-drm-guide</guid><dc:creator><![CDATA[felix]]></dc:creator><pubDate>Wed, 05 Aug 2026 18:28:47 GMT</pubDate></item><item><title><![CDATA[深入理解 Python 内存机制：类、对象、变量与 del 的底层真相]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://root.mom/posts/python/understanding-python-memory-mechanism-classes-objects-variables-del">https://root.mom/posts/python/understanding-python-memory-mechanism-classes-objects-variables-del</a></blockquote><div><blockquote><p>本文基于 CPython 实现讨论引用计数和对象释放。其他 Python 解释器的具体实现可能不同。</p></blockquote>
<p>学习 Python 或其他面向对象语言时，我们经常听到这样的说法：</p><blockquote><p>“创建一个 <code>cat</code> 对象，然后用 <code>del</code> 销毁它。”</p></blockquote>
<p>这种表达便于入门，却隐藏了许多重要细节。</p><p>严格来说，变量并不等于对象，<code>del</code> 也不一定是在“销毁对象”。如果不能区分名称、引用和对象，后续理解浅拷贝、深拷贝、可变对象、循环引用和垃圾回收时就很容易产生混乱。</p><p>本文从下面这行代码出发：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
</code></pre>
<p>从 Python 语言语义和 CPython 实现两个层面，分析类、对象、变量以及垃圾回收之间的关系。</p><hr/><h2 id="">一、类、对象与变量分别是什么？</h2><h3 id="1-">1. 类：创建对象的规则，同时它自己也是对象</h3><p>类描述了一类对象所具有的属性和行为。</p><pre class="language-python lang-python"><code class="language-python lang-python">class Cat:
    def __init__(self, color):
        self.color = color

    def meow(self):
        print(&quot;喵&quot;)
</code></pre>
<p>这里的 <code>Cat</code> 定义了：</p><ul><li>实例如何初始化；</li><li>实例可以保存哪些属性；</li><li>实例可以执行哪些方法。</li></ul><p>可以把类类比成建筑图纸，但这个比喻并不完全严谨。</p><p>在 Python 中，类本身也是对象：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(type(Cat))
</code></pre>
<p>输出结果为：</p><pre class="language-text lang-text"><code class="language-text lang-text">&lt;class &#x27;type&#x27;&gt;
</code></pre>
<p>因此，类并不是一份完全不占内存的抽象说明。创建类时，Python 同样需要保存类名、方法、属性和继承关系等信息。</p><h3 id="2-">2. 对象：运行期间存在的数据实体</h3><p>执行下面的代码：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
</code></pre>
<p>会创建一个 <code>Cat</code> 类的实例。这个实例保存着属于自己的状态：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(cat.color)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">白色
</code></pre>
<p>如果再创建一个实例：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat_2 = Cat(&quot;黑色&quot;)
</code></pre>
<p>那么 <code>cat</code> 和 <code>cat_2</code> 指向两个不同的对象：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(cat is cat_2)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">False
</code></pre>
<p>它们可以共享类中定义的方法，但各自拥有独立的实例属性。</p><h3 id="3-">3. 变量：绑定到对象的名称</h3><p>在 Python 中，更准确的说法不是“变量里面装着对象”，而是：</p><blockquote><p>名称绑定到了对象。</p></blockquote>
<p>执行：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
</code></pre>
<p>可以理解为完成了两个主要步骤：</p><ol start="1"><li><code>Cat(&quot;白色&quot;)</code> 创建一个新的实例对象；</li><li>名称 <code>cat</code> 与这个对象建立绑定关系。</li></ol><p>可以用下面的示意图表示：</p><pre class="language-text lang-text"><code class="language-text lang-text">名称空间                         Python 对象

cat  ───────────────────────▶  Cat 实例
                               color = &quot;白色&quot;
</code></pre>
<p>我们通过名称 <code>cat</code> 找到对象，然后访问对象的属性和方法：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat.color
cat.meow()
</code></pre>
<p>需要注意的是，将 Python 变量简单描述成“栈中保存的物理内存地址”并不严谨。</p><p>“栈中引用、堆中对象”可以作为理解高级语言内存模型的入门类比，但 Python 语言规范并没有要求名称必须存放在栈中。局部变量、全局变量、闭包变量和对象属性的保存方式并不完全相同。</p><p>因此，更通用、准确的表达是：</p><blockquote><p>Python 名称存在于某个名称空间中，并绑定到相应的对象。</p></blockquote>
<hr/><h2 id="cat--cat-">二、<code>cat = Cat(&quot;白色&quot;)</code> 到底发生了什么？</h2><p>先看完整代码：</p><pre class="language-python lang-python"><code class="language-python lang-python">class Cat:
    def __new__(cls, color):
        print(&quot;1. 分配并创建实例&quot;)
        return super().__new__(cls)

    def __init__(self, color):
        print(&quot;2. 初始化实例&quot;)
        self.color = color


cat = Cat(&quot;白色&quot;)
</code></pre>
<p>运行结果：</p><pre class="language-text lang-text"><code class="language-text lang-text">1. 分配并创建实例
2. 初始化实例
</code></pre>
<p>从概念上看，这个过程可以分成以下几步。</p><h3 id="-cat">第一步：查找名称 <code>Cat</code></h3><p>解释器首先在当前作用域中查找 <code>Cat</code>，找到对应的类对象。</p><h3 id="">第二步：调用类对象</h3><p>执行 <code>Cat(&quot;白色&quot;)</code> 本质上是在调用 <code>Cat</code> 类。在常见情况下，这会依次涉及：</p><ol start="1"><li><code>__new__()</code> 创建并返回实例；</li><li><code>__init__()</code> 初始化该实例。</li></ol><p>需要特别注意：</p><ul><li><code>__new__()</code> 负责创建实例；</li><li><code>__init__()</code> 负责初始化实例；</li><li><code>__init__()</code> 并不真正负责分配对象。</li></ul><h3 id="">第三步：建立名称绑定</h3><p>实例创建完成后，赋值语句把名称 <code>cat</code> 绑定到这个实例。此时可以通过 <code>cat</code> 操作该对象。</p><hr/><h2 id="">三、多个变量可以指向同一个对象</h2><p>执行下面的代码：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
another_cat = cat
</code></pre>
<p>这不会创建第二只猫，而是让两个名称绑定到同一个对象：</p><pre class="language-text lang-text"><code class="language-text lang-text">cat ───────────────┐
                   ├────▶ Cat 实例
another_cat ───────┘      color = &quot;白色&quot;
</code></pre>
<p>可以通过 <code>is</code> 验证对象身份：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(cat is another_cat)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">True
</code></pre>
<p>因此，通过其中一个名称修改对象，另一个名称也能观察到变化：</p><pre class="language-python lang-python"><code class="language-python lang-python">another_cat.color = &quot;黑色&quot;
print(cat.color)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">黑色
</code></pre>
<p>原因并不是两个变量的数据被自动同步，而是它们从始至终指向同一个对象。这也是理解浅拷贝、可变对象和函数传参的基础。</p><hr/><h2 id="del-cat-">四、<code>del cat</code> 删除的究竟是什么？</h2><p>假设存在下面的代码：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
another_cat = cat

del cat
</code></pre>
<p>执行 <code>del cat</code> 后，被删除的是名称 <code>cat</code> 与对象之间的绑定。此时对象仍然可以通过 <code>another_cat</code> 访问：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(another_cat.color)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">白色
</code></pre>
<p>这说明：</p><blockquote><p><code>del</code> 删除名称绑定，并不等价于直接销毁对象。</p></blockquote>
<p>只有当对象不再拥有任何强引用时，它才可能进入释放流程。</p><hr/><h2 id="cpython-">五、CPython 的引用计数</h2><p>CPython 是目前最常用的 Python 实现。它主要通过引用计数管理对象生命周期。</p><p>每个对象都会记录当前有多少个强引用指向自己。可以使用 <code>sys.getrefcount()</code> 进行观察：</p><pre class="language-python lang-python"><code class="language-python lang-python">import sys

cat = Cat(&quot;白色&quot;)
print(sys.getrefcount(cat))
</code></pre>
<p>实际输出通常会比预期多 <code>1</code>，因为把 <code>cat</code> 传给 <code>getrefcount()</code> 时，函数调用本身会临时增加一次引用。</p><p>继续执行：</p><pre class="language-python lang-python"><code class="language-python lang-python">another_cat = cat
print(sys.getrefcount(cat))
</code></pre>
<p>引用计数会相应增加。删除一个名称后，引用计数又会下降：</p><pre class="language-python lang-python"><code class="language-python lang-python">del another_cat
print(sys.getrefcount(cat))
</code></pre>
<p>当对象的引用计数下降到 <code>0</code> 时，CPython 通常会立即进入对象释放流程。</p><p>因此，把这一过程描述为“垃圾回收器定期巡逻后才发现对象”并不准确。对于普通的非循环引用对象，CPython 的引用计数通常可以立即处理它们。</p><hr/><h2 id="">六、为什么只有引用计数还不够？</h2><p>引用计数无法独立解决循环引用问题。</p><pre class="language-python lang-python"><code class="language-python lang-python">class Node:
    def __init__(self):
        self.next = None


node_a = Node()
node_b = Node()

node_a.next = node_b
node_b.next = node_a
</code></pre>
<p>此时对象之间形成了循环：</p><pre class="language-text lang-text"><code class="language-text lang-text">node_a 对象 ─────▶ node_b 对象
     ▲                 │
     └─────────────────┘
</code></pre>
<p>即使删除外部名称：</p><pre class="language-python lang-python"><code class="language-python lang-python">del node_a
del node_b
</code></pre>
<p>两个对象内部仍然互相引用。为了解决这种问题，CPython 还提供了循环垃圾回收机制，用来寻找已经无法从程序访问、却因为互相引用而没有归零的对象。</p><p>因此，CPython 的内存管理可以概括为：</p><pre class="language-text lang-text"><code class="language-text lang-text">引用计数：处理大多数普通对象
循环垃圾回收：补充处理循环引用
</code></pre>
<p>二者并不是互相替代的关系。</p><hr/><h2 id="del--del">七、<code>__del__()</code> 并不等于 <code>del</code></h2><p>这两个名称非常相似，但作用完全不同。</p><h3 id="del"><code>del</code></h3><p><code>del</code> 是 Python 语句，用来删除名称、容器元素或对象属性：</p><pre class="language-python lang-python"><code class="language-python lang-python">del cat
del numbers[0]
del user.name
</code></pre>
<h3 id="del"><code>__del__()</code></h3><p><code>__del__()</code> 是对象的终结器。当对象即将被回收时，解释器可能调用它：</p><pre class="language-python lang-python"><code class="language-python lang-python">class Cat:
    def __init__(self, name):
        self.name = name

    def __del__(self):
        print(f&quot;{self.name} 对象即将被回收&quot;)


cat = Cat(&quot;小白&quot;)
del cat
</code></pre>
<p>在 CPython 的简单场景中，可能立即输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">小白 对象即将被回收
</code></pre>
<p>但不应该把 <code>__del__()</code> 当作可靠的资源管理工具。原因包括：</p><ul><li>对象具体何时被回收可能与解释器实现有关；</li><li>循环引用会让对象生命周期更加复杂；</li><li>解释器退出时，全局对象可能已经被部分清理；</li><li><code>__del__()</code> 中出现异常时不容易正确处理；</li><li>对象甚至可能在 <code>__del__()</code> 中重新建立引用。</li></ul><p>因此，不应依赖 <code>__del__()</code> 关闭文件、数据库连接或网络连接。</p><hr/><h2 id="">八、资源释放应该使用上下文管理器</h2><p>管理文件等外部资源时，推荐使用 <code>with</code>：</p><pre class="language-python lang-python"><code class="language-python lang-python">with open(&quot;example.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as file:
    content = file.read()
</code></pre>
<p>离开 <code>with</code> 代码块后，即使发生异常，文件也会被正确关闭。</p><p>对于自定义资源，可以实现上下文管理协议：</p><pre class="language-python lang-python"><code class="language-python lang-python">class Connection:
    def __enter__(self):
        print(&quot;建立连接&quot;)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print(&quot;关闭连接&quot;)


with Connection() as connection:
    print(&quot;使用连接&quot;)
</code></pre>
<p>输出：</p><pre class="language-text lang-text"><code class="language-text lang-text">建立连接
使用连接
关闭连接
</code></pre>
<p>这比依赖 <code>__del__()</code> 更明确、更安全，也更容易维护。</p><hr/><h2 id="">九、对象身份与内存地址</h2><p>Python 提供了 <code>id()</code> 函数，用来返回对象在当前生命周期中的唯一标识：</p><pre class="language-python lang-python"><code class="language-python lang-python">cat = Cat(&quot;白色&quot;)
print(id(cat))
</code></pre>
<p>在 CPython 中，<code>id()</code> 通常与对象当前的内存地址有关，但 Python 语言规范只保证：</p><blockquote><p>一个对象在其生命周期内拥有唯一且不变的 <code>id</code>。</p></blockquote>
<p>对象被释放后，这个标识可能被新对象重复使用。因此，不应该把 <code>id()</code> 当作永久编号。</p><p>判断两个名称是否指向同一对象时，应使用 <code>is</code>；比较两个对象的值是否相等时，通常使用 <code>==</code>：</p><pre class="language-python lang-python"><code class="language-python lang-python">print(object_a is object_b)
print(object_a == object_b)
</code></pre>
<ul><li><code>is</code>：是否为同一个对象；</li><li><code>==</code>：对象的值是否相等。</li></ul><hr/><h2 id="">十、完整实验：观察对象生命周期</h2><p>可以通过下面的代码观察名称绑定和对象回收：</p><pre class="language-python lang-python"><code class="language-python lang-python">import sys


class Cat:
    def __init__(self, name):
        self.name = name
        print(f&quot;创建对象：{self.name}&quot;)

    def __del__(self):
        print(f&quot;回收对象：{self.name}&quot;)


cat = Cat(&quot;小白&quot;)
print(&quot;第一次引用计数：&quot;, sys.getrefcount(cat))

another_cat = cat
print(&quot;第二次引用计数：&quot;, sys.getrefcount(cat))

del cat
print(&quot;删除 cat 后，对象仍然存在&quot;)
print(&quot;another_cat.name =&quot;, another_cat.name)

del another_cat
print(&quot;最后一个名称绑定已删除&quot;)
</code></pre>
<p>可能得到类似结果：</p><pre class="language-text lang-text"><code class="language-text lang-text">创建对象：小白
第一次引用计数：2
第二次引用计数：3
删除 cat 后，对象仍然存在
another_cat.name = 小白
回收对象：小白
最后一个名称绑定已删除
</code></pre>
<p>这个实验说明：</p><ol start="1"><li>创建实例后，名称 <code>cat</code> 绑定到对象；</li><li><code>another_cat = cat</code> 不会复制对象；</li><li><code>del cat</code> 只删除一个名称绑定；</li><li>只要还存在其他强引用，对象就不会被释放；</li><li>最后一个强引用消失后，CPython 通常立即释放对象。</li></ol><hr/><h2 id="">十一、总结</h2><p>理解 Python 对象模型时，可以记住下面几句话：</p><h3 id="1-">1. 类也是对象</h3><p>类不仅用于描述实例，它本身也由 Python 的对象系统管理。</p><h3 id="2-">2. 变量是名称绑定</h3><p>Python 变量不是存放完整对象的盒子，而是名称空间中绑定到对象的名称。</p><h3 id="3-">3. 赋值通常不会复制对象</h3><pre class="language-python lang-python"><code class="language-python lang-python">b = a
</code></pre>
<p>通常意味着让 <code>b</code> 和 <code>a</code> 指向同一个对象，而不是自动复制一份数据。</p><h3 id="4-del-">4. <code>del</code> 删除的是绑定关系</h3><pre class="language-python lang-python"><code class="language-python lang-python">del a
</code></pre>
<p>不代表一定立即销毁对象。只有在不存在其他有效引用时，对象才可能被释放。</p><h3 id="5-cpython-">5. CPython 主要使用引用计数</h3><p>普通对象通常在引用计数归零时立即释放；循环垃圾回收用于补充处理循环引用。</p><h3 id="6--del-">6. 不要依赖 <code>__del__()</code> 管理重要资源</h3><p>文件、锁、数据库连接和网络连接等资源，应使用上下文管理器或显式清理逻辑。</p><hr/><h2 id="">结语</h2><p>面向对象语言帮助我们隐藏了大量内存管理细节，但这些细节并没有消失。</p><p>当我们从“创建变量”和“删除对象”这样的表层说法继续向下探索，就会发现真正发生的是：</p><pre class="language-text lang-text"><code class="language-text lang-text">创建对象
    ↓
建立名称绑定
    ↓
增加或减少引用
    ↓
引用计数归零
    ↓
释放对象占用的资源
</code></pre>
<p>掌握对象、名称、引用和生命周期之间的关系，不仅能帮助我们理解 Python，也能为学习数据结构、操作系统、编译原理和垃圾回收算法建立更扎实的基础。</p><p>真正理解一门语言，不只是记住它的语法，更要理解语法背后的运行机制。</p></div><p style="text-align:right"><a href="https://root.mom/posts/python/understanding-python-memory-mechanism-classes-objects-variables-del#comments">览毕，何不一言？</a></p></div>]]></description><link>https://root.mom/posts/python/understanding-python-memory-mechanism-classes-objects-variables-del</link><guid isPermaLink="true">https://root.mom/posts/python/understanding-python-memory-mechanism-classes-objects-variables-del</guid><dc:creator><![CDATA[felix]]></dc:creator><pubDate>Sun, 26 Jul 2026 17:35:34 GMT</pubDate></item><item><title><![CDATA[《Git Push 一直失败？记一次 Docker + Gitea 部署的底层网络排错实战》]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://root.mom/posts/experience/git">https://root.mom/posts/experience/git</a></blockquote><div><h2 id="1-background">1. 踩坑背景（Background）</h2><p>最近在物理服务器上使用 1Panel 面板，通过 Docker 部署了 Gitea 服务。</p><p>网页端通过域名的 80/443 端口访问一切正常，但在本地终端使用 Git 推送代码时，却遭遇了连环报错。</p><p>起初，报错表现得像是 Git 密钥或权限问题。但顺着网络传输层（Transport Layer）一层层排查后，才发现这是一个非常典型的网络隔离与端口映射问题。</p><h2 id="2-tldr">2. 核心结论（TL;DR）</h2><blockquote><p>这不是 Git 认证问题，而是纯粹的 TCP 连通性问题。</p></blockquote>
<p>在使用 Docker 部署非 HTTP 协议的服务时，例如：</p><ul><li>SSH 的 <code>22</code> 端口</li><li>MySQL 的 <code>3306</code> 端口</li><li>Gitea 自定义的 SSH 端口</li></ul><p>如果容器的宿主机端口只绑定在 <code>127.0.0.1</code>，公网直连请求就无法访问该端口。</p><p>需要将监听地址改为 <code>0.0.0.0</code>，或者在 Docker 端口映射中直接省略绑定 IP，才能允许外部网络访问。</p><h2 id="3-troubleshooting">3. 剥丝抽茧的排错过程（Troubleshooting）</h2><h3 id="31-">3.1 第一层伪装：权限的错觉</h3><ul><li><strong>现象：</strong> 使用默认命令推送时，终端一直卡住或提示输入密码。</li><li><strong>误区：</strong> 以为是本地 SSH 公私钥没有配置正确。</li><li><strong>破局：</strong> 指定自定义的 <code>222</code> 端口后，真正的底层网络错误浮出水面。</li></ul><h3 id="32-connection-timed-out">3.2 第二层阻碍：Connection timed out</h3><p>明确指定端口后，Git 出现以下报错：</p><pre class="language-text lang-text"><code class="language-text lang-text">ssh: connect to host example.com port 222: Connection timed out
</code></pre>
<p><code>Connection timed out</code> 表示数据包没有获得任何响应，通常意味着数据包在途中被拦截。</p><p>常见原因包括：</p><ul><li>云服务器安全组未放行端口</li><li>UFW、iptables 等服务器防火墙拦截</li><li>CDN 不支持该 TCP 端口</li><li>域名经过 Cloudflare 等代理平台</li><li>运营商或网络出口限制</li></ul><p>将域名替换为服务器真实公网 IP 后，成功绕过了超时问题。</p><p>这也证明：如果域名启用了 Cloudflare 等 CDN，其默认代理通常只支持部分 Web 端口，不会自动代理 SSH 等普通 TCP 服务。</p><h3 id="33-connection-refused">3.3 第三层铁壁：Connection refused</h3><p>绕过 CDN 后，错误从超时变成了：</p><pre class="language-text lang-text"><code class="language-text lang-text">ssh: connect to host 152.32.191.114 port 222: Connection refused
</code></pre>
<p><code>Connection refused</code> 表示数据包已经到达服务器，但目标端口没有正确对外监听。</p><p>进入服务器检查 Docker 端口映射后，发现 Gitea 的 SSH 端口被绑定成：</p><pre class="language-text lang-text"><code class="language-text lang-text">127.0.0.1:222-&gt;22/tcp
</code></pre>
<p>这意味着：</p><ul><li>宿主机内部可以访问 <code>222</code> 端口</li><li>公网无法访问 <code>222</code> 端口</li><li>外部请求到达服务器后会被立即拒绝</li></ul><p><code>127.0.0.1</code> 是本地回环地址，只接受服务器内部请求。</p><h3 id="34-">3.4 最终破局：解除本地绑定</h3><p>修改 Docker Compose 配置，将：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">ports:
  - &quot;127.0.0.1:222:22&quot;
</code></pre>
<p>改为：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">ports:
  - &quot;222:22&quot;
</code></pre>
<p>也可以明确写成：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">ports:
  - &quot;0.0.0.0:222:22&quot;
</code></pre>
<p>如果使用 1Panel，也可以在容器端口设置中开启“端口外部访问”。</p><p>重建容器后重新测试，Git SSH 连接恢复正常，代码可以正常推送。</p><h2 id="4-key-takeaways">4. 核心知识点（Key Takeaways）</h2><h3 id="41--tcp-">4.1 读懂 TCP 的报错语言</h3><h4 id="connection-timed-out">Connection timed out</h4><p>表示数据包在途中没有获得响应。</p><p>优先检查：</p><ul><li>云服务器安全组</li><li>UFW 或 iptables</li><li>CDN 代理状态</li><li>域名 DNS 解析</li><li>公网 IP 是否正确</li><li>运营商网络限制</li></ul><h4 id="connection-refused">Connection refused</h4><p>表示数据包已经到达目标服务器，但目标端口没有服务监听，或者服务没有监听公网网卡。</p><p>优先检查：</p><ul><li>服务进程是否启动</li><li>Docker 容器是否运行</li><li>端口映射是否正确</li><li>服务监听地址是否为 <code>0.0.0.0</code></li><li>端口是否只绑定在 <code>127.0.0.1</code></li></ul><h2 id="5-127001--0000-">5. 127.0.0.1 与 0.0.0.0 的区别</h2><h3 id="51-127001">5.1 127.0.0.1：仅限本机访问</h3><p><code>127.0.0.1</code> 是本地回环地址。</p><p>绑定该地址意味着：</p><ul><li>只允许宿主机内部访问</li><li>不接受公网请求</li><li>适合数据库、内部 API 等不应暴露到公网的服务</li></ul><p>可以将它理解为“闭门运行”。</p><h3 id="52-0000">5.2 0.0.0.0：监听所有网卡</h3><p><code>0.0.0.0</code> 表示监听服务器上的所有网络接口。</p><p>绑定该地址意味着：</p><ul><li>可以接受本机请求</li><li>可以接受局域网请求</li><li>在防火墙放行后，可以接受公网请求</li></ul><p>可以将它理解为“开门监听”。</p><blockquote><p>将端口绑定到 <code>0.0.0.0</code> 会扩大服务的网络暴露范围，因此必须同时配置安全组、防火墙和身份认证。</p></blockquote>
<h2 id="6-">6. 反向代理与客户端直连的区别</h2><h3 id="61-">6.1 为什么网页端可以正常访问？</h3><p>因为 1Panel 通常会通过 OpenResty 或 Nginx 监听公网的 <code>80</code> 和 <code>443</code> 端口。</p><p>请求流程如下：</p><pre class="language-text lang-text"><code class="language-text lang-text">浏览器
  ↓ HTTPS 443
OpenResty / Nginx
  ↓ 内部转发
Gitea Web 容器
</code></pre>
<p>即使 Gitea Web 端口只绑定在 <code>127.0.0.1</code>，Nginx 仍然可以从服务器内部访问它。</p><h3 id="62--git-ssh-">6.2 为什么 Git SSH 必须单独开放端口？</h3><p>Git SSH 使用的是 SSH 协议，不会经过普通的 HTTP 反向代理。</p><p>请求流程如下：</p><pre class="language-text lang-text"><code class="language-text lang-text">本地 Git 客户端
  ↓ SSH TCP 222
服务器公网端口
  ↓ Docker 端口映射
Gitea 容器 22 端口
</code></pre>
<p>本地终端必须直接访问服务器的 TCP 端口。</p><p>如果该端口只绑定在 <code>127.0.0.1</code>，公网客户端就无法建立连接。</p><h2 id="7-">7. 排查流程总结</h2><p>遇到 Docker 服务无法从公网访问时，可以按照以下顺序排查：</p><ol start="1"><li>检查域名是否解析到正确的公网 IP。</li><li>绕过 CDN，直接使用公网 IP 测试。</li><li>检查云服务器安全组是否放行端口。</li><li>检查 UFW、iptables 等本机防火墙。</li><li>检查 Docker 容器是否正常运行。</li><li>检查 Docker 端口映射。</li><li>检查端口绑定的是 <code>127.0.0.1</code> 还是 <code>0.0.0.0</code>。</li><li>检查容器内部服务是否真正监听目标端口。</li><li>最后再排查 SSH 密钥、用户权限和仓库权限。</li></ol><h2 id="8-summary">8. 总结（Summary）</h2><p>排查网络问题时，不能只盯着应用层配置。</p><p>错误表面上可能表现为：</p><ul><li>Git 权限异常</li><li>SSH 密钥无效</li><li>密码认证失败</li><li>仓库没有写入权限</li></ul><p>但真正的问题可能发生在更底层的 TCP 传输层。</p><p>只有结合以下信息进行判断：</p><ul><li>报错类型</li><li>DNS 与 CDN</li><li>云服务器安全组</li><li>系统防火墙</li><li>Docker 端口映射</li><li>服务监听地址</li></ul><p>才能准确定位数据包究竟中断在哪一层。</p><p>理解 <code>Connection timed out</code> 与 <code>Connection refused</code> 的区别，以及 <code>127.0.0.1</code> 与 <code>0.0.0.0</code> 的作用，是排查 Docker 网络问题最基础也最重要的一步。</p></div><p style="text-align:right"><a href="https://root.mom/posts/experience/git#comments">览毕，何不一言？</a></p></div>]]></description><link>https://root.mom/posts/experience/git</link><guid isPermaLink="true">https://root.mom/posts/experience/git</guid><dc:creator><![CDATA[felix]]></dc:creator><pubDate>Sun, 26 Jul 2026 12:40:14 GMT</pubDate></item></channel></rss>