<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://pinw.ca/feed" rel="self" type="application/rss+xml"/>
<title>Pintaste</title>
<link>https://pinw.ca</link>
<description>一个关于编程、AI 与生活的个人博客。</description>
<language>zh-CN</language>
<copyright>© pintaste </copyright>
<pubDate>Tue, 10 Nov 2026 16:24:00 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://api.pinw.ca/api/v3/objects/avatar/tnqqbrqd307i7pdkbb.png</url>
    <title>Pintaste</title>
    <link>https://pinw.ca</link>
</image>
<item>
    <title>gas：Grok CLI 多账号切换工具（Grok Account Switch）</title>
    <link>https://pinw.ca/posts/default/gas-grok-account-switch</link>
    <pubDate>Fri, 17 Jul 2026 03:55:28 GMT</pubDate>
    <description>Rich-text content; please visit the original site to view.</description>
    <content:encoded><![CDATA[
      <p>View on the original site: <a href="https://pinw.ca/posts/default/gas-grok-account-switch">https://pinw.ca/posts/default/gas-grok-account-switch</a></p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">159872308649922560</guid>
  <category>post</category>
<category>Default</category>
 </item>
  <item>
    <title>OpenInOtty：在 Finder 工具栏一键用 Otty 打开当前目录</title>
    <link>https://pinw.ca/posts/macos/openinotty-finder-toolbar-otty</link>
    <pubDate>Thu, 16 Jul 2026 12:00:00 GMT</pubDate>
    <description>Rich-text content; please visit the original site to view.</description>
    <content:encoded><![CDATA[
      <p>View on the original site: <a href="https://pinw.ca/posts/macos/openinotty-finder-toolbar-otty">https://pinw.ca/posts/macos/openinotty-finder-toolbar-otty</a></p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">159878867262115840</guid>
  <category>post</category>
<category>macOS</category>
 </item>
  <item>
    <title>把一个 AI Skill 改造成适配自己基础设施的踩坑记录</title>
    <link>https://pinw.ca/posts/default/adapt-session-to-skill-and-blog</link>
    <pubDate>Wed, 01 Jul 2026 03:38:08 GMT</pubDate>
    <description>Rich-text content; please visit the original site to view.</description>
    <content:encoded><![CDATA[
      <p>View on the original site: <a href="https://pinw.ca/posts/default/adapt-session-to-skill-and-blog">https://pinw.ca/posts/default/adapt-session-to-skill-and-blog</a></p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">154069741231673344</guid>
  <category>post</category>
<category>Default</category>
 </item>
  <item>
    <title>Ubuntu 截图后文字识别 (OCR) 复制到剪切板</title>
    <link>https://pinw.ca/posts/ubuntu/screenshot-for-ocr-on-ubuntu</link>
    <pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate>
    <description>
前言 
在 macOS 中习惯了截图后直接识别文字，于是在 Ubuntu 中也想要这个功能。

实</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/ubuntu/screenshot-for-ocr-on-ubuntu'>https://pinw.ca/posts/ubuntu/screenshot-for-ocr-on-ubuntu</a></blockquote>
          
<h2>前言</h2>
<p>在 macOS 中习惯了截图后直接识别文字，于是在 Ubuntu 中也想要这个功能。</p>
<p>实现思路的思路大概是：截图 -&gt; 保存 -&gt; 识别 -&gt; 复制到剪贴板</p>
<p>在一番搜索后，发现了 <a href="https://github.com/tesseract-ocr/tesseract">teseract</a> 一款开源的 OCR 识别引擎，它可以识别多种语言，支持多种格式的图片，满足我们的需要。</p>
<h2>系统信息</h2>
<ul>
<li>Ubuntu 22.10 kinetic</li>
</ul>
<h2>安装过程</h2>
<pre><code class="language-bash"># 安装 tesseract-ocr 识别引擎
sudo apt install tesseract-ocr -y
# 安装 tesseract-ocr 简体中文
sudo apt install tesseract-ocr-chi-sim -y
# 安装 xclip 剪贴板工具
sudo apt install xclip -y
# 安装 gnome-screenshot 截图工具
sudo apt install gnome-screenshot -y
# 安装 imagemagick 图片处理工具
sudo apt install imagemagick -y</code></pre><h2>制作脚本</h2>
<p>在 <code>~/Documents/OCR</code> 目录下创建 <code>ocr.sh</code> 文件，写入以下内容：</p>
<pre><code class="language-bash">#!/bin/env bash 

# 修改 {Username} 为你的用户名
SCR="/home/{Username}/Documents/OCR/temp"

# 截图
gnome-screenshot -a -f $SCR.png

# 图片处理来增强识别率
mogrify -modulate 100,0 -resize 400% $SCR.png 

# 使用 tesseract 识别英文和简体中文 eng+chi_sim
tesseract $SCR.png $SCR &&gt; /dev/null -l eng+chi_sim

# 将识别结果复制到剪贴板
cat $SCR.txt | xclip -selection clipboard

# 删除临时文件
rm $SCR.png $SCR.txt

exit</code></pre><h2>添加快捷键</h2>
<p>在 <code>设置</code> -&gt; <code>键盘</code> -&gt; <code>快捷键</code> 中添加快捷键
名称为 <code>OCR</code>
命令为 <code>bash /home/{Username}/Documents/OCR/ocr.sh</code>
快捷键为 <code>Ctrl + Print Screen</code> </p>
<h2>测试</h2>
<p>使用 <code>Ctrl + Print Screen</code> 截图，然后使用 <code>Ctrl + V</code> 粘贴，就可以看到识别的结果了。</p>
<h2>参考资料</h2>
<p><a href="https://jasonkayzk.github.io/2021/05/07/Linux%E4%B8%8B%E8%87%AA%E5%88%B6ocr%E8%BD%AF%E4%BB%B6/">Linux下自制OCR软件</a></p>

          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/ubuntu/screenshot-for-ocr-on-ubuntu#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303395032535040</guid>
  <category>post</category>
<category>Ubuntu</category>
 </item>
  <item>
    <title>Docker 容器中 PostgreSQL 数据库迁移</title>
    <link>https://pinw.ca/posts/postgresql/postgresql-database-migration-on-docker-container</link>
    <pubDate>Wed, 08 Feb 2023 00:00:00 GMT</pubDate>
    <description>前言
前段时间用 Miniflux 搭建了一个 RSS 服务，其数据库使用的是 PostgreSQL</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/postgresql/postgresql-database-migration-on-docker-container'>https://pinw.ca/posts/postgresql/postgresql-database-migration-on-docker-container</a></blockquote>
          <h2>前言</h2>
<p>前段时间用 Miniflux 搭建了一个 RSS 服务，其数据库使用的是 PostgreSQL, 但运行在 Docker 容器中，现在想把数据库迁移到另一台服务器上的 PostgreSQL 中，然后 Miniflux 使用 Database URL 来连接到迁移后的数据库。</p>
<h2>详细步骤</h2>
<p>1.使用 docker exec 命令进入容器</p>
<pre><code class="language-bash"># 列出所有容器
docker ps -a
# 进入到 postgresql 容器
docker exec -it 68b4 /bin/bash
# 注意：这里的 68b4 是容器的 ID 的前几位</code></pre><p>2.使用 pg_dump 备份数据库</p>
<pre><code class="language-bash"># 备份数据库
pg_dump --username=admin \
--host=0.0.0.0 --port=5432 \
--format=plain --file=/var/lib/postgresql/data/backup.sql db_name
# 注意：这里的 admin 是数据库的用户名，db_name 是数据库的名称

# 查看备份文件
cat /var/lib/postgresql/data/backup.sql
# 退出容器
exit</code></pre><p>3.将备份文件拷贝到宿主机</p>
<pre><code class="language-bash">docker cp 68b4cbec5817:/var/lib/postgresql/data/backup.sql ~/
# 注意：这里的 68b4cbec5817 是容器的 ID, ~/ 是宿主机的当前用户的 home 目录</code></pre><p>4.在新的服务器上创建数据库</p>
<p>这里假设 PostgreSQL 已经安装好了，教程参考 <a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-24-04">How To Install and Use PostgreSQL on Ubuntu 24.04</a></p>
<pre><code class="language-bash"># 切换到 postgres 用户
sudo su postgres
# 进入 postgres CLI
psql
# 创建用户
postgres=# CREATE USER username WITH PASSWORD '*****';
# 这里 postgres=# 是 postgresql 的提示符，不需要输入
# 这里 username 是数据库的用户名，***** 是数据库的密码

# 创建数据库
postgres=# CREATE DATABASE dbname OWNER dbuser;
# 这里 dbname 是数据库的名称，dbuser 是刚刚创建的用户名

# 授权
postgres=# GRANT ALL PRIVILEGES ON DATABASE dbname TO dbuser;
# 这里 dbname 是数据库的名称，dbuser 是刚刚创建的用户名

# 查看数据库
postgres=# \l

# 查看用户
postgres=# \du

# 退出 postgres CLI
postgres=# \q

# 退出 postgres 用户
exit</code></pre><p>5.将备份文件拷贝到新的服务器</p>
<pre><code class="language-bash">scp ~/backup.sql username@ip:/home/username/
# 注意：这里的 username 是新服务器的用户名，ip 是新服务器的 IP 地址，backup.sql 是备份文件的名称</code></pre><p>6.在新的服务器的 PostgreSQL 中导入数据库</p>
<pre><code class="language-bash"># 切换到 postgres 用户
sudo su postgres
# 导入数据库
psql --host=localhost --port=5432 \
--username=miniflux --dbname=minifluxdb \
--file=/home/unbuntu/backup.sql
# 注意：这里的 miniflux 是数据库的用户名，minifluxdb 是数据库的名称，backup.sql 是备份文件的名称</code></pre><p>7.配置新的服务器的 PostgreSQL</p>
<pre><code class="language-bash"># 切换到 postgres 用户
sudo su postgres

# 进入 postgres CLI
psql

# 显示 hba_file 文件路径
SHOW hba_file;

# 示例输出
/etc/postgresql/14/main/pg_hba.conf 

# 退出 postgres CLI
\q

# 编辑 hba_file 文件
vim /etc/postgresql/14/main/pg_hba.conf

# 修改监听端口
host    all     all     0.0.0.0/0     md5
host    all     all     ::/0          md5

# 退出保存 
ESC + :wq

# 编辑 postgresql.conf 文件
vim /etc/postgresql/14/main/postgresql.conf

# 修改监听地址
将      listen_address = 'localhost' 
修改为   listen_address = '*'

# 退出保存
ESC + :wq

# 重启 PostgreSQL 服务
systemctl restart postgresql

# 退出 postgres 用户
exit</code></pre><p>8.修改 Miniflux 的 Database URL
在 Miniflux 的 compose 文件中修改数据库的 URL</p>
<pre><code class="language-yaml"># 数据库 URL Schema
postgres://DatabaseUserName:DatabasePassword@HostIP:5432/DatabaseName?sslmode=disable</code></pre><p>9.重启 Miniflux 服务</p>
<pre><code class="language-bash">docker compose up -d</code></pre>
          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/postgresql/postgresql-database-migration-on-docker-container#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303398295703552</guid>
  <category>post</category>
<category>PostgreSQL</category>
 </item>
  <item>
    <title>甲骨文云 Permission denied (publickey) 问题</title>
    <link>https://pinw.ca/posts/oracle-cloud/oracle-cloud-recovering-ssh</link>
    <pubDate>Sat, 04 Feb 2023 00:00:00 GMT</pubDate>
    <description>问题描述

在尝试连接甲骨文云主机实例时，当时使用的 SSH 密钥损坏或被丢失了，导致无法连接到云主</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/oracle-cloud/oracle-cloud-recovering-ssh'>https://pinw.ca/posts/oracle-cloud/oracle-cloud-recovering-ssh</a></blockquote>
          <h2>问题描述</h2>
<p>在尝试连接甲骨文云主机实例时，当时使用的 SSH 密钥损坏或被丢失了，导致无法连接到云主机。</p>
<p>错误信息如下：</p>
<pre><code class="language-bash">Permission denied (publickey).</code></pre><h2>解决方法</h2>
<p>通过分离实例的引导卷，然后将其挂载到新的实例上，然后添加新的 SSH 密钥，最后将引导卷重新挂载到原来的实例上。</p>
<blockquote>
<p><strong>注意</strong>：本文截图已遗失（原图床 cdn.staticaly.com 已停止服务），但文字步骤完整可用。Oracle Cloud 控制台界面可能已有变化，请以实际界面为准。</p>
</blockquote>
<h2>详细步骤</h2>
<h3>1.停止无法连接的实例</h3>
<p>从控制台访问实例详细信息页面，然后点击停止按钮。</p>
<h3>2.分离引导卷</h3>
<p>在实例详细信息页面的下方，找到引导卷，找到相应的引导卷，点击三个点，选择分离引导卷。</p>
<h3>3.挂载引导卷到可以访问的实例上</h3>
<p>注意：这里的要挂载引导卷实例必须是可以访问（连接）的，后面会连接到这个实例上，来挂载引导卷。</p>
<p>在控制台上，找到可以访问的实例，找到附加的块存储卷，点击附加块存储卷，选择刚才分离的引导卷，点击附加。</p>
<h3>4.使用 iSCSI 命令将引导卷挂载到运行的实例上</h3>
<p>使用 SSH 连接到可以访问的实例上，然后使用 iSCSI 命令将引导卷挂载到运行的实例上。</p>
<p>iSCSI 命令可以在附加的块存储卷页面，找到刚才挂载的引导卷，点击三个点，就可以看到 iSCSI 命令。</p>
<p>展开 Linux 选项卡，复制 Linux iSCSI 连接命令，然后在已经连接的实例上执行。</p>
<h3>5.修改 authorized_keys 文件</h3>
<p>sudo su -
fdisk -l
mount /dev/sdb1 /mnt
sudo vi /home/opc/.ssh/authorized_keys
sudo vi /home/ubuntu/.ssh/authorized_keys
umount /mnt</p>
<h3>6.使用 iSCSI 命令将引导卷卸载并分离引导卷</h3>
<p>复制 Linux iSCSI 断开命令，然后在已经连接的实例上执行。</p>
<p>使用 fdisk -l 命令查看引导卷是否已经卸载，如果已经卸载，就可以分离引导卷了。</p>
<h3>7.将引导卷挂载到原来的实例上</h3>
<p>在控制台上，找到原来的实例，找到附加的块存储卷，点击附加块存储卷，选择刚才分离的引导卷，点击附加。</p>
<p>等待引导卷挂载完成，点击实例的启动按钮，启动实例，就可以使用新的 SSH 密钥登录实例了。</p>
<h2>相关链接</h2>
<p><a href="https://blogs.oracle.com/cloud-infrastructure/post/recovering-opc-user-ssh-key-on-oracle-cloud-infrastructure">Recovering opc user SSH Key on Oracle Cloud Infrastructure</a></p>

          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/oracle-cloud/oracle-cloud-recovering-ssh#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303397624614912</guid>
  <category>post</category>
<category>Oracle Cloud</category>
 </item>
  <item>
    <title>在 1 亿个数中取出最大的 1000 个数？</title>
    <link>https://pinw.ca/posts/algorithm/top-k-question</link>
    <pubDate>Wed, 18 Jan 2023 00:00:00 GMT</pubDate>
    <description>
起因
上个月的一场面试中，面试官问了我这个问题，当时没能很好的回答出来。后来认真的思考了下这个问题</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/algorithm/top-k-question'>https://pinw.ca/posts/algorithm/top-k-question</a></blockquote>
          
<h2>起因</h2>
<p>上个月的一场面试中，面试官问了我这个问题，当时没能很好的回答出来。后来认真的思考了下这个问题，用此文记录下来。</p>
<h2>排序算法</h2>
<p>符合第一直觉的回答应该是<code>快速排序</code>，在系统的学过排序算法后，我们都知道快排是一种很快的排序方法。我们先对数据进行排序，然后再取出其中前 1000 个数。不过你要是真的在面试中只那么回答的话，那么基本也无缘后面的面试了。这种思路在输入的数据规模很小时，不失为一种便捷的方法，但是当数据规模扩大时（题目中的 1 亿），其对运行的内存要求也将增大。在快排中，其时间复杂度为 O(n log n)，要同时对 1 亿整数排序，需要将其读入内存中，以每个 int 类型占 4 字节为例，1 亿整数则需要占用 400MB 的内存。虽然现代的计算机可用内存远远大于400MB，但当数据规模再次扩大到 10 亿甚至是 100 亿时，其对内存的要求则上升到了 4GB 和 40GB，算法的可用性 (availability) 和扩展性 (scalability) 将受到严重的影响。同时，在这种情况下还需要做全排序的话，就只能借助<code>外部排序</code>、<code>位图排序</code>、<code>基数排序</code>、<code>桶排序</code> 等算法来解决内存不足的问题了。同时，题目中只要求取 top 1000 的数据，对所有元素排序很明显是不必要的。</p>
<p>我写一个简单的演示程序，代码如下：</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;  
#include &lt;algorithm&gt;  
#include &lt;random&gt;  
#include &lt;chrono&gt;  
  
using namespace std;  
  
#define MAX INT32_MAX  
#define MIN INT32_MIN  
#define SIZE 100000000 // 100 million  
#define K 1000  
  
int main() {   
    auto start = chrono::high_resolution_clock::now();  

	  // Create a random number generator 
    random_device rd;  
    mt19937 gen(rd());  
    uniform_int_distribution&lt;&gt; dis(MIN, MAX);  
  
    // Create an array of random numbers  
    int *arr = new int[SIZE];  
    for (int i = 0; i &lt; SIZE; i++) {  
        arr[i] = dis(gen);  
    }  
  
    auto end = chrono::high_resolution_clock::now();  
    chrono::duration&lt;double&gt; diff = end - start;  
    cout &lt;&lt; "Time taken to create array: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;  
  

    start = chrono::high_resolution_clock::now();  
	// sorting via std::sort() - optimized quicksort
    sort(arr, arr + SIZE);  
  
    end = chrono::high_resolution_clock::now();  
    diff = end - start;  
    // the arr[0]~arr[k-1] is the top k elements in arr.
    cout &lt;&lt; "Time taken to get top K elements: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;
  
    free(arr);  
    return 0;  
}</code></pre><pre><code class="language-bash"># CPU: i7-8700k
# Memory: 16 GB
# OS: Ubuntu 22.10 Kinetic
Time taken to create array: 2.13481 s
Time taken to get top K elements: 22.3849 s</code></pre><h2>局部淘汰法</h2>
<p>为了避免上面的算法中的不必要排序，我们可以选取出待排序数组中的前 k (= 1000) 个元素构建一个最小堆。然后从 k + 1 开始与容器中的最小元素 m 相比，如果大于 m，则替换 m 并重新调整最小堆。</p>
<p>其算法如下：</p>
<ol>
<li>选取前K个元素，构建一个最小堆。</li>
<li>从第 K+1 个元素开始，与堆顶元素比较，如果比堆顶元素大，则替换堆顶元素，并调整堆。</li>
<li>重复步骤 2，直到数组遍历完毕。</li>
<li>堆中的元素即为前K个最大元素。</li>
</ol>
<p>其代码如下：</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;  
#include &lt;algorithm&gt;  
#include &lt;random&gt;  
#include &lt;chrono&gt;  
  
using namespace std;  
  
#define MAX INT32_MAX  
#define MIN INT32_MIN  
#define SIZE 100000000 // 100 million  
#define K 1000  
  
int main() {  
    // Create a random number generator  
    auto start = chrono::high_resolution_clock::now();  
  
    random_device rd;  
    mt19937 gen(rd());  
    uniform_int_distribution&lt;&gt; dis(MIN, MAX);  
  
    // Create an array of random numbers  
    int *arr = new int[SIZE];  
    for (int i = 0; i &lt; SIZE; i++) {  
        arr[i] = dis(gen);  
    }  
  
    auto end = chrono::high_resolution_clock::now();  
    chrono::duration&lt;double&gt; diff = end - start;  
    cout &lt;&lt; "Time taken to create array: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;  
  
    // clock start  
    start = chrono::high_resolution_clock::now();  

	// generate a min-heap with first k elements
    int *heap = new int[K];  
    for (int i = 0; i &lt; K; i++) {  
        heap[i] = arr[i];  
    }
    make_heap(heap, heap + K, greater&lt;&gt;());  
    for (int i = K; i &lt; SIZE; i++) {  
        if (arr[i] &gt; heap[0]) {  
            pop_heap(heap, heap + K, greater&lt;&gt;());  
            heap[K - 1] = arr[i];  
            push_heap(heap, heap + K, greater&lt;&gt;());  
        }  
    }  
  
    // clock end  
    end = chrono::high_resolution_clock::now();  
    diff = end - start;  
    cout &lt;&lt; "Time taken to get top K elements: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;  
  
    free(arr);  
    free(heap);  
    return 0;  
}</code></pre><pre><code class="language-bash">Time taken to create array: 2.18854 s
Time taken to get top K elements: 0.178133 s</code></pre><h2>C++ STL <code>set</code></h2>
<p>我们知道，标准库中的 set 底层实现是一棵红黑树（平衡二叉树），在这里我想到了是不是可以借助 set 容器来取 top k 元素呢。如果允许重复元素的话，则使用 <code>multiset</code> 来代替 <code>set</code>.</p>
<p>代码如下：</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;  
#include &lt;random&gt;  
#include &lt;chrono&gt;  
#include &lt;set&gt;  
  
using namespace std;  
  
#define MAX INT32_MAX  
#define MIN INT32_MIN  
#define SIZE 100000000 // 100 million  
#define K 1000  
  
int main() {  
    // Create a random number generator  
    auto start = chrono::high_resolution_clock::now();  
  
    random_device rd;  
    mt19937 gen(rd());  
    uniform_int_distribution&lt;&gt; dis(MIN, MAX);  
  
    // Create an array of random numbers  
    int *arr = new int[SIZE];  
    for (int i = 0; i &lt; SIZE; i++) {  
        arr[i] = dis(gen);  
    }  
  
    auto end = chrono::high_resolution_clock::now();  
    chrono::duration&lt;double&gt; diff = end - start;  
    cout &lt;&lt; "Time taken to create array: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;  
  
    // clock start  
    start = chrono::high_resolution_clock::now();  
  
    // Create a set of K the largest numbers  
    set&lt;int&gt; s;  
    for (int i = 0; i &lt; SIZE; i++) {  
        if (s.size() &lt; K) {  
            s.insert(arr[i]);  
        } else {  
            if (arr[i] &gt; *s.begin()) {  
                s.erase(s.begin());  
                s.insert(arr[i]);  
            }  
        }  
    }  
  
    // clock end  
    end = chrono::high_resolution_clock::now();  
    diff = end - start;  
    cout &lt;&lt; "Time taken to get top K elements: " &lt;&lt; diff.count() &lt;&lt; " s" &lt;&lt; endl;  
  
    // Print the set in reverse order  
//    for (auto it = s.rbegin(); it != s.rend(); it++) {  
//        cout &lt;&lt; *it &lt;&lt; endl;  
//    }
  
    free(arr);  
    return 0;  
}</code></pre><pre><code class="language-bash">Time taken to create array: 2.16181 s
Time taken to get top K elements: 1.68677 s</code></pre><h2>总结</h2>
<p>通过上面的实验，我们可以看到，使用局部淘汰的思想和最小堆的数据结构来取 top k 元素的效率是最高的，其次是使用 set 容器，最后是使用排序算法。</p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/super828/article/details/108890489">1 亿个数中找出最大的100个数 (top K问题) - CSDN</a></p>

          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/algorithm/top-k-question#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303391152803840</guid>
  <category>post</category>
<category>Algorithm</category>
 </item>
  <item>
    <title>使用 Miniflux 搭建自己的 RSS 服务</title>
    <link>https://pinw.ca/posts/docker/self-hosted-rss</link>
    <pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate>
    <description>前言
一直使用 Inoreader 作为自己的 RSS 服务，但是最近发现 Inoreader 访问</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/docker/self-hosted-rss'>https://pinw.ca/posts/docker/self-hosted-rss</a></blockquote>
          <h2>前言</h2>
<p>一直使用 Inoreader 作为自己的 RSS 服务，但是最近发现 Inoreader 访问不是很稳定，有些 RSS 源的内容无法正常显示，正好看到 <a href="https://soulteary.com/2020/02/02/build-your-own-miniflux-rss-service-with-docker-and-traefik-v2.html">使用 Docker 和 Traefik v2 搭建 RSS 服务 - 苏洋博客</a>用 Docker 和 Miniflux 搭建了自己的 RSS 服务，正好手上有一台闲置的白嫖云(甲骨文云)服务器，于是便动手实践了起来。</p>
<p>一些相关的工具和服务：</p>
<p><a href="https://miniflux.app/">Miniflux</a>: Frédéric Guillot 开发用 Go 语言开发维护的一个的开源极简风格的 RSS 服务，支持浏览器访问，支持 Telegram 推送，可以通过 Fever API 和与 Reeder(macOS)、FeedMe(Android) 等客户端同步订阅。</p>
<p><a href="https://docs.rsshub.app/">RSSHub</a>: <a href="https://diygod.me/">DIYGod</a> 开发维护的一个 RSS 生成器，可以将一些网站的内容转换成 RSS 订阅源，配合 Miniflux 食用更佳。</p>
<p><a href="https://github.com/DIYgod/RSSHub-Radar">RSSHub Radar</a>: 同样由 <a href="https://diygod.me/">DIYGod</a> 开发维护。是一款 Chrome 插件，作为 RSSHub 的补充，可以将检测当前页面的 RSS 订阅源并一键添加到 Miniflux 中。</p>
<p><a href="https://core.telegram.org/bots">Telegram Bot</a>: Telegram 的机器人服务，可以通过 Telegram Bot API 来与 Telegram 交互。</p>
<p><a href="https://github.com/timqian/chinese-independent-blogs">中文独立博客列表</a>: 由 <a href="https://timqian.com/">timqian</a> 维护的一个 Github 仓库，收集了一些优质的中文独立博客的 RSS 订阅源。</p>
<p><a href="https://kill-the-newsletter.com/">Kill the Newsletter</a>: 可以将 Email Newsletter 转换成 RSS 订阅源的服务。</p>
<p><a href="https://play.google.com/store/apps/details?id=com.seazon.feedme">FeedMe - Android</a>: Android RSS App</p>
<p><a href="https://reederapp.com/">Reeder - macOS</a>: macOS RSS App</p>
<h2>准备工作</h2>
<ul>
<li>一个域名 (用于支持 HTTPS 访问，可使用二级域名)</li>
<li>一台服务器 (本文用的甲骨文 x86 VPS，配置 1C1G)</li>
</ul>
<h2>系统环境</h2>
<ul>
<li>Ubuntu Server 24.04 LTS (x86_64)</li>
</ul>
<h2>安装 Docker &amp; Docker Compose</h2>
<p>为了方便管理，我们使用 Docker &amp; Docker Compose 来部署 Miniflux 和 RSSHub 服务。</p>
<p>首先，我们需要安装 Docker 和 Docker Compose.</p>
<p>更新软件源</p>
<pre><code class="language-bash">sudo apt update && sudo apt full-upgrade -y</code></pre><p>安装 Docker</p>
<pre><code class="language-bash">curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh</code></pre><p>安装 Docker Compose</p>
<pre><code class="language-bash">sudo apt install docker-compose-plugin -y</code></pre><p>测试是否安装成功</p>
<pre><code class="language-bash"># 查看 Docker 版本
docker --version
# - 输出
Docker version 27.0.0, build e92dd87

# 查看 Docker Compose 版本
docker compose --version
# - 输出
Docker Compose version v2.27.0</code></pre><p>将当前用户加入 docker 用户组 (省去 sudo 前缀)</p>
<pre><code class="language-bash"># 将当前用户加入 docker 用户组
sudo usermod -aG docker $USER

# 修改守护进程绑定的套接字的权限，使其能够被 docker 分组访问
sudo chmod a+rw /var/run/docker.sock

# 重启 Docker 服务
sudo systemctl restart docker

# 测试是否成功
docker info

# 如果没有提示权限不足，说明成功</code></pre><p>设置 Docker 开机启动</p>
<pre><code class="language-bash">sudo systemctl enable docker</code></pre><p>在安装 Docker &amp; Docker Compose 之后，我们就可以开始安装 Miniflux 了。</p>
<h2>安装 Miniflux</h2>
<p>在安装 Miniflux 之前，我们需要在同目录下创建 .env 文件，用于存放环境变量。
例如： MiniFlux 的用户名、密码、数据库用户名、数据库密码等。</p>
<pre><code class="language-bash"># - 创建目录
mkdir -p ~/docker/miniflux

# - 创建 env 文件
vim ~/docker/miniflux/.env
# Esc + :wq 退出 vim 并保存</code></pre><p><strong>注意：</strong> 请将下面的配置信息替换成你自己的配置信息。</p>
<pre><code class="language-bash"># .env
# 数据库用户名
POSTGRES_USER=erica
# 数据库密码
POSTGRES_PASSWORD=erica1234
# Miniflux 用户名
MINIFLUX_USERNAME=erica
# Miniflux 密码
MINIFLUX_PASSWORD=rss1234
# Miniflux 访问地址
MINIFLUX_BASE_URL=https://miniflux.domain.com</code></pre><p>在创建完 .env 文件之后，我们需要创建 compose.yml 文件，用于启动 Miniflux。</p>
<pre><code class="language-bash"># 创建配置文件
vim ~/docker/miniflux/compose.yml</code></pre><pre><code class="language-yaml"># compose.yml
services:
  miniflux:
    image: miniflux/miniflux:latest
    container_name: miniflux
    restart: always
    ports:
      - "17025:8080"
    depends_on:
      - db
    environment:
      - POLLING_FREQUENCY=15
      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/$miniflux?sslmode=disable
      - BASE_URL=${MINIFLUX_BASE_URL}
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=${MINIFLUX_USERNAME}
      - ADMIN_PASSWORD=${MINIFLUX_PASSWORD}
    healthcheck:
      test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
  db:
    image: postgres:latest
    container_name: postgres
    restart: always
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - miniflux-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s
volumes:
  miniflux-db:

# Esc + :wq 退出 vim 并保存</code></pre><p>其中，我们需要注意的是：</p>
<ul>
<li><code>ports</code>：17025 端口是容器外部的端口，8080 则是容器内部的端口，可以根据自己的需要进行修改。</li>
<li><code>POLLING_FREQUENCY=15</code> ：Miniflux 的轮询频率，这里我们设置为 15 分钟。</li>
</ul>
<p>如果使用自己的数据库，可以将 <code>db</code> 服务注释掉，然后将 <code>DATABASE_URL</code> 的值修改为自己的数据库地址。</p>
<p><strong>启动 Miniflux 容器</strong></p>
<pre><code class="language-bash">docker compose up -d</code></pre><p><strong>测试是否安装成功</strong>
浏览器访问 Miniflux 的地址，如果能够正常访问，说明 Miniflux 已经安装成功。</p>
<pre><code class="language-bash"># - 服务器中查看 IP 地址
curl -s https://icanhazip.com</code></pre><p>在浏览器中输入 <code>http://&lt;IP&gt;:17025</code>，如果能够正常访问，说明 Miniflux 已经安装成功。</p>
<p></p>
<p>如果服务无法正常访问，请检查防火墙设置和云主机安全组设置。</p>
<p>参考资料：<a href="https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm">甲骨文网络安全组设置 - EN</a></p>
<h4></h4>
<h2>安装 RRSHub</h2>
<p>然后我们继续安装 RSSHub，最后再用 Nginx 来反向代理 RSSHub 和 Miniflux 服务。</p>
<pre><code class="language-bash"># 创建目录
mkdir -p ~/docker/rsshub && cd ~/docker/rsshub
# 下载 compose.yml 文件
wget https://raw.githubusercontent.com/DIYgod/RSSHub/master/compose.yml
# 检查有无需要修改的配置
vim compose.yml
# 创建 volume 持久化 Redis 缓存
docker volume create redis-data
# 启动 RSSHub，不带 -d 参数则会在前台输出日志方便检查
docker compose up -d
# 查看 RSSHub 日志
docker compose logs -f</code></pre><p><strong>测试是否安装成功</strong>
浏览器访问 RSSHub 的地址，如果能够正常访问，说明 RSSHub 已经安装成功。</p>
<pre><code class="language-bash"># - 获取 IP 地址
curl -s https://icanhazip.com</code></pre><p>在浏览器中输入 <code>http://&lt;IP&gt;:1200</code>，如果能够正常访问，说明 RSSHub 已经安装成功。</p>
<p></p>
<h2>安装 Nginx Proxy Manager (NPM)</h2>
<p>在安装启动 MiniFlux 和 RSSHub 之后，我们需要用 Nginx 来反向代理这两个服务，这样我们就可以通过域名来访问这两个服务了同时对服务进行 SSL 加密。</p>
<pre><code class="language-bash"># 创建目录
mkdir -p ~/docker/npm && cd ~/docker/npm

# 创建配置文件
vim ~/docker/npm/compose.yml</code></pre><p><strong>编辑 compose.yml 文件</strong></p>
<pre><code class="language-yaml"># compose.yml
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt</code></pre><p><strong>启动 Nginx Proxy Manager 容器</strong></p>
<pre><code class="language-bash">docker compose up -d</code></pre><p><strong>测试是否安装成功</strong>
浏览器访问 NPM 的地址，如果能够正常访问，说明 NPM 已经安装成功。</p>
<pre><code class="language-bash"># - 获取 IP 地址
curl -s https://icanhazip.com</code></pre><p>在浏览器中输入 <code>http://&lt;IP&gt;:81</code>，如果能够正常访问，说明 NPM 已经安装成功。</p>
<p></p>
<h2>设置 NPM 反向代理</h2>
<p>登录 <code>http://&lt;IP&gt;:81</code>，默认的用户名 <code>admin@example.com</code> 密码 <code>changeme</code> </p>
<p><strong>登录后请务必修改默认邮箱和密码，邮箱将用于接收安全证书更新通知。</strong></p>
<h3>获取 Docker 容器的 IP</h3>
<p>在我们设置反向代理之前，我们需要先获取 Docker 容器的 IP。</p>
<p>在终端中输入以下命令，获取 Docker 容器的 IP：</p>
<pre><code class="language-bash"># 输入以下命令
ip addr show docker0
# 输出结果如下
3: docker0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:a6:c8:15:72 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever</code></pre><p>上面的 <code>172.17.0.1</code> 就是 Docker 容器的 IP。</p>
<p>接下来我们接着上面的步骤，设置 NPM 的反向代理。</p>
<h3>添加 Miniflux 反向代理</h3>
<ol>
<li>在 Dashboard 页面中，点击 <code>0 Proxy Host</code> 卡片，添加反向代理。</li>
<li>在 <code>Proxy Host</code> 页面中，点击 <code>Add Proxy Host</code> 按钮，弹出 <code>New Proxy Host</code> 窗口。</li>
<li>在 <code>New Proxy Host</code> 窗口中，填写以下内容：<ul>
<li><code>Domain Names</code>：填写你的域名，例如 <code>miniflux.domain.com</code>。</li>
<li><code>Scheme</code>：默认选择 <code>HTTP</code>。</li>
<li><code>Forward Hostname / IP</code>：填写 Docker 容器的 IP，例如 <code>172.17.0.1</code>。</li>
<li><code>Forward Port</code>：填写 <code>17025</code>。</li>
</ul>
</li>
<li>切换到 <code>SSL</code> 选项卡，在 <code>SSL Certificate</code> 选项中，选择 <code>Request a new SSL Certificate with Let&#39;s Encrypt</code>。<ul>
<li>打开 <code>Force SSL</code> 选项。</li>
<li>打开 <code>HTTP/2 Support</code> 选项。</li>
<li>同意 <code>Let&#39;s Encrypt Terms of Service</code>。</li>
</ul>
</li>
<li>点击 <code>Save</code> 按钮，保存设置。(这里可能需要等待一段时间，等待证书生成)</li>
<li>最后，点击自己刚刚添加的 <code>miniflux.domain.com</code> 链接，如果能够正常访问，说明 NPM 反向代理设置成功。</li>
</ol>
<h3>添加 RSSHub 反向代理</h3>
<p>重复上面的步骤，添加 RSSHub 反向代理，填写以下内容：</p>
<ul>
<li><code>Domain Names</code>：填写你的域名，例如 <code>rsshub.domain.com</code>。</li>
<li><code>Scheme</code>：默认选择 <code>HTTP</code>。</li>
<li><code>Forward Hostname / IP</code>：填写 &#39;Docker 容器的 IP&#39;。</li>
<li><code>Forward Port</code>：填写 <code>1200</code>。</li>
<li>切换到 <code>SSL</code> 选项卡，在 <code>SSL Certificate</code> 选项中，选择 <code>Request a new SSL Certificate with Let&#39;s Encrypt</code>。<ul>
<li>打开 <code>Force SSL</code> 选项。</li>
<li>打开 <code>HTTP/2 Support</code> 选项。</li>
<li><code>Email Address for Let&#39;s Encrypt</code>：填写你的邮箱地址。</li>
<li>同意 <code>Let&#39;s Encrypt Terms of Service</code>。</li>
</ul>
</li>
<li>点击 <code>Save</code> 按钮，保存设置。(这里可能需要等待一段时间，等待证书生成)</li>
<li>最后，点击自己刚刚添加的 <code>rsshub.domain.com</code> 链接，如果能够正常访问，说明 NPM 反向代理设置成功。</li>
</ul>
<h3>验证反向代理设置是否成功</h3>
<p>✅ 通过 <code>https://miniflux.domain.com</code> 访问 Miniflux，如果能够正常访问，说明 NPM 反向代理设置成功。</p>
<p>✅ 通过 <code>https://rsshub.domain.com</code> 访问 RSSHub，如果能够正常访问，说明 NPM 反向代理设置成功。</p>
<p>✅ 设置 NPM 反向代理后建议在云主机安全组中关闭对应端口的访问权限，以防止被恶意访问。</p>
<h3>(可选) 使用 Caddy 反向代理</h3>
<p>如果你不想使用 NPM 来反向代理，也可以使用 <a href="https://caddyserver.com/">Caddy</a> 来反向代理。</p>
<p>配置文件如下：</p>
<pre><code class="language-bash">vim caddyfile
miniflux.example.org
reverse_proxy miniflux:17025</code></pre><pre><code class="language-yaml">services:
  caddy:
    image: caddy:2
    container_name: caddy
    depends_on:
      - miniflux
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - $PWD/Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
  miniflux:
    image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
    container_name: miniflux
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=test123
      - BASE_URL=https://miniflux.example.org
  db:
    image: postgres:15
    container_name: postgres
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=secret
    volumes:
      - miniflux-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s
volumes:
  miniflux-db:
  caddy_data:
  caddy_config:</code></pre><p>Source: <a href="https://github.com/miniflux/v2/blob/main/contrib/docker-compose/caddy.yml">caddy.yml - github</a></p>
<h3>(可选) 使用 traefik 反向代理</h3>
<p>如果你不想使用 NPM 来反向代理，<a href="https://traefik.io/">traefik</a> 也是一个不错的选择。</p>
<p>配置文件如下：</p>
<pre><code class="language-yaml">services:
  traefik:
    image: "traefik:v2.3"
    container_name: traefik
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    depends_on:
      - miniflux
    ports:
      - "443:443"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
  miniflux:
    image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
    container_name: miniflux
    depends_on:
      - db
    expose:
      - "8080"
    environment:
      - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=test123
      - BASE_URL=https://miniflux.example.org
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.miniflux.rule=Host(`miniflux.example.org`)"
      - "traefik.http.routers.miniflux.entrypoints=websecure"
      - "traefik.http.routers.miniflux.tls.certresolver=myresolver"
  db:
    image: postgres:15
    container_name: postgres
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=secret
    volumes:
      - miniflux-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux"]
      interval: 10s
      start_period: 30s
volumes:
  miniflux-db:</code></pre><p>Source: <a href="https://github.com/miniflux/v2/blob/main/contrib/docker-compose/traefik.yml">traefik.yml - github</a></p>
<h2>使用 Miniflux 订阅 RSS</h2>
<p>语言设置：登录 Miniflux 后，点击 <code>Settings</code>，在 <code>Language</code> 选项中，选择 <code>简体中文</code>，点击页面最底的 <code>Save</code> 按钮，保存设置。</p>
<p>文章排序：在同一个页面，在 <code>文章排序</code> 标签下，选择 <code>新-&gt;旧</code>，点击页面最底的 <code>Save</code> 按钮，保存设置。</p>
<p>添加新源：在网页上方，找到 <code>源(+)</code> 标签后，点击 <code>+</code> 标签，输入 <code>https://pinw.ca/zhs/index.xml</code>, 点击蓝色 <code>查找源</code> 按钮，稍等片刻，Miniflux 会自动获取该 RSS 的信息并添加到订阅列表中。</p>
<p><em>注意：上面的 <code>https://pinw.ca/zhs/index.xml</code> 为 <code>本人博客</code> 的 RSS 地址，欢迎订阅。</em></p>
<h2>使用 RSSHub 订阅 RSS</h2>
<p>虽然 Miniflux 的体验很好，但是它只能订阅 RSS 源，无法订阅其他类型的内容，例如 Bilibili、YouTube 等。这时候，我们就需要 RSSHub 来帮助我们订阅其他类型的内容。</p>
<p><strong>订阅 Bilibili</strong>
打开 <a href="https://docs.rsshub.app/">RSSHub</a>，在搜索框中输入 <code>bilibili</code>，点击 <code>Bilibili-&gt;UP主动态</code>。</p>
<p><strong>订阅 YouTube</strong>
打开 <a href="https://docs.rsshub.app/">RSSHub</a>，在搜索框中输入 <code>youtube</code>，点击 <code>YouTube-&gt;用户</code>。</p>
<p><em>记得将 rsshub.domain.com 替换成刚刚为刚刚设置的反向代理域名</em></p>
<h2>使用 RSSHub Radar 订阅 RSS</h2>
<p>虽然 RSSHub 提供了很多路由规则，但是每次都要打开 <a href="https://docs.rsshub.app/">RSSHub</a> 来搜索查找对应的路由很不方便。这时候，我们就需要一个工具来帮助我们发现 RSSHub 提供的路由。</p>
<p><a href="https://github.com/DIYgod/RSSHub-Radar">RSSHub Radar</a> 是RSSHub 作者 <a href="https://diygod.me/">DIYgod</a> 开发的一个 Chrome 插件，可以帮助我们快速发现当前网站的 RSS 路由规则。</p>
<ul>
<li>点击 <a href="https://chrome.google.com/webstore/detail/rsshub-radar/kefjpfngnndepjbopdmoebkipbgkggaa">RSSHub Radar - Chrome Extension</a> 下载安装 RSSHub Radar 插件。</li>
<li>点击 RSSHub Radar 插件图标，点击 <code>设置</code> 按钮，进入设置页面。</li>
<li>在<code>常规</code>设置中，填写<code>自定义 RSSHub 域名</code> 为 <code>rsshub.domain.com</code></li>
<li>在<code>一键订阅</code>设置中，勾选<code>开启</code>填写<code>自定义 Miniflux 域名</code> 为 <code>miniflux.domain.com</code>。</li>
</ul>
<p></p>
<p>接下来，我们就可以在任意网站中使用 RSSHub Radar 插件了。</p>
<h2>使用 Fever API 同步 RSS</h2>
<p>Fever API 是一个 RSS 同步协议，可以通过 Fever API 同步 (只读不写) RSS 到其他 RSS 阅读器，比如 FeedMe 等。</p>
<ul>
<li>点击 <code>设置</code> 按钮，进入设置页面。</li>
<li>点击 <code>集成</code> 按钮，进入集成页面。</li>
<li>勾选 <code>启用 Fever API</code> 选项并填写 <code>Fever 用户名</code> 和 <code>Fever 密码</code>。</li>
<li>点击 <code>更新</code> 按钮，保存设置。</li>
</ul>
<p></p>
<h2>在 Android 平台阅读</h2>
<p><a href="https://play.google.com/store/apps/details?id=com.seazon.feedme">FeedMe</a> 是一款 Android 平台的 RSS 阅读器，可以使用 Fever API 同步 Miniflux 的 RSS。</p>
<h2>Telegram Bot 推送设置</h2>
<ol>
<li>在 Telegram 中搜索 <code>@BotFather</code>，并发送 <code>/start</code> 按钮，进入 BotFather。</li>
<li>发送 <code>/newbot</code> 按钮，进入创建 Bot。</li>
<li>输入 Bot 的名称，例如 <code>RSS Notification</code>。</li>
<li>输入 Bot 的用户名，例如 <code>123wasdRSSPushbot</code>。</li>
<li>复制 Bot 的 Token (机器人令牌)，例如 <code>123456789:ABCDEF1234567890ABCDEF1234567890ABC</code> 之后会用到。</li>
<li>在 Telegram 中搜索 <code>@getidsbot</code>，并发送 <code>/start</code> 按钮，获取 Chat ID。</li>
<li>复制 Chat ID (聊天 ID)，例如 <code>1234567890</code> 之后会用到。</li>
<li>打开登录 Miniflux，点击 <code>设置</code> 标签，进入设置页面。</li>
<li>点击 <code>集成</code> 标签，进入集成页面。 </li>
<li>将页面滚动到最下面，找到勾选 <code>将新文章推送到 Telegram</code> 选项并填写刚刚复制的 <code>机器人令牌</code> 和 <code>Chat ID</code>。</li>
<li>点击 <code>更新</code> 按钮，保存设置。</li>
</ol>
<p>之后，每当 Miniflux 有新文章时，都会通过 Telegram Bot 推送到你的 Telegram 账号。</p>
<p><em>注意：如果需要推送到群组或者频道, 需要将机器人添加到群组或者频道中，然后在群组或者频道中发送 <code>/start</code> ，获取当前 Chat ID。</em></p>
<h2>总结</h2>
<p>经过愉快的一番折腾，我们终于把可以畅快的使用自己的 RSS 服务了。</p>
<p>enjoy it!</p>
<h2>参考</h2>
<ul>
<li><a href="https://docs.rsshub.app/">RSSHub Docs</a></li>
<li><a href="https://docs.docker.com/get-started/">Getting Started - Docker</a></li>
<li><a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">Docker Compose - Docker</a></li>
<li><a href="https://miniflux.app/docs/index.html">Documentation - Miniflux</a></li>
<li><a href="https://github.com/DIYgod/RSSHub-Radar">RSSHub Radar - Github</a></li>
<li><a href="https://soulteary.com/2020/02/02/build-your-own-miniflux-rss-service-with-docker-and-traefik-v2.html">使用 Docker 和 Traefik v2 搭建 RSS 服务（Miniflux）</a></li>
</ul>

          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/docker/self-hosted-rss#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303391719034880</guid>
  <category>post</category>
<category>Docker</category>
 </item>
  <item>
    <title>使用 SSH 连接虚拟机中的 Ubuntu Server</title>
    <link>https://pinw.ca/posts/ubuntu/how-to-ssh-into-virtualbox-ubuntu</link>
    <pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate>
    <description>本文简单的介绍了如何通过 SSH 连接到 VirtualBox 中的 Ubuntu Server 来</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/ubuntu/how-to-ssh-into-virtualbox-ubuntu'>https://pinw.ca/posts/ubuntu/how-to-ssh-into-virtualbox-ubuntu</a></blockquote>
          <p>本文简单的介绍了如何通过 SSH 连接到 VirtualBox 中的 Ubuntu Server 来进行日常的开发工作。</p>
<p>准备工作</p>
<ul>
<li>安装好的 <a href="https://www.virtualbox.org/wiki/Downloads">VirtualBox</a> 软件</li>
<li>创建好的 <a href="https://ubuntu.com/download/server">Ubuntu Server 24.04 LTS</a> 虚拟机</li>
</ul>
<h2>1.虚拟机中安装 SSH 服务</h2>
<pre><code class="language-bash"># 更新软件源
sudo apt update && sudo apt upgrade

# 安装 SSH 服务
sudo apt install openssh-server

# 验证 SSH 服务是否安装成功
sudo systemctl status ssh</code></pre><p></p>
<h2>2.配置 VirtualBox 网络</h2>
<p>默认情况下，VirtualBox 会为您的虚拟机创建网络地址转换 (NAT) 适配器。这允许虚拟机访问互联网，但会阻止其他设备通过 SSH 访问它。</p>
<p>在虚拟机启动的时候，我们可以通过 <code>ip -4 addr</code> 命令查看虚拟机的 IP 地址，我们可以看到虚拟机的 IP 地址为 <code>10.0.2.16</code> 记录下来，后面会用到。</p>
<p></p>
<p>然后将虚拟机的电源关闭，开始配置虚拟机的网络。</p>
<ul>
<li>右键单机要设置的虚拟机，点击设置(Settings)齿轮图标打开设置窗口。</li>
<li>点击网络(Network)选项卡，默认选择第一个适配器(Adapter 1)。</li>
<li>接下来点击高级(Advanced)选项展开高级设置，点击端口转发(Port Forwarding)打开设置窗口。</li>
</ul>
<p></p>
<p></p>
<ul>
<li>点击右边绿色的添加(Add)按钮，添加一个新的端口转发规则。</li>
<li>在弹出的窗口中，设置如下参数：<ul>
<li>名称：SSH</li>
<li>协议：TCP</li>
<li>主机IP：127.0.0.1</li>
<li>主机端口：12022</li>
<li>虚拟机IP：10.0.2.15</li>
<li>虚拟机端口：22</li>
</ul>
</li>
<li>点击确定(OK)按钮，完成端口转发规则的添加。</li>
</ul>
<h2>3.通过 SSH 连接</h2>
<p>在本地终端中，通过 <code>ssh</code> 命令连接到虚拟机。</p>
<pre><code class="language-bash">ssh -p 12022 username@127.0.0.1
# username 为虚拟机的登录用户名

# 输入密码
(invisible)

# 连接成功
Welcome to Ubuntu 24.04 LTS (GNU/Linux 5.4.0-137-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

 System information disabled due to load higher than 1.0


0 updates can be applied immediately.


Last login: Tue Jan 17 23:30:22 2023 from 10.0.2.2
</code></pre><h2>4.配置 SSH 免密登录</h2>
<p>在本地终端中，通过 <code>ssh-keygen</code> 命令生成 SSH 密钥对。</p>
<pre><code class="language-bash">ssh-keygen -t rsa -b 4096 -C "awsuser@myserver"
# 输入文件名
mykey
# 输入密码
(invisible)
# 再次输入密码
(invisible)</code></pre><p><em>记得将 <code>awsuser@myserver</code> 替换成你自己的邮箱地址。</em></p>
<p><em>记得修改文件名，否则会覆盖掉原来的密钥对。</em></p>
<p>在本地终端中，通过 <code>ssh-copy-id</code> 命令将公钥复制到虚拟机中。</p>
<pre><code class="language-bash">ssh-copy-id -i ~/.ssh/mykey -p 12022 user@127.0.0.1
# 输入密码
(invisible)</code></pre><p>在本地终端中，通过 <code>ssh</code> 命令连接到虚拟机。</p>
<pre><code class="language-bash">ssh -p 12022 username@127.0.0.1
# 已经不需要输入密码了
Welcome to Ubuntu 24.04 LTS (GNU/Linux 5.4.0-137-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

 System information disabled due to load higher than 1.0
  
0 updates can be applied immediately.

Last login: Tue Jan 17 23:30:22 2023 from 10.0.2.2</code></pre><h2>5.禁用 SSH 密码登录</h2>
<p>在虚拟机中，通过 <code>sudo</code> 命令修改 SSH 配置文件。</p>
<pre><code class="language-bash">sudo vim /etc/ssh/sshd_config

# 修改 PasswordAuthentication 参数
PasswordAuthentication no

# ESC 退出编辑模式，输入 :wq 保存并退出。

# 重启 SSH 服务。
sudo systemctl restart sshd</code></pre>
          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/ubuntu/how-to-ssh-into-virtualbox-ubuntu#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303390691430400</guid>
  <category>post</category>
<category>Ubuntu</category>
 </item>
  <item>
    <title>简单 UDP 服务器和客户端</title>
    <link>https://pinw.ca/posts/socket-programming/simple-udp-server-and-client</link>
    <pubDate>Fri, 06 Jan 2023 00:00:00 GMT</pubDate>
    <description>什么是数据报套接字？
数据报套接字 =&amp;gt; Datagram Socket
流套接字 =&amp;gt; Strea</description>
    <content:encoded><![CDATA[
      <blockquote>This rendering is produced by marked and may have formatting issues. For the best experience, visit: <a href='https://pinw.ca/posts/socket-programming/simple-udp-server-and-client'>https://pinw.ca/posts/socket-programming/simple-udp-server-and-client</a></blockquote>
          <h2>什么是数据报套接字？</h2>
<p>数据报套接字 =&gt; Datagram Socket
流套接字 =&gt; Stream Socket</p>
<p>创建一个数据报套接字与创建一个流套接字类似， 主要的区别是数据报套接字使用 UDP 协议而不是 TCP 协议，这意味着客户端和服务器不需要在发送数据之前建立连接。</p>
<p>这对于不需要维护连接的应用程序很有用，因为客户端和服务器可以在不必首先建立连接的情况下发送和接收数据。
例如，DNS 服务器使用 UDP 协议，因为它不需要维护连接，只需要发送数据并接收响应。</p>
<h2>工作流程</h2>
<p></p>
<p><em>图片来源: <a href="https://www.geeksforgeeks.org/udp-server-client-implementation-c/">UDP Server-Client implementation in C</a></em></p>
<h3>Server 如何工作？</h3>
<p>Listener 通过创建一个套接字，将其绑定到一个端口，并等待传入的消息来工作。</p>
<ul>
<li><p>首先，listener 创建一个 hints 结构体，该结构体包含了创建套接字所需的信息，例如协议族，套接字类型，以及端口号。随后调用 getaddrinfo() 函数获取地址信息，保存到 servinfo 链表中。</p>
</li>
<li><p>然后，listener 遍历 servinfo 链表，直到找到可以使用的地址信息，然后通过 socket() 函数创建套接字，如果成功，返回套接字描述符，如果失败，返回 -1。</p>
</li>
<li><p>然后，listener 调用 bind 函数将套接字绑定到指定的端口。这允许 listener 接收发送到该端口的消息。</p>
</li>
<li><p>在套接字绑定成功后，程序将监听来自 recvfrom 函数的传入消息。recvfrom 函数将阻塞程序，直到收到消息，将消息存储在 buf 缓冲区中，并将发送者的地址信息存储在 their_addr 中，最后将收到的消息和发送者的 IP 地址打印到屏幕上。</p>
</li>
<li><p>最后，listener 关闭套接字并退出。</p>
</li>
</ul>
<h3>Client 如何工作？</h3>
<p>Talker 通过设置套接字，向监听器发送消息，关闭套接字来完成工作。</p>
<ul>
<li><p>首先，talker 创建一个 hints 结构体，用于保存地址信息，例如协议族，套接字类型，以及端口号。随后调用 getaddrinfo() 函数获取地址信息，保存到 servinfo 链表中。</p>
</li>
<li><p>然后，talker 遍历 servinfo 链表，直到找到可以使用的地址信息，然后通过 socket() 函数创建套接字，如果成功，返回套接字描述符，如果失败，返回 -1。</p>
</li>
<li><p>在成功创建套接字后，talker 调用 sendto() 函数通过套接字发送消息，如果成功，返回发送的字节数，如果失败，返回 -1。</p>
</li>
<li><p>最后，talker 关闭套接字。</p>
</li>
</ul>
<h2>服务端 demo</h2>
<pre><code class="language-c">/*
** listener.c: a datagram sockets "server" demo - 数据报套接字监听端
** from Beej's Guide to Network Programming Using Internet Sockets
** https://beej.us/guide/bgnet/html/#datagram
*/

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;errno.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;netdb.h&gt;

#define MYPORT "4950" /* 监听端口号，即服务器端口号，客户端要连接的端口号 */
#define MAXBUFLEN 100 /* 最大数据包大小 */

/* 根据协议族，返回 sockaddr 结构体中的 IPv4 或 IPv6 地址 */
void *get_in_addr(struct sockaddr *sa)
{
	// 判断地址族
	if (sa-&gt;sa_family == AF_INET)
	{
		// 返回 IPv4 地址结构体
		return &(((struct sockaddr_in *)sa)-&gt;sin_addr);
	}
	// 返回 IPv6 地址结构体
	return &(((struct sockaddr_in6 *)sa)-&gt;sin6_addr);
}

/* 服务器端主函数 */
int main(void)
{
	int sockfd;							// 监听套接字
	struct addrinfo hints;				// hints 用于 getaddrinfo() 函数 - 用于指定地址信息
	struct addrinfo *servinfo;			// servinfo 用于储存 getaddrinfo() 函数返回的链表 - 用于保存地址信息
	struct addrinfo *p;					// 用于遍历 servinfo 链表
	int rv;								// 用于储存 getaddrinfo() 函数返回值 - 错误代码
	int numbytes;						// 用于储存 recvfrom() 函数返回值 - 接收到的字节数
	struct sockaddr_storage their_addr; // 用于储存客户端地址信息 - sockaddr_storage 可用于保存任何地址 IPv4 或 IPv6
	char buf[MAXBUFLEN];				// 用于保存接收到的数据 - 数据包
	socklen_t addr_len;					// 用于保存客户端地址信息的长度 - sockaddr_storage 结构体长度
	char s[INET6_ADDRSTRLEN];			// 用于保存客户端地址信息 - 地址字符串

	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_INET6;		// 使用 IPv6
	hints.ai_socktype = SOCK_DGRAM; // 使用 UDP 协议
	hints.ai_flags = AI_PASSIVE;	// 使用本机 IP

	// 构建地址信息 servinfo
	if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0)
	{
		// gai_strerror 用于将错误代码转换为错误信息字符串
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}

	// 遍历 servinfo 链表，直到找到可以使用的地址信息
	for (p = servinfo; p != NULL; p = p-&gt;ai_next)
	{
		// 创建监听套接字
		if ((sockfd = socket(p-&gt;ai_family, p-&gt;ai_socktype,
							 p-&gt;ai_protocol)) == -1)
		{
			perror("listener: socket");
			continue;
		}

		// 绑定套接字
		if (bind(sockfd, p-&gt;ai_addr, p-&gt;ai_addrlen) == -1)
		{
			close(sockfd);
			perror("listener: bind");
			continue;
		}

		break;
	}

	// 如果遍历完 servinfo 链表，仍然没有找到可以使用的地址信息，报错退出
	if (p == NULL)
	{
		fprintf(stderr, "listener: failed to bind socket\n");
		return 2;
	}

	// 释放 servinfo 链表
	freeaddrinfo(servinfo);

	printf("listener: waiting to recvfrom...\n");

	// addr_len 用于保存客户端地址信息的长度
	addr_len = sizeof their_addr;

	// 接收数据包并保存到 buf 中，保存的数据包的长度保存到 numbytes 中
	if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN - 1, 0,
							 (struct sockaddr *)&their_addr, &addr_len)) == -1)
	{
		perror("recvfrom");
		exit(1);
	}

	// inet_ntop 用于将客户端地址信息转换为字符串
	printf("listener: got packet from %s\n",
		   inet_ntop(their_addr.ss_family,
					 get_in_addr((struct sockaddr *)&their_addr),
					 s, sizeof s));
	printf("listener: packet - 然后，listener is %d bytes long\n", numbytes);

	// 将数据包的末尾设置为 '\0'，以便于打印
	buf[numbytes] = '\0';

	// 打印数据包
	printf("listener: packet contains \"%s\"\n", buf);

	// 关闭套接字
	close(sockfd);

	return 0;
}</code></pre><h2>客户端 demo</h2>
<pre><code class="language-c">/*
** talker.c -- a datagram "client" demo - 数据报套接字发送端
** from Beej's Guide to Network Programming Using Internet Sockets
** https://beej.us/guide/bgnet/html/#datagram
*/

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;errno.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;netdb.h&gt;

#define SERVERPORT "4950" // 服务器端口

int main(int argc, char *argv[])
{
	int sockfd;				   // 套接字描述符
	struct addrinfo hints;	   // hints 用来指定我们想要的地址类型
	struct addrinfo *servinfo; // servinfo 用来指向返回的地址链表
	struct addrinfo *p;		   // p 用来指向 servinfo 的每一个地址
	int rv;					   // rv 用来存储 getaddrinfo 的返回值,用于判断是否出错
	int numbytes;			   // numbytes 用来存储 sendto 的返回值, 记录发送的字节数

	// 检查参数个数
	if (argc != 3)
	{
		fprintf(stderr, "usage: talker hostname message\n");
		exit(1);
	}

	memset(&hints, 0, sizeof hints); // 初始化 hints 结构体
	hints.ai_family = AF_INET6;		 // 使用 IPv6 协议
	hints.ai_socktype = SOCK_DGRAM;	 // 使用 UDP 协议

	// getaddrinfo() 用来获取地址信息, 并将结果存储在 servinfo 中
	if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0)
	{
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}

	// 循环遍历 servinfo 中的每一个地址, 并尝试创建套接字
	for (p = servinfo; p != NULL; p = p-&gt;ai_next)
	{
		// 创建套接字
		if ((sockfd = socket(p-&gt;ai_family, p-&gt;ai_socktype,
							 p-&gt;ai_protocol)) == -1)
		{
			perror("talker: socket");
			continue;
		}

		break;
	}

	// 如果 p 为 NULL, 则说明没有找到可用的地址
	if (p == NULL)
	{
		fprintf(stderr, "talker: failed to create socket\n");
		return 2;
	}

	// 发送数据, 并记录发送的字节数
	if ((numbytes = sendto(sockfd, a### Potential Issues
1. Error handling: Both programs do not have robust error handling. For example, in the client program, if sendto returns an error, it just prints an error message and exits, rather than trying to recover from the error.

2. Timeouts: If the server is not running or if the message gets lost in transit, the client program will hang indefinitely, waiting for a response that never comes. It might be a good idea to set a timeout for the sendto and recvfrom calls to avoid this.

3. Limited message size: The programs only allow for messages of up to 100 bytes. If you need to send larger messages, you will need to split them into smaller chunks and send them separately.

4. Lack of security: The programs do not use any form of encryption or authentication, so anyone on the network can potentially intercept or forge messages. If you need to secure your communication, you should consider using TLS or SSH to encrypt the messages.rgv[2], strlen(argv[2]), 0,
						   p-&gt;ai_addr, p-&gt;ai_addrlen)) == -1)
	{
		perror("talker: sendto");
		exit(1);
	}

	// 释放 servinfo
	freeaddrinfo(servinfo);

	// 打印发送的字节数
	printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
	
	// 关闭套接字
	close(sockfd);

	return 0;
}</code></pre><h2>程序编译</h2>
<pre><code class="language-bash">gcc -o talker talker.c
gcc -o listener listener.c</code></pre><h2>运行与输出</h2>
<pre><code class="language-bash">./listener      
# 在运行 talker 之前, listener 会一直等待     
listener: waiting to recvfrom...
# 在另一个终端运行 talker 程序之后, listener 会输出以下内容
listener: got packet from ::1
listener: packet is 5 bytes long
listener: packet contains "hello"

# 另一个终端运行 talker 程序
./talker localhost hello 
talker: sent 5 bytes to localhost</code></pre><h2>本文总结</h2>
<p>本文的代码实现了一个简单的 UDP 客户端和服务器端程序, 客户端程序是 talker, 服务器端程序是 listener.</p>
<p>运行 listener 程序, 然后运行 talker 程序. listener 程序会等待来自 talker 程序的消息. 当 talker 程序运行时, 它会发送一个消息给 listener 程序, listener 程序会打印这个消息.</p>
<p>如果没有运行 listener 程序, talker 程序会发送消息, 然后退出. 这些消息会丢失, 因为没有程序来接收它们.</p>
<p>UDP 协议是一个简单的, 轻量级的协议, 它很容易使用. 但是, 它不提供任何错误检查或恢复, 并且不能保证将消息传递到目的地, 因此它不适合需要可靠通信的应用程序.</p>
<h2>参考资料</h2>
<ol>
<li><p><a href="https://beej.us/">Beej&#39;s Guide to Network Programming Using Internet Sockets</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/udp-server-client-implementation-c/">UDP Server-Client implementation in C</a></p>
</li>
</ol>

          <p style='text-align: right'>
          <a href='https://pinw.ca/posts/socket-programming/simple-udp-server-and-client#comments'>Finished reading? Leave a comment</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">152303391425433600</guid>
  <category>post</category>
<category>Socket Programming</category>
 </item>
  
</channel>
</rss>