普通视图

无服务器部署定时脚本推送

2025年10月21日 01:55

去年10月,我写了一篇利用python搭建TG消息推送的文章,当时买了一台几十块钱一年的小鸡来挂这个脚本,虽然隔三差五机器嗝屁,但还是持续使用到了今年8月直到服务器到期,到期后一时没有合适机器来放脚本,于是又花几十块续费了一年,但是今年极其不稳定,每天都嗝屁,甚至有时候嗝好几天,截止本文发布时依旧处于嗝屁状态已连续一周。

今天在抖音刷到视频有人在GitHub放脚本,我突然冒出把推送脚本也放到GitHub的想法,在和豆包简单了解这个想法可行之后,马上开始了实践。

准备工作

  • 创建 Telegram Bot

联系 Telegram 官方机器人 @BotFather,发送/newbot,按提示设置名称和用户名,最终会获得一个Bot Token(格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11),保存备用。

  • 获取群组 ID

将你的 Bot 加入群组,然后在群组内发送任意消息,再向 @getidsbot 发送该消息的转发,会返回群组 ID(格式:-1001234567890),保存备用。

  • 注册或登录GitHub。

开始搭建

  • 配置 GitHub 仓库

新建一个 GitHub 仓库(示例命名rss-telegram-pusher),设置公开状态。

  • 仓库根目录创建文件:rss_telegram.py
    import feedparser
    import logging
    import asyncio
    import json
    import os
    from telegram import Bot
    from telegram.error import TelegramError
    
    # 从环境变量读取配置
    TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
    CHAT_ID = os.getenv("CHAT_ID")
    RSS_URL = os.getenv("RSS_URL")
    
    # 存储已发送ID的本地文件
    POSTS_FILE = "sent_posts.json"
    
    # 配置日志
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(levelname)s - %(message)s"
    )
    
    # 读取已发送的post_id
    def load_sent_posts():
        try:
            if os.path.exists(POSTS_FILE):
                with open(POSTS_FILE, "r", encoding="utf-8") as f:
                    content = f.read().strip()
                    return json.loads(content) if content else []
            logging.info("首次运行,创建空ID列表")
            return []
        except Exception as e:
            logging.error(f"读取已发送ID失败:{str(e)}")
            return []
    
    # 保存已发送的post_id
    def save_sent_posts(post_ids):
        try:
            with open(POSTS_FILE, "w", encoding="utf-8") as f:
                json.dump(post_ids, f, ensure_ascii=False, indent=2)
            logging.info(f"已保存ID列表(共{len(post_ids)}条):{post_ids}")
        except Exception as e:
            logging.error(f"保存已发送ID失败:{str(e)}")
    
    # 获取RSS更新
    def fetch_updates():
        try:
            logging.info(f"获取RSS源:{RSS_URL}")
            feed = feedparser.parse(RSS_URL)
            if feed.bozo:
                logging.error(f"RSS解析错误:{feed.bozo_exception}")
                return None
            logging.info(f"成功获取{len(feed.entries)}条RSS条目")
            return feed
        except Exception as e:
            logging.error(f"获取RSS失败:{str(e)}")
            return None
    
    # 转义Markdown特殊字符
    def escape_markdown(text):
        special_chars = r"_*~`>#+-.!()"
        for char in special_chars:
            text = text.replace(char, f"\{char}")
        return text
    
    # 发送单条消息到Telegram(带间隔)
    async def send_message(bot, title, link, delay=3):
        try:
            # 发送前等待指定秒数(避免频率限制)
            await asyncio.sleep(delay)
            escaped_title = escape_markdown(title)
            escaped_link = escape_markdown(link)
            message = f"`{escaped_title}`\n{escaped_link}"
            logging.info(f"发送消息:{message[:100]}")
            await bot.send_message(
                chat_id=CHAT_ID,
                text=message,
                parse_mode="MarkdownV2"
            )
            logging.info("消息发送成功")
            return True
        except TelegramError as e:
            logging.error(f"Telegram发送失败:{str(e)}")
            return False
    
    # 检查更新并推送所有新帖子
    async def check_for_updates(sent_post_ids):
        updates = fetch_updates()
        if not updates:
            return
    
        new_posts = []
        for entry in updates.entries:
            try:
                # 提取帖子ID(适配URL格式)
                guid_parts = entry.guid.split("-")
                if len(guid_parts) < 2:
                    logging.warning(f"无效GUID格式:{entry.guid},跳过")
                    continue
                post_id = guid_parts[-1].split(".")[0]
                if not post_id.isdigit():
                    logging.warning(f"提取的ID非数字:{post_id},跳过")
                    continue
                logging.info(f"解析到有效ID:{post_id},标题:{entry.title[:20]}...")
                if post_id not in sent_post_ids:
                    new_posts.append((post_id, entry.title, entry.link))
            except Exception as e:
                logging.error(f"解析条目失败(GUID:{entry.guid}):{str(e)}")
                continue
    
        if new_posts:
            # 按ID升序排序(从旧到新推送),若想从新到旧则用reverse=True
            new_posts.sort(key=lambda x: int(x[0]))  # 从小到大:旧→新
            # new_posts.sort(key=lambda x: int(x[0]), reverse=True)  # 从大到小:新→旧
    
            logging.info(f"发现{len(new_posts)}条新帖子,准备依次推送(间隔3秒)")
            async with Bot(token=TELEGRAM_TOKEN) as bot:
                # 逐条推送,每条间隔3秒
                for i, (post_id, title, link) in enumerate(new_posts):
                    # 第一条消息延迟0秒,后续每条延迟3秒
                    success = await send_message(bot, title, link, delay=3 if i > 0 else 0)
                    if success:
                        sent_post_ids.append(post_id)  # 仅成功推送的ID才记录
    
            # 保存所有成功推送的ID
            save_sent_posts(sent_post_ids)
        else:
            logging.info("无新帖子需要推送")
    
    # 主函数
    async def main():
        logging.info("===== 脚本开始运行 =====")
        sent_post_ids = load_sent_posts()
        try:
            await check_for_updates(sent_post_ids)
        except Exception as e:
            logging.error(f"主逻辑执行失败:{str(e)}")
        logging.info("===== 脚本运行结束 =====")
    
    if __name__ == "__main__":
        asyncio.run(main())
  • 仓库根目录创建.github/workflows/rss.yml(定时任务配置):
        name: RSS to Telegram
    on:
      schedule:
        - cron: "*/2 * * * *"  # 每2分钟运行一次
      workflow_dispatch:  # 允许手动触发
    
    jobs:
      run:
        runs-on: ubuntu-latest
        steps:
          # 拉取仓库代码(带令牌认证,后续步骤自动继承权限)
          - name: 拉取仓库代码
            uses: actions/checkout@v4
            with:
              token: ${{ secrets.MY_GITHUB_TOKEN }}  # 用令牌拉取,确保推送权限
              fetch-depth: 0  # 获取完整历史,避免分支冲突
    
          # 设置Python环境
          - name: 设置Python环境
            uses: actions/setup-python@v5
            with:
              python-version: "3.10"
    
          # 安装依赖库
          - name: 安装依赖
            run: pip install python-telegram-bot feedparser
    
          # 运行脚本(检测新内容并推送)
          - name: 运行脚本
            env:
              TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
              CHAT_ID: ${{ secrets.CHAT_ID }}
              RSS_URL: ${{ secrets.RSS_URL }}
            run: python rss_pusher.py
    
          # 提交更新后的ID文件(无repo_token,消除警告)
          - name: 提交更新后的ID文件
            uses: stefanzweifel/git-auto-commit-action@v4
            with:
              commit_message: "更新已发送的帖子ID"
              file_pattern: "sent_posts.json"  # 仅提交ID文件
              branch: main  # 仓库主分支(确保与你的分支名称一致)
              commit_user_name: "GitHub Actions"
              commit_user_email: "actions@github.com"
  • 仓库根目录创建文件 sent_posts.json
