惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
Hacker News: Ask HN
Hacker News: Ask HN
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
MyScale Blog
MyScale Blog
月光博客
月光博客
W
WeLiveSecurity
T
Threat Research - Cisco Blogs
Martin Fowler
Martin Fowler
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
The GitHub Blog
The GitHub Blog
Webroot Blog
Webroot Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
F
Full Disclosure
U
Unit 42
Jina AI
Jina AI
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
腾讯CDC
T
Threatpost
H
Hacker News: Front Page
P
Palo Alto Networks Blog
博客园 - 聂微东
Last Week in AI
Last Week in AI
有赞技术团队
有赞技术团队
Help Net Security
Help Net Security
L
LINUX DO - 热门话题
N
News and Events Feed by Topic
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派 万字剖析:千问App深度体验报告(2026) - 少数派 在2026年,如何真正防止别人抄袭你的作品 - 少数派 怎么用 50 块搭个 AI 语音助手?我踩了 3 天坑 - 少数派 YeeroAI:让 AI 对话真正成为知识管理的一部分 - 少数派 爬泰山 - 少数派 「旅图显影」 App 更新:这次,我们补上了一点「手感」 - 少数派 假期出门太折磨?我的 23 条经验帮你规划惬意旅行 - 少数派 工作流会变吗 - 少数派 Claude Opus 4.6 怎么用最省钱?我测了 5 种方案 - 少数派 GPT Image 2 让图文并茂不再稀罕 - 少数派 用户侧出发——什么是AI,我要不要学习? - 少数派 找片、转存、整理、播放一条龙!让你的付费网盘值回票价 - 少数派 欢迎试用!日课一问2.0插件 - 少数派 自己做的MDeditor,原本想购买 Typora 试了两次支付不成功,干脆自己做一个 - 少数派 vibe coding了一个 3MB 的小工具,让 ~/Downloads 彻底告别混乱 - 少数派 因为受不了 Mac 的风扇策略,我做了一个风扇控制工具 - 少数派 别只怪模型 - 少数派 Warp 终端的 AI 功能怎么用?我测了一周的体验 - 少数派 AI 写代码老是出 bug?这 5 个配置我后悔没早知道 - 少数派 「新玩意」苹果出相机可能就这样:Sigma BF + 45mm F2.8 DG Contemporary - 少数派 一个面向2030年的AI操作系统是什么样子的:浅谈cola这款有灵魂的Agent - 少数派 别只看写代码 - 少数派 每天解决10个问题,还是一口气攻坚解决400个? - 少数派 AI 交易机器人怎么搭?我用 Claude 跑了一周实盘 - 少数派 Maptoposter Online:把你爱的城市画成艺术海报 - 少数派 Function Calling 怎么用?我测了 3 个模型发现差距真大 - 少数派 Legend Talk:我做了个 AI 圆桌,让 160 位思想家围着你的问题转 - 少数派 如何找到自己的蓝方?在小县城寻找压力测试 - 少数派 语音输入与软件接口|2026年聊AI时,我们都聊些什么(上) - 少数派 混动已经卖爆,纯电又来补刀——钛7闪充版简直“不讲武德” - 少数派 本月玩什么|朋友收藏、识质存在、沙罗周期 - 少数派 为什么要每天坚持输出? - 少数派 Claude API 挂了好几个小时,你的项目有备用方案吗? - 少数派 Function Calling 没你想的复杂——我用它做了个有点用的工具 - 少数派 登录系统立即播放视频或者图片音乐的软件 - 少数派 我为什么创建 FlipHTML5 下载工具 - 少数派 残局没电?多品牌外设电量统一管理软件EasyBluetooth已支持RTSS游戏内显示以及AIDA64 - 少数派 前往通义路的路 - 少数派 太好看了,媲美Sun的个人导航页,NAS部署星云门户 - 少数派 乌黑嘴唇“一键检测”上线了 - 少数派 派早报:Claude AI 接入多个创意软件生态、FILCO 生产方接手品牌等 - 少数派 【更新】BearCLI、Claude 连接器与 MCP 服务器 - 少数派 记了上千条流水,还是看不懂财务?我做了一个让 AI 读懂账本的工作台 - 少数派 MINI R56 升级原厂 Sport 模式 - 少数派 新玩意 | 一棵柠檬树(仿真版) - 少数派 Momenta的“物理AI”野望,需迈过“含摩量”这道关 - 少数派 网页直接投屏控制手机!NAS一键部署PandaScrcpy,流畅丝滑可远程。 - 少数派 众测|邀你一同探索随身 AI 硬件入口 YoooClaw C·ONE - 少数派 2050大会:分享时间是真诚 参会记 - 少数派 iPad 赋能电影创作:国内首部宣纸手绘长片《燃比娃》的幕后故事 - 少数派 AI的审美:我用 8 个大模型给 100 张旅行照片打分 - 少数派 普通人如何破圈?去参加一个本地协会 - 少数派 把极空间的图标全换了,主题DIY全攻略打造你的专属NAS桌面 - 少数派 电子便签墙,帮你实现便签自由 - 少数派 我如何用三个 CLI 工具取代文档创建需求 - 少数派 原来真的有人可以玩一辈子 - 少数派 社区速递 139 | 派友热议三月买了啥、复古单反尼康 Df 体验 - 少数派 06 作品的赏析与评价 - 少数派 TDS REVIEW|索尼 WF-1000XM6 降噪真无线耳机体验 - 少数派 35.98万起售的第二代腾势D9,我看重的不是堆料,而是不凑合 - 少数派 鼠须管 Squirrel 皮肤配置指北 - 少数派 从watch ultra2换到redmi watch6 - 少数派 派早报:阿里巴巴发布视频生成模型 HappyHorse 1.0 等 - 少数派 别迷信1M - 少数派 家人们天塌了!网盘“大封杀”,多个渠道多条路,NAS部署PanHub - 少数派 AI与人勾心斗角!NAS一键部署AI狼人杀,假日休闲必备。 - 少数派 电商必备!Comfyui工作流批量生图插件,一次生成12张!支持Nano banana pro模型 - 少数派 Comfyui工作流配置Gpt-image-2模型教程,0.03/张 - 少数派 OpenClaw第三方APi怎么配置?可使用Gpt-image-2模型 - 少数派 会员社区话题精选 Ep. 103 - 少数派
飞书多维表格+扫码枪快速构建个人家庭图书管理系统 - 少数派
2024-10-23 · via 少数派

Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。

