2024年3月

之前我们已经详细讨论了如何使用BeautifulSoup这个强大的工具来解析HTML页面,另外还介绍了利用在线工具来抓取HTTP请求以获取数据的方法。在今天的学习中,我们将继续探讨另一种常见的网络爬虫技巧:XPath。XPath是一种用于定位和选择XML文档中特定部分的语言,虽然它最初是为XML设计的,但同样适用于HTML文档的解析。

HTML和XML有很多相似之处,比如标签、属性等,因此XPath同样可以在HTML文档中有效地定位元素。爬虫可以利用XPath表达式来指定需要提取的数据的位置,然后通过XPath解析器来解析HTML文档,从而提取所需的信息。

好的,我们不多说,直接开始今天的任务,爬取36kr的热榜新闻以及新闻搜索。

XPath爬虫

如果对XPath不熟悉也没关系,可以直接使用它,就能发现它与我们之前使用的BeautifulSoup有着相同的目的。只是在表达式和方法的使用上略有不同。在进行爬虫之前,我们可以先下载一个XPath工具。之前我们编写BeautifulSoup代码时,需要自行查找HTML代码中的标签并编写代码进行解析,这样很费眼。而在浏览器中可以使用插件工具来直接提取XPath元素。

XPath插件

有很多浏览器插件可供选择,我们只需直接获取一个即可。最重要的是,这些插件可以让我们在选择时轻松复制表达式,就像这样:

image

当我打开插件工具后,立即触发左键操作,从而开始显示红色框框,用户选择后,系统会呈现一系列XPath表达式供选择,用户只需选取适当的表达式即可。

image

热榜新闻

会使用工具后,我们将继续进行数据爬取和页面信息解析。在此之前,需要安装一个新的依赖库lxml。以下是一个示例代码供参考:

from lxml import etree
import requests
 
hot_article_list = []

headers = {
    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
}
def get_hot_article():
    url = 'https://36kr.com/hot-list/catalog'
    response = requests.get(url=url,headers=headers)
    # 获取的html内容是字节,将其转化为字符串
    #使用etree进行解析
    data = etree.HTML(response.text)

    # 使用XPath定位元素
    # 提取a标签的article-item-title文本数据以及url连接
    article_titles = data.xpath("(//a[@class='article-item-title weight-bold'])")
    article_desc = data.xpath("(//a[@class='article-item-description ellipsis-2'])")

    if len(article_titles) == len(article_desc):
        for article, desc in zip(article_titles, article_desc):
            # 获取元素的链接(href 属性)
            link = article.get('href')
            # 获取元素的文本内容
            title = article.text
            desc = desc.text
            hot_article_list.append({
                "title":title,
                "link":link,
                "desc":desc
            })
    else:
        print("未找到指定元素")
print(hot_article_list)

这段代码的功能是从36氪网站的热门文章列表中提取文章的标题、链接和描述信息,并将这些信息存储在一个列表中。其中,lxml库用于HTML解析,requests库用于发送HTTP请求。接着,定义了一个空列表hot_article_list,用于存储提取的文章信息。

踩个小坑

在前面已经成功提取了热门文章标题和链接,接下来通常应该开始逐个访问这些链接以查看新闻详情。然而,在发送请求获取单个URL链接时,却未能获得预期的新闻信息,出现了以下情况:

image

通常情况下,网页中的数据要么直接包含在静态HTML中,比如之前我们解析的美食菜谱等;要么是通过Ajax的HTTP请求获取的,比如我们尝试获取腾讯云社区的文章列表。通常,这些数据都可以在搜索中找到相应的匹配项。然而,我花了一个小时的时间仍未能成功获取所需信息。最初,我怀疑可能是因为网页中存在跳转页面传输数据,因此我特意使用抓包工具进行了下载,但令人失望的是,并没有发现相关数据。

因此,我又仔细检查了一遍静态HTML代码,并在代码末尾发现了一个奇怪之处——HTML页面的部分竟然被加密了。让我们来看看这段代码吧。

image

如果你对这些内容感到疑惑,建议再次在搜索框中输入相关关键字以查找更多信息。很可能存在解密函数。果然如此。我们接下来看下。

image

既然官方对数据进行了加密处理,显然是出于一定的考虑,其中可能包括对爬虫的防护等因素。鉴于此,我决定不再尝试对其进行解密操作,这个就这样吧。

信息搜索

36氪网站不仅提供了热门文章信息,还支持新闻搜索功能。让我们深入探讨一下搜索功能的实现方式。通常情况下,静态页面即可满足需求进行信息提取。但若希望获取更多数据,就需要通过发送ajax请求来实现。

image

接着看代码:

from lxml import etree
from urllib.parse import quote
import requests

def get_article_search(keyword):
    qk = quote(keyword)
    url = f'https://36kr.com/search/articles/{qk}'
    response = requests.get(url=url,headers=headers)
    # 获取的html内容是字节,将其转化为字符串
    #使用etree进行解析
    data = etree.HTML(response.text)
    # 使用XPath定位元素
    # 提取a标签的article-item-title文本数据以及url连接
    article_detail = data.xpath("(//p[@class='title-wrapper ellipsis-2']//a)")
    for a_tag in article_detail:
        text = a_tag.xpath("string()").strip()
        url = a_tag.get("href")
        print("文本:", text)
        print("URL连接:", url)