[] # 留空即可
  • 创建访问令牌

创建地址,创建个人访问令牌(经典),填写自定义令牌名称,有效期建议90天,令牌范围必须勾选repo(写入仓库),完成创建后只会显示一次令牌token,复制保存。

设置仓库密钥

步骤: Settings(设置) → Secrets and variables(和变量) → Actions(行动) → Repository → secrets(存储库机密) → New repository secret(创建存储库机密)

添加以下密钥:

TELEGRAM_TOKEN:你的 Telegram Bot Token
CHAT_ID:目标群组 ID
RSS_URL:你的 RSS 源地址
MY_GITHUB_TOKEN:GitHub 令牌token

添加完成后如图:
1760968222781.png

测试与验证

进入仓库 → 点击顶部 “Actions” → 左侧选择 “RSS to Telegram” → 点击 “Run workflow” → “Run workflow”,手动触发一次运行,查看群内是否有信息,如果没有,在仓库页点击顶部导航栏的Actions标签,进入工作流运行记录页面,可查看报错原因。


注:此脚本用于本论坛使用,适配本论坛帖子url,其它网站需稍作调整才可正常使用。

仓库地址:https://github.com/kannimade/rss-telegram-pusher

论坛原文:https://www.dalao.net/thread-51611.htm

  •  

提取特定的字体

2025年4月19日 13:26

有时候想给网站某标题特定使用一个字体,但是整个字体包可能会有几十MB大小,利用字体编辑器可以提取我们需要的那几个字,直接将字体包压缩到几KB。

首先打开 查看字符编码 ,输入你需要提取的字,如:

1745039859334.png

右侧的“Unicode编码16进制”可以方便我们定位文字,打开 字体编辑器 ,新建一个项目,导入完整的字体包,点击菜单上的“查找字形”,选择uni编码,粘贴上一步的编码,需要在前方加入“uni”。

1745039978849.png

回车就会准确定位到我们需要的文字上,接下来就是CV大法了,新建一个项目,把需要的文字复制进去:

1745040065537.png

搞定之后直接导出成字体文件就可以放进网站使用了。

1745040217626.png


当然还有更方便的办法,用python可以自动化操作,有需求的自行问GPT。

  •  

使用giffgaff卡搞TG过程

2025年3月23日 22:49

曲折的故事起因源于突然不想使用+86手机号使用TG了,至于原因dddd.

偶然在论坛看到有用户发帖“付邮送giffgaff卡”,于是简单的对giffgaff做了一下了解(以下检测gg卡),gg卡是位于英国的移动电话公司产品,并且提供实体 sim 卡且免实名,还可自由切换为 esim,或再换回实体 sim 卡

拿到卡激活时选择“Pay as you go”套餐,在中国内地的漫游费用即为:收短信免费,发短信 £0.3/条,接打电话 £1/分钟,流量 £0.2/MB。所以此卡的主要作用是只收短信,免实名注册各类境外账号。

我果断的支付6元邮费拿了一张,第二天就收到了卡片,这些人手里或多或少都有从英国寄来的多余卡片,是他们走自己的邀请通道申请的,有人激活后会获得部分奖励,如果你不想走他们的推荐的话可以自己在gg官网免费申请包邮到家,但路途遥远,可能得在一个月后才能收到卡片,所以找他们付个邮费拿一张是最简单的,而且走推荐激活后会赠送5英镑话费,自己直接申请激活的则没有赠送。

简单的在官网操作激活之后,迫不及待的打开TG进行手机号换绑,但TG出了一个谜之操作,换绑提示“我们已经将验证码发送到该帐号(+44 XXXXXXXX)其他设备上的 Telegram应用。

大致意思就是我的gg号码已经有人注册账号了,并且还在线,验证码发到了登陆者的设备上。