文章代表作者个人观点,少数派仅对标题和排版略作修改。


一句话介绍:飞书多维表格 + 扫码枪读取纸质书 ISBN 号 + 爬取豆瓣数据 + 飞书应用开发能力 = 快速构建个人家庭图书管理系统。

先看一下最后的成果吧。虽然我还没完全把书全录进去,但模板和代码已经搞定了。大家可以直接使用我的模板

数据表
封面图
查询页(支持以书名、作者或分类查询图书)
统计图仪表盘

接下来详细记录一下我创建模板和录入数据的过程。

契机是这样的,最近家里新买了一个书柜,我想着反正要把书都倒腾出来摆放一次,不妨顺便把家里的纸质书登记为一个表格;而且我最近翻出了一个以前买的扫码枪,可以发挥一下作用(实践下来发现确实挺好玩的)。

在工具的选择上,我没犹豫太多就选了飞书的多维表格。多维表格的扩展和开发能力很强,也能够快速把一个基础的数据表呈现为多种看板、画廊效果。而且多维表格最近还新增了 AI 捷径,可以直接调用 AI 能力填充表格。

整体设计思路大概分为以下几部分:

创建表格模板

在飞书云文档新建多维表格。如果不想逐个手动配置字段的话,可以点击右上角的 AI 助手飞飞,告诉它希望增加哪些字段,AI 就会帮助我们生成表头并设置出最为合适的字段属性:

