Commit ·
d0f36e8
0
Parent(s):
初始提交:闲鱼搜索API服务
Browse files- .env.example +2 -0
- .github/workflows/main.yml +25 -0
- .gitignore +37 -0
- Dockerfile +18 -0
- README-HF.md +74 -0
- README.md +66 -0
- api_server.py +74 -0
- app.py +8 -0
- data_parser.py +109 -0
- goofish_api.py +138 -0
- requirements.txt +7 -0
- space.yaml +8 -0
- static/index.html +92 -0
.env.example
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 闲鱼cookies(需包含_m_h5_tk和_m_h5_tk_enc)
|
| 2 |
+
GOOFISH_COOKIES=_m_h5_tk=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxx; _m_h5_tk_enc=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
.github/workflows/main.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
workflow_dispatch: # 允许手动触发
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
deploy:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- uses: actions/checkout@v2
|
| 13 |
+
with:
|
| 14 |
+
fetch-depth: 0
|
| 15 |
+
|
| 16 |
+
- name: 推送到Hugging Face Spaces
|
| 17 |
+
env:
|
| 18 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 19 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 20 |
+
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }}
|
| 21 |
+
run: |
|
| 22 |
+
git config --global user.email "action@github.com"
|
| 23 |
+
git config --global user.name "GitHub Action"
|
| 24 |
+
git remote add space https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/$HF_USERNAME/$HF_SPACE_NAME
|
| 25 |
+
git push --force space main
|
.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
build/
|
| 9 |
+
develop-eggs/
|
| 10 |
+
dist/
|
| 11 |
+
downloads/
|
| 12 |
+
eggs/
|
| 13 |
+
.eggs/
|
| 14 |
+
lib/
|
| 15 |
+
lib64/
|
| 16 |
+
parts/
|
| 17 |
+
sdist/
|
| 18 |
+
var/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# 环境变量
|
| 24 |
+
.env
|
| 25 |
+
|
| 26 |
+
# 日志
|
| 27 |
+
*.log
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.idea/
|
| 31 |
+
.vscode/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
|
| 35 |
+
# 系统文件
|
| 36 |
+
.DS_Store
|
| 37 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
# 设置环境变量
|
| 11 |
+
ENV HOST=0.0.0.0
|
| 12 |
+
ENV PORT=7860
|
| 13 |
+
|
| 14 |
+
# 暴露端口(Hugging Face Spaces使用7860端口)
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
# 启动应用
|
| 18 |
+
CMD ["python", "app.py"]
|
README-HF.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 闲鱼搜索API
|
| 2 |
+
|
| 3 |
+
这是一个用于搜索闲鱼商品的API服务,提供快速、简单的方式获取闲鱼商品数据。
|
| 4 |
+
|
| 5 |
+
## API文档
|
| 6 |
+
|
| 7 |
+
访问 `/docs` 可查看完整的API文档。
|
| 8 |
+
|
| 9 |
+
### 主要功能
|
| 10 |
+
|
| 11 |
+
- 商品搜索(关键词搜索)
|
| 12 |
+
- 价格区间筛选
|
| 13 |
+
- 发布时间筛选
|
| 14 |
+
- 分页功能
|
| 15 |
+
|
| 16 |
+
### 使用示例
|
| 17 |
+
|
| 18 |
+
1. 基本搜索:
|
| 19 |
+
```
|
| 20 |
+
/api/search?keyword=手机
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
2. 价格区间搜索:
|
| 24 |
+
```
|
| 25 |
+
/api/search?keyword=手机&min_price=100&max_price=500
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
3. 最近发布时间:
|
| 29 |
+
```
|
| 30 |
+
/api/search?keyword=手机&publish_days=3
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
4. 组合条件:
|
| 34 |
+
```
|
| 35 |
+
/api/search?keyword=手机&min_price=100&max_price=500&publish_days=3&page=1&page_size=20
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## 返回数据格式
|
| 39 |
+
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"code": 0,
|
| 43 |
+
"message": "success",
|
| 44 |
+
"data": [
|
| 45 |
+
{
|
| 46 |
+
"title": "商品标题",
|
| 47 |
+
"price": 100.0,
|
| 48 |
+
"item_id": "123456789",
|
| 49 |
+
"area": "商品所在地区",
|
| 50 |
+
"seller_nick": "卖家昵称",
|
| 51 |
+
"publish_time": "发布时间",
|
| 52 |
+
"pics": ["商品图片URL"],
|
| 53 |
+
"want_count": 5,
|
| 54 |
+
"detail_url": "https://www.goofish.com/item?id=123456789"
|
| 55 |
+
},
|
| 56 |
+
...
|
| 57 |
+
],
|
| 58 |
+
"total": 30
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## 部署说明
|
| 63 |
+
|
| 64 |
+
本API服务基于FastAPI构建,使用Docker部署在Hugging Face Spaces上。
|
| 65 |
+
|
| 66 |
+
如需本地运行,请确保:
|
| 67 |
+
|
| 68 |
+
1. 安装所需依赖:`pip install -r requirements.txt`
|
| 69 |
+
2. 设置环境变量:在 `.env` 文件中添加 `GOOFISH_COOKIES=你的闲鱼cookies`
|
| 70 |
+
3. 运行服务:`python api_server.py`
|
| 71 |
+
|
| 72 |
+
## 免责声明
|
| 73 |
+
|
| 74 |
+
本项目仅用于学习和研究目的,请勿用于商业用途。使用本API访问数据时请遵守闲鱼的使用条款和政策。
|
README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
layout: default
|
| 3 |
+
title: 闲鱼搜索API
|
| 4 |
+
description: 一个用于搜索闲鱼商品的API服务
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# 闲鱼搜索API
|
| 8 |
+
|
| 9 |
+
这是一个用于搜索闲鱼商品的API服务,提供快速、简单的方式获取闲鱼商品数据。
|
| 10 |
+
|
| 11 |
+
## 功能特点
|
| 12 |
+
|
| 13 |
+
- 商品搜索(关键词搜索)
|
| 14 |
+
- 价格区间筛选
|
| 15 |
+
- 发布时间筛选
|
| 16 |
+
- 分页功能
|
| 17 |
+
- 完整的API文档
|
| 18 |
+
|
| 19 |
+
## 使用方法
|
| 20 |
+
|
| 21 |
+
### 安装依赖
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
pip install -r requirements.txt
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 设置环境变量
|
| 28 |
+
|
| 29 |
+
创建一个 `.env` 文件,添加以下内容:
|
| 30 |
+
|
| 31 |
+
```
|
| 32 |
+
GOOFISH_COOKIES=你的闲鱼cookies
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### 启动服务
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
python api_server.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
访问 http://localhost:8000/docs 查看API文档
|
| 42 |
+
|
| 43 |
+
## API使用示例
|
| 44 |
+
|
| 45 |
+
1. 基本搜索:
|
| 46 |
+
```
|
| 47 |
+
http://localhost:8000/api/search?keyword=手机
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
2. 价格区间搜索:
|
| 51 |
+
```
|
| 52 |
+
http://localhost:8000/api/search?keyword=手机&min_price=100&max_price=500
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
3. 最近发布时间:
|
| 56 |
+
```
|
| 57 |
+
http://localhost:8000/api/search?keyword=手机&publish_days=3
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## 部署到Hugging Face Spaces
|
| 61 |
+
|
| 62 |
+
本项目可以部署到Hugging Face Spaces,详情请参考 `README-HF.md`。
|
| 63 |
+
|
| 64 |
+
## 免责声明
|
| 65 |
+
|
| 66 |
+
本项目仅用于学习和研究目的,请勿用于商业用途。使用本API访问数据时请遵守闲鱼的使用条款和政策。
|
api_server.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fastapi import FastAPI, Query, HTTPException
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.responses import RedirectResponse
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from goofish_api import GoofishAPI
|
| 8 |
+
from data_parser import ItemDetail, parse_search_result
|
| 9 |
+
|
| 10 |
+
# 定义搜索响应模型
|
| 11 |
+
class SearchResponse(BaseModel):
|
| 12 |
+
code: int = 0
|
| 13 |
+
message: str = "success"
|
| 14 |
+
data: List[ItemDetail]
|
| 15 |
+
total: int = 0
|
| 16 |
+
|
| 17 |
+
app = FastAPI(
|
| 18 |
+
title="闲鱼搜索API",
|
| 19 |
+
description="搜索闲鱼商品的API服务",
|
| 20 |
+
version="1.0.0"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# 挂载静态文件
|
| 24 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 25 |
+
|
| 26 |
+
# 初始化API客户端
|
| 27 |
+
goofish_client = GoofishAPI()
|
| 28 |
+
|
| 29 |
+
@app.get("/", include_in_schema=False)
|
| 30 |
+
async def root():
|
| 31 |
+
return RedirectResponse(url="/static/index.html")
|
| 32 |
+
|
| 33 |
+
@app.get("/api/search", response_model=SearchResponse, tags=["搜索"])
|
| 34 |
+
async def search(
|
| 35 |
+
keyword: str = Query(..., description="搜索关键词"),
|
| 36 |
+
page: int = Query(1, description="页码,从1开始"),
|
| 37 |
+
page_size: int = Query(10, description="每页结果数量,最大40"),
|
| 38 |
+
min_price: Optional[float] = Query(None, description="最低价格"),
|
| 39 |
+
max_price: Optional[float] = Query(None, description="最高价格"),
|
| 40 |
+
publish_days: Optional[int] = Query(None, description="最近发布天数,例如3表示最近3天发布的商品")
|
| 41 |
+
):
|
| 42 |
+
"""
|
| 43 |
+
搜索闲鱼商品
|
| 44 |
+
"""
|
| 45 |
+
try:
|
| 46 |
+
# 调用GoofishAPI的search方法,参数名称要与GoofishAPI.search方法一致
|
| 47 |
+
raw_result = goofish_client.search(
|
| 48 |
+
keyword=keyword,
|
| 49 |
+
page_number=page,
|
| 50 |
+
rows_per_page=page_size,
|
| 51 |
+
min_price=min_price,
|
| 52 |
+
max_price=max_price,
|
| 53 |
+
publish_days=publish_days
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# 解析结果
|
| 57 |
+
items = parse_search_result(raw_result)
|
| 58 |
+
total = len(items)
|
| 59 |
+
|
| 60 |
+
# 构建响应
|
| 61 |
+
return SearchResponse(
|
| 62 |
+
code=0,
|
| 63 |
+
message="success",
|
| 64 |
+
data=items,
|
| 65 |
+
total=total
|
| 66 |
+
)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"搜索出错: {str(e)}")
|
| 69 |
+
raise HTTPException(status_code=500, detail=f"搜索出错:{str(e)}")
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
port = int(os.environ.get("PORT", 8000))
|
| 73 |
+
import uvicorn
|
| 74 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
app.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from api_server import app
|
| 3 |
+
import uvicorn
|
| 4 |
+
|
| 5 |
+
# Hugging Face Spaces默认使用端口7860
|
| 6 |
+
if __name__ == "__main__":
|
| 7 |
+
port = int(os.environ.get("PORT", 7860))
|
| 8 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
data_parser.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Optional
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
class ItemLocation(BaseModel):
|
| 6 |
+
area: str
|
| 7 |
+
|
| 8 |
+
class ItemPrice(BaseModel):
|
| 9 |
+
price: float
|
| 10 |
+
|
| 11 |
+
class ItemDetail(BaseModel):
|
| 12 |
+
title: str
|
| 13 |
+
price: float
|
| 14 |
+
item_id: str
|
| 15 |
+
area: str
|
| 16 |
+
seller_nick: str
|
| 17 |
+
publish_time: Optional[str]
|
| 18 |
+
pics: List[str]
|
| 19 |
+
want_count: int = 0
|
| 20 |
+
detail_url: str = "" # 添加商品详情页URL字段
|
| 21 |
+
|
| 22 |
+
def safe_int(value: str, default: int = 0) -> int:
|
| 23 |
+
"""安全地将字符串转换为整数"""
|
| 24 |
+
try:
|
| 25 |
+
if not value:
|
| 26 |
+
return default
|
| 27 |
+
return int(value)
|
| 28 |
+
except (ValueError, TypeError):
|
| 29 |
+
return default
|
| 30 |
+
|
| 31 |
+
def safe_float(value: str, default: float = 0.0) -> float:
|
| 32 |
+
"""安全地将字符串转换为浮点数"""
|
| 33 |
+
try:
|
| 34 |
+
if not value:
|
| 35 |
+
return default
|
| 36 |
+
return float(value)
|
| 37 |
+
except (ValueError, TypeError):
|
| 38 |
+
return default
|
| 39 |
+
|
| 40 |
+
def parse_search_result(raw_data: Dict) -> List[ItemDetail]:
|
| 41 |
+
"""解析搜索结果数据"""
|
| 42 |
+
print("开始解析数据...")
|
| 43 |
+
|
| 44 |
+
if not raw_data or 'data' not in raw_data:
|
| 45 |
+
print("无效的数据格式:缺少 'data' 字段")
|
| 46 |
+
return []
|
| 47 |
+
|
| 48 |
+
if 'resultList' not in raw_data['data']:
|
| 49 |
+
print("无效的数据格式:缺少 'resultList' 字段")
|
| 50 |
+
print(f"可用的字段: {list(raw_data['data'].keys())}")
|
| 51 |
+
return []
|
| 52 |
+
|
| 53 |
+
items = []
|
| 54 |
+
items_array = raw_data['data'].get('resultList', [])
|
| 55 |
+
print(f"找到 {len(items_array)} 个商品")
|
| 56 |
+
|
| 57 |
+
for idx, item_data in enumerate(items_array):
|
| 58 |
+
try:
|
| 59 |
+
if 'data' not in item_data:
|
| 60 |
+
print(f"商品 {idx} 缺少 'data' 字段")
|
| 61 |
+
continue
|
| 62 |
+
|
| 63 |
+
if 'item' not in item_data['data']:
|
| 64 |
+
print(f"商品 {idx} 缺少 'item' 字段")
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
item = item_data['data']['item']
|
| 68 |
+
if 'main' not in item:
|
| 69 |
+
print(f"商品 {idx} 缺少 'main' 字段")
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
if 'exContent' not in item['main']:
|
| 73 |
+
print(f"商品 {idx} 缺少 'exContent' 字段")
|
| 74 |
+
continue
|
| 75 |
+
|
| 76 |
+
ex_content = item['main']['exContent']
|
| 77 |
+
detail_params = ex_content.get('detailParams', {})
|
| 78 |
+
|
| 79 |
+
# 提取价格
|
| 80 |
+
price = safe_float(detail_params.get('soldPrice', 0))
|
| 81 |
+
|
| 82 |
+
# 提取商品ID
|
| 83 |
+
item_id = ex_content.get('itemId', '')
|
| 84 |
+
|
| 85 |
+
# 构建商品详情页URL
|
| 86 |
+
detail_url = f"https://www.goofish.com/item?id={item_id}" if item_id else ""
|
| 87 |
+
|
| 88 |
+
# 构建商品详情
|
| 89 |
+
item_detail = ItemDetail(
|
| 90 |
+
title=ex_content.get('title', ''),
|
| 91 |
+
price=price,
|
| 92 |
+
item_id=item_id,
|
| 93 |
+
area=ex_content.get('area', ''),
|
| 94 |
+
seller_nick=ex_content.get('userNickName', ''),
|
| 95 |
+
publish_time=str(detail_params.get('publishTime', '')),
|
| 96 |
+
pics=[ex_content.get('picUrl', '')] if ex_content.get('picUrl') else [],
|
| 97 |
+
want_count=safe_int(ex_content.get('want', '0')),
|
| 98 |
+
detail_url=detail_url
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
items.append(item_detail)
|
| 102 |
+
print(f"成功解析商品 {idx}: {item_detail.title[:30]}...")
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"解析商品 {idx} 时出错: {str(e)}")
|
| 106 |
+
continue
|
| 107 |
+
|
| 108 |
+
print(f"成功解析 {len(items)} 个商品")
|
| 109 |
+
return items
|
goofish_api.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import time
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
def parse_cookies(cookie_str):
|
| 9 |
+
"""将cookie字符串解析为字典"""
|
| 10 |
+
cookies = {}
|
| 11 |
+
for item in cookie_str.split(';'):
|
| 12 |
+
if '=' in item:
|
| 13 |
+
name, value = item.strip().split('=', 1)
|
| 14 |
+
cookies[name] = value
|
| 15 |
+
return cookies
|
| 16 |
+
|
| 17 |
+
class GoofishAPI:
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.base_url = "https://h5api.m.goofish.com"
|
| 20 |
+
self.app_key = "34839810"
|
| 21 |
+
self.headers = {
|
| 22 |
+
'accept': 'application/json',
|
| 23 |
+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
| 24 |
+
'content-type': 'application/x-www-form-urlencoded',
|
| 25 |
+
'origin': 'https://www.goofish.com',
|
| 26 |
+
'referer': 'https://www.goofish.com/',
|
| 27 |
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0'
|
| 28 |
+
}
|
| 29 |
+
# 从环境变量加载cookie
|
| 30 |
+
load_dotenv()
|
| 31 |
+
cookie_str = os.getenv('GOOFISH_COOKIES', '')
|
| 32 |
+
|
| 33 |
+
# 如果没有找到环境变量,尝试使用默认cookie
|
| 34 |
+
if not cookie_str:
|
| 35 |
+
print("警告:未找到GOOFISH_COOKIES环境变量,使用内置默认cookie")
|
| 36 |
+
# 设置一个简单的默认cookie,可能无法正常工作
|
| 37 |
+
cookie_str = "_m_h5_tk=6e9d46fed73aae0bf6be61ee132e9a06_1742723039105; _m_h5_tk_enc=6eb4c709b4fbcad1a927c771c7beef21"
|
| 38 |
+
|
| 39 |
+
self.cookies = parse_cookies(cookie_str)
|
| 40 |
+
|
| 41 |
+
def _get_sign(self, t, data):
|
| 42 |
+
"""生成签名"""
|
| 43 |
+
token = self.cookies.get('_m_h5_tk', '').split('_')[0]
|
| 44 |
+
sign_str = f"{token}&{t}&{self.app_key}&{data}"
|
| 45 |
+
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
|
| 46 |
+
|
| 47 |
+
def search(self, keyword, page_number=1, rows_per_page=30, min_price=None, max_price=None, publish_days=None):
|
| 48 |
+
"""
|
| 49 |
+
搜索商品
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
keyword (str): 搜索关键词
|
| 53 |
+
page_number (int): 页码,从1开始
|
| 54 |
+
rows_per_page (int): 每页数量
|
| 55 |
+
min_price (float, optional): 最低价格
|
| 56 |
+
max_price (float, optional): 最高价格
|
| 57 |
+
publish_days (int, optional): 发布时间范围(天)
|
| 58 |
+
"""
|
| 59 |
+
t = str(int(time.time() * 1000))
|
| 60 |
+
|
| 61 |
+
# 构建搜索过滤条件
|
| 62 |
+
search_filters = []
|
| 63 |
+
|
| 64 |
+
# 添加价格区间
|
| 65 |
+
if min_price is not None or max_price is not None:
|
| 66 |
+
min_price = min_price if min_price is not None else 0
|
| 67 |
+
max_price = max_price if max_price is not None else ''
|
| 68 |
+
search_filters.append(f"priceRange:{min_price},{max_price}")
|
| 69 |
+
|
| 70 |
+
# 添加发布时间
|
| 71 |
+
if publish_days is not None:
|
| 72 |
+
search_filters.append(f"publishDays:{publish_days}")
|
| 73 |
+
|
| 74 |
+
# 构建propValueStr
|
| 75 |
+
prop_value_str = {}
|
| 76 |
+
if search_filters:
|
| 77 |
+
prop_value_str["searchFilter"] = ";".join(search_filters)
|
| 78 |
+
|
| 79 |
+
# 构建请求数据
|
| 80 |
+
data = {
|
| 81 |
+
"pageNumber": page_number,
|
| 82 |
+
"keyword": keyword,
|
| 83 |
+
"fromFilter": bool(search_filters), # 如果有过滤条件则为True
|
| 84 |
+
"rowsPerPage": rows_per_page,
|
| 85 |
+
"sortValue": "",
|
| 86 |
+
"sortField": "",
|
| 87 |
+
"customDistance": "",
|
| 88 |
+
"gps": "",
|
| 89 |
+
"propValueStr": prop_value_str,
|
| 90 |
+
"customGps": "",
|
| 91 |
+
"searchReqFromPage": "pcSearch",
|
| 92 |
+
"extraFilterValue": "{}",
|
| 93 |
+
"userPositionJson": "{}"
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
data_str = json.dumps(data)
|
| 97 |
+
sign = self._get_sign(t, data_str)
|
| 98 |
+
|
| 99 |
+
# 构建URL参数
|
| 100 |
+
params = {
|
| 101 |
+
'jsv': '2.7.2',
|
| 102 |
+
'appKey': self.app_key,
|
| 103 |
+
't': t,
|
| 104 |
+
'sign': sign,
|
| 105 |
+
'v': '1.0',
|
| 106 |
+
'type': 'originaljson',
|
| 107 |
+
'accountSite': 'xianyu',
|
| 108 |
+
'dataType': 'json',
|
| 109 |
+
'timeout': '20000',
|
| 110 |
+
'api': 'mtop.taobao.idlemtopsearch.pc.search',
|
| 111 |
+
'sessionOption': 'AutoLoginOnly',
|
| 112 |
+
'spm_cnt': 'a21ybx.search.0.0',
|
| 113 |
+
'spm_pre': 'a21ybx.home.searchInput.0'
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
url = f"{self.base_url}/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/"
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
response = requests.post(
|
| 120 |
+
url,
|
| 121 |
+
params=params,
|
| 122 |
+
data={'data': data_str},
|
| 123 |
+
headers=self.headers,
|
| 124 |
+
cookies=self.cookies
|
| 125 |
+
)
|
| 126 |
+
return response.json()
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"请求失败: {str(e)}")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
def main():
|
| 132 |
+
api = GoofishAPI()
|
| 133 |
+
# 测试搜索手机
|
| 134 |
+
result = api.search("手机")
|
| 135 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
| 136 |
+
|
| 137 |
+
if __name__ == "__main__":
|
| 138 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.0
|
| 2 |
+
uvicorn==0.23.2
|
| 3 |
+
requests==2.31.0
|
| 4 |
+
python-dotenv==1.0.0
|
| 5 |
+
pydantic==2.4.2
|
| 6 |
+
starlette==0.27.0
|
| 7 |
+
httpx==0.24.1
|
space.yaml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: 闲鱼搜索API
|
| 2 |
+
emoji: 🦑
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: purple
|
| 5 |
+
sdk: docker
|
| 6 |
+
app_port: 7860
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
static/index.html
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>闲鱼搜索API</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
line-height: 1.6;
|
| 11 |
+
color: #333;
|
| 12 |
+
max-width: 800px;
|
| 13 |
+
margin: 0 auto;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
}
|
| 16 |
+
h1 {
|
| 17 |
+
color: #2c3e50;
|
| 18 |
+
border-bottom: 2px solid #3498db;
|
| 19 |
+
padding-bottom: 10px;
|
| 20 |
+
}
|
| 21 |
+
h2 {
|
| 22 |
+
color: #2980b9;
|
| 23 |
+
margin-top: 30px;
|
| 24 |
+
}
|
| 25 |
+
pre {
|
| 26 |
+
background-color: #f8f9fa;
|
| 27 |
+
border: 1px solid #e9ecef;
|
| 28 |
+
border-radius: 4px;
|
| 29 |
+
padding: 15px;
|
| 30 |
+
overflow-x: auto;
|
| 31 |
+
}
|
| 32 |
+
code {
|
| 33 |
+
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
| 34 |
+
color: #e74c3c;
|
| 35 |
+
}
|
| 36 |
+
.btn {
|
| 37 |
+
display: inline-block;
|
| 38 |
+
background-color: #3498db;
|
| 39 |
+
color: white;
|
| 40 |
+
padding: 10px 15px;
|
| 41 |
+
text-decoration: none;
|
| 42 |
+
border-radius: 4px;
|
| 43 |
+
margin-top: 10px;
|
| 44 |
+
}
|
| 45 |
+
.btn:hover {
|
| 46 |
+
background-color: #2980b9;
|
| 47 |
+
}
|
| 48 |
+
</style>
|
| 49 |
+
</head>
|
| 50 |
+
<body>
|
| 51 |
+
<h1>🦑 闲鱼搜索API</h1>
|
| 52 |
+
<p>欢迎使用闲鱼搜索API服务,本服务提供简单易用的API接口,帮助您快速获取闲鱼商品数据。</p>
|
| 53 |
+
|
| 54 |
+
<a href="/docs" class="btn">查看完整API文档</a>
|
| 55 |
+
|
| 56 |
+
<h2>API使用示例</h2>
|
| 57 |
+
|
| 58 |
+
<h3>基本搜索</h3>
|
| 59 |
+
<pre><code>GET /api/search?keyword=手机</code></pre>
|
| 60 |
+
|
| 61 |
+
<h3>带价格区间的搜索</h3>
|
| 62 |
+
<pre><code>GET /api/search?keyword=手机&min_price=1000&max_price=2000</code></pre>
|
| 63 |
+
|
| 64 |
+
<h3>搜索最近3天发布的商品</h3>
|
| 65 |
+
<pre><code>GET /api/search?keyword=手机&publish_days=3</code></pre>
|
| 66 |
+
|
| 67 |
+
<h3>分页</h3>
|
| 68 |
+
<pre><code>GET /api/search?keyword=手机&page=2&page_size=20</code></pre>
|
| 69 |
+
|
| 70 |
+
<h2>返回数据格式</h2>
|
| 71 |
+
<pre><code>{
|
| 72 |
+
"code": 0,
|
| 73 |
+
"message": "success",
|
| 74 |
+
"data": [
|
| 75 |
+
{
|
| 76 |
+
"item_id": "商品ID",
|
| 77 |
+
"title": "商品标题",
|
| 78 |
+
"price": "价格",
|
| 79 |
+
"pics": ["图片URL列表"],
|
| 80 |
+
"location": "地点",
|
| 81 |
+
"publish_time": "发布时间",
|
| 82 |
+
"detail_url": "详情页URL"
|
| 83 |
+
},
|
| 84 |
+
...
|
| 85 |
+
],
|
| 86 |
+
"total": 商品总数
|
| 87 |
+
}</code></pre>
|
| 88 |
+
|
| 89 |
+
<h2>免责声明</h2>
|
| 90 |
+
<p>本项目仅用于学习和研究目的,请勿用于商业用途。使用本API访问数据时请遵守闲鱼的使用条款和政策。</p>
|
| 91 |
+
</body>
|
| 92 |
+
</html>
|