一脸懵逼的我一脸懵逼,给TG发邮件苦等也没有结果,恰好网友说gg可以在官网免费换两次号码,去换一个号码再试试。

于是我马上去官网换号码,但必须在他们的工作时间才能操作换号(页面会有提示在不在更换时间内)。

1742740561907.png

1742740579521.png

直接输入密码后页面就会显示新的号码了,这时候手机里的卡会短暂的出现无服务现象,稍等一会后信号就会恢复,这时候就可以使用新的号码了。

可当我拿着新号码去换绑时,TG上依旧显示“我们已经将验证码发送到该帐号(+44 XXXXXXXX)其他设备上的 Telegram应用。

我突然意识到这可能是TG的一种机制,于是我随机乱输入一些号码,结果都是这个提示,当我尝试更换为其它+86手机号时,正常发送了。

姑且判定是TG不允许+86手机号换绑海外手机了?

于是我直接用新的+44 手机号在我的苹果设备上注册了一个新的TG号,这次直接正常发送短信验证码了,但是注册成功之后不过一分钟就弹出了:“此手机号码已被封禁。

网上搜索了一下什么情况,大概是设备上登录账号过多(一共登录3个),且短时间内同一IP设备不停的发送验证码,被TG判为刷号给封了,网上给了一些申诉方案,但我觉得麻烦,且不知道要等待多久,于是打算明天消耗掉最后一次更换gg号的机会重新注册(gg在24小时内只能更换一次号码)。

第二天卡点更换了新的号码,这次我拿了另外一部安卓手机,且使用了住宅IP进行注册,从中午注册到现在,账号还是一切正常,应该不会出什么问题了,先在同设备IP挂个一个月沉淀沉淀,之后就着手注销掉两个+86账号了。


补充一句如果你也想入手gg卡,请确保自己有外币卡哈,比如VISA等,因为激活时需要充值10英镑。

参考阅读文章:https://www.hui.ke/posts/giffgaff/https://dnswiz.com/read-1940-1.html

另外,gg卡默认是开启语音信箱的,也就是有来电时,我们拒接后会转入语音信箱,这时候也会收取我们的话费,所以需要关闭这个功能。

网上说的是在拨号界面输##002#,但其实gg早就关闭这个通道了,需要在gg官网提工单关闭才行。

  •  

TG自建Python版的RSS订阅Bot

2024年10月9日 13:02

以前找moe.one要了一个TG新帖推送插件,我上TG上的少没细心关注,这几天才发现这个推送机器人抽风很厉害,不能做到实时推送,中间会间隔一堆才推一次,看不懂代码逻辑,于是自己在网上找了RSS订阅BOT部署,试了两个用的人多的项目,都是docker部署,我想改改消息格式都不知道怎么下手,实在对docker不懂...默认的消息格式让我强迫症实在难受,于是让AI配合写了如下Python脚本,此文记录一下搭建步骤,避免以后有需要时又要去找AI掰扯。

打开服务器终端,安装Python和所需库

安装 pip
更新包列表:

apt update

安装 python3-pip:

apt install python3-pip

安装完成后,确认 pip 是否正确安装:

pip3 --version

正常会返回 pip 的版本信息。

安装所需库

pip3 install feedparser requests python-telegram-bot

下一步,我们需要去Telegram创建Bot,在TG里搜索@BotFather,进入对话框,输入/newbot以创建新机器人,然后回复一个机器人名字,再回复一个机器人的用户名,需要带bot,可以参考截图设置:

1728448409299.png

@BotFather会回复你一个机器人的HTTP API,保存它,后面会用到。

进入刚刚创建的机器人对话框,将他拉进一个你需要推送的群,或者直接给你自己推送也行,拉群记得给它管理员和消息权限。

TG搜索@get_id_bot,进入对话框点击右上角,将他拉到刚刚的群里,在群聊对话框输入:/my_id@get_id_bot发送,会得到一个-开头的群聊ID,记录它,包括-符号。

1728449020735.png

信息获取完成,接下来就是创建脚本了,回到服务器终端

  1. 创建目录
    假设创建一个名为 dalaorss 的目录:
mkdir dalaorss
cd dalaorss
  1. 创建 Python 文件
    在 dalaorss 目录中,创建一个 Python 文件,这里使用 nano 编辑器创建一个名为 rss_bot.py 的文件:
nano rss_bot.py
  1. 添加代码
    将下面代码复制进脚本中,注意修改为你的信息
import feedparser
import logging
import asyncio
import json
import os
from telegram import Bot
from telegram.error import TelegramError

# Telegram Bot Token 和目标聊天 ID
TELEGRAM_TOKEN = 'Bot Token'
CHAT_ID = '群ID'

# 存储已发送的帖子 ID 的文件
POSTS_FILE = 'sent_posts.json'

# 读取已发送的帖子 ID
def load_sent_posts():
    if os.path.exists(POSTS_FILE):
        with open(POSTS_FILE, 'r') as f:
            return json.load(f)
    return []

# 保存已发送的帖子 ID
def save_sent_posts(post_ids):
    with open(POSTS_FILE, 'w') as f:
        json.dump(post_ids, f)

# 从 RSS 源获取更新
def fetch_updates():
    feed_url = "订阅地址"
    try:
        return feedparser.parse(feed_url)
    except Exception as e:
        logging.error(f"获取 RSS 更新时出错: {e}")
        return None

# 转义 Markdown 特殊字符
def escape_markdown(text):
    special_chars = r"_*~`>#+-.!"
    for char in special_chars:
        text = text.replace(char, f"\{char}")
    return text

# 发送消息到 Telegram
async def send_message(bot, title, link):
    # 转义 Markdown 特殊字符
    escaped_title = escape_markdown(title)  # 转义 Markdown 特殊字符
    escaped_link = escape_markdown(link)    # 转义 Markdown 特殊字符

    # 使用 Markdown 格式,将标题包裹在反引号中以避免超链接,链接直接显示
    message = f"`{escaped_title}`\n{escaped_link}"
    try:
        await bot.send_message(chat_id=CHAT_ID, text=message, parse_mode='MarkdownV2')
        logging.info(f"消息发送成功: {escaped_title}")
    except TelegramError as e:
        logging.error(f"发送消息时出错: {e}")