我这次的思路是:先通过 ISBN 获取豆瓣图书数据,然后使用飞书的 AI 能力补充生成需要的字段,最后再手动录入少量数据。基于此,要录入的信息分为三类:

  • 基础信息:豆瓣中能够获取到的基本信息,比如书名、作者、出版社、简介、评分等;
  • AI 生成信息:基于基础信息,生成豆瓣中无法获取的信息,比如基于把图书简介概括为一句话总结,生成合适的图书分类等;
  • 手动录入信息:当前阅读状态(读完、在读、待读),读书笔记等。

通过自动抓取导入 + AI 补全生成,尽可能减少手工录入的工作量。同时在多维表格中,数据表是最基础的数据库,后续可以基于数据表格创建不同的视图、表单或仪表盘,不过现在还是空表,所以我们把其他视图放到最后再介绍。

爬取豆瓣接口获取书籍信息

如果可以的话,用 API 是最好的,不过豆瓣的接口很早就关闭了,我在网上搜了一下,也没有其他太好用的图书 API(或者都不免费)。鉴于个人使用一共也没多少本书,所以可以还是做一下豆瓣的爬虫吧,仅供学习使用,不会对豆瓣服务器造成太大压力。

使用豆瓣的搜索接口提交 ISBN 号1,从第一条搜索结果中获取图书链接并爬取更详细的信息。

如图,豆瓣的搜索接口是:https://book.douban.com/j/subject_suggest?q=

    params = {'q': isbn}
    response_suggest = requests.get('https://book.douban.com/j/subject_suggest', params=params, headers=headers_suggest)
    if response_suggest.status_code != 200 or not response_suggest.json():
        return "未找到相关书籍信息"
    book_suggest = response_suggest.json()[0]
    book_url = book_suggest['url'].replace('\\', '')

获取图书详情页https://book.douban.com/subject/35335514/后,请求该页面数据,并从页面中提取相应信息:

不过由于豆瓣的图书简介内容并没有统一数据格式,这里还需要做很多错误处理,举个例子,比如定价这里,有时会写 59.00 元,有时会写¥59 或 CNY 59,有时会写 59,有时信息缺失干脆没有这个字段,所以为了避免程序处理时报错,就需要做一些处理,比如使用正则:

    try:
        price = float(re.search(r"(?:CNY\s*)?(\d+(?:\.\d+)?)(?:\s*元)?", soup.find('span', text='定价:').next_sibling.strip().replace('元', '')).group(1))
    except AttributeError:
        price = ''

其他字段的处理类似,这里就不再赘述了。

还有一个稍微特殊一点的就是封面图片,多维表格中支持上传图片附件,可以将封面图生成画廊,所以我们也需要顺便记录一下封面图片的 URL 地址,后续可以下载图片并上传到飞书中。

豆瓣的信息里,其实没有图书的分类信息,我本来想参考中图法2去做图书分类,后来发现太复杂了,家里这么点书确实没必要那么精细的分类。

然后我试了一下飞书的 AI 功能,其实可以用 AI 自动补充图书分类:

再比如,豆瓣简介写的比较长,我们可以让 AI 进行总结,生成一句话简介:

这样设置好了之后,只要我们填入前几列的字段,飞书 AI 就会帮助我们自动补全这两列数据,非常好用。

通过接口向飞书云文档推送数据

多维表格其实自带一个 Webhook 接口用于提交数据,具体做法是点击文档右上角的「自动化」:

「创建自定义流程」:

左边选择「接收到 Webhook 时」,会得到一个 Webhook 地址,向这个地址发送 JSON 格式的数据,服务器就会收到并自动解析为字段;然后右侧选择「新增记录」,选择插入数据的位置为数据表,然后设置记录内容即可:

不过……我感觉这样做还是不是太方便,因为需要手动逐个去设置字段匹配,字段一多或者需要修改时也比较麻烦。所以我还是用开发应用的方式来实现,登录飞书开放平台,选择创建企业自建应用:

