MoreRSS

site iconKingname | 谢乾坤修改

精通数据抓取、爬虫。网易 - 字节 - 红杉中国。 微软 MVP。 出版了《Python爬虫开发 从入门到实战》。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Kingname | 谢乾坤的 RSS 预览

一日一技:315晚会曝光的获客软件是什么原理

2025-03-16 07:44:29

今年315晚会曝光了几个获客软件,号称可以拦截任何人的网络浏览记录,并根据对方在直播软件的留言、打过的电话、浏览过的网址,获取对方的手机号和微信号。还有在地图上随便画一个圈,就能找到圈里面130万人的联系方式。

作为一个软件工程师,我来说说我对他们背后原理的猜测。

晚会里面笼统的说到他们使用了爬虫技术。其实这种说法并不准确。爬虫做不到这种程度。爬虫只能爬取到人眼能看到的各种公开数据。例如有人在直播软件下面回复了评论,爬虫能爬到评论人的用户昵称、评论的内容。但是因为评论人的真名、手机号码和微信号并没有显示在直播软件上,所以爬虫是不能爬到的。它后续还需要使用撞库、社工库、社会工程学等等一系列操作,才能定位到用户的手机号。

以它直播软件获客这个例子,我觉得它背后的原理是这样的:

  1. 获客公司有大量的爬虫,他会在各种社交网站上面爬取每个人公开的信息。例如微博、小红书、某些论坛等等。然后把这些信息储存在数据库中。也会记录他们的发帖、回帖。
  2. 收集各种社工库泄露出来的信息,也储存在数据库中。这些社工库里面可能包含了某些著名的社交网站。
  3. 根据用户需求,在某个特定的直播中,抓到其他用户的评论,发现这个评论显示用户对直播的产品有兴趣。
  4. 根据这个用户的用户名,去撞库。因为根据社会工程学的原理,很多人在多个不同的网站,会使用相同的用户名,因此通过用户名去撞库,能够把某人在不同社交网站上面的账号关联起来。
  5. 先看社工库里面,这个用户名对应的用户有没有联系方式,如果有,搞定
  6. 如果社工库没有联系方式,再去搜索这个人其他社交网络上面的发帖回帖记录,有很多人会在别人的帖子下面回复自己的手机号或者邮箱。(例如早期很多人在贴吧、在58同城、在某些招聘论坛的帖子下面,都会发布自己的联系方式)
  7. 某些国产手机的系统里面,会内置广告联盟的SDK,这些SDK会监控手机屏幕上面的各种操作,甚至截屏上传。这些SDK厂商也会出售获得的用户信息。

再说说它在地图上随便画一个圈,就能找到联系方式这个能力。我怀疑它是使用了WIFI探针加上商场的WIFI。

如果我今天刚刚买了一个新的手机卡,把它插在手机上,我不太相信他们能够随便画一个圈,就把我的新手机号获取到了。肯定有一个地方会泄露手机号。那么泄露途径可能有如下几个:

  1. 快递订单。他们通过各种渠道,获取到快递订单。订单上面有地址和手机号。这样简单直接把地址和手机号建立了联系。
  2. WIFI探针+商场WIFI。很多商场为了定位客流量,都会安装WIFI探针。当我们拿着手机在商场走的时候,即便我们没有连接商场的WIFI,他们也能拿到我的手机无线网卡的mac地址。但这个时候它还没有办法拿到我的手机号。它只能知道有一个人,此刻站在第几层哪个门店前面。但由于提供这种客流定位系统的公司,一般都是那几家大公司,因此他们此时已经收集到了大量的手机无线网卡mac地址。如果某一天,我在某个商场正好连了他们的WIFI,一般连这种公共WIFI都需要输入手机号的,这个时候我的手机号就跟mac地址绑定了。以后即使我走到了另一个城市另一个商场,即使我没有连WIFI,只要这个WIFI探针的供应商或者客流定位系统是同一个公司,那么他们立刻就能知道这个手机号现在到这里了。
  3. 有了手机号,结合社工库,各种信息也都能获取到。