def get_article_url(keyword):

    headers = {
        'Accept': '*/*',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'Connection': 'keep-alive',
        'Content-Type': 'application/json',
        'Cookie': 'Hm_lvt_713123c60a0e86982326bae1a51083e1=1710743069; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2218b40a4b8576e0-0508814adc1724-745d5774-2073600-18b40a4b858109a%22%2C%22%24device_id%22%3A%2218b40a4b8576e0-0508814adc1724-745d5774-2073600-18b40a4b858109a%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_referrer_host%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%7D; Hm_lvt_1684191ccae0314c6254306a8333d090=1710743069; aliyungf_tc=9f944307bb330cb7a00e123533aad0ee8a0e932e77510b0782e3ea63cddc99cf; Hm_lpvt_713123c60a0e86982326bae1a51083e1=1710750569; Hm_lpvt_1684191ccae0314c6254306a8333d090=1710750569',
        'Origin': 'https://36kr.com',
        'Referer': 'https://36kr.com/',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
        'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }

    json_data = {
        'partner_id': 'web',
        'timestamp': 1710751467467,
        'param': {
            'searchType': 'article',
            'searchWord': keyword,
            'sort': 'score',
            'pageSize': 20,
            'pageEvent': 1,
            'pageCallback': 'eyJmaXJzdElkIjo5NSwibGFzdElkIjo1MSwiZmlyc3RDcmVhdGVUaW1lIjo3NTU4MSwibGFzdENyZWF0ZVRpbWUiOjIzOTk3LCJsYXN0UGFyYW0iOiJ7XCJwcmVQYWdlXCI6MSxcIm5leHRQYWdlXCI6MixcInBhZ2VOb1wiOjEsXCJwYWdlU2l6ZVwiOjIwLFwidG90YWxQYWdlXCI6MTAsXCJ0b3RhbENvdW50XCI6MjAwfSJ9',
            'siteId': 1,
            'platformId': 2,
        },
    }

    response = requests.post(
        'https://gateway.36kr.com/api/mis/nav/search/resultbytype',
        headers=headers,
        json=json_data,
    )
    
    data = response.json()
    for parsed_data  in data['data']['itemList']:
        widget_title = parsed_data['widgetTitle'].replace('<em>', '').replace('</em>', '')
        print(widget_title)
        widget_url = parsed_data['route']
        print(widget_url)
        
get_article_search('OpenAI')
get_article_url('我要')

get_article_search

get_article_url
。这两个函数都是用来从36氪网站上获取文章信息的。

  1. get_article_search(keyword)
    :
    • 首先,将关键词进行URL编码。
    • 构建搜索URL并发送GET请求获取页面内容。
    • 使用lxml库的etree模块解析HTML内容。
    • 使用XPath定位元素,提取文章标题和URL连接。
  2. get_article_url(keyword)
    :
    • 函数中定义了请求头(headers)和请求体(json_data)。
    • 发送POST请求到指定的API接口获取文章URL数据。
    • 解析返回的JSON数据,提取文章标题和URL连接。

总结

在这篇文章中,我们深入学习了XPath作为一种常见的网络爬虫技巧。XPath是一种用于定位和选择XML文档中特定部分的语言,尽管最初是为XML设计的,但同样适用于HTML文档的解析。我们探讨了如何使用XPath来定位元素并提取所需信息。

通过这篇文章的学习,我们对XPath的应用有了更深入的了解,也提升了我们在网络爬虫领域的技能。继续努力学习和实践,相信我们可以在爬虫技术上取得更大的进步!

一、场景要求

我们在使用locust时,有时候默认的场景无法满足我们的要求时,这时后我们需要自定义场景

比如我们要设置每一段时间启动10个用户运行,执行60s后再一次启动10个用户,总共运行10分钟,默认的场景是无法满足这样的要求的,我们可以使用LoadTestshape类,LoadTestshape类提供了几种负载测试策略

二、用法

在脚本文件中定义一个类继承LoadTestshape类,locust在启动时发现文件中有使用这个类会自动启动。


在该类中需要定义tick()方法,该方法返回用户数以及产生率的元组(如果没有返回这两个测试将停止),locust启动后每秒调用一次该函数。


在LoadTestshape类中可以使用get_run_time()方法来获取测试运行的时间,使用此方法可以用来控制压测的总时间。

三、基于时间峰值策略

需求:比如我们要设置每一段时间启动10个用户运行,过一段时间后再一次启动10个用户,持续加压 60s

四、代码实现

import os
from locust import *


'''实现目标:每隔一段时间增加十个用户,实现持续加压'''


class CustomTaskSet(LoadTestShape):
# 设置压测时间60s
time_limit=60
#设置启动/停止的用户数
spawn_rate=10
def tick(self):
"""
返回一个元组,包含两值:
user_count -- 总用户数
spawn_rate -- 每秒启动/停止用户数
返回None时,停止负载测试
"""
#获取压测时间
run_time=self.get_run_time()
if run_time<self.time_limit:
#每隔一段时间启动10个用户;为-1时,表示将个位变为0,逢5进一
user_count=round(run_time,-1)
print(f'当前用户数{user_count},当前时间{run_time}')
return user_count,self.spawn_rate

return None

class IncrementalPressureMeasurement(HttpUser):
wait_time =between(1,2)
host="http://localhost:8080"
def on_start(self):

print("负载加压开始")

def on_stop(self):
print("负载加压结束")

@task
def increment_pressure(self):
self.client.post('/measurement',data={'measurement':''})


if __name__ == '__main__':
file_path = os.path.abspath(__file__)
os.system(f'locust -f {file_path} --web-host=127.0.0.1')
五、实现效果
最后我们欣赏下劳动成果吧,haha!

最后,还请大家可以点个免费的赞,你们的点赞才是我更新的动力!

 

好家伙,

demo-general项目运行后主界面如下

解析阿里低开引擎中的初始化方法init

拆解项目来自阿里的lowcode engine目录下的 demo general项目

0.找到入口文件

可以看到整个项目用到的插件非常之多

于是

  1. init:
    init
    方法用于初始化低代码引擎,负责加载各种插件并配置引擎的运行环境。

  2. plugins:
    plugins
    是一个插件集合,包含了多个插件,用于扩展低代码引擎的功能。

  3. createFetchHandler:
    createFetchHandler
    方法用于创建一个数据源的处理器,用于处理数据源相关的操作。

  4. EditorInitPlugin: 编辑器初始化插件,用于初始化低代码引擎的编辑器。

  5. UndoRedoPlugin: 撤销重做插件,提供撤销和重做操作的功能。

  6. ZhEnPlugin: 中英文切换插件,用于实现界面语言的切换。

  7. CodeGenPlugin: 代码生成插件,用于生成代码。

  8. DataSourcePanePlugin: 数据源面板插件,用于管理数据源。

  9. SchemaPlugin: Schema 插件,用于处理数据模型的定义和管理。

  10. CodeEditorPlugin: 代码编辑器插件,用于提供代码编辑功能。

  11. ManualPlugin: 手册插件,提供用户手册和帮助文档。

  12. InjectPlugin: 注入插件,用于注入特定功能或代码。

  13. SimulatorResizerPlugin: 模拟器调整插件,用于调整模拟器的大小。

  14. ComponentPanelPlugin: 组件面板插件,用于管理可用组件。

  15. DefaultSettersRegistryPlugin: 默认设置注册插件,用于注册默认设置。

  16. LoadIncrementalAssetsWidgetPlugin: 加载增量资源小部件插件,用于加载增量资源。

  17. SaveSamplePlugin: 保存示例插件,用于保存示例代码。

  18. PreviewSamplePlugin: 预览示例插件,用于预览示例代码。

  19. CustomSetterSamplePlugin: 自定义设置示例插件,用于自定义设置示例。

  20. SetRefPropPlugin: 设置引用属性插件,用于设置引用属性。

  21. LogoSamplePlugin: Logo 示例插件,用于展示 Logo 示例。

  22. SimulatorLocalePlugin: 模拟器语言插件,用于设置模拟器的语言。

  23. lowcodePlugin: 低代码组件插件,用于提供低代码组件功能。

  24. appHelper: 应用程序辅助方法,可能包含一些辅助函数或工具函数。

  25. global.scss: 全局样式文件,定义了全局的样式规则

那么把我们主要要解析的文件拿出来

就这行

import { init, plugins } from '@alilc/lowcode-engine';

1.官方文档定位包位置

2.在lowcode-engine中寻找init方法

直接找到

engine-core引擎核心,是他没错了

我们来看看这段代码到底在做什么?

//engine-core.ts
export async functioninit(
container
?: HTMLElement,
options
?: IPublicTypeEngineOptions,
pluginPreference
?: PluginPreference,
) {
await destroy();
let engineOptions
= null;if(isPlainObject(container)) {
engineOptions
=container;
engineContainer
= document.createElement('div');
engineContainer.
id = 'engine';
document.body.appendChild(engineContainer);
}
else{
engineOptions
=options;
engineContainer
=container;if (!container) {
engineContainer
= document.createElement('div');
engineContainer.
id = 'engine';
document.body.appendChild(engineContainer);
}
}
engineConfig.setEngineOptions(engineOptions as any);

const { Workbench }
=common.skeletonCabin;if (options &&options.enableWorkspaceMode) {
const disposeFun
=await pluginPromise;
disposeFun
&&disposeFun();
render(
createElement(WorkSpaceWorkbench, {
workspace: innerWorkspace,
//skeleton: workspace.skeleton,
className: 'engine-main',
topAreaItemClassName:
'engine-actionitem',
}),
engineContainer,
);
innerWorkspace.enableAutoOpenFirstWindow
= engineConfig.get('enableAutoOpenFirstWindow', true);
innerWorkspace.setActive(
true);
innerWorkspace.initWindow();
innerHotkey.activate(
false);
await innerWorkspace.plugins.init(pluginPreference);
return;
}

await plugins.init(pluginPreference as any);

render(
createElement(Workbench, {
skeleton: innerSkeleton,
className:
'engine-main',
topAreaItemClassName:
'engine-actionitem',
}),
engineContainer,
);
}

回到前面demo-general项目中中初始化部分

//index.tx
(async
functionmain() {
await registerPlugins();

init(document.getElementById(
'lce-container')!, {
locale:
'zh-CN',
enableCondition:
true,
enableCanvasLock:
true,//默认绑定变量
supportVariableGlobally: true,
requestHandlersMap: {
fetch: createFetchHandler(),
},
appHelper,
});
})();

3.解释init()

export async functioninit(
container
?: HTMLElement, //初始化函数参数:容器元素,可选
options?: IPublicTypeEngineOptions, //初始化函数参数:引擎选项,可选
pluginPreference?: PluginPreference, //初始化函数参数:插件偏好设置,可选
) {
await destroy();
//销毁之前的状态,确保初始化干净
let engineOptions= null; //初始化引擎选项变量

if (isPlainObject(container)) { //如果容器是一个普通对象
engineOptions = container; //将容器作为引擎选项
engineContainer = document.createElement('div'); //创建一个新的 div 元素作为引擎容器
engineContainer.id = 'engine'; //设置容器的 id 为 'engine'
document.body.appendChild(engineContainer); //将容器添加到 body 中
} else{
engineOptions
= options; //使用传入的引擎选项
engineContainer = container; //使用传入的容器
if (!container) { //如果容器不存在
engineContainer = document.createElement('div'); //创建一个新的 div 元素作为引擎容器
engineContainer.id = 'engine'; //设置容器的 id 为 'engine'
document.body.appendChild(engineContainer); //将容器添加到文档的 body 中
}
}

engineConfig.setEngineOptions(engineOptions as any);
//设置引擎配置的选项
const { Workbench }= common.skeletonCabin; //从骨架中解构出 Workbench 组件

if (options && options.enableWorkspaceMode) { //如果启用工作区模式
const disposeFun = await pluginPromise; //等待插件 Promise 的解析
disposeFun && disposeFun(); //如果存在 disposeFun 函数,则执行
render(//渲染工作区工作台组件
createElement(WorkSpaceWorkbench, {
workspace: innerWorkspace,
//传入内部工作区
className: 'engine-main', //设置类名
topAreaItemClassName: 'engine-actionitem', //设置顶部区域项的类名
}),
engineContainer,
//渲染到引擎容器中
);

innerWorkspace.enableAutoOpenFirstWindow
= engineConfig.get('enableAutoOpenFirstWindow', true); //设置内部工作区自动打开第一个窗口的属性
innerWorkspace.setActive(true); //设置工作区为活动状态
innerWorkspace.initWindow(); //初始化窗口
innerHotkey.activate(false); //激活快捷键
await innerWorkspace.plugins.init(pluginPreference); //初始化工作区插件
return; //返回
}

await plugins.init(pluginPreference as any);
//初始化插件
render(//渲染工作台组件
createElement(Workbench, {
skeleton: innerSkeleton,
//传入内部骨架
className: 'engine-main', //设置类名
topAreaItemClassName: 'engine-actionitem', //设置顶部区域项的类名
}),
engineContainer,
//渲染到引擎容器中
);
}

再来找

最后,来到workbench.tsx中

4.workbench.tsx

export class Workbench extends Component<{
workspace: Workspace;
//工作空间对象
config?: EditorConfig; //编辑器配置(可选)
components?: PluginClassSet; //插件类集合
className?: string; //类名
topAreaItemClassName?: string; //顶部区域项的类名
}, {
workspaceEmptyComponent: any;
//工作空间为空时的组件
theme?: string; //主题
}>{
constructor(props: any) {
super(props);
const { config, components, workspace }
=this.props;
const { skeleton }
=workspace;
skeleton.buildFromConfig(config, components);
//从配置和组件构建骨架
engineConfig.onGot('theme', (theme) =>{
this.setState({
theme,
});
});
engineConfig.onGot(
'workspaceEmptyComponent', (workspaceEmptyComponent) =>{
this.setState({
workspaceEmptyComponent,
});
});
this.state
={
workspaceEmptyComponent: engineConfig.get(
'workspaceEmptyComponent'), //获取工作空间为空时的组件
theme: engineConfig.get('theme'), //获取主题
};
}

render() {
const { workspace, className, topAreaItemClassName }
=this.props;
const { skeleton }
=workspace;
const { workspaceEmptyComponent: WorkspaceEmptyComponent, theme }
=this.state;

return (
<div className={classNames('lc-workspace-workbench', className, theme)}>
<SkeletonContext.Provider value={skeleton}>
<TopArea className="lc-workspace-top-area" area={skeleton.topArea} itemClassName={topAreaItemClassName} /> {/*渲染顶部区域*/}<div className="lc-workspace-workbench-body">
<LeftArea className="lc-workspace-left-area lc-left-area" area={skeleton.leftArea} /> {/*渲染左侧区域*/}<LeftFloatPane area={skeleton.leftFloatArea} /> {/*渲染左侧浮动区域*/}<LeftFixedPane area={skeleton.leftFixedArea} /> {/*渲染左侧固定区域*/}<div className="lc-workspace-workbench-center">
<div className="lc-workspace-workbench-center-content">
<SubTopArea area={skeleton.subTopArea} itemClassName={topAreaItemClassName} /> {/*渲染中上区域*/}<div className="lc-workspace-workbench-window">{
workspace.windows.map(d
=>(<WindowView
active
={d.id === workspace.window?.id} //判断窗口是否激活
window={d}
key
={d.id}/>))
}

{
!workspace.windows.length && WorkspaceEmptyComponent ? <WorkspaceEmptyComponent /> : null //根据条件渲染工作空间为空时的组件
}</div>
</div>
<MainArea area={skeleton.mainArea} /> {/*渲染主区域*/}<BottomArea area={skeleton.bottomArea} /> {/*渲染底部区域*/}</div>{/*<RightArea area={skeleton.rightArea} />*/}</div>
<TipContainer /> {/*渲染提示容器*/}</SkeletonContext.Provider>
</div>);
}
}

5.main-area.tsx

export default class MainArea extends Component<{ area: Area<any, Panel | Widget> }>{
render() {
const { area }
=this.props;
return (
<div className={classNames('lc-main-area engine-workspacepane')}>{area.container.items.map((item)=>item.content)}</div>);
}
}

以上代码,

将area.container.items数组中每个元素的content属性渲染到页面上,展示在MainArea组件所代表的区域内。

至此,低开引擎的初始化完成

部署一个自己的大模型,没事的时候玩两下,这可能是很多技术同学想做但又迟迟没下手的事情,没下手的原因很可能是成本太高,近万元的RTX3090显卡,想想都肉疼,又或者官方的部署说明过于简单,安装的时候总是遇到各种奇奇怪怪的问题,难以解决。本文就来分享下我的安装部署经验,包括本地和租用云服务器的方式,以及如何通过API调用大模型开发自己的AI应用,希望能解决一些下不去手的问题。

ChatGLM3-6B

本次部署使用的的大模型是ChatGLM3-6B,这个大模型是清华智谱研发并开源的高性能中英双语对话语言模型,它凭借创新的GLM(Gated Linear Units with Memory)架构及庞大的60亿参数量,在对话理解与生成能力上表现卓越。

ChatGLM3-6B不仅能够处理复杂的跨语言对话场景,实现流畅的人机互动,还具备函数调用以及代码解释执行的能力。这意味着开发者可以通过API调用,让模型执行特定任务或编写、解析简单的代码片段,从而将应用拓展到更为广泛的开发和智能辅助领域。

ChatGLM3-6B还允许开发者对预训练模型进行定制化微调,让它在某个领域工作的更好,比如代码编写、电商文案编写等。另外开发者还能对模型进行量化,使用较低的数字精度来表示权重,这使得模型可以运行在消费级显卡甚至CPU上。

ChatGLM3-6B的仓库地址:https://github.com/THUDM/ChatGLM3

效果展示

先看两个比较正常的效果:

能正常调用天气工具,记得上下文,这里点个赞!

再画一个满满的爱心,画的也不错。

再看两个跑疯的效果:

我问你天气,你不好好回答就算了,还反过来问我有啥意义,太爱管闲事。

看来ChatGLM对正六边形的感知有误啊,确实它还不能识别这个图像。

虽然有时不那么如人意,不过整体用起来还是有很多可圈可点的地方,就是提示词要好好写一下,不能太凑合。

云环境部署

这里以AutoDL为例(
https://www.autodl.com
),AutoDL上的GPU实例价格比较公道,ChatGLM3-6B需要13G以上的显存,可以选择RTX4090、RTX3090、RTX3080*2、A5000等GPU规格。

这里提供两种方法,一是直接使用我已经创建好的镜像,二是自己从基础镜像一步步安装。

使用现有镜像

创建容器实例时镜像选择“社区镜像”,输入 yinghuoai ,选择 ChatGLM3 的最新镜像。

容器实例开机成功后,点击对应实例的 JupyterLab 就能开始使用了。

这个镜像包含三个Notebook,方便我们启动WebUI服务器和API服务器,并进行相关的测试。我将在下文介绍具体的使用方法。

自己手动安装

创建容器实例时我们选择一个基础镜像 Miniconda -> conda3 -> Python 3.10(ubuntu22.04) -> Cuda11.8。

容器实例开机完毕后,点击对应实例的 JupyterLab 进入一个Web管理界面。

在“启动页”这里点击“终端”,进入一个命令窗口。

首先需要设置下网络,用以加速访问Github。这是AutoDL实例才能使用的,本地无效。

source /etc/network_turbo

然后需要把代码下载到本地,使用Git即可。

git clone https://github.com/THUDM/ChatGLM3
cd ChatGLM3

然后创建一个Python的虚拟环境,这样方便隔离不同项目对Python环境的不同要求。这里使用 source activate 激活虚拟环境,很多文章中是 conda activate,这和conda的版本有关系,AutoDL中的版本不支持 conda activate。

conda create -n chatglm3-6b python=3.10.8 
source activate chatglm3-6b

然后使用 uv 安装依赖的程序包。为什么用uv?因为requirements中很多包的版本要求都是 >=,直接使用pip的时候会安装最新的版本,最新的版本往往和开发者使用的版本不同,这会导致一些兼容问题,所以最好就是 == 的那个版本,这个版本能用,而且一般就是开发者使用的版本。

pip install uv
uv pip install --resolution=lowest-direct -r requirements.txt

然后我们还要下载大模型文件,这里从AutoDL的模型库中下载,速度比较快。下边的模型文件是别人分享出来的,我们使用AutoDL提供的一个下载工具进行下载。下载目标目录是/root/autodl-tmp,会自动在这个目录中创建一个名为 chatglm3-6b 的子目录,并保存这些文件。

pip install codewithgpu
cg down xxxiu/chatglm3-6b/config.json -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/configuration_chatglm.py -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/gitattributes -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model.safetensors.index.json -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/MODEL_LICENSE -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00001-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00002-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00003-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00004-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00005-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00006-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/model-00007-of-00007.safetensors -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/modeling_chatglm.py -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/pytorch_model.bin.index.json -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/quantization.py -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/README.md -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/tokenization_chatglm.py -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/tokenizer.model -t /root/autodl-tmp
cg down xxxiu/chatglm3-6b/tokenizer_config.json -t /root/autodl-tmp

最后我们做一个简单的测试,找到这个文件:ChatGLM3/basic_demo/cli_demo.py,修改其中的模型路径为上边的下载路径:/root/autodl-tmp/chatglm3-6b

在终端执行命令:
python basic_demo/cli_demo.py
,然后我们就可以在终端与大模型进行交流了。

本地环境安装

注意需要13G显存以上的Nvidia显卡,否则跑不起来。这里以Windows系统为例。

首先本地要有一个Python的运行环境,建议使用 Anaconda,可以把它理解为一个Python集成环境,通过它我们可以方便的开发Python程序。Anaconda的官方下载地址是:
www.anaconda.com/download

这个安装文件比较大,下载时间取决于你的网速,下载成功后按照提示一步步安装就行了。

安装成功后,启动“Anaconda Navigator”,在其中点击“Environments”->"base(root)" ->"Open Terminal",打开终端。

这是一个命令行工具,我们将主要在这里边通过执行命令安装ChatGLM3-6B。

然后我们还需要从Github上下载代码到本地,推荐使用Git,没有Git的同学可以先去安装一个:
https://git-scm.com/
。当然直接从Github下载程序包到本地也可以,不方便访问Github的同学也可以使用我整理的程序包,给公众号“萤火遛AI”发消息 ChatGLM3 即可获取。

这里我将程序放到了C盘下的ChatGLM3目录。

cd C:\
git clone https://github.com/THUDM/ChatGLM3
cd ChatGLM3

使用下边的命令创建一个Python的虚拟环境并激活,这样方便隔离不同项目对Python环境的不同要求。

conda create -n chatglm3-6b python=3.10.8 
conda activate chatglm3-6b

然后还需要把相关模型文件下载到本地,为了防止下载方式失效,这里提供多种方法:

(1)下载AutoDL用户分享的模型,执行下边的命令,它会下载到 C:\ChatGLM3\THUDM,速度还可以。

pip install requests
pip install codewithgpu
cg down xxxiu/chatglm3-6b/config.json -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/configuration_chatglm.py -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/gitattributes -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model.safetensors.index.json -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/MODEL_LICENSE -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00001-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00002-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00003-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00004-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00005-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00006-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/model-00007-of-00007.safetensors -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/modeling_chatglm.py -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/pytorch_model.bin.index.json -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/quantization.py -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/README.md -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/tokenization_chatglm.py -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/tokenizer.model -t C:\ChatGLM3\THUDM
cg down xxxiu/chatglm3-6b/tokenizer_config.json -t C:\ChatGLM3\THUDM

(2)从HuggingFace的镜像下载,地址是:
https://hf-mirror.com/THUDM/chatglm3-6b/tree/main

(3)给公众号“萤火遛AI”发消息 ChatGLM3 获取最新下载方式。

最后我们做一个简单的测试,执行命令:
python basic_demo/cli_demo.py
,然后我们就可以在终端与大模型进行交流了。

如果程序出现下边的错误:

RuntimeError: "addmm_impl_cpu_" not implemented for 'Half'

首先确定你的电脑是安装了Nvida显卡的,然后使用下边的命令补充安装相关的pytorch-cuda包。

conda install pytorch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 pytorch-cuda=11.8 -c pytorch -c nvidia
pip install chardet

使用WebUI体验

ChatGLM提供了一个Web界面,用户可以直接在这个页面上聊天、使用插件,以及执行Python代码,就像使用大多数的大语言模型一样。额外的用户还可以配置一些参数,比如一次生成Token的数量、系统提示词、采样的随机性控制等。

启动WebUI服务

首先修改程序中的模型目录,在下载程序中找到文件 composite_demo/client.py,修改 MODEL_PATH 为你的模型存放地址。

然后进入 ChatGLM3-6B 程序的根目录(根据自己的部署来),激活Python虚拟环境:

cd /root/ChatGLM3
conda activate chatglm3-6b
# conda如果不行就使用 source activate chatglm3-6b

因为需要执行代码,我们还要安装 Jupyter 内核:

ipython kernel install --name chatglm3-6b --user

并修改文件 composite_demo/demo_ci.py 中的 IPYKERNEL 的值为设置的值。

最后启动API服务器:
streamlit run composite_demo/main.py
可知这个WebUI使用的是streamlit框架。

如果是在个人电脑上安装的,点击这里的连接就可以在浏览器访问了。

如果是在AutoDL上的实例,还需要再折腾一下。因为这个WebUI使用了WebSocket,但是AutoDL开放的外网端口不支持WebSocket。此时可以通过SSH隧道的方式来打通本地与AutoDL实例的网络。

我们需要类似下边这样的一条指令:

sudo ssh -CNg -L 8501:127.0.0.1:8501 root@connect.westb.seetacloud.com -p 12357

其中的 connect.westb.seetacloud.com 和 10757 需要替换成你自己实例的,在实例列表中复制登录指令。

然后把它随便粘贴到一个地方,就可以得到所需的地址和端口号了:

在个人电脑的终端或者命令行工具执行上边写好的指令,首先需要统一添加主机(输入 yes),然后需要输入主机登录密码,还是从AutoDL的实例列表拷贝。

登录成功后,这个界面会挂起,不会输出任何内容。此时我们在浏览器地址栏输入
http://127.0.0.1:8501
就可以访问了。

使用WebUI

这个WebUI左边是参数区域,右边是使用区域,有三种使用方式:Chat、Tool和Code Interpreter,分别就是聊天、工具或插件、代码解释器。相关参数我在之前的文章中介绍过,可以参考下:
https://juejin.cn/post/7323449163420680202#heading-7

聊天就不用说了,我们看下工具或插件的使用。它会完整的展现出来插件的使用过程,用户询问问题,触发大模型调用插件,展现插件返回的内容,大模型整理插件返回的内容并输出给用户。中间的两个过程这里只是为了方便用户了解原理,其实可以在展现大模型返回值时将它们过滤掉。具体的可以修改这个文件中的第144行-198行:composite_demo/demo_tool.py 。

实例代码中提供了两个工具,一个是获取实时天气,另一个是生成随机数,用户还可以修改代码增加自己的工具插件,在 composite_demo/tool_registry.py 这个文件中。

只需要使用 `@register_tool` 装饰函数即可完成注册。对于工具声明,函数名称即为工具的名称,函数 docstring 即为工具的说明;对于工具的参数,使用 `Annotated[typ: type, description: str, required: bool]` 标注参数的类型、描述和是否必须。例如,`get_weather` 工具的注册如下:

@register_tool
def get_weather(
    city_name: Annotated[str, 'The name of the city to be queried', True],
) -> str:
    """
    Get the weather for `city_name` in the following week
    """
    ...

再看看代码解释器的效果,模型会根据对任务完成情况的理解自动地连续执行多个代码块,直到任务完成。比如让它用Python画一个爱心。

如果代码执行有错误,模型会自动修正错误,然后继续生成,直到能够正常执行成功。这个能力其实是通过系统提示词和observation角色实现的。

在 composite_demo/demo_ci.py 中可以看到提示词:

当程序执行出错的时候,程序会通过observation角色把错误再发给ChatGLM进行分析,然后ChatGLM会修改代码,再重新输出到程序中,最后使用 Jupyter 内核执行代码。

使用API开发应用

使用大模型API,我们可以完全自定义自己的交互页面,增加很多有趣的功能,比如提供联网能力。

这里我们使用的是ChatGLM3-6B自带的一个API示例程序,这个程序中有一个参考OpenAI接口规范开发的API服务,我们可以直接使用OpenAI的客户端进行调用,这避免了很多学习成本,降低了使用难度。

启动API服务

首先修改程序中的模型目录,在下载程序中找到文件 openai_api_demo/api_server.py,修改 MODEL_PATH 为你的模型存放地址。

然后进入 ChatGLM3-6B 程序的根目录(根据自己的部署来),激活Python虚拟环境:

cd C:\ChatGLM3
conda activate chatglm3-6b
# conda如果不行就使用 source activate chatglm3-6b

最后启动API服务器:
python openai_api_demo/api_server.py

看到 running on http://0.0.0.0 的提示信息就代表启动成功了。

注意这里的端口号,如果你是在AutoDL部署的程序,需要将端口号修改为6006,然后才能通过AutoDL提供的“自定义服务”在外网访问,端口号在openai_api_demo/api_server.py 文件的最末尾。

修改后重启API服务,然后在AutoDL的容器实例列表中点击“自定义服务”,即可获取外网访问地址。

调用API服务

这里还是以Python为例,首先使用pip安装OpenAI的SDK。

pip install --upgrade openai httpx[socks]

我准备了两个简单的应用示例,一个是简单的聊天程序,另一个是在大模型中使用插件的方法。

先看聊天程序,这里让它扮演一个数学老师进行出题,之前我写过一篇文章介绍相关参数的含义,这里就不罗嗦了,需要的请看:
https://juejin.cn/post/7323449163420680202

# 一个简单的聊天程序

from openai import OpenAI

client = OpenAI(api_key='not-need-key',base_url="http://127.0.0.1:6006/v1")
stream = client.chat.completions.create(
    messages=[{
        "role": "system", "content": "你是一名数学老师,从事小学数学教育30年,精通设计各种数学考试题"
    },{
        "role": "user", "content": "请给我出10道一年级的计算题。"
    }],
    model='chatglm3-6b',
    max_tokens=1024,
    #temperature=0.1,
    top_p=0.3,
    #frequency_penalty=0.5,
    presence_penalty=0.2,
    seed=12345,
    #stop='30年',
    response_format={ "type": "json_object" },
    n=1,
    stream=True
)

for chunk in stream:
    msg = chunk.choices[0].delta.content
    if msg is not None:
        print(msg, end='')

下边是程序的执行结果,大模型理解的很正确,并生成了合理的输出。

再看大模型中使用插件的方法,这里让ChatGLM根据用户要求调用天气函数查询实时天气,注意ChatGLM3-6B调用函数的方法没有支持最新的OpenAI API规范,目前只实现了一半,能通过tools传入函数,但是响应消息中命中函数还是使用的 function_call,而不是最新的 tool_calls。相关参数我也在别的文章中做过详细介绍,请参考:
https://juejin.cn/post/7325360810226630706

from openai import OpenAI
import json
import requests
import time

# 获取天气的方法
def get_city_weather(param):
    city = json.loads(param)["city"]
    r = requests.get(f"https://wttr.in/{city}?format=j1")

    data = r.json()["current_condition"]
    #print(json.dumps(data))
    temperature = data[0]['temp_C']
    humidity= data[0]['humidity']
    text = data[0]['weatherDesc'][0]["value"]
    return "当前天气:"+text+",温度:"+temperature+ "℃,湿度:"+humidity+"%"

# 天气插件的定义
weather_tool = {
    "type": "function",
    "function": {
        "name": "get_city_weather",
        "description": "获取某个城市的天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称",
                },
            },
            "required": ["city"],
        },
    }
}

