感谢竹白,以及如何支持竹白用户的迁移 | 歌词经理

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 文件,大概是这样的格式:

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

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

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

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

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

一些优化

预迁移

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

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

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

这样的体验比较好。

Onboarding

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

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

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

感谢竹白

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

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

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

以上。