# 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
```

浏览器的版本需要和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')
```
## 数据分析与可视化

```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
```
图形就出现了。

### 2:更改首页成为我自己首页
拷贝 js css img目录到static目录下,覆盖index.html就完事。
```
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

#### 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);
}
```

### 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 }}房源
| 小区名称 |
所在区域 |
户型结构 |
装修情况 |
房屋户型 |
房屋朝向 |
所在楼层 |
产权 |
房屋结构 |
建筑面积 |
单价 |
总价 |
房源类型 |
{% for item in house_list %}
| {{ item.小区名称 }} |
{{ item.所在区域}} |
{{ item.房屋户型 }} |
{{ item.装修情况 }} |
{{ item.房屋户型 }} |
{{ item.房屋朝向 }} |
{{ item.所在楼层 }} |
{{ item.产权性质 }} |
{{ item.房屋结构 }} |
{{ item.建筑面积 }} |
{{ item.单价 }} |
{{ item.总价 }} |
{{ item.房源类型 }} |
{% empty %}
| 暂无数据 |
{% endfor %}
{% 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 协议返回。
**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。
- 保存并启用:点击保存。如果服务器状态显示为绿灯,说明配置成功。
则可以对本地文件进行测试了。



### Mysql Mcp
最推荐使用PowerShell脚本来安装。请以管理员身份打开一个PowerShell窗口,然后执行以下命令:
powershell
```
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

这条命令会从官方源下载安装脚本并执行,通常会将 `uv.exe` 和 `uvx.exe` 安装到 `C:\Users\<你的用户名>\.local\bin\` 目录下。
UV安装完毕之后,手工将 uv加入到 PATH中。
```
uv --version
```
#### 安装 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
```

至此,我们将 `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"
}
}
}
```
#### **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服务器。



### 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,并将搜索的结果给我截图。
```
对比传统工具:Playwright + MCP的颠覆性优势

```
打开https://news.hsw.cn/ 并获取前5条新闻的标题
```

## 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}")
```

## 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());
}
```


##### 流式
```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
问题:
回答:
```

#### 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();
}
}
```


**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

```
ollama pull all-minilm
```

#### nomic-embed-text

```
ollama pull nomic-embed-text
```

#### 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);
```

#### **maven依赖**

```
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());
}
```

#### 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);
}
```

### 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
问题:
回答:
```



### 结构化数据
#### 问题:
**为什么查不到“蚜虫”?**
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

```
ollama pull all-minilm
```

#### nomic-embed-text

```
ollama pull nomic-embed-text
```

#### bge-m3:latest
RAGFlow 默认模型 bge-m3:latest,
#### bge-large-zh
号称最好用的中文 Embedding 模型(我试用下来跟 bge-m3:latest 没啥区别)
```
ollama pull quentinz/bge-large-zh-v1.5
```
## 模型类型说明
#### 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
镜像站是社区公益服务,域名可能变动,如果某个连接不通,换一个试试。同时,不要通过镜像站克隆私有仓库,以免泄露凭据
```

- 也可以修改`.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 上安装:

```
docker ps -a
```

```
docker logs -f ragflow-server
```
### 3: 使用步骤及注意事项
登录Web界面
- 浏览器访问`http://192.168.2.100/login`(或修改后的端口),注册账号并登录

### 4: 本地模型本地知识库
```
ollama run qwen3:latest
```
```
ollama pull quentinz/bge-large-zh-v1.5
```

#### 网络设置
如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
```
FagFlow中配置ollama
默认模型设置
#### 本地模型知识库
完全没有网络也可以实现,这是对数据安全性要求较高的选择。

#### 代码访问
##### 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();
}
}
}
```
##### 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", "
");
});
}
}
```

##### 5: web页面
```html
RAG Flow流式聊天
```

### 5:远程模型本地知识库
#### 设置网络
```
# 创建或编辑Docker守护进程配置文件
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <
数据集**
**聊天**

那你的代码跟过去有什么区别呢?仅仅更改聊天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
```


## 代码在您哪里怎么跑
```
我是用了pip freeze > requirments.txt 你的文件理由有个requirments.txt
你得 pip install -r requirments.txt #这样依赖库就装完了
改utils.py里面的数据库用户名和密码
改exprotdb.py里面的文件路径
```