再说一说根据网站访问记录获取手机号。这个我只能说是运营商信息泄露了。2017年,我在北京某公司工作的时候,就拿到过这种运营商数据。不过当时这种数据是脱敏过的。用户信息是md5值,只能根据不同的md5值判断这些请求是不同人的设备发送的,但无法知道具体是谁。这种情况是合法的,本来就有这种公开运营商数据买卖。市面上很多做尽职调查的公司都会采购。提供这种运营商数据的公司,他们会在运营商的机房里面安装记录设备,记录详细信息,然后经过脱敏以后卖给下游公司。

但说不定他们自己也会把没有脱敏的数据经过特殊渠道卖出去,于是就有了今年晚会上的这种功能。

有同学可能会担心这种运营商数据,是不是会把自己访问的每一个URL都记录下来?其实大可不必担心,我们要相信HTTPS。对于使用了HTTPS的网站,运营商那边拿到的数据只能定位到你访问的域名,但无法知道具体的网址。例如你访问了https://xxx.com/aa/bb/cc,运营商记录只能拿到https://xxx.com。无法拿到后面的具体地址。除非他们在你的手机上安装了根证书。所以不要安装来历不明的证书,是保证数据安全的重要前提。

实际上不仅是运营商数据会被出售,银行卡、信用卡、POS机数据也会被出售。有一些做尽职调查的公司,如果要调查某教育机构的学生报名情况,他们会从刷卡数据中筛选出支付给这个教育机构的费用,这样就能算出机构的课程报名情况了。

从上面的分析可以看出,其实要获取一个人的个人信息,爬虫在里面发挥的作用其实是最无足轻重的。随便一个数据的泄露,产生的影响远远超过爬虫。

以上技术方法都是我个人的猜测。都是基于著名的直播软件不可能主动买用户手机号这个前提来做的猜测。

一日一技:我的Cursor开发经验

2025-03-14 05:41:41

这两天我使用Cursor开发了一个新闻网站的前端+后端。在开发的过程中,我总结了一些适合于我自己的最佳实践。这些方法让我在使用Cursor的时候,几乎没有遇到任何阻碍,非常顺利,非常流畅地完成了网站的开发。

我的开发经验,总结起来一句话就能说清楚:多写文档少聊天。下面我来详细说一下具体方法。

我全程使用Cursor的agent模式,模型使用Claude 3.7 Sonnet。这个项目是一个新闻网站,需要写前端+后端。

前端我首先使用Trickle生成了页面。大家也可以使用Bolt.new或者lovable,效果都差不多。需要和后端交互的地方都先使用假数据模拟。生成好以后,把代码下载到本地。

改写前端代码

使用Cursor打开下载的前端代码,让它阅读代码,并使用Next.js + tailwind css + shadcn/ui改写代码。并特别提醒,新版本的shadcn/ui对应的命令应该是npx shadcn xxx,让他不要再使用老版本的写法。

改写完成以后,执行npm run dev预览前端页面,确保改写以后的效果跟你之前的一样。

创建临时API文档

由于前端页面本来就是你设计的,因此你肯定很清楚这个前端页面在哪些地方需要跟后端做交互。

现在,在代码根目录创建一个markdown文件,例如叫做api_desc.md,然后在里面描述你的后端API。这里描述不需要写得很细节,关键是要写清楚api的功能,路径,参数和返回示例。如下图所示。

由于新闻列表页和详情页需要从MongoDB里面读取,因此再创建一个article_schema.md,在里面描述数据在MongoDB中的储存结构。如下图所示:

接下来,在Cursor的聊天窗口中,新开一个后端代码专用的对话,让它创建一个后端服务:

1
请使用FastAPI帮我创建一个后端服务。这个服务包含多个API接口,接口的需求请参考api_desc.md。列表页的数据和详情页的数据,需要从MongoDB读取,数据结构参考article_schema.md。后端代码写好以后,需要生成一个api_doc.md文件,里面详细描述API的请求地址、参数和返回值格式。以后每次对后端API做修改,都需要同步更新api_doc.md

Cursor执行完成以后,不仅生成了后端代码,还生成了API文档,文档如下图所示。

这个API文档可以说是相当的标准。

前后端对接