# 主函数
async def check_for_updates(sent_post_ids):
    updates = fetch_updates()

    if updates is None:
        return  # 如果获取更新出错,则返回

    new_post_ids = []  # 用于存储新帖子 ID

    for entry in updates.entries:
        # 从 guid 中提取帖子 ID
        post_id = entry.guid.split('-')[-1].split('.')[0]  # 提取 ID

        # 检查是否为新帖子
        if post_id not in sent_post_ids:
            new_post_ids.append((post_id, entry.title, entry.link))  # 存储 ID, 标题和链接

    # 如果有新帖子,按 ID 升序排序并发送最新帖子
    if new_post_ids:
        new_post_ids.sort(key=lambda x: int(x[0]))  # 升序排序
        latest_post_id, title, link = new_post_ids[0]  # 获取最新的帖子
        async with Bot(token=TELEGRAM_TOKEN) as bot:
            await send_message(bot, title, link)

        # 更新已发送的帖子 ID
        sent_post_ids.append(latest_post_id)
        save_sent_posts(sent_post_ids)  # 保存到文件

# 主循环
async def main():
    logging.basicConfig(level=logging.INFO)

    # 加载已发送的帖子 ID
    sent_post_ids = load_sent_posts()

    while True:
        try:
            await check_for_updates(sent_post_ids)
        except Exception as e:
            logging.error(f"检查更新时出错: {e}")
        await asyncio.sleep(60)  # 每 60 秒检查一次

if __name__ == "__main__":
    asyncio.run(main())
  1. 保存并退出
    在 nano 编辑器中,按 Ctrl + O 保存文件(字母O不是数字0),然后按 Enter 确认,接着按 Ctrl + X 退出编辑器。
  2. 运行脚本
    nohup(no hang up)命令可以让脚本在后台运行,即使您关闭终端会话也能保持运行。
nohup python3 rss_bot.py &

现在就可以去更新rss看看推送状态了,这里的脚本内容以我自己论坛为例,如果你也是xiuno论坛那么照抄就行,如果是博客或者其它程序,可能还需要做一些修改。

推送效果:

1728449810062.png

个人比较喜欢这种格式,如果你喜欢别的模式比如消息内预览,可以使用docker版本的,网上搜一下就有。

脚本运行后,怎么查看或关闭:

检查当前运行的进程

ps aux | grep python

会得到正在运行的进程,查看到rss_bot那条,最前面有一个ID,命令行输入

kill ID

即可停止。

  •  

网站自动增量备份到另一台服务器

2024年10月5日 12:15

国庆的时候,智联IDC联系我说:给你赞助服务器,需要啥开口就行。

于是,我要了一台韩国物理机。

1728120062137.png

大陆优化,我还是保持套用CDN,昨晚已经将论坛迁移上去了,可能是心理作祟,明显觉得响应速度比之前快了。

第一件事还是搞定时备份,数据库好说,压缩下来也就40多MB,搞了个每两小时备份一次到又拍,但是网站压缩包有5GB大,每次自动备份上传都会因为文件过大失败,所以昨晚研究了一下增量备份。

我的操作很简单,这个备份方式也适用所有人,非常方便,以下教程以宝塔为例,把网站数据从主服务器(网站所在)备份到副服务器(专门备份)

首先,安装 rsync:
确保在两台服务器上都安装了 rsync,可以使用以下命令安装,打开终端输入对应命令:

sudo apt-get install rsync    # Debian/Ubuntu
sudo yum install rsync        # CentOS/RHEL

然后,在主服务器SSH里连接副服务器:

ssh-keygen
ssh-copy-id 副服务器SSH用户名@副服务器IP

回车根据提示输入密码即可,弹出Enter passphrase (empty for no passphrase):是让你设置密码短语,直接回车不要设置。

然后,在主服务器根目录下的/root/目录里,创建一个文件:backup.sh,内容写:

#!/bin/bash

# 设置变量
SOURCE_DIR="/www/wwwroot/网站目录"  # 主服务器上的网站目录
DEST_DIR="副服务器SSH用户名@副服务器IP:/www/wwwroot/网站目录"  # 副服务器上的备份目录
LOG_FILE="/root/backup.log"  # 日志文件路径

# 输出脚本开始执行的消息
echo "脚本开始执行" | tee -a "$LOG_FILE"

# 使用 rsync 进行增量备份,排除 .user.ini 系统文件
rsync -avz --delete --exclude='.user.ini' "$SOURCE_DIR/" "$DEST_DIR" >> "$LOG_FILE" 2>&1

# 输出备份完成的消息
echo "备份完成" | tee -a "$LOG_FILE"

因为在上面脚本中我们加入了日志,所以还需要在/root目录下手动创建一个backup.log文件,以便备份日志记录。

再次打开终端,执行

chmod +x ~/backup.sh

赋予它执行权限,现在可以运行脚本。执行以下命令:

~/backup.sh

看到提示信息就说明备份成功了,接下来创建一个脚本来定时执行这个任务。

要定时执行 ~/backup.sh 脚本,在终端中运行以下命令,以打开 crontab 编辑器:

crontab -e

crontab 的格式如下:
第一个 * 表示分钟(0 - 59)
第二个 * 表示小时(0 - 23)
第三个 * 表示日期(1 - 31)
第四个 * 表示月份(1 - 12)
第五个 * 表示星期几(0 - 7,0和7都表示星期日)

比如我每小时执行,就是:

0 * * * * ~/backup.sh

按 Ctrl + O 保存,按 Ctrl + X 退出。

查看当前的定时任务是否创建成功:

crontab -l

或者