填写基本信息:

创建完成后有很多可以设置的选项,不过我们这次主要会用到「应用凭证」、「权限管理」和「版本发布」这几项功能,其余的大家可以自行阅读飞书的开发文档(在这里要保存好 App IDApp Secret 供后面使用)。

想要通过应用向多维表格中提交数据,主要需要几个过程,我会把文档附在这里供大家参考:

这个过程中,还需要向应用开放相应的操作权限,我建议是在「开发文档」中调试接口时,开通相应的权限。

具体来说,我们需要提前准备好 4 个参数,分别是:

  • appid:飞书应用的唯一标识,在应用设置页面获取
  • app_secret:在应用设置页面获取,和 appid 一起使用,用于获取tenant_access_token
  • table_apptoken:多维表格 App 的唯一标识,从多维表格链接中获取。
  • table_id:多维表格数据表的唯一标识,从多维表格链接中获取。

获取鉴权

提交appidapp_secret ,获得tenant_access_token

def access_token():
    url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
    payload = json.dumps({
        "app_id": feishu_appid,
        "app_secret": feishu_app_secret
    })
    headers = {
        'Content-Type': 'application/json'
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    return response.json()['tenant_access_token']

上传图片

对于附件类内容,需要先完成上传获取file_token ,后续提交表格记录时需要用到。这一步直接用飞书文档里的示例代码就行:

import os
import requests
from requests_toolbelt import MultipartEncoder
def upload_media(file_path, access_token):
    file_size = os.path.getsize(file_path)
    url = "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all"

    form = {
        'file_name': os.path.basename(file_path),
        'parent_type': 'bitable_image',
        'parent_node': table_apptoken,
        'size': str(file_size),
        'file': (open(file_path, 'rb'))
    }
    print(form)
    multi_form = MultipartEncoder(form)
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': multi_form.content_type
    }

    response = requests.request("POST", url, headers=headers, data=multi_form)
    print(response.json())
    if response.status_code == 200:
        return response.json().get('data', {}).get('file_token')
    else:
        return None

上传记录

这一步主要是结合数据表的结构,构造好需要提交的 JSON。主要是需要结合文档,注意不同类型的字段必须匹配相应的格式。比如,在表格中设置「豆瓣链接」的类型为超链接,这里就必须提供textlink ;「出版时间」设置为日期,就需要提交时间戳而非字符串;「封面图」设置为附件,就需要提交file_token;还有一些数字格式,也要匹配具体的设置。

如果提交的数据和设置的字段类型不匹配就会报错,所以需要仔细 debug 一下。代码比较简单,大家应该能看明白:

def insert_book_info_to_feishu(book_info, file_token, access_token):
    url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{table_apptoken}/tables/{table_id}/records"

    payload = json.dumps({
        "fields": {
            "书名": book_info["书名"],
            "作者": book_info["作者"],
            "出版社": book_info["出版社"],
            "出版时间": book_info["出版时间"],
            "页数": book_info["页数"],
            "定价": book_info["定价"],
            "ISBN": book_info["ISBN"],
            "豆瓣评分": book_info["豆瓣评分"],
            "评分人数": book_info["评分人数"],
            "豆瓣链接": {
                "text": book_info["书名"],
                "link": book_info["豆瓣链接"]
            },
            "豆瓣简介": book_info["豆瓣简介"],
            "封面图": [
                {
                    "file_token": file_token
                }
            ]
        }
    })
    payload = construct_payload(book_info, file_token)
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {access_token}'
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    print(response.json())
    print(f'飞书接口状态:{response.status_code}')
    return response.status_code

哦对顺便说一句,如果是专业搞飞书应用开发的,可以直接用 Python SDK,我手搓代码是这次因为一共也就两三个接口的事。

使用扫码枪录入 ISBN