回到前端的对话历史中,让Cursor对接后端:

1
现在,我们要开始对接后端API。后端的接口我已经写到了api_doc.md文件中,请仔细阅读,然后修改前端代码的对应位置,把原来的假数据代码改为请求后端API获得正式数据。

Cursor修改以后,第一个前后端联动的版本就已经完成了。正常情况下应该已经可以使用了。如果遇到一些小的报错,你可以通过对话的形式让它修复。

进一步开发

接下来,你可能需要在已有的API上面修改字段,或者新增API接口。这种情况下一般都是先让Cursor修改后端代码。它修改以后,会自动更新api_doc.md文件。然后再让前端代码基于api_doc.md适配后端的修改。

如果你的修改是小修改,例如在列表页API中添加一个tags参数,用来根据tags过滤新闻,那么你可以直接通过聊天对话的形式跟Cursor沟通,让它完成。

如果你的修改是一个相当完整独立的新功能,那么你可以新增一个需求文档,例如要做一个新闻订阅页面,这个时候就可以创建subscribe_api.md文件,在里面描述自己需要的API,从而先生成后端代码和API文档,再创建一个subscribe.md,在里面描述前端页面的需求,并让Cursor基于这个需求加上API文档,生成前端代码。

总结

在使用Cursor的时候,我更倾向于人机协作开发而不是让你当甩手掌柜。你需要给Cursor提供必要的指导,从而让它顺着你的思路来做开发。你的脑子里面要有这个系统的开发路线和架构,你需要知道系统由哪些部分组成,每个部分需要怎么做。软件开发是系统设计+编码。让Cursor去做编码工作,而不是去做设计工作。

不要相信网上那些完全不懂代码的人纯靠文本描述就做出复杂功能的说辞,要不就是他们嘴里的复杂功能其实是简单功能,要不就是他在吹牛。

你应该多写文档,通过文档来描述你的需求。这样Cursor以后的每次修改都会注意不违背你的需求文档。聊天窗口一般是告诉Cursor应该使用哪个文档来进行开发。尽量不要在聊天窗口里面提需求。

一日一技:如何实现临时密码?

2025-02-25 04:26:24

我买的房子今天交房了。开发商配的门锁是某品牌的智能门锁,它可以使用指纹开锁,也可以使用密码开锁。在使用手机跟门锁配对以后,可以远程在手机上生成临时密码。临时密码只能使用1次,并且在生成的30分钟内有效。这个功能可以方便装修人员进出又不用担心泄露密码。

因为新房子还没有通网,所以门锁肯定是无法连接互联网的。而装修人员给我打电话要临时密码时,我在公司,离家几十公里外,门锁也不可能跟手机通信。

那么问题来了,门锁是怎么验证这个临时密码合法的?

今天我一直在想这个问题,目前有一些思路,但无法确定。所以发出来跟大家一起讨论一下它的实现方法。

已知:

  1. 手机App只有第一次跟门锁配对时,会通信,之后就完全不会有任何通信
  2. 门锁无法连接外网
  3. 无论我在任何地方,手机上都能生成临时密码。门锁输入临时密码就能解锁
  4. 临时密码只能使用一次,之后就会失效
  5. 临时密码是8位数字
  6. 临时密码有效期30分钟,超时以后就会失效
  7. 手机可以连续多次生成临时密码,每一次密码都不一样,但每个临时密码都可以使用

首先第4条非常简单,在门锁里面记录一下已经使用的密码就可以实现密码只能使用1次。所以不需要考虑这个问题了。

另外几个问题,我根据我自己的编程经验做一些推测。

临时密码是一个8位数字,例如8031 1257。由于手机不需要跟门锁通信,门锁就能够识别这个密码,因此我一开始觉得这个8位数字包含某种校验规则。例如,前4个数字,乘以100以后对26取余数,就是第5、6位数字。前6个数乘以5643然后对97取余数,就是第7、8位数字。这里的四个关键数字10026564397,可能是手机在和门锁配对的时候发送给门锁的。