直接在宝塔定时任务那里添加shell脚本任务,以root身份执行,脚本内容:

sh ~/backup.sh

增量备份机制:

在备份过程中,会比较源目录和备份目录中的文件,它通过检查文件的大小和修改时间来判断文件是否被修改过,如果源目录中的文件被修改,只传输这些变化的文件,而不是重新传输整个目录,这种机制大大减少了需要传输的数据量,提高了备份速度和效率。

假设在主服务器的 /www/wwwroot/www.dalao.net 目录中有以下文件:
file1.txt(未修改)
file2.txt(已修改)
file3.txt(已删除)

当执行命令后:
file1.txt 会保持不变(未修改)。
file2.txt 会被传输到备份服务器,因为它已经被修改。
file3.txt 会从备份服务器中删除,因为它在源目录中被删除了。

以上是网站文件的增量备份,数据库也大同小异,但我没有去做,因为文件不大,直接备份到七牛了,这样增量似乎也可以实现多源站方案。

  •  

通过HTTP代理获取RSS

2024年8月31日 20:02

几个月前,为论坛上线了“博文贡献”功能,简单的说就是将你的博客绑定上论坛账号,发布博客后系统可以使用你的论坛账号自动将博文发布到论坛并附上你的原文地址,实现“你为论坛添砖,论坛为你加瓦”。

陆续到现在有30多位用户绑定了博客同步,但实际因功能不完善的原因,每次都只有几个博客完成了同步,前天在家调整了一天,适配兼容Atom和RSS2.0,结果总是“你的同步了,他的又不行了。”

忽然想到之前找叶开拿了一个博客的RSS订阅,似乎能获取大部分的RSS(因订阅的博客较少,暂未发现不能获取的),于是直接把这个订阅器的获取代码部分COPY到了论坛上,经测试,30多个绑定博客,已有20多个能正常获取,其余的几个可能因网络问题和爬虫拦截原因没能获取。

但随着想法的延伸,开始担心有人利用此同步方式查看网站访问日志,获取到源站 IP,与益达叶开交流后,提出了使用代理获取的方案。

本想将此间过程记录,但碍于自己也是东搜西搜,主要还是GPT帮我完成了此方案,所以就不记录那些随手一搜就有的结果了,只讲一下使用的软件吧。

一开始,我选择的是Tor,通过Tor的SOCKS代理访问网站成功,留下的都是Tor分发出来的IP,每次都不同,但都是欧洲那边的,可能访问国内的博客会导致超时等各种原因,总之我在放进PHP里之后,每次进行获取RSS数据都会异常。

尝试几次之后,决定还是自己搭建HTTP代理服务, 先后试了Squid 和 3Proxy,都是遇到一些莫名其妙的错误,也有安装不上的。

最后是使用了Tinyproxy,完美的安装运行,放进了博客和论坛的获取PHP里面,正常的获取数据,留下了代理服务器的IP。现在,我在几台不同的服务器上都安装了Tinyproxy,进行随机使用。

TIPS:如果遭遇恶意攻击代理服务器,还可选择购买别人的代理服务,专门为爬虫做的代理池,以下是我随便找的一个HTTP代理商,买了个一块钱做测试,付费后体验30分钟,会给一个API和令牌,用它来随机获取代理IP。

1725127721663.png
(避免广告,不提供地址)

一个月20块钱也可以接受的,我测试这个全是国内的IP,请求海外服务器的网站可能就不行了。

更新: 因部分人使用的是国内服务器并且屏蔽了海外IP,导致无法获取RSS,所以我在网上寻觅了2个小时找到了一个性价比还算可以的国内HTTP代理提供商家,支持按请求次数付费,掏腰包买了一个套餐:

1725138529375.png
(避免广告,不提供地址)

无时间限制,请求数用完即止,在调试的时候需要区分哪个RSS源用哪个区域IP访问,就用了较简单的思路:默认使用购买的代理池获取,如果国内代理池获取不了,再使用自己搭建的海外HTTP服务器获取。

尝试一遍后,发现存在一个浪费请求数的问题,每访问一个RSS源都去请求了一遍代理池,达到了每个访问RSS的IP都是不同的,提倡节约的原则,又加上了一个新的思路:只在获取RSS的第一次请求代理池IP,并存储下来,遇到不能访问的切换到自建IP,下一条RSS再用存储的IP进行访问。只存储3分钟,3分钟过后如果还有新的RSS获取请求,再从代理池拉新的IP(代理池规定的动态IP存活时间在3分钟左右)。


这或许也并不能完全的隐藏源站IP,但也并不会很直观的就把源站IP暴漏出去。

欢迎你也加入论坛绑定自己的博客,如果绑定后无法同步,请尝试放行 User agent 请求标识:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Dalao/1.0 www.dalao.net


PS:该功能已下线。

关于博文同步:因需要保证同步质量,拒绝绑定水文极其严重的博客,包括说说站点、撰写敏感言论的站点、宗教信仰类站点等。

附:大佬论坛:“博文贡献 BETA 2.0” 版本说明

  •  

狗云的服务器赞助

2024年3月7日 13:49

之前提到了论坛跳H的问题,给它更换了付费SSL之后没几天,我又给域名解析加了DNSSEC。

原本域名的DNS是在华为云的,因为华为云解析免费,而且支持全球各地分区域解析,但唯独不支持DNSSEC,去年就给他们反馈过希望增加这一功能,但没有下文。

国内常用的解析无非是 华为云、阿里云、腾讯云,除开华为云之后,其它两家都支持DNSSEC,但需要开通付费版套餐才能使用,而且分区域解析也被列入了套餐中,比如开通个人版套餐,给你开放国内的华中华北、国外的亚洲欧洲区域解析,开通专业版再给你开放国内的城市线路和国外的国家细分。

我只为DNSSEC,所以选择开通了阿里云的云解析个人版。

DNS切换到阿里云之后,过了一个多星期都没有人反馈跳H问题了,似乎问题得到了解决。