# 创建OpenAI客户端,获取API Key请看文章最后
client = OpenAI(api_key='no-need-key', base_url="http://127.0.0.1:6006/v1")

# 定义请求GPT的通用方法
def create_completion():
    return client.chat.completions.create(
        messages=messages,
        model='chatglm3-6b',
        stream=False,
        tool_choice="auto",
        tools=[weather_tool]
    )


# 我的三个问题
questions = ["请问上海天气怎么样?","请问广州天气怎么样?","成都呢?","北京呢?"]

# 聊天上下文,初始为空
messages=[]

print("---GLM天气插件演示--- ")

# 遍历询问我的问题
for question in questions:  

    # 将问题添加到上下文中
    messages.append({
        "role": "user",
        "content": question,
    })
    print("路人甲: ",question)

    # 请求GPT,并拿到响应
    response_message = create_completion().choices[0].message
    # 把响应添加到聊天上下文中
    messages.append(response_message)
    #print(response_message)
    # 根据插件命中情况,执行插件逻辑
    if response_message.function_call is not None:
        function_call = response_message.function_call
        # 追加插件生成的天气内容到聊天上下文
        weather_info = get_city_weather(function_call.arguments)
        #print(weather_info)
        messages.append({
            "role": "function",
            "content": weather_info,
            "name": function_call.name
        })
        # 再次发起聊天
        second_chat_completion = create_completion()
        gpt_output = second_chat_completion.choices[0].message.content
        # 打印GPT合成的天气内容
        print("GLM: ",gpt_output)
        time.sleep(0.2)
        # 将GPT的回答也追加到上下文中
        messages.append({
            "role": "assistant",
            "content": gpt_output,
        })
    else:
        print("GLM: ",response_message.content)