但这里无法解释门锁怎么知道数字什么时候过期。难道8位数字能够包含精确到分钟的时间戳信息?例如现在我写文章的时候,对应的时间戳是1740143928。这是一个10位数字,我实在想不到如何把一个10位的数字藏在8位数字里面,并且在必要的时候还能还原回原来的10位数字。

因此,我换了一个思路,有没有可能密码锁里面自带一个时钟?在手机配对时,会同步校准这个时钟,使它跟手机保持相同的时间。如果是这种方案,那么这个临时密码8位数字,其实可以不用包含自我校验。我们可以使用app和密码锁各自按相同的逻辑走一轮加密,然后对比生成的密钥是否相同。

手机在跟密码锁配对时,发送一个密钥到密码锁里面。要生成临时密码时,手机使用时间戳和密钥通过某个算法生成8个数字。密码锁也使用时间戳和这个相同的密钥,相同的算法,也生成8位数字,如果跟临时密码相同,就开锁。对应的Python密码类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib
import time


SECRET_KEY = 123456 # 手机同步给密码锁的密钥
now = int(time.time())
timespan = now // 1800 # 在30分钟内,这个值都是相同的

temp_key_hex = hashlib.md5(str(SECRET_KEY + timespan).encode()).hexdigest() # 16进制的密码
temp_key_full = int(temp_key_hex, 16) # 转成10进制

temp_key = str(temp_key_full)[-8:] # 取最后8位数字
print('临时密码:', temp_key)

运行效果如下图所示:

由于从任何时间戳x开始,x // 1800 == (x + n) // 1800x // 1800 + 1 == (x + n) // 1800,其中0 <= n <= 1800。密码锁使用相同的代码,只不过分别把timespantimespan + 1都生成一个密码,然后对比,这样就可以实现手机生成密码过几分钟再在锁上面输入密码,锁也能成功验证。

使用这种方式,可以满足编号1-6的需求。但问题是遇到需求7怎么办?上面这种方式,会导致在30分钟内,临时密码只有这一个。

我绞尽脑汁想不出一个聪明的算法能解决这个问题。但是我在多次尝试生成临时密码时,发现两次密码的生成间隔必须大于5秒。

如果它的算法真的那么聪明,为什么不能让我生成无限个可用的临时密码?所以我怀疑它可能没有那么聪明,它可能用的是笨办法,例如:穷举。

对手机来说,生成密码的算法跟上面差不多,唯一的区别是,把其中的1800换成5。也就是说,每5秒钟会生成不同的临时密码。因为now // 5在5秒钟内是相同的,超过5秒就会变。

而对于门锁,当它感知到用户正在输入密码时,以当前时间戳为起点,每5秒一轮,往前推360轮,生成360个密码。如果发现用户输入的8位数跟这360个密码中的某一个相同,就解锁。

如果锁的芯片性能好,在用户按完临时密码前,就能够做完360次计算,用户完全不会有任何感知。如果锁的芯片不够好,它可以在内存中维持一个长度为360的双向链表,每5秒计算一个密码放进去,然后把末尾的密码丢掉。这样当用户输入密码的时候,它可以直接跟这个双向链表中的密码逐一比对。

当然,以上全都是我个人的推测。如果大家知道它是怎么做的,或者想到了什么更好的方法,欢迎留言一起交流。

一日一技:如何使用Cursor学习开源项目

2025-01-30 07:09:16

大家肯定经常在微信公众号里面看到类似于《30秒使用Cursor开发xxx》这种文章。典型的标题党装逼货,大家当个笑话看就行了。

Cursor目前还没有强到真的让一个完全不懂代码的人轻轻松松开发一个有用的软件,但Cursor确实可以让懂代码的人如虎添翼。正好最近有不少同学在群里面问我,如何正确使用Cursor:

那么今天我就来讲讲我使用Cursor的一个场景:快速理解开源项目的核心逻辑。

Cline为例,这是一个VSCode插件,能够让VSCode实现Cursor的功能,配合DeepSeek最新模型,有人声称可以完美平替Cursor。那么,如果我完全看懂了Cline的原理,也就相当于看懂了Cursor的实现原理了。那么我们来看看如何使用Cursor辅助我学习Cline的源代码。

