# xd26 **Repository Path**: hanweiwn/xd26 ## Basic Information - **Project Name**: xd26 - **Description**: xd26实训 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-06-29 - **Last Updated**: 2026-08-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 问卷: ``` https://v.wjx.cn/vm/rCU1YFU.aspx ``` 小组分组:3-5人。 第一天的任务: 1:小组分组。 2:项目立项(10个题目中选择一个) ## 项目: ### 1:需求分析 给用户看的。没有编程语言。没有专业术语。 会有用户故事。解决了什么问题。 大量UML,用例图,功能图,站立会。 ### 2:系统设计 把需求转化为技术方案。 包含硬件,软件,技术栈。系统模块的划分。数据库建模等等。 写接口文档,接口API,算法等。 ER,架构图,UI原型。 ### 3:开发 写代码,文档针对代码的。用版本控制器(GIT),你们小组开发时候必须用GIT。 以前的CVS,SVn集中式的。GIT是分布式的版本控制器。 ### 4:测试 单元测试,JUnit。系统测试。 自动化测试。Selenium等。 ### 5:部署维护迭代。 **去看一下阿里的开发手册。** 举几个例子:告诉你开发规范的重要性。 货币运算一定不能用Float,Double。 后端:雪花算法生成的一个32位的ID. 用Number类型传递给了前端。前端会产生精度丢失。如何改:后端传入的事String类型。 主键无关性:身份证号能不能做主键?不能,只要是跟业务逻辑有关的。就坚决不能用。 **数据分析与可视化** 1:数据抓取--爬虫。 2:数据清洗 3:数据的可视化 4:数据的Web展示。图表展示。 5:机器学习。预测一些数据的归纳。 6:LLM(私有知识库切片) 我们要用虚拟环境。如果自己不会虚拟环境,自己百度。 ## 爬虫 ``` pip install requests lxml ``` ### 爬取当当 ```python import requests from lxml import etree import time def parsedangdang(url): #会设置一个请求头 用这个请求头来模拟浏览器操作 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36' } response = requests.get(url,headers=headers) html = etree.HTML(response.text) lis = html.xpath('//div[@id="search_nature_rg"]//li') count = 0 for li in lis: count+=1 title = li.xpath('.//p[@class="name"]//a/@title')[0] price_n = li.xpath('.//span[@class="price_n"]/text()') price_r = li.xpath('.//span[@class="price_r"]/text()') price_s = li.xpath('.//span[@class="price_s"]/text()') print(f"标题:{title}") print(f"现价:{price_n}") print(f"原价:{price_r}") print(f"折扣:{price_s}") print(f"公共发现{count}本书") if __name__ == '__main__': url = 'https://search.dangdang.com/?key=%B5%E7%D7%D3&act=input' parsedangdang(url) ``` ``` 折扣:['(2.61折)'] 标题: 电子系统设计与实战:C8051F单片机 FPGA控制版 薛小铃 著 高等教育出版社 现价:['¥6.07'] 原价:['¥209.46'] 折扣:['(0.29折)'] 标题: 电子技术基础解题指导 现价:['¥14.00'] 原价:['¥18.00'] 折扣:['(7.78折)'] 标题: 电子技术实验教程 现价:['¥14.40'] 原价:['¥18.00'] 折扣:['(8折)'] ``` ``` // 从整个文档的任何位置查找 .// 从当前节点开始查找 ./ 从当前节点的直接子节点查找 ``` ```python import requests from lxml import etree import time def parsedangdang(url): #会设置一个请求头 用这个请求头来模拟浏览器操作 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36' } response = requests.get(url,headers=headers) html = etree.HTML(response.text) lis = html.xpath('//div[@id="search_nature_rg"]//li') count = 0 for li in lis: count+=1 title = li.xpath('.//p[@class="name"]//a/@title')[0] price_n = li.xpath('.//span[@class="price_n"]/text()') price_r = li.xpath('.//span[@class="price_r"]/text()') price_s = li.xpath('.//span[@class="price_s"]/text()') print(f"标题:{title}") print(f"现价:{price_n}") print(f"原价:{price_r}") print(f"折扣:{price_s}") print(f"公共发现{count}本书") if __name__ == '__main__': for i in range(1,10): url = f'https://search.dangdang.com/?key=%B5%E7%D7%D3&act=input&page_index={i}' parsedangdang(url) print("*"*60) # time.sleep(5)py ``` 你们抓的时候,尽量用自己的流量,不要用学校的wifi。 设置全局镜像库,不管是全局还是虚拟环境默认全部走清华,速度快。 ``` pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple ``` ### 模拟浏览器 很多时候,你会发现你用requests访问页面,和浏览器中访问页面出来的内容不一样。导致很多时候,访问很多网站,都访问不了。假装告诉服务器,我是一个浏览器。这样能绕过一部分有反扒机制的服务器。 你打印的html内容,找不到你通过浏览器看到的内容的时候,你就应该干这活了。90%以上的网站都需要设置header和cookie等。 在你访问的页面中按下F12,打开网络。按下F5刷新地址。吧你的地址copyCURL. 然后再https://tool.tushuoit.com/curlconverter/index.html进行转化。 转化后把header和cookie弄到你的代码了,基本就能解决大部分反扒机制不是那么狠的网站了。 ```python import requests from lxml import etree import time cookies = { #此处应该换成你的值 } headers = { #此处应该换成你的值 } response = requests.get( 'https://xian.esf.fang.com/chushou/3_173068020.htm?channel=2,2&psid=1_1_60', cookies=cookies, headers=headers, ) def parseHtml(): html = etree.HTML(response.text) allmoney = html.xpath('//div[@class="tab-cont-right"]//div[1]//div[1]//div[1]//i[1]//text()') print(allmoney) # print(response.text) if __name__ == '__main__': parseHtml() print("*"*60) # time.sleep(5) ``` 如果对方的反扒的机制比较强,还有登录。还必须让你选择不同的红绿灯,不同的颜色,不同的汽车。 1:我用大模型来进行验证。 2:用Selenium ### Selenium 能够做自动化的测试,自动化的爬虫。尤其可以解决一些动态的内容的反扒机制。基本99%都可以搞。就是在浏览器做操作。你在浏览器里什么样子,selenium就是什么样。速度不如requests,requests没有界面的。但是Selenium也可以做到无头。 #### 下载驱动: ``` https://googlechromelabs.github.io/chrome-for-testing/#stable ``` ![image-20260629112041194](images/image-20260629112041194.png) 浏览器的版本需要和chromeDriver的版本要一致,如果在这个链接没有找到对应版本的驱动。 把驱动拷贝到项目当中去。 给你的环境装入selenium的类库 ``` pip install selenium -i http://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn ``` ```python from selenium import webdriver from selenium.webdriver.chrome.service import Service import time #设置当前浏览器的类型 service = Service('chromedriver.exe') #通过service作为参数传递给 webdriver driver = webdriver.Chrome(service=service) #指挥浏览器,打开百度 driver.get('https://www.baidu.com/') #你可以对网页做任何操纵,就跟人一样 # 也可以操作页面的按钮 也可以操作页面上的任何元素,就跟真人访问一样。 # 能够搞抖音 如果有代理池的,就可以搞抢鞋抢茅台 for i in range(10): driver.maximize_window()#把窗体最大化 time.sleep(1) driver.minimize_window() time.sleep(1) time.sleep(10) ``` ### Debug模式 在这种调试模式下。能够保存用户的登录信息。用户可以进行部分的手工干预。有手机验证,选红绿灯,汽车。 甚至用PlayWight进行大模型的填充。 在你的电脑上建立如下bat。 PATH C:\Program Files\Google\Chrome\Application\chrome.exe 浏览器程序的地址 c:\AutomationProfile1\lc 存储用户的相关信息 ``` @echo off PATH C:\Program Files\Google\Chrome\Application\ chrome.exe --remote-debugging-port=9527 --user-data-dir="c:\AutomationProfile1\lc" exit ``` 下面这个代码要运行,你必须先双击你的批处理文件,让居于调试模式的浏览器先运行起来。 ```python from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options import time #用调试模式,一般情况下浏览器已经打开了 chrome_options = Options() chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9527") driver = webdriver.Chrome(options=chrome_options) #有了driver对象,就可以进行操作了 driver.get('https://www.douyin.com/') print(driver.title) #可以操控页面上的所有元素,什么模拟点击事件,填充文本框。提交按钮。 time.sleep(3) ``` #### 抓淘宝的数据 ```python from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By import time def parse(url): #用调试模式,一般情况下浏览器已经打开了 chrome_options = Options() chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9527") driver = webdriver.Chrome(options=chrome_options) #有了driver对象,就可以进行操作了 driver.get(url) print(driver.title) #可以操控页面上的所有元素,什么模拟点击事件,填充文本框。提交按钮。 time.sleep(2) wrapper = driver.find_element(By.ID, 'content_items_wrapper') goods = wrapper.find_elements(By.XPATH,"./div") print(f"找到商品数量为:{len(goods)}") for item in goods: try: #下面两句话不会写,具体的样式,这个可以交给AI title = item.find_element(By.CSS_SELECTOR, 'div.title--ASSt27UY span').text price = item.find_element(By.CSS_SELECTOR, '.priceInt--yqqZMJ5a').text print(f"商品标题为:{title},价格为:{price}") except: pass time.sleep(3) if __name__ == '__main__': for i in range(1,3): #你只要把这个值换成100 所有的ipad的数据你就全部都有了 url = "https://s.taobao.com/search?clientPreloadId=preload_1782714027340&commend=all&ie=utf8&initiative_id=tbindexz_20170306&page={i}&preLoadOrigin=https%3A%2F%2Fwww.taobao.com&q=ipad&search_type=item&sourceId=tb.index&spm=a21bo.jianhua%2Fa.search_manual.0&ssid=s5-e&tab=all" parse(url) ``` 我可以先把淘宝的所有商品的名称和类别抓出来,然后再抓具体的数据。 **请你注意:** 一定注意控制抓取的频率。不要太高。也不要上多线程(除非你有代理池)。 数据来源:也可以不用自己爬虫。阿里云天池。 ## 房天下 ``` https://xian.esf.fang.com/ ``` 1:一定注意控制抓取的频率。不要太高。也不要上多线程(除非你有代理池)。 2:该休眠就休眠。 3:用多台机器抓。 ```python import requests from lxml import etree import time import re cookies = { 'global_cookie': '1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq', '__utmz': '147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)', 'csrfToken': '-x7Dg8iDcMujcyLQZhP4XzSx', 'city': 'xian', '__utmc': '147393320', 'otherid': '36e15bb5f2f5663c16b18f0add5119dc', '__utma': '147393320.244321793.1781146046.1782550236.1782701864.8', '__utmt_t0': '1', '__utmt_t1': '1', '__utmt_t2': '1', 'g_sourcepage': 'esf_juhe%5Exq_web', '__utmb': '147393320.9.10.1782701864', 'unique_cookie': 'U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } headers = { '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', 'cache-control': 'max-age=0', 'priority': 'u=0, i', 'referer': 'https://xian.esf.fang.com/house-a016698/', 'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36', # 'cookie': 'global_cookie=1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq; __utmz=147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); csrfToken=-x7Dg8iDcMujcyLQZhP4XzSx; city=xian; __utmc=147393320; otherid=36e15bb5f2f5663c16b18f0add5119dc; __utma=147393320.244321793.1781146046.1782550236.1782701864.8; __utmt_t0=1; __utmt_t1=1; __utmt_t2=1; g_sourcepage=esf_juhe%5Exq_web; __utmb=147393320.9.10.1782701864; unique_cookie=U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } #解析页面上的西安市下的所有区域的链接 def parseZonePage(url): response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) zone_list = [] zones = html.xpath('//span[@class="screen_title" and contains(text(),"区域")]/following-sibling::ul[1]/li/a') for zone in zones: zone_name = zone.text zone_url = url+zone.get('href') print(f"区域:{zone_name} 链接:{zone_url}") if zone_name and zone_url: zone_list.append({"name":zone_name,"url":zone_url}) return zone_list def parsePageSize(url): print("正在获取总页数...") response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) #共4页 allpageSize = html.xpath('//span[@class="last"]//text()') # print(f"总页数:{allpageSize[0]}") #共4页 numbers = re.findall(r'\d+',allpageSize[0]) page_num = int(numbers[0]) return page_num if __name__ == '__main__': #区域 zone_list = parseZonePage("https://xian.esf.fang.com") # print("*"*60) # time.sleep(5) for zone in zone_list: print(f"{zone.get('name')}:{zone.get('url')}") #获取总页数 page_num = parsePageSize(zone.get('url')) print(f"共有{page_num}页") #分页规则 #https://xian.esf.fang.com/house-a016698/i3100/ 100 #https://xian.esf.fang.com/house-a016698/i330/ 30 #https://xian.esf.fang.com/house-a016698/i32/ 2 #https://xian.esf.fang.com/house-a016698/i31/ 1 #获取当面区域分页的的所有链接 for i in range(1,page_num+1): pageurl = f"{zone.get('url')}i3{i}/" print(f"正在爬取的链接为{zone.get('name')}:{pageurl}") time.sleep(2) #还缺少每个页面上的具体的每个链接 #还缺少每个具体链接上的所有数据 ``` ### CSV ```python import csv def write(): rows = [ ['张三1',26,'上海'], ['张三2',27,'上海'], ['张三3',28,'上海'] ] headers = ['姓名', '年龄', '地址'] #写入 with open('test.csv', 'a+', encoding='utf-8', newline='') as f: writer = csv.writer(f) f.seek(0) first_line = f.readline() # 如果有这个头,就不加头,如果没有再加 if not first_line: # 如果没有头 writer.writerow(headers) f.seek(0, 2) writer.writerows(rows) def read(): with open('test.csv', 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: 姓名 = row['姓名'] 年龄 = row['年龄'] 地址 = row['地址'] print(f"姓名:{姓名} 年龄:{年龄} 地址:{地址}") if __name__ == '__main__': # write() read() ``` 解析了具体的房源信息 ```python import requests from lxml import etree import time import re import csv import random cookies = { 'global_cookie': '1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq', '__utmz': '147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)', 'csrfToken': '-x7Dg8iDcMujcyLQZhP4XzSx', 'city': 'xian', '__utmc': '147393320', 'otherid': '36e15bb5f2f5663c16b18f0add5119dc', '__utma': '147393320.244321793.1781146046.1782550236.1782701864.8', '__utmt_t0': '1', '__utmt_t1': '1', '__utmt_t2': '1', 'g_sourcepage': 'esf_juhe%5Exq_web', '__utmb': '147393320.9.10.1782701864', 'unique_cookie': 'U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } headers = { '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', 'cache-control': 'max-age=0', 'priority': 'u=0, i', 'referer': 'https://xian.esf.fang.com/house-a016698/', 'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36', # 'cookie': 'global_cookie=1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq; __utmz=147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); csrfToken=-x7Dg8iDcMujcyLQZhP4XzSx; city=xian; __utmc=147393320; otherid=36e15bb5f2f5663c16b18f0add5119dc; __utma=147393320.244321793.1781146046.1782550236.1782701864.8; __utmt_t0=1; __utmt_t1=1; __utmt_t2=1; g_sourcepage=esf_juhe%5Exq_web; __utmb=147393320.9.10.1782701864; unique_cookie=U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } #解析页面上的西安市下的所有区域的链接 def parseZonePage(url): response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) zone_list = [] zones = html.xpath('//span[@class="screen_title" and contains(text(),"区域")]/following-sibling::ul[1]/li/a') for zone in zones: zone_name = zone.text zone_url = url+zone.get('href') print(f"区域:{zone_name} 链接:{zone_url}") if zone_name and zone_url: zone_list.append({"name":zone_name,"url":zone_url}) return zone_list def parsePageSize(url): print("正在获取总页数...") response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) #共4页 allpageSize = html.xpath('//span[@class="last"]//text()') # print(f"总页数:{allpageSize[0]}") #共4页 numbers = re.findall(r'\d+',allpageSize[0]) page_num = int(numbers[0]) return page_num #解析每一个分页具体页面的具体链接 def parseHouseListPage(url): response = requests.get(url,cookies=cookies,headers=headers) html = response.text html = etree.HTML(response.text) house_urls = [] links = html.xpath('//div[contains(@class,"shop_list")]//dl//dt//a/@href') for link in links: full_link = 'https://xian.esf.fang.com'+link print(f'{full_link}') house_urls.append(full_link) return house_urls def parseDetailHouse(url): response = requests.get(url,cookies=cookies,headers=headers) html = response.text html = etree.HTML(response.text) allmoney = html.xpath('//div[@class="tab-cont-right"]/div[1]/div[1]/div[1]//i[1]//text()') info_nodes = html.xpath('//div[@class="tt"]//text()') print(f'长度:{len(info_nodes)}') for node in info_nodes: print(node) #房屋的基本信息 house_nodes = html.xpath('//div[@class="tab-cont-left"]//div[3]//div[@class="text-item clearfix"]') for node in house_nodes: name = node.xpath('./span[@class="lab"]/text()') value = node.xpath('./span[@class="rcont"]/text()') print(f'{name} {value}') # print(len(house_nodes)) print(f'总价:{allmoney}') if __name__ == '__main__': url = "https://xian.esf.fang.com/chushou/3_173068020.htm?channel=2,2&psid=1_1_60" parseDetailHouse(url) #下面的代码泡一下就能得到整个房天下西安地区的所有的房源链接 # #测试获取当前分页代码中的所有具体房源的链接 # # url = "https://xian.esf.fang.com/house-a016698/" # # parseHouseListPage(url) # # #这是一个单独的存储csv的链接的测试 # # with open('links.csv', 'a+', encoding='utf-8', newline='') as f: # # writer = csv.writer(f) # # writer.writerow(['碑林','http://wwww.baidu.com']) # #区域 # zone_list = parseZonePage("https://xian.esf.fang.com") # # print("*"*60) # # time.sleep(5) # for zone in zone_list: # print(f"{zone.get('name')}:{zone.get('url')}") # #获取总页数 # page_num = parsePageSize(zone.get('url')) # print(f"共有{page_num}页") # #分页规则 # #https://xian.esf.fang.com/house-a016698/i3100/ 100 # #https://xian.esf.fang.com/house-a016698/i330/ 30 # #https://xian.esf.fang.com/house-a016698/i32/ 2 # #https://xian.esf.fang.com/house-a016698/i31/ 1 # #获取当面区域分页的的所有链接 # for i in range(1,page_num+1): # pageurl = f"{zone.get('url')}i3{i}/" #这个是分页的链接 # print(f"正在爬取的链接为{zone.get('name')}:{pageurl}") # house_urls = parseHouseListPage(pageurl) #得到当前分页页面中的所有范媛链接的数组 # #循环这个数组,并且放入csv文件中 # for house_url in house_urls: # print(f"正在写入的链接为:{house_url}") # with open('links.csv', 'a+', encoding='utf-8', newline='') as f: # writer = csv.writer(f) # writer.writerow([zone.get('name'),house_url]) # time.sleep(random.uniform(3,6)) # # #还缺少每个页面上的具体的每个链接 # # #还缺少每个具体链接上的所有数据 ``` ### 完整的房天下解析代码 ```python import requests from lxml import etree import time import re import csv import random cookies = { 'global_cookie': '1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq', '__utmz': '147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)', 'csrfToken': '-x7Dg8iDcMujcyLQZhP4XzSx', 'city': 'xian', '__utmc': '147393320', 'otherid': '36e15bb5f2f5663c16b18f0add5119dc', '__utma': '147393320.244321793.1781146046.1782550236.1782701864.8', '__utmt_t0': '1', '__utmt_t1': '1', '__utmt_t2': '1', 'g_sourcepage': 'esf_juhe%5Exq_web', '__utmb': '147393320.9.10.1782701864', 'unique_cookie': 'U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } headers = { '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', 'cache-control': 'max-age=0', 'priority': 'u=0, i', 'referer': 'https://xian.esf.fang.com/house-a016698/', 'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36', # 'cookie': 'global_cookie=1dfswltme8o1qzwv85f2fgi0e2ymq8wd6uq; __utmz=147393320.1781146046.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); csrfToken=-x7Dg8iDcMujcyLQZhP4XzSx; city=xian; __utmc=147393320; otherid=36e15bb5f2f5663c16b18f0add5119dc; __utma=147393320.244321793.1781146046.1782550236.1782701864.8; __utmt_t0=1; __utmt_t1=1; __utmt_t2=1; g_sourcepage=esf_juhe%5Exq_web; __utmb=147393320.9.10.1782701864; unique_cookie=U_rffvssi4xxjf8o1g0xp3qonb12bmqokg9fs*31', } #解析页面上的西安市下的所有区域的链接 def parseZonePage(url): response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) zone_list = [] zones = html.xpath('//span[@class="screen_title" and contains(text(),"区域")]/following-sibling::ul[1]/li/a') for zone in zones: zone_name = zone.text zone_url = url+zone.get('href') print(f"区域:{zone_name} 链接:{zone_url}") if zone_name and zone_url: zone_list.append({"name":zone_name,"url":zone_url}) return zone_list def parsePageSize(url): print("正在获取总页数...") response = requests.get(url,cookies=cookies,headers=headers) response.encoding = 'utf-8' html = etree.HTML(response.text) #共4页 allpageSize = html.xpath('//span[@class="last"]//text()') # print(f"总页数:{allpageSize[0]}") #共4页 numbers = re.findall(r'\d+',allpageSize[0]) page_num = int(numbers[0]) return page_num #解析每一个分页具体页面的具体链接 def parseHouseListPage(url): response = requests.get(url,cookies=cookies,headers=headers) html = response.text html = etree.HTML(response.text) house_urls = [] links = html.xpath('//div[contains(@class,"shop_list")]//dl//dt//a/@href') for link in links: full_link = 'https://xian.esf.fang.com'+link print(f'{full_link}') house_urls.append(full_link) return house_urls def parseDetail(url, zonename): try: response = requests.get(url, cookies=cookies, headers=headers, timeout=10) html = response.text html_text = etree.HTML(html) # 总价 容错 allmoney = html_text.xpath('//div[@class="tab-cont-right"]//div[1]/div[1]/div[1]//i[1]//text()') 区 = zonename 总价 = allmoney[0].strip() if allmoney else "" print(f'总价:{总价}') # 户型等6个字段统一处理 info_nodes = html_text.xpath('//div[@class="tt"]//text()') info_nodes = [x.strip() for x in info_nodes] floor_node = html_text.xpath('//div[@class="tab-cont-right"]//div[3]/div[2]/div[2]//text()') floor = floor_node[0][2:].strip() if (floor_node and floor_node[0].startswith('楼层')) else '' 房屋户型 = info_nodes[0] if len(info_nodes) > 0 else "" 建筑面积 = info_nodes[1] if len(info_nodes) > 1 else "" 单价 = info_nodes[2] if len(info_nodes) > 2 else "" 房屋朝向 = info_nodes[3] if len(info_nodes) > 3 else "" 所在楼层_raw = info_nodes[4] if len(info_nodes) > 4 else "" 所在楼层 = f"{所在楼层_raw}{floor}".strip() 装修情况 = info_nodes[5] if len(info_nodes) > 5 else "" print(f'房屋户型:{房屋户型}') print(f'建筑面积:{建筑面积}') print(f'单 价:{单价}') print(f'房屋朝向:{房屋朝向}') print(f'所在楼层:{所在楼层}') print(f'装修情况:{装修情况}') # 小区名称、区域容错 name_node = html_text.xpath('//div[@class="tab-cont-right"]//div[4]//div[1]//div[@class="rcont"]//div[1]//a//text()') load_node = html_text.xpath('//div[@id="address"]//a[2]//text()') 小区名称 = name_node[0].strip() if name_node else "" 所在区域 = load_node[0].strip() if load_node else "" print(f'小区名称:{小区名称},所在区域:{所在区域}') # 获取所有 text-item 元素(注意:不要加外层的 []) base_nodes = html_text.xpath('//div[@class="tab-cont-left"]//div[3]//div[@class="text-item clearfix"]') base_dict = {} for base_node in base_nodes: # 获取 span.lab 作为 key key_spans = base_node.xpath('./span[@class="lab"]/text()') key = key_spans[0].strip() if key_spans else "" # 获取 span.rcont 作为 value value_spans = base_node.xpath('./span[@class="rcont"]/text()') value = value_spans[0].strip() if value_spans else "" base_dict[key] = value # 注意这里是字典赋值,不是 append 建筑年代 = base_dict.get('建筑年代', "") 有无电梯 = base_dict.get('有无电梯', "") 产权性质 = base_dict.get('产权性质', "") 挂牌时间 = base_dict.get('挂牌时间', "") 梯户比例 = base_dict.get('梯户比例', "") 供暖方式 = base_dict.get('供暖方式', "") 房屋结构 = base_dict.get('房屋结构', "") 抵押信息 = base_dict.get('抵押信息', "") 楼层高度 = base_dict.get('楼层高度', "") 上次交易 = base_dict.get('上次交易', "") print(f'建筑年代:{建筑年代}') print(f'有无电梯:{有无电梯}') print(f'产权性质:{产权性质}') print(f'挂牌时间:{挂牌时间}') print(f'梯户比例:{梯户比例}') print(f'供暖方式:{供暖方式}') print(f'房屋结构:{房屋结构}') print(f'抵押信息:{抵押信息}') print(f'楼层高度:{楼层高度}') print(f'上次交易:{上次交易}') # 小区均价同比环比容错 xiaoquinfo = [x.strip() for x in html_text.xpath('//div[@class="topt clearfix"]//span[@class="rcont"]//text()')] 参考均价 = f"{xiaoquinfo[0]}{xiaoquinfo[1]}" if len(xiaoquinfo)>=2 else "" 同比 = xiaoquinfo[2] if len(xiaoquinfo)>2 else "" 环比 = xiaoquinfo[3] if len(xiaoquinfo)>3 else "" print(f'参考均价:{参考均价}') print(f'同比:{同比}') print(f'环比:{环比}') # 物业信息转字典,缺失键返回空 wuyeinfo = [x.strip() for x in html_text.xpath('//div[@class="content-item"]//div[@class="clearfix"]//text()')] wuye_dict = {} for i in range(0, len(wuyeinfo), 2): if i + 1 < len(wuyeinfo): key = wuyeinfo[i] val = wuyeinfo[i+1] wuye_dict[key] = val print(f"{key}: {val}") 物业类型 = wuye_dict.get('物业类型', "") 建筑类型 = wuye_dict.get('建筑类型', "") 绿化率 = wuye_dict.get('绿 化 率', "") 容积率 = wuye_dict.get('容 积 率', "") 人车分流 = wuye_dict.get('人车分流', "") 总楼栋数 = wuye_dict.get('总楼栋数', "") # 表头 header = [ "区", "总价", "房屋户型", "建筑面积", "单价", "房屋朝向", "所在楼层", "装修情况", "小区名称", "所在区域", "建筑年代", "有无电梯", "产权性质", "挂牌时间", "梯户比例", "供暖方式", "房屋结构", "抵押信息", "楼层高度", "上次交易", "小区参考均价", "同比", "环比", "物业类型", "建筑类型", "绿化率", "容积率", "人车分流", "总楼栋数" ] rows = [ 区, 总价, 房屋户型, 建筑面积, 单价, 房屋朝向, 所在楼层, 装修情况, 小区名称, 所在区域, 建筑年代, 有无电梯, 产权性质, 挂牌时间, 梯户比例, 供暖方式, 房屋结构, 抵押信息, 楼层高度, 上次交易, 参考均价, 同比, 环比, 物业类型, 建筑类型, 绿化率, 容积率, 人车分流, 总楼栋数 ] # 写入csv with open('房天下.csv', 'a+', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) f.seek(0) first_line = f.readline() if not first_line: writer.writerow(header) f.seek(0, 2) writer.writerow(rows) print(f"===== {url} 抓取完成 =====\n") except Exception as e: # 捕获所有异常,打印报错,继续下一条链接 print(f"【抓取失败】链接:{url},错误信息:{str(e)}\n") return if __name__ == '__main__': # url = "https://xian.esf.fang.com/chushou/3_173068020.htm?channel=2,2&psid=1_1_60" # parseDetail(url,'碑林') with open('links.csv', 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: zone = row['zone'] link = row['link'] print(f"zone:{zone} link:{link} ") parseDetail(link,zone) time.sleep(2) #线运行下面代码 得到所有的房源链接 然后再运行上面的代码 进行每个链接的解析 #下面的代码泡一下就能得到整个房天下西安地区的所有的房源链接 # #测试获取当前分页代码中的所有具体房源的链接 # # url = "https://xian.esf.fang.com/house-a016698/" # # parseHouseListPage(url) # # #这是一个单独的存储csv的链接的测试 # # with open('links.csv', 'a+', encoding='utf-8', newline='') as f: # # writer = csv.writer(f) # # writer.writerow(['碑林','http://wwww.baidu.com']) # #区域 # zone_list = parseZonePage("https://xian.esf.fang.com") # # print("*"*60) # # time.sleep(5) # for zone in zone_list: # print(f"{zone.get('name')}:{zone.get('url')}") # #获取总页数 # page_num = parsePageSize(zone.get('url')) # print(f"共有{page_num}页") # #分页规则 # #https://xian.esf.fang.com/house-a016698/i3100/ 100 # #https://xian.esf.fang.com/house-a016698/i330/ 30 # #https://xian.esf.fang.com/house-a016698/i32/ 2 # #https://xian.esf.fang.com/house-a016698/i31/ 1 # #获取当面区域分页的的所有链接 # for i in range(1,page_num+1): # pageurl = f"{zone.get('url')}i3{i}/" #这个是分页的链接 # print(f"正在爬取的链接为{zone.get('name')}:{pageurl}") # house_urls = parseHouseListPage(pageurl) #得到当前分页页面中的所有范媛链接的数组 # #循环这个数组,并且放入csv文件中 # for house_url in house_urls: # print(f"正在写入的链接为:{house_url}") # with open('links.csv', 'a+', encoding='utf-8', newline='') as f: # writer = csv.writer(f) # writer.writerow([zone.get('name'),house_url]) # time.sleep(random.uniform(3,6)) # # #还缺少每个页面上的具体的每个链接 # # #还缺少每个具体链接上的所有数据 ``` ## 数据清洗 ``` pip install pandas numpy ``` pandas:处理数据表格。读取写入csv,execl 排序,合并,处理缺失值,格式转换。分组统计。 numpy: 数学计算+数组运算的工具。 ```python import pandas as pd import numpy as np from datetime import datetime # 4.0 简单图表验证(可选) import matplotlib.pyplot as plt #pip install matplotlib # ========== 1. 读取数据 ========== # 读取你的CSV文件(根据实际文件名修改) df = pd.read_csv('房天下_房源信息.csv', encoding='utf-8') print("原始数据形状:", df.shape) print("\n前5行数据:") print(df.head()) print("\n数据信息:") print(df.info()) # 检测完全重复的行(所有列都相同) duplicate_count = df.duplicated().sum() # 只指定你想要检测的列(其他列自动被排除) # specific_columns = ['总价', '建筑面积', '单价', '房屋朝向', '房屋户型'] # # 只基于这些列检测重复 # duplicate_count = df.duplicated(subset=specific_columns).sum() print(f"完全重复的数据行数: {duplicate_count} 条") # 查看第一条重复数据 print(df[df.duplicated(keep='first')].head(1)) # 删除完全重复的行,保留第一次出现 # 记录去重前的数据量 before_count = len(df) df = df.drop_duplicates(keep='first') # 记录去重后的数据量 after_count = len(df) print(f"去重后数据量: {after_count} 条") print(f"删除了 {before_count - after_count} 条重复数据") # ========== 2. 删除不需要的字段 ========== # 根据文档思路,删除分析用不到的字段 # 注意:根据你实际的字段名调整 columns_to_drop = ['有无电梯'] # 这些字段文档中没有或不需要 # 只保留存在的列 columns_to_drop = [col for col in columns_to_drop if col in df.columns] data = df.drop(columns_to_drop, axis=1) print(f"\n删除不需要的列后,剩余{len(data.columns)}个字段") print("保留的字段:", list(data.columns)) # ========== 3. 删除缺失值 ========== # dropna() 是 Pandas 中用于删除缺失值(NaN)的方法。 data = data.dropna(subset=['总价']) # 删除总价、单价、建筑面积任一为空的 # data = data.dropna(subset=['总价', '单价', '建筑面积']) # # 或者只删除总价和单价都为空的 # data = data.dropna(subset=['总价', '单价'], how='all') print(f"删除后的数据形状: {data.shape}") # ========== 4. 数据清洗 ========== # 4.1 处理建筑面积(去掉"平米"单位,转为float) print("\n--- 处理建筑面积 ---") # strip() 是 Python 字符串方法,用于去除字符串首尾的指定字符(默认去除空白字符)。 data['建筑面积'] = data['建筑面积'].astype(str).str.replace('平米', '').str.strip() # errors 处理无法转换的值的方式 'raise'(报错)、'coerce'(变NaN)、'ignore'(保留原值) data['建筑面积'] = pd.to_numeric(data['建筑面积'], errors='coerce') print(f"转换完成,数据类型: {data['建筑面积'].dtype}") # 4.1.1 查看前10个转换结果 print("\n前10个建筑面积值:") print(data['建筑面积'].head(10)) # 4.1.2 查看统计信息 print("\n统计信息:") print(data['建筑面积'].describe()) # 定义清洗楼层的函数 def clean_floor(floor): if pd.isna(floor): return np.nan floor = str(floor).strip() # 统一为:低楼层、中楼层、高楼层、底层、顶层 if '底' in floor: return '底层' elif '顶' in floor: return '顶层' elif '低' in floor: return '低楼层' elif '中' in floor: return '中楼层' elif '高' in floor: return '高楼层' else: return floor data['所在楼层'] = data['所在楼层'].apply(clean_floor) print(f"清洗后所在楼层的唯一值: {data['所在楼层'].unique()}") print(f"清洗完成,各楼层数量统计:") print(data['所在楼层'].value_counts()) #清洗装修情况 # 查看装修情况的唯一值 print(data['装修情况'].unique()) def clean_decoration_type(decoration): if pd.isna(decoration): return '未知' decoration = str(decoration).strip() # 统一为:毛坯、简装、中装、精装、豪华装、未知 if '毛坯' in decoration: return '毛坯' elif '豪华' in decoration: return '豪华装修' elif '精' in decoration: return '精装修' elif '中' in decoration: return '中装修' elif '简' in decoration: return '简装修' elif decoration in ['暂无', '其他']: return '未知' else: return '未知' # 应用清洗 data['装修情况'] = data['装修情况'].apply(clean_decoration_type) print(data['装修情况'].unique()) # 4.1.3 查看无效数据(哪些转换成了NaN) # isna() 是 Pandas 中用于检测缺失值的方法,返回布尔值(True/False)。 invalid_area = data[data['建筑面积'].isna()]['建筑面积_原始'] if '建筑面积_原始' in data.columns else data[data['建筑面积'].isna()] print(f"\n无效数据示例(前5条):") if '建筑面积_原始' in data.columns: print(data[data['建筑面积'].isna()][['建筑面积_原始']].head()) else: # 如果没有保存原始列,显示其他列帮助定位问题 print(data[data['建筑面积'].isna()].head()) # 4.1.4 建筑面积图表验证(可选) plt.rcParams['font.sans-serif'] = ['SimHei'] # 显示中文 plt.rcParams['axes.unicode_minus'] = False fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 直方图 axes[0].hist(data['建筑面积'].dropna(), bins=30, edgecolor='black') axes[0].set_title('建筑面积分布') axes[0].set_xlabel('平方米') axes[0].set_ylabel('频数') # # 箱线图 axes[1].boxplot(data['建筑面积'].dropna()) axes[1].set_title('建筑面积箱线图') axes[1].set_ylabel('平方米') plt.tight_layout() plt.show() # 4.1.5 建筑面积清洗报告 # print("\n" + "="*50) # print("清洗报告") # print("="*50) # print(f"原始数据量: {len(df)}") # print(f"清洗后数据量: {len(data)}") # print(f"建筑面积缺失数: {data['建筑面积'].isna().sum()}") # print(f"建筑面积有效数: {data['建筑面积'].count()}") # print(f"建筑面积范围: {data['建筑面积'].min():.1f} ~ {data['建筑面积'].max():.1f} 平米") # print(f"建筑面积平均值: {data['建筑面积'].mean():.1f} 平米") # print(f"建筑面积中位数: {data['建筑面积'].median():.1f} 平米") # 4.2 处理单价(去掉"元/平米",转为float) print("\n--- 处理单价 ---") if '单价' in data.columns: data['单价'] = data['单价'].astype(str).str.replace('元/平米', '').str.strip() data['单价'] = pd.to_numeric(data['单价'], errors='coerce') data['单价'] = data['单价'].round(2) # 保留两位小数 print(f"单价转换完成") # 4.2.1 查看单价统计信息 print(data['单价'].describe()) # count 16707 有效数据有 16707 条(成功转换的) # mean 11366 平均单价约 1.1万元/平米 # std 4262 标准差,表示数据波动范围(越大越分散) # min 2824 最低单价 2824元/平米(可能是偏远地区) # 25% 8520 25%的房源单价低于 8520元/平米 # 50% 10366 中位数,一半房源低于这个价 # 75% 13234 75%的房源单价低于 13234元/平米 # max 61181 最高单价 6.1万元/平米(可能是豪宅) # 4.2.2 按价格区间分组统计(可选) # bins = [0, 5000, 8000, 10000, 15000, 20000, 50000, 100000] # labels = ['<5000', '5000-8000', '8000-10000', '10000-15000', '15000-20000', '20000-50000', '>50000'] # data['价格区间'] = pd.cut(data['单价'], bins=bins, labels=labels) # print("\n单价区间分布:") # print(data['价格区间'].value_counts().sort_index()) # 4.2.3 单价直方图(可选) # plt.rcParams['font.sans-serif'] = ['SimHei'] # # bins 分组数量 50 将数据分成50个区间 data['单价'].dropna()去掉缺失值后的单价列 edgecolor 柱子边框颜色 # plt.hist(data['单价'].dropna(), bins=50, edgecolor='black') # plt.title('单价分布') # plt.xlabel('单价 (元/平米)') # plt.ylabel('数量') # plt.show() # 4.3 处理总价(转为float,如果总价列存在) print("\n--- 处理总价 ---") if '总价' in data.columns: data['总价'] = pd.to_numeric(data['总价'], errors='coerce') print(f"总价转换完成") print(data['总价'].describe()) # 4.6 建筑年代清洗 # 去掉末尾的"年"字,并转换为数值类型 data['建筑年代'] = data['建筑年代'].astype(str).str.replace('年', '', regex=False) # 将空字符串、'nan'、'None'等转换为NaN data['建筑年代'] = data['建筑年代'].replace(['', 'nan', 'None', 'null'], np.nan) # 转换为数值类型,无法转换的会成为NaN data['建筑年代'] = pd.to_numeric(data['建筑年代'], errors='coerce') # 删除建筑年代为空的行 data = data.dropna(subset=['建筑年代']) # 转换为整数类型 data['建筑年代'] = data['建筑年代'].astype(int) print(f"\n清洗后数据量: {len(data)}") print(f"\n建筑年代统计信息:") print(data['建筑年代'].describe()) print(f"\n建筑年代分布:") print(data['建筑年代'].value_counts().sort_index().head(10)) # 4.8 清除产权性质为空的行 print("\n--- 清除产权性质为空的行 ---") print(f"清洗前数据量: {len(data)}") # 将空值、空字符串等转换为NaN data['产权性质'] = data['产权性质'].replace(['', ' ', 'nan', 'None', 'null'], np.nan) # 删除产权性质为空的行 data = data.dropna(subset=['产权性质']) print(f"\n清洗后数据量: {len(data)}") print(f"\n产权性质分布:") print(data['产权性质'].value_counts()) # 4.9 清除房屋朝向为空的行 # 将空值、空字符串等转换为NaN data['房屋朝向'] = data['房屋朝向'].replace(['', ' ', 'nan', 'None', 'null', '未知'], np.nan) # 删除房屋朝向为空的行 data = data.dropna(subset=['房屋朝向']) print(f"\n清洗后数据量: {len(data)}") print(f"\n房屋朝向分布:") print(data['房屋朝向'].value_counts()) # 4.9 清除所在楼层空的行 # 将空值、空字符串等转换为NaN data['所在楼层'] = data['所在楼层'].replace(['', ' ', 'nan', 'None', 'null', '未知'], np.nan) # 删除房屋朝向为空的行 data = data.dropna(subset=['所在楼层']) ###清洗装修情况没有或者未知的数据 data['装修情况'] = data['装修情况'].replace(['', ' ', 'nan', 'None', 'null', '未知'], np.nan) # 删除房屋朝向为空的行 data = data.dropna(subset=['装修情况']) ###清洗小区名称没有或者未知的数据 data['小区名称'] = data['小区名称'].replace(['', ' ', 'nan', 'None', 'null', '未知'], np.nan) # 删除房屋朝向为空的行 data = data.dropna(subset=['小区名称']) ###清洗所在区域没有或者未知的数据 data['所在区域'] = data['所在区域'].replace(['', ' ', 'nan', 'None', 'null', '未知'], np.nan) # 删除房屋朝向为空的行 data = data.dropna(subset=['所在区域']) # 4.4 处理挂牌时间 print("\n--- 处理挂牌时间 ---") now = pd.to_datetime('2026-06-07') # 根据当前日期修改 data['挂牌时间'] = pd.to_datetime(data['挂牌时间'], errors='coerce') # 4.4.1 计算挂牌距今天数 data['挂牌天数'] = now - data['挂牌时间'] # 转换为天数(数值) data['挂牌天数'] = data['挂牌天数'].dt.days print(data['挂牌天数'].head(10)) # 4.4.2 对挂牌时间离散化处理 print("\n--- 挂牌时间离散化 ---") bins1 = [0, 60, 120, 365, 730, 1460, 100000] # 天 labels1 = ['两个月内', '一个季度', '一年', '两年', '3年', '3年以上'] # pd.cut() 是 Pandas 中用于将连续数值分成离散区间的函数,类似于数据分组/分箱。 data['挂牌时长'] = pd.cut(data['挂牌天数'], bins=bins1, labels=labels1) print(f"挂牌时间范围: {data['挂牌时间'].min()} 到 {data['挂牌时间'].max()}") print(data['挂牌时长'].head(10)) # 4.4.3 摘取挂牌时间的年-月做辅助列 data['挂牌年月'] = data['挂牌时间'].dt.strftime("%Y-%m") print(data['挂牌年月'].head(10)) # 4.5 面积离散化 print("\n--- 面积离散化 ---") bins = [0, 60, 90, 120, 150, 300, 1000] labels = ['60平以下', '60-90平', '90-120平', '120-150平', '150-300平', '300平以上'] data['面积区间'] = pd.cut(data['建筑面积'], bins=bins, labels=labels) print(data['面积区间'].head(10)) # 这就是数据库索引的核心原理:对原始数值列建索引,远比对派生分类列建索引有效。 # 有了这个面积区间,查询速度并不一定速度快 # 查询对比 # sql # -- 方式1:用面积区间列(慢) # SELECT * FROM 房源表 WHERE 面积区间 = '90-120平'; # -- 问题:'90-120平' 这个标签对应成千上万条记录 # -- 即使有索引,MySQL也要扫描大量数据 # -- 方式2:用建筑面积列(快) # SELECT * FROM 房源表 WHERE 建筑面积 BETWEEN 90 AND 120; # -- 优势:直接范围查找,索引精确定位 # -- 速度可快 10-100 倍 # 4.6 异常值处理:删除异常面积和单价 print("\n--- 异常值处理 ---") # 4.6.1 查看异常数据 print(data[(data['建筑面积'] <= 10) | (data['建筑面积'] >= 500) | (data['单价'] <= 1000) | (data['单价'] >= 100000) | (data['总价'] <= 10) | (data['总价'] >= 5000)]) initial_count = len(data) data = data[data['建筑面积'] > 10] # 面积至少10平米 data = data[data['建筑面积'] < 500] # 面积不超过500平米 if '单价' in data.columns: data = data[data['单价'] > 1000] # 单价至少1000元/平米 data = data[data['单价'] < 100000] # 单价不超过10万/平米 if '总价' in data.columns: data = data[data['总价'] > 10] # 总价至少10万 data = data[data['总价'] < 5000] # 总价不超过5000万 removed_count = initial_count - len(data) print(f"删除异常数据 {removed_count} 条,剩余 {len(data)} 条") # 4.7 清洗区域和所在区域的特殊字符 print("\n--- 清洗区域和所在区域 ---") # # 清洗区域列(去除制表符、空格等) 这里的碑林区域后面有个\t.所以才要处理 # data['区域'] = data['区域'].astype(str).str.strip().str.replace('\t', '').str.replace('\n', '') print("\n--- 处理所在区域 ---") def clean_location(x): if pd.isna(x): return np.nan x_str = str(x).strip() # 如果包含空格,取空格后面的部分 if ' ' in x_str: return x_str.split(' ')[-1] # 取最后一段 else: return x_str # 应用处理 data['所在区域'] = data['所在区域'].apply(clean_location) # 查看处理结果 print("处理后的所在区域:") print(data[['区域', '所在区域']].head(10)) print("\n" + "="*50) print("房源类型智能分类") print("="*50) def classify_house_by_price_area(row): """ 根据面积和单价双重条件分类房源 """ area = row['建筑面积'] price = row['单价'] # 1. 豪宅/别墅判断(满足任一条件即可) # 条件A:大面积 + 高单价 if area >= 180 and price >= 15000: return '豪宅/大平层' # 条件B:超大面积(即使单价不高) if area >= 300: return '豪宅/别墅' # 条件C:极高单价(即使面积不大) if price >= 25000: return '豪宅/大平层' # 2. 改善型住房 if area >= 140 and price >= 12000: return '改善型' if area >= 160 and price >= 10000: return '改善型' # 3. 普通住宅 if area >= 60 and area < 140: return '普通住宅' # 4. 小户型 if area < 60: return '小户型' # 5. 其他特殊情况 if area >= 140 and price < 10000: return '大户型低价' if area >= 200: return '大户型' return '普通住宅' # 应用分类 print("\n应用分类函数...") data['房源类型'] = data.apply(classify_house_by_price_area, axis=1) # 查看分类结果 print("\n房源类型分布:") print("="*50) type_counts = data['房源类型'].value_counts() print(type_counts) print("\n占比:") print(type_counts / len(data) * 100) # 查看各类型的统计信息 print("\n各类型房源统计:") print("="*50) type_stats = data.groupby('房源类型').agg({ '建筑面积': ['count', 'mean', 'min', 'max'], '单价': ['mean', 'min', 'max'], '总价': ['mean', 'min', 'max'] }).round(2) print(type_stats) # 6 处理同比环比的数据 def clean_percent(x): """将 '↓13.72%' 转换为 -13.72""" if pd.isna(x): return np.nan x_str = str(x).strip() # 去除百分号 x_str = x_str.replace('%', '') if x_str in ['暂无资料', '暂无', '无', '-', '']: return np.nan # 判断方向 if x_str.startswith('↓'): return -float(x_str[1:]) # 下降 → 负数 elif x_str.startswith('↑'): return float(x_str[1:]) # 上升 → 正数 else: return float(x_str) # 应用清洗 data['同比去年'] = data['同比去年'].apply(clean_percent) data['环比上月'] = data['环比上月'].apply(clean_percent) print(data[['同比去年', '环比上月']].head()) # ========== 6. 查看最终数据 ========== print("\n" + "="*50) print("最终数据信息:") print(data.info()) print("\n缺失值统计:") print(data.isna().sum()) print("\n数值字段统计:") print(data.describe()) # ========== 7. 保存清洗后的数据 ========== # 保存为CSV data.to_csv('房源数据_清洗后.csv', index=False, encoding='utf-8-sig') print("\n清洗后的数据已保存为: 房源数据_清洗后.csv") # 可选:保存到MySQL(如果需要) # from sqlalchemy import create_engine # engine = create_engine('mysql+pymysql://root:1234@localhost:3306/house') # data.to_sql(name='HouseData', con=engine, index=False, if_exists='replace') ``` ## 数据分析与可视化 ![image-20260701090609949](images/image-20260701090609949.png) ```python import pandas as pd import numpy as np from datetime import datetime import matplotlib.pyplot as plt from wordcloud import WordCloud # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 读取数据 df = pd.read_csv('房源数据_清洗后.csv', encoding='utf-8') print("原始数据形状:", df.shape) print("\n数据信息:") print(df.info()) def parse1(): # 各区域房源统计 region_stats = df.groupby('区域')['单价'].describe().sort_values('mean', ascending=False) print("\n--- 各区域单价统计(按均价降序)---") print(region_stats) region_count = region_stats['count'].sort_values(ascending=False) plt.figure(figsize=(10, 6)) plt.bar(region_count.index, region_count.values, color='steelblue') plt.title('各区域房源数量统计', fontsize=16) plt.xlabel('区域', fontsize=12) plt.ylabel('房源数量(套)', fontsize=12) plt.xticks(rotation=45) for a, b in zip(region_count.index, region_count.values): plt.text(a, b + 50, int(b), ha='center', fontsize=10) plt.tight_layout() plt.show() def parse2(): # 西安市各区域均价统计 region_avg_price = df.groupby('区域')['单价'].mean().sort_values(ascending=False).round(0) print("\n" + "="*40) print("西安市各区域均价统计") print("="*40) for region, price in region_avg_price.items(): print(f"{region}: {int(price)} 元/平") print("="*40) plt.figure(figsize=(10, 6)) bars = plt.bar(region_avg_price.index, region_avg_price.values) plt.title('西安市各区域平均单价', fontsize=14) plt.xlabel('区域', fontsize=12) plt.ylabel('平均单价(元/平)', fontsize=12) plt.xticks(rotation=45) for bar, price in zip(bars, region_avg_price.values): plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 100, f'{int(price)}', ha='center', va='bottom', fontsize=10) plt.tight_layout() plt.show() return region_avg_price def parse3(): # 雁塔区各板块房源数量 yanta_data = df[df['区域'] == '雁塔'] subregion_count = yanta_data['所在区域'].value_counts().sort_values(ascending=False) plt.figure(figsize=(10, 6)) plt.bar(subregion_count.index, subregion_count.values, color='coral') plt.title('雁塔区各板块房源数量统计', fontsize=14) plt.xlabel('板块', fontsize=12) plt.ylabel('房源数量(套)', fontsize=12) plt.xticks(rotation=45) for i, (name, count) in enumerate(subregion_count.items()): plt.text(i, count + 5, str(count), ha='center', fontsize=10) plt.tight_layout() plt.show() def parse4(): # 雁塔区各板块均价 yanta_data = df[df['区域'] == '雁塔'] subregion_price = yanta_data.groupby('所在区域')['单价'].mean().sort_values(ascending=False) print(subregion_price) plt.figure(figsize=(12, 6)) bars = plt.bar(subregion_price.index, subregion_price.values, color='steelblue', edgecolor='black') plt.title('雁塔区各板块均价统计', fontsize=16) plt.xlabel('板块', fontsize=12) plt.ylabel('均价(元/平米)', fontsize=12) plt.xticks(rotation=45, ha='right') for bar, price in zip(bars, subregion_price.values): plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 100, f'{price:.0f}', ha='center', fontsize=10) plt.tight_layout() plt.show() def parse5(): # 西安市各月份挂牌数量统计 df['挂牌年月'] = pd.to_datetime(df['挂牌时间']).dt.strftime('%Y-%m') time_data = df.groupby('挂牌年月')['城市'].count() plt.figure(figsize=(12, 6)) plt.plot(time_data.index, time_data.values, 'o-', color='steelblue') plt.title('西安市各月份挂牌数量变化情况', fontsize=14) plt.xlabel('月份', fontsize=12) plt.ylabel('挂牌数量(套)', fontsize=12) for a, b in zip(time_data.index, time_data.values): plt.text(a, b + 5, int(b), ha='center', fontsize=9) plt.xticks(rotation=45) plt.tight_layout() plt.show() return time_data def parse6(): # 西安市各季度挂牌数量统计 df['挂牌季度'] = pd.to_datetime(df['挂牌时间']).dt.to_period('Q').astype(str) time_data = df.groupby('挂牌季度')['城市'].count() plt.figure(figsize=(12, 6)) plt.plot(time_data.index, time_data.values, 'o-', color='steelblue') plt.title('西安市各季度挂牌数量变化情况', fontsize=14) plt.xlabel('季度', fontsize=12) plt.ylabel('挂牌数量(套)', fontsize=12) for a, b in zip(time_data.index, time_data.values): plt.text(a, b + 5, int(b), ha='center', fontsize=9) plt.xticks(rotation=45) plt.tight_layout() plt.show() return time_data def parse7(): # 房源主要朝向占比统计 orientation_count = df['房屋朝向'].value_counts() orientation_percent = (orientation_count / len(df) * 100).round(1) orientation_stats = pd.DataFrame({ '数量': orientation_count, '占比(%)': orientation_percent }) print("\n" + "="*50) print("西安市房源主要朝向占比统计") print("="*50) print(orientation_stats.to_string()) print("="*50) plt.figure(figsize=(10, 8)) threshold = 3 other_sum = orientation_percent[orientation_percent < threshold].sum() main_orientations = orientation_percent[orientation_percent >= threshold] if other_sum > 0: main_orientations['其他'] = other_sum colors_pie = plt.cm.Pastel1(range(len(main_orientations))) plt.pie(main_orientations.values, labels=main_orientations.index, autopct='%1.1f%%', colors=colors_pie, startangle=90, textprops={'fontsize': 11}) plt.title('西安市房源各朝向占比', fontsize=16, fontweight='bold') plt.tight_layout() plt.show() return orientation_stats def parse8(): # 西安市所在楼层占比统计 floor_count = df['所在楼层'].value_counts() floor_percent = (floor_count / len(df) * 100).round(1) floor_stats = pd.DataFrame({ '数量': floor_count, '占比(%)': floor_percent }) print("\n" + "="*50) print("西安市房源所在楼层占比统计") print("="*50) print(floor_stats.to_string()) print("="*50) plt.figure(figsize=(10, 8)) colors_pie = plt.cm.Pastel1(range(len(floor_percent))) plt.pie(floor_percent.values, labels=floor_percent.index, autopct='%1.1f%%', colors=colors_pie, startangle=90, textprops={'fontsize': 11}) plt.title('西安市房源各所在楼层占比', fontsize=16, fontweight='bold') plt.tight_layout() plt.show() return floor_stats def parse9(): # 各楼层类别平均价格统计 floor_avg_price = df.groupby('所在楼层')['单价'].mean().round(0).sort_values(ascending=False) print("\n" + "="*50) print("各楼层类别平均单价统计") print("="*50) for floor, price in floor_avg_price.items(): print(f"{floor}: {int(price)} 元/平") print("="*50) plt.figure(figsize=(10, 6)) colors = plt.cm.Set3(range(len(floor_avg_price))) bars = plt.bar(floor_avg_price.index, floor_avg_price.values, color=colors) plt.title('各楼层类别平均单价对比', fontsize=14, fontweight='bold') plt.xlabel('所在楼层', fontsize=12) plt.ylabel('平均单价(元/平)', fontsize=12) plt.xticks(rotation=45) for bar, price in zip(bars, floor_avg_price.values): plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 100, f'{int(price)}', ha='center', va='bottom', fontsize=10) plt.tight_layout() plt.show() return floor_avg_price def parse10(): # 西安市产权性质统计图 property_count = df['产权性质'].value_counts() property_percent = (property_count / len(df) * 100).round(1) property_stats = pd.DataFrame({ '数量': property_count, '占比(%)': property_percent }) print("\n" + "="*50) print("西安市产权性质统计") print("="*50) print(property_stats.to_string()) print("="*50) threshold = 2 main_property = property_percent[property_percent >= threshold] other_sum = property_percent[property_percent < threshold].sum() if other_sum > 0: main_property['其他'] = other_sum plt.figure(figsize=(10, 8)) colors_pie = plt.cm.Set3(range(len(main_property))) plt.pie(main_property.values, labels=main_property.index, autopct='%1.1f%%', colors=colors_pie, startangle=90, textprops={'fontsize': 11}) plt.title('西安市房源产权性质占比', fontsize=16, fontweight='bold') plt.tight_layout() plt.show() return property_stats def parse11(): # 建筑面积数量分布折线图 area_data = df['建筑面积'].dropna() bins = range(0, int(area_data.max()) + 20, 20) area_counts, bin_edges = np.histogram(area_data, bins=bins) bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 plt.figure(figsize=(12, 5)) plt.plot(bin_centers, area_counts, 'o-', color='steelblue') plt.title('西安市房源建筑面积分布') plt.xlabel('建筑面积(平米)') plt.ylabel('房源数量(套)') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() def parse12(): # 装修情况饼图 decoration_count = df['装修情况'].value_counts() plt.figure(figsize=(8, 8)) plt.pie(decoration_count.values, labels=decoration_count.index, autopct='%1.1f%%') plt.title('西安市房源装修情况占比') plt.tight_layout() plt.show() def parse13(): # 房源定位统计图 position_count = df['房源定位'].value_counts() print("\n房源定位统计:") print(position_count) plt.figure(figsize=(8, 8)) plt.pie(position_count.values, labels=position_count.index, autopct='%1.1f%%') plt.title('西安市房源定位占比') plt.tight_layout() plt.show() def parse14(): # 单价大于2.5万的小区词云图 high_price_community = df[df['单价'] > 25000]['小区名称'].drop_duplicates() try: word_cloud = WordCloud(width=800, height=600, font_path='simhei.ttf', background_color='white').generate(' '.join(high_price_community)) plt.figure(figsize=(10, 8)) plt.imshow(word_cloud, interpolation="bilinear") plt.axis("off") plt.title('单价大于2.5万/平的小区', fontsize=14) plt.tight_layout() plt.show() except Exception as e: print(f"生成词云图失败: {e}") parse14() # 执行所有分析 if __name__ == "__main__": print("\n开始执行数据分析...") # parse1() # parse2() # parse3() # parse4() # parse5() # parse6() # parse7() # parse8() # parse9() # parse10() # parse11() # parse12() # parse13() # parse14() ``` ## 房价预测模型 机器学习 ​ 线性回归,紧邻回归,决策树等自适应提升回归能够进行网格搜索进行比对参数,随机森林。专业性质。 ``` 1:把你准备好的数据进行找规律。数据【结构化数据】 2:训练 ---创建多科决策树 ---每棵树随机抽样部分样本和特征 ---没课书学习一个简单的预测规则 ---取所有的平均值做为预测结果 for 每科树 in 200课树: 1: 选择部分样本(80%) 2:随机选择部分特征(比如5个特征) 3:寻找最佳的分割点(面积》100) 4:递归分割至到满足条件就停止 5: 记录每个节点的决策规则。 决策:重要性。 你通过这个决策规则: 预测房价。 ``` AI大模型 ​ 学习的参数上几十亿。训练数据TB。训练时间 上月。对机器要求也比较高。显卡。 ``` import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score import pickle import warnings warnings.filterwarnings('ignore') print("数据处理开始...") data = pd.read_csv('房源数据_清洗后.csv') print(data.info()) # ============ 关键点1: 特征选择 ============ # 删除与预测目标(单价)无关或包含噪声的列 # 注意: '总价'和'建筑面积'相关,'单价'=总价/面积,删除总价避免数据泄露 # '小区名称'等文本特征维度太高,'挂牌年月'等时间特征可能不适合直接编码 # '经度''纬度'需要特殊处理,'面积区间'是建筑面积的分箱,与建筑面积重复 new_data = data.drop(['总价', '小区名称', '挂牌年月', '房屋户型', '挂牌时间', '房源标签', '地铁信息', '小区参考均价', '挂牌时长', '城市', '同比去年', '环比上月', '经度', '纬度', '面积区间'], axis=1) # ============ 关键点2: 特征编码策略 ============ # 获取所有非数值型特征列名(排除建筑面积、单价、总楼层、建筑年代、挂牌天数这些数值列) col_names = new_data.columns.drop(['建筑面积', '单价', '总楼层', '建筑年代', '挂牌天数']) le = LabelEncoder() # 难点: LabelEncoder会将每个类别映射为0到n_classes-1的整数 # 注意: 这种编码方式假设类别之间有大小关系,对树模型影响较小(树模型基于分裂点) # 但对线性模型不适用。这里使用随机森林,影响可以接受 for col_name in col_names: new_data[col_name] = le.fit_transform(new_data[col_name]) print(new_data.info()) x = new_data.drop(['单价'], axis=1) y = new_data['单价'] # ============ 关键点3: 保存特征列名 ============ # 难点: 预测时必须使用相同的特征顺序和列名 # 保存特征列名确保后续部署时输入数据与训练数据对齐 feature_columns = x.columns.tolist() print(f"特征数量: {len(feature_columns)}") print(feature_columns) # ============ 关键点4: 缺失值处理 ============ # 使用中位数填充数值型特征的缺失值 # 中位数对异常值不敏感,比均值更稳健 numeric_cols = ['建筑面积', '总楼层', '建筑年代'] for col in numeric_cols: if col in x.columns: x[col] = x[col].fillna(x[col].median()) # ============ 关键点5: 数据标准化 ============ # 虽然随机森林不需要标准化(基于树的模型对尺度不敏感), # 但为了便于特征重要性比较和后续可能换用其他模型,这里进行标准化 # 注意: 使用训练集的均值和标准差来转换测试集,避免数据泄露 #80%作为我们的训练集 random_state=42 固定的随机种子,每次运行结果一致 方便实验复现。 x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42) #特征标准化,将所有特征缩放到均值0,方差1 #不同的特征差异(面积差个几十,楼层差个个位数,数据差距大的时候能够影响模型) scaler = StandardScaler() x_train_scale = scaler.fit_transform(x_train) # fit+transform训练集 x_test_scale = scaler.transform(x_test) # 只用transform测试集 # ============ 关键点6: 目标变量对数变换 ============ # 难点: 房价(单价)通常呈右偏分布,取对数可以: # 1. 使分布更接近正态分布,让模型更容易拟合 # 2. 减少异常值的影响(大房价的误差被压缩) # 3. 对数变换后预测的是ln(单价),后续需要exp还原 y_train_log = np.log(y_train) y_test_log = np.log(y_test) print("\n开始训练随机森林...") # ============ 关键点7: 随机森林超参数调优 ============ #这个参数你是可以通过网格搜索进行调优的,现在的参数是固定的。但不代表针对当前数据,这个参数就是最优的。 #参数的不同,有可能会导致过拟合和欠拟合。 rf_model = RandomForestRegressor( n_estimators=200, # 树的数量: 越多越稳定,但计算成本增加 max_depth=15, # 最大深度: 限制树的复杂度,防止过拟合 min_samples_split=10, # 分裂所需最小样本数: 越大模型越简单 min_samples_leaf=5, # 叶节点最小样本数: 防止叶节点样本太少导致过拟合 max_features='sqrt', # 每次分裂考虑的特征数: sqrt(总特征数),增加随机性 random_state=42, # 固定随机种子,保证结果可复现 n_jobs=-1 # 使用所有CPU核心并行计算 ) #对标准化之后的特性,进行数值转换后的房价标签进行训练 rf_model.fit(x_train_scale, y_train_log) # 评估 y_train_pred = rf_model.predict(x_train_scale) y_test_pred = rf_model.predict(x_test_scale) # ============ 关键点8: 交叉验证评估 ============ # 难点: 5折交叉验证能更可靠地评估模型泛化能力 # 它将训练集分成5份,轮流用4份训练1份验证,比单次train-test split更稳健 scores = cross_val_score(rf_model, x_train_scale, y_train_log, scoring='r2', cv=5) print(f"5折交叉验证平均R²: {scores.mean():.4f} (±{scores.std():.4f})") print(f"训练集R方: {r2_score(y_train_log, y_train_pred):.4f}") print(f"测试集R方: {r2_score(y_test_log, y_test_pred):.4f}") # ============ 关键点9: 特征重要性分析 ============ # RandomForest的feature_importances_基于: # 1. 该特征被选为分裂点的次数 # 2. 该特征作为分裂点时减少的不纯度(方差减少量) # 注意: 重要性是相对值,总和为1 feature_importance = pd.DataFrame({ '特征': feature_columns, # 使用保存的特征列名 '重要性': rf_model.feature_importances_ }).sort_values('重要性', ascending=False) print("\n特征重要性 TOP 10:") print(feature_importance.head(10)) # ============ 关键点10: 模型持久化 ============ # 难点: 部署时需要同时保存模型、标准化器、特征列名 # 1. 模型: 用于预测 # 2. 标准化器: 新数据需要同样的标准化(用训练集的均值和标准差) # 3. 特征列名: 保证新数据特征顺序和训练时一致 with open('house_price_model.pkl', 'wb') as f: pickle.dump(rf_model, f) with open('scaler.pkl', 'wb') as f: pickle.dump(scaler, f) with open('feature_columns.pkl', 'wb') as f: # pickle.dump(feature_columns, f) print(f"特征列已保存,共 {len(feature_columns)} 个特征") ``` ``` 特征重要性 TOP 10: 特征 重要性 11 房源类型 0.300976 0 区域 0.167937 1 建筑面积 0.138325 7 建筑年代 0.129856 6 所在区域 0.082363 4 总楼层 0.067609 5 装修情况 0.039439 8 产权性质 0.028985 10 挂牌天数 0.023581 3 所在楼层 0.008167 特征列已保存,共 12 个特征 ``` ```python import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV, KFold from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.ensemble import RandomForestRegressor, AdaBoostRegressor from sklearn.linear_model import LinearRegression from sklearn.neighbors import KNeighborsRegressor from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import r2_score import pickle import warnings import time warnings.filterwarnings('ignore') print("数据处理开始...") data = pd.read_csv('房源数据_清洗后.csv') print(data.info()) new_data = data.drop(['总价', '小区名称', '挂牌年月', '房屋户型', '挂牌时间', '房源标签', '地铁信息', '小区参考均价', '挂牌时长', '城市', '同比去年', '环比上月', '经度', '纬度', '面积区间'], axis=1) col_names = new_data.columns.drop(['建筑面积', '单价', '总楼层', '建筑年代', '挂牌天数']) le = LabelEncoder() for col_name in col_names: new_data[col_name] = le.fit_transform(new_data[col_name]) print(new_data.info()) x = new_data.drop(['单价'], axis=1) y = new_data['单价'] feature_columns = x.columns.tolist() print(f"特征数量: {len(feature_columns)}") print(feature_columns) numeric_cols = ['建筑面积', '总楼层', '建筑年代'] for col in numeric_cols: if col in x.columns: x[col] = x[col].fillna(x[col].median()) x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42) scaler = StandardScaler() x_train_scale = scaler.fit_transform(x_train) x_test_scale = scaler.transform(x_test) y_train_log = np.log(y_train) y_test_log = np.log(y_test) print("\n开始训练随机森林...") rf_model = RandomForestRegressor( n_estimators=100, random_state=42, n_jobs=-1 ) rf_model.fit(x_train_scale, y_train_log) # 评估原始模型 y_train_pred = rf_model.predict(x_train_scale) y_test_pred = rf_model.predict(x_test_scale) scores = cross_val_score(rf_model, x_train_scale, y_train_log, scoring='r2', cv=5) print(f"5折交叉验证平均R2: {scores.mean():.4f} (±{scores.std():.4f})") print(f"训练集R方: {r2_score(y_train_log, y_train_pred):.4f}") print(f"测试集R方: {r2_score(y_test_log, y_test_pred):.4f}") # 特征重要性 feature_importance = pd.DataFrame({ '特征': feature_columns, '重要性': rf_model.feature_importances_ }).sort_values('重要性', ascending=False) print("\n特征重要性 TOP 10:") print(feature_importance.head(10)) # ========================================== # 新增:模型对比与参数优化 # ========================================== print("\n" + "="*60) print("开始模型对比与参数优化...") print("="*60) # ========================================== # 第一步:快速模型对比 # ========================================== print("\n【1】快速模型对比(5折交叉验证)...") print("-"*40) models = { 'Linear': LinearRegression(), 'KNN': KNeighborsRegressor(), 'Tree': DecisionTreeRegressor(random_state=42), 'Adaboost': AdaBoostRegressor(random_state=42) } kf = KFold(n_splits=5, shuffle=True, random_state=42) print("模型性能对比(基于训练集):") for name, model in models.items(): start_time = time.time() scores = cross_val_score(model, x_train_scale, y_train_log, scoring='r2', cv=kf) elapsed = time.time() - start_time print(f' {name:10s}: R2 = {scores.mean():.4f} (±{scores.std():.4f}) 耗时: {elapsed:.1f}s') # 显示当前RandomForest的性能 rf_scores = cross_val_score(rf_model, x_train_scale, y_train_log, scoring='r2', cv=kf) print(f' {"RandomForest":10s}: R2 = {rf_scores.mean():.4f} (±{rf_scores.std():.4f}) (当前模型)') # ========================================== # 第二步:网格搜索优化RandomForest # ========================================== print("\n【2】开始网格搜索(寻找RandomForest最优参数)...") print("-"*40) print("搜索参数组合:") print(" - n_estimators: [100, 200, 300]") print(" - max_depth: [15, 20, 25]") print(" - min_samples_split: [2, 3, 5]") print(" - min_samples_leaf: [1, 2, 4]") print(" 总共 3×3×3×3 = 81 种组合") print(" 预计耗时: 3-8分钟,请耐心等待...") parameters = { 'n_estimators': [100, 200, 300], 'max_depth': [15, 20, 25], 'min_samples_split': [2, 3, 5], 'min_samples_leaf': [1, 2, 4] } grid_search = GridSearchCV( estimator=RandomForestRegressor(random_state=42), param_grid=parameters, cv=3, scoring='r2', n_jobs=-1, verbose=1 ) start_time = time.time() print("\n开始搜索...") grid_search.fit(x_train_scale, y_train_log) elapsed = time.time() - start_time print(f"\n网格搜索完成!耗时: {elapsed/60:.1f} 分钟") # ========================================== # 第三步:选择最优模型 # ========================================== print("\n【3】选择最优参数") print("-"*40) best_model = grid_search.best_estimator_ print(f"最佳参数组合:") for param, value in grid_search.best_params_.items(): print(f" {param}: {value}") print(f"交叉验证最佳R2: {grid_search.best_score_:.4f}") # 使用最优模型评估 y_train_pred_opt = best_model.predict(x_train_scale) y_test_pred_opt = best_model.predict(x_test_scale) train_r2_opt = r2_score(y_train_log, y_train_pred_opt) test_r2_opt = r2_score(y_test_log, y_test_pred_opt) print(f"\n最优模型性能:") print(f" 训练集R2: {train_r2_opt:.4f}") print(f" 测试集R2: {test_r2_opt:.4f}") # 性能对比 original_test_r2 = r2_score(y_test_log, y_test_pred) improvement = (test_r2_opt - original_test_r2) * 100 print(f" 原始模型测试集R2: {original_test_r2:.4f}") print(f" 优化模型测试集R2: {test_r2_opt:.4f}") print(f" 提升幅度: {improvement:.2f}%") # ========================================== # 第四步:最优模型的特征重要性 # ========================================== print("\n【4】最优模型的特征重要性 TOP 10") print("-"*40) feature_importance_opt = pd.DataFrame({ '特征': feature_columns, '重要性': best_model.feature_importances_ }).sort_values('重要性', ascending=False) print(feature_importance_opt.head(10)) # ========================================== # 第五步:保存最终模型 # ========================================== print("-"*40) # 保存最优模型(覆盖原模型,使用最优参数) with open('house_price_model.pkl', 'wb') as f: pickle.dump(best_model, f) # 保存标准化器 with open('scaler.pkl', 'wb') as f: pickle.dump(scaler, f) # 保存特征列 with open('feature_columns.pkl', 'wb') as f: pickle.dump(feature_columns, f) # 保存最优参数到文本文件(便于查看) with open('best_params.txt', 'w', encoding='utf-8') as f: f.write("="*50 + "\n") f.write("最优参数配置\n") f.write("="*50 + "\n\n") f.write("最佳参数组合:\n") for param, value in grid_search.best_params_.items(): f.write(f" {param}: {value}\n") f.write(f"\n交叉验证最佳R2: {grid_search.best_score_:.4f}\n") f.write(f"测试集R2: {test_r2_opt:.4f}\n") f.write(f"\n性能提升: {improvement:.2f}%\n") f.write("\n" + "="*50 + "\n") f.write("\n特征重要性 TOP 10:\n") f.write(feature_importance_opt.head(10).to_string()) print("\n最终模型已保存!") print(f" - 模型文件: house_price_model.pkl (已更新为最优参数)") print(f" - 标准化器: scaler.pkl") print(f" - 特征列名: feature_columns.pkl") print(f" - 参数记录: best_params.txt") print("\n" + "="*60) print("全部完成!") print(f" 原始模型测试集R2: {original_test_r2:.4f}") print(f" 优化模型测试集R2: {test_r2_opt:.4f} ") print(f" 性能提升: {improvement:.2f}%") print("="*60) ``` ## Diango web界面。java也能做。 ``` python开发速度快,做出来的东西运行速度慢。集群,熔断等 java开发速度慢,做出来的东西运行速度快。集群,熔断等。高并发的能力。 ``` 1:创建虚拟环境 2:django-admin startproject web 安装一个web环境名称叫web 3: 进入到web目录中 python manager.py runserver就可以启动项目了。 4:项目的目录下建立了templates目录 /index.html文件 5:settings.py ``` TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] ``` 6:urls.py ``` def home(request): return render(request,'index.html') urlpatterns = [ path('admin/', admin.site.urls), path('',home) ] ``` 首页修改完毕了。 ### Diango如何给页面传参 在urls.py中设置数据,添加给前端。 前端采用{{}}来进行获取数据。 urls.py ``` """ URL configuration for web project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/5.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path from django.shortcuts import render def home(request): context = { 'title':'Home', 'content':'This is home page', 'msg':'西安电子科技大学' } return render(request,'index.html',context) urlpatterns = [ path('admin/', admin.site.urls), path('',home) ] ``` index.html ``` hello {{title}} {{content}} {{msg}} ``` ### 正式的Diango项目结构 ### MTV架构 M:模型层,数据结构的定义。 T:Template 模版层。HTML V: View 视图层。写的是业务逻辑。整个函数类处理流程的核心。 ## 首页大图 ### 1:Echarts。 ``` https://echarts.apache.org/examples/zh/ ``` ``` 在项目目录下建立 static/js目录 修改setting.py import os STATIC_URL = 'static/' #我的静态文件从哪里去找 STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static') ] ``` 下载echarts.min.js。从官网下载就好了。 ``` 把echar.min.js放入js目录下,同时建立demo.js。demo.js从官网直接拷贝就完事。 ``` **demo.js** ``` console.log('into2 demo.js') var chartDom = document.getElementById('main'); var myChart = echarts.init(chartDom); var option; option = { xAxis: { type: 'category', boundaryGap: false, data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [ { data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line', areaStyle: {} } ] }; option && myChart.setOption(option); console.log('into2 end') ``` **index.html** ``` {% load static %} Document
``` 图形就出现了。 ![image-20260701143606829](images/image-20260701143606829.png) ### 2:更改首页成为我自己首页 拷贝 js css img目录到static目录下,覆盖index.html就完事。 image-20260701144044097 ``` https://datav.aliyun.com/portal/school/atlas/area_selector ``` 阿里云的地图的数据。 ### 图1: 从eacharts官网随便找了一个图。把源码拷贝到chart1.js文件中。 根据house_mean这个id去找,我在html代码中要显示的地方。 ``` var chartDom = document.getElementById('house_mean'); ``` ### 把csv文件,导入到数据库中去。 ``` create database house_data CHARACTER set utf8mb4 COLLATE utf8mb4_unicode_ci; ``` ```python import pandas as pd from sqlalchemy import create_engine #连接数据库,你们将来需要换成自己的信息 user = 'root' password = 'ilovelaohan' host = '127.0.0.1' port = '3306' database = 'house_data' #创建数据库引擎 engine = create_engine('mysql+pymysql://' + user + ':' + password + '@' + host + ':' + port + '/' + database) df = pd.read_csv('D:/temp/xd/webprj/web/房源数据_清洗后.csv') #插入数据库 table_name = 'house' df.to_sql(table_name, engine, if_exists='append', index=False) print(f'插入数据 {len(df)} 条') ``` 但是表的定义的,的数据类型都是完全不对的。还是要用合适的类型。 下面这个代码跟上面的唯一区别,就是有了数据定义,decimal ```python import pandas as pd from sqlalchemy import create_engine from sqlalchemy.types import Integer, DECIMAL, VARCHAR, DateTime # 1. 准备数据库连接(请替换成你自己的信息) user = 'root' # 通常是 'root' password = 'ilovelaohan' host = 'localhost' # 数据库在本机 port = 3306 # MySQL 默认端口 database = 'house_data' # 需要提前创建好 # 创建数据库引擎 engine = create_engine(f'mysql+pymysql://{user}:{password}@{host}:{port}/{database}') # 2. 读取 CSV 文件 df = pd.read_csv('D:/temp/xd/webprj/web/房源数据_清洗后.csv') # 3. 数据清洗与预处理 # 把 CSV 中的空格替换为 NaN,否则导入的都是带空格的字符串 df.replace(' ', pd.NA, inplace=True) # 将挂牌时间从 '2026/4/16' 标准格式改为 datetime 类型 df['挂牌时间'] = pd.to_datetime(df['挂牌时间']) # 4. 将数据写入 MySQL 表 table_name = 'house' # 你的表名 # 3. 按你要求的精度定义数据类型映射 dtype_mapping = { # 精确数值类型(DECIMAL) '建筑面积': DECIMAL(8, 2), # 总位数8,小数2位 '单价': DECIMAL(10, 2), # 总位数10,小数2位 '同比去年': DECIMAL(5, 2), # 总位数5,小数2位,范围 -99.99 ~ 99.99 '环比上月': DECIMAL(5, 2), # 总位数5,小数2位 '经度': DECIMAL(11, 8), # 整数3位+小数8位 '纬度': DECIMAL(11, 8), # 整数3位+小数8位 '总价': DECIMAL(8, 2), # 总位数8,小数2位 # 整数类型 '总楼层': Integer(), '挂牌天数': Integer(), '所在楼层': VARCHAR(50), # 字符串类型 '城市': VARCHAR(30), '区域': VARCHAR(30), '所在区域': VARCHAR(30), '小区名称': VARCHAR(50), '房屋朝向': VARCHAR(50), '装修情况': VARCHAR(50), '产权性质': VARCHAR(50), '建筑年代': VARCHAR(50), '房源定位': VARCHAR(50), '面积区间': VARCHAR(50), '房屋户型': VARCHAR(200), '房源标签': VARCHAR(200), '地铁信息': VARCHAR(200), '房屋结构': VARCHAR(20), '挂牌年月': VARCHAR(20), '挂牌时长': VARCHAR(20), # 日期时间 '挂牌时间': DateTime(), } # df.to_sql(table_name, engine, if_exists='replace', index=False, chunksize=1000) df.to_sql(table_name, engine, if_exists='replace', chunksize=1000,dtype=dtype_mapping,index=True,index_label='houseid') print(f"成功将 {len(df)} 条数据导入到 {database}.{table_name}") ``` ### 活的数据的图1 ![image-20260701155756078](images/image-20260701155756078.png) #### utils.py ```python import pandas as pd from sqlalchemy import create_engine #工具类,读取我数据库的数据 def getHouseData(): #连接数据库,你们将来需要换成自己的信息 user = 'root' password = 'ilovelaohan' host = '127.0.0.1' port = '3306' database = 'house_data' #创建数据库引擎 engine = create_engine('mysql+pymysql://' + user + ':' + password + '@' + host + ':' + port + '/' + database) query = "select * from house" df = pd.read_sql(query, engine) return df ``` #### views.py ```python from .utils import getHouseData from django.shortcuts import render def home(request): df = getHouseData() xian_data = df[df['城市']=='西安'] #定义一个显示区域列表 main_dist = ['未央','雁塔','碑林','莲湖','灞桥','新城','长安','高新','曲江新区'] xian_data = xian_data[xian_data['区域'].isin(main_dist)] #计算各区域的均价 region_price = xian_data.groupby('区域')['单价'].mean().round(0) context = { "region_names": list(region_price.index), # "region_prices": list(region_price.values) #float64在js当中是不支持的 "region_prices": [float(p) for p in region_price.values], } return render(request, 'index.html', context) ``` #### urls.py ```python """ URL configuration for web project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/5.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path from django.shortcuts import render from . import views urlpatterns = [ path('admin/', admin.site.urls), path('',views.home) ] ``` #### index.html ``` ``` #### chart1.js ```js function chart1(){ var chartDom = document.getElementById('house_mean'); var myChart = echarts.init(chartDom); var option; option = { xAxis: { type: 'category', data: region_names, axisLabel:{ rotate:45, interval:0, fontsize:10 } }, yAxis: { type: 'value' }, series: [ { data: region_prices, type: 'bar', showBackground: true, backgroundStyle: { color: 'rgba(180, 180, 180, 0.2)' } } ] }; option && myChart.setOption(option); } ``` ![image-20260702092406483](images/image-20260702092406483.png) ### Views.py ``` from .utils import getHouseData from django.shortcuts import render import pickle import pandas as pd def home(request): df = getHouseData() xian_data = df[df['城市']=='西安'] #定义一个显示区域列表 main_dist = ['未央','雁塔','碑林','莲湖','灞桥','新城','长安','高新','曲江新区'] xian_data = xian_data[xian_data['区域'].isin(main_dist)] #计算各区域的均价 region_price = xian_data.groupby('区域')['单价'].mean().round(0) #挂牌数量 recent_data = xian_data[xian_data['挂牌年月'] >= '2025-06'] month_stats = recent_data.groupby('挂牌年月').size().sort_index() #各区域的统计 region_count = xian_data.groupby('区域').size() #词云图 应该用昨天机器学生生成那两个特种重要性的文件来获取 with open('house_price_model.pkl','rb') as f: model = pickle.load(f) with open('feature_columns.pkl', 'rb') as f: feaure_columns = pickle.load(f) #计算特正的重要性 feature_importance = pd.DataFrame({ '特征': feaure_columns, '重要性': model.feature_importances_ }).sort_values('重要性',ascending=False) feature_names = feature_importance['特征'].tolist() importance_values = [float(v) for v in feature_importance['重要性'].tolist()] context = { "region_names": list(region_price.index), # "region_prices": list(region_price.values) #float64在js当中是不支持的 "region_prices": [float(p) for p in region_price.values], "month_lables": list(month_stats.index), "month_counts": [float(p) for p in month_stats.values], "region_counts": [int(c) for c in region_count.values], "feature_names": feature_names, "importance_values": importance_values, } return render(request, 'index.html', context) ``` ### index.html ``` {% load static %} 智慧房源探索平台