执行效果如下:


以上就是本文的主要内容,有兴趣的快去体验下吧。

如需GPT账号、学习陪伴群、AI编程训练营,点此进入:
大模型应用开发 | API 实操

关注萤火架构,加速技术提升!

前言:

Emit 是开发者在掌握反射的使用后,进阶需要的知识,它能显著的改善因反射带来的性能影响。

目前能搜到的 Emit 的相关文章,都是一篇系列,通常推荐对照着反编绎后的 IL 编写 Emit 代码,门槛太高。

借着优化 CYQ.Data 时使用 Emit 的心得体会及记忆,写个简单的入门教程,以帮助后来者相对系统的了解及掌握这一块知识。

入门教程的大纲:

第一部分:
Emit
介绍

第二部分:构建动态程序集

第三部分:构建模块(
Module

第四部分:构建类型(
Type

第五部分:动态生成方法

第五部分:
IL
指令

第六部分:实战项目

第七部分:性能优化与注意事项

每个部分的内容,根据情况,可能一篇介绍或拆分成多章节介绍。

下面开始这个这个系列的第一篇吧:

什么是 Emit

在.NET开发中,Emit 是一种动态生成IL代码的技术,通过使用 System.Reflection.Emit 命名空间中的类,可以在运行时创建和修改程序集、类型和方法。

这种技术为开发人员提供了一种强大的方式来在运行时动态生成和执行代码:

Emit 技术通常用于解决需要在运行时动态生成代码的特定场景,例如在ORM(对象关系映射)框架中动态创建实体类,或者在AOP(面向切面编程)中动态创建代理类。

使用 Emit 技术,可以直接操作IL(Intermediate Language,中间语言)代码,而不是使用传统的编写源代码和编译的方式。

IL代码是一种与平台无关的低级语言,它是在.NET运行时执行的指令集。

通过使用Emit,我们可以以一种更底层的方式来控制代码的生成和执行过程。

Emit 技术的核心是 System.Reflection.Emit 命名空间中的 TypeBuilder 和 MethodBuilder 类。

TypeBuilder类允许我们在运行时动态创建新的类型。

MethodBuilder类允许我们在运行时动态创建新的方法。

通过这些类,我们可以定义类型的结构、成员和方法,并使用 IL 生成器(ILGenerator)来编写实际的 IL 代码。

使用 Emit 技术的步骤通常包括以下几个步骤:

1、创建一个动态程序集(AssemblyBuilder)。
2、在程序集中创建一个动态模块(ModuleBuilder)。
3、在模块中创建一个动态类型(TypeBuilder)。
4、在类型中创建动态方法(MethodBuilder)。
5、使用ILGenerator编写实际的IL代码,包括加载、存储和计算等操作。
7、完成IL代码的生成后,使用CreateType方法将动态类型创建为实际的类型。
最后,通过反射或创建委托或其他方式,可以在运行时调用动态生成的方法。

更通常情况下,使用创建动态方法更常见,即从第4步开始,使用 DynamicMethod 直接创建动态方法。

Emit 技术的优势在于它提供了对IL代码的直接控制,使开发人员能够灵活地生成和修改代码逻辑。

这种动态生成代码的能力在某些特定场景下非常有用,例如需要在运行时根据不同条件生成不同代码的情况。

Emit 的作用和优势

Emit 作为.NET开发中的一项重要技术,具有诸多作用和优势。

作用

  1. 动态代码生成:允许开发人员在运行时动态生成IL代码,包括创建程序集、类型和方法。

  2. ORM框架:在ORM(对象关系映射)框架中,Emit 技术常被用于动态创建实体类,或构建SQL执行语句。

  3. AOP编程:面向切面编程(AOP)通常需要动态创建代理类来实现横切关注点的功能。Emit 技术可以在运行时动态生成代理类,并在其中添加所需的横切逻辑,从而实现AOP编程的目标。

  4. 其它:......。

优势

  1. 灵活性:Emit 技术赋予了开发人员更高级别的灵活性,他们可以根据具体需求动态生成和执行代码。

  2. 性能优势:通过 Emit 技术生成的代码通常可以实现更高的性能,因为它直接操作IL代码,避免了一些动态语言或解释语言的性能损失。

  3. 动态代理:在AOP编程中,Emit 技术可以帮助实现动态代理,从而实现横切关注点的功能。

  4. 底层控制:通过Emit 技术,开发人员可以直接操作IL代码,实现对代码生成和执行过程的更底层控制。

Emit 技术在.NET开发中具有重要的作用和诸多优势,为开发人员提供了一种强大的动态代码生成和执行的方式。

总结:

Emit 技术是一种强大的动态生成IL代码的技术,通过使用 System.Reflection.Emit 命名空间中的类,开发人员可以在运行时创建和修改程序集、类型和方法。

它为开发人员提供了更高级别的灵活性,使他们能够根据需要动态生成和执行代码。