完成以上准备工作之后,我们就可以开始录入数据了。这时就要祭出「扫码枪」了。具体来说,我用到的这种扫码枪叫做「红光一维码扫码枪」3

大家现在用的更多的是手机扫描二维码。其实扫码枪这玩意儿一点都不神秘,超市收银台标配。而且一维码条形发明都几十年了,被广泛应用于商品标识。

扫码枪的原理其实很简单,就是通过光学扫描系统读取条形码中的数据,将其转换为电信号,再经过处理器处理后,模拟键盘输入的形式将数据传递给计算机等设备。

不过我用的并不是那种「有线枪式」的扫码枪,而是一个便携的蓝牙扫描枪(不带货哈)。所以对于电脑来说,扫码枪类似于一个无线键盘:

对于用户来说,只需要知道,连接扫码枪之后,你把键入的光标放在哪里,它就会把扫码结果直接输入到哪里(我录了个短视频,不知道能不能看清楚)。

扫码枪也有许多设置项,其中一项便是扫码结尾的字符,如果不做设置,扫码枪在连续扫描时就会录入一长串字符,所以一般来说会在结尾设置\n换行符。

到此我们就可以把以上所有工作流程串在一起了,运行起来的效果大概是这样:

比较费时间的其实是下载和上传图片。大家注意看最后几秒,在自动上传一条新纪录后,多维表格的快捷 AI 会根据我们的设置,自动填充好「一句话总结」和「AI 分类」两个字段。

这样基本上就能 10s 内录完一本书的数据,而且全程只需要做一个动作,就是扫码,很快就能整理出家里的图书数据库。

有了基础的数据表之后,就可以根据自己的需要创建更多视图。所谓视图,就是展现数据的不同方式。大家可以根据自己的数据类型和情况探索一下展示效果,这里不再详细介绍了~

而且你别说,做完这个图书数据表之后,我觉得其他家庭数据库也挺有搞头的,用类似的思路,可以很快搭建出「家庭药箱」「家庭食品保质期」「老婆的化妆品」等等数据表,无非就是替换一下条形码查询数据的接口,而且类似这种需要注意保质期的数据表,还能利用多维表格中的到期提醒功能自动发送提醒。下周有空就做。

最后我把全部代码分享一下,由于是自己用的,所以并没有刻意做什么整理,大家随便看看,仅供参考。

以上。


附录(本文用到的全部代码):

import requests
from bs4 import BeautifulSoup
import json
import os
from requests_toolbelt import MultipartEncoder
import time
from datetime import datetime
import re
#设置飞书应用id
feishu_appid = ''
feishu_app_secret = ''
#设置飞书表格的id
table_apptoken = ''
table_id = ''
def access_token():
    url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
    payload = json.dumps({
        "app_id": feishu_appid,
        "app_secret": feishu_app_secret
    })
    headers = {
        'Content-Type': 'application/json'
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    # print(response.json())
    return response.json()['tenant_access_token']
def upload_media(file_path, access_token):
    file_size = os.path.getsize(file_path)
    url = "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all"

    form = {
        'file_name': os.path.basename(file_path),
        'parent_type': 'bitable_image',
        'parent_node': table_apptoken,
        'size': str(file_size),
        'file': (open(file_path, 'rb'))
    }
    print(form)
    multi_form = MultipartEncoder(form)
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': multi_form.content_type
    }

    response = requests.request("POST", url, headers=headers, data=multi_form)
    print(response.json())
    if response.status_code == 200:
        return response.json().get('data', {}).get('file_token')
    else:
        return None
