

<feed xmlns="http://www.w3.org/2005/Atom">
  <follow_challenge>
    <feedId>66860831563739339</feedId>
    <userId>57418972250665984</userId>
  </follow_challenge>
  <id>https://aturret.space/</id>
  <title>交界处的空间站</title>
  <subtitle>干正事博客</subtitle>
  <updated>2026-03-21T20:39:06-05:00</updated>
  <author>
    <name>ATurret</name>
    <uri>https://aturret.space/</uri>
  </author>
  <link rel="self" type="application/atom+xml"
    href="https://aturret.space/zh-CN/feed.xml" />
  <link rel="alternate" type="text/html"
    hreflang="en"
    href="https://aturret.space/zh-CN/" />
  <generator uri="https://jekyllrb.com/"
    version="4.4.1">Jekyll</generator>
  <rights> © 2026 ATurret </rights>
  <icon>/assets/img/favicons/favicon.ico</icon>
  <logo>/assets/img/favicons/favicon-96x96.png</logo> 
   <entry>
    <title>自建 Firecrawl 进行通用型网页抓取</title>
    <link href="https://aturret.space/zh-CN/posts/Self-host-firecrawl/" rel="alternate"
      type="text/html" title="自建 Firecrawl 进行通用型网页抓取" />
    <published>2026-02-09T00:00:00-06:00</published>  <updated>2026-03-08T21:45:26-05:00</updated>  <id>https://aturret.space/posts/Self-host-firecrawl/</id>
    <content type="text/html"
      src="https://aturret.space/posts/Self-host-firecrawl/" />
    <author>
      <name>aturret</name>
    </author>   <category term="科技" />  <category term="开发" />   <summary>偷懒了很久，把抓取 Bot 的这个功能补上了。</summary>
  <content type="html">
    <![CDATA[




<p>这几年来，我一直在维护一个方便朋友 Telegram 群聊的社交媒体抓取 Bot <a href="https://github.com/aturret/FastFetchBot">FastFetchBot</a>。这个 Bot 见证了我自学编程找工作的历程。其功能很简单：自动读取群聊中每一条消息中的 URL，如果 URL 匹配了特定社交媒体转发链接的正则表达式，使用各种方法对其进行爬取并发送至群聊中，搭配以 <a href="https://telegra.ph/">telegra.ph</a> 作为备份，提升群友阅读体验的同时亦可防止该内容原文丢失。</p>

<p>这个项目运行了多年，我一直没有去做一个更有趣的功能：通用型网页抓取。除了那些设置起了高墙的主流信息流通渠道——社交媒体之外，占据互联网世界另外半壁江山的“古法通讯”的传统网页，包括各类机构的官方网站，大型公共新闻媒体网站，以及个人博客，同样值得留档。</p>

<p>我正在对这坨屎山进行清理重构。一旦完成，会另写文章讲解开发该项目时的一些心得。</p>

<h2 id="通用型网页抓取原理">通用型网页抓取原理</h2>

<p>常规的网站难以被抓取的一大原因是其网页格式的不确定性。各个社交媒体的帖子格式完全固定。哪怕没有访问 API 到机会，只要能通过浏览器访问，就有机会将其结构化爬取。而随便扔出来一条古法时代的屎山网页，其对 Web 语义规范的遵循程度各异。此时我们很难直接通过特定的 HTML 标签将我们所需的正文爬取。</p>

<p>我在 Telegram 上曾长期利用过另一古法爬取通用型网页的公共 bot <a href="https://github.com/albertincx/formatbot1">InstantViewBot</a>。该 bot 的爬取发挥时好时坏，正是吃了各个网站格式各异的亏。</p>

<p>好在大语言模型的出现解决了这一痛点。大语言模型可以根据其经验准确地判断出繁杂的 HTML 屎山里何为标题何为作者名何为正文内容何为评论区，并生成结构化的输出。我们只需要攻克爬取部分即可，解析交由大语言模型来做。</p>

<h2 id="firecrawl">Firecrawl</h2>

<p><a href="https://github.com/firecrawl/firecrawl">Firecrawl</a> 是一个用于通用型网页批量爬取的开源项目，同时<a href="https://www.firecrawl.dev/">其官方也提供量贩爬取服务</a>。</p>

<p>该项目系统化地集成了从 Playwright 虚拟浏览器爬取网页，到使用大语言模型解析网页内容，再到保存内容至数据库的流程。整个系统还搭配了 Redis 和 RabbitMQ 等消息队列组件，健壮性尚可。对使用者来说，只要访问 API 就可等待解析结果送上桌来。</p>

<p>我们只要自己部署，就不用给官方送钱了。</p>

<h3 id="自建流程参考">自建流程参考</h3>

<p><a href="https://docs.firecrawl.dev/contributing/self-host">Firecrawl 的官方文档</a>对如何自建写得含混不清，强烈怀疑是为了骗你买官方服务。</p>

<p>建议在自己的 VPS 上直接 <code class="language-plaintext highlighter-rouge">git clone</code> 下载 Firecrawl repo，然后本地编译 image，否则 Postgre  部分还需要额外自建，略显麻烦。而且实际上数据库以外的其他几个组件使用线上 image 亦可。</p>

<p>我的服务器用的是 Traefik 做的反向代理，所以部署的难点在于如何对 Traefik 进行设置。</p>

<p>自建公网服务的一大隐患是被攻击者滥用。Firecrawl 官方不支持自建用户使用内置的鉴权系统，但是我们可以使用 Traefik 的中间件实现相同的效果。以下是 Traefik 的 <code class="language-plaintext highlighter-rouge">traefik.yml</code> 所需添加的中间件部分：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td> --><td class="rouge-code"><pre><span class="na">experimental</span><span class="pi">:</span>
  <span class="na">plugins</span><span class="pi">:</span>
    <span class="na">api-key-middleware</span><span class="pi">:</span>
      <span class="na">moduleName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">github.com/dtomlinson91/traefik-api-key-middleware"</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">v0.1.2"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Docker compose 文件如下，记得把 API 所需的 CPU 调低一点（我最多只能到3），免得把小鸡烧了：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
</pre></td> --><td class="rouge-code"><pre><span class="na">name</span><span class="pi">:</span> <span class="s">firecrawl</span>

<span class="na">x-common-service</span><span class="pi">:</span> <span class="nl">&amp;amp;common-service</span>
  <span class="c1"># NOTE: If you don't want to build the service locally,</span>
  <span class="c1"># comment out the build: statement and uncomment the image: statement</span>
  <span class="c1"># image: ghcr.io/firecrawl/firecrawl</span>
  <span class="na">build</span><span class="pi">:</span> <span class="s">apps/api</span>

  <span class="na">ulimits</span><span class="pi">:</span>
    <span class="na">nofile</span><span class="pi">:</span>
      <span class="na">soft</span><span class="pi">:</span> <span class="m">65535</span>
      <span class="na">hard</span><span class="pi">:</span> <span class="m">65535</span>
  <span class="na">networks</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">backend</span>
  <span class="na">extra_hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s2">"</span><span class="s">host.docker.internal:host-gateway"</span>
  <span class="na">logging</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">json-file"</span>
    <span class="na">options</span><span class="pi">:</span>
      <span class="na">max-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10m"</span>
      <span class="na">max-file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
      <span class="na">compress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>

<span class="na">x-common-env</span><span class="pi">:</span> <span class="nl">&amp;amp;common-env</span>
  <span class="na">REDIS_URL</span><span class="pi">:</span> <span class="s">${REDIS_URL:-redis://redis:6379}</span>
  <span class="na">REDIS_RATE_LIMIT_URL</span><span class="pi">:</span> <span class="s">${REDIS_URL:-redis://redis:6379}</span>
  <span class="na">PLAYWRIGHT_MICROSERVICE_URL</span><span class="pi">:</span> <span class="s">${PLAYWRIGHT_MICROSERVICE_URL:-http://playwright-service:3000/scrape}</span>
  <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">${POSTGRES_USER:-postgres}</span>
  <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">${POSTGRES_PASSWORD:-postgres}"</span>
  <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">${POSTGRES_DB:-postgres}</span>
  <span class="na">POSTGRES_HOST</span><span class="pi">:</span> <span class="s">${POSTGRES_HOST:-nuq-postgres}</span>
  <span class="na">POSTGRES_PORT</span><span class="pi">:</span> <span class="s">${POSTGRES_PORT:-5432}</span>
  <span class="na">USE_DB_AUTHENTICATION</span><span class="pi">:</span> <span class="s">${USE_DB_AUTHENTICATION:-false}</span>
  <span class="na">NUM_WORKERS_PER_QUEUE</span><span class="pi">:</span> <span class="s">${NUM_WORKERS_PER_QUEUE:-8}</span>
  <span class="na">CRAWL_CONCURRENT_REQUESTS</span><span class="pi">:</span> <span class="s">${CRAWL_CONCURRENT_REQUESTS:-10}</span>
  <span class="na">MAX_CONCURRENT_JOBS</span><span class="pi">:</span> <span class="s">${MAX_CONCURRENT_JOBS:-5}</span>
  <span class="na">BROWSER_POOL_SIZE</span><span class="pi">:</span> <span class="s">${BROWSER_POOL_SIZE:-5}</span>
  <span class="na">OPENAI_API_KEY</span><span class="pi">:</span> <span class="s">${OPENAI_API_KEY}</span>
  <span class="na">OPENAI_BASE_URL</span><span class="pi">:</span> <span class="s">${OPENAI_BASE_URL}</span>
  <span class="na">MODEL_NAME</span><span class="pi">:</span> <span class="s">${MODEL_NAME}</span>
  <span class="na">MODEL_EMBEDDING_NAME</span><span class="pi">:</span> <span class="s">${MODEL_EMBEDDING_NAME}</span> 
  <span class="na">OLLAMA_BASE_URL</span><span class="pi">:</span> <span class="s">${OLLAMA_BASE_URL}</span> 
  <span class="na">SLACK_WEBHOOK_URL</span><span class="pi">:</span> <span class="s">${SLACK_WEBHOOK_URL}</span>
  <span class="na">BULL_AUTH_KEY</span><span class="pi">:</span> <span class="s">${BULL_AUTH_KEY}</span>
  <span class="na">TEST_API_KEY</span><span class="pi">:</span> <span class="s">${TEST_API_KEY}</span>
  <span class="na">SUPABASE_ANON_TOKEN</span><span class="pi">:</span> <span class="s">${SUPABASE_ANON_TOKEN}</span>
  <span class="na">SUPABASE_URL</span><span class="pi">:</span> <span class="s">${SUPABASE_URL}</span>
  <span class="na">SUPABASE_SERVICE_TOKEN</span><span class="pi">:</span> <span class="s">${SUPABASE_SERVICE_TOKEN}</span>
  <span class="na">SELF_HOSTED_WEBHOOK_URL</span><span class="pi">:</span> <span class="s">${SELF_HOSTED_WEBHOOK_URL}</span>
  <span class="na">LOGGING_LEVEL</span><span class="pi">:</span> <span class="s">${LOGGING_LEVEL}</span>
  <span class="na">PROXY_SERVER</span><span class="pi">:</span> <span class="s">${PROXY_SERVER}</span>
  <span class="na">PROXY_USERNAME</span><span class="pi">:</span> <span class="s">${PROXY_USERNAME}</span>
  <span class="na">PROXY_PASSWORD</span><span class="pi">:</span> <span class="s">${PROXY_PASSWORD}</span>
  <span class="na">SEARXNG_ENDPOINT</span><span class="pi">:</span> <span class="s">${SEARXNG_ENDPOINT}</span>
  <span class="na">SEARXNG_ENGINES</span><span class="pi">:</span> <span class="s">${SEARXNG_ENGINES}</span>
  <span class="na">SEARXNG_CATEGORIES</span><span class="pi">:</span> <span class="s">${SEARXNG_CATEGORIES}</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">playwright-service</span><span class="pi">:</span>
    <span class="c1"># NOTE: If you don't want to build the service locally,</span>
    <span class="c1"># comment out the build: statement and uncomment the image: statement</span>
    <span class="c1"># image: ghcr.io/firecrawl/playwright-service:latest</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">apps/playwright-service-ts</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">PORT</span><span class="pi">:</span> <span class="m">3000</span>
      <span class="na">PROXY_SERVER</span><span class="pi">:</span> <span class="s">${PROXY_SERVER}</span>
      <span class="na">PROXY_USERNAME</span><span class="pi">:</span> <span class="s">${PROXY_USERNAME}</span>
      <span class="na">PROXY_PASSWORD</span><span class="pi">:</span> <span class="s">${PROXY_PASSWORD}</span>
      <span class="na">BLOCK_MEDIA</span><span class="pi">:</span> <span class="s">${BLOCK_MEDIA}</span>
      <span class="c1"># Configure maximum concurrent pages for Playwright browser instances</span>
      <span class="na">MAX_CONCURRENT_PAGES</span><span class="pi">:</span> <span class="s">${CRAWL_CONCURRENT_REQUESTS:-10}</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">backend</span>
    <span class="c1"># Resource limits for Docker Compose (not Swarm)</span>
    <span class="na">cpus</span><span class="pi">:</span> <span class="m">2.0</span>
    <span class="na">mem_limit</span><span class="pi">:</span> <span class="s">4G</span>
    <span class="na">memswap_limit</span><span class="pi">:</span> <span class="s">4G</span>
    <span class="na">logging</span><span class="pi">:</span>
      <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">json-file"</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="na">max-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10m"</span>
        <span class="na">max-file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
        <span class="na">compress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">tmpfs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/tmp/.cache:noexec,nosuid,size=1g</span>

  <span class="na">firecrawlapi</span><span class="pi">:</span>
    <span class="na">&amp;lt;&amp;lt;</span><span class="pi">:</span> <span class="nv">*common-service</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">&amp;lt;&amp;lt;</span><span class="pi">:</span> <span class="nv">*common-env</span>
      <span class="na">HOST</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0.0.0.0"</span>
      <span class="na">PORT</span><span class="pi">:</span> <span class="s">${INTERNAL_PORT:-3002}</span>
      <span class="na">EXTRACT_WORKER_PORT</span><span class="pi">:</span> <span class="s">${EXTRACT_WORKER_PORT:-3004}</span>
      <span class="na">WORKER_PORT</span><span class="pi">:</span> <span class="s">${WORKER_PORT:-3005}</span>
      <span class="na">NUQ_RABBITMQ_URL</span><span class="pi">:</span> <span class="s">amqp://rabbitmq:5672</span>
      <span class="na">ENV</span><span class="pi">:</span> <span class="s">local</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">redis</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
      <span class="na">playwright-service</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
      <span class="na">rabbitmq</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_healthy</span>
    <span class="c1"># ports:</span>
    <span class="c1">#   - "${PORT:-3002}:${INTERNAL_PORT:-3002}"</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">node dist/src/harness.js --start-docker</span>
    <span class="c1"># Resource limits for Docker Compose (not Swarm)</span>
    <span class="c1"># Increase if you have more CPU cores/RAM available</span>
    <span class="na">cpus</span><span class="pi">:</span> <span class="m">3.0</span>
    <span class="na">mem_limit</span><span class="pi">:</span> <span class="s">8G</span>
    <span class="na">memswap_limit</span><span class="pi">:</span> <span class="s">8G</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span> 
      <span class="pi">-</span> <span class="s">traefik.http.routers.firecrawlapi.rule=Host(`example.com`)</span> 
      <span class="pi">-</span> <span class="s">traefik.http.routers.firecrawlapi.entrypoints=websecure</span> 
      <span class="pi">-</span> <span class="s">traefik.http.routers.firecrawlapi.tls.certresolver=myResolver</span> 
      <span class="pi">-</span> <span class="s">traefik.http.services.firecrawlapi.loadbalancer.server.port=3002</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.firecrawlapi.middlewares=fc-auth</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.fc-auth.plugin.api-key-middleware.bearerHeader=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.fc-auth.plugin.api-key-middleware.bearerHeaderName=Authorization</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.fc-auth.plugin.api-key-middleware.keys=yourkeys</span>


  <span class="na">redis</span><span class="pi">:</span>
    <span class="c1"># NOTE: If you want to use Valkey (open source) instead of Redis (source available),</span>
    <span class="c1"># uncomment the Valkey statement and comment out the Redis statement.</span>
    <span class="c1"># Using Valkey with Firecrawl is untested and not guaranteed to work. Use with caution.</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">redis:alpine</span>
    <span class="c1"># image: valkey/valkey:alpine</span>

    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">backend</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">redis-server --bind 0.0.0.0</span>
    <span class="na">logging</span><span class="pi">:</span>
      <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">json-file"</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="na">max-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5m"</span>
        <span class="na">max-file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
        <span class="na">compress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    
  <span class="na">rabbitmq</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">rabbitmq:3-management</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">backend</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">rabbitmq-server</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">rabbitmq-diagnostics"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-q"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">check_running"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>
      <span class="na">start_period</span><span class="pi">:</span> <span class="s">5s</span>
    <span class="na">logging</span><span class="pi">:</span>
      <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">json-file"</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="na">max-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5m"</span>
        <span class="na">max-file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
        <span class="na">compress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
  
  <span class="na">nuq-postgres</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">apps/nuq-postgres</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">${POSTGRES_USER:-postgres}</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">${POSTGRES_PASSWORD:-postgres}</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">${POSTGRES_DB:-postgres}</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">backend</span>
    <span class="na">logging</span><span class="pi">:</span>
      <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">json-file"</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="na">max-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10m"</span>
        <span class="na">max-file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
        <span class="na">compress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">backend</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">bridge</span>

</pre></td></tr></tbody></table></code></pre></div></div>

<p>环境变量方面，<code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code> 自然不可少，而 <code class="language-plaintext highlighter-rouge">OPENAI_BASE_URL</code> 居然也需要手动设置，使用官方的 <code class="language-plaintext highlighter-rouge">https://api.openai.com/v1</code> 即可。此外别忘记设置 <code class="language-plaintext highlighter-rouge">BULL_AUTH_KEY</code> ，避免被人看光光管理界面。其他采用默认即可。</p>

<h3 id="代码整合">代码整合</h3>

<p>Firecrawl 官方提供了 Python SDK。下面是我手动调整过的一个 Gemini 3 Pro 给我 vibe 出来的单例模式客户端，供君参考：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
</pre></td> --><td class="rouge-code"><pre>
<span class="kn">from</span> <span class="n">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>

<span class="kn">import</span> <span class="n">threading</span>
<span class="kn">from</span> <span class="n">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span>

<span class="kn">from</span> <span class="n">firecrawl</span> <span class="kn">import</span> <span class="n">Firecrawl</span>

<span class="kn">from</span> <span class="n">app.config</span> <span class="kn">import</span> <span class="n">FIRECRAWL_API_URL</span><span class="p">,</span> <span class="n">FIRECRAWL_API_KEY</span><span class="p">,</span> <span class="n">FIRECRAWL_TIMEOUT_SECONDS</span>


<span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">FirecrawlSettings</span><span class="p">:</span>
    <span class="n">api_url</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">api_key</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">timeout_seconds</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">60</span>  <span class="c1"># 你也可以在反代侧控制超时
</span>

<span class="k">class</span> <span class="nc">FirecrawlClient</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    FirecrawlClient: 对 firecrawl python SDK 的封装 + 单例访问点。

    - 提供 scrape / crawl 等常用方法，方便其他模块调用
    - 线程安全单例（适合 Web 服务 / worker 多线程场景）
    </span><span class="sh">"""</span>

    <span class="n">_instance</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="sh">"</span><span class="s">FirecrawlClient</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">_lock</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="nc">Lock</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">config</span><span class="p">:</span> <span class="n">FirecrawlSettings</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_settings</span><span class="p">:</span> <span class="n">FirecrawlSettings</span> <span class="o">=</span> <span class="n">config</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_app</span><span class="p">:</span> <span class="n">Firecrawl</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_create_app</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>

    <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">_create_app</span><span class="p">(</span><span class="n">config</span><span class="p">:</span> <span class="n">FirecrawlSettings</span><span class="p">)</span> <span class="o">-&amp;gt;</span> <span class="n">Firecrawl</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="nc">Firecrawl</span><span class="p">(</span><span class="n">api_url</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_url</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_key</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">TypeError</span><span class="p">:</span>
            <span class="k">return</span> <span class="nc">Firecrawl</span><span class="p">(</span><span class="n">api_url</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_url</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_key</span><span class="p">)</span>

    <span class="nd">@classmethod</span>
    <span class="k">def</span> <span class="nf">get_instance</span><span class="p">(</span><span class="n">cls</span><span class="p">)</span> <span class="o">-&amp;gt;</span> <span class="sh">"</span><span class="s">FirecrawlClient</span><span class="sh">"</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        线程安全的单例获取。
        - 首次调用可传 settings
        - 之后重复调用可不传
        </span><span class="sh">"""</span>
        <span class="k">if</span> <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span>

        <span class="k">with</span> <span class="n">cls</span><span class="p">.</span><span class="n">_lock</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span>

            <span class="n">config</span> <span class="o">=</span> <span class="nc">FirecrawlSettings</span><span class="p">(</span>
                <span class="n">api_url</span><span class="o">=</span><span class="n">FIRECRAWL_API_URL</span><span class="p">,</span>
                <span class="n">api_key</span><span class="o">=</span><span class="n">FIRECRAWL_API_KEY</span><span class="p">,</span>
                <span class="n">timeout_seconds</span><span class="o">=</span><span class="n">FIRECRAWL_TIMEOUT_SECONDS</span><span class="p">,</span>
            <span class="p">)</span>

            <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span> <span class="o">=</span> <span class="nf">cls</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span>

    <span class="nd">@classmethod</span>
    <span class="k">def</span> <span class="nf">reset_instance</span><span class="p">(</span><span class="n">cls</span><span class="p">)</span> <span class="o">-&amp;gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">测试用：重置单例。</span><span class="sh">"""</span>
        <span class="k">with</span> <span class="n">cls</span><span class="p">.</span><span class="n">_lock</span><span class="p">:</span>
            <span class="n">cls</span><span class="p">.</span><span class="n">_instance</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">scrape_url</span><span class="p">(</span>
            <span class="n">self</span><span class="p">,</span>
            <span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
            <span class="n">formats</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
            <span class="n">only_main_content</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
            <span class="n">timeout_seconds</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
            <span class="n">extra_params</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&amp;gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="sh">"""</span><span class="s">
        单页抓取（最常用）
        </span><span class="sh">"""</span>
        <span class="n">params</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">formats</span><span class="sh">"</span><span class="p">:</span> <span class="n">formats</span> <span class="ow">or</span> <span class="p">[</span><span class="sh">"</span><span class="s">markdown</span><span class="sh">"</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">onlyMainContent</span><span class="sh">"</span><span class="p">:</span> <span class="n">only_main_content</span><span class="p">,</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="n">extra_params</span><span class="p">:</span>
            <span class="n">params</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">extra_params</span><span class="p">)</span>

        <span class="c1"># if timeout_seconds is None:
</span>        <span class="c1">#     timeout_seconds = self._settings.timeout_seconds
</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_app</span><span class="p">.</span><span class="nf">scrape</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">formats</span><span class="o">=</span><span class="n">formats</span><span class="p">,</span> <span class="n">only_main_content</span><span class="o">=</span><span class="n">only_main_content</span><span class="p">).</span><span class="nf">model_dump</span><span class="p">(</span>
                <span class="n">exclude_none</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">RuntimeError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Firecrawl scrape_url failed: url=</span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="k">from</span> <span class="n">e</span>


</pre></td></tr></tbody></table></code></pre></div></div>

<p>在你的代码中记得把 Traefik 那个中间件的密钥输入进 <code class="language-plaintext highlighter-rouge">api_key</code> 部分去创建 Firecrawl 对象。Firecrawl 的 SDK 会把这个 key 添加到 HTTP Header 的 <code class="language-plaintext highlighter-rouge">Authorization</code> 里。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre> <span class="k">return</span> <span class="nc">Firecrawl</span><span class="p">(</span><span class="n">api_url</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_url</span><span class="p">,</span> <span class="n">api_key</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_key</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>调用该单例 <code class="language-plaintext highlighter-rouge">fc = FirecrawlClient.get_instance()</code> ，使用 <code class="language-plaintext highlighter-rouge">scrape_url</code> 即可进行爬取。</p>

<h2 id="后-ai-时代写技术博客的碎碎念">后 AI 时代写技术博客的碎碎念</h2>

<p>虽然本文的绝大多数内容都是通过和 Gemini 3 Pro 聊天聊出来的，但我觉得进行一个记录还是有价值的。一是把最近做的有趣的事情分析给朋友；二是其实做一件事的大多数流程都很顺利，但被卡住的那部分痛点可能即使是问 AI 也要折腾很久，而这部分才是 AI 时代真正值得人类去做的工作。</p>

<h2 id="参考文献">参考文献</h2>

<ol>
  <li><a href="https://tim-kleyersburg.de/articles/setup-firecrawl-with-docker-and-traefik">Setup Firecrawl with Docker and Traefik - Tim Kleyersburg</a></li>
</ol>
]]>
  </content>

  </entry>
  
   <entry>
    <title>Sony WH-1000XM4 固件降级指南，顺便骂骂索尼</title>
    <link href="https://aturret.space/zh-CN/posts/SonyWH1000XM4-Firmware-Downgrade/" rel="alternate"
      type="text/html" title="Sony WH-1000XM4 固件降级指南，顺便骂骂索尼" />
    <published>2025-05-26T11:33:00-05:00</published>  <updated>2025-05-26T11:33:00-05:00</updated>
   <id>https://aturret.space/posts/SonyWH1000XM4-Firmware-Downgrade/</id>
    <content type="text/html"
      src="https://aturret.space/posts/SonyWH1000XM4-Firmware-Downgrade/" />
    <author>
      <name>aturret</name>
    </author>   <category term="生活" />  <category term="数码" />   <summary>本来想着三年之期已到，该买最新款了，结果发现，得，这台 XM4 还能再战三年……</summary>
  <content type="html">
    <![CDATA[




<p>我人生中的第一款蓝牙降噪耳机是20年在友人安利下买的 Sony WH-1000XM3，其1400元人民币的价格让当时还是学生的我倍感心疼。但其优秀的佩戴体验和强劲的降噪性能也确实是给我带来了人生的头戴式纯享宁静初体验，确实是大幅提升了生活质量的实在好物。只要有效果，这个价格真的不算贵。从此之后，我买头戴耳机就认准这个系列了。</p>

<h2 id="先辱骂一下索尼">先辱骂一下索尼</h2>

<p>然后，这个系列就不负众望的拉了。</p>

<h3 id="狗屎一样的升级">狗屎一样的升级</h3>

<p>22年年初，我把那台 XM3 摔坏了。本着买新不买旧的原则不得已买了一个 XM4。用到了2023年年初，我的 XM4 又不幸中招，降噪器元件坏掉，自己反复维修都失败。于是我本着买新不买旧的原则，想要试试 XM5。只在图书馆试用了一个小时，我就决定把这台 XM5 退掉：XM5 没法折叠，放到书包里都怕挤坏；佩戴起来非常不适，拉到最大依然夹头；自适应降噪无法关闭，给我的感觉就是外界的声音忽大忽小，非常干扰我的注意力，这种情况还不如没降噪呢。</p>

<p>结果是，我又买了一台 XM4，用到现在。</p>

<p>如今，三年之期已到，Sony 又推出了最新款的 WH1000-XM6。我满怀期待地看了一些 XM6 的测评视频，发现哪怕是再收了钱的商单评测，也不敢回避 XM6 最大的设计问题：为了所谓的机制降噪性能，佩戴起来更夹头了，而降噪性能本身却不存在什么质的飞跃——和两年前一样，这就直接把我劝退了啊。得，要不，再买一台 XM4？</p>

<p>甚至再找到一些23年时的帖子，我发现当年很多人实测就和我个人感受一样：XM5 相对 XM4 的升级聊胜于无，反而有很多激进但未必讨喜的设计更新，比如夹头的结构和性能不稳定的自适应降噪。</p>

<p>我就不懂了，从我第一次持有 Sony WH-1000XM 系列耳机到现在五年过去了，我居然对这个系列的新产品毫无购买欲望。而索尼也就死心眼，绝对不愿意基于 XM4 的模具推出更新版。看来这台作为 XM3 增强版的 XM4 还得再战三年……</p>

<h3 id="定期报废的固件负优化">定期报废的固件负优化</h3>

<p>制造业的悖论：硬件做得质量太好，反而会导致下一代产品卖不出去没法持续割韭菜。于是和苹果会对先前两代的 iPhone 性能在 iOS 更新后进行负优化一样，也有大量群众反映，Sony 对降噪蓝牙耳机也会在固件升级后有意识地削弱其降噪性能。我个人也有类似体验。</p>

<p>朋友，我真不是不愿意给你的新品掏钱，但是你的新模具真的太他妈夹头了。算了，还是研究一下怎么降级固件吧。索尼你逼我的啊。</p>

<h2 id="固件降级">固件降级</h2>

<p>搜来搜去，搜到<a href="https://www.mrwalkman.com/p/mdrproxyfwsidegradetool.html">一篇帖子讲怎么固件降级的</a>。感谢 GitHub 上有一位国人大神开发出了专门针对索尼系耳机固件的定向刷机软件。</p>

<p>其原理是：反编译 Sound Connect 手机 APP 后拆解其逻辑，伪造索尼官网的 SSL 证书并在本地架设固件更新服务器，诱使 Sound Connect 从用户自行架设的更新服务器获取更新信息并安装特定版本的固件。</p>

<p>警告：民间大神毕竟不可能面面俱到地考虑到所有更新时可能出现的意外情况，他仅仅修改了提供的固件资源那部分。所以这么做多少还是有变砖风险。当然我是办成了没事，你们自己看着办。</p>

<h3 id="流程">流程</h3>

<p>前往项目的 GitHub 页面：https://github.com/lzghzr/MDR_Proxy。进入 Release 页面下载打包好的使用包即可。</p>

<p>XM4 的版本已经算比较老的了，所以必须先卸载手机上已有的 Sound Connect，再下载 10 版本的 Sound Connect APP 进行安装。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250526121724145.png" alt="image-20250526121724145" /></p>

<p>除此之外，还要下载 <code class="language-plaintext highlighter-rouge">MDR_Proxy</code> 这个代理服务器架设工具本体。</p>

<p>把 APP 安装包 zip 下的证书文件放到 <code class="language-plaintext highlighter-rouge">MDR_Proxy/security</code> 目录下。然后点击 <code class="language-plaintext highlighter-rouge">run.bat</code> 或者进入 terminal 输入 <code class="language-plaintext highlighter-rouge">node mdrproxy.js</code> 即可启动运行。为了方便不懂编程的群众使用，作者还在安装包里内置了一个 Node.js 环境的 exe 文件，非常感人了。（如果你是 Mac OS，请自行安装 node。）</p>

<p>在那之前，先阅读 GitHub 上的 README 文档，下载对应版本的固件，放置在 <code class="language-plaintext highlighter-rouge">MDR_Proxy/custom</code> 目录下。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250526135139174.png" alt="image-20250526135139174" /></p>

<p>其中，00-03是不同地区固件的编码。00、01，和 02 分别是国际，日本，和中国。应该是对应固件的语音语言。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250526135155582.png" alt="image-20250526135155582" /></p>

<p>在手机方面。需要在 Sound Connect APP 内对手机 APP 进行设置：</p>

<ul>
  <li>禁止手机 APP 自动更新 - 防止应用商城把手机 APP 更新到新版，导致作者的代理服务器失效。</li>
  <li>禁止耳机一段时间不使用后自动关机 - 防止更新过程中出现意外变砖。</li>
</ul>

<p>然后，将手机和电脑置于同一 Wi-Fi 环境下，在手机的 Wi-Fi 设置中调整代理类型为“手动”，输入运行代理服务器软件的设备的 IP 地址，以及该代理服务器的默认架设端口 8848。</p>

<p>Windows 电脑的局域网 IP 地址，可以通过在控制台中输入 <code class="language-plaintext highlighter-rouge">ipconfig</code> 找到。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250526134413343.png" alt="image-20250526134413343" /></p>

<p>一切准备就绪后，手机启动 Sound Connect，电脑输入3，然后选择你想要刷的固件。Sound Connect 上就会出现更新提示了。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250526134903119.png" alt="image-20250526134903119" /></p>

<p><strong>记得在更新前确保网络稳定，给手机和耳机都充好电，防止意外变砖。</strong></p>

<h3 id="降级成功后体验">降级成功后体验</h3>

<p>……还真更安静了一点。群众测试结果诚不我欺。可惜的是目前还找不到 2.5 以前版本的固件，新出厂的 XM4 都是预装 2.7.1 版本了。不知道是不是其实 XM4 的降噪性能本身还可以更好。</p>

<h2 id="参考文献">参考文献</h2>

<ol>
  <li><a href="https://www.bilibili.com/video/BV1gHjGzyE5Z/">哪一代最值得买 ？索尼 WH-1000XM6 全面测评 - 直推小新</a></li>
  <li><a href="https://www.bilibili.com/video/BV1Se411C7hq/">2024年了，它还值不值 索尼 WH-1000XM4 &amp;amp; XM5 性能对比测评 - 直推小新</a></li>
  <li><a href="https://www.mrwalkman.com/p/mdrproxyfwsidegradetool.html">MDR Proxy - FW sidegrade tool for Sony bluetooth headphones/earbuds - MrWalkman</a></li>
  <li><a href="https://www.bilibili.com/video/BV1UTHie8EQR/">想回味xm4刚出时候的降噪性能？xm5刚出时候的默认声音？【索尼耳机固件降级方法】</a></li>
  <li><a href="https://www.reddit.com/r/sony/comments/dpsmsq/wh1000xm3_custom_firmware_flash_mdr_proxy/">WH-1000XM3 custom firmware flash (MDR_Proxy) - Reddit r/sony</a></li>
</ol>
]]>
  </content>

  </entry>
  
   <entry>
    <title>外接托管 MySQL 数据库平台 Railway</title>
    <link href="https://aturret.space/zh-CN/posts/Railway-Rent-MySQL/" rel="alternate"
      type="text/html" title="外接托管 MySQL 数据库平台 Railway" />
    <published>2025-04-17T00:00:00-05:00</published>  <updated>2025-04-17T00:00:00-05:00</updated>
   <id>https://aturret.space/posts/Railway-Rent-MySQL/</id>
    <content type="text/html"
      src="https://aturret.space/posts/Railway-Rent-MySQL/" />
    <author>
      <name>aturret</name>
    </author>   <category term="科技" />  <category term="运维" />   <summary>为我的 Waline 评论系统寻找外接的 MySQL 云提供商。</summary>
  <content type="html">
    <![CDATA[




<p>为了方便访客留言，我为我的<a href="https://blog.aturret.com/">吹牛逼博客</a>部署了 <a href="https://waline.js.org/">Waline 评论系统</a>。这个系统需要接入一个数据库，MongoDB、MySQL、Postgres 都行。我想了想，如果想要长久保存，还是生态最丰富的 MySQL 最靠谱。</p>

<p>我不想把 MySQL 运行在我的 VPS 上，因为我的 VPS 经常会搞一些实验性质的东西把服务器搞炸。</p>

<p>2025年了，于是我直接问我们的 GPT 老师，外接 MySQL 托管哪家强：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td> --><td class="rouge-code"><pre>I have an application that needs a MySQL database. I dont want to set it up on my own server but use a cloud provider's. Do you have any suggestions?
The data volume and traffic volume is low but I hope it can persist long with low price while I can backup the data and transfer it at any time
</pre></td></tr></tbody></table></code></pre></div></div>

<p>答曰：</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
</pre></td> --><td class="rouge-code"><pre>Absolutely — for low-traffic, low-volume MySQL databases with persistent storage, backups, and easy migration, there are several good cloud options. Since you're optimizing for <span class="gs">**low cost**</span>, <span class="gs">**longevity**</span>, and <span class="gs">**portability**</span>, here are some curated suggestions:
<span class="p">
---
</span>
<span class="gu">### 🔹 1. **PlanetScale** *(Highly recommended for your use case)*</span>
<span class="p">-</span> <span class="gs">**Pricing**</span>: Free tier available with generous limits.
<span class="p">-</span> <span class="gs">**Key Features**</span>:
<span class="p">  -</span> MySQL-compatible (built on Vitess, used by YouTube).
<span class="p">  -</span> Serverless scaling.
<span class="p">  -</span> Non-blocking schema changes.
<span class="p">  -</span> Automatic daily backups (paid tiers have longer retention).
<span class="p">  -</span> CLI and web-based data export.
<span class="p">-</span> <span class="gs">**Best for**</span>: Projects needing long-term free hosting and easy backups.

📌 <span class="gs">**Notes**</span>:
<span class="p">-</span> Doesn't allow direct access to <span class="sb">`mysqldump`</span> (uses its own export tools).
<span class="p">-</span> Slightly different from traditional MySQL in some low-level admin operations.
<span class="p">
---
</span>
<span class="gu">### 🔹 2. **Railway**</span>
<span class="p">-</span> <span class="gs">**Pricing**</span>: Free tier includes 500MB of storage, with paid plans starting low.
<span class="p">-</span> <span class="gs">**Key Features**</span>:
<span class="p">  -</span> Easy MySQL deployment with backup/export options.
<span class="p">  -</span> Integration with Git-based workflows.
<span class="p">  -</span> Nice for quick prototyping and persistent development.
<span class="p">-</span> <span class="gs">**Best for**</span>: Simple apps needing database + backend hosted together.
<span class="p">
---
</span>
<span class="gu">### 🔹 3. **Render**</span>
<span class="p">-</span> <span class="gs">**Pricing**</span>: $7/month for Managed MySQL.
<span class="p">-</span> <span class="gs">**Key Features**</span>:
<span class="p">  -</span> Daily backups, automatic scaling.
<span class="p">  -</span> Simple UI for database access.
<span class="p">-</span> <span class="gs">**Best for**</span>: More traditional MySQL feel, small production apps.
<span class="p">
---
</span>
<span class="gu">### 🔹 4. **Google Cloud SQL / Amazon RDS / Azure Database for MySQL**</span>
<span class="p">-</span> <span class="gs">**Pricing**</span>: Paid from the start (~$15+/month for the lowest setup).
<span class="p">-</span> <span class="gs">**Key Features**</span>:
<span class="p">  -</span> Fully managed, high availability, easy snapshot backup and export.
<span class="p">-</span> <span class="gs">**Best for**</span>: Production-grade reliability, enterprise-level features.
<span class="p">-</span> <span class="gs">**Not recommended**</span> if you're trying to keep it super cheap.
<span class="p">
---
</span>
<span class="gu">### 🔹 5. **DigitalOcean Managed Databases**</span>
<span class="p">-</span> <span class="gs">**Pricing**</span>: Starts from $15/month, but frequent discounts for new users.
<span class="p">-</span> <span class="gs">**Key Features**</span>:
<span class="p">  -</span> Automatic backups, metrics, scaling.
<span class="p">  -</span> Easy migration using <span class="sb">`mysqldump`</span> or <span class="sb">`pg_dump`</span>.
<span class="p">-</span> <span class="gs">**Best for**</span>: If you may later scale up without changing platforms.
<span class="p">
---
</span>
<span class="gu">### ✅ Summary Recommendation:</span>

| Provider       | Free Tier | Backup/Export | Best For                            |
|----------------|-----------|----------------|-------------------------------------|
| <span class="gs">**PlanetScale**</span> | ✅         | ✅ (with CLI)   | Long-term, low-traffic apps         |
| <span class="gs">**Railway**</span>     | ✅         | ✅              | Quick setups with portability       |
| <span class="gs">**Render**</span>      | ❌         | ✅              | More control with affordable price  |
| <span class="gs">**GCP/AWS/Azure**</span>| ❌        | ✅              | Production, but more costly         |
| <span class="gs">**DigitalOcean**</span>| ❌ (trials) | ✅             | Budget production hosting           |
<span class="p">
---
</span>
Let me know your preferred tech stack or budget limit and I can help you choose/setup one of these platforms with actual steps.
</pre></td></tr></tbody></table></code></pre></div></div>

<p>PlanetScale 已经不给白嫖了，于是我去查 Railway。Railway 价格还是可以的，基本款5刀一个月，而且给5刀的免费试用。</p>

<p>用了一下，发现这个 Railway 其实就是拖控件开 Docker 构建数据流。理论上可以把自己的大多数容器化服务都托管到上面。</p>

<p><img src="https://raw.githubusercontent.com/aturret/AnotherStorageZone/main/img/image-20250417235056668.png" alt="image-20250417235056668" /></p>

<p>部署过程中遇到了一个问题。Waline 不支持8以上的 MySQL，于是我需要把 Railway 默认提供的 MySQL 版本改掉。但是当我把 Docker image 的版本从9改到5.7再重新部署，Railway 一直在报错。查看错误日志，原来是因为绑定的 Data Volume 依然保存了创建时默认的9版本的内容。所以需要先改 image 版本，再手动删掉那个默认提供的 Volume ，再绑一个新的空 Volume 上去。</p>

<p>为了这个我折腾了半个多小时。这个网站的设计师一定觉得自己聪明坏了，拖控件编程好帅好爽好方便一定能大卖吧……然而这样的反直觉的操作流程还不如用命令行操作 Docker 时得到的提示直接——至少我可以直接 <code class="language-plaintext highlighter-rouge">docker compose down -v</code>，连带着 container 及其挂载的持久化卷一同删除。总觉得搞这种创业项目费力不讨好，高不成低不就，不知道什么样的用户会依赖这样的服务。</p>

<p>不管怎么说，这家还算价格公道，暂且先用着了。以后可以考虑把更多 VPS 上的东西用这家部署。</p>
]]>
  </content>

  </entry>
  
   <entry>
    <title>记一次 VPS 硬盘容量爆炸修复</title>
    <link href="https://aturret.space/zh-CN/posts/VPS-storage-outrage/" rel="alternate"
      type="text/html" title="记一次 VPS 硬盘容量爆炸修复" />
    <published>2025-01-24T00:00:00-06:00</published>  <updated>2025-01-24T00:00:00-06:00</updated>
   <id>https://aturret.space/posts/VPS-storage-outrage/</id>
    <content type="text/html"
      src="https://aturret.space/posts/VPS-storage-outrage/" />
    <author>
      <name>aturret</name>
    </author>   <category term="科技" />  <category term="运维" />   <summary>1月20日突然发现服务器的很多服务停了，也没法用 VS Code SSH 进去，进行了一番修复。</summary>
  <content type="html">
    <![CDATA[




<p>1月20日起床，我发现有几个运行在服务器上的 Telegram Bot 停止了运行。</p>

<p>躺在床上，赶紧用手机上的 JuiceSSH 进去看了一下，有好几个 Docker 服务都停了。其中一个是我 Telegram Bot 的自建 API 服务器，提示</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre>can't create directories in the directory "/var/lib/telegram-bot-api/". Use --dir option to specify a writable working directory
</pre></td></tr></tbody></table></code></pre></div></div>

<p>我觉得很奇怪，一开始以为是 Docker Image 出了问题。于是起床打开电脑，发现居然无法用 VS Code 的 Remote 功能 SSH 进入服务器。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre>Failed to connect to the remote extension host server (Error: WrappedError(WrappedError { message: "error unpacking /root/.vscode-server/cli/servers/Stable-cd4ee3b1c348a13bafd8f9ad8060705f6d4b9cba.staging/server/node_modules/@xterm/addon-webgl/lib/addon-webgl.mjs.map", original: "Custom { kind: StorageFull, error: TarError { desc: \"failed to unpack vscode-server-linux-x64/node_modules/@xterm/addon-webgl/lib/addon-webgl.mjs.map into /root/.vscode-server/cli/servers/Stable-cd4ee3b1c348a13bafd8f9ad8060705f6d4b9cba.staging/server/node_modules/@xterm/addon-webgl/lib/addon-webgl.mjs.map\", io: Os { code: 28, kind: StorageFull, message: \"No space left on device\" } } }" }))
</pre></td></tr></tbody></table></code></pre></div></div>

<p>反复尝试后无果。直接复制这段问 ChatGPT 老师。回答说“The error message indicates that the system is running out of disk space (<code class="language-plaintext highlighter-rouge">No space left on device</code>)”。</p>

<p>于是进一步询问如何查容量使用。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td> --><td class="rouge-code"><pre><span class="nb">df</span> <span class="nt">-h</span>
<span class="nb">du</span> <span class="nt">-ah</span> / | <span class="nb">sort</span> <span class="nt">-rh</span> | <span class="nb">head</span> <span class="nt">-n</span> 20
</pre></td></tr></tbody></table></code></pre></div></div>

<p>然而由于空间已经所剩无几，连运行完检索最大20个文件的 pipeline 的容量都不足了。因为我服务器上所有的服务都是跑在 Docker 上的，我估计大概率又是哪个 Docker 容器出问题了。</p>

<p>于是我先清理了一下不用的容器</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre>docker prune image
</pre></td></tr></tbody></table></code></pre></div></div>

<p>有了空间再运行 <code class="language-plaintext highlighter-rouge">du -ah / | sort -rh | head -n 20</code>。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td> --><td class="rouge-code"><pre>76G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240
39G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/merged
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/merged/tmp/rod/user-data
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/merged/tmp/rod
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/merged/tmp
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/diff/tmp/rod/user-data
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/diff/tmp/rod
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/diff/tmp
38G     /var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240/diff
4.3G    /var/lib/docker/volumes
3.0G    /var/lib/docker/overlay2/3762ba957ba504bf1fe2de7e08b46ab449133ccb2292fab0f61e26901efb916e/merged
3.0G    /var/lib/docker/overlay2/3762ba957ba504bf1fe2de7e08b46ab449133ccb2292fab0f61e26901efb916e
2.8G    /var/lib/docker/overlay2/64759dec53597f35b23513cf7bec2880d43ef6acc79ece0338a5d0ec314702c7/merged
2.8G    /var/lib/docker/overlay2/64759dec53597f35b23513cf7bec2880d43ef6acc79ece0338a5d0ec314702c7
</pre></td></tr></tbody></table></code></pre></div></div>

<p>虽然不知道是哪个容器造成的容量无限吞噬，只好直接 <code class="language-plaintext highlighter-rouge">docker prune</code> 了，然后关闭 Docker，手动移除<code class="language-plaintext highlighter-rouge">/var/lib/docker/overlay2/c4fd02de5c772625977613bd995d71be1faa6f8fd237d54c88028b6b75123240</code> 目录。</p>

<p>之后就好了，好像也没啥事。</p>

<hr />

<p>更新：</p>

<p>手动移除 <code class="language-plaintext highlighter-rouge">/var/lib/docker/overlay2/</code> 是不合适的。实际上这些文件是 Docker container 挂载在硬盘的文件目录。只要关闭对应的 container，这些文件就会立刻消失。</p>

<p>可以使用这个命令查看哪个目录占空间最多：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre><span class="nb">du</span> <span class="nt">-h</span> <span class="nt">--max-depth</span><span class="o">=</span>1 /var/lib/docker/overlay2 | <span class="nb">sort</span> <span class="nt">-hr</span> | <span class="nb">head</span> <span class="nt">-20</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Claude 帮我写了一个脚本查看每个目录对应的 container 是哪个：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td> --><td class="rouge-code"><pre><span class="k">for </span>container <span class="k">in</span> <span class="si">$(</span>docker ps <span class="nt">-q</span><span class="si">)</span><span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"Container: </span><span class="si">$(</span>docker inspect <span class="nt">--format</span> <span class="s1">''</span> <span class="nv">$container</span><span class="si">)</span><span class="s2">"</span>
  <span class="nb">echo</span> <span class="s2">"ID: </span><span class="nv">$container</span><span class="s2">"</span>
  <span class="nb">echo</span> <span class="s2">"Overlay: </span><span class="si">$(</span>docker inspect <span class="nt">--format</span> <span class="s1">''</span> <span class="nv">$container</span><span class="si">)</span><span class="s2">"</span>
  <span class="nb">echo</span> <span class="s2">"---------------------------------------"</span>
<span class="k">done</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>查出来后关掉对应的 container 就好。</p>
]]>
  </content>

  </entry>
  
   <entry>
    <title>WordPress 博客迁移 Jekyll 小记</title>
    <link href="https://aturret.space/zh-CN/posts/Migrate-WordPress-to-Jekyll/" rel="alternate"
      type="text/html" title="WordPress 博客迁移 Jekyll 小记" />
    <published>2024-11-04T00:00:00-06:00</published>  <updated>2024-11-04T00:00:00-06:00</updated>
   <id>https://aturret.space/posts/Migrate-WordPress-to-Jekyll/</id>
    <content type="text/html"
      src="https://aturret.space/posts/Migrate-WordPress-to-Jekyll/" />
    <author>
      <name>aturret</name>
    </author>   <category term="科技" />  <category term="运维" />   <summary>用了一个周末的时间写了一个 Jekyll 主题，把我的 WordPress 博客搬了过去。</summary>
  <content type="html">
    <![CDATA[




<p>我一直都有<a href="https://blog.aturret.com">一个 WordPress 博客</a>。和很多人一样，这个博客也是我逐渐开始自学转码，开始玩 VPS 时候（2019年）最先接触到并部署的站点。因为写作是一直的爱好，就把大大小小的随笔都扔到了上面。</p>

<p>几年来，我的 IT 水平突飞猛进，逐渐意识到 WordPress 其实不太适合我这种纯文字为主的站点。很多 PHP 才能做到的高级动态功能我都没用上，反而各类 PHP 的使用还加剧了内存消耗，降低了读者访问速度和阅读体验。干脆把这个博客也迁移到 Jekyll 得了。</p>

<h2 id="提取-wordpress-内容为-jekyll-所需格式">提取 WordPress 内容为 Jekyll 所需格式</h2>

<p>好在 GitHub 早年为了推自家的 GitHub Pages，做了个 WordPress 插件 <a href="https://ben.balter.com/wordpress-to-jekyll-exporter/">WordPress to Jekyll Exporter</a>。坏消息是，</p>

<h3 id="docker-在容器中调试-wordpress">Docker 在容器中调试 WordPress</h3>

<p>使用 Docker 命令 <code class="language-plaintext highlighter-rouge">docker exec -it &amp;lt;container_id&amp;gt; bash</code> 进入该容器的 Shell 界面。</p>

<p>一路 cd 进入到 <code class="language-plaintext highlighter-rouge">/var/www/html/wp-content/plugins/jekyll-exporter/</code> 里，根据 WordPress to Jekyll Exporter 官网的建议<a href="https://ben.balter.com/wordpress-to-jekyll-exporter/command-line-usage/">使用命令行运行</a>。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre>php jekyll-export-cli.php <span class="o">&amp;gt;</span> jekyll-export.zip
</pre></td></tr></tbody></table></code></pre></div></div>

<p>发现提示内存不足。修改配置文件把允许读取内存从64MB提升到256MB。</p>

<p>此外，还发现其他插件有影响该插件运行的可能，于是试图关闭其他插件。</p>

<p>WordPress 的原理是只看 <code class="language-plaintext highlighter-rouge">wp-content/plugins/</code> 里的内容。所以只要把插件目录用 <code class="language-plaintext highlighter-rouge">mv</code> 指令改名，然后弄一个新的 <code class="language-plaintext highlighter-rouge">plugins</code> 目录，再把需要的插件复制一份进去就好。</p>

<h3 id="docker-部署下操作容器内的文件">Docker 部署下操作容器内的文件</h3>

<p>依然提示有 bug，似乎是某个 php 的函数无法顺利运行。搜索 GitHub 的项目 Issue 区，看到了<a href="https://github.com/benbalter/wordpress-to-jekyll-exporter/issues/24#issuecomment-1893502570">一个 Issue 提起此问题</a>。</p>

<p>原来是少添加了一条引用的函数。只要把 <code class="language-plaintext highlighter-rouge">include "../../../wp-admin/includes/file.php";</code> 加到 <code class="language-plaintext highlighter-rouge">jekyll-export-cli.php</code> 的最上方就好。非常可笑的是，十年过去了，这招居然还适用。</p>

<p>然而，我的 WordPress Docker 容器里并没有 <code class="language-plaintext highlighter-rouge">vi</code> 命令软件，没法直接编辑 <code class="language-plaintext highlighter-rouge">jekyll-export-cli.php</code>。这里就只能曲线救国了：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><!-- <td class="rouge-gutter gl"><pre class="lineno">1
</pre></td> --><td class="rouge-code"><pre>docker <span class="nb">cp</span> &amp;lt;container_id&amp;gt;:/var/www/html/wp-content/plugins/jekyll-exporter/jekyll-export-cli.php ~
</pre></td></tr></tbody></table></code></pre></div></div>

<p>即把该文件复制到我的 VPS 的用户根目录下。修改完文件后把 <code class="language-plaintext highlighter-rouge">cp</code> 命令的源地址和目标地址交换，再次使用即可传回容器内。</p>

<p>这次再使用 <code class="language-plaintext highlighter-rouge">php jekyll-export-cli.php &amp;gt; jekyll-export.zip</code>，终于成功了。</p>

<h2 id="编辑文章元数据">编辑文章元数据</h2>

<p>这个插件导出的文章 markdown 文件的文件名都是一串乱码，然而文件内的 YAML 头文字 <code class="language-plaintext highlighter-rouge">title</code> 都是对的。这种事情用一个 Python 脚本处理再好不过了。现如今，这种前人已经做过无数次的文本处理小场景问题肯定用不着自己重新造轮子。用 ChatGPT 写 Python 脚本还是很方便的，直接问：</p>

<blockquote>
  <p>write me a python script to change all my .md files’ title from “yyyy-mm-dd-xxxxxx” to “yyyy-mm-dd-$title” according to the yaml front matter in each files. they both have <code class="language-plaintext highlighter-rouge">title</code> attribute in the yaml frontmatters</p>
</blockquote>

<p>出来的内容复制粘贴，稍微改一下目录所在地址，直接在 VS Code 下运行，一点 bug 都没有。</p>

<p>我还类似地问 ChatGPT 写脚本，添加了一些 YAML 元数据，脚本的表现都很好。</p>

<p>AI 确实取代了一部分所谓程序员的工作——然而，这只是小场景的切入，是最容易被模仿并且用自然语言表达的。仅仅会做这些是没法完成大型工程的。反倒是某些希望能使用编程提升自己工作效率的文本处理人员会学一点这些——不过也可以由 GPT 代劳了。由此看来，AI 目前取代的其实依然只是低级文员的工作。</p>

<h2 id="山寨制造-jekyll-博客主题">山寨制造 Jekyll 博客主题</h2>

<p>一直没下定决心把主博客搬迁到 Jekyll 的原因还有一个，就是没有找到心仪的适合随笔类博客主体。姑且先山寨之前在 WordPress 上使用的那个主题随手做一个吧，也自己看习惯了……毕竟博客很大的程度上就是给自己看的小玩具嘛。</p>

<p>ChatGPT 做前端小组件什么的还是很方便的。给出 prompt，导航栏的下拉菜单和主页尾端的分页菜单这种一下就写好，再稍微手动调整下 CSS 就好。</p>

<p>很多实用 HTML 小组件比如分页和图片包装，直接抄了 Chirpy 的代码稍微改改就用。说实在的，我觉得 Chirpy 的那一套设计都可以当作 Jekyll 主题制作的规范了，功能实在是全面到不像话。</p>

<p>具体怎么制作的主题，遇到了以后再单出一篇细讲吧。也许那时候抄袭完善得差不多了可以放出来给大家用。现在还有很多想要的功能没做出来，CSS 之类的也写得很混乱，不方便放出来。</p>

<h2 id="参考文献">参考文献</h2>

<ol>
  <li><a href="https://heidloff.net/article/migrating-from-wordpress-to-jekyll/">Migrating from Wordpress to Jekyll</a></li>
  <li><a href="https://jj09.net/moving-from-wordpress-to-jekyll/">Moving from WordPress to Jekyll</a></li>
  <li><a href="https://www.bawbgale.com/from-wordpress-to-jekyll/">From Wordpress To Jekyll</a></li>
</ol>
]]>
  </content>

  </entry>
   </feed>

