2 月竹白发布下线公告的当天,我发了「🚚 指南:从竹白(zhubai.love)迁移到 Quaily」。

倒不是手有多快,而是 Quaily 很早就支持从竹白导入了。

迁移结果

截止到 2025 年 3 月 31 日,共计迁移了 31 个频道,1715 篇文章,9845 位订阅者。

他们是:

  • CyberClip: 一份臻选互联网上有价值内容的赛博剪报,两周一期,涵盖新奇趣闻、热点议题、前沿科技以及其他关于生活、关于未来的事物。
  • 熊言熊语: 与你分享肿瘤生物医药领域的研究进展以及我的所思所学所想。
  • 为何是他: 以信求知。
  • 无限自然: 《无限自然》对优质信息源/AI自动化提效有足够的好奇心,关注AIGC对产品设计的影响。通过逐步建立良好的系统,无限接近自然的状态。
  • 素生: 误读人生,化人生活
  • 事不过三: 重要的事不过三件:认识自己,好好学习,好好生活。
  • 试行错误: 反复探索,不断试错。重在实践和落地。希望能和你一起找到「改变」的突破口!同行的人比目的地更重要。
  • 乔尔事务所: 关于地方和空间的观察备忘录
  • 海上星光: PM@阿小小海和设计师@尤a娜共营的科技、设计、生活型博客/上海/东京
  • 我脑袋里的怪东西: 无聊的通勤 + 奇怪的脑袋。
  • Off-Beat: 欢迎看到,这里会存一些个人感兴趣的东西;分享一些个人经历影响下,看到的喜欢的书、音乐、视频、文章;说一些评价、推荐、想法、见闻以及别的该说不该说的乱七八糟的东西吧。
  • 日出山谷: 好奇心是智慧的种子!
  • 跳歇间: 这是一段时间,也是一个空间。在这里,保持一种轻快的脚步,间歇小跳。
  • 有(冇)用: 有(冇)用,源自粤语俗语,意为有(无)用。在当下的生活里,每天信息宛如瀑布般涌现在这小小电子屏幕上,同时信息内涵也悄然变化着,不再单纯且多少夹杂某些目的。但......这些信息是真的有用吗?
  • Moonvy月维设计周刊: 每周一更新设计相关的素材和资讯
  • 播客相对论: 分享有趣、有意思、值得被更多人听到的播客节目,也希望能在评论中看到你给我推荐一些播客节目。
  • 大人的坚持魔法术: 分享一些坚持的魔法,做乌龟赛跑里的乌龟就够了。每周四发出。
  • 闲时杂记: 记录生活的美好与不美好✨
  • 简悦周报: 简悦周报官方推送渠道,内容涵盖了:新用户指南 / 简悦阅读模式使用技巧 / 通用性问题的处理和解决方案 / 与其它生产力工具的联动 / 简悦用户提供的基于简悦的工作流等。
  • 显济的闲言碎语: 下午2:00-5:00在喝咖啡|海外智能硬件产品设计师|做了自己的网站:Jefferyho.com
  • 喧哗上等Radio: 播客《喧哗上等》的邮件推送
  • 中文播客行业动态: 中文播客领域的动态和最新消息
  • 花生碎: 记录是种自我建档。
  • 学到老 Shadow law: 一个爱饮咖啡、常听播客、养了只猫的人。读了什么、想到什么,可能都会记录下来,随机从里面抽取片段分享。
  • 自说自话: 没有记录就没有发生,而记录本身已经是一种反抗。
  • 一颗小树: 分享我的日常,包括但不限于互联网、技术、开源、投资理财,欢迎你来。
  • 吾栖之地 | Soul Place: Chuwen的不定期回顾,用文字创作Cyber空间的个人纪录片。
  • 驯鹿漫游: 记录我近期收集的有趣信息及一些联想。不定期更新。
  • 家书: 以书信形式给青少年看的生命哲思。
  • 未闻Code·会员精选: 未闻 Code 精选内容/会员专属内容/我每周的思考总结/我每周学到的新东西。
  • 曲率飞船: 这里有诚实真挚的人间体悟,有趣独特的阅读新知。

内容导入

用爬虫抓内容

竹白的网站是一个 Web App,所以常规的爬虫抓不到内容。

所以用了 headless chrome 来抓取内容。

例如,如果要获得文章的标题,发布日期等内容,就在 headless chrome 里运行这一段:

function get() {
  const h1 = document.querySelector('h1');
  const meta = h1.nextElementSibling;
  const content = meta.nextElementSibling;
  const metaSpans = meta.querySelectorAll('span');
  let dateString = '';
  let premium = '0';
  if (metaSpans.length >= 2) {
    dateString = metaSpans[1].innerText;
  }
  if (meta.innerText.indexOf('付费内容') !== -1) {
    premium = '1';
  }

  const cover = content.querySelector('figure');
  let img = null;
  if (cover) {
    img = cover.querySelector('img');
  }

  return {
    title: h1.innerText,
    date: dateString,
    content: content.innerHTML,
    cover: img ? img.src: '',
    premium: premium,
  }
}