补充一句为什么DNS选择用国内的,因为使用国外的DNS会影响国内的解析访问速度,而且套上CDN之后,CDN也会根据DNS分配成海外的远距离节点。


前几天,我寻思给论坛的服务器找个赞助商吧,虽然不久前才在狗云给机器续费了一年,但拉个赞助不过分吧?

去年使用的是狗云的香港KC A节点,配置拉到了顶配,8H16G,运行论坛妥妥性能过剩浪费钱,不过我利用充值2000赠送的5折折扣,顶配机器一年折扣下来只花了1300左右。

今年春节后,机器年付到期,想着节约一下成本,给配置拉到了4H8G,5折后年付价格700多。

5折是狗云弹性云用户能拿到的最低折扣。

说回找赞助,心想着肯定得找老牌且口碑不错的商家,于是我联系了一下v.ps的老板,之前也有过几次与他的域名交易,说明来意后他二话不说开了一台荷兰的4H4G机器给我,我测试了一番发现效果不太理想,源站放荷兰的话CDN回源延迟太高,除非放弃国内加速,使用CF众生平等。

我联系他,看能不能换个香港的机器,对大陆访问友好一些,可能觉得我挑来挑去太麻烦,就没继续赞助。

我也不能随便找个机器就把论坛往上搬不是,既然用不上,也就不去浪费资源了,顺手把v.ps账户注销了。

最后抱着试试看的态度问了一嘴狗云的老板德克,德克倒是爽快的很,半夜三点给我把机器从香港KC A节点迁移到了性能更好的C节点,并且将机器配置又拉到了顶配8H16G,还问我要不要给机器加个高防IP。

当然狗云给我的并不是纯免费赞助,只是给了我一个极低的折扣,1折。

之前5折年付顶配需要1300左右,现在只需要300出头,一天不到1块钱,属实爽歪歪。

当然我也不可能白嫖赞助,去年一年给狗云AFF推广,百分之十的提成我拿了近2000元,算回去就是给狗云拉了两万元的单子,互利谁也不亏。

  •  

给友链页面改为随机排序

2023年12月23日 15:25

由AI提供的代码,根据自己博客UI增加样式,如下:

  <div id="linkList" class="grid grid-cols-2 gap-4 my-7"></div>
  <script>
    // 定义链接数据
    var links = [
      { title: '名字', url: '链接' },
      { title: '名字', url: '链接' }
    ];

    // 随机排序链接
    shuffle(links);

    // 输出随机排序后的链接列表
var linkList = document.getElementById('linkList');
for (var i = 0; i < links.length; i++) {
  var anchor = document.createElement('a');
  anchor.href = links[i].url;
  anchor.title = links[i].title;
  anchor.target = "_blank";
  anchor.rel = "noopener";
  anchor.className = "card";
  anchor.textContent = links[i].title;
  linkList.appendChild(anchor);
}

    // 随机排序数组函数
    function shuffle(array) {
      for (var i = array.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var temp = array[i];
        array[i] = array[j];
        array[j] = temp;
      }
      return array;
    }
  </script>
  •  

给博客添加一个输出友链RSS的页面

2023年12月22日 13:00

FreshRSS

看了rz的文章,跟着做了个订阅博客的页面,然后由rz给我将它和友情链接放到了同一个页面,这里记录一下搭建方式,以备日后需要再次使用时查阅(以下内容结合了rz和网友小宋的文章,以宝塔为例搭建):

1、在安装了Docker的宝塔服务器终端输入(此处使用默认8080端口,可按需修改):

docker run -d --restart unless-stopped --log-opt max-size=10m \
  -p 8080:80 \
  -e TZ=Europe/Paris \
  -e 'CRON_MIN=1,31' \
  -v freshrss_data:/var/www/FreshRSS/data \
  -v freshrss_extensions:/var/www/FreshRSS/extensions \
  --name freshrss \
  freshrss/freshrss

2、在你的防火墙安全组放行上面设置的端口,我的是8080,然后访问:服务器ip:8080(你的端口)

3、安装过程中数据库那里选择 " SQLite ",不然会报错。

4、在宝塔新建一个站点,域名填你要作为rss后台的域名,数据库不用,PHP设置纯静态即可。

5、创建完毕后,选择站点设置-反向代理,其中目标url可以根据自己实际设置的端口,我是8080,就写 刚刚docker服务器的IP:8080,如果你的反代站和docker在同一个服务器,就写 127.0.0.1:8080

6、给域名申请ssl证书并配置到刚刚新建的站点上,记得给域名做解析,这时候你就可以用这个域名访问后台了。

7、进入后台,在【设置->管理->认证】去开启允许api。

8、在【设置->账户->账户管理->API 管理】设置密码并提交保存,记住设置的api密码

9、在自己站点根目录下创建一个php文件,用于放FreshRSS api调用函数,例如:rss.php,内容:

<?php
/**
 * 获取最新订阅文章并生成JSON文件
 */