智慧房源探索平台

西安市各区域房源数量
西安市房源数量
西安市房源数量
西安市挂牌时间房源数量
房价影响因素
``` ### Chart2.js ``` function chart2(){ var chartDom = document.getElementById('year_chart'); var myChart = echarts.init(chartDom); var option; option = { xAxis: { type: 'category', data: month_lables }, yAxis: { type: 'value' }, series: [ { data: month_counts, type: 'line', smooth: true } ] }; option && myChart.setOption(option); } ``` ### Chart3.js ``` function chart3(){ var chartDom = document.getElementById('tree_chart'); var myChart = echarts.init(chartDom); var option; //组装数据 var regionData = []; for(var i=0;i= '2025-06'] month_stats = recent_data.groupby('挂牌年月').size().sort_index() #各区域的统计 region_count = xian_data.groupby('区域').size() #词云图 应该用昨天机器学生生成那两个特种重要性的文件来获取 with open('house_price_model.pkl','rb') as f: model = pickle.load(f) with open('feature_columns.pkl', 'rb') as f: feaure_columns = pickle.load(f) #计算特正的重要性 feature_importance = pd.DataFrame({ '特征': feaure_columns, '重要性': model.feature_importances_ }).sort_values('重要性',ascending=False) feature_names = feature_importance['特征'].tolist() importance_values = [float(v) for v in feature_importance['重要性'].tolist()] context = { "region_names": list(region_price.index), # "region_prices": list(region_price.values) #float64在js当中是不支持的 "region_prices": [float(p) for p in region_price.values], "month_lables": list(month_stats.index), "month_counts": [float(p) for p in month_stats.values], "region_counts": [int(c) for c in region_count.values], "feature_names": feature_names, "importance_values": importance_values, } return render(request, 'index.html', context) def region_list(request,region_name): #获取所有房屋数据并进行筛选区域 df = getHouseData() house_data_temp = df[(df['城市']=='西安')&(df['区域']==region_name)] #加入筛选条件 select_list = request.GET.getlist("select_list") #构建筛选的下拉框 select_data={ "装修情况":house_data_temp['装修情况'].unique().tolist(), "产权性质":house_data_temp['产权性质'].unique().tolist(), "面积区间":house_data_temp['面积区间'].unique().tolist(), "房源类型":house_data_temp['房源类型'].unique().tolist(), } #应用筛选条件 fiter_cols = ['装修情况','产权性质','面积区间','房源类型'] #组装条件 for i,col in enumerate(fiter_cols): if i/',views.region_list,name='region_list'), ] ``` ### region_list.html ``` {% load static %} 西安{{ region_name }}房源信息
西安{{ region_name }}房源信息
大屏 > 西安市房源 > {{ region_name }}房源
{% for item in house_list %} {% empty %} {% endfor %}
小区名称 所在区域 户型结构 装修情况 房屋户型 房屋朝向 所在楼层 产权 房屋结构 建筑面积 单价 总价 房源类型
{{ item.小区名称 }} {{ item.所在区域}} {{ item.房屋户型 }} {{ item.装修情况 }} {{ item.房屋户型 }} {{ item.房屋朝向 }} {{ item.所在楼层 }} {{ item.产权性质 }} {{ item.房屋结构 }} {{ item.建筑面积 }} {{ item.单价 }} {{ item.总价 }} {{ item.房源类型 }}
暂无数据
{% if page_obj.has_previous %} 首页 上一页 {% endif %} 第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页 {% if page_obj.has_next %} 下一页 末页 {% endif %}
``` ## 大模型 推荐大家在你的项目里,大面积的使用AI 大模型。 ### **对话功能。** #### 远程 硅基流动 ``` pip install openai ``` ``` https://cloud.siliconflow.cn/i/cQcr5n96 ``` ```python from openai import OpenAI client = OpenAI( api_key="sk-qpniofalprutccivhqokdzuqqdlaemnzjmgwrzsvwwxjblbi", base_url="https://api.siliconflow.cn/v1" ) response = client.chat.completions.create( model="zai-org/GLM-5.2", messages=[ {"role": "system", "content": "你是一个有用的助手"}, {"role": "user", "content": "怎么办"} ], stream=True #流式输出 ) #处理流式输出 for chunk in response: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="", flush=True) ``` #### 本地 ollama一般都是量化模型。 ``` https://ollama.com/ ``` ``` https://ollama.com/download 可以下载ollama进行安装。 ``` ``` ollama run qwen3-next:80b 下载量化模型了。并且运行。 ``` 本机模型你是可以微调。 ```python from openai import OpenAI client = OpenAI( api_key="sd", base_url="http://127.0.0.1:11434/v1" ) response = client.chat.completions.create( model="qwen3:0.6b", messages=[ {"role": "system", "content": "你是一个有用的助手"}, {"role": "user", "content": "同学怎么办"} ], stream=True #流式输出 ) #处理流式输出 for chunk in response: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="", flush=True) ``` # MCP ## NodeJs ### 方案 A:下载官方安装包(推荐) 1. 访问 [Node.js 官网](https://nodejs.org/) 2. 下载 **LTS 版本**(长期支持版) 3. 运行安装程序,一路 **"Next"** 4. **重要**:安装过程中确保勾选: - `Add to PATH`(添加到环境变量) - `Automatically install the necessary tools...`(自动安装必要工具) ## MCP(模型上下文协议)** MCP(Model Context Protocol)是由 Anthropic 公司(Claude 模型的开发者)提出的一个**开放标准协议**,旨在解决 AI 模型与外部数据源、工具交互的标准化难题。 MCP 则致力于成为一个**“通用接口”**(类似于 USB 接口)。它定义了一套统一的规范,让 AI 模型无论是连接数据库、调用第三方 API,还是访问本地文件,都可以通过这套标准协议来完成交互。 MCP 的目标是让 AI 模型与外部工具、数据源的连接变得**标准化、可复用**。 简单来说,MCP就像是AI的"手脚",让AI不再局限于纯文本对话,而是能够: - **主动获取信息**:通过网络搜索、数据库查询获取实时数据 - **操作文件系统**:读写文件、创建目录、管理文档 - **调用API服务**:与第三方服务交互,如天气查询、股票数据等 - **执行代码**:运行Python、JavaScript等代码并获取结果 - **使用专业工具**:数据分析、图像处理、文本转换等 **MCP 架构简述:** - MCP Host (如 Claude Desktop, Cursor): 用户使用的客户端工具,内置了 MCP Client。 - MCP Client: 负责通过标准 MCP 协议与 MCP Server 通信。 - MCP Server: 由第三方开发者提供,实现了与特定外部资源(数据库、浏览器、本地文件等)交互的逻辑。它接收来自 MCP Client 的请求,执行操作,并将结果通过标准 MCP 协议返回。 image-20250708145914814 **MCP 的核心价值:** - 标准化开发:开发者只需按照 MCP 协议开发一次 Server,理论上就能被所有兼容 MCP 的 Host 使用,无需为不同模型或平台重复编写适配代码,显著降低开发工作量。 - 生态共享:开发出的 MCP Server 可以开放共享,减少开发者的重复劳动。例如,一个数据库查询插件,无需在 Coze 和 Dify 上分别实现,只要两者都支持 MCP,就能使用同一个 Server。 ## MCP Server 简单理解为:一个轻量级程序,它通过标准化的 MCP 协议向客户端“暴露”特定的功能或数据访问能力,让大模型能够间接调用。 常见的 MCP Server 类型: - 文件和数据访问类:允许模型操作本地文件或数据库(如 FileSystem MCP Server, MongoDB MCP Server)。 - Web 自动化类:允许模型控制浏览器执行任务(如 Puppeteer MCP Server)。 - 三方工具集成类:允许模型调用外部平台 API(如 高德地图 MCP Server)。 - 官方 Server 仓库 https://github.com/modelcontextprotocol/servers 包含官方示例、官方集成和社区开发的 Server。 ``` https://glama.ai/mcp/servers ``` ## **在 Cherry Studio 中 MCP** ### 文件系统MCP - 安装环境:打开 Cherry Studio 客户端,在“设置” -> “MCP 服务器”中,根据提示安装所需的环境。 - 添加 Server:在搜索框搜索 @modelcontextprotocol/server-filesystem,这是一个简单的文件系统访问 Server。 - 配置 Server:点击“+”号添加,系统会自动填充部分配置。我们需要补充一个 args 参数,指定允许访问的本地文件夹路径,例如 D:/Temp。 - 保存并启用:点击保存。如果服务器状态显示为绿灯,说明配置成功。 image-20250708154956024 image-20250708144429773 则可以对本地文件进行测试了。 image-20250708144445032 image-20250708155148955![image-20250708155220389](images/image-20250708155220389.png) image-20250708155148955![image-20250708155220389](images/image-20250708155220389.png)![image-20250708155238403](images/image-20250708155238403.png) image-20250708155148955![image-20250708155220389](../../udisk/PPT/LLM/Java版AI对接/images/image-20250708155220389.png)![image-20250708155238403](../../udisk/PPT/LLM/Java版AI对接/images/image-20250708155238403.png) ### Mysql Mcp 最推荐使用PowerShell脚本来安装。请以管理员身份打开一个PowerShell窗口,然后执行以下命令: powershell ``` powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` ![image-20251222165016729](images/image-20251222165016729.png) 这条命令会从官方源下载安装脚本并执行,通常会将 `uv.exe` 和 `uvx.exe` 安装到 `C:\Users\<你的用户名>\.local\bin\` 目录下。 UV安装完毕之后,手工将 uv加入到 PATH中。 image-20251222165137101 ``` uv --version ``` image-20251222165209893 #### 安装 mysql-mcp-server 在空闲磁盘上找一个目录,比如 `C:/mcp`, 打开命令行工具,输出以下命令: ``` # 安装pyton uv python install 3.13 # 初始化 uv init mysql-mcp # 进入mysql-mcp目录 cd mysql-mcp # 添加mysql-mcp-server包 uv add mysql-mcp-server ``` ![image-20250708165050238](images/image-20250708165050238.png) 至此,我们将 `mysql-mcp-server`安装至了 `C:/mcp/mysql-mcp` 下。 进入Cherry Studio, 按下图所示,点击左侧“设置”图标,接着选择“MCP服务器”, 然后添加服务器, 在下拉菜单中选择“从JSON导入”。内容如下: ```json { "mcpServers": { "mysql": { "isActive": true, "command": "npx.cmd", "args": [ "-y", "mysql-mcp-server" ], "env": { "MYSQL_HOST": "127.0.0.1", "MYSQL_PORT": "3306", "MYSQL_USER": "root", "MYSQL_PASSWORD": "ilovelaohan", "MYSQL_DATABASE": "school" }, "name": "mysql" } } } ``` image-20250708170651113 #### **SQL脚本** ```sql CREATE DATABASE /*!32312 IF NOT EXISTS*/`school` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `school`; /*Table structure for table `course` */ DROP TABLE IF EXISTS `course`; CREATE TABLE `course` ( `CourseId` varchar(3) primary key NOT NULL COMMENT '课程编号', `CourseName` varchar(20) NOT NULL COMMENT '课程名称', `TeacherId` varchar(3) DEFAULT NULL COMMENT '教师编号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='课程表'; /*Data for the table `course` */ /*Table structure for table `score` */ DROP TABLE IF EXISTS `score`; CREATE TABLE `score` ( `StudentId` VARCHAR(5) DEFAULT NULL COMMENT '学号', `CourseId` VARCHAR(3) DEFAULT NULL COMMENT '课程编号', `Score` FLOAT COMMENT '成绩', PRIMARY KEY `idx_css` (`CourseId`,`StudentId`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='成绩表'; /*Data for the table `score` */ insert into `score`(`StudentId`,`CourseId`,`Score`) values ('1001','002',38),('1001','001',38),('1001','010',38),('1002','003',85),('1002','002',92),('1002','001',47),('1002','010',70),('1003','006',83),('1003','003',85),('1003','005',75),('1003','001',37),('1003','010',90),('1003','011',26),('1004','007',95),('1004','002',92),('1005','001',28),('1006','015',90),('1006','006',51),('1006','003',74),('1006','004',75),('1007','011',90),('1007','006',89),('1007','003',87),('1007','002',92),('1008','001',99),('1008','012',90),('1008','006',37),('1009','002',87),('1009','010',87),('1009','008',97),('1010','002',92),('1011','009',30),('1012','003',45),('1013','002',42),('1013','001',21),('1013','007',60),('1013','006',47),('1013','012',39),('1000','004',21),('1002','004',60),('1004','004',47),('1008','004',39),('1013','016',49),('1000','014',80),('1004','001',88),('1000','003',76.4286),('1004','003',76.4286),('1005','003',76.4286),('1008','003',76.4286),('1011','003',76.4286),('1013','003',76.4286),('1014','003',76.4286),('1015','003',76.4286),('1016','003',76.4286),('1017','003',76.4286),('1018','003',76.4286),('1019','003',76.4286),('1020','003',76.4286),('1021','003',76.4286),('1022','003',76.4286),('1023','003',76.4286),('1001','003',76.4286); /*Table structure for table `student` */ DROP TABLE IF EXISTS `student`; CREATE TABLE `students` ( `StudentId` varchar(5) NOT NULL COMMENT '学号', `StudentName` varchar(10) NOT NULL COMMENT '学生姓名', `StudentAge` int(11) DEFAULT NULL COMMENT '学生年龄', `StudentSex` char(1) DEFAULT '' COMMENT '学生性别', PRIMARY KEY (`StudentId`), KEY `ix` (`StudentName`), KEY `idx_stname_stuage` (`StudentAge`,`StudentName`), KEY `idx_stnameage_stuage` (`StudentAge`,`StudentSex`,`StudentName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='学生表'; /*Table structure for table `teacher` */ DROP TABLE IF EXISTS `teachers`; CREATE TABLE `teachers` ( `Teacherid` varchar(3) NOT NULL COMMENT '教师编号', `TeacherName` varchar(10) NOT NULL COMMENT '教师名称', `age` int(11) DEFAULT NULL, PRIMARY KEY (`Teacherid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='教师表'; /*Data for the table `teacher` */ ``` 在对话框中要选中MCP服务器。 ![image-20250709104833212](images/image-20250709104833212.png) ![image-20250709104750319](images/image-20250709104806435.png) ![image-20250709104651734](images/image-20250709104651734.png) ### Playwright Playwright是微软开源的自动化测试工具,其核心优势在于: 跨浏览器支持:原生兼容Chromium(Chrome/Edge)、Firefox、WebKit(Safari),无需手动安装驱动。 高效稳定: 自动等待机制:操作前自动等待元素加载,减少硬编码sleep。 智能选择器:支持Shadow DOM穿透和动态元素定位,降低维护成本。 多场景覆盖:支持文件上传下载、跨域操作、移动端模拟等复杂需求。 **在CharryStudio中搜索并添加** ``` @executeautomation/playwright-mcp-server ``` **安装Playwright:** ``` pip install playwright playwright install # 自动安装浏览器驱动 ``` **部署MCP Server:** ``` 用法: npx @executeautomation/playwright-mcp-server NPM: https://www.npmjs.com/package/@executeautomation/playwright-mcp-server ``` 然后就可以用了。 ``` 打开百度首页,搜索kfc,并将搜索的结果给我截图。 ``` image-20250710090053914 对比传统工具:Playwright + MCP的颠覆性优势 ![image-20250710090234469](images/image-20250710090234469.png) ``` 打开https://news.hsw.cn/ 并获取前5条新闻的标题 ``` ![image-20250710093935318](images/image-20250710093935318.png) ## Python+MCP ```python import asyncio import json import sys import warnings import os import gc from openai import OpenAI from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client # 忽略所有警告 warnings.filterwarnings("ignore") # 解决Windows事件循环问题 if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) # 表结构信息 TABLE_SCHEMA = """ CREATE TABLE `house` ( `houseid` bigint(20) DEFAULT NULL COMMENT '房屋ID', `城市` varchar(30) DEFAULT NULL COMMENT '所在城市', `区域` varchar(30) DEFAULT NULL COMMENT '所在区域', `总价` decimal(8,2) DEFAULT NULL COMMENT '房屋总价(万元)', `单价` decimal(10,2) DEFAULT NULL COMMENT '每平米单价(元)', `建筑面积` decimal(8,2) DEFAULT NULL COMMENT '建筑面积(平米)', `小区名称` varchar(50) DEFAULT NULL COMMENT '小区名称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房屋信息表'; """ class MySQLMCP: def __init__(self): self.ai_client = OpenAI( api_key="sk-qpniofalprutccivhqokdzuqqdlaemnzjmgwrzsvwwxjblbi", base_url="https://api.siliconflow.cn/v1" ) self.model = "deepseek-ai/DeepSeek-V3" self.server_params = StdioServerParameters( command="npx.cmd", args=["-y", "mysql-mcp-server"], env={ "MYSQL_HOST": "127.0.0.1", "MYSQL_PORT": "3306", "MYSQL_USER": "root", "MYSQL_PASSWORD": "ilovelaohan", "MYSQL_DATABASE": "house_data", } ) self.session = None self.mcp_tools = [] self._stdio_context = None self._session_context = None self._read = None self._write = None async def connect(self): """连接MCP Server""" self._stdio_context = stdio_client(self.server_params) self._read, self._write = await self._stdio_context.__aenter__() self._session_context = ClientSession(self._read, self._write) self.session = await self._session_context.__aenter__() await self.session.initialize() result = await self.session.list_tools() self.mcp_tools = result.tools print(f"MCP Server连接成功") print(f"可用工具: {[tool.name for tool in self.mcp_tools]}") async def ask(self, question: str) -> str: """自然语言查询数据库""" if not self.session: await self.connect() tools = [] for tool in self.mcp_tools: tools.append({ "type": "function", "function": { "name": tool.name, "description": tool.description or f"执行{tool.name}操作", "parameters": tool.inputSchema } }) print(f"\n问题: {question}") print("AI思考中...") response = self.ai_client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": f"""MySQL助手,数据库: house_data 表结构: {TABLE_SCHEMA} 规则: 1. 使用 execute_query 执行SQL 2. 中文字段用反引号: `城市`、`总价` 3. 直接返回查询结果"""}, {"role": "user", "content": question} ], tools=tools, tool_choice="auto" ) message = response.choices[0].message if message.tool_calls: tool_results = [] for tool_call in message.tool_calls: func_name = tool_call.function.name args = json.loads(tool_call.function.arguments) print(f"调用工具: {func_name}") print(f"参数: {json.dumps(args, ensure_ascii=False)}") result = await self.session.call_tool(func_name, args) tool_result = result.content[0].text if result.content else str(result) tool_results.append({ "tool_call_id": tool_call.id, "content": tool_result }) print(f"结果: {tool_result[:200]}..." if len(tool_result) > 200 else f"结果: {tool_result}") messages = [ {"role": "system", "content": "根据查询结果回答用户问题。"}, {"role": "user", "content": question}, message ] for tr in tool_results: messages.append({ "role": "tool", "tool_call_id": tr["tool_call_id"], "content": tr["content"] }) final = self.ai_client.chat.completions.create( model=self.model, messages=messages ) return final.choices[0].message.content return message.content or "无法处理" async def close(self): """安全关闭MCP连接,解决Windows管道析构报错""" # 反向顺序关闭session -> stdio if self._session_context is not None: try: await self._session_context.__aexit__(None, None, None) except Exception: pass self._session_context = None self.session = None if self._stdio_context is not None: try: await self._stdio_context.__aexit__(None, None, None) except Exception: pass self._stdio_context = None # 清空管道引用,提前释放资源 self._read = None self._write = None # Windows 额外等待一小段时间让子进程正常退出 if sys.platform == "win32": await asyncio.sleep(0.05) # 强制垃圾回收,避免__del__延迟触发报错 gc.collect() # 使用 subprocess 隔离解决警告 import subprocess def query_sync(question: str) -> str: """同步查询 - 使用子进程隔离,彻底解决Windows警告""" script = os.path.join(os.path.dirname(__file__), "mysql_mcp_worker.py") try: result = subprocess.run( [sys.executable, script, question], capture_output=True, text=True, timeout=60 ) if result.returncode == 0 and result.stdout: data = json.loads(result.stdout) return data.get("result", "查询失败") else: return f"查询失败: {result.stderr}" except subprocess.TimeoutExpired: return "错误: 查询超时" except Exception as e: return f"错误: {str(e)}" #直接使用 asyncio.run async def query_async(question: str) -> str: """异步查询""" client = MySQLMCP() try: await client.connect() return await client.ask(question) finally: await client.close() # 主协程结束前手动回收资源 gc.collect() def query(question: str) -> str: """推荐使用:直接查询""" res = asyncio.run(query_async(question)) # 顶层再执行一次gc,兜底清理管道对象 gc.collect() return res if __name__ == "__main__": result = query("西安最大的房子在哪里?") print(f"\n回答:\n{result}") ``` ![image-20260702143517839](images/image-20260702143517839.png) ## SpringBoot+AI+MCP #### mysql-mcp-server ##### **配置调用的MCP Server** 执行以下命令安装对应的MCP Server到机器上: ``` npm install mysql-mcp-server ``` ##### **Maven依赖** ```xml 4.0.0 com.han mcpai 0.0.1-SNAPSHOT mcpai mcpai 17 UTF-8 UTF-8 3.2.5 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.ai spring-ai-openai-spring-boot-starter 1.0.0-M6 org.springframework.ai spring-ai-mcp-client-spring-boot-starter 1.0.0-M6 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 1.8 UTF-8 org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} com.han.mcpai.McpaiApplication true repackage repackage spring-milestones Spring Milestones https://repo.spring.io/milestone false ``` ##### **application.yml** ```xml spring: ai: openai: chat: options: model: "deepseek-ai/DeepSeek-V3.1-Terminus" # 需要替换为自己的api-key api-key: "sk-xestzlwwdhguluxwuwqprontqhsvukcoutqtlhyaqksfquis" base-url: "https://api.siliconflow.cn/" mcp: client: stdio: connections: mysql: # Windows操作系统使用npx.cmd,Linux和MacOS使用npx command: "npx.cmd" args: # mcp server名称 - "mysql-mcp-server" env: # 数据库ip "MYSQL_HOST": "127.0.0.1" # 数据库服务端口 "MYSQL_PORT": "3306" # 数据库用户名 "MYSQL_USER": "root" # 数据库密码 "MYSQL_PASSWORD": "ilovelaohan" # 数据库名称 "MYSQL_DATABASE": "school" filesystem: # Windows操作系统使用npx.cmd,Linux和MacOS使用npx command: "npx.cmd" args: - "-y" - "@modelcontextprotocol/server-filesystem" # 在此文件夹内进行文件操作,需根据具体需求调整目标文件夹 - "D:\\temp" ``` ##### **配置类** ```java package com.han.mcpai; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpClientConfiguration { /** * 注入ChatClient * @param chatModel * @param toolCallbackProvider 报红不用管,没有任何问题 * @return */ @Bean ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) { return ChatClient .builder(chatModel) .defaultTools(toolCallbackProvider.getToolCallbacks()) .build(); } } ``` ##### 测试类 ```java @Resource private ChatClient chatClient; @Test void test1() { System.out.println(chatClient.prompt() .user("在school数据库那个老师的年龄最大") .call() .content()); } ``` ![image-20250710151146553](images/image-20250710151146553.png) ![image-20250710151456555](images/image-20250710151456555.png) ##### 流式 ```java @ResponseBody @CrossOrigin @GetMapping(value = "/aistream") Flux generationStream(@RequestParam("input") String input) { Flux flux = this.chatClient.prompt() .user(input) .stream().chatResponse(); return flux.flatMapSequential(chatResponse -> { String content = chatResponse.getResult().getOutput().toString();//.getContent(); if (content != "" && content != null) { System.out.println(content); //AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=!, metadata={messageType=ASSISTANT}] return Flux.just(content); } return Flux.just(""); } ); } ``` ###### 前端页面 ```html 问题:


回答:
``` ![image-20250710171600109](images/image-20250710171600109.png) #### filesystem ``` npm install @modelcontextprotocol/server-filesystem ``` ##### application.yml ```xml spring: ai: openai: chat: options: model: "Qwen/Qwen3-8B" # 需要替换为自己的api-key api-key: "sk-bhoehpwdfiamfexehaosnzibverizlrpysogaetojpoqhubo" base-url: "https://api.siliconflow.cn/" mcp: client: stdio: connections: mysql: # Windows操作系统使用npx.cmd,Linux和MacOS使用npx command: "npx.cmd" args: # mcp server名称 - "mysql-mcp-server" env: # 数据库ip "MYSQL_HOST": "127.0.0.1" # 数据库服务端口 "MYSQL_PORT": "3306" # 数据库用户名 "MYSQL_USER": "root" # 数据库密码 "MYSQL_PASSWORD": "ilovelaohan" # 数据库名称 "MYSQL_DATABASE": "school" filesystem: # Windows操作系统使用npx.cmd,Linux和MacOS使用npx command: "npx.cmd" args: - "-y" - "@modelcontextprotocol/server-filesystem" # 在此文件夹内进行文件操作,需根据具体需求调整目标文件夹 - "D:\\temp" ``` ##### 控制器 ```java package com.han.mcpai.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class AiController { @Resource private ChatClient chatClient; @GetMapping("/ai") String generation(@RequestParam("input") String input) { return this.chatClient.prompt() .user(input) .call() .content(); } } ``` ![image-20250710162256090](images/image-20250710162256090.png) ![image-20250710163232454](images/image-20250710163232454.png) **maven依赖难找** 虽然现在有很多文章聊MCP的,但是大多数都是科普文,即使是实战的,也很少有java或者spring相关的。另一个就是spring ai目前还没有release版本的mcp jar包,很多LLM实现上没有完全支持mcp,导致想要找到合适的maven比较困难。 # RAG向量数据库(不要看) 逻辑流程:数据提取->embedding(向量化)->索引创建->检索->排序->LLM生成。 检索增强生成 RAG即检索增强生成,为 LLM 提供了从某些数据源检索到的信息,并基于此修正生成的答案。RAG 基本上是 Search + LLM 提示,可以通过大模型回答查询,并将搜索算法所找到的信息作为大模型的上下文。查询和检索到的上下文都会被注入到发送到 LLM 的提示语中。 ### **为什么需要RAG**: **1、减少幻觉:**依赖检索结果生成,降低模型编造信息的风险。 **2、动态知识库:**支持实时更新[知识库](https://so.csdn.net/so/search?q=知识库&spm=1001.2101.3001.7020),无需重新训练模型。 **3、领域适配:**通过定制知识库快速适配垂直场景。 ### 使用Embeding模型与向量数据库 Embeding的核心思想是将原本复杂,稀疏的输入数据(如一个词或一张图像)映射到一个连续的向量空间,其中相似的输入会被映射到空间中相近的点。通过训练神经网络或其他机器学习算法,模型可以学习到如何在这个向量空间中表示输入数据。 需要通过此模型将文件中的数据转成向量然后存储到数据库中。 Embeding模型:https://ollama.com/search?c=embedding | 模型 | 向量维度 | | :----------------- | :------- | | `all-minilm` | 384 | | `nomic-embed-text` | 768 | #### all-minilm ![image-20250818165336729](images/image-20250818165336729.png) ``` ollama pull all-minilm ``` ![image-20250818170146341](images/image-20250818170146341.png) #### nomic-embed-text ![image-20250818165410948](images/image-20250818165410948.png) ``` ollama pull nomic-embed-text ``` ![image-20250818172404993](images/image-20250818172404993.png) #### postgresql ``` CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS hstore; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE IF NOT EXISTS vector_store( id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, content text, metadata json, embedding vector(768) ); CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops); ``` ![image-20250818165935161](images/image-20250818165935161.png) #### **maven依赖** ![image-20250818171901540](images/image-20250818171901540.png) ``` org.springframework.ai spring-ai-pgvector-store-spring-boot-starter 1.0.0-M6 ``` #### yml ```yaml server: port: 8080 spring: application: name: springai datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: ilovelaohan ai: vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 384 batching-strategy: TOKEN_COUNT max-document-batch-size: 1000 ollama: base-url: http://127.0.0.1:11434 chat: model: "deepseek-r1:1.5b" options: temperature: 0.7 embedding: enabled: true model: "all-minilm" ``` #### AiConfig ```java package com.example.ollama; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @RequiredArgsConstructor public class AiConfig { final OllamaChatModel ollamaChatModel; private final static String SYSTEM_PROMPT = """ 请严格根据文档内容回答用户问题。必须满足以下条件: 1. 如果文档中未直接提及问题中的关键词,则回答“未找到相关文档”。 2. 禁止自行推断或回答与问题无关的内容。 文档内容如下: {documents} """; @Bean ChatClient chatClient(ChatMemory chatMemory){ return ChatClient.builder(ollamaChatModel) //system角色用于设置ai的行为,角色,背景等,通常可以用于设定对话的语境,让ai在指定的语境下工作 .defaultSystem(SYSTEM_PROMPT) // 联系上下文,存储历史信息,到本地内存中 // .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); }; /** * 联系上下文,存储历史信息,到本地内存中 * @return */ @Bean public ChatMemory chatMemory(){ return new InMemoryChatMemory(); } } ``` #### 文本知识库 /resources/zs.txt ``` 饿了怎么办,那么就使劲吃好吃的 java怎么运行,直接javac就可以编译 java就可以运行 如果长高,使劲吃就可以长高 ``` ##### 测试 ```java package com.example.ollama; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.stream.Collectors; import org.springframework.ai.document.Document; @SpringBootTest class OllamaApplicationTests { @Resource private ChatClient chatClient; @Autowired private VectorStore store; @Autowired private OllamaChatModel ollamachatModel; @Autowired private ChatMemory chatMemory; @Test void importtxt() throws Exception { StringBuffer text =new StringBuffer(); ClassLoader classLoader = getClass().getClassLoader(); InputStream inputStream = classLoader.getResourceAsStream("nocode.txt"); try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { text.append(line); } } store.add( Arrays.stream(text.toString().split("\n")) .map(Document::new) .collect(Collectors.toList()) // 返回 ArrayList(可修改) ); } private final static String SYSTEM_PROMPT = """ 请严格根据文档内容回答用户问题。必须满足以下条件: 1. 如果文档中未直接提及问题中的关键词,则回答“未找到相关文档”。 2. 禁止自行推断或回答与问题无关的内容。 文档内容如下: {documents} """; @Test void readTxt(){ ChatClient chatClient1 = ChatClient.builder(ollamachatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(store)) .build(); String returnStr = chatClient1.prompt() .user("如何学习好") .call() .content(); System.out.println(returnStr); } } ``` #### chatpdf ``` org.springframework.ai spring-ai-pdf-document-reader 1.0.0-M6 ``` ```java @Test void readTxt(){ ChatClient chatClient1 = ChatClient.builder(ollamachatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(store)) .build(); String returnStr = chatClient1.prompt() .user("如何使用SqlSession") .call() .content(); System.out.println(returnStr); } @Test void importpdf() throws Exception { PagePdfDocumentReader pagePdfDocumentReader = new PagePdfDocumentReader("classpath:/mybatis.pdf", PdfDocumentReaderConfig.builder() .withPageTopMargin(0) .withPageExtractedTextFormatter(ExtractedTextFormatter.builder() .withNumberOfTopTextLinesToDelete(0) .build()) .withPagesPerDocument(1) .build()); store.write(pagePdfDocumentReader.read()); } ``` ![image-20250819121233901](images/image-20250819121233901.png) #### markdown ```xml org.springframework.ai spring-ai-markdown-document-reader 1.0.0-M6 ``` ```java @Test void importmd() throws Exception { MarkdownDocumentReader mdr = new MarkdownDocumentReader("classpath:/mysql.md"); store.write(mdr.read()); } @Test void readTxt(){ ChatClient chatClient1 = ChatClient.builder(ollamachatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(store)) .build(); String returnStr = chatClient1.prompt() .user("Mybatis创建 SqlSession") .call() .content(); System.out.println(returnStr); } ``` #### 测试 ```java @Test void importpdf() throws Exception { PagePdfDocumentReader pagePdfDocumentReader = new PagePdfDocumentReader("classpath:/4gateway.pdf", PdfDocumentReaderConfig.builder() .withPageTopMargin(0) .withPageExtractedTextFormatter(ExtractedTextFormatter.builder() .withNumberOfTopTextLinesToDelete(0) .build()) .withPagesPerDocument(1) .build()); store.write(pagePdfDocumentReader.read()); } ``` ```java private final static String SYSTEM_PROMPT = """ 请严格根据文档内容回答用户问题。必须满足以下条件: 1. 如果文档中未直接提及问题中的关键词,则回答“未找到相关文档”。 2. 禁止自行推断或回答与问题无关的内容。 文档内容如下: {documents} """; @Test void readTxt(){ ChatClient chatClient1 = ChatClient.builder(ollamachatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(store)) .build(); String returnStr = chatClient1.prompt() .user("如何启动nacos") .call() .content(); System.out.println(returnStr); } ``` ![image-20250819153659527](images/image-20250819153659527.png) ### Boot演示 #### yml ``` server: port: 8080 spring: application: name: springai datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: ilovelaohan ai: vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 384 batching-strategy: TOKEN_COUNT max-document-batch-size: 1000 ollama: base-url: http://127.0.0.1:11434 chat: model: "deepseek-r1:1.5b" options: temperature: 0.7 embedding: enabled: true model: "all-minilm" ``` #### AiConfig ```java package com.example.ollama; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @RequiredArgsConstructor public class AiConfig { final OllamaChatModel ollamaChatModel; final private VectorStore store; private final static String SYSTEM_PROMPT = """ 请严格根据文档内容回答用户问题。必须满足以下条件: 1. 如果文档中未直接提及问题中的关键词,则回答“未找到相关文档”。 2. 禁止自行推断或回答与问题无关的内容。 文档内容如下: {documents} """; @Bean ChatClient chatClient(ChatMemory chatMemory){ return ChatClient.builder(ollamaChatModel) //system角色用于设置ai的行为,角色,背景等,通常可以用于设定对话的语境,让ai在指定的语境下工作 .defaultSystem(SYSTEM_PROMPT) // 联系上下文,存储历史信息,到本地内存中 .defaultAdvisors(new QuestionAnswerAdvisor(store),new MessageChatMemoryAdvisor(chatMemory)) .build(); }; /** * 联系上下文,存储历史信息,到本地内存中 * @return */ @Bean public ChatMemory chatMemory(){ return new InMemoryChatMemory(); } } ``` #### 控制器 ```java package com.example.ollama.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import org.springframework.ai.chat.model.ChatResponse; import java.util.Collections; import java.util.List; @RestController public class RagController { @Autowired private ChatClient chatClient; @ResponseBody @GetMapping(value = "/sayFlux") @CrossOrigin public Flux chat(@RequestParam("message") String message) { return chatClient.prompt() .user(message) .stream() // 返回 StreamResponseSpec .content()// 获取 Flux .doOnNext(text -> System.out.println("Raw Response: " + text)); } } ``` #### 前端页面 ```html 问题:


回答:
``` ![image-20250819164157439](images/image-20250819164157439.png) ![image-20250819164545018](images/image-20250819164545018.png) ![image-20250819170227068](images/image-20250819170227068.png) ### 结构化数据 #### 问题: **为什么查不到“蚜虫”?** 1. **文本长度和密度不匹配**: - 查询**:“蚜虫” (非常短,只有1个词/2个字) - **库中的文本**:很长的一段,包含了“病虫名称”、“病虫危害”、“寄主树种”等多个字段的大量信息。 - **后果**:短的查询语句与长的文档段落之间的语义相似度计算可能非常低。数据库更可能匹配到那些也包含大量其他信息的文本,而“蚜虫”这个关键信息在长文本中的“权重”被稀释了。 2. **关键词不匹配**: - 查询是“蚜虫”,但库中对应的字段是 `病虫名称:蚜虫`。 - 计算相似度时,`病虫名称:美国白蛾` 和 `病虫名称:蚜虫` 因为有相同的“病虫名称:”前缀,它们之间的相似度可能很高。但 `蚜虫` 和 `美国白蛾` 这两个核心实体的相似度其实很低。 - 如果您的查询没有包含“病虫名称:”这个前缀,直接查询“蚜虫”,它与 `病虫名称:蚜虫` 的相似度可能反而不如与其他段落中某些描述(例如其他段落中出现了“害虫”、“昆虫”等更通用的词)的相似度高。 ``` 病虫名称:蚜虫 病虫危害:叶片失绿发黄、卷曲变形,分泌蜜露引发煤污病,严重时苗木生长停滞、新梢枯萎 寄主树种:松树,杨树,柳树,桃树,樱花 传播途径:成虫迁飞传播、苗木调运携带、风力辅助传播 树种症状描述:叶片背面 / 嫩梢聚集虫体,叶片失绿发黄、卷曲变形,分泌蜜露引发煤污病 病虫防治方法:生物防治 (投放瓢虫 / 蚜茧蜂),化学防治 (喷施吡虫啉 / 啶虫脒),物理防治 (黄板诱杀成虫) 防治药物:吡虫啉,啶虫脒 防治安全距离:5 米 --- 病虫名称:红蜘蛛 病虫危害:叶片正面出现白色小点,逐渐扩大为灰白色斑块,背面可见红色虫体及白色蛛网,严重时叶片干枯脱落、树木光合作用受阻 寄主树种:柏树,杉树,月季,苹果树,梨树 传播途径:爬行扩散、风力传播、苗木携带,高温干旱环境加速传播 树种症状描述:叶片正面出现白色小点,逐渐扩大为灰白色斑块,背面可见红色虫体及白色蛛网 病虫防治方法:生物防治(投放捕食螨/草蛉),化学防治(喷施阿维菌素/螺螨酯),矿物油防治(喷施99%矿物油乳油) 防治药物:阿维菌素,螺螨酯 防治安全距离:3 米 --- 病虫名称:松材线虫 病虫危害:针叶先变为黄绿色,后迅速变为黄褐色至红褐色,整株针叶枯萎但不脱落,树干有天牛蛀孔及虫粪,木质部变褐松软 寄主树种:马尾松,黑松,赤松,落叶松 传播途径:主要通过松褐天牛传播,苗木调运、木材运输为远距离传播途径,天牛取食时将线虫注入树体 树种症状描述:针叶先变为黄绿色,后迅速变为黄褐色至红褐色,整株针叶枯萎但不脱落,树干有天牛蛀孔及虫粪 病虫防治方法:化学防治(树干注射吡虫啉/阿维菌素),物理防治(砍伐病死树并烧毁),生物防治(释放天牛天敌管氏肿腿蜂) 防治药物:吡虫啉,阿维菌素 防治安全距离:10 米 --- 病虫名称:美国白蛾 病虫危害:幼虫群集取食叶片,形成网幕,初期网幕小,后期扩大至整个枝条,叶片被啃食殆尽仅留叶脉,严重时树木秋季二次发芽 寄主树种:杨树,柳树,榆树,法国梧桐,苹果,梨 传播途径:成虫迁飞传播(单次可飞2-3公里),苗木调运、货物运输携带卵块为远距离传播途径 树种症状描述:幼虫群集取食叶片,形成网幕,叶片被啃食殆尽仅留叶脉 病虫防治方法:化学防治(喷施氯氰菊酯/甲维盐),生物防治(释放周氏啮小蜂),物理防治(人工摘除网幕/诱杀成虫) 防治药物:氯氰菊酯,甲维盐 防治安全距离:8 米 --- 病虫名称:松褐天牛 病虫危害:幼虫蛀食树干木质部,形成不规则蛀道,排出红褐色虫粪及木屑,堆积在蛀孔外,树干表皮出现流脂现象,严重时树干中空、易风折 寄主树种:松树,柏树,杉树,栎树 传播途径:成虫爬行/短距离飞行传播,苗木调运、木材运输携带幼虫为远距离传播途径 树种症状描述:树干有蛀孔及红褐色虫粪,木质部形成不规则蛀道,表皮流脂 病虫防治方法:化学防治(树干注射敌敌畏/毒死蜱),物理防治(人工钩杀幼虫/蛀孔塞药棉),生物防治(释放管氏肿腿蜂/花绒寄甲) 防治药物:敌敌畏,毒死蜱 防治安全距离:6 米 --- 病虫名称:杨扇舟蛾 病虫危害:幼虫取食叶片,1-2龄幼虫啃食叶肉留表皮,3龄后蚕食整个叶片,严重时将叶片吃光,仅留叶柄,影响树木生长及光合作用 寄主树种:杨树,柳树,白桦 传播途径:成虫迁飞传播,风力辅助扩散,卵块产于叶片背面 树种症状描述:叶片出现缺刻、孔洞,3龄后叶片被蚕食至仅留叶柄 病虫防治方法:化学防治(喷施高效氯氟氰菊酯/甲维盐),生物防治(喷施苏云金杆菌/释放赤眼蜂),物理防治(人工摘除卵块) 防治药物:高效氯氟氰菊酯,甲维盐 防治安全距离:4 米 --- 病虫名称:柳蓝叶甲 病虫危害:成虫、幼虫均取食叶片,造成叶片缺刻、孔洞,严重时叶片被啃食成网状,仅留叶脉,影响树木景观及生长 寄主树种:柳树,杨树,桦树 传播途径:成虫爬行/短距离飞行传播,卵产于叶片背面,幼虫群集危害 树种症状描述:叶片出现缺刻、孔洞,严重时呈网状 病虫防治方法:化学防治(喷施吡虫啉/氯氰菊酯),物理防治(人工震落捕杀成虫),生物防治(保护瓢虫/草蛉等天敌) 防治药物:吡虫啉,氯氰菊酯 防治安全距离:3 米 --- 病虫名称:国槐尺蠖 病虫危害:幼虫取食叶片,低龄幼虫啃食叶肉留表皮,高龄幼虫蚕食整个叶片,严重时将叶片吃光,仅留叶柄,造成树木二次发芽 寄主树种:国槐,龙爪槐,刺槐 传播途径:成虫迁飞传播,幼虫吐丝下垂扩散,卵产于叶片边缘或嫩梢 树种症状描述:叶片出现缺刻,高龄幼虫将叶片吃光仅留叶柄 病虫防治方法:化学防治(喷施甲维盐/氯氰菊酯),生物防治(喷施核型多角体病毒/释放赤眼蜂),物理防治(人工摘除幼虫) 防治药物:甲维盐,氯氰菊酯 防治安全距离:5 米 --- 病虫名称:柏小爪螨 病虫危害:叶片正面出现黄白色小点,逐渐扩大为黄褐色斑块,严重时整个树冠呈黄褐色,叶片失绿脱落,影响树木观赏价值 寄主树种:侧柏,圆柏,龙柏,桧柏 传播途径:爬行扩散、风力传播,高温干旱环境加速繁殖 树种症状描述:叶片正面出现黄白色小点,逐渐扩大为黄褐色斑块 病虫防治方法:化学防治(喷施阿维菌素/螺螨酯),生物防治(投放捕食螨/草蛉),矿物油防治(喷施矿物油乳油) 防治药物:阿维菌素,螺螨酯 防治安全距离:3 米 --- 病虫名称:落叶松毛虫 病虫危害:幼虫取食针叶,低龄幼虫啃食针叶边缘,高龄幼虫蚕食整个针叶,严重时将树冠针叶吃光,呈“火烧状”,影响树木生长及结实 寄主树种:落叶松,红松,云杉 传播途径:成虫迁飞传播,幼虫爬行扩散,卵块产于针叶背面 树种症状描述:针叶被啃食,严重时树冠呈“火烧状” 病虫防治方法:化学防治(喷施氯氰菊酯/甲维盐),生物防治(喷施苏云金杆菌/释放赤眼蜂),物理防治(人工摘除卵块) 防治药物:氯氰菊酯,甲维盐 防治安全距离:7 米 --- 病虫名称:桃小食心虫 病虫危害:幼虫蛀入果实,在果内取食,形成“豆沙馅”状虫道,排出粪便堆积在果内,果实表面出现凹陷,严重时果实腐烂脱落 寄主树种:苹果,梨,桃,山楂,枣 传播途径:成虫飞行传播,幼虫随果实运输远距离传播,卵产于果实萼洼处 树种症状描述:果实表面有凹陷,果内有“豆沙馅”状虫道及粪便 病虫防治方法:化学防治(喷施氯氟氰菊酯/甲维盐),物理防治(果实套袋/糖醋液诱杀),生物防治(释放赤眼蜂/茧蜂) 防治药物:氯氟氰菊酯,甲维盐 防治安全距离:5 米 --- 病虫名称:梨小食心虫 病虫危害:幼虫蛀入新梢后,新梢枯萎下垂;蛀入果实后,在果内取食,形成虫道,粪便排出果外,果实易腐烂脱落 寄主树种:梨,桃,苹果,李,杏 传播途径:成虫迁飞传播,幼虫随果实/苗木运输传播,前期危害新梢,后期危害果实 树种症状描述:新梢枯萎下垂,果实有蛀孔及粪便排出 病虫防治方法:化学防治(喷施溴氰菊酯/甲维盐),物理防治(性信息素诱杀/人工剪除病梢),生物防治(释放赤眼蜂) 防治药物:溴氰菊酯,甲维盐 防治安全距离:4 米 --- 病虫名称:苹果红蜘蛛 病虫危害:成虫、若螨取食叶片,叶片出现灰白色小点,逐渐扩大为苍白斑块,严重时叶片枯黄脱落,影响果实发育及花芽分化 寄主树种:苹果,梨,桃,山楂 传播途径:爬行扩散、风力传播,高温干旱环境加速繁殖,卵产于枝条芽鳞间 树种症状描述:叶片出现灰白色小点,逐渐扩大为苍白斑块 病虫防治方法:化学防治(喷施阿维菌素/螺螨酯),生物防治(投放捕食螨/草蛉),矿物油防治(喷施矿物油乳油) 防治药物:阿维菌素,螺螨酯 防治安全距离:3 米 --- 病虫名称:桃蚜 病虫危害:成虫、若蚜群集在新梢、叶片背面取食,叶片卷曲变形,新梢生长受阻,分泌蜜露引发煤污病,影响光合作用 寄主树种:桃,李,杏,樱桃,十字花科蔬菜 传播途径:成虫迁飞传播(春季迁回桃树,秋季迁飞越冬),苗木携带传播 树种症状描述:叶片卷曲变形,新梢生长受阻,分泌蜜露引发煤污病 病虫防治方法:化学防治(喷施吡虫啉/啶虫脒),生物防治(投放瓢虫/蚜茧蜂),物理防治(黄板诱杀) 防治药物:吡虫啉,啶虫脒 防治安全距离:5 米 --- 病虫名称:柑橘红蜘蛛 病虫危害:成虫、若螨取食叶片、果实汁液,叶片出现灰白色小点,严重时叶片苍白脱落;果实表面出现褐色斑点,影响外观及品质 寄主树种:柑橘,柠檬,柚子,橙 传播途径:爬行扩散、风力传播,高温干旱环境(25 - 30℃)加速繁殖,卵产于叶片背面/果实表面 树种症状描述:叶片出现灰白色小点,果实表面有褐色斑点 病虫防治方法:化学防治(喷施阿维菌素/螺螨酯),生物防治(投放捕食螨/钝绥螨),矿物油防治(喷施99%矿物油) 防治药物:阿维菌素,螺螨酯 防治安全距离:3 米 --- 病虫名称:柑橘潜叶蛾 病虫危害:幼虫潜入叶片表皮下取食叶肉,形成弯曲的虫道,虫道内有黑色粪便,叶片卷曲、硬化脱落,影响新梢生长及光合作用 寄主树种:柑橘,柠檬,柚子 传播途径:成虫夜间飞行传播,卵产于嫩梢叶片背面,幼虫潜入叶肉危害 树种症状描述:叶片表皮下有弯曲虫道,内有黑色粪便,叶片卷曲 病虫防治方法:化学防治(喷施甲维盐/氯氟氰菊酯),生物防治(释放赤眼蜂/茧蜂),物理防治(人工摘除被害叶片) 防治药物:甲维盐,氯氟氰菊酯 防治安全距离:4 米 --- 病虫名称:荔枝蒂蛀虫 病虫危害:幼虫蛀入果实后,在果内取食,粪便堆积在果蒂处,果实成熟时出现“蛀果”,严重时果实腐烂,失去商品价值 寄主树种:荔枝,龙眼 传播途径:成虫飞行传播,卵产于果实蒂部,幼虫蛀入果实危害 树种症状描述:果实蒂部有蛀孔,果内有粪便,成熟时腐烂 病虫防治方法:化学防治(喷施氯氟氰菊酯/甲维盐),物理防治(性信息素诱杀/果实套袋),生物防治(释放赤眼蜂) 防治药物:氯氟氰菊酯,甲维盐 防治安全距离:5 米 --- 病虫名称:梨木虱 病虫危害:成虫、若虫刺吸叶片、嫩梢汁液,叶片失绿发黄、卷曲,若虫分泌的蜜露引发煤污病,严重时叶片脱落、果实变小 寄主树种:梨 传播途径:成虫飞行传播,若虫分泌蜡质物覆盖身体,抗药性强 树种症状描述:叶片失绿发黄、卷曲,有煤污病 病虫防治方法:化学防治(喷施吡虫啉/阿维菌素),物理防治(人工清理蜡质物),生物防治(保护瓢虫/草蛉等天敌) 防治药物:吡虫啉,阿维菌素 防治安全距离:3 米 --- 病虫名称:葡萄透翅蛾 病虫危害:幼虫蛀入新梢后,新梢枯萎下垂,蛀孔外有褐色虫粪,严重时新梢折断,影响葡萄结果枝生长 寄主树种:葡萄 传播途径:成虫飞行传播,卵产于新梢叶片基部,幼虫蛀入新梢危害 树种症状描述:新梢枯萎下垂,蛀孔外有褐色虫粪 病虫防治方法:化学防治(喷施氯氰菊酯/甲维盐),物理防治(人工剪除病梢/黑光灯诱杀),生物防治(释放茧蜂) 防治药物:氯氰菊酯,甲维盐 防治安全距离:4 米 --- 病虫名称:枣尺蠖 病虫危害:幼虫取食叶片,低龄幼虫啃食叶肉留表皮,高龄幼虫蚕食整个叶片,严重时将叶片吃光,影响枣树开花结果 寄主树种:枣,苹果,梨,桃 传播途径:成虫爬行(雌蛾无翅)/飞行(雄蛾有翅)传播,卵产于树干裂缝处 树种症状描述:叶片出现缺刻,高龄幼虫将叶片吃光 病虫防治方法:化学防治(喷施氯氰菊酯/甲维盐),物理防治(树干绑膜/人工捕捉),生物防治(喷施苏云金杆菌) 防治药物:氯氰菊酯,甲维盐 防治安全距离:5 米 --- 病虫名称:东亚飞蝗 病虫危害:成虫、若虫取食作物叶片,造成缺刻、孔洞,严重时将作物叶片吃光,仅留茎秆,导致大面积减产甚至绝收 寄主树种:小麦,玉米,水稻,高粱,芦苇 传播途径:群居型成虫远距离迁飞(可飞数百公里),卵块随土壤/作物运输传播 树种症状描述:叶片出现缺刻、孔洞,严重时仅留茎秆 病虫防治方法:化学防治(喷施氯氰菊酯/马拉硫磷),生物防治(喷施蝗虫微孢子虫/释放粉红椋鸟),物理防治(黑光灯诱杀) 防治药物:氯氰菊酯,马拉硫磷 防治安全距离:10 米 --- 病虫名称:玉米螟 病虫危害:幼虫蛀入玉米茎秆,造成折秆;蛀入果穗,啃食籽粒,严重时玉米减产10% - 30% 寄主树种:玉米,高粱,谷子,棉花 传播途径:成虫飞行传播,幼虫随秸秆/玉米穗运输传播,卵产于玉米叶片背面 树种症状描述:茎秆折断,果穗籽粒被啃食 病虫防治方法:化学防治(喷施氯氟氰菊酯/甲维盐),物理防治(黑光灯诱杀/投放毒土),生物防治(释放赤眼蜂/喷施苏云金杆菌) 防治药物:氯氟氰菊酯,甲维盐 防治安全距离:6 米 --- 病虫名称:棉铃虫 病虫危害:幼虫蛀入棉花蕾、铃,造成落蕾落铃;蛀入番茄果实,形成蛀孔,果实腐烂,严重时产量损失30%以上 寄主树种:棉花,玉米,番茄,辣椒,花生 传播途径:成虫迁飞传播(单次可飞50 - 100公里),幼虫随果实/作物运输传播 树种症状描述:棉花落蕾落铃,番茄果实有蛀孔且腐烂 病虫防治方法:化学防治(喷施氯氟氰菊酯/甲维盐),物理防治(糖醋液诱杀/性信息素诱杀),生物防治(释放赤眼蜂/喷施核型多角体病毒) 防治药物:氯氟氰菊酯,甲维盐 防治安全距离:8 米 --- 病虫名称:麦蚜 病虫危害:成虫、若蚜群集在小麦叶片、穗部取食,叶片失绿发黄,穗部灌浆受阻,严重时小麦减产10% - 20% 寄主树种:小麦,大麦,燕麦,玉米 传播途径:有翅蚜迁飞传播(春季迁回麦田,夏季迁飞扩散),风力辅助传播 树种症状描述:叶片失绿发黄,穗部灌浆受阻 病虫防治方法:化学防治(喷施吡虫啉/啶虫脒),生物防治(投放瓢虫/蚜茧蜂),物理防治(黄板诱杀) 防治药物:吡虫啉,啶虫脒 防治安全距离:5 米 --- 病虫名称:稻飞虱 病虫危害:成虫、若虫刺吸水稻汁液,造成稻丛基部变黑、腐烂,植株倒伏,严重时形成“虱烧”,水稻成片枯死 寄主树种:水稻 传播途径:成虫迁飞传播(远距离迁飞),若虫爬行扩散,喜欢群集在稻丛基部 树种症状描述:稻丛基部变黑、腐烂,植株倒伏,形成“虱烧” 病虫防治方法:化学防治(喷施吡蚜酮/噻虫嗪),生物防治(保护青蛙/蜘蛛等天敌),物理防治(黑光灯诱杀) 防治药物:吡蚜酮,噻虫嗪 防治安全距离:4 米 ``` #### 优化数据预处理 **推荐方案:拆分字段,分别嵌入** - 不要将一整条记录存为一个向量。应该将每个字段拆分开,分别生成向量并存储。 - **例如**: - Vector 1: `病虫名称:美国白蛾` - Vector 2: `病虫危害:幼虫群集取食叶片,形成网幕...` - Vector 3: `寄主树种:杨树,柳树,榆树,法国梧桐,苹果,梨` - ...以此类推 - **优点**:当用户查询“蚜虫”时,它能够精确地匹配到 `病虫名称:蚜虫` 这个简短的、高相关度的向量,检索成功率大大提升。当用户查询“杨树”时,又能匹配到 `寄主树种:...杨树...` 的向量。 - **存储时,将文本片段和它的元数据一起存入**。 - **检索时,向量库会返回匹配的文本片段 \*及其元数据\***。 ``` @Test void importtxtbch() throws Exception { StringBuffer text = new StringBuffer(); ClassLoader classLoader = getClass().getClassLoader(); InputStream inputStream = classLoader.getResourceAsStream("病虫害知识库.txt"); try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { text.append(line).append("\n"); } } List documentsToStore = new ArrayList<>(); String[] originalLines = text.toString().split("---"); for (String originalLine : originalLines) { if (originalLine.trim().isEmpty()) continue; Map fieldMap = new HashMap<>(); // 按一个或多个空格分割整行字符串 String[] parts = originalLine.split("\\\n"); for (int i = 0; i < parts.length; i++) { String part = parts[i]; if(part.indexOf(":")==-1) continue; String[] split = part.split(":"); String name = ""; String harm = ""; String symptom = ""; String method = ""; String way = ""; String desc = ""; String medic = ""; if(split[0].trim().equals("病虫名称")){ name= split[1].trim(); } if(split[0].trim().equals("病虫危害")){ harm= split[1].trim(); } if(split[0].trim().equals("树种症状描述")){ symptom= split[1].trim(); } if(split[0].trim().equals("病虫防治方法")){ method= split[1].trim(); } if(split[0].trim().equals("传播途径")){ way= split[1].trim(); } if(split[0].trim().equals("防治药物")){ medic= split[1].trim(); } // System.out.println(harm+symptom+method+way+desc+medic); // System.out.println(harm); // // // 创建元数据,关联回原始行,便于最终回答时获取完整上下文 Map metadata = new HashMap<>(); metadata.put("original_line", text); metadata.put("pest_name", harm); // // 存入关键字段 if (name != null && !name.isEmpty()) { documentsToStore.add(new Document("病虫名称:" + name, metadata)); } if (harm != null && !harm.isEmpty()) { documentsToStore.add(new Document("病虫危害:" + harm, metadata)); } if (symptom != null && !symptom.isEmpty()) { documentsToStore.add(new Document("树种症状描述:" + symptom, metadata)); } if (method != null && !method.isEmpty()) { documentsToStore.add(new Document("病虫防治方法:" + method, metadata)); } if (way != null && !way.isEmpty()) { documentsToStore.add(new Document("传播途径:" + way, metadata)); } if (desc != null && !desc.isEmpty()) { documentsToStore.add(new Document("树种症状描述:" + desc, metadata)); } if (medic != null && !medic.isEmpty()) { documentsToStore.add(new Document("防治药物:" + medic, metadata)); } } } store.add(documentsToStore); } ``` #### 检索 ``` private final static String SYSTEM_PROMPT = """ 你是一个专业的林业病虫害查询助手。请严格根据提供的背景资料回答问题。 # 背景资料: {documents} # 回答要求: 1. **精准基于资料**:你的回答必须完全基于上述背景资料。如果资料中没有提到相关问题,请直接说“根据现有资料,未查询到相关信息”。 2. **综合与总结**:仔细阅读所有资料,如果有多条资料相关,就把多条资料都显示出来。 3. **结构化输出(非常重要)**:如果查询到结果,请按以下格式清晰回答: - **可能相关的病虫害名称**: [此处列出资料中提到的病虫名称,如:杨扇舟蛾、国槐尺蠖] - **主要症状描述**: [此处总结资料中对应的症状描述] - **防治方法建议**: [此处总结资料中提到的防治方法] - **来源依据**:上述信息来源于病虫害数据库中对“【病虫名称】”的记录。 # 禁止事项: - 禁止编造资料中不存在的信息。 - 禁止使用资料外的知识进行推断。 """; @Test void search(){ // 测试向量检索本身是否准确 List results = store.similaritySearch("蚜虫"); System.out.println("检索到 " + results.size() + " 条结果:"); for (Document doc : results) { System.out.println("-> " + doc.toString()); System.out.println("---"); } } @Test void readTxt(){ ChatClient chatClient1 = ChatClient.builder(ollamachatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), new QuestionAnswerAdvisor(store)) .build(); String returnStr = chatClient1.prompt() .user("松材线虫") .call() .content(); System.out.println(returnStr); } ``` ## ollama的Embedding 模型 ### 使用Embeding模型与向量数据库 Embeding的核心思想是将原本复杂,稀疏的输入数据(如一个词或一张图像)映射到一个连续的向量空间,其中相似的输入会被映射到空间中相近的点。通过训练神经网络或其他机器学习算法,模型可以学习到如何在这个向量空间中表示输入数据。 需要通过此模型将文件中的数据转成向量然后存储到数据库中。 Embeding模型:https://ollama.com/search?c=embedding | 模型 | 向量维度 | | :----------------- | :------- | | `all-minilm` | 384 | | `nomic-embed-text` | 768 | #### all-minilm ![image-20250818165336729](../../udisk/PPT/LLM/Java版AI对接/images/image-20250818165336729.png) ``` ollama pull all-minilm ``` ![image-20250818170146341](../../udisk/PPT/LLM/Java版AI对接/images/image-20250818170146341.png) #### nomic-embed-text ![image-20250818165410948](../../udisk/PPT/LLM/Java版AI对接/images/image-20250818165410948.png) ``` ollama pull nomic-embed-text ``` ![image-20250818172404993](../../udisk/PPT/LLM/Java版AI对接/images/image-20250818172404993.png) #### bge-m3:latest RAGFlow 默认模型 bge-m3:latest, #### bge-large-zh 号称最好用的中文 Embedding 模型(我试用下来跟 bge-m3:latest 没啥区别) ``` ollama pull quentinz/bge-large-zh-v1.5 ``` image-20251224112430142 ## 模型类型说明 #### A)Chat模型 (AI的“嘴皮子”) - 干啥用的:负责和你聊天对话,生成人类能看懂的回答 - 经典模型:Llama3、Qwen、GPT、DeepSeek - 举个栗子 : 当你问“如何安装Ollama”,Chat模型就会像话痨同事一样,把安装步骤一条条讲给你听 - 选型秘诀: - 需要“话多且准”:选参数大的模型(比如70B) - 电脑配置低:用7B小模型(虽然可能胡说八道) #### B)Embedding模型 (文字“翻译官”) - 干啥用的:把文字变成一串数字(向量),方便计算机理解语义 - 经典模型:BGE、text2vec、OpenAI的text-embedding-3-small - 灵魂比喻: 相当于给每句话发一个身份证号,说“春天”和“花开”的号码接近,“冰箱”和“宇宙”的号码离得远 - 选型秘诀: - 中文优先:选bge-large-zh这类中文专用模型 - 速度优先:用small版(精度会下降) #### C)Rerank模型 (结果“质检员”) - 干啥用的:对搜索出的100条结果重新打分,把最相关的排到最前面 - 经典模型:bge-reranker、cohere-rerank - 举个栗子 : 你搜“苹果”,初步结果可能包含水果、手机、电影公司。Rerank模型会说:“根据上下文,用户其实想查iPhone”,然后把手机相关结果置顶 - 选型秘诀: - 精准度 vs 速度:大模型准但慢,小模型快但糙 - 可选项少:目前主流就2-3种 #### D)Image2Text模型 —— 图片的“解码器” - 干啥用的:把图片里的文字抠出来(比如扫描版PDF/照片里的文字) - 经典模型:PaddleOCR、Donut、EasyOCR - 灵魂场景: 你上传一张表情包截图,AI能读出上面的字:“一键三连的都是帅哥美女!” - 选型秘诀: - 中文场景:优先选PaddleOCR(国产之光) - 复杂排版:用Donut(但吃显卡) # RagFlow ### 1:克隆仓库并配置环境 ``` sudo yum install git ``` ``` git clone https://github.com/infiniflow/ragflow.git 或者国内 git clone https://gitee.com/lllgggccc1024/ragflow.git cd ragflow/docker ``` ``` # 如果你访问不了可以 git clone https://gitee.com/infiniflow/ragflow.git git clone https://gitclone.com/github.com/infiniflow/ragflow git clone https://kkgithub.com/github.com/infiniflow/ragflow.git git clone https://bgithub.xyz/github.com/infiniflow/ragflow.git 镜像站是社区公益服务,域名可能变动,如果某个连接不通,换一个试试。同时,不要通过镜像站克隆私有仓库,以免泄露凭据 ``` ![image-20250901151108643](images/image-20250901151108643.png) - 也可以修改`.env`文件,选择国内镜像源(阿里云失效改用华为云镜像源)加速下载: ``` RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:nightly ``` 也可以手动下载,然后运行。这个 ### 2: 启动服务 ``` 如果你是使用CPU执行嵌入 docker compose -f docker-compose.yml up -d 如果你是使用GPU加速执行 docker compose -f docker-compose-gpu.yml up -d 如果长时间拉取不下来。更彻底地清理未使用的镜像层(谨慎使用) 然后重新拉取。 docker system prune -f ``` #### 2.1设置显卡 #### 步骤 1:检查系统和 GPU 环境 首先确认你的 CentOS 系统有 NVIDIA GPU,且已安装 NVIDIA 驱动(**驱动版本需≥450.80.02**)。 ```bash # 检查NVIDIA驱动是否安装 nvidia-smi ``` 如果该命令能输出 GPU 信息,说明驱动已安装;如果提示命令未找到,需要先安装 NVIDIA GPU 驱动(可参考 NVIDIA 官方文档)。 #### 步骤 2:安装 NVIDIA Container Toolkit 这是让 Docker 支持 GPU 的核心组件,执行以下命令在 CentOS 上安装: ![image-20250901151211521](images/image-20250901151211521.png) ``` docker ps -a ``` ![image-20250902102409337](images/image-20250902102409337.png) ``` docker logs -f ragflow-server ``` ### 3: 使用步骤及注意事项 登录Web界面 - 浏览器访问`http://192.168.2.100/login`(或修改后的端口),注册账号并登录 ![image-20250902103813082](images/image-20250902103813082.png) ### 4: 本地模型本地知识库 ``` ollama run qwen3:latest ``` ``` ollama pull quentinz/bge-large-zh-v1.5 ``` ![image-20251224144939070](images/image-20251224144939070.png) #### 网络设置 如RagFlow何Ollama都在虚拟机上则不存在一下问题。可以跳过。 Centos ping不通 192.168.2.1 192.168.2.2 ```cmd # 1. 允许 ICMP(ping) netsh advfirewall firewall add rule name="Allow ICMPv4" dir=in action=allow protocol=icmpv4 # 2. 允许 Ollama 端口(11434) New-NetFirewallRule -DisplayName "Ollama-11434" -Direction Inbound -LocalPort 11434 -Protocol TCP -Action Allow # 3. 如果需要,允许其他端口(比如 80、443 等) New-NetFirewallRule -DisplayName "HTTP-80" -Direction Inbound -LocalPort 80 -Protocol TCP -Action Allow New-NetFirewallRule -DisplayName "HTTPS-443" -Direction Inbound -LocalPort 443 -Protocol TCP -Action Allow ``` 在C:\Users\Administrator\.ollama建立start_ollama.bat文件 ```bat echo Starting Ollama with network access... set OLLAMA_HOST=0.0.0.0 start ollama serve echo Waiting... timeout /t 5 echo Checking ports... netstat -ano | findstr :11434 echo. echo If you see "0.0.0.0:11434", it's working! echo Test from CentOS: curl http://192.168.2.1:11434/api/tags pause ``` image-20251224160321120 FagFlow中配置ollama image-20251224154758731 image-20251224160634203 默认模型设置 image-20251224160803451 #### 本地模型知识库 image-20251224161259281 完全没有网络也可以实现,这是对数据安全性要求较高的选择。 ![image-20251224161812516](images/image-20251224161812516.png) #### 代码访问 ##### 0: 原始HttpRequest访问 ``` com.fasterxml.jackson.core jackson-databind 2.15.0 ``` ```java package com.jiazhong.springopenai; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; import java.time.Duration; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; public class Test { private static final String CHAT_ID = "dc9f7604e0a411f09d090242ac120003"; private static final String BASE_URL = "http://192.168.2.100/"; private static final String API_KEY = "ragflow-ViNmE2NThjZTA2YzExZjBiOWQyMDI0Mm"; private static final HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); private static final ObjectMapper objectMapper = new ObjectMapper(); // 响应体类 @JsonIgnoreProperties(ignoreUnknown = true) public static class ChatResponse { public Choice[] choices; } @JsonIgnoreProperties(ignoreUnknown = true) public static class Choice { public Message message; } @JsonIgnoreProperties(ignoreUnknown = true) public static class Message { public String content; public String role; } public static String sendChatMessage(String msg) { try { String url = BASE_URL + "api/v1/chats_openai/" + CHAT_ID + "/chat/completions"; // 手动构建JSON请求体 String requestBody = String.format("{" + "\"model\": \"Qwen/Qwen3-8B\"," + "\"messages\": [{\"role\": \"user\", \"content\": \"%s\"}]," + "\"stream\": false" + "}", msg.replace("\"", "\\\"")); // 转义引号 HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + API_KEY) .POST(BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = client.send(request, BodyHandlers.ofString()); if (response.statusCode() == 200) { // 解析JSON响应,提取中文内容 ChatResponse chatResponse = objectMapper.readValue(response.body(), ChatResponse.class); if (chatResponse.choices != null && chatResponse.choices.length > 0) { return chatResponse.choices[0].message.content; } else { return "未找到回复内容"; } } else { throw new RuntimeException("HTTP Error: " + response.statusCode() + " - " + response.body()); } } catch (Exception e) { throw new RuntimeException("Failed to send message: " + e.getMessage(), e); } } public static void main(String[] args) { try { String response = sendChatMessage("如何做jwt"); System.out.println(response); } catch (Exception e) { e.printStackTrace(); } } } ``` image-20251225112409629 ##### 1:pom.xml ``` 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.8 com.jiazhong springopenairagflow 0.0.1-SNAPSHOT springopenairagflow springopenairagflow 17 17 UTF-8 com.fasterxml.jackson.core jackson-databind 2.15.0 org.springframework.boot spring-boot-starter-web org.projectlombok lombok org.springframework.ai spring-ai-starter-model-openai org.springframework.ai spring-ai-bom 1.0.0 import pom ``` ##### 2: application.yml ``` spring: ai: openai: base-url: http://192.168.2.100/api/v1/chats_openai/dc9f7604e0a411f09d090242ac120003 api-key: "ragflow-ViNmE2NThjZTA2YzExZjBiOWQyMDI0Mm" chat: options: model: "qwen3:latest" temperature: 0.7 logging: level: org.springframework.ai: INFO org.springframework.web.client: INFO ``` ##### 3: ChatClientConfig ```java package com.jiazhong.springopenai.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.model.SimpleApiKey; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class ChatClientConfig { @Value("${spring.ai.openai.base-url}") private String baseUrl; @Value("${spring.ai.openai.api-key}") private String apiKey; @Value("${spring.ai.openai.chat.options.model:qwen3:latest}") private String model; @Value("${spring.ai.openai.chat.options.temperature:0.7}") private double temperature; /** * 根据OpenAiApi源码修正配置 * 关键:baseUrl不能包含/chat/completions,completionsPath必须是/v1/chat/completions */ @Bean @Primary public OpenAiChatModel ragFlowChatModel() { // 重要:baseUrl不能包含/chat/completions // RAG Flow的baseUrl应该是:http://192.168.2.100/api/v1/chats_openai/dc9f7604e0a411f09d090242ac120003 // 然后completionsPath是:/chat/completions System.out.println("=== 配置RAG Flow API ==="); System.out.println("Base URL: " + baseUrl); System.out.println("Model: " + model); System.out.println("Temperature: " + temperature); // 1. 创建ApiKey SimpleApiKey simpleApiKey = new SimpleApiKey(apiKey); // 2. 创建headers(可以为空) MultiValueMap headers = new LinkedMultiValueMap<>(); // 3. 创建OpenAiApi OpenAiApi openAiApi = OpenAiApi.builder() .baseUrl(baseUrl) // 重要:这里是基础URL,不包含/chat/completions .apiKey(simpleApiKey) .headers(headers) .completionsPath("/chat/completions") // 重要:这里设置路径 .embeddingsPath("/v1/embeddings") // 随便设置 .build(); // 验证URL System.out.println("完整API URL: " + baseUrl + "/chat/completions"); // 4. 创建OpenAiChatOptions OpenAiChatOptions options = OpenAiChatOptions.builder() .model(model) .temperature(temperature) .build(); // 5. 创建OpenAiChatModel return OpenAiChatModel.builder() .openAiApi(openAiApi) .defaultOptions(options) .build(); } /** * 创建ChatClient */ @Bean public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) { System.out.println("=== 创建支持中文的ChatClient ==="); return ChatClient.builder(openAiChatModel) .defaultSystem(""" 你是一个智能助手,请使用中文回答问题。 请用中文总结知识库的内容来回答问题,详细列举知识库中的数据。 当知识库内容与问题无关时,回答必须包括“知识库中未找到您要的答案!”这句话。 所有回答都必须使用简体中文。 """) .defaultOptions(OpenAiChatOptions.builder() .model(model) .temperature(temperature) .build()) .build(); } /** * 打印配置信息 */ @Bean public String configInfo() { System.out.println("=== 配置验证 ==="); System.out.println("1. Base URL: " + baseUrl); System.out.println("2. Completions Path: /chat/completions"); System.out.println("3. 完整URL: " + baseUrl + "/chat/completions"); System.out.println("4. 模型: " + model); System.out.println("5. 温度: " + temperature); System.out.println("=== 配置验证完成 ==="); return "RAG Flow配置验证完成"; } } ``` ##### 4: 控制器 ```java package com.jiazhong.springopenai.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class UserController { @Resource private ChatClient openAiChatClient; @GetMapping(value = "/say", produces = "text/html;charset=utf-8") @CrossOrigin public String say(@RequestParam("question") String question) { return openAiChatClient.prompt() .user(question) .call().content(); } @GetMapping(value = "/saystream", produces = "text/html;charset=utf-8") @CrossOrigin public Flux saystream(@RequestParam("question") String question) { return openAiChatClient .prompt() .user(question) .stream().content() .map(chunk -> { // 处理流式输出中的换行符 return chunk.replace("\n", "
"); }); } } ``` ![image-20251225112637827](images/image-20251225112637827.png) ##### 5: web页面 ```html RAG Flow流式聊天

RAG Flow流式聊天助手

基于Spring AI + Vue的流式对话

{{ msg.content }}
思考中...
已连接后端: {{ backendUrl }}
``` ![image-20251225112731432](images/image-20251225112731432.png) ### 5:远程模型本地知识库 #### 设置网络 ``` # 创建或编辑Docker守护进程配置文件 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json < 数据集** image-20251224110750582 **聊天** ![image-20251225113341106](images/image-20251225113341106.png) 那你的代码跟过去有什么区别呢?仅仅更改聊天ID就好了。 ##### application.yml ```yml spring: ai: openai: base-url: http://192.168.2.100/api/v1/chats_openai/80e1b374e14211f0a2e20242ac120003 api-key: "ragflow-ViNmE2NThjZTA2YzExZjBiOWQyMDI0Mm" chat: options: model: "Qwen/QwQ-32B" temperature: 0.7 logging: level: org.springframework.ai: INFO org.springframework.web.client: INFO ``` ![image-20251225113815432](images/image-20251225113815432.png) ![image-20251225114035866](images/image-20251225114035866.png) ## 代码在您哪里怎么跑 ``` 我是用了pip freeze > requirments.txt 你的文件理由有个requirments.txt 你得 pip install -r requirments.txt #这样依赖库就装完了 改utils.py里面的数据库用户名和密码 改exprotdb.py里面的文件路径 ```