def get_book_info_by_isbn(isbn):
    # 请求书籍搜索建议接口,获取书籍的URL和封面图片链接
    # 如请求失败,修改headers或增加cookies
    headers_suggest = {
        'sec-ch-ua': '"Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
        'x-requested-with': 'XMLHttpRequest',
    }
    params = {'q': isbn}
    response_suggest = requests.get('https://book.douban.com/j/subject_suggest', params=params, headers=headers_suggest)
    if response_suggest.status_code != 200 or not response_suggest.json():
        return "未找到相关书籍信息"
    book_suggest = response_suggest.json()[0]
    book_url = book_suggest['url'].replace('\\', '')
    pic_url = book_suggest['pic'].replace('\\', '').replace('subject/s','subject/l')
    # 请求书籍详情页,提取书籍的详细信息
    # 如请求失败,修改headers或增加cookies
    headers_detail = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,
*/*
;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
    }
    response_detail = requests.get(book_url, headers=headers_detail)
    soup = BeautifulSoup(response_detail.text, 'html.parser')
    # 提取信息
    try:
        author = soup.find('span', text=' 作者').find_next_sibling('a').text.strip()
    except AttributeError:
        author = ''
    try:
        publisher = soup.find('span', text='出版社:').find_next_sibling('a').text.strip()
    except AttributeError:
        publisher = ''
    try:
        pub_year = soup.find('span', text='出版年:').next_sibling.strip()
        pub_year = convert_to_timestamp(pub_year)
    except AttributeError:
        pub_year = ''
    try:
        pages = int(soup.find('span', text='页数:').next_sibling.strip())
    except AttributeError:
        pages = ''
    try:
        price = float(re.search(r"(?:CNY\s*)?(\d+(?:\.\d+)?)(?:\s*元)?", soup.find('span', text='定价:').next_sibling.strip().replace('元', '')).group(1))
    except AttributeError:
        price = ''
    try:
        rating = float(soup.find('strong', class_='rating_num').text.strip())
    except:
        rating = ''
    try:
        votes = int(soup.find('span', property='v:votes').text.strip())
    except AttributeError:
        votes = ''
    try:
        intro_div = soup.find('div', class_='intro')
        paragraphs = intro_div.find_all('p')
        intro = "\n".join([p.get_text() for p in paragraphs])
    except AttributeError:
        intro = ''
    # 整合提取的书籍信息
    book_info = {
        "书名": book_suggest['title'],
        "作者": author,
        "出版社": publisher,
        "出版时间": pub_year,
        "页数": pages,
        "定价": price,
        "ISBN": isbn,
        "豆瓣评分": rating,
        "评分人数": votes,
        "豆瓣链接": book_url,
        "封面图链接": pic_url,
        "豆瓣简介": intro
    }
    return book_info
def download_cover_image(cover_url, save_path):
    response = requests.get(cover_url)
    if response.status_code == 200:
        with open(save_path, 'wb') as file:
            file.write(response.content)
        return save_path
    else:
        return None
def convert_to_timestamp(date_str):
    # 使用正则表达式匹配不同的日期格式
    date_formats = [
        r'^(\d{4})-(\d{1,2})-(\d{1,2})
$',  # 2023-02-01
        r'^(\d{4})-(\d{1,2})$
',            # 2023-02 或 2023-2
        r'^(\d{4})$',                      # 仅有年份 2023
    ]
    for date_format in date_formats:
        match = re.match(date_format, date_str)
        if match:
            groups = match.groups()
            if len(groups) == 3:  # 解析到年-月-日
                year, month, day = int(groups[0]), int(groups[1]), int(groups[2])
            elif len(groups) == 2:  # 解析到年-月
                year, month, day = int(groups[0]), int(groups[1]), 1  # 默认设置为1日
            elif len(groups) == 1:  # 仅解析到年份
                year, month, day = int(groups[0]), 1, 1  # 默认设置为1月1日
            # 构造日期对象并转化为时间戳
            date_obj = datetime(year, month, day)
            timestamp = int(date_obj.timestamp() * 1000)  # 转为毫秒级时间戳
            return timestamp
    raise ValueError("无法解析日期格式")
def construct_payload(book_info, file_token):
    # 初始化空的字段字典
    fields = {}
    # 动态构造需要提交的字段,跳过空值的字段
    if book_info.get("书名"):
        fields["书名"] = book_info["书名"]
    if book_info.get("作者"):
        fields["作者"] = book_info["作者"]
    if book_info.get("出版社"):
        fields["出版社"] = book_info["出版社"]
    if book_info.get("出版时间"):
        fields["出版时间"] = book_info["出版时间"]
    if book_info.get("页数"):
        fields["页数"] = book_info["页数"]
    if book_info.get("定价"):
        fields["定价"] = book_info["定价"]
    if book_info.get("ISBN"):
        fields["ISBN"] = book_info["ISBN"]
    if book_info.get("豆瓣评分"):
        fields["豆瓣评分"] = book_info["豆瓣评分"]
    if book_info.get("评分人数"):
        fields["评分人数"] = book_info["评分人数"]
    if book_info.get("豆瓣链接") and book_info.get("书名"):
        fields["豆瓣链接"] = {
            "text": book_info["书名"],
            "link": book_info["豆瓣链接"]
        }
    if book_info.get("豆瓣简介"):
        fields["豆瓣简介"] = book_info["豆瓣简介"]
    if file_token:
        fields["封面图"] = [{"file_token": file_token}]
    # 构造最终的 payload
    payload = json.dumps({"fields": fields})
    return payload
def insert_book_info_to_feishu(book_info, file_token, access_token):
    url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{table_apptoken}/tables/{table_id}/records"

    payload = json.dumps({
        "fields": {
            "书名": book_info["书名"],
            "作者": book_info["作者"],
            "出版社": book_info["出版社"],
            "出版时间": book_info["出版时间"],
            "页数": book_info["页数"],
            "定价": book_info["定价"],
            "ISBN": book_info["ISBN"],
            "豆瓣评分": book_info["豆瓣评分"],
            "评分人数": book_info["评分人数"],
            "豆瓣链接": {
                "text": book_info["书名"],
                "link": book_info["豆瓣链接"]
            },
            "豆瓣简介": book_info["豆瓣简介"],
            "封面图": [
                {
                    "file_token": file_token
                }
            ]
        }
    })
    payload = construct_payload(book_info, file_token)
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {access_token}'
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    print(response.json())
    print(f'飞书接口状态:{response.status_code}')
    return response.status_code
def upload_book_to_feishu(isbn):
    # 获取飞书的 access_token
    token = access_token()

    # 获取书籍信息
    book_info = get_book_info_by_isbn(isbn)
    print(book_info)

    # 下载封面图片
    cover_url = book_info['封面图链接']
    save_path = f"./{isbn}.jpg"
    cover_file_path = download_cover_image(cover_url, save_path)

    if cover_file_path:
        # 上传封面图片至飞书并获取file_token
        file_token = upload_media(cover_file_path, token)

        if file_token:
            # 插入书籍信息到飞书表格
            status_code = insert_book_info_to_feishu(book_info, file_token, token)
            return status_code
        else:
            print("封面图片上传失败")
    else:
        print("封面图片下载失败")
#使用示例
def scan_isbn_and_upload():
    print("请使用扫码枪输入ISBN,按 Ctrl+C 结束程序。")
    while True:
        try:
            # 等待用户通过扫码枪输入ISBN
            isbn = input("扫描ISBN: ").strip()  # .strip() 用于去除前后多余的空白和换行符

            if isbn:
                print(f"正在处理ISBN: {isbn}")
                upload_book_to_feishu(isbn)  # 调用函数上传书籍信息
                print(f"ISBN: {isbn} 处理完成\n")
            else:
                print("未检测到有效的ISBN,请重试。")

        except KeyboardInterrupt:
            # 用户按下 Ctrl+C 后,退出循环
            print("\n程序已终止。")
            break
#调用函数,开始监听扫码输入
scan_isbn_and_upload()

题图来自 Hermann Kollinger, Pixabay

> 关注 少数派小红书,感受精彩数字生活 🍃

> 实用、好用的 正版软件,少数派为你呈现 🚀