function getAllSubscribedArticlesAndSaveToJson($user, $password)
{
    $apiUrl = 'https://你部署FreshRSS的域名/api/greader.php';
    $loginUrl = $apiUrl . '/accounts/ClientLogin?Email=' . urlencode($user) . '&Passwd=' . urlencode($password);
    $loginResponse = curlRequest($loginUrl);
    if (strpos($loginResponse, 'Auth=') !== false) {
        $authToken = substr($loginResponse, strpos($loginResponse, 'Auth=') + 5);
        $articlesUrl = $apiUrl . '/reader/api/0/stream/contents/reading-list?&n=1000';
        $articlesResponse = curlRequest($articlesUrl, $authToken);
        $articles = json_decode($articlesResponse, true);
        if (isset($articles['items'])) {
            usort($articles['items'], function ($a, $b) {
                return $b['published'] - $a['published'];
            });
            $subscriptionsUrl = $apiUrl . '/reader/api/0/subscription/list?output=json';
            $subscriptionsResponse = curlRequest($subscriptionsUrl, $authToken);
            $subscriptions = json_decode($subscriptionsResponse, true);
            if (isset($subscriptions['subscriptions'])) {
                $subscriptionMap = array();
                foreach ($subscriptions['subscriptions'] as $subscription) {
                    $subscriptionMap[$subscription['id']] = $subscription;
                }
                $formattedArticles = array();
                foreach ($articles['items'] as $article) {
                    $desc_length = mb_strlen(strip_tags(html_entity_decode($article['summary']['content'], ENT_QUOTES, 'UTF-8')), 'UTF-8');
                    if ($desc_length > 20) {
                        $short_desc = mb_substr(strip_tags(html_entity_decode($article['summary']['content'], ENT_QUOTES, 'UTF-8')), 0, 99, 'UTF-8') . '...';
                    } else {
                        $short_desc = strip_tags(html_entity_decode($article['summary']['content'], ENT_QUOTES, 'UTF-8'));
                    }
                    
                    $formattedArticle = array(
                        'site_name' => $article['origin']['title'],
                        'title' => $article['title'],
                        'link' => $article['alternate'][0]['href'],
                        'time' => date('Y-m-d H:i', $article['published']),
                        'description' => $short_desc,
                    );

                    $subscriptionId = $article['origin']['streamId'];
                    if (isset($subscriptionMap[$subscriptionId])) {
                        $subscription = $subscriptionMap[$subscriptionId];
                        $iconUrl = $subscription['iconUrl'];
                        $filename = 'https://你部署FreshRSS的域名/'.substr($iconUrl, strrpos($iconUrl, '/') + 1);
                        $formattedArticle['icon'] = $filename;
                    }

                    $formattedArticles[] = $formattedArticle;
                }

                saveToJsonFile($formattedArticles);
                return $formattedArticles;
            } else {
                echo 'Error retrieving articles.';
            }
        } else {
            echo 'Error retrieving articles.';
        }
    } else {
        echo 'Login failed.';
    }
    return null;
}
function curlRequest($url, $authToken = null)
{
    $ch = curl_init($url);
    if ($authToken) {
        $headers = array(
            'Authorization: GoogleLogin auth=' . $authToken,
        );
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    }
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}
/**
 * 将数据保存到JSON文件中
 */
function saveToJsonFile($data)
{
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    file_put_contents('output.json', $json);
    echo '数据已保存到JSON文件中';
}

// 调用函数并提供用户名和密码
getAllSubscribedArticlesAndSaveToJson('这里是FreshRSS的用户名', '这里是第3步设置的api密码');

10、在宝塔添加一个计划任务,定时访问url,填写上一步创建的php文件(博客地址/rss.php),时间建议2小时访问一次,以更新订阅数据,如果使用cdn,记得添加白名单,避免被拦截(一般不会)。

11、在你博客需要显示订阅数据的地方插入以下代码。

<?php
            // 获取JSON数据
            $jsonData = file_get_contents('./output.json');
            // 将JSON数据解析为PHP数组
            $articles = json_decode($jsonData, true);
            // 对文章按时间排序(最新的排在前面)
            usort($articles, function ($a, $b) {
                return strtotime($b['time']) - strtotime($a['time']);
            });
            // 设置每页显示的文章数量
            $itemsPerPage = 30;
            // 生成文章列表
            foreach (array_slice($articles, 0, $itemsPerPage) as $article) {
                $articles_list ='
                图标:' . htmlspecialchars($article['icon']) . '
                站点标题:' . htmlspecialchars($article['site_name']) . '
                文章标题:' . htmlspecialchars($article['title']) . '
                文章内容摘要:' . htmlspecialchars($article['description']) . '
                文章链接:' . htmlspecialchars($article['link']) . '
                文章发布时间:' . htmlspecialchars($article['time']) . '
                ';
                echo $articles_list;
            }
        ?>

获取到数据之后需要根据自己博客UI调整样式,以下是我的,不含样式文件,仅供参考:

      <?php
          // 获取JSON数据
          $jsonData = file_get_contents('./output.json');
          // 将JSON数据解析为PHP数组
          $articles = json_decode($jsonData, true);
          // 对文章按时间排序(最新的排在前面)
          usort($articles, function ($a, $b) {
              return strtotime($b['time']) - strtotime($a['time']);
          });
          // 设置每页显示的文章数量
          $itemsPerPage = 15;
          // 生成文章列表
          foreach (array_slice($articles, 0, $itemsPerPage) as $article) {
          ?>
          <article class="flex comment-body my-2" style="margin-top: 2.5rem;">
              <div class="flex-none mr-1">
                    <div class="relative">
                      <!--输出头像-->
                        <img no-view="" class="relative z-10 w-12 object-cover border-2 border-gray-200 rounded-md scrollLoading mr-1" src="<?php echo htmlspecialchars($article['icon']); ?>" alt="up">
                        </div>
                    </div>
                    <div class="flex-initial w-full text-sm">
                        <div class="comment-author mb-1">
                            <div class="flex items-center">
                                <!--输出文章链接和站名-->
                                <a href="<?php echo htmlspecialchars($article['link']); ?>" target="_blank" rel="external nofollow" class="" data-ajax="false" style="text-decoration: none;"><?php echo htmlspecialchars($article['title']); ?></a><span class="mx-1"></span> 
                            </div>
                        </div>
                        <div class="comment-content card mb-2">
                            <!--输出文章摘要-->
                            <p><?php echo htmlspecialchars($article['description']); ?> <a href="<?php echo htmlspecialchars($article['link']); ?>" target="_blank" rel="external nofollow" class="" data-ajax="false">
                           查看全文>>></a></p> 
                        </div>
                        <div class="flex items-center comment-meta text-xs text-gray-500 mt-1" data-no-instant="">
                            <!--输出站名-->
                            <span class="flex items-center comment-reply text-muted comment-reply-link hover:text-blue-500" ><span><?php echo htmlspecialchars($article['site_name']); ?></span></span>・
                            <div>
                                <!--输出发布时间-->
                            <time class="mr-1"><?php echo htmlspecialchars($article['time']); ?></time>
                        </div>
                    </div>
                </div>
            </article>            
         <?php } ?>