首先把Cline的代码clone到本地,然后用Cursor打开。如下图所示:

这个时候,如果是完全不懂代码的人,肯定一上来就让Cursor解释这个项目的原理。但这个插件的代码量还是挺大的,完全没有重点的让Cursor来解释,只会得到一个大而空的整体解释,对你的学习没有任何帮助。

我们作为工程师,在提问之前,一定要对我们想问的东西有一个初步的了解,否则没有办法提出有用的问题。要初步了解一个程序项目,第一步肯定是看一下这个项目的文件结构,通过它的文件结构,应该能够知道它每个文件夹里面的代码大概是什么功能。这样一来可以直接略过不太重要的部分。例如这个项目是一个VSCode插件,那么里面肯定有一部分代码是为了让他能被VSCode识别和调用。这种代码我们完全不需要关心。我们只需要关心它怎么让AI生成代码,怎么自动修改代码就可以了。

这就像是在拿到一本新书的时候,我一般会先看书的目录,知道这本书的整体结构,然后再带着问题来读书。

浏览一下这个项目文件结构,可以看到,AI生成代码的相关逻辑,应该在src/core文件夹里面。其中src/core/prompts里面是相关的提示词,src/core/assistant-message里面是解析大模型返回的XML并实现自动化操作的逻辑。

Cline的功能跟Cursor很像,能自动执行命令,能自动生成文件,能修改已经有的文件。

以Cline自动修改已有文件这个功能为例。假设我们自己的程序已经有不少代码了,现在我在安装了Cline的VSCode中,让AI帮我给这个项目增加一些功能。它的流程肯定是这样的:

  1. 读取已经有的代码
  2. 构造出一段Prompt,里面包含已经有的代码以及我们的新需求,调用大模型
  3. 大模型返回一段内容,Cline解析这段内容,根据里面的提示,修改对应的文件中的对应的部分。

现在,我就想学习一下,大模型返回的内容长什么样?Cline是怎么解析这段内容,并让他变成对文件的操作的?

所以,我首先在Cursor中提出第一个问题:

1
2
@folder src/core/assistant-message
这是cline这个自动化编程copilot在收到大模型返回的信息以后,对信息进行处理的逻辑。请阅读它的代码并告诉我它的解析逻辑。

如下图所示

从它返回的内容,我们可以知道,大模型返回给Cline的内容是XML格式,Cline解析这个XML,从而进一步执行具体的操作。在它返回的内容中,支持的操作包含下面这一段内容:

我最关心的就是replace_in_file这个功能是怎么实现的,所以我进一步提问:

1
详细解释一下replace_in_file的具体逻辑和流程

返回的部分内容如下:

这段内容比较长,我总结一下它返回的重点:

  1. 显示了大模型返回的内容格式
  2. 代码里面如何解析大模型返回的内容
  3. 如何修改代码

它解释得已经比较清楚了,但由于Cline是使用JavaScript语法写的,有些同学可能对JS没有Python熟悉,所以,我们让大模型再做一步翻译,把核心代码改写成Python,并且创建一个Demo来运行这段Python代码:

1
2
3
4
5
6
7
现在,为了便于我的理解,请帮我实现一个replace_in_file 的Python版本。请在项目根目录创建一个example文件夹。这个文件夹里面有4个文件,分别为:

1. example_llm_response.txt:假设一段从大模型返回的内容
2. example_old.py:一段需要被修改的代码
3. replacer.py: Python版本的replace_in_file

当我运行replacer.py以后,它应该能够根据example_llm_response.txt中的内容,修改example_old.py,然后生成example_new.py

如下图所示:

我们可以先看一下它生成的example_llm_response.txt,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我会帮你修复calculate_multiply函数中的bug。

<replace_in_file>
<diff>
<<<<<<< SEARCH
def calculate_multiply(a, b):
# 这是一个有bug的乘法函数
return a + b # 这里错误地使用了加法
=======
def calculate_multiply(a, b):
# 修复后的乘法函数
return a * b # 修正为正确的乘法运算
>>>>>>> REPLACE
</diff>
</replace_in_file>

现在乘法函数已经修复了,它会返回正确的结果。