而要获得文章列表,则运行这一段:

function getArticles() {
  const links = document.querySelectorAll('.PublicationPage_item__38S6L');
  let freeLinks = [];
  let paidInfo = [];
  for (let i = 0; i < links.length; i ++) {
    const slug=links[i].href.substring(8, links[i].href.indexOf('.zhubai.love'))
    freeLinks.push(`./quail-prod -c ../../config.prod.yaml import -t zhubai -l ${slug} --url='${links[i].href}'`);
    const timeEl = links[i].querySelector('.PublicationPage_time__1xXKG')
    let time = timeEl.innerText.trim()
    // check the first character is digit or not
    if (!/^\d+$/.test(time[0])) {
      time = time.substring(1).trim()
    }
    const paidTagEl = links[i].querySelector('.PublicationPage_paidTag__vwcz8')
    const titleEl = links[i].querySelector('.PublicationPage_title__1pSF4')
    if (paidTagEl && titleEl) {
      paidInfo.push({
        slug,
        title: titleEl.innerText.trim(),
        time
      })
    }
  }

  let paidSql = [];
  for (let i = 0; i < paidInfo.length; i ++) {
    paidSql.push(`update posts set first_published_at = '${paidInfo[i].time}' where title = '${paidInfo[i].title}' and slug='${paidInfo[i].slug}';`)
  }
  return {
    freeLinks,
    paidSql
  }
}
NOTE

这里对收费文章区分对待,是因为爬虫抓不到收费文章的内容,但是可以抓到收费文章的时间。
把时间记录一下是有用的,看到后面就知道了。

于是,对于免费文章,无论是否有竹白提供的导出数据,只要得到了作者授权,就可以无缝迁移到 Quaily 了。

收费文章内容的导入

三月开始,竹白提供了数据导出,可以导出订阅数据和文章数据。

文章数据里也包括付费的文章,但是实际使用这些数据的时候有两个问题:

  1. 竹白的导出文章数据里没有文章的发布时间
  2. 导出数据里的所有链接都是坏链

于是上一个脚本中,抓取付费文章的发布时间就有用了:

使用竹白导出数据完成收费文章的导入以后,再更新一下付费文章的发布时间,那么文章排序才是正确的。


最终完整的文章导入流程变成:

  1. 免费文章:直接爬虫抓。
  2. 收费文章:先用备份数据恢复,然后用爬虫抓发布时间,然后更新。

坏链我搞不定,需要作者自己修复了。

订阅者导入

相比内容导入,订阅者导入要简单得多。

竹白导出的订阅信息是一个 csv 文件,大概是这样的格式:

用户名 邮件地址 订阅时间 付费时间 到期时间 累计付费 是否为付费用户
某某 [email protected] 2022-02-09 - - 0.00

我有一个专门的脚本来转换各类订阅信息到 Quaily 的 JSON 订阅格式,其中也包括竹白导出的 csv。

但大量导入很容易命中 ESP 的风控,所以这个脚本会把订阅信息拆分成很多份,每一份都是一个单独的 json 文件。

An image to describe post

然后这些 json 文件会被提交给 Quaily 的任务调度程序,这个程序会自己去安排导入任务。

调度程序部署好任务以后,会返回预计完成时间,我会把这个时间通过邮件回复给让我帮迁移的作者。

最后,我自己维护了一个 Google Sheets 来记录迁移的进展

An image to describe post

一些优化

预迁移

一些作者在竹白上发了迁移公告,还没有邮件联系我,我已经把内容给迁移完毕了。

于是当他们给 Quaily 发邮件时,我会回复内容已经迁移完了,他们会很开心。

这是因为对于新创建的 Quaily 频道,我会得到通知。然后我会去看看竹白上有没有同名的。如果有的话,并且作者声明授权的迁移目的地也对得上,就可以直接迁移了。

这样的体验比较好。

Onboarding

观察了一些作者的使用习惯以后我发现:

  1. 竹白似乎不支持完整的 Markdown,所以写了「五分钟掌握 Markdown」来帮助作者们学习 Markdown
  2. 不少作者同时经营公众号,那么把微信公众号排版功能介绍给作者

最后挑选了一些中文作者可能感兴趣的话题,在文章「✉️ 给中文创作者朋友的信」一起集中介绍,并在迁移邮件中推荐作者阅读和订阅。

感谢竹白

Quaily 刚刚起步时,就收到了不少竹白作者的信任——像 一石千浪DEX 周刊AIGC Weekly,都是最初就选择了 Quaily 的几位。也正因为大家的鼓励,我才有了更多前进的动机。

没想到到了最后,Quaily 的导入在竹白下线时也帮到了更多作者。

感谢竹白和竹白的创作者,有了大家,Quaily 才像今天这样具备一定的承载力,也让我有机会遇见你们。

以上。