至此,去rss管理后台添加你要订阅的博客就行了。

  •  

网站防护措施

2023年10月17日 14:56

最近在浏览大家博客和窥群时都有发现,不少网友在网上发布自己网站链接后遭到了恶意攻击,我们无法确定是何人所为,所以在你发布网站链接之前,一定要做好防护措施,我会在本文里写上一些我用到的防护方案,欢迎参考。

分区域解析

将大陆和海外分开解析,推荐使用华为云 DNS,支持单独为各个国家和区域设置解析线路。

dns

无备案网站的大陆线路推荐使用一些专为大陆优化的香港/日本等节点的CDN,如FarCDN,如您网站有备案,则可以选择使用更快的国内CDN服务,但建议不要直接使用腾讯云之类的CDN,因为一旦欠费,巨额账单你无法承受,可以使用像多吉云这样的融合 CDN 商家,价格便宜,线路更多;当然还是推荐使用带有防护的 CDN 产品,如百度云加速,注意不是百度智能云。

海外线路可以直接无脑接入 CloudFlare,目前 CloudFlare 除了DNS接入方式外,可以使用 SAAS 方式接入,这个方法可以自行百度,此处不做过多赘述。

CF

使用分区域解析的原因很简单,攻击者使用的都是海外IP池,分开解析后这部分攻击会被 CloudFlare 接下,不影响正常访问。

如果你在此前已经被攻击过,请在你的服务商更换 IP 后再使用此方案,否则源 IP 仍会被恶意攻击。

加强方案

给服务器设置一个默认站点,并给默认站点配置一个 ssl,可以有效避免攻击者通过 ssl 证书查询到您的源服务器 IP。

宝塔用户可添加一个网站(域名随意写),然后为这个网站配置一个无效的证书,配置好证书之后在宝塔面板后台:【网站】-【默认站点】中心选择刚才添加的这个网站作为默认站点。

可以使用这个证书:

证书(PEM格式)

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgK0HE3hTJQDg6p/fj
nS92eSuRKZEZ5F4grT6tWFKNYVmhRANCAAQIP4WfZQx4/3/tIw0QDdt05DRKiIuO
pghp8GVQ94JcS5fmtZqX1yx0hBU4qZ0skIJr5D2M0BmhCBQ9Kulv2YDL
-----END PRIVATE KEY-----

密钥(KEY)

-----BEGIN CERTIFICATE-----
MIIDITCCAsagAwIBAgIUTcEWLzynkLCFCoAC1iDH2vG3EkYwCgYIKoZIzj0EAwIw
gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T
YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL
Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0
eTAeFw0xOTAxMTMxNDMxMDBaFw0zNDAxMDkxNDMxMDBaMGIxGTAXBgNVBAoTEENs
b3VkRmxhcmUsIEluYy4xHTAbBgNVBAsTFENsb3VkRmxhcmUgT3JpZ2luIENBMSYw
JAYDVQQDEx1DbG91ZEZsYXJlIE9yaWdpbiBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABAg/hZ9lDHj/f+0jDRAN23TkNEqIi46mCGnwZVD3glxL
l+a1mpfXLHSEFTipnSyQgmvkPYzQGaEIFD0q6W/ZgMujggEqMIIBJjAOBgNVHQ8B
Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB
/wQCMAAwHQYDVR0OBBYEFCEZF6Eyem01XPbgwr6DXLZV1qsQMB8GA1UdIwQYMBaA
FIUwXTsqcNTt1ZJnB/3rObQaDjinMEQGCCsGAQUFBwEBBDgwNjA0BggrBgEFBQcw
AYYoaHR0cDovL29jc3AuY2xvdWRmbGFyZS5jb20vb3JpZ2luX2VjY19jYTAjBgNV
HREEHDAaggwqLmRuc3BvZC5jb22CCmRuc3BvZC5jb20wPAYDVR0fBDUwMzAxoC+g
LYYraHR0cDovL2NybC5jbG91ZGZsYXJlLmNvbS9vcmlnaW5fZWNjX2NhLmNybDAK
BggqhkjOPQQDAgNJADBGAiEAnrequCk/QZOOrcPH6C3Hgcy4SPNUy5rQtku/aYkj
qQoCIQCN6IyYNiXuwG+8jUgJrveiirBjiz2jXZSTEfVAyibjTg==
-----END CERTIFICATE-----

以上证书可生成的一个 dnspod.com 的域名 ssl 证书,可用于预防 ssl 证书泄露 IP,证书过期也可以继续使用。

您可以访问 https://search.censys.io/ 输入域名查询您的 IP 有没有被证书泄露,可访问 https://ipchaxun.com/ 查询到域名历史解析记录,如您直接解析过 A 记录,可能已经被这种网站记录下来,所以强烈建议更换 IP 后再使用以上方案。

邮件通知

如您网站有邮件通知,请务必换成第三方发信,如阿里云邮件推送,否则攻击者可通过 SMTP 邮件获取到你的服务器 IP 地址,从而攻击源站。

对象存储

如您使用了国内大厂的对象存储服务,您可以给对象存储域名再套一层防护CDN,比如您使用了腾讯云oss,要求您将xx.xx别名解析到oo.oo,那么你可以使用CDN防护服务,将CDN源站地址写oo.oo,然后将xx.xx解析到CDN提供的地址上,再开启CDN的防御策略,则可有效避免对象存储被盗刷。

禁用端口

大部分攻击方式为 UDP,您可在服务器防火墙禁用并拦截所有 UDP 端口。

  •  
❌