需要被修改的,有问题的example_old.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def calculate_sum(a, b):
# 计算两个数的和
return a + b

def calculate_multiply(a, b):
# 这是一个有bug的乘法函数
return a + b # 这里错误地使用了加法

def greet(name):
# 打招呼函数
print("Hello " + name)

if __name__ == "__main__":
result = calculate_multiply(3, 4)
print(f"3 x 4 = {result}") # 这里会输出错误结果

直接运行,会看到最后输出的结果是错误的:

现在运行replacer.py,会自动生成example_new.py,内容如下:

可以看到,输出的结果已经正确了。虽然新代码最后一行的注释还有问题,但毕竟这个返回的内容是模拟的,所以可以理解。

现在,我们直接阅读replacer.py文件,就可以用Python的语法来理解Cline的逻辑了。生成的代码不依赖任何第三方库,因此理论上可以在任何能够运行Python的环境运行。大家还可以把自己的一些想法直接改到代码上,来测试它的运行效果。

生成的代码这里是使用正则表达式来提取XML。在正式项目中,肯定需要使用专门的XML解析模块来解析。不过这个Demo使用正则表达式反而帮我们能更好理解代码。

完整的代码我就不贴上来了,有Cursor的同学可以使用Cursor试一试。没有Cursor的同学可以使用Cline + DeepSeek来试一试,得到的结果应该跟我这个是一样的。

再附上我使用Cusror解析Bolt.new的代码结构,并通过Mermaid语法生成的时序图:

总结

Cursor不仅可以写代码,还能帮我们学习代码。大家在提问时,一定要针对某个功能精确提问,只有你的问题越具体,它返回的内容才会越具体。

一日一技:如何用编程的方式来编排工作流

2025-01-23 07:13:26

使用过Dify的同学都知道,你可以在上面拖动方框和箭头来编排大模型的逻辑,如下图所示。


这种拖动框图编排工作流的方式,确实非常简单方便,以至于不会代码的人也可以用来编排大模型Agent。但你有没有考虑过一个问题——你作为一个工程师,有没有可能通过写代码的形式来编排工作流?否则你和不懂代码的人相比有什么竞争力?

CrewAI是一个Agent开发框架,通过它可以非常方便地开发Agent。它提供的Flow功能,可以用来以编程的方式构建工作流。我向来推崇重器轻用的原则,虽然CrewAI是用来做Agent开发的,但它的Flow功能也可以用在不含AI的任何工程代码中。

我们来看一个例子。现在你要从硬盘中读取doc.txt文件,把里面的所有字母转换为大写。然后保存为doc_upper.txt。按常规的写法,我们把这个任务分为3步:

  1. 读取文件
  2. 转换大小写
  3. 写入文件

那么常规代码可能是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def step_1_read_file():
with open('doc.txt') as f:
content = f.read()
return content

def step_2_to_upper(content):
return content.upper()

def step_3_save_file(content):
with open('doc_upper.txt', 'w') as f:
f.write(content)

def start():
content = step_1_read_file()
content_upper = step_2_to_upper(content)
step_3_save_file(content_upper)


start()

其中函数start就是用来控制代码的工作流。

现在,我们使用crewAI的flow功能来重构这个代码,那么代码可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from crewai.flow.flow import Flow, listen, start
class UpperTask(Flow):
@start()
def step_1_read_file(self):
with open('doc.txt') as f:
content = f.read()
return content

@listen(step_1_read_file)
def step_2_to_upper(self, content):
return content.upper()

@listen(step_2_to_upper)
def step_3_save_file(self, content):
with open('doc_upper.txt', 'w') as f:
f.write(content)


flow = UpperTask()
result = flow.kickoff()

工作流会从@start装饰器装饰的方法开始运行。@listen装饰器用来装饰后续的每一个节点。当@listen参数对应的节点运行完成以后,就会自动触发自身装饰的节点。被listen的节点return的数据,就会作为参数传入当前节点。而kickoff()会返回最后一个被@listen装饰的节点的返回值。

Flow还支持状态管理、条件逻辑和路由控制。详情可以查看官方文档。Flow更方便的地方在于,它可以把你的工作流可视化出来,例如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import random
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel

