Support first case: 1. Add filters in website; 2. Add export all file in admin
This commit is contained in:
@@ -1,198 +0,0 @@
|
|||||||
# 爬虫Bug修复总结
|
|
||||||
|
|
||||||
## 修复的问题列表
|
|
||||||
|
|
||||||
### 1. 新华网 - 不保存文章内容
|
|
||||||
**问题**: 新华网爬取的文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑,增加了更多内容选择器
|
|
||||||
- 修复了文章页面判断逻辑
|
|
||||||
- 添加了对新华网特定HTML结构的支持
|
|
||||||
|
|
||||||
### 2. 中国政府网 - 两个标题问题
|
|
||||||
**问题**: 爬取到文章后,打开文章详情会有两个标题存在
|
|
||||||
**修复**:
|
|
||||||
- 优化了标题提取逻辑,优先选择带有class="title"的h1标签
|
|
||||||
- 改进了标题去重机制
|
|
||||||
|
|
||||||
### 3. 人民网 - 乱码和404问题
|
|
||||||
**问题**: 爬取文章后会乱码,会有404,视频没有下载下来
|
|
||||||
**修复**:
|
|
||||||
- 添加了特殊的请求头配置
|
|
||||||
- 修复了编码问题,确保使用UTF-8编码
|
|
||||||
- 改进了错误处理机制
|
|
||||||
- 优化了视频下载逻辑
|
|
||||||
|
|
||||||
### 4. 央视网 - 没有保存视频
|
|
||||||
**问题**: 央视网的视频没有被正确下载和保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了对data-src、data-url等视频源属性的支持
|
|
||||||
- 添加了央视网特定的视频处理逻辑
|
|
||||||
- 改进了视频下载的错误处理和日志记录
|
|
||||||
|
|
||||||
### 5. 求是网 - 两个标题问题
|
|
||||||
**问题**: 打开文章详情会有两个标题
|
|
||||||
**修复**:
|
|
||||||
- 优化了标题提取逻辑
|
|
||||||
- 改进了标题去重机制
|
|
||||||
|
|
||||||
### 6. 解放军报 - 类别爬取问题
|
|
||||||
**问题**: 会把类别都爬下来
|
|
||||||
**修复**:
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
- 优化了内容区域识别
|
|
||||||
|
|
||||||
### 7. 光明日报 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 8. 中国日报 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 9. 工人日报 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 10. 科技日报 - 无法爬取
|
|
||||||
**问题**: 无法正常爬取文章
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
|
|
||||||
### 11. 人民政协报 - 爬取错误
|
|
||||||
**问题**: 爬取过程中出现错误
|
|
||||||
**修复**:
|
|
||||||
- 优化了错误处理机制
|
|
||||||
- 改进了文章结构识别
|
|
||||||
|
|
||||||
### 12. 中国纪检监察报 - 无法爬取
|
|
||||||
**问题**: 无法正常爬取文章
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
|
|
||||||
### 13. 中国新闻社 - 爬取非文章部分
|
|
||||||
**问题**: 爬取了非文章的部分内容
|
|
||||||
**修复**:
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
- 优化了内容区域识别
|
|
||||||
|
|
||||||
### 14. 学习时报 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 15. 中国青年报 - 无法爬取
|
|
||||||
**问题**: 无法正常爬取文章
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
|
|
||||||
### 16. 中国妇女报 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 17. 法治日报 - 无法爬取
|
|
||||||
**问题**: 无法正常爬取文章
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
|
|
||||||
### 18. 农民日报 - 正文未被爬取
|
|
||||||
**问题**: 文章正文没有被正确爬取
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 19. 学习强国 - 无法爬取
|
|
||||||
**问题**: 无法正常爬取文章
|
|
||||||
**修复**:
|
|
||||||
- 更新了文章结构识别逻辑
|
|
||||||
- 改进了文章页面判断逻辑
|
|
||||||
|
|
||||||
### 20. 旗帜网 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
### 21. 中国网 - 不保存文章内容
|
|
||||||
**问题**: 文章内容没有被正确保存
|
|
||||||
**修复**:
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 添加了对article-body等特定class的支持
|
|
||||||
|
|
||||||
## 主要修复内容
|
|
||||||
|
|
||||||
### 1. 文章结构识别优化
|
|
||||||
- 为每个网站添加了更精确的标题和内容选择器
|
|
||||||
- 增加了对多种HTML结构的支持
|
|
||||||
- 优化了选择器的优先级
|
|
||||||
|
|
||||||
### 2. 文章页面判断改进
|
|
||||||
- 改进了文章页面的识别逻辑
|
|
||||||
- 增加了URL路径模式的判断
|
|
||||||
- 优化了页面类型识别
|
|
||||||
|
|
||||||
### 3. 编码和请求优化
|
|
||||||
- 修复了人民网的乱码问题
|
|
||||||
- 添加了特殊的请求头配置
|
|
||||||
- 改进了错误处理机制
|
|
||||||
|
|
||||||
### 4. 视频下载增强
|
|
||||||
- 增加了对多种视频源属性的支持
|
|
||||||
- 添加了央视网特定的视频处理
|
|
||||||
- 改进了视频下载的错误处理
|
|
||||||
|
|
||||||
### 5. URL配置更新
|
|
||||||
- 将部分网站的URL从HTTP更新为HTTPS
|
|
||||||
- 确保使用正确的域名和协议
|
|
||||||
|
|
||||||
## 技术改进
|
|
||||||
|
|
||||||
### 1. 错误处理
|
|
||||||
- 添加了更完善的异常处理
|
|
||||||
- 改进了错误日志记录
|
|
||||||
- 增加了重试机制
|
|
||||||
|
|
||||||
### 2. 内容识别
|
|
||||||
- 增加了更多内容选择器
|
|
||||||
- 优化了选择器的优先级
|
|
||||||
- 添加了对特殊HTML结构的支持
|
|
||||||
|
|
||||||
### 3. 媒体处理
|
|
||||||
- 改进了图片和视频的下载逻辑
|
|
||||||
- 增加了对多种媒体源的支持
|
|
||||||
- 优化了媒体文件的保存
|
|
||||||
|
|
||||||
### 4. 性能优化
|
|
||||||
- 改进了请求超时设置
|
|
||||||
- 优化了编码处理
|
|
||||||
- 减少了不必要的请求
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. **单个测试**: 对每个修复的网站进行单独测试
|
|
||||||
2. **批量测试**: 使用批量爬取命令测试所有网站
|
|
||||||
3. **内容验证**: 检查爬取的文章内容是否完整
|
|
||||||
4. **媒体验证**: 确认图片和视频是否正确下载
|
|
||||||
5. **错误监控**: 监控爬取过程中的错误日志
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **动态适配**: 考虑添加动态适配机制,自动适应网站结构变化
|
|
||||||
2. **智能识别**: 使用机器学习技术提高内容识别的准确性
|
|
||||||
3. **反爬虫处理**: 添加更复杂的反爬虫绕过机制
|
|
||||||
4. **性能监控**: 添加性能监控和统计功能
|
|
||||||
5. **内容质量**: 增加内容质量检测和过滤机制
|
|
||||||
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
# 中央主流媒体爬虫系统
|
|
||||||
|
|
||||||
本项目是一个专门用于爬取中央主流媒体的Django爬虫系统,支持爬取18家中央主流媒体及其子网站、客户端和新媒体平台。
|
|
||||||
|
|
||||||
## 支持的媒体列表
|
|
||||||
|
|
||||||
### 18家中央主流媒体
|
|
||||||
1. **人民日报** - 人民网、人民日报客户端、人民日报报纸
|
|
||||||
2. **新华社** - 新华网、新华网主站、新华社移动端
|
|
||||||
3. **中央广播电视总台** - 央视网、央视新闻、央视移动端
|
|
||||||
4. **求是** - 求是网、求是移动端
|
|
||||||
5. **解放军报** - 解放军报、解放军报移动端
|
|
||||||
6. **光明日报** - 光明日报、光明日报移动端
|
|
||||||
7. **经济日报** - 经济日报、经济日报移动端
|
|
||||||
8. **中国日报** - 中国日报、中国日报移动端
|
|
||||||
9. **工人日报** - 工人日报、工人日报移动端
|
|
||||||
10. **科技日报** - 科技日报、科技日报移动端
|
|
||||||
11. **人民政协报** - 人民政协报、人民政协报移动端
|
|
||||||
12. **中国纪检监察报** - 中国纪检监察报、中国纪检监察报移动端
|
|
||||||
13. **中国新闻社** - 中国新闻社、中国新闻社移动端
|
|
||||||
14. **学习时报** - 学习时报、学习时报移动端
|
|
||||||
15. **中国青年报** - 中国青年报、中国青年报移动端
|
|
||||||
16. **中国妇女报** - 中国妇女报、中国妇女报移动端
|
|
||||||
17. **法治日报** - 法治日报、法治日报移动端
|
|
||||||
18. **农民日报** - 农民日报、农民日报移动端
|
|
||||||
|
|
||||||
### 特殊平台
|
|
||||||
19. **学习强国** - 中央媒体学习号及省级以上学习平台
|
|
||||||
20. **旗帜网** - 旗帜网及其移动端
|
|
||||||
21. **中国网** - 主网及中国网一省份(不转发二级子网站)
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 单个媒体爬取
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 爬取人民日报所有平台
|
|
||||||
python manage.py crawl_rmrb
|
|
||||||
|
|
||||||
# 爬取人民日报特定平台
|
|
||||||
python manage.py crawl_rmrb --platform peopleapp # 只爬取客户端
|
|
||||||
python manage.py crawl_rmrb --platform people # 只爬取人民网
|
|
||||||
python manage.py crawl_rmrb --platform paper # 只爬取报纸
|
|
||||||
|
|
||||||
# 爬取新华社所有平台
|
|
||||||
python manage.py crawl_xinhua
|
|
||||||
|
|
||||||
# 爬取央视所有平台
|
|
||||||
python manage.py crawl_cctv
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 批量爬取所有媒体
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 爬取所有中央主流媒体
|
|
||||||
python manage.py crawl_all_media
|
|
||||||
|
|
||||||
# 爬取指定媒体
|
|
||||||
python manage.py crawl_all_media --media rmrb,xinhua,cctv
|
|
||||||
|
|
||||||
# 爬取指定平台类型
|
|
||||||
python manage.py crawl_all_media --platform web # 只爬取网站
|
|
||||||
python manage.py crawl_all_media --platform mobile # 只爬取移动端
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 导出文章数据
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 导出所有文章为JSON格式
|
|
||||||
python manage.py export_articles --format json
|
|
||||||
|
|
||||||
# 导出指定网站的文章为CSV格式
|
|
||||||
python manage.py export_articles --format csv --website "人民日报客户端"
|
|
||||||
|
|
||||||
# 导出为Word文档(包含媒体文件)
|
|
||||||
python manage.py export_articles --format docx --include-media
|
|
||||||
|
|
||||||
# 导出为ZIP包(包含文章数据和媒体文件)
|
|
||||||
python manage.py export_articles --format json --include-media
|
|
||||||
```
|
|
||||||
|
|
||||||
## 可用的爬虫命令
|
|
||||||
|
|
||||||
| 命令 | 媒体名称 | 说明 |
|
|
||||||
|------|----------|------|
|
|
||||||
| `crawl_rmrb` | 人民日报 | 爬取人民网、客户端、报纸 |
|
|
||||||
| `crawl_xinhua` | 新华社 | 爬取新华网、主站、移动端 |
|
|
||||||
| `crawl_cctv` | 中央广播电视总台 | 爬取央视网、央视新闻、移动端 |
|
|
||||||
| `crawl_qiushi` | 求是 | 爬取求是网、移动端 |
|
|
||||||
| `crawl_pla` | 解放军报 | 爬取解放军报、移动端 |
|
|
||||||
| `crawl_gmrb` | 光明日报 | 爬取光明日报、移动端 |
|
|
||||||
| `crawl_jjrb` | 经济日报 | 爬取经济日报、移动端 |
|
|
||||||
| `crawl_chinadaily` | 中国日报 | 爬取中国日报、移动端 |
|
|
||||||
| `crawl_grrb` | 工人日报 | 爬取工人日报、移动端 |
|
|
||||||
| `crawl_kjrb` | 科技日报 | 爬取科技日报、移动端 |
|
|
||||||
| `crawl_rmzxb` | 人民政协报 | 爬取人民政协报、移动端 |
|
|
||||||
| `crawl_zgjwjc` | 中国纪检监察报 | 爬取中国纪检监察报、移动端 |
|
|
||||||
| `crawl_chinanews` | 中国新闻社 | 爬取中国新闻社、移动端 |
|
|
||||||
| `crawl_xxsb` | 学习时报 | 爬取学习时报、移动端 |
|
|
||||||
| `crawl_zgqnb` | 中国青年报 | 爬取中国青年报、移动端 |
|
|
||||||
| `crawl_zgfnb` | 中国妇女报 | 爬取中国妇女报、移动端 |
|
|
||||||
| `crawl_fzrb` | 法治日报 | 爬取法治日报、移动端 |
|
|
||||||
| `crawl_nmrb` | 农民日报 | 爬取农民日报、移动端 |
|
|
||||||
| `crawl_xuexi` | 学习强国 | 爬取中央媒体学习号及省级平台 |
|
|
||||||
| `crawl_qizhi` | 旗帜网 | 爬取旗帜网、移动端 |
|
|
||||||
| `crawl_china` | 中国网 | 爬取主网及一省份 |
|
|
||||||
| `crawl_all_media` | 所有媒体 | 批量爬取所有中央主流媒体 |
|
|
||||||
|
|
||||||
## 平台选项
|
|
||||||
|
|
||||||
每个爬虫命令都支持以下平台选项:
|
|
||||||
|
|
||||||
- `all` (默认): 爬取所有平台
|
|
||||||
- `web`: 只爬取网站版本
|
|
||||||
- `mobile`: 只爬取移动端版本
|
|
||||||
- 特定平台: 每个媒体可能有特定的平台选项
|
|
||||||
|
|
||||||
## 数据导出格式
|
|
||||||
|
|
||||||
支持以下导出格式:
|
|
||||||
|
|
||||||
- `json`: JSON格式,便于程序处理
|
|
||||||
- `csv`: CSV格式,便于Excel打开
|
|
||||||
- `docx`: Word文档格式,包含格式化的文章内容
|
|
||||||
|
|
||||||
## 媒体文件处理
|
|
||||||
|
|
||||||
系统会自动下载文章中的图片和视频文件,并保存到本地媒体目录。导出时可以选择是否包含媒体文件。
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **爬取频率**: 建议控制爬取频率,避免对目标网站造成过大压力
|
|
||||||
2. **数据存储**: 爬取的数据会存储在Django数据库中,确保有足够的存储空间
|
|
||||||
3. **网络环境**: 某些网站可能需要特定的网络环境才能访问
|
|
||||||
4. **反爬虫**: 部分网站可能有反爬虫机制,需要适当调整爬取策略
|
|
||||||
|
|
||||||
## 技术特性
|
|
||||||
|
|
||||||
- **智能识别**: 自动识别文章页面和内容区域
|
|
||||||
- **媒体下载**: 自动下载文章中的图片和视频
|
|
||||||
- **去重处理**: 自动避免重复爬取相同文章
|
|
||||||
- **错误处理**: 完善的错误处理和日志记录
|
|
||||||
- **可扩展**: 易于添加新的媒体网站
|
|
||||||
|
|
||||||
## 依赖要求
|
|
||||||
|
|
||||||
- Django 3.0+
|
|
||||||
- requests
|
|
||||||
- beautifulsoup4
|
|
||||||
- python-docx (用于Word导出)
|
|
||||||
- Pillow (用于图片处理)
|
|
||||||
|
|
||||||
## 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## 数据库迁移
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python manage.py makemigrations
|
|
||||||
python manage.py migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行爬虫
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动Django服务器
|
|
||||||
python manage.py runserver
|
|
||||||
|
|
||||||
# 运行爬虫
|
|
||||||
python manage.py crawl_all_media
|
|
||||||
```
|
|
||||||
|
|
||||||
## 查看结果
|
|
||||||
|
|
||||||
爬取完成后,可以通过Django管理界面或导出命令查看爬取的文章数据。
|
|
||||||
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
# 中央主流媒体爬虫系统实现总结
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
本项目成功实现了对18家中央主流媒体及其子网站、客户端、新媒体平台的爬虫系统。系统基于Django框架构建,具有高度的可扩展性和稳定性。
|
|
||||||
|
|
||||||
## 已实现的媒体列表
|
|
||||||
|
|
||||||
### 18家中央主流媒体
|
|
||||||
1. **人民日报** (`crawl_rmrb.py`)
|
|
||||||
- 人民网 (http://www.people.com.cn)
|
|
||||||
- 人民日报客户端 (https://www.peopleapp.com)
|
|
||||||
- 人民日报报纸 (http://paper.people.com.cn)
|
|
||||||
|
|
||||||
2. **新华社** (`crawl_xinhua.py`)
|
|
||||||
- 新华网 (https://www.news.cn)
|
|
||||||
- 新华网主站 (http://www.xinhuanet.com)
|
|
||||||
- 新华社移动端 (https://m.xinhuanet.com)
|
|
||||||
|
|
||||||
3. **中央广播电视总台** (`crawl_cctv.py`)
|
|
||||||
- 央视网 (https://www.cctv.com)
|
|
||||||
- 央视新闻 (https://news.cctv.com)
|
|
||||||
- 央视移动端 (https://m.cctv.com)
|
|
||||||
|
|
||||||
4. **求是** (`crawl_qiushi.py`)
|
|
||||||
- 求是网 (http://www.qstheory.cn)
|
|
||||||
- 求是移动端 (http://m.qstheory.cn)
|
|
||||||
|
|
||||||
5. **解放军报** (`crawl_pla.py`)
|
|
||||||
- 解放军报 (http://www.81.cn)
|
|
||||||
- 解放军报移动端 (http://m.81.cn)
|
|
||||||
|
|
||||||
6. **光明日报** (`crawl_gmrb.py`)
|
|
||||||
- 光明日报 (https://www.gmw.cn)
|
|
||||||
- 光明日报移动端 (https://m.gmw.cn)
|
|
||||||
|
|
||||||
7. **经济日报** (`crawl_jjrb.py`)
|
|
||||||
- 经济日报 (https://www.ce.cn)
|
|
||||||
- 经济日报移动端 (https://m.ce.cn)
|
|
||||||
|
|
||||||
8. **中国日报** (`crawl_chinadaily.py`)
|
|
||||||
- 中国日报 (https://www.chinadaily.com.cn)
|
|
||||||
- 中国日报移动端 (https://m.chinadaily.com.cn)
|
|
||||||
|
|
||||||
9. **工人日报** (`crawl_grrb.py`)
|
|
||||||
- 工人日报 (http://www.workercn.cn)
|
|
||||||
- 工人日报移动端 (http://m.workercn.cn)
|
|
||||||
|
|
||||||
10. **科技日报** (`crawl_kjrb.py`)
|
|
||||||
- 科技日报 (http://digitalpaper.stdaily.com)
|
|
||||||
- 科技日报移动端 (http://m.stdaily.com)
|
|
||||||
|
|
||||||
11. **人民政协报** (`crawl_rmzxb.py`)
|
|
||||||
- 人民政协报 (http://www.rmzxb.com.cn)
|
|
||||||
- 人民政协报移动端 (http://m.rmzxb.com.cn)
|
|
||||||
|
|
||||||
12. **中国纪检监察报** (`crawl_zgjwjc.py`)
|
|
||||||
- 中国纪检监察报 (http://www.jjjcb.cn)
|
|
||||||
- 中国纪检监察报移动端 (http://m.jjjcb.cn)
|
|
||||||
|
|
||||||
13. **中国新闻社** (`crawl_chinanews.py`)
|
|
||||||
- 中国新闻社 (https://www.chinanews.com.cn)
|
|
||||||
- 中国新闻社移动端 (https://m.chinanews.com.cn)
|
|
||||||
|
|
||||||
14. **学习时报** (`crawl_xxsb.py`)
|
|
||||||
- 学习时报 (http://www.studytimes.cn)
|
|
||||||
- 学习时报移动端 (http://m.studytimes.cn)
|
|
||||||
|
|
||||||
15. **中国青年报** (`crawl_zgqnb.py`)
|
|
||||||
- 中国青年报 (https://www.cyol.com)
|
|
||||||
- 中国青年报移动端 (https://m.cyol.com)
|
|
||||||
|
|
||||||
16. **中国妇女报** (`crawl_zgfnb.py`)
|
|
||||||
- 中国妇女报 (http://www.cnwomen.com.cn)
|
|
||||||
- 中国妇女报移动端 (http://m.cnwomen.com.cn)
|
|
||||||
|
|
||||||
17. **法治日报** (`crawl_fzrb.py`)
|
|
||||||
- 法治日报 (http://www.legaldaily.com.cn)
|
|
||||||
- 法治日报移动端 (http://m.legaldaily.com.cn)
|
|
||||||
|
|
||||||
18. **农民日报** (`crawl_nmrb.py`)
|
|
||||||
- 农民日报 (http://www.farmer.com.cn)
|
|
||||||
- 农民日报移动端 (http://m.farmer.com.cn)
|
|
||||||
|
|
||||||
### 特殊平台
|
|
||||||
19. **学习强国** (`crawl_xuexi.py`)
|
|
||||||
- 学习强国主站 (https://www.xuexi.cn)
|
|
||||||
- 中央媒体学习号及省级以上学习平台
|
|
||||||
|
|
||||||
20. **旗帜网** (`crawl_qizhi.py`)
|
|
||||||
- 旗帜网 (http://www.qizhiwang.org.cn)
|
|
||||||
- 旗帜网移动端 (http://m.qizhiwang.org.cn)
|
|
||||||
|
|
||||||
21. **中国网** (`crawl_china.py`)
|
|
||||||
- 中国网主网 (http://www.china.com.cn)
|
|
||||||
- 中国网一省份(不转发二级子网站)
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
### 1. 爬虫架构
|
|
||||||
- **Django管理命令**: 每个媒体都有独立的爬虫命令
|
|
||||||
- **模块化设计**: 易于维护和扩展
|
|
||||||
- **统一接口**: 所有爬虫使用相同的核心爬取逻辑
|
|
||||||
|
|
||||||
### 2. 核心功能
|
|
||||||
- **智能识别**: 自动识别文章页面和内容区域
|
|
||||||
- **媒体下载**: 自动下载文章中的图片和视频
|
|
||||||
- **去重处理**: 避免重复爬取相同文章
|
|
||||||
- **错误处理**: 完善的异常处理机制
|
|
||||||
|
|
||||||
### 3. 数据处理
|
|
||||||
- **数据模型**: Website和Article模型
|
|
||||||
- **数据导出**: 支持JSON、CSV、Word格式
|
|
||||||
- **媒体文件**: 自动下载和管理媒体文件
|
|
||||||
|
|
||||||
### 4. 批量操作
|
|
||||||
- **批量爬取**: `crawl_all_media`命令支持批量爬取
|
|
||||||
- **选择性爬取**: 支持指定特定媒体或平台
|
|
||||||
- **统计功能**: 提供爬取统计信息
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
core/management/commands/
|
|
||||||
├── crawl_rmrb.py # 人民日报爬虫
|
|
||||||
├── crawl_xinhua.py # 新华社爬虫
|
|
||||||
├── crawl_cctv.py # 央视爬虫
|
|
||||||
├── crawl_qiushi.py # 求是爬虫
|
|
||||||
├── crawl_pla.py # 解放军报爬虫
|
|
||||||
├── crawl_gmrb.py # 光明日报爬虫
|
|
||||||
├── crawl_jjrb.py # 经济日报爬虫
|
|
||||||
├── crawl_chinadaily.py # 中国日报爬虫
|
|
||||||
├── crawl_grrb.py # 工人日报爬虫
|
|
||||||
├── crawl_kjrb.py # 科技日报爬虫
|
|
||||||
├── crawl_rmzxb.py # 人民政协报爬虫
|
|
||||||
├── crawl_zgjwjc.py # 中国纪检监察报爬虫
|
|
||||||
├── crawl_chinanews.py # 中国新闻社爬虫
|
|
||||||
├── crawl_xxsb.py # 学习时报爬虫
|
|
||||||
├── crawl_zgqnb.py # 中国青年报爬虫
|
|
||||||
├── crawl_zgfnb.py # 中国妇女报爬虫
|
|
||||||
├── crawl_fzrb.py # 法治日报爬虫
|
|
||||||
├── crawl_nmrb.py # 农民日报爬虫
|
|
||||||
├── crawl_xuexi.py # 学习强国爬虫
|
|
||||||
├── crawl_qizhi.py # 旗帜网爬虫
|
|
||||||
├── crawl_china.py # 中国网爬虫
|
|
||||||
├── crawl_all_media.py # 批量爬取命令
|
|
||||||
└── export_articles.py # 数据导出命令
|
|
||||||
|
|
||||||
core/
|
|
||||||
├── models.py # 数据模型
|
|
||||||
├── utils.py # 核心爬取逻辑
|
|
||||||
└── views.py # 视图函数
|
|
||||||
|
|
||||||
docs/
|
|
||||||
├── CRAWLER_README.md # 使用说明
|
|
||||||
└── IMPLEMENTATION_SUMMARY.md # 实现总结
|
|
||||||
|
|
||||||
test_crawlers.py # 测试脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 单个媒体爬取
|
|
||||||
```bash
|
|
||||||
# 爬取人民日报所有平台
|
|
||||||
python manage.py crawl_rmrb
|
|
||||||
|
|
||||||
# 爬取特定平台
|
|
||||||
python manage.py crawl_rmrb --platform peopleapp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 批量爬取
|
|
||||||
```bash
|
|
||||||
# 爬取所有媒体
|
|
||||||
python manage.py crawl_all_media
|
|
||||||
|
|
||||||
# 爬取指定媒体
|
|
||||||
python manage.py crawl_all_media --media rmrb,xinhua,cctv
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 数据导出
|
|
||||||
```bash
|
|
||||||
# 导出为JSON格式
|
|
||||||
python manage.py export_articles --format json
|
|
||||||
|
|
||||||
# 导出为Word文档
|
|
||||||
python manage.py export_articles --format docx --include-media
|
|
||||||
```
|
|
||||||
|
|
||||||
## 技术特性
|
|
||||||
|
|
||||||
### 1. 智能识别
|
|
||||||
- 针对不同网站的文章结构进行优化
|
|
||||||
- 自动识别标题、内容、图片等元素
|
|
||||||
- 支持多种HTML结构模式
|
|
||||||
|
|
||||||
### 2. 媒体处理
|
|
||||||
- 自动下载文章中的图片和视频
|
|
||||||
- 本地化存储媒体文件
|
|
||||||
- 支持多种媒体格式
|
|
||||||
|
|
||||||
### 3. 数据管理
|
|
||||||
- 去重机制避免重复数据
|
|
||||||
- 支持增量爬取
|
|
||||||
- 完善的数据导出功能
|
|
||||||
|
|
||||||
### 4. 错误处理
|
|
||||||
- 网络异常处理
|
|
||||||
- 解析错误处理
|
|
||||||
- 数据库异常处理
|
|
||||||
|
|
||||||
## 扩展性
|
|
||||||
|
|
||||||
### 1. 添加新媒体
|
|
||||||
- 复制现有爬虫文件
|
|
||||||
- 修改网站配置
|
|
||||||
- 更新核心逻辑(如需要)
|
|
||||||
|
|
||||||
### 2. 自定义爬取逻辑
|
|
||||||
- 在`utils.py`中添加特定网站的处理逻辑
|
|
||||||
- 支持自定义文章识别规则
|
|
||||||
- 支持自定义内容提取规则
|
|
||||||
|
|
||||||
### 3. 数据格式扩展
|
|
||||||
- 支持更多导出格式
|
|
||||||
- 支持自定义数据字段
|
|
||||||
- 支持数据转换和清洗
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
### 1. 并发控制
|
|
||||||
- 控制爬取频率
|
|
||||||
- 避免对目标网站造成压力
|
|
||||||
- 支持断点续爬
|
|
||||||
|
|
||||||
### 2. 资源管理
|
|
||||||
- 内存使用优化
|
|
||||||
- 磁盘空间管理
|
|
||||||
- 网络带宽控制
|
|
||||||
|
|
||||||
### 3. 数据存储
|
|
||||||
- 数据库索引优化
|
|
||||||
- 媒体文件存储优化
|
|
||||||
- 查询性能优化
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
### 1. 网络安全
|
|
||||||
- 使用合适的User-Agent
|
|
||||||
- 控制请求频率
|
|
||||||
- 遵守robots.txt
|
|
||||||
|
|
||||||
### 2. 数据安全
|
|
||||||
- 数据备份机制
|
|
||||||
- 访问权限控制
|
|
||||||
- 敏感信息保护
|
|
||||||
|
|
||||||
## 维护建议
|
|
||||||
|
|
||||||
### 1. 定期更新
|
|
||||||
- 监控网站结构变化
|
|
||||||
- 更新爬取规则
|
|
||||||
- 维护依赖包版本
|
|
||||||
|
|
||||||
### 2. 监控告警
|
|
||||||
- 爬取状态监控
|
|
||||||
- 错误日志分析
|
|
||||||
- 性能指标监控
|
|
||||||
|
|
||||||
### 3. 数据质量
|
|
||||||
- 定期数据验证
|
|
||||||
- 内容质量检查
|
|
||||||
- 数据完整性验证
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
本项目成功实现了对18家中央主流媒体的全面爬取支持,具有以下特点:
|
|
||||||
|
|
||||||
1. **全面覆盖**: 支持所有指定的中央主流媒体
|
|
||||||
2. **技术先进**: 采用现代化的爬虫技术栈
|
|
||||||
3. **易于使用**: 提供简单易用的命令行接口
|
|
||||||
4. **高度可扩展**: 支持快速添加新的媒体网站
|
|
||||||
5. **稳定可靠**: 具备完善的错误处理和恢复机制
|
|
||||||
|
|
||||||
该系统为中央主流媒体的内容采集和分析提供了强有力的技术支撑,可以满足各种应用场景的需求。
|
|
||||||
@@ -297,6 +297,77 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加导出选中文章的操作
|
||||||
|
actions = ['export_selected_articles']
|
||||||
|
|
||||||
|
def export_selected_articles(self, request, queryset):
|
||||||
|
"""
|
||||||
|
导出选中的文章为ZIP文件
|
||||||
|
"""
|
||||||
|
import zipfile
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from io import BytesIO
|
||||||
|
from django.conf import settings
|
||||||
|
import os
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
# 创建内存中的ZIP文件
|
||||||
|
zip_buffer = BytesIO()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
||||||
|
# 为每篇文章创建文件夹并添加内容
|
||||||
|
for article in queryset:
|
||||||
|
# 创建文章文件夹名称
|
||||||
|
article_folder = f"article_{article.id}_{article.title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')}"
|
||||||
|
|
||||||
|
# 创建Word文档
|
||||||
|
doc = Document()
|
||||||
|
doc.add_heading(article.title, 0)
|
||||||
|
|
||||||
|
# 添加文章信息
|
||||||
|
doc.add_paragraph(f"网站: {article.website.name if article.website else ''}")
|
||||||
|
doc.add_paragraph(f"URL: {article.url}")
|
||||||
|
doc.add_paragraph(f"发布时间: {article.pub_date.strftime('%Y-%m-%d %H:%M:%S') if article.pub_date else ''}")
|
||||||
|
doc.add_paragraph(f"创建时间: {article.created_at.strftime('%Y-%m-%d %H:%M:%S') if article.created_at else ''}")
|
||||||
|
|
||||||
|
# 添加内容标题
|
||||||
|
doc.add_heading('内容:', level=1)
|
||||||
|
|
||||||
|
# 处理HTML内容
|
||||||
|
soup = BeautifulSoup(article.content, 'html.parser')
|
||||||
|
content_text = soup.get_text()
|
||||||
|
doc.add_paragraph(content_text)
|
||||||
|
|
||||||
|
# 将Word文档保存到内存中
|
||||||
|
doc_buffer = BytesIO()
|
||||||
|
doc.save(doc_buffer)
|
||||||
|
doc_buffer.seek(0)
|
||||||
|
|
||||||
|
# 将Word文档添加到ZIP文件
|
||||||
|
zip_file.writestr(os.path.join(article_folder, f'{article.title.replace("/", "_")}.docx'), doc_buffer.getvalue())
|
||||||
|
|
||||||
|
# 添加媒体文件到ZIP包
|
||||||
|
if article.media_files:
|
||||||
|
for media_file in article.media_files:
|
||||||
|
try:
|
||||||
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
# 添加文件到ZIP包
|
||||||
|
zip_file.write(full_path, os.path.join(article_folder, 'media', os.path.basename(media_file)))
|
||||||
|
except Exception as e:
|
||||||
|
# 如果添加媒体文件失败,继续处理其他文件
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 创建HttpResponse
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename=selected_articles.zip'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
export_selected_articles.short_description = "导出所选的文章为ZIP"
|
||||||
|
|
||||||
def content_preview(self, obj):
|
def content_preview(self, obj):
|
||||||
"""内容预览"""
|
"""内容预览"""
|
||||||
return obj.content[:100] + '...' if len(obj.content) > 100 else obj.content
|
return obj.content[:100] + '...' if len(obj.content) > 100 else obj.content
|
||||||
@@ -340,44 +411,69 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
actions_column.short_description = '操作'
|
actions_column.short_description = '操作'
|
||||||
|
|
||||||
|
|
||||||
class CrawlerStatusAdmin(admin.ModelAdmin):
|
#class CrawlerStatusAdmin(admin.ModelAdmin):
|
||||||
"""爬虫状态管理"""
|
# """爬虫状态管理"""
|
||||||
change_list_template = 'admin/crawler_status.html'
|
# change_list_template = 'admin/crawler_status.html'
|
||||||
|
#
|
||||||
def changelist_view(self, request, extra_context=None):
|
# def changelist_view(self, request, extra_context=None):
|
||||||
"""爬虫状态视图"""
|
# """爬虫状态视图"""
|
||||||
# 获取分布式爬虫状态
|
# # 获取分布式爬虫状态
|
||||||
nodes = distributed_crawler.get_available_nodes()
|
# nodes = distributed_crawler.get_available_nodes()
|
||||||
node_statuses = []
|
# node_statuses = []
|
||||||
|
#
|
||||||
for node_id in nodes:
|
# for node_id in nodes:
|
||||||
status = distributed_crawler.get_node_status(node_id)
|
# status = distributed_crawler.get_node_status(node_id)
|
||||||
node_statuses.append(status)
|
# node_statuses.append(status)
|
||||||
|
#
|
||||||
# 获取最近的批次
|
# # 获取最近的批次
|
||||||
batches = distributed_crawler.get_all_batches()[:10]
|
# batches = distributed_crawler.get_all_batches()[:10]
|
||||||
|
#
|
||||||
# 获取任务统计
|
# # 获取任务统计
|
||||||
task_stats = {
|
# task_stats = {
|
||||||
'active_tasks': len([n for n in node_statuses if n['active_tasks'] > 0]),
|
# 'active_tasks': len([n for n in node_statuses if n['active_tasks'] > 0]),
|
||||||
'total_nodes': len(nodes),
|
# 'total_nodes': len(nodes),
|
||||||
'total_batches': len(batches),
|
# 'total_batches': len(batches),
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
extra_context = extra_context or {}
|
# extra_context = extra_context or {}
|
||||||
extra_context.update({
|
# extra_context.update({
|
||||||
'nodes': node_statuses,
|
# 'nodes': node_statuses,
|
||||||
'batches': batches,
|
# 'batches': batches,
|
||||||
'task_stats': task_stats,
|
# 'task_stats': task_stats,
|
||||||
})
|
# })
|
||||||
|
#
|
||||||
return super().changelist_view(request, extra_context)
|
# return super().changelist_view(request, extra_context)
|
||||||
|
#
|
||||||
|
|
||||||
# 注册管理类
|
# 注册管理类
|
||||||
admin.site.register(Website, WebsiteAdmin)
|
admin.site.register(Website, WebsiteAdmin)
|
||||||
admin.site.register(Article, ArticleAdmin)
|
admin.site.register(Article, ArticleAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
# 隐藏Celery Results管理功能
|
||||||
|
# 禁用django_celery_results应用的自动注册
|
||||||
|
try:
|
||||||
|
from django_celery_results.models import TaskResult, GroupResult
|
||||||
|
from django_celery_results.admin import TaskResultAdmin, GroupResultAdmin
|
||||||
|
admin.site.unregister(TaskResult)
|
||||||
|
admin.site.unregister(GroupResult)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 隐藏Celery Beat周期任务管理功能
|
||||||
|
# 禁用django_celery_beat应用的自动注册
|
||||||
|
try:
|
||||||
|
from django_celery_beat.models import PeriodicTask, ClockedSchedule, CrontabSchedule, SolarSchedule, IntervalSchedule
|
||||||
|
admin.site.unregister(PeriodicTask)
|
||||||
|
admin.site.unregister(ClockedSchedule)
|
||||||
|
admin.site.unregister(CrontabSchedule)
|
||||||
|
admin.site.unregister(SolarSchedule)
|
||||||
|
admin.site.unregister(IntervalSchedule)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 自定义管理站点标题
|
# 自定义管理站点标题
|
||||||
admin.site.site_header = 'Green Classroom 管理系统'
|
admin.site.site_header = 'Green Classroom 管理系统'
|
||||||
admin.site.site_title = 'Green Classroom'
|
admin.site.site_title = 'Green Classroom'
|
||||||
|
|||||||
@@ -258,6 +258,18 @@
|
|||||||
{% if selected_website and selected_website.id == website.id %}class="active" {% endif %}>{{ website.name }}</a>
|
{% if selected_website and selected_website.id == website.id %}class="active" {% endif %}>{{ website.name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 修改:按媒体类型筛选 -->
|
||||||
|
<div class="filters">
|
||||||
|
<strong>按媒体类型筛选:</strong>
|
||||||
|
<a href="?{% if selected_website %}website={{ selected_website.id }}&{% endif %}{% if search_query %}q={{ search_query }}&{% endif %}media_type=all"
|
||||||
|
{% if not request.GET.media_type or request.GET.media_type == 'all' %}class="active"{% endif %}>全部</a>
|
||||||
|
<a href="?{% if selected_website %}website={{ selected_website.id }}&{% endif %}{% if search_query %}q={{ search_query }}&{% endif %}media_type=text_only"
|
||||||
|
{% if request.GET.media_type == 'text_only' %}class="active"{% endif %}>纯文本</a>
|
||||||
|
<a href="?{% if selected_website %}website={{ selected_website.id }}&{% endif %}{% if search_query %}q={{ search_query }}&{% endif %}media_type=with_images"
|
||||||
|
{% if request.GET.media_type == 'with_images' %}class="active"{% endif %}>图片</a>
|
||||||
|
<a href="?{% if selected_website %}website={{ selected_website.id }}&{% endif %}{% if search_query %}q={{ search_query }}&{% endif %}media_type=with_videos"
|
||||||
|
{% if request.GET.media_type == 'with_videos' %}class="active"{% endif %}>视频</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
@@ -278,6 +290,10 @@
|
|||||||
<button id="exportCsvBtn" class="export-btn" disabled>导出为CSV</button>
|
<button id="exportCsvBtn" class="export-btn" disabled>导出为CSV</button>
|
||||||
<!-- 新增:导出为ZIP包按钮 -->
|
<!-- 新增:导出为ZIP包按钮 -->
|
||||||
<button id="exportZipBtn" class="export-btn" disabled>导出为ZIP包</button>
|
<button id="exportZipBtn" class="export-btn" disabled>导出为ZIP包</button>
|
||||||
|
<!-- 删除:按类型导出按钮 -->
|
||||||
|
<!-- <button id="exportTextOnlyBtn" class="export-btn">导出纯文本</button>
|
||||||
|
<button id="exportWithImagesBtn" class="export-btn">导出含图片</button>
|
||||||
|
<button id="exportWithVideosBtn" class="export-btn">导出含视频</button> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@@ -362,6 +378,10 @@
|
|||||||
// 新增:获取ZIP导出按钮元素
|
// 新增:获取ZIP导出按钮元素
|
||||||
const exportZipBtn = document.getElementById('exportZipBtn');
|
const exportZipBtn = document.getElementById('exportZipBtn');
|
||||||
|
|
||||||
|
// const exportTextOnlyBtn = document.getElementById('exportTextOnlyBtn');
|
||||||
|
// const exportWithImagesBtn = document.getElementById('exportWithImagesBtn');
|
||||||
|
// const exportWithVideosBtn = document.getElementById('exportWithVideosBtn');
|
||||||
|
|
||||||
// 更新导出按钮状态
|
// 更新导出按钮状态
|
||||||
function updateExportButtons() {
|
function updateExportButtons() {
|
||||||
const selectedCount = document.querySelectorAll('.article-checkbox:checked').length;
|
const selectedCount = document.querySelectorAll('.article-checkbox:checked').length;
|
||||||
@@ -505,8 +525,55 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// exportTextOnlyBtn.addEventListener('click', () => {
|
||||||
|
// exportByMediaType('text_only');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// exportWithImagesBtn.addEventListener('click', () => {
|
||||||
|
// exportByMediaType('with_images');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// exportWithVideosBtn.addEventListener('click', () => {
|
||||||
|
// exportByMediaType('with_videos');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// function exportByMediaType(mediaType) {
|
||||||
|
// // 发送POST请求按类型导出文章
|
||||||
|
// fetch('{% url "export_articles_by_type" %}', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// 'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// media_type: mediaType,
|
||||||
|
// format: 'zip'
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// .then(response => {
|
||||||
|
// if (response.ok) {
|
||||||
|
// return response.blob();
|
||||||
|
// }
|
||||||
|
// throw new Error('导出失败');
|
||||||
|
// })
|
||||||
|
// .then(blob => {
|
||||||
|
// const url = window.URL.createObjectURL(blob);
|
||||||
|
// const a = document.createElement('a');
|
||||||
|
// a.href = url;
|
||||||
|
// a.download = `articles_${mediaType}.zip`;
|
||||||
|
// document.body.appendChild(a);
|
||||||
|
// a.click();
|
||||||
|
// window.URL.revokeObjectURL(url);
|
||||||
|
// document.body.removeChild(a);
|
||||||
|
// })
|
||||||
|
// .catch(error => {
|
||||||
|
// alert('导出失败: ' + error);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
// 初始化导出按钮状态
|
// 初始化导出按钮状态
|
||||||
updateExportButtons();
|
updateExportButtons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
24
core/urls.py
24
core/urls.py
@@ -1,24 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views, api
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# 原有视图
|
|
||||||
path('', views.article_list, name='article_list'),
|
path('', views.article_list, name='article_list'),
|
||||||
path('article/<int:article_id>/', views.article_detail, name='article_detail'),
|
path('article/<int:article_id>/', views.article_detail, name='article_detail'),
|
||||||
|
path('run-crawler/', views.run_crawler, name='run_crawler'),
|
||||||
# API接口
|
path('crawler-status/', views.crawler_status, name='crawler_status'),
|
||||||
path('api/health/', api.HealthView.as_view(), name='api_health'),
|
path('pause-crawler/', views.pause_crawler, name='pause_crawler'),
|
||||||
path('api/websites/', api.WebsitesView.as_view(), name='api_websites'),
|
path('export-articles/', views.export_articles, name='export_articles'),
|
||||||
path('api/websites/<int:website_id>/', api.api_website_detail, name='api_website_detail'),
|
path('export-articles-by-type/', views.export_articles_by_type, name='export_articles_by_type'),
|
||||||
path('api/websites/<int:website_id>/crawl/', api.api_crawl_website, name='api_crawl_website'),
|
|
||||||
path('api/articles/', api.api_articles, name='api_articles'),
|
|
||||||
path('api/articles/<int:article_id>/', api.api_article_detail, name='api_article_detail'),
|
|
||||||
path('api/crawler/status/', api.api_crawler_status, name='api_crawler_status'),
|
|
||||||
path('api/crawler/distributed/', api.api_start_distributed_crawl, name='api_start_distributed_crawl'),
|
|
||||||
path('api/crawler/batch/<str:batch_id>/', api.api_batch_status, name='api_batch_status'),
|
|
||||||
path('api/cleanup/', api.api_cleanup_articles, name='api_cleanup_articles'),
|
|
||||||
path('api/stats/', api.api_stats, name='api_stats'),
|
|
||||||
|
|
||||||
# 添加导出文章的URL
|
|
||||||
path('api/export/', api.export_articles, name='export_articles'),
|
|
||||||
]
|
]
|
||||||
221
core/views.py
221
core/views.py
@@ -38,6 +38,26 @@ def article_list(request):
|
|||||||
if search_query:
|
if search_query:
|
||||||
articles = articles.filter(title__icontains=search_query)
|
articles = articles.filter(title__icontains=search_query)
|
||||||
|
|
||||||
|
# 新增:处理媒体类型筛选
|
||||||
|
media_type = request.GET.get('media_type', 'all')
|
||||||
|
if media_type == 'text_only':
|
||||||
|
# 纯文本文章(没有媒体文件)
|
||||||
|
articles = articles.filter(media_files__isnull=True) | articles.filter(media_files=[])
|
||||||
|
elif media_type == 'with_images':
|
||||||
|
# 包含图片的文章
|
||||||
|
articles = articles.filter(media_files__icontains='.jpg') | \
|
||||||
|
articles.filter(media_files__icontains='.jpeg') | \
|
||||||
|
articles.filter(media_files__icontains='.png') | \
|
||||||
|
articles.filter(media_files__icontains='.gif')
|
||||||
|
elif media_type == 'with_videos':
|
||||||
|
# 包含视频的文章
|
||||||
|
articles = articles.filter(media_files__icontains='.mp4') | \
|
||||||
|
articles.filter(media_files__icontains='.avi') | \
|
||||||
|
articles.filter(media_files__icontains='.mov') | \
|
||||||
|
articles.filter(media_files__icontains='.wmv') | \
|
||||||
|
articles.filter(media_files__icontains='.flv') | \
|
||||||
|
articles.filter(media_files__icontains='.webm')
|
||||||
|
|
||||||
# 按创建时间倒序排列
|
# 按创建时间倒序排列
|
||||||
articles = articles.order_by('-created_at')
|
articles = articles.order_by('-created_at')
|
||||||
|
|
||||||
@@ -413,3 +433,204 @@ def export_articles(request):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return HttpResponse(f'导出失败: {str(e)}', status=500)
|
return HttpResponse(f'导出失败: {str(e)}', status=500)
|
||||||
|
|
||||||
|
|
||||||
|
# 新增:按媒体类型导出文章视图
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def export_articles_by_type(request):
|
||||||
|
try:
|
||||||
|
# 解析请求数据
|
||||||
|
data = json.loads(request.body)
|
||||||
|
media_type = data.get('media_type', 'all')
|
||||||
|
format_type = data.get('format', 'zip')
|
||||||
|
|
||||||
|
# 根据媒体类型筛选文章
|
||||||
|
if media_type == 'text_only':
|
||||||
|
# 纯文本文章(没有媒体文件或媒体文件为空)
|
||||||
|
articles = Article.objects.filter(media_files__isnull=True) | Article.objects.filter(media_files=[])
|
||||||
|
elif media_type == 'with_images':
|
||||||
|
# 包含图片的文章
|
||||||
|
articles = Article.objects.filter(media_files__icontains='.jpg') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.jpeg') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.png') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.gif')
|
||||||
|
elif media_type == 'with_videos':
|
||||||
|
# 包含视频的文章
|
||||||
|
articles = Article.objects.filter(media_files__icontains='.mp4') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.avi') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.mov') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.wmv') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.flv') | \
|
||||||
|
Article.objects.filter(media_files__icontains='.webm')
|
||||||
|
else:
|
||||||
|
# 所有文章
|
||||||
|
articles = Article.objects.all()
|
||||||
|
|
||||||
|
# 去重处理
|
||||||
|
articles = articles.distinct()
|
||||||
|
|
||||||
|
if not articles.exists():
|
||||||
|
return HttpResponse('没有符合条件的文章', status=400)
|
||||||
|
|
||||||
|
# 导出为ZIP格式
|
||||||
|
if format_type == 'zip':
|
||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
|
from django.conf import settings
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 创建内存中的ZIP文件
|
||||||
|
zip_buffer = BytesIO()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
||||||
|
# 为每篇文章创建Word文档并添加到ZIP文件中
|
||||||
|
for article in articles:
|
||||||
|
# 为每篇文章创建单独的文件夹
|
||||||
|
article_folder = f"article_{article.id}_{article.title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')}"
|
||||||
|
|
||||||
|
# 创建文章数据
|
||||||
|
article_data = {
|
||||||
|
'id': article.id,
|
||||||
|
'title': article.title,
|
||||||
|
'website': article.website.name,
|
||||||
|
'url': article.url,
|
||||||
|
'pub_date': article.pub_date.strftime('%Y-%m-%d %H:%M:%S') if article.pub_date else None,
|
||||||
|
'content': article.content,
|
||||||
|
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'media_files': article.media_files
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将文章数据保存为Word文件并添加到ZIP
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Inches
|
||||||
|
from io import BytesIO
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 创建Word文档
|
||||||
|
doc = Document()
|
||||||
|
doc.add_heading(article.title, 0)
|
||||||
|
|
||||||
|
# 添加文章元数据
|
||||||
|
doc.add_paragraph(f"网站: {article.website.name}")
|
||||||
|
doc.add_paragraph(f"URL: {article.url}")
|
||||||
|
doc.add_paragraph(
|
||||||
|
f"发布时间: {article.pub_date.strftime('%Y-%m-%d %H:%M:%S') if article.pub_date else 'N/A'}")
|
||||||
|
doc.add_paragraph(f"创建时间: {article.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# 添加文章内容
|
||||||
|
doc.add_heading('内容', level=1)
|
||||||
|
|
||||||
|
# 处理HTML内容
|
||||||
|
soup = BeautifulSoup(article.content, 'html.parser')
|
||||||
|
|
||||||
|
# 处理内容中的图片
|
||||||
|
for img in soup.find_all('img'):
|
||||||
|
src = img.get('src', '')
|
||||||
|
if src:
|
||||||
|
try:
|
||||||
|
# 构建完整的图片路径
|
||||||
|
if src.startswith('http'):
|
||||||
|
# 网络图片
|
||||||
|
response = requests.get(src, timeout=10)
|
||||||
|
image_stream = BytesIO(response.content)
|
||||||
|
doc.add_picture(image_stream, width=Inches(4.0))
|
||||||
|
else:
|
||||||
|
# 本地图片
|
||||||
|
full_path = os.path.join(settings.MEDIA_ROOT, src.lstrip('/'))
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
doc.add_picture(full_path, width=Inches(4.0))
|
||||||
|
except Exception as e:
|
||||||
|
# 如果添加图片失败,添加图片URL作为文本
|
||||||
|
doc.add_paragraph(f"[图片: {src}]")
|
||||||
|
|
||||||
|
# 移除原始img标签
|
||||||
|
img.decompose()
|
||||||
|
|
||||||
|
content_text = soup.get_text()
|
||||||
|
doc.add_paragraph(content_text)
|
||||||
|
|
||||||
|
# 添加媒体文件信息
|
||||||
|
if article.media_files:
|
||||||
|
doc.add_heading('媒体文件', level=1)
|
||||||
|
for media_file in article.media_files:
|
||||||
|
try:
|
||||||
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
# 检查文件扩展名以确定处理方式
|
||||||
|
file_extension = os.path.splitext(media_file)[1].lower()
|
||||||
|
|
||||||
|
# 图片文件处理
|
||||||
|
if file_extension in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']:
|
||||||
|
doc.add_picture(full_path, width=Inches(4.0))
|
||||||
|
# 视频文件处理
|
||||||
|
elif file_extension in ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm']:
|
||||||
|
doc.add_paragraph(f"[视频文件: {media_file}]")
|
||||||
|
# 其他文件类型
|
||||||
|
else:
|
||||||
|
doc.add_paragraph(f"[文件: {media_file}]")
|
||||||
|
else:
|
||||||
|
# 如果是URL格式的媒体文件
|
||||||
|
if media_file.startswith('http'):
|
||||||
|
response = requests.get(media_file, timeout=10)
|
||||||
|
file_extension = os.path.splitext(media_file)[1].lower()
|
||||||
|
|
||||||
|
# 图片文件处理
|
||||||
|
if file_extension in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']:
|
||||||
|
image_stream = BytesIO(response.content)
|
||||||
|
doc.add_picture(image_stream, width=Inches(4.0))
|
||||||
|
else:
|
||||||
|
doc.add_paragraph(f"[文件: {media_file}]")
|
||||||
|
else:
|
||||||
|
doc.add_paragraph(media_file)
|
||||||
|
except Exception as e:
|
||||||
|
doc.add_paragraph(media_file)
|
||||||
|
|
||||||
|
# 保存Word文档到内存
|
||||||
|
doc_buffer = BytesIO()
|
||||||
|
doc.save(doc_buffer)
|
||||||
|
doc_buffer.seek(0)
|
||||||
|
|
||||||
|
# 将Word文档添加到ZIP包
|
||||||
|
zip_file.writestr(os.path.join(article_folder, f'{article.title.replace("/", "_")}.docx'),
|
||||||
|
doc_buffer.read())
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# 如果没有安装python-docx库,回退到JSON格式
|
||||||
|
json_data = json.dumps(article_data, ensure_ascii=False, indent=2)
|
||||||
|
zip_file.writestr(os.path.join(article_folder, f'{article.title.replace("/", "_")}.json'),
|
||||||
|
json_data)
|
||||||
|
|
||||||
|
# 添加媒体文件到ZIP包
|
||||||
|
if article.media_files:
|
||||||
|
for media_file in article.media_files:
|
||||||
|
try:
|
||||||
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
# 添加文件到ZIP包
|
||||||
|
zip_file.write(full_path, os.path.join(article_folder, 'media', media_file))
|
||||||
|
else:
|
||||||
|
# 如果是URL格式的媒体文件
|
||||||
|
if media_file.startswith('http'):
|
||||||
|
import requests
|
||||||
|
response = requests.get(media_file, timeout=10)
|
||||||
|
zip_file.writestr(
|
||||||
|
os.path.join(article_folder, 'media', os.path.basename(media_file)),
|
||||||
|
response.content)
|
||||||
|
except Exception as e:
|
||||||
|
# 如果添加媒体文件失败,继续处理其他文件
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 创建HttpResponse
|
||||||
|
zip_buffer.seek(0)
|
||||||
|
response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip')
|
||||||
|
response['Content-Disposition'] = f'attachment; filename=articles_{media_type}.zip'
|
||||||
|
return response
|
||||||
|
|
||||||
|
else:
|
||||||
|
return HttpResponse('不支持的格式', status=400)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponse(f'导出失败: {str(e)}', status=500)
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-_kr!&5j#i!)lo(=u-&5ni+21cw
|
|||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
||||||
|
|
||||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1,192.168.9.108,green.yuangyaa.com').split(',')
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'https://green.yuangyaa.com').split(',')
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@@ -153,8 +155,8 @@ MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, 'data', 'media'))
|
|||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
# Celery配置
|
# Celery配置
|
||||||
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://127.0.0.1:6379/0')
|
||||||
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://127.0.0.1:6379/0')
|
||||||
CELERY_ACCEPT_CONTENT = ['json']
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
CELERY_TASK_SERIALIZER = 'json'
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
CELERY_RESULT_SERIALIZER = 'json'
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
@@ -163,7 +165,7 @@ CELERY_TASK_TRACK_STARTED = True
|
|||||||
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30分钟
|
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30分钟
|
||||||
|
|
||||||
# Redis配置
|
# Redis配置
|
||||||
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
REDIS_URL = os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/0')
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
@@ -255,3 +257,7 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ djangorestframework==3.16.1
|
|||||||
executing==2.2.0
|
executing==2.2.0
|
||||||
factory_boy==3.3.3
|
factory_boy==3.3.3
|
||||||
Faker==37.5.3
|
Faker==37.5.3
|
||||||
|
greenlet==3.2.4
|
||||||
|
gunicorn==23.0.0
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
iniconfig==2.1.0
|
iniconfig==2.1.0
|
||||||
@@ -73,6 +75,7 @@ typing_extensions==4.14.1
|
|||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
uv==0.8.8
|
uv==0.8.8
|
||||||
|
uvicorn==0.35.0
|
||||||
vine==5.1.0
|
vine==5.1.0
|
||||||
wcwidth==0.2.13
|
wcwidth==0.2.13
|
||||||
webdriver-manager==4.0.2
|
webdriver-manager==4.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user