class ExampleState(BaseModel):
success_flag: bool = False

class RouterFlow(Flow[ExampleState]):

@start()
def start_method(self):
print("Starting the structured flow")
random_boolean = random.choice([True, False])
self.state.success_flag = random_boolean

@router(start_method)
def second_method(self):
if self.state.success_flag:
return "success"
else:
return "failed"

@listen("success")
def third_method(self):
print("Third method running")

@listen("failed")
def fourth_method(self):
print("Fourth method running")


flow = RouterFlow()
flow.plot('test')

生成的流程图如下图所示:

对于简单的逻辑,可能不好区分使用Flow和常规写法的区别。但当你的代码流程多起来,逻辑复杂起来以后,使用Flow就会方便很多。

一日一技:如何使用大模型提取结构化数据

2025-01-21 04:52:58

经常有同学在微信群里面咨询,如何使用大模型从非结构化的信息里面提取出结构化的内容。最常见的就是从网页源代码或者长报告中提取各种字段和数据。

最直接,最常规的方法,肯定就是直接写Prompt,然后把非结构化的长文本放到Prompt里面,类似于下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from zhipuai import ZhipuAI
client = ZhipuAI(api_key="") # 填写您自己的APIKey
response = client.chat.completions.create(
model="glm-4-air-0111",
messages=[
{"role": "system", "content": '''你是一个数据提取专家,非常善于从
从长文本中,提取结构化的数据。
'''},
{"role": "user", "content": '''你需要从下面的文本中,提取出姓名,工资,地址,然后以JSON格式返回。返回字段示例:{"name": "xxx", "salary": "yyy", "address": "zzz"}.只需要返回JSON字符串就可以了,不要解释,不要返回无关的内容。

"""
长文本
"""
'''}
],
)
print(response.choices[0].message)

如果你每次只需要提取一两个数据,用这种方式确实没什么问题。不过正如我之前一篇文章《一日一技:超简单方法显著提高大模型答案质量》中所说,返回的JSON不一定是标准格式,你需要通过多种方式来强迫大模型以标准JSON返回。并且要使用一些Prompt技巧,来让大模型返回你需要的字段,不要随意乱编字段名。

当你需要提取的数据非常多时,使用上面这种方法就非常麻烦了。例如我们打开某个二手房网站,它上面某个楼盘的信息如下图所示:

一方面是因为字段比较多,你使用纯文本的Prompt并不好描述字段。另一方面是HTML原文很长,这种情况基于纯Prompt的提取,字段名会不稳定,例如占地面积,有时候它给你返回floor_area有时候返回floorArea有时候又是其他词。但如果你直接在Prompt给出一个字段示例,例如:

1
2
3
4
5
6
7
8
9
……上面是一大堆描述……

返回的字段必须按如下示例返回:

{
"floor_area": 100,
"building_area": 899
...
}

有时候你会发现,对于多个不同的楼盘,大模型返回给里的floor_area的值都是100,因为它直接把你的例子中的示例数据给返回了。

如果你只是写个Demo,你可能会觉得大模型真是天然适合做结构化数据的提取,又方便又准确。但当你真的尝试过几百次,几千次不同文本中的结构化数据提取后,你会发现里面太多的坑。

好在,Python有一个专门的第三方库,用来从非结构化的数据中提取结构化的信息,并且已经经过了深度的优化,大量常见的坑都已经被解决掉了。配合Python专门的结构化数据校验模块Pydantic,能够让提取出来的数据直接以类的形式储存,方便后续的使用。

这个模块叫做Instructor。使用这个模块,我们只需要先在Pydantic中定义好结果的数据结构,就能从长文本中提取数据。并且代码非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import instructor
from pydantic import BaseModel
from openai import OpenAI

# Define your desired output structure
class ExtractUser(BaseModel):
name: str
age: int

# Patch the OpenAI client
client = instructor.from_openai(OpenAI())

# Extract structured data from natural language
res = client.chat.completions.create(
model="gpt-4o-mini",
response_model=ExtractUser,
messages=[{"role": "user", "content": "John Doe is 30 years old."}],
)

assert res.name == "John Doe"
assert res.age == 30

当然,正如我前面说的,一个小小的Demo能够完美运行并不能说明任何问题,我们要使用更多的实际例子来进行测试。假设我们的场景就是爬虫解析HTML,从上面的二手房网站提取房屋信息。

考虑到大部分情况下,HTML都非常长,即便我们提前对HTML代码做了精简,移除了<style><script>等等标签,剩余的内容都会消耗大量的Token。因此我们需要选择一个支持长上下文,同时价格又相对便宜的大模型来进行提取。

正好智谱最近升级了GLM-4-Air系列大模型,最新的GLM-4-Air-0111模型,Token费用直接减半,每1000 Token只需要0.0005 元,每100万Token只需要5毛钱。而模型的智力跟旗舰模型GLM-4-Plus相差不大,因此非常适合用来做数据提取的任务。

Instructor本身不直接支持智谱的模型,因此需要使用它提供的LiteLLM配合智谱的OpenAI兼容接口来实现对接。

首先使用pip命令安装支持LiteLLM的Instructor:

1
pip install 'instructor[litellm]'

然后通过下面这样的代码就可以借助LiteLLM来链接智谱大模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import instructor
from litellm import completion
client = instructor.from_litellm(completion)
resp = client.chat.completions.create(
model="openai/glm-4-air-0111",
api_key="对应的API Key",
api_base="https://open.bigmodel.cn/api/paas/v4/",
max_tokens=1024,
messages=[
{
"role": "user",
"content": html,
}
],
response_model=HouseInfo,
)

其中的HouseInfo定义的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pydantic import BaseModel, Field

class HouseInfo(BaseModel):
floor_area: int = Field(description="占地面积")
building_area: int = Field(description="建筑面积")
plot_ratio: int = Field(description="容积率")
greening_rate: int = Field(description="绿化率")
total_buildings: int = Field(description="楼栋总数")
total_households: int = Field(description="总户数")
property_management_company: str = Field(description="物业公司")
property_management_fee: str = Field(description="物业费")
property_management_fee_description: str = Field(description="物业费描述")
parking_spaces: str = Field(description="停车位")
parking_space_description: str = Field(description="停车位描述")
floor_status: str = Field(description="楼层状况")

这就是一个标准的Pydantic类,定义了字段的名字,类型和意义。在调用Instructor时,传入这个类,传入精简以后的网页源代码,就能直接从网页中提取出对应的字段了。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import instructor
from litellm import completion
from pydantic import BaseModel, Field

class HouseInfo(BaseModel):
floor_area: int = Field(description="占地面积")
building_area: int = Field(description="建筑面积")
plot_ratio: int = Field(description="容积率")
greening_rate: int = Field(description="绿化率")
total_buildings: int = Field(description="楼栋总数")
total_households: int = Field(description="总户数")
property_management_company: str = Field(description="物业公司")
property_management_fee: str = Field(description="物业费")
property_management_fee_description: str = Field(description="物业费描述")
parking_spaces: str = Field(description="停车位")
parking_space_description: str = Field(description="停车位描述")
floor_status: str = Field(description="楼层状况")

client = instructor.from_litellm(completion)

html = '''
精简以后的HTML代码
'''

resp = client.chat.completions.create(
model="openai/glm-4-air-0111",
api_key="你的API Key",
api_base="https://open.bigmodel.cn/api/paas/v4/",
max_tokens=1024,
messages=[
{
"role": "user",
"content": html,
}
],
response_model=HouseInfo,
)

print(resp.model_dump_json(indent=2))
print(f'提取到的占地面积是:{resp.floor_area}')

运行情况如下图所示:

得到的resp就是一个Pydantic对象,可以直接使用resp.floor_area来查看每个字段,也可以使用resp.model_dump_json转成JSON字符串。

Pydantic还可以指定一些字段是可选字段,一些字段是必选字段,也可以自动做类型转换,这些语法都可以在Instructor的Tips中看到。

总结一下,使用Instructor,配合智谱GLM-4-Air-0111模型,可以大大提高结构化信息的提取效率。