├── .vscode └── settings.json ├── LICENSE ├── README.md ├── backend ├── Dockerfile ├── README.md ├── __pycache__ │ └── manage.cpython-36.pyc ├── flask_nlp │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── config.cpython-36.pyc │ │ └── models.cpython-36.pyc │ ├── api │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-36.pyc │ │ │ ├── article.cpython-36.pyc │ │ │ ├── post.cpython-36.pyc │ │ │ ├── text_generate.cpython-36.pyc │ │ │ ├── text_process.cpython-36.pyc │ │ │ └── utils.cpython-36.pyc │ │ ├── text_generate.py │ │ ├── text_process.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-36.pyc │ │ │ ├── function.cpython-36.pyc │ │ │ └── variable.cpython-36.pyc │ │ │ ├── function.py │ │ │ └── variable.py │ ├── config.py │ ├── data-dev.sqlite │ ├── models.py │ └── spider │ │ ├── __pycache__ │ │ └── spider.cpython-36.pyc │ │ └── spider.py ├── manage.py ├── migrations │ ├── README │ ├── __pycache__ │ │ └── env.cpython-36.pyc │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 281e32ecd391_v1_0.py │ │ ├── 599c9cca170b_v1_0.py │ │ ├── __pycache__ │ │ ├── 281e32ecd391_v1_0.cpython-36.pyc │ │ ├── 599c9cca170b_v1_0.cpython-36.pyc │ │ └── a04c5a468b8a_v1_0.cpython-36.pyc │ │ └── a04c5a468b8a_v1_0.py └── requirements.txt ├── docker-compose.yml ├── frontend ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── dist │ ├── css │ │ └── app.a3438fe3.css │ ├── favicon.ico │ ├── fonts │ │ ├── element-icons.535877f5.woff │ │ └── element-icons.732389de.ttf │ ├── index.html │ └── js │ │ ├── app.245c7112.js │ │ ├── app.245c7112.js.map │ │ ├── chunk-vendors.16c1783a.js │ │ └── chunk-vendors.16c1783a.js.map ├── nginx │ └── default.conf ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── common │ │ ├── config.json │ │ └── theme │ │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ │ ├── github_corner.css │ │ │ └── index.css │ ├── components │ │ ├── Alert.vue │ │ ├── BookDetail.vue │ │ ├── Books.vue │ │ ├── ChapterDetail.vue │ │ └── HelloWorld.vue │ ├── main.js │ └── router.js └── static │ └── wordcloud2.js └── pics ├── api结果示例1.png ├── api结果示例2.png ├── api结果示例3.png ├── 书籍.png ├── 分词.png ├── 命名实体识别.png ├── 基础.png ├── 屏蔽.png ├── 情感识别.png ├── 架构图.png ├── 词云1.png ├── 词云2.png ├── 词云3.png ├── 词云4.png ├── 词性标注.png └── 首页.png /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "D:\\Web_Env\\flask-vuejs-nlp\\Scripts\\python.exe", 3 | "python.linting.enabled": false, 4 | "files.associations": { 5 | "algorithm": "cpp", 6 | "cmath": "cpp", 7 | "cstddef": "cpp", 8 | "cstdint": "cpp", 9 | "cstdio": "cpp", 10 | "cstdlib": "cpp", 11 | "cstring": "cpp", 12 | "cwchar": "cpp", 13 | "deque": "cpp", 14 | "exception": "cpp", 15 | "initializer_list": "cpp", 16 | "ios": "cpp", 17 | "iosfwd": "cpp", 18 | "iostream": "cpp", 19 | "istream": "cpp", 20 | "limits": "cpp", 21 | "memory": "cpp", 22 | "new": "cpp", 23 | "ostream": "cpp", 24 | "queue": "cpp", 25 | "stdexcept": "cpp", 26 | "streambuf": "cpp", 27 | "string": "cpp", 28 | "system_error": "cpp", 29 | "tuple": "cpp", 30 | "type_traits": "cpp", 31 | "typeinfo": "cpp", 32 | "utility": "cpp", 33 | "vector": "cpp", 34 | "xfacet": "cpp", 35 | "xiosbase": "cpp", 36 | "xlocale": "cpp", 37 | "xlocinfo": "cpp", 38 | "xlocnum": "cpp", 39 | "xmemory": "cpp", 40 | "xmemory0": "cpp", 41 | "xstddef": "cpp", 42 | "xstring": "cpp", 43 | "xtr1common": "cpp", 44 | "xutility": "cpp" 45 | } 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CYQ1999 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-vuejs-nlp 2 | 本项目是我大数据的大作业,要求是在网站里实现nlp的一些功能。 3 | 前端:vue, Jquery, elment-ui 4 | 后端:flask 5 | 部署:docker-compose 6 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E6%9E%B6%E6%9E%84%E5%9B%BE.png) 7 | 8 | ## api展示 9 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/api%E7%BB%93%E6%9E%9C%E7%A4%BA%E4%BE%8B1.png) 10 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/api%E7%BB%93%E6%9E%9C%E7%A4%BA%E4%BE%8B2.png) 11 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/api%E7%BB%93%E6%9E%9C%E7%A4%BA%E4%BE%8B3.png) 12 | ## 页面展示 13 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E9%A6%96%E9%A1%B5.png) 14 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E4%B9%A6%E7%B1%8D.png) 15 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E5%9F%BA%E7%A1%80.png) 16 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E5%B1%8F%E8%94%BD.png) 17 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E5%88%86%E8%AF%8D.png) 18 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E5%91%BD%E5%90%8D%E5%AE%9E%E4%BD%93%E8%AF%86%E5%88%AB.png) 19 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E4%BA%911.png) 20 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E4%BA%912.png) 21 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E4%BA%913.png) 22 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E4%BA%914.png) 23 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E6%80%A7%E6%A0%87%E6%B3%A8.png) 24 | ![image](https://github.com/keleqnma/flask-vuejs-nlp/blob/master/pics/%E8%AF%8D%E6%80%A7%E6%A0%87%E6%B3%A8.png) 25 | 26 | ## Docker部署 27 | ``` 28 | git clone https://github.com/keleqnma/flask-vuejs-nlp 29 | cd flask-vuejs-nlp 30 | docker-compose up-d 31 | ``` 32 | 打开 http://localhost:3000/ 查看前端页面,后端的接口和开发环境一样,请看下面的测试api部分 33 | ## 开发环境运行 34 | ``` 35 | git clone https://github.com/keleqnma/flask-vuejs-nlp 36 | cd flask-vuejs-nlp 37 | ``` 38 | ### Backend 39 | #### 项目准备工作 40 | 41 | ```python 42 | #建立新虚拟环境(conda也可以,我用的是virtualenv) 43 | pip install virtualenvwrapper#虚拟环境管理包 44 | virtualenv flask-vuejs-nlp 45 | 46 | #进入激活虚拟环境 47 | #1.如果安装virtualenvwrapper 48 | workon flask-vuejs-nlp 49 | #2.如果没安装virtualenvwrapper,使用虚拟环境的激活脚本 50 | \path\to\env\Scripts\activate 51 | 52 | #安装依赖包 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | ##### 迁移更新数据库 57 | ```python 58 | python manage.py db migrate -m "v1.0" 59 | python manage.py db upgrade 60 | ``` 61 | 62 | ##### 运行 63 | 64 | ``` 65 | python manage.py runserver --host 0.0.0.1 66 | ``` 67 | 68 | ##### 测试api 69 | 70 | | http方法 | api列表 | 描述 | 71 | | -------- | ------------------------------------------------------------ | -------------------------- | 72 | | GET | localhost:5000/cpNlp/api/v1.0/books | 获取缓存小说列表随机十本书 | 73 | | GET | localhost:5000/cpNlp/api/v1.0/books/[keyword] | 通过关键字获取小说列表 | 74 | | GET | localhost:5000/cpNlp/api/v1.0/books/[int:book_id] | 获取某本小说的详细信息 | 75 | | GET | localhost:5000/cpNlp/api/v1.0/chapters/[int:book_id] | 获取某本小说的章节列表 | 76 | | GET | localhost:5000/cpNlp/api/v1.0/chaptercontent/[int:chapter_id] | 获取某个章节 | 77 | | GET | localhost:5000/cpNlp/api/v1.0/process/segcontent/[int:chapter_id] | 某个章节的分词结果 | 78 | | GET | localhost:5000/cpNlp/api/v1.0/process/postagcontentseg/[int:chapter_id] | 某个章节的词性标注 | 79 | | GET | localhost:5000/cpNlp/api/v1.0/process/nercontent/[int:chapter_id]/ | 某个章节的命名实体识别 | 80 | | GET | localhost:5000/cpNlp/api/v1.0/process/senticontent/[int:chapter_id]/ | 某个章节的情感分析 | 81 | | GET | localhost:5000/cpNlp/api/v1.0/process/wordcloud/[int:chapter_id] | 某个章节的词云展示 | 82 | 83 | ### Frontend 84 | 85 | #### 项目安装 86 | 87 | ``` 88 | npm install 89 | ``` 90 | 91 | ##### 开发选项(一般用这个) 92 | 93 | ``` 94 | npm run serve 95 | ``` 96 | 97 | ##### 部署选项 98 | 99 | ``` 100 | npm run build 101 | ``` 102 | 103 | ##### 测试 104 | 105 | ``` 106 | npm run test 107 | ``` 108 | 109 | ##### 自动修正 110 | 111 | ``` 112 | npm run lint 113 | ``` 114 | 115 | ##### 个性化设置 116 | 117 | 参考 [Configuration Reference](https://cli.vuejs.org/config/). 118 | 119 | ##### 查看结果 120 | 121 | 打开 http://localhost:8080 查看 -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.7 2 | WORKDIR /Project/demo 3 | RUN pip install --upgrade pip 4 | COPY requirements.txt ./ 5 | RUN pip install -r requirements.txt 6 | 7 | COPY . . 8 | 9 | EXPOSE 5000 10 | CMD ["python", "manage.py", "runserver", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | ## 项目准备工作 4 | ```python 5 | #建立新虚拟环境(conda也可以,我用的是virtualenv) 6 | pip install virtualenvwrapper#虚拟环境管理包 7 | virtualenv flask-vuejs-nlp 8 | 9 | #进入激活虚拟环境 10 | #1.如果安装virtualenvwrapper 11 | workon flask-vuejs-nlp 12 | #2.如果没安装virtualenvwrapper,使用虚拟环境的激活脚本 13 | \path\to\env\Scripts\activate 14 | 15 | #安装依赖包 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | ### 迁移更新数据库 20 | ```python 21 | python manage.py db migrate -m "v1.0" 22 | python manage.py db upgrade 23 | ``` 24 | 25 | ### 运行 26 | ``` 27 | python manage.py runserver --host 0.0.0.1 28 | ``` 29 | 30 | ### 测试api 31 | | http方法 | api列表 | 描述 | 32 | | -------- | ------------------------------------------------------------ | -------------------------- | 33 | | GET | localhost:5000/cpNlp/api/v1.0/books | 获取缓存小说列表随机十本书 | 34 | | GET | localhost:5000/cpNlp/api/v1.0/books/[keyword] | 通过关键字获取小说列表 | 35 | | GET | localhost:5000/cpNlp/api/v1.0/books/[int:book_id] | 获取某本小说的详细信息 | 36 | | GET | localhost:5000/cpNlp/api/v1.0/chapters/[int:book_id] | 获取某本小说的章节列表 | 37 | | GET | localhost:5000/cpNlp/api/v1.0/chaptercontent/[int:chapter_id] | 获取某个章节 | 38 | | GET | localhost:5000/cpNlp/api/v1.0/process/segcontent/[int:chapter_id] | 某个章节的分词结果 | 39 | | GET | localhost:5000/cpNlp/api/v1.0/process/postagcontentseg/[int:chapter_id] | 某个章节的词性标注 | 40 | | GET | localhost:5000/cpNlp/api/v1.0/process/nercontent/[int:chapter_id]/ | 某个章节的命名实体识别 | 41 | | GET | localhost:5000/cpNlp/api/v1.0/process/senticontent/[int:chapter_id]/ | 某个章节的情感分析 | 42 | | GET | localhost:5000/cpNlp/api/v1.0/process/wordcloud/[int:chapter_id] | 某个章节的词云展示 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /backend/__pycache__/manage.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/__pycache__/manage.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from sqlalchemy import MetaData 5 | from flask_cors import CORS 6 | from flask_nlp.config import config 7 | 8 | naming_convention = { 9 | "ix": 'ix_%(column_0_label)s', 10 | "uq": "uq_%(table_name)s_%(column_0_name)s", 11 | "ck": "ck_%(table_name)s_%(column_0_name)s", 12 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 13 | "pk": "pk_%(table_name)s" 14 | } 15 | 16 | db = SQLAlchemy(metadata=MetaData(naming_convention=naming_convention)) 17 | 18 | 19 | def create_app(config_name): 20 | app = Flask(__name__, 21 | static_folder="../dist/static", 22 | template_folder="../dist") 23 | cors = CORS(app, resources={r"/cpNlp/api/v1.0/*": {"origins": "*"}}) 24 | # 我们使用app.config对象提供的from_object()方法导入配置 25 | app.config.from_object(config[config_name]) 26 | # 注册数据库实例 27 | 28 | db.init_app(app) 29 | # 构造蓝本 30 | 31 | if not app.debug and not app.testing and not app.config['SSL_DISABLE']: 32 | from flask_sslify import SSLify 33 | sslify = SSLify(app) 34 | 35 | from .api.text_generate import api as api_blueprint 36 | app.register_blueprint(api_blueprint, url_prefix='/cpNlp/api/v1.0') 37 | 38 | from .api.text_process import api as api_process_blueprint 39 | app.register_blueprint(api_process_blueprint, 40 | url_prefix='/cpNlp/api/v1.0/process') 41 | 42 | return app -------------------------------------------------------------------------------- /backend/flask_nlp/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/__pycache__/config.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/__pycache__/config.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/__pycache__/models.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/__pycache__/models.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__init__.py -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/article.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/article.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/post.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/post.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/text_generate.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/text_generate.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/text_process.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/text_process.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/__pycache__/utils.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/__pycache__/utils.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/text_generate.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import json 3 | import random 4 | from flask.blueprints import Blueprint 5 | from flask import Flask, request, jsonify 6 | from .utils.function import add_novel, add_chapter 7 | from ..models import * 8 | from flask_nlp import db 9 | from ..spider.spider import CpSpider 10 | 11 | api = Blueprint('api', __name__) 12 | 13 | spider = CpSpider() 14 | 15 | 16 | #随机抽取十本小说放在首页 17 | @api.route('/books', methods=['GET']) 18 | def book_random(): 19 | rand = random.randrange(0, Novel.query.count() - 10) 20 | books = Novel.query.filter(Novel.id >= rand).limit(10).all() 21 | return jsonify({'books': [book.to_json() for book in books]}), 200 22 | 23 | 24 | @api.route('/books/', methods=['GET']) 25 | def book_one(book_id): 26 | book = Novel.query.filter_by(id=book_id).first() 27 | return jsonify(book.to_json()), 200 28 | 29 | 30 | @api.route('/books/keyword/', methods=['GET', 'POST']) 31 | def book_list(keyword): 32 | books = Novel.query.filter_by(keyword=keyword).all() 33 | if books: 34 | return jsonify({'books': [book.to_json() for book in books]}), 200 35 | 36 | for data in spider.get_index_result(keyword, page=0): 37 | add_novel(data, keyword) 38 | 39 | books = Novel.query.filter_by(keyword=keyword).all() 40 | return jsonify({'books': [book.to_json() for book in books]}), 200 41 | 42 | 43 | @api.route('/chapters/', methods=['GET', 'POST']) 44 | def chapter_list(book_id): 45 | chapters = Chapter.query.filter_by(book_id=book_id).all() 46 | 47 | if chapters == []: 48 | 49 | book = Novel.query.filter_by(id=book_id).first() 50 | for data in spider.get_chapter(book.book_url): 51 | add_chapter(data, book_id) 52 | 53 | chapters = Chapter.query.filter_by(book_id=book_id).all() 54 | 55 | return jsonify({'chapters': 56 | [chapter.to_json() for chapter in chapters]}), 200 57 | 58 | 59 | @api.route('/chaptercontent/') 60 | def contentlist(chapter_id): 61 | content = Content.query.filter_by(chapter_id=chapter_id).first() 62 | if content: 63 | return jsonify(content.to_json()), 200 64 | 65 | chapter = Chapter.query.filter_by(id=chapter_id).first() 66 | content = Content(content=spider.get_content(chapter.chapter_url), 67 | chapter_id=chapter_id) 68 | db.session.add(content) 69 | return jsonify(content.to_json()), 200 70 | -------------------------------------------------------------------------------- /backend/flask_nlp/api/text_process.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import json 3 | from flask.blueprints import Blueprint 4 | from flask import Flask, request, jsonify 5 | from ..models import * 6 | # import pkuseg 7 | import jiagu 8 | from .utils.function import divide_sentence 9 | from .utils.variable import post_dict, ner_dict, stop_words 10 | from .text_generate import contentlist 11 | 12 | api = Blueprint('api_process', __name__) 13 | 14 | 15 | #分词结果 16 | @api.route('/segcontent/') 17 | def contentseg(chapter_id): 18 | contents_sentences = getContentSentence(chapter_id) 19 | 20 | if WordSeg.query.filter_by( 21 | sentence_id=contents_sentences[0].id).all() == []: 22 | #切割句子 23 | for contents_sentence in contents_sentences: 24 | #wordsegs = pkuseg.pkuseg().cut(contents_sentence.sentenceseg) 25 | wordsegs = jiagu.seg(contents_sentence.sentenceseg) 26 | for word in wordsegs: 27 | wordseg = WordSeg(wordseg=word, 28 | sentence_id=contents_sentence.id) 29 | db.session.add(wordseg) 30 | 31 | wordsegss = [[word.wordseg for word in contents_sentence.words] 32 | for contents_sentence in contents_sentences] 33 | return jsonify({'words': wordsegss}), 200 34 | 35 | 36 | # 情感分析 37 | @api.route('/senticontent/') 38 | def senticontent(chapter_id): 39 | contents_sentences = getContentSentence(chapter_id) 40 | 41 | if SentiContent.query.filter_by( 42 | sentence_id=contents_sentences[0].id).all() == []: 43 | for i, contents_sentence in enumerate(contents_sentences): 44 | sentiment = jiagu.sentiment(contents_sentence.sentenceseg) 45 | senticontent = SentiContent(senti=sentiment[0], 46 | degree=sentiment[1], 47 | sentence_id=contents_sentence.id) 48 | db.session.add(senticontent) 49 | 50 | senticontents = [{ 51 | 'sentence': contents_sentence.sentenceseg, 52 | 'sentiment': senticontent.senti, 53 | 'degree': senticontent.degree 54 | } for contents_sentence in contents_sentences 55 | for senticontent in contents_sentence.senti] 56 | 57 | return jsonify({'sentiments': senticontents}), 200 58 | 59 | 60 | @api.route('/postagcontentseg/') 61 | def contentpostagseg(chapter_id): 62 | wordsegss = getContentSeg(chapter_id) 63 | 64 | if PostWordSeg.query.filter_by(word_id=wordsegss[0][0].id).all() == []: 65 | for wordsegs in wordsegss: 66 | poss = jiagu.pos([wordseg.wordseg for wordseg in wordsegs]) # 词性标注 67 | for i, pos in enumerate(poss): 68 | wordseg = PostWordSeg(postag=post_dict[pos], 69 | word_id=wordsegs[i].id) 70 | db.session.add(wordseg) 71 | 72 | tags = [[ 73 | PostWordSeg.query.filter_by(word_id=wordseg.id).first().postag 74 | for wordseg in wordsegs 75 | ] for wordsegs in wordsegss] 76 | 77 | return jsonify({'tags': tags}), 200 78 | 79 | 80 | @api.route('/wordcloud/') 81 | def wordcloud(chapter_id): 82 | wordsegss = getContentSeg(chapter_id) 83 | words = [wordseg.wordseg for wordsegs in wordsegss for wordseg in wordsegs] 84 | words_set = set(words) 85 | res = [] 86 | for word in words_set: 87 | if word not in stop_words: 88 | count = words.count(word) 89 | res.append({'word': word, 'count': count}) 90 | 91 | res.sort(key=lambda x: x['count'], reverse=True) 92 | # 排序 做到词频高的在中间 93 | # 只取前两千个词 94 | if len(res) > 2000: 95 | res = res[0:2000] 96 | return jsonify(res), 200 97 | 98 | 99 | @api.route('/nercontent/') 100 | def nercontent(chapter_id): 101 | wordsegss = getContentSeg(chapter_id) 102 | 103 | if NerWordSeg.query.filter_by(word_id=wordsegss[0][0].id).all() == []: 104 | for wordsegs in wordsegss: 105 | ners = jiagu.ner([wordseg.wordseg for wordseg in wordsegs]) # 词性标注 106 | for i, ner in enumerate(ners): 107 | wordseg = NerWordSeg(nertag=ner_dict[ner], 108 | word_id=wordsegs[i].id) 109 | db.session.add(wordseg) 110 | 111 | ners = [[ 112 | NerWordSeg.query.filter_by(word_id=wordseg.id).first().nertag 113 | for wordseg in wordsegs 114 | ] for wordsegs in wordsegss] 115 | 116 | return jsonify({'ners': ners}), 200 117 | 118 | 119 | #获取内容 120 | def getContent(chapter_id): 121 | content = Content.query.filter_by(chapter_id=chapter_id).first() 122 | if content: 123 | return content.content 124 | 125 | contentlist(chapter_id) 126 | content = Content.query.filter_by(chapter_id=chapter_id).first().content 127 | return content 128 | 129 | 130 | #获取章节分词结果 131 | def getContentSeg(chapter_id): 132 | contents_sentences = getContentSentence(chapter_id) 133 | 134 | if WordSeg.query.filter_by( 135 | sentence_id=contents_sentences[0].id).all() == []: 136 | contentseg(chapter_id) 137 | 138 | wordsegss = [[word for word in contents_sentence.words] 139 | for contents_sentence in contents_sentences] 140 | return wordsegss 141 | 142 | 143 | #获取章节分句结果 144 | def getContentSentence(chapter_id): 145 | contents_sentences = SentenceSeg.query.filter_by( 146 | chapter_id=chapter_id).all() 147 | 148 | if contents_sentences == []: 149 | content = getContent(chapter_id) 150 | contents_sentences = divide_sentence(content) 151 | 152 | for contents_sentence in contents_sentences: 153 | sentenceSeg = SentenceSeg(sentenceseg=contents_sentence, 154 | chapter_id=chapter_id) 155 | db.session.add(sentenceSeg) 156 | contents_sentences = SentenceSeg.query.filter_by( 157 | chapter_id=chapter_id).all() 158 | 159 | return contents_sentences 160 | -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/utils/__init__.py -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/utils/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/__pycache__/function.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/utils/__pycache__/function.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/__pycache__/variable.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/api/utils/__pycache__/variable.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/function.py: -------------------------------------------------------------------------------- 1 | from flask_nlp import db 2 | from flask_nlp.models import * 3 | from flask import make_response 4 | from wordcloud import WordCloud, STOPWORDS 5 | import base64 6 | from io import BytesIO 7 | import re 8 | import os 9 | 10 | JSON_MIME_TYPE = 'application/json' 11 | 12 | 13 | #分句 14 | def divide_sentence(content): 15 | content = re.split('(。|!|\!|\.|?|\?)', content) # 简单分句,保留分割符 16 | last = '' 17 | if len(content) % 2 != 0: 18 | last = content[-1] 19 | content = [ 20 | content[2 * i] + content[2 * i + 1] 21 | for i in range(int(len(content) / 2)) 22 | ] # 将分隔符与句子拼接 23 | if last != '': 24 | content.append(last) 25 | 26 | return content 27 | 28 | 29 | def add_novel(data, keyword): 30 | novels = Novel.query.filter_by(book_url=data['url']).all() 31 | for novel in novels: 32 | db.session.delete(novel) 33 | 34 | novel = Novel(book_name=data['title'], 35 | book_url=data['url'], 36 | book_img=data['image'], 37 | author=data['author'], 38 | type=data['type'], 39 | profile=data['profile'], 40 | last_update=data['time'], 41 | keyword=keyword) 42 | 43 | db.session.add(novel) 44 | 45 | 46 | def add_chapter(data, book_id): 47 | chapter = Chapter(chapter=data['chapter'], 48 | chapter_url=data['url'], 49 | book_id=book_id) 50 | db.session.add(chapter) 51 | 52 | 53 | def json_response(data='', status=200, headers=None): 54 | headers = headers or {} 55 | if 'Content-Type' not in headers: 56 | headers['Content-Type'] = JSON_MIME_TYPE 57 | 58 | return make_response(data, status, headers) 59 | 60 | ''' 61 | def generate_wordcloud(content): 62 | stopwords = set(STOPWORDS) 63 | 64 | font_path = '../backend/flask_nlp/static/simsun.ttf' 65 | font_path = os.path.abspath(font_path) 66 | font_path = font_path.replace('\\', '/') 67 | 68 | wordcloud = WordCloud(scale=3.5, 69 | max_font_size=100, 70 | background_color="white", 71 | stopwords=stopwords, 72 | contour_width=3, 73 | max_words=2000, 74 | contour_color='steelblue', 75 | font_path=font_path).generate(content) 76 | image = wordcloud.to_image() 77 | 78 | output_buffer = BytesIO() 79 | image.save(output_buffer, format='JPEG') 80 | byte_data = output_buffer.getvalue() 81 | base64_str = base64.b64encode(byte_data) 82 | return base64_str 83 | ''' 84 | -------------------------------------------------------------------------------- /backend/flask_nlp/api/utils/variable.py: -------------------------------------------------------------------------------- 1 | ner_dict = { 2 | 'B-PER': '人名', 3 | 'I-PER': '人名', 4 | 'B-LOC': '地名', 5 | 'I-LOC': '地名', 6 | 'B-ORG': '机构名', 7 | 'I-ORG': '机构名', 8 | 'O': '无' 9 | } 10 | 11 | post_dict = { 12 | 'n': '普通名词', 13 | 'nt': '时间名词', 14 | 'nd': '方位名词', 15 | 'nl': '处所名词', 16 | 'nh': '人名', 17 | 'nhf': '姓', 18 | 'nhs': '名', 19 | 'ns': '地名', 20 | 'nn': '族名', 21 | 'ni': '机构名', 22 | 'nz': '其他专名', 23 | 'v': '动词', 24 | 'vd': '趋向动词', 25 | 'vl': '联系动词', 26 | 'vu': '能愿动词', 27 | 'a': '形容词', 28 | 'f': '区别词', 29 | 'm': '数词', 30 | 'q': '量词', 31 | 'd': '副词', 32 | 'r': '代词', 33 | 'p': '介词', 34 | 'c': '连词', 35 | 'u': '助词', 36 | 'e': '叹词', 37 | 'o': '拟声词', 38 | 'i': '习用语', 39 | 'j': '缩略语', 40 | 'h': '前接成分', 41 | 'k': '后接成分', 42 | 'g': '语素字', 43 | 'x': '非语素字', 44 | 'w': '标点符号', 45 | 'ws': '非汉字字符串', 46 | 'wu': '其他未知的符号', 47 | 'mq': '未知' 48 | } 49 | 50 | stop_words = { 51 | '\n', 52 | '…', 53 | '...', 54 | '···', 55 | '$', 56 | '0', 57 | '1', 58 | '2', 59 | '3', 60 | '4', 61 | '5', 62 | '6', 63 | '7', 64 | '8', 65 | '9', 66 | '?', 67 | '_', 68 | '“', 69 | '"', 70 | '”', 71 | '、', 72 | '`', 73 | '·', 74 | '。', 75 | '.', 76 | '《', 77 | ',', 78 | '》', 79 | ',', 80 | '[', 81 | ']', 82 | '【', 83 | '】', 84 | '-', 85 | '=', 86 | '*', 87 | '&', 88 | '%', 89 | '#', 90 | '@', 91 | '说', 92 | '问', 93 | '没', 94 | '觉得', 95 | '东西', 96 | '觉得', 97 | '竟然', 98 | '一个', 99 | '说道', 100 | '是', 101 | '里', 102 | '好像', 103 | '像', 104 | '', 105 | '', 106 | '一', 107 | '一些', 108 | '一何', 109 | '一切', 110 | '一则', 111 | '一方面', 112 | '一旦', 113 | '一来', 114 | '一样', 115 | '一般', 116 | '一转眼', 117 | '万一', 118 | '上', 119 | '上下', 120 | '下', 121 | '不', 122 | '不仅', 123 | '不但', 124 | '不光', 125 | '不单', 126 | '不只', 127 | '不外乎', 128 | '不如', 129 | '不妨', 130 | '不尽', 131 | '不尽然', 132 | '不得', 133 | '不怕', 134 | '不惟', 135 | '不成', 136 | '不拘', 137 | '不料', 138 | '不是', 139 | '不比', 140 | '不然', 141 | '不特', 142 | '不独', 143 | '不管', 144 | '不至于', 145 | '不若', 146 | '不论', 147 | '不过', 148 | '不问', 149 | '以后', 150 | '没有', 151 | '与', 152 | '与其', 153 | '与其说', 154 | '与否', 155 | '与此同时', 156 | '且', 157 | '且不说', 158 | '且说', 159 | '两者', 160 | '个', 161 | '个别', 162 | '临', 163 | '为', 164 | '为了', 165 | '为什么', 166 | '为何', 167 | '为止', 168 | '为此', 169 | '为着', 170 | '乃', 171 | '乃至', 172 | '乃至于', 173 | '么', 174 | '之', 175 | '之一', 176 | '之所以', 177 | '之类', 178 | '乌乎', 179 | '乎', 180 | '乘', 181 | '也', 182 | '也好', 183 | '也罢', 184 | '了', 185 | '二来', 186 | '于', 187 | '于是', 188 | '于是乎', 189 | '云云', 190 | '云尔', 191 | '些', 192 | '亦', 193 | '人', 194 | '人们', 195 | '人家', 196 | '什么', 197 | '什么样', 198 | '今', 199 | '介于', 200 | '仍', 201 | '仍旧', 202 | '从', 203 | '从此', 204 | '从而', 205 | '他', 206 | '他人', 207 | '他们', 208 | '以', 209 | '以上', 210 | '以为', 211 | '以便', 212 | '以免', 213 | '以及', 214 | '以故', 215 | '以期', 216 | '以来', 217 | '以至', 218 | '以至于', 219 | '以致', 220 | '们', 221 | '任', 222 | '任何', 223 | '任凭', 224 | '似的', 225 | '但', 226 | '但凡', 227 | '但是', 228 | '何', 229 | '何以', 230 | '何况', 231 | '何处', 232 | '何时', 233 | '余外', 234 | '作为', 235 | '你', 236 | '你们', 237 | '使', 238 | '使得', 239 | '例如', 240 | '依', 241 | '依据', 242 | '依照', 243 | '便于', 244 | '俺', 245 | '俺们', 246 | '倘', 247 | '倘使', 248 | '倘或', 249 | '倘然', 250 | '倘若', 251 | '借', 252 | '假使', 253 | '假如', 254 | '假若', 255 | '傥然', 256 | '像', 257 | '儿', 258 | '先不先', 259 | '光是', 260 | '全体', 261 | '全部', 262 | '兮', 263 | '关于', 264 | '其', 265 | '其一', 266 | '其中', 267 | '其二', 268 | '其他', 269 | '其余', 270 | '其它', 271 | '其次', 272 | '具体地说', 273 | '具体说来', 274 | '兼之', 275 | '内', 276 | '再', 277 | '再其次', 278 | '再则', 279 | '再有', 280 | '再者', 281 | '再者说', 282 | '再说', 283 | '冒', 284 | '冲', 285 | '况且', 286 | '几', 287 | '几时', 288 | '凡', 289 | '凡是', 290 | '凭', 291 | '凭借', 292 | '出于', 293 | '出来', 294 | '分别', 295 | '则', 296 | '则甚', 297 | '别', 298 | '别人', 299 | '别处', 300 | '别是', 301 | '别的', 302 | '别管', 303 | '别说', 304 | '到', 305 | '前后', 306 | '前此', 307 | '前者', 308 | '加之', 309 | '加以', 310 | '即', 311 | '即令', 312 | '即使', 313 | '即便', 314 | '即如', 315 | '即或', 316 | '即若', 317 | '却', 318 | '去', 319 | '又', 320 | '又及', 321 | '及', 322 | '及其', 323 | '及至', 324 | '反之', 325 | '反而', 326 | '反过来', 327 | '反过来说', 328 | '受到', 329 | '另', 330 | '另一方面', 331 | '另外', 332 | '另悉', 333 | '只', 334 | '只当', 335 | '只怕', 336 | '只是', 337 | '只有', 338 | '只消', 339 | '只要', 340 | '只限', 341 | '叫', 342 | '叮咚', 343 | '可', 344 | '可以', 345 | '可是', 346 | '可见', 347 | '各', 348 | '各个', 349 | '各位', 350 | '各种', 351 | '各自', 352 | '同', 353 | '同时', 354 | '后', 355 | '后者', 356 | '向', 357 | '向使', 358 | '向着', 359 | '吓', 360 | '吗', 361 | '否则', 362 | '吧', 363 | '吧哒', 364 | '吱', 365 | '呀', 366 | '呃', 367 | '呕', 368 | '呗', 369 | '呜', 370 | '呜呼', 371 | '呢', 372 | '呵', 373 | '呵呵', 374 | '呸', 375 | '呼哧', 376 | '咋', 377 | '和', 378 | '咚', 379 | '咦', 380 | '咧', 381 | '咱', 382 | '咱们', 383 | '咳', 384 | '哇', 385 | '哈', 386 | '哈哈', 387 | '哉', 388 | '哎', 389 | '哎呀', 390 | '哎哟', 391 | '哗', 392 | '哟', 393 | '哦', 394 | '哩', 395 | '哪', 396 | '哪个', 397 | '哪些', 398 | '哪儿', 399 | '哪天', 400 | '哪年', 401 | '哪怕', 402 | '哪样', 403 | '哪边', 404 | '哪里', 405 | '哼', 406 | '哼唷', 407 | '唉', 408 | '唯有', 409 | '啊', 410 | '啐', 411 | '啥', 412 | '啦', 413 | '啪达', 414 | '啷当', 415 | '喂', 416 | '喏', 417 | '喔唷', 418 | '喽', 419 | '嗡', 420 | '嗡嗡', 421 | '嗬', 422 | '嗯', 423 | '嗳', 424 | '嘎', 425 | '嘎登', 426 | '嘘', 427 | '嘛', 428 | '嘻', 429 | '嘿', 430 | '嘿嘿', 431 | '因', 432 | '因为', 433 | '因了', 434 | '因此', 435 | '因着', 436 | '因而', 437 | '固然', 438 | '在', 439 | '在下', 440 | '在于', 441 | '地', 442 | '基于', 443 | '处在', 444 | '多', 445 | '多么', 446 | '多少', 447 | '大', 448 | '大家', 449 | '她', 450 | '她们', 451 | '好', 452 | '如', 453 | '如上', 454 | '如上所述', 455 | '如下', 456 | '如何', 457 | '如其', 458 | '如同', 459 | '如是', 460 | '如果', 461 | '如此', 462 | '如若', 463 | '始而', 464 | '孰料', 465 | '孰知', 466 | '宁', 467 | '宁可', 468 | '宁愿', 469 | '宁肯', 470 | '它', 471 | '它们', 472 | '对', 473 | '对于', 474 | '对待', 475 | '对方', 476 | '对比', 477 | '将', 478 | '小', 479 | '尔', 480 | '尔后', 481 | '尔尔', 482 | '尚且', 483 | '就', 484 | '就是', 485 | '就是了', 486 | '就是说', 487 | '就算', 488 | '就要', 489 | '尽', 490 | '尽管', 491 | '尽管如此', 492 | '岂但', 493 | '己', 494 | '已', 495 | '已矣', 496 | '巴', 497 | '巴巴', 498 | '并', 499 | '并且', 500 | '并非', 501 | '庶乎', 502 | '庶几', 503 | '开外', 504 | '开始', 505 | '归', 506 | '归齐', 507 | '当', 508 | '当地', 509 | '当然', 510 | '当着', 511 | '彼', 512 | '彼时', 513 | '彼此', 514 | '往', 515 | '待', 516 | '很', 517 | '得', 518 | '得了', 519 | '怎', 520 | '怎么', 521 | '怎么办', 522 | '怎么样', 523 | '怎奈', 524 | '怎样', 525 | '总之', 526 | '总的来看', 527 | '总的来说', 528 | '总的说来', 529 | '总而言之', 530 | '恰恰相反', 531 | '您', 532 | '惟其', 533 | '慢说', 534 | '我', 535 | '我们', 536 | '或', 537 | '或则', 538 | '或是', 539 | '或曰', 540 | '或者', 541 | '截至', 542 | '所', 543 | '所以', 544 | '所在', 545 | '所幸', 546 | '所有', 547 | '才', 548 | '才能', 549 | '打', 550 | '打从', 551 | '把', 552 | '抑或', 553 | '拿', 554 | '按', 555 | '按照', 556 | '换句话说', 557 | '换言之', 558 | '据', 559 | '据此', 560 | '接着', 561 | '故', 562 | '故此', 563 | '故而', 564 | '旁人', 565 | '无', 566 | '无宁', 567 | '无论', 568 | '既', 569 | '既往', 570 | '既是', 571 | '既然', 572 | '时候', 573 | '是', 574 | '是以', 575 | '是的', 576 | '曾', 577 | '替', 578 | '替代', 579 | '最', 580 | '有', 581 | '有些', 582 | '有关', 583 | '有及', 584 | '有时', 585 | '有的', 586 | '望', 587 | '朝', 588 | '朝着', 589 | '本', 590 | '本人', 591 | '本地', 592 | '本着', 593 | '本身', 594 | '来', 595 | '来着', 596 | '来自', 597 | '来说', 598 | '极了', 599 | '果然', 600 | '果真', 601 | '某', 602 | '某个', 603 | '某些', 604 | '某某', 605 | '根据', 606 | '欤', 607 | '正值', 608 | '正如', 609 | '正巧', 610 | '正是', 611 | '此', 612 | '此地', 613 | '此处', 614 | '此外', 615 | '此时', 616 | '此次', 617 | '此间', 618 | '毋宁', 619 | '每', 620 | '每当', 621 | '比', 622 | '比及', 623 | '比如', 624 | '比方', 625 | '没奈何', 626 | '沿', 627 | '沿着', 628 | '漫说', 629 | '焉', 630 | '然则', 631 | '然后', 632 | '然而', 633 | '照', 634 | '照着', 635 | '犹且', 636 | '犹自', 637 | '甚且', 638 | '甚么', 639 | '甚或', 640 | '甚而', 641 | '甚至', 642 | '甚至于', 643 | '用', 644 | '用来', 645 | '由', 646 | '由于', 647 | '由是', 648 | '由此', 649 | '由此可见', 650 | '的', 651 | '的确', 652 | '的话', 653 | '直到', 654 | '相对而言', 655 | '省得', 656 | '看', 657 | '眨眼', 658 | '着', 659 | '着呢', 660 | '矣', 661 | '矣乎', 662 | '矣哉', 663 | '离', 664 | '竟而', 665 | '第', 666 | '等', 667 | '等到', 668 | '等等', 669 | '简言之', 670 | '管', 671 | '类如', 672 | '紧接着', 673 | '纵', 674 | '纵令', 675 | '纵使', 676 | '纵然', 677 | '经', 678 | '经过', 679 | '结果', 680 | '给', 681 | '继之', 682 | '继后', 683 | '继而', 684 | '综上所述', 685 | '罢了', 686 | '者', 687 | '而', 688 | '而且', 689 | '而况', 690 | '而后', 691 | '而外', 692 | '而已', 693 | '而是', 694 | '而言', 695 | '能', 696 | '能否', 697 | '腾', 698 | '自', 699 | '自个儿', 700 | '自从', 701 | '自各儿', 702 | '自后', 703 | '自家', 704 | '自己', 705 | '自打', 706 | '自身', 707 | '至', 708 | '至于', 709 | '至今', 710 | '至若', 711 | '致', 712 | '般的', 713 | '若', 714 | '若夫', 715 | '若是', 716 | '若果 ', 717 | '若非', 718 | '莫不然', 719 | '莫如', 720 | '莫若', 721 | '虽', 722 | '虽则', 723 | '虽然', 724 | '虽说', 725 | '被', 726 | '要', 727 | '要不', 728 | '要不是', 729 | '要不然', 730 | '要么', 731 | '要是', 732 | '譬喻', 733 | '譬如', 734 | '让', 735 | '许多', 736 | '论', 737 | '设使', 738 | '设或', 739 | '设若', 740 | '诚如', 741 | '诚然', 742 | '该', 743 | '说来', 744 | '诸', 745 | '诸位', 746 | '诸如', 747 | '谁', 748 | '谁人', 749 | '谁料', 750 | '谁知', 751 | '贼死', 752 | '赖以', 753 | '赶', 754 | '起', 755 | '起见', 756 | '趁', 757 | '趁着', 758 | '越是', 759 | '距', 760 | '跟', 761 | '较', 762 | '较之', 763 | '边', 764 | '过', 765 | '还', 766 | '还是', 767 | '还有', 768 | '还要', 769 | '这', 770 | '这一来', 771 | '这个', 772 | '这么', 773 | '这么些', 774 | '这么样', 775 | '这么点儿', 776 | '这些', 777 | '这会儿', 778 | '这儿', 779 | '这就是说', 780 | '这时', 781 | '这样', 782 | '这次', 783 | '这般', 784 | '这边', 785 | '这里', 786 | '进而', 787 | '连', 788 | '连同', 789 | '逐步', 790 | '通过', 791 | '遵循', 792 | '遵照', 793 | '那', 794 | '那个', 795 | '那么', 796 | '那么些', 797 | '那么样', 798 | '那些', 799 | '那会儿', 800 | '那儿', 801 | '那时', 802 | '那样', 803 | '那般', 804 | '那边', 805 | '那里', 806 | '都', 807 | '鄙人', 808 | '鉴于', 809 | '针对', 810 | '阿', 811 | '除', 812 | '除了', 813 | '除外', 814 | '除开', 815 | '除此之外', 816 | '除非', 817 | '随', 818 | '随后', 819 | '随时', 820 | '随着', 821 | '难道说', 822 | '非但', 823 | '非徒', 824 | '非特', 825 | '非独', 826 | '靠', 827 | '顺', 828 | '顺着', 829 | '首先', 830 | '!', 831 | ',', 832 | ':', 833 | ';', 834 | '?', 835 | '?', 836 | ':', 837 | '!', 838 | ';' 839 | } -------------------------------------------------------------------------------- /backend/flask_nlp/config.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | basedir = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | 6 | class Config: 7 | CSRF_ENABLED = True 8 | SECRET_KEY = 'you-guess' 9 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 10 | SQLALCHEMY_TRACK_MODIFICATIONS = False 11 | 12 | CHAPTER_PER_PAGE = 20 13 | 14 | SSL_DISABLE = True 15 | 16 | @staticmethod 17 | def init_app(app): 18 | pass 19 | 20 | 21 | class DevelopmentConfig(Config): 22 | SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 23 | 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') 24 | DEBUG = True 25 | 26 | 27 | class HerokuConfig(Config): 28 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 29 | 'sqlite:///' + os.path.join(basedir, 'data.sqlite') 30 | 31 | SSL_DISABLE = bool(os.environ.get('SSL_DISABLE')) 32 | 33 | @classmethod 34 | def init_app(cls, app): 35 | Config.init_app(app) 36 | 37 | # 处理代理服务器首部 38 | from werkzeug.contrib.fixers import ProxyFix 39 | app.wsgi_app = ProxyFix(app.wsgi_app) 40 | 41 | 42 | config = { 43 | 'development': DevelopmentConfig, 44 | 'heroku': HerokuConfig, 45 | 'default': DevelopmentConfig 46 | } 47 | -------------------------------------------------------------------------------- /backend/flask_nlp/data-dev.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/data-dev.sqlite -------------------------------------------------------------------------------- /backend/flask_nlp/models.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from flask import url_for 3 | 4 | # 导入数据库实例 5 | from flask_nlp import db 6 | 7 | 8 | # Novel模型 9 | class Novel(db.Model): 10 | __tablename__ = 'novels' 11 | id = db.Column(db.Integer, primary_key=True) 12 | book_name = db.Column(db.String(64), index=True) 13 | book_url = db.Column(db.String) 14 | book_img = db.Column(db.String) 15 | author = db.Column(db.String(64)) 16 | type = db.Column(db.String(64), nullable=True) 17 | last_update = db.Column(db.String(64), nullable=True) 18 | profile = db.Column(db.Text, nullable=True) 19 | keyword = db.Column(db.String) 20 | page = db.Column(db.Integer) 21 | 22 | chapters = db.relationship('Chapter', backref='book', lazy='dynamic') 23 | 24 | __table_args__ = {'mysql_charset': 'utf8'} 25 | 26 | def to_json(self): 27 | json_novel = { 28 | 'id': self.id, 29 | 'book_url': self.book_url, 30 | 'book_name': self.book_name, 31 | 'book_img': self.book_img, 32 | 'author': self.author, 33 | 'type': self.type, 34 | 'last_update': self.last_update, 35 | 'profile': self.profile, 36 | } 37 | return json_novel 38 | 39 | 40 | #Chapter模型 41 | class Chapter(db.Model): 42 | __tablename__ = 'chapters' 43 | id = db.Column(db.Integer, primary_key=True) 44 | chapter = db.Column(db.String(64)) 45 | chapter_url = db.Column(db.String, index=True) 46 | 47 | content = db.relationship('Content', backref='chapter', lazy='dynamic') 48 | sentences = db.relationship('SentenceSeg', 49 | backref='chapter', 50 | lazy='dynamic') 51 | book_id = db.Column(db.Integer, db.ForeignKey('novels.id')) 52 | 53 | def to_json(self): 54 | json_chapter = { 55 | 'id': self.id, 56 | 'book_id': self.book_id, 57 | 'chapter_name': self.chapter 58 | } 59 | 60 | return json_chapter 61 | 62 | 63 | class Content(db.Model): 64 | __tablename__ = 'contents' 65 | id = db.Column(db.Integer, primary_key=True) 66 | content = db.Column(db.String) 67 | 68 | chapter_id = db.Column(db.Integer, db.ForeignKey('chapters.id')) 69 | 70 | def to_json(self): 71 | json_content = {'id': self.chapter_id, 'content': self.content} 72 | 73 | return json_content 74 | 75 | 76 | #一个章节分很多句子 77 | class SentenceSeg(db.Model): 78 | __tablename__ = 'sentencesegs' 79 | id = db.Column(db.Integer, primary_key=True) 80 | 81 | sentenceseg = db.Column(db.Text) 82 | 83 | chapter_id = db.Column(db.Integer, db.ForeignKey('chapters.id')) 84 | 85 | words = db.relationship("WordSeg", backref='sentence', lazy='dynamic') 86 | senti = db.relationship("SentiContent", backref='sentence', lazy='dynamic') 87 | 88 | 89 | #一个句子分很多词 90 | class WordSeg(db.Model): 91 | __tablename__ = 'wordsegs' 92 | id = db.Column(db.Integer, primary_key=True) 93 | 94 | wordseg = db.Column(db.String) 95 | 96 | sentence_id = db.Column(db.Integer, db.ForeignKey('sentencesegs.id')) 97 | 98 | 99 | #词性分词 100 | class PostWordSeg(db.Model): 101 | __tablename__ = 'postcontentsegs' 102 | id = db.Column(db.Integer, primary_key=True) 103 | 104 | postag = db.Column(db.String) 105 | 106 | word_id = db.Column(db.Integer, db.ForeignKey('wordsegs.id')) 107 | 108 | 109 | #ner分词 110 | class NerWordSeg(db.Model): 111 | __tablename__ = 'nercontentsegs' 112 | id = db.Column(db.Integer, primary_key=True) 113 | 114 | nertag = db.Column(db.String) 115 | 116 | word_id = db.Column(db.Integer, db.ForeignKey('wordsegs.id')) 117 | 118 | 119 | #情感判别 120 | class SentiContent(db.Model): 121 | __tablename__ = 'sentiContent' 122 | id = db.Column(db.Integer, primary_key=True) 123 | 124 | senti = db.Column(db.String) 125 | degree = db.Column(db.Float) 126 | 127 | sentence_id = db.Column(db.Integer, db.ForeignKey('sentencesegs.id')) 128 | 129 | 130 | class Alembic(db.Model): 131 | __tablename__ = 'alembic_version' 132 | version_num = db.Column(db.String(32), primary_key=True, nullable=False) 133 | 134 | @staticmethod 135 | def clear_A(): 136 | for a in Alembic.query.all(): 137 | print(a.version_num) 138 | db.session.delete(a) 139 | db.session.commit() 140 | print('======== data in Table: Alembic cleared!') -------------------------------------------------------------------------------- /backend/flask_nlp/spider/__pycache__/spider.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/flask_nlp/spider/__pycache__/spider.cpython-36.pyc -------------------------------------------------------------------------------- /backend/flask_nlp/spider/spider.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | #动态加载数据处理 3 | from selenium import webdriver 4 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 5 | from selenium.webdriver.chrome.options import Options 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.support import expected_conditions as EC 9 | import os, time 10 | 11 | chrome_options = Options() 12 | chrome_options.add_argument('--headless') 13 | chrome_options.add_argument('--disable-gpu') 14 | 15 | driver_path = '../backend/flask_nlp/static/chromedriver.exe' 16 | driver_path = os.path.abspath(driver_path) 17 | driver_path = driver_path.replace('\\', '/') 18 | """ 19 | 爬虫api: 20 | 搜索结果页:get_index_result(search) 21 | 小说章节页:get_chapter(url) 22 | 章节内容:get_content(url) 23 | """ 24 | 25 | 26 | class CpSpider(object): 27 | def __init__(self): 28 | time.sleep(5) 29 | self.search_url = 'https://www.gongzicp.com/novel/search/module/novel/keyword/' 30 | self.base_url = 'https://www.gongzicp.com/' 31 | self.headers = { 32 | 'User-Agent': 33 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0' 34 | } 35 | # 本地driver运行: 36 | ''' 37 | self.browser = webdriver.Chrome(executable_path=driver_path,options=chrome_options) 38 | ''' 39 | # docker运行 40 | 41 | self.browser = webdriver.Remote( 42 | command_executor='http://chrome:4444/wd/hub', 43 | options=chrome_options) 44 | 45 | def parse_url(self, url): 46 | try: 47 | self.browser.get(url) 48 | # 需要等一下,直到页面加载完成 49 | # 超时时间10s,每0.2秒检查一次 50 | wait = WebDriverWait(self.browser, 10, 0.2) 51 | wait.until(EC.presence_of_element_located((By.ID, 'vueBox'))) 52 | except ConnectionError: 53 | print('Error.') 54 | 55 | # 搜索结果页数据 56 | def get_index_result(self, keyword, page=0): 57 | #TODO 解决有些小说搜索页没有tag无法一起打包yield的情况 58 | url = self.search_url + keyword 59 | self.parse_url(url) 60 | 61 | try: 62 | titles = self.browser.find_elements_by_xpath( 63 | '//*[@class="cp-novel-name"]') 64 | urls = self.browser.find_elements_by_xpath( 65 | '//*[@class="cp-novel-name"]') 66 | images = self.browser.find_elements_by_xpath( 67 | '//*[@class="cp-novel-cover"]/img') 68 | authors = self.browser.find_elements_by_xpath( 69 | '//*[@id="vueBox"]/div[3]/div/div[1]/div/div[2]/div[2]/a') 70 | profiles = self.browser.find_elements_by_xpath( 71 | '//*[@id="vueBox"]/div[3]/div/div[1]/div/div[2]/p') 72 | types = self.browser.find_elements_by_xpath( 73 | '//*[@class="cp-novel-type"]') 74 | # tags = self.browser.find_elements_by_xpath('//*[@class="cp-novel-tag"]') 75 | times = self.browser.find_elements_by_xpath( 76 | '//*[@class="cp-novel-update"]') 77 | except Exception as e: 78 | print(e) 79 | finally: 80 | # print(len(titles),len(urls),len(images),len(authors),len(profiles),len(types),len(tags),len(times)) 81 | for title, url, image, author, profile, type, tim in zip( 82 | titles, urls, images, authors, profiles, types, times): 83 | data = { 84 | 'title': title.text, 85 | 'url': url.get_attribute('href'), 86 | 'image': image.get_attribute('src'), 87 | 'author': author.text, 88 | 'profile': profile.text, 89 | 'type': type.text, 90 | 'time': tim.text 91 | } 92 | yield data 93 | 94 | # 小说章节页数据 95 | def get_chapter(self, url): 96 | self.parse_url(url) 97 | chapters = self.browser.find_elements_by_xpath( 98 | '//*[@id="vueBox"]/div[1]/div[3]/div[2]/div[3]/ul/li/a/span[2]') 99 | urls = self.browser.find_elements_by_xpath( 100 | '//*[@id="vueBox"]/div[1]/div[3]/div[2]/div[3]/ul/li/a') 101 | for chapter_url, chapter in zip(urls, chapters): 102 | url = chapter_url.get_attribute('href') 103 | if url != 'javascript:;': 104 | chapter_text = chapter.text 105 | else: 106 | chapter_text = '章节已锁定' 107 | 108 | data = {'url': url, 'chapter': chapter_text} 109 | yield data 110 | 111 | # 章节内容页数据 112 | def get_content(self, url): 113 | self.parse_url(url) 114 | contents = self.browser.find_elements_by_xpath( 115 | '//*[@id="cpReadContent"]/p') 116 | return '\n'.join(i.text for i in contents) 117 | 118 | 119 | # Test 120 | # cp = CpSpider() 121 | # print("2") 122 | # for i in cp.get_index_result('卡比丘', page=0): 123 | 124 | # print(i) 125 | 126 | # cp.parse_url(cp.search_url + '{keyword}'.format(keyword='卡比丘')) 127 | # print(cp.get_content('https://www.gongzicp.com/read-112530.html')) -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | from flask_script import Manager 4 | from flask_migrate import Migrate, MigrateCommand 5 | from flask_nlp import db 6 | from flask_nlp import create_app 7 | 8 | app = create_app(os.getenv('CONFIG') or 'default') 9 | manager = Manager(app) 10 | migrate = Migrate(app, db, render_as_batch=True) 11 | manager.add_command('db', MigrateCommand) 12 | 13 | 14 | @manager.command 15 | def deploy(): 16 | ''' 17 | from flask_migrate import upgrade 18 | 19 | # 情况数据库的操作只在运行过后才可以取消注释使用 20 | from flask_nlp.models import Search, Novel, Chapter, Article 21 | # 22 | # 清空数据库 23 | searchs = Search.query.all() 24 | for s in searchs: 25 | db.session.delete(s) 26 | novels = Novel.query.all() 27 | for n in novels: 28 | db.session.delete(n) 29 | chapters = Chapter.query.all() 30 | for c in chapters: 31 | db.session.delete(c) 32 | articles = Article.query.all() 33 | for a in articles: 34 | db.session.delete(a) 35 | 36 | db.session.commit() 37 | ''' 38 | 39 | from flask_nlp.models import Alembic 40 | Alembic.clear_A() 41 | 42 | 43 | if __name__ == '__main__': 44 | manager.run() 45 | -------------------------------------------------------------------------------- /backend/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/migrations/__pycache__/env.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/migrations/__pycache__/env.cpython-36.pyc -------------------------------------------------------------------------------- /backend/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /backend/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /backend/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/migrations/versions/281e32ecd391_v1_0.py: -------------------------------------------------------------------------------- 1 | """v1.0 2 | 3 | Revision ID: 281e32ecd391 4 | Revises: 599c9cca170b 5 | Create Date: 2019-12-18 23:42:59.239878 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '281e32ecd391' 14 | down_revision = '599c9cca170b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('nercontentsegs', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('nertag', sa.String(), nullable=True), 24 | sa.Column('word_id', sa.Integer(), nullable=True), 25 | sa.ForeignKeyConstraint(['word_id'], ['wordsegs.id'], name=op.f('fk_nercontentsegs_word_id_wordsegs')), 26 | sa.PrimaryKeyConstraint('id', name=op.f('pk_nercontentsegs')) 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('nercontentsegs') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /backend/migrations/versions/599c9cca170b_v1_0.py: -------------------------------------------------------------------------------- 1 | """v1.0 2 | 3 | Revision ID: 599c9cca170b 4 | Revises: a04c5a468b8a 5 | Create Date: 2019-12-18 22:45:22.961753 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '599c9cca170b' 14 | down_revision = 'a04c5a468b8a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('postcontentsegs', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('word_id', sa.Integer(), nullable=True)) 23 | batch_op.drop_constraint('fk_postcontentsegs_chapter_id_sentencesegs', type_='foreignkey') 24 | batch_op.create_foreign_key(batch_op.f('fk_postcontentsegs_word_id_wordsegs'), 'wordsegs', ['word_id'], ['id']) 25 | batch_op.drop_column('chapter_id') 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | with op.batch_alter_table('postcontentsegs', schema=None) as batch_op: 33 | batch_op.add_column(sa.Column('chapter_id', sa.INTEGER(), nullable=True)) 34 | batch_op.drop_constraint(batch_op.f('fk_postcontentsegs_word_id_wordsegs'), type_='foreignkey') 35 | batch_op.create_foreign_key('fk_postcontentsegs_chapter_id_sentencesegs', 'sentencesegs', ['chapter_id'], ['id']) 36 | batch_op.drop_column('word_id') 37 | 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /backend/migrations/versions/__pycache__/281e32ecd391_v1_0.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/migrations/versions/__pycache__/281e32ecd391_v1_0.cpython-36.pyc -------------------------------------------------------------------------------- /backend/migrations/versions/__pycache__/599c9cca170b_v1_0.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/migrations/versions/__pycache__/599c9cca170b_v1_0.cpython-36.pyc -------------------------------------------------------------------------------- /backend/migrations/versions/__pycache__/a04c5a468b8a_v1_0.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/backend/migrations/versions/__pycache__/a04c5a468b8a_v1_0.cpython-36.pyc -------------------------------------------------------------------------------- /backend/migrations/versions/a04c5a468b8a_v1_0.py: -------------------------------------------------------------------------------- 1 | """v1.0 2 | 3 | Revision ID: a04c5a468b8a 4 | Revises: 5 | Create Date: 2019-12-18 21:10:52.005843 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a04c5a468b8a' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('novels', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('book_name', sa.String(length=64), nullable=True), 24 | sa.Column('book_url', sa.String(), nullable=True), 25 | sa.Column('book_img', sa.String(), nullable=True), 26 | sa.Column('author', sa.String(length=64), nullable=True), 27 | sa.Column('type', sa.String(length=64), nullable=True), 28 | sa.Column('last_update', sa.String(length=64), nullable=True), 29 | sa.Column('profile', sa.Text(), nullable=True), 30 | sa.Column('keyword', sa.String(), nullable=True), 31 | sa.Column('page', sa.Integer(), nullable=True), 32 | sa.PrimaryKeyConstraint('id', name=op.f('pk_novels')), 33 | mysql_charset='utf8' 34 | ) 35 | with op.batch_alter_table('novels', schema=None) as batch_op: 36 | batch_op.create_index(batch_op.f('ix_novels_book_name'), ['book_name'], unique=False) 37 | 38 | op.create_table('chapters', 39 | sa.Column('id', sa.Integer(), nullable=False), 40 | sa.Column('chapter', sa.String(length=64), nullable=True), 41 | sa.Column('chapter_url', sa.String(), nullable=True), 42 | sa.Column('book_id', sa.Integer(), nullable=True), 43 | sa.ForeignKeyConstraint(['book_id'], ['novels.id'], name=op.f('fk_chapters_book_id_novels')), 44 | sa.PrimaryKeyConstraint('id', name=op.f('pk_chapters')) 45 | ) 46 | with op.batch_alter_table('chapters', schema=None) as batch_op: 47 | batch_op.create_index(batch_op.f('ix_chapters_chapter_url'), ['chapter_url'], unique=False) 48 | 49 | op.create_table('contents', 50 | sa.Column('id', sa.Integer(), nullable=False), 51 | sa.Column('content', sa.String(), nullable=True), 52 | sa.Column('chapter_id', sa.Integer(), nullable=True), 53 | sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], name=op.f('fk_contents_chapter_id_chapters')), 54 | sa.PrimaryKeyConstraint('id', name=op.f('pk_contents')) 55 | ) 56 | op.create_table('sentencesegs', 57 | sa.Column('id', sa.Integer(), nullable=False), 58 | sa.Column('sentenceseg', sa.Text(), nullable=True), 59 | sa.Column('chapter_id', sa.Integer(), nullable=True), 60 | sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], name=op.f('fk_sentencesegs_chapter_id_chapters')), 61 | sa.PrimaryKeyConstraint('id', name=op.f('pk_sentencesegs')) 62 | ) 63 | op.create_table('postcontentsegs', 64 | sa.Column('id', sa.Integer(), nullable=False), 65 | sa.Column('postag', sa.String(), nullable=True), 66 | sa.Column('chapter_id', sa.Integer(), nullable=True), 67 | sa.ForeignKeyConstraint(['chapter_id'], ['sentencesegs.id'], name=op.f('fk_postcontentsegs_chapter_id_sentencesegs')), 68 | sa.PrimaryKeyConstraint('id', name=op.f('pk_postcontentsegs')) 69 | ) 70 | op.create_table('sentiContent', 71 | sa.Column('id', sa.Integer(), nullable=False), 72 | sa.Column('senti', sa.String(), nullable=True), 73 | sa.Column('degree', sa.Float(), nullable=True), 74 | sa.Column('sentence_id', sa.Integer(), nullable=True), 75 | sa.ForeignKeyConstraint(['sentence_id'], ['sentencesegs.id'], name=op.f('fk_sentiContent_sentence_id_sentencesegs')), 76 | sa.PrimaryKeyConstraint('id', name=op.f('pk_sentiContent')) 77 | ) 78 | op.create_table('wordsegs', 79 | sa.Column('id', sa.Integer(), nullable=False), 80 | sa.Column('wordseg', sa.String(), nullable=True), 81 | sa.Column('sentence_id', sa.Integer(), nullable=True), 82 | sa.ForeignKeyConstraint(['sentence_id'], ['sentencesegs.id'], name=op.f('fk_wordsegs_sentence_id_sentencesegs')), 83 | sa.PrimaryKeyConstraint('id', name=op.f('pk_wordsegs')) 84 | ) 85 | # ### end Alembic commands ### 86 | 87 | 88 | def downgrade(): 89 | # ### commands auto generated by Alembic - please adjust! ### 90 | op.drop_table('wordsegs') 91 | op.drop_table('sentiContent') 92 | op.drop_table('postcontentsegs') 93 | op.drop_table('sentencesegs') 94 | op.drop_table('contents') 95 | with op.batch_alter_table('chapters', schema=None) as batch_op: 96 | batch_op.drop_index(batch_op.f('ix_chapters_chapter_url')) 97 | 98 | op.drop_table('chapters') 99 | with op.batch_alter_table('novels', schema=None) as batch_op: 100 | batch_op.drop_index(batch_op.f('ix_novels_book_name')) 101 | 102 | op.drop_table('novels') 103 | # ### end Alembic commands ### 104 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.3.1 2 | aniso8601==8.0.0 3 | atomicwrites==1.3.0 4 | attrs==19.3.0 5 | autopep8==1.4.4 6 | Click==7.0 7 | colorama==0.4.3 8 | cycler==0.10.0 9 | Flask==1.1.1 10 | Flask-Cors==3.0.8 11 | Flask-Migrate==2.5.2 12 | Flask-RESTful==0.3.7 13 | Flask-Script==2.0.6 14 | Flask-SQLAlchemy==2.4.1 15 | Flask-SSLify==0.1.5 16 | importlib-metadata==1.2.0 17 | itsdangerous==1.1.0 18 | jiagu==0.2.2 19 | Jinja2==2.10.3 20 | kiwisolver==1.1.0 21 | Mako==1.1.0 22 | MarkupSafe==1.1.1 23 | matplotlib==3.1.2 24 | more-itertools==8.0.2 25 | numpy==1.17.4 26 | packaging==19.2 27 | Pillow==6.2.1 28 | pkuseg==0.0.22 29 | pluggy==0.13.1 30 | py==1.8.0 31 | pycodestyle==2.5.0 32 | pyparsing==2.4.5 33 | pytest==5.3.1 34 | python-dateutil==2.8.1 35 | python-editor==1.0.4 36 | pytz==2019.3 37 | selenium==3.141.0 38 | six==1.13.0 39 | SQLAlchemy==1.3.11 40 | urllib3==1.25.7 41 | vue==0.0.1 42 | wcwidth==0.1.7 43 | Werkzeug==0.16.0 44 | wordcloud==1.6.0 45 | yapf==0.29.0 46 | zipp==0.6.0 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | #后端,爬虫+api 4 | backend: 5 | image: flask-vuejs-nlp_backend 6 | build: ./backend 7 | hostname: backend 8 | depends_on: 9 | - chrome 10 | ports: 11 | - "5000:5000" 12 | links: 13 | - chrome 14 | 15 | #后端备份,爬虫+api 16 | backend_backup: 17 | image: flask-vuejs-nlp_backend 18 | build: ./backend 19 | hostname: backend_backup 20 | depends_on: 21 | - chrome 22 | ports: 23 | - "6000:5000" 24 | links: 25 | - chrome 26 | 27 | 28 | #前端 29 | frontend: 30 | #image: vuenginxcontainer 31 | build: ./frontend 32 | ports: 33 | - "3000:80" 34 | depends_on: 35 | - backend 36 | - backend_backup 37 | links: 38 | - backend 39 | - backend_backup 40 | volumes: 41 | - type: bind 42 | source: ./frontend/nginx 43 | target: /etc/nginx/conf.d 44 | - type: bind 45 | source: ./frontend/dist 46 | target: /usr/share/nginx/html 47 | 48 | 49 | 50 | #chrome驱动 51 | chrome: 52 | image: selenium/standalone-chrome:latest 53 | ports: 54 | - "4444:4444" 55 | environment: 56 | - SE_OPTS=-sessionTimeout 31536000 57 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | ## 项目安装 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### 开发选项(一般用这个) 10 | 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### 部署选项 16 | 17 | ``` 18 | npm run build 19 | ``` 20 | 21 | ### 测试 22 | 23 | ``` 24 | npm run test 25 | ``` 26 | 27 | ### 自动修正 28 | 29 | ``` 30 | npm run lint 31 | ``` 32 | 33 | ### 个性化设置 34 | 35 | 参考 [Configuration Reference](https://cli.vuejs.org/config/). 36 | 37 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/dist/favicon.ico -------------------------------------------------------------------------------- /frontend/dist/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/dist/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /frontend/dist/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/dist/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | Flask + Vue NLP
-------------------------------------------------------------------------------- /frontend/dist/js/app.245c7112.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var n,r,s=e[0],c=e[1],l=e[2],u=0,d=[];u0&&void 0!==arguments[0]?arguments[0]:this.chapter_id;if(""==this.ner_content){if(""==this.segcontent){var o="/api/process/segcontent/".concat(e);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(e){t.segcontent=e.data.words}))}var n="/api/process/nercontent/".concat(e);this.loading=!0,axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(n).then((function(e){t.ner_content=e.data.ners,t.loading=!1})).catch((function(t){console.error(t)}))}else this.loading=!1},getChapters:function(t){var e=this,o="/api/chapters/".concat(t);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(t){e.chapters=t.data.chapters,e.fullscreenLoading=!1})).catch((function(t){console.error(t)}))},getContent:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.chapter_id;if(""==this.content){var o="/api/chaptercontent/".concat(e);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(e){t.content=e.data.content,t.loading=!1})).catch((function(t){console.error(t)}))}else this.loading=!1},getContentSeg:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.chapter_id;if(""==this.segcontent){var o="/api/process/segcontent/".concat(e);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(e){t.segcontent=e.data.words,t.loading=!1})).catch((function(t){console.error(t)}))}else this.loading=!1},getContentSenti:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.chapter_id;if(""==this.senti_content){var o="/api/process/senticontent/".concat(e);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(e){t.senti_content=e.data.sentiments,t.loading=!1})).catch((function(t){console.error(t)}))}else this.loading=!1},getPostContentSeg:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.chapter_id;if(""==this.post_segcontent){if(""==this.segcontent){var o="/api/process/segcontent/".concat(e);axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(o).then((function(e){t.segcontent=e.data.words}))}var n="/api/process/postagcontentseg/".concat(e);this.loading=!0,axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(n).then((function(e){t.post_segcontent=e.data.tags,t.loading=!1})).catch((function(t){console.error(t)}))}else this.loading=!1},getImages:function getImages(color){var wordFreqs=this.wordFreqs,background_color="",plain_color="",important_color="",color_var="";switch(color){case 1:background_color="#e9eef3",plain_color="#b3c0d1",important_color="#116de6";break;case 2:background_color="#ffe0e0",plain_color="#c09292",important_color="#f02222";break;case 3:background_color="#E2FAE7",plain_color="#82A088",important_color="#63D0A4";break;case 4:background_color="#e9eef3",color_var="random-dark";break;case 5:background_color="#2D365B",color_var="random-light";break}var max_3_freq=wordFreqs[2].count,canvas=document.getElementById("canvas"),color_func=function(t,e){return e>=max_3_freq?important_color:plain_color},options=eval({list:words,gridSize:Math.round(16*jquery__WEBPACK_IMPORTED_MODULE_3___default()("#canvas").width()/1024),weightFactor:function(t){return Math.pow(t,.7)*jquery__WEBPACK_IMPORTED_MODULE_3___default()("#canvas").width()/32},maxFontSize:100,minFontSize:30,rotationSteps:2,fontWeight:"normal",fontFamily:"Times, serif",color:color>=4?color_var:color_func,backgroundColor:background_color,rotateRatio:.5});wordcloud__WEBPACK_IMPORTED_MODULE_2___default()(canvas,options),this.loading=!1},getWordCloud:function(t){var e=this,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.chapter_id;if(console.log("color"+t),""==this.wordFreqs){var n="/api/process/wordcloud/".concat(o);console.log(n),axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(n).then((function(o){e.wordFreqs=o.data;for(var n=e.wordFreqs,i=0;i 1%", 53 | "last 2 versions" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flask + Vue NLP 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 68 | 91 | 95 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/common/config.json: -------------------------------------------------------------------------------- 1 | {"global":{"$--color-primary":"#17757B"},"local":{}} -------------------------------------------------------------------------------- /frontend/src/common/theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/src/common/theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /frontend/src/common/theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/frontend/src/common/theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /frontend/src/common/theme/github_corner.css: -------------------------------------------------------------------------------- 1 | /* GitHub Cornor */ 2 | .github-corner :hover .octo-arm { 3 | animation: octocat-wave 560ms ease-in-out; 4 | } 5 | 6 | @media (max-width: 991px) { 7 | .github-corner>svg { 8 | fill: #fff !important; 9 | color: #222 !important; 10 | } 11 | 12 | .github-corner .github-corner:hover .octo-arm { 13 | animation: none; 14 | } 15 | 16 | .github-corner .github-corner .octo-arm { 17 | animation: octocat-wave 560ms ease-in-out; 18 | } 19 | } 20 | 21 | @-moz-keyframes octocat-wave { 22 | 23 | 0%, 24 | 100% { 25 | -webkit-transform: rotate(0); 26 | -moz-transform: rotate(0); 27 | -ms-transform: rotate(0); 28 | -o-transform: rotate(0); 29 | transform: rotate(0); 30 | } 31 | 32 | 20%, 33 | 60% { 34 | -webkit-transform: rotate(-25deg); 35 | -moz-transform: rotate(-25deg); 36 | -ms-transform: rotate(-25deg); 37 | -o-transform: rotate(-25deg); 38 | transform: rotate(-25deg); 39 | } 40 | 41 | 40%, 42 | 80% { 43 | -webkit-transform: rotate(10deg); 44 | -moz-transform: rotate(10deg); 45 | -ms-transform: rotate(10deg); 46 | -o-transform: rotate(10deg); 47 | transform: rotate(10deg); 48 | } 49 | } 50 | 51 | @-webkit-keyframes octocat-wave { 52 | 53 | 0%, 54 | 100% { 55 | -webkit-transform: rotate(0); 56 | -moz-transform: rotate(0); 57 | -ms-transform: rotate(0); 58 | -o-transform: rotate(0); 59 | transform: rotate(0); 60 | } 61 | 62 | 20%, 63 | 60% { 64 | -webkit-transform: rotate(-25deg); 65 | -moz-transform: rotate(-25deg); 66 | -ms-transform: rotate(-25deg); 67 | -o-transform: rotate(-25deg); 68 | transform: rotate(-25deg); 69 | } 70 | 71 | 40%, 72 | 80% { 73 | -webkit-transform: rotate(10deg); 74 | -moz-transform: rotate(10deg); 75 | -ms-transform: rotate(10deg); 76 | -o-transform: rotate(10deg); 77 | transform: rotate(10deg); 78 | } 79 | } 80 | 81 | @-o-keyframes octocat-wave { 82 | 83 | 0%, 84 | 100% { 85 | -webkit-transform: rotate(0); 86 | -moz-transform: rotate(0); 87 | -ms-transform: rotate(0); 88 | -o-transform: rotate(0); 89 | transform: rotate(0); 90 | } 91 | 92 | 20%, 93 | 60% { 94 | -webkit-transform: rotate(-25deg); 95 | -moz-transform: rotate(-25deg); 96 | -ms-transform: rotate(-25deg); 97 | -o-transform: rotate(-25deg); 98 | transform: rotate(-25deg); 99 | } 100 | 101 | 40%, 102 | 80% { 103 | -webkit-transform: rotate(10deg); 104 | -moz-transform: rotate(10deg); 105 | -ms-transform: rotate(10deg); 106 | -o-transform: rotate(10deg); 107 | transform: rotate(10deg); 108 | } 109 | } 110 | 111 | @keyframes octocat-wave { 112 | 113 | 0%, 114 | 100% { 115 | -webkit-transform: rotate(0); 116 | -moz-transform: rotate(0); 117 | -ms-transform: rotate(0); 118 | -o-transform: rotate(0); 119 | transform: rotate(0); 120 | } 121 | 122 | 20%, 123 | 60% { 124 | -webkit-transform: rotate(-25deg); 125 | -moz-transform: rotate(-25deg); 126 | -ms-transform: rotate(-25deg); 127 | -o-transform: rotate(-25deg); 128 | transform: rotate(-25deg); 129 | } 130 | 131 | 40%, 132 | 80% { 133 | -webkit-transform: rotate(10deg); 134 | -moz-transform: rotate(10deg); 135 | -ms-transform: rotate(10deg); 136 | -o-transform: rotate(10deg); 137 | transform: rotate(10deg); 138 | } 139 | } -------------------------------------------------------------------------------- /frontend/src/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/BookDetail.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 118 | -------------------------------------------------------------------------------- /frontend/src/components/Books.vue: -------------------------------------------------------------------------------- 1 | 2 | 79 | 80 | 135 | -------------------------------------------------------------------------------- /frontend/src/components/ChapterDetail.vue: -------------------------------------------------------------------------------- 1 | 2 | 152 | 153 | 516 | -------------------------------------------------------------------------------- /frontend/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import './common/theme/index.css' 5 | import './common/theme/github_corner.css' 6 | import ElementUI from 'element-ui' 7 | 8 | Vue.use(ElementUI); 9 | 10 | Vue.config.productionTip = false; 11 | 12 | new Vue({ 13 | router, 14 | render: h => h(App), 15 | }).$mount('#app'); 16 | -------------------------------------------------------------------------------- /frontend/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Books from './components/Books.vue'; 4 | import BookDetail from './components/BookDetail.vue'; 5 | import ChapterDetail from './components/ChapterDetail.vue'; 6 | 7 | Vue.use(Router); 8 | 9 | export default new Router({ 10 | mode: 'history', 11 | base: process.env.BASE_URL, 12 | routes: [{ 13 | path: '/', 14 | name: 'Books', 15 | component: Books, 16 | }, 17 | { 18 | path: '/book', 19 | name: 'BookDetail', 20 | component: BookDetail, 21 | }, 22 | { 23 | path: '/chapters', 24 | name: 'ChapterDetail', 25 | component: ChapterDetail, 26 | } 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /frontend/static/wordcloud2.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * wordcloud2.js 3 | * http://timdream.org/wordcloud2.js/ 4 | * 5 | * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors. 6 | * Released under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // setImmediate 12 | if (!window.setImmediate) { 13 | window.setImmediate = (function setupSetImmediate() { 14 | return window.msSetImmediate || 15 | window.webkitSetImmediate || 16 | window.mozSetImmediate || 17 | window.oSetImmediate || 18 | (function setupSetZeroTimeout() { 19 | if (!window.postMessage || !window.addEventListener) { 20 | return null; 21 | } 22 | 23 | var callbacks = [undefined]; 24 | var message = 'zero-timeout-message'; 25 | 26 | // Like setTimeout, but only takes a function argument. There's 27 | // no time argument (always zero) and no arguments (you have to 28 | // use a closure). 29 | var setZeroTimeout = function setZeroTimeout(callback) { 30 | var id = callbacks.length; 31 | callbacks.push(callback); 32 | window.postMessage(message + id.toString(36), '*'); 33 | 34 | return id; 35 | }; 36 | 37 | window.addEventListener('message', function setZeroTimeoutMessage(evt) { 38 | // Skipping checking event source, retarded IE confused this window 39 | // object with another in the presence of iframe 40 | if (typeof evt.data !== 'string' || 41 | evt.data.substr(0, message.length) !== message 42 | /* || 43 | evt.source !== window */ 44 | ) { 45 | return; 46 | } 47 | 48 | evt.stopImmediatePropagation(); 49 | 50 | var id = parseInt(evt.data.substr(message.length), 36); 51 | if (!callbacks[id]) { 52 | return; 53 | } 54 | 55 | callbacks[id](); 56 | callbacks[id] = undefined; 57 | }, true); 58 | 59 | /* specify clearImmediate() here since we need the scope */ 60 | window.clearImmediate = function clearZeroTimeout(id) { 61 | if (!callbacks[id]) { 62 | return; 63 | } 64 | 65 | callbacks[id] = undefined; 66 | }; 67 | 68 | return setZeroTimeout; 69 | })() || 70 | // fallback 71 | function setImmediateFallback(fn) { 72 | window.setTimeout(fn, 0); 73 | }; 74 | })(); 75 | } 76 | 77 | if (!window.clearImmediate) { 78 | window.clearImmediate = (function setupClearImmediate() { 79 | return window.msClearImmediate || 80 | window.webkitClearImmediate || 81 | window.mozClearImmediate || 82 | window.oClearImmediate || 83 | // "clearZeroTimeout" is implement on the previous block || 84 | // fallback 85 | function clearImmediateFallback(timer) { 86 | window.clearTimeout(timer); 87 | }; 88 | })(); 89 | } 90 | 91 | (function (global) { 92 | 93 | // Check if WordCloud can run on this browser 94 | var isSupported = (function isSupported() { 95 | var canvas = document.createElement('canvas'); 96 | if (!canvas || !canvas.getContext) { 97 | return false; 98 | } 99 | 100 | var ctx = canvas.getContext('2d'); 101 | if (!ctx) { 102 | return false; 103 | } 104 | if (!ctx.getImageData) { 105 | return false; 106 | } 107 | if (!ctx.fillText) { 108 | return false; 109 | } 110 | 111 | if (!Array.prototype.some) { 112 | return false; 113 | } 114 | if (!Array.prototype.push) { 115 | return false; 116 | } 117 | 118 | return true; 119 | }()); 120 | 121 | // Find out if the browser impose minium font size by 122 | // drawing small texts on a canvas and measure it's width. 123 | var minFontSize = (function getMinFontSize() { 124 | if (!isSupported) { 125 | return; 126 | } 127 | 128 | var ctx = document.createElement('canvas').getContext('2d'); 129 | 130 | // start from 20 131 | var size = 20; 132 | 133 | // two sizes to measure 134 | var hanWidth, mWidth; 135 | 136 | while (size) { 137 | ctx.font = size.toString(10) + 'px sans-serif'; 138 | if ((ctx.measureText('\uFF37').width === hanWidth) && 139 | (ctx.measureText('m').width) === mWidth) { 140 | return (size + 1); 141 | } 142 | 143 | hanWidth = ctx.measureText('\uFF37').width; 144 | mWidth = ctx.measureText('m').width; 145 | 146 | size--; 147 | } 148 | 149 | return 0; 150 | })(); 151 | 152 | // Based on http://jsfromhell.com/array/shuffle 153 | var shuffleArray = function shuffleArray(arr) { 154 | for (var j, x, i = arr.length; i; j = Math.floor(Math.random() * i), 155 | x = arr[--i], arr[i] = arr[j], 156 | arr[j] = x) {} 157 | return arr; 158 | }; 159 | 160 | var WordCloud = function WordCloud(elements, options) { 161 | if (!isSupported) { 162 | return; 163 | } 164 | 165 | if (!Array.isArray(elements)) { 166 | elements = [elements]; 167 | } 168 | 169 | elements.forEach(function (el, i) { 170 | if (typeof el === 'string') { 171 | elements[i] = document.getElementById(el); 172 | if (!elements[i]) { 173 | throw 'The element id specified is not found.'; 174 | } 175 | } else if (!el.tagName && !el.appendChild) { 176 | throw 'You must pass valid HTML elements, or ID of the element.'; 177 | } 178 | }); 179 | 180 | /* Default values to be overwritten by options object */ 181 | var settings = { 182 | list: [], 183 | fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + 184 | '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', 185 | fontWeight: 'normal', 186 | color: 'random-dark', 187 | minSize: 0, // 0 to disable 188 | weightFactor: 1, 189 | clearCanvas: true, 190 | backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) 191 | 192 | gridSize: 8, 193 | drawOutOfBound: false, 194 | shrinkToFit: false, 195 | origin: null, 196 | 197 | drawMask: false, 198 | maskColor: 'rgba(255,0,0,0.3)', 199 | maskGapWidth: 0.3, 200 | 201 | wait: 0, 202 | abortThreshold: 0, // disabled 203 | abort: function noop() {}, 204 | 205 | minRotation: -Math.PI / 2, 206 | maxRotation: Math.PI / 2, 207 | rotationSteps: 0, 208 | 209 | shuffle: true, 210 | rotateRatio: 0.1, 211 | 212 | shape: 'circle', 213 | ellipticity: 0.65, 214 | 215 | classes: null, 216 | 217 | hover: null, 218 | click: null 219 | }; 220 | 221 | if (options) { 222 | for (var key in options) { 223 | if (key in settings) { 224 | settings[key] = options[key]; 225 | } 226 | } 227 | } 228 | 229 | /* Convert weightFactor into a function */ 230 | if (typeof settings.weightFactor !== 'function') { 231 | var factor = settings.weightFactor; 232 | settings.weightFactor = function weightFactor(pt) { 233 | return pt * factor; //in px 234 | }; 235 | } 236 | 237 | /* Convert shape into a function */ 238 | if (typeof settings.shape !== 'function') { 239 | switch (settings.shape) { 240 | case 'circle': 241 | /* falls through */ 242 | default: 243 | // 'circle' is the default and a shortcut in the code loop. 244 | settings.shape = 'circle'; 245 | break; 246 | 247 | case 'cardioid': 248 | settings.shape = function shapeCardioid(theta) { 249 | return 1 - Math.sin(theta); 250 | }; 251 | break; 252 | 253 | /* 254 | 255 | To work out an X-gon, one has to calculate "m", 256 | where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) 257 | http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 258 | 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 259 | 260 | Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) 261 | where t' equals to mod(t, 2PI/X); 262 | 263 | */ 264 | 265 | case 'diamond': 266 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 267 | // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D 268 | // +0+..+2*PI 269 | settings.shape = function shapeSquare(theta) { 270 | var thetaPrime = theta % (2 * Math.PI / 4); 271 | return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); 272 | }; 273 | break; 274 | 275 | case 'square': 276 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t 277 | // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI 278 | settings.shape = function shapeSquare(theta) { 279 | return Math.min( 280 | 1 / Math.abs(Math.cos(theta)), 281 | 1 / Math.abs(Math.sin(theta)) 282 | ); 283 | }; 284 | break; 285 | 286 | case 'triangle-forward': 287 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 288 | // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 289 | // %29%29%2C+t+%3D+0+..+2*PI 290 | settings.shape = function shapeTriangle(theta) { 291 | var thetaPrime = theta % (2 * Math.PI / 3); 292 | return 1 / (Math.cos(thetaPrime) + 293 | Math.sqrt(3) * Math.sin(thetaPrime)); 294 | }; 295 | break; 296 | 297 | case 'triangle': 298 | case 'triangle-upright': 299 | settings.shape = function shapeTriangle(theta) { 300 | var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3); 301 | return 1 / (Math.cos(thetaPrime) + 302 | Math.sqrt(3) * Math.sin(thetaPrime)); 303 | }; 304 | break; 305 | 306 | case 'pentagon': 307 | settings.shape = function shapePentagon(theta) { 308 | var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5); 309 | return 1 / (Math.cos(thetaPrime) + 310 | 0.726543 * Math.sin(thetaPrime)); 311 | }; 312 | break; 313 | 314 | case 'star': 315 | settings.shape = function shapeStar(theta) { 316 | var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10); 317 | if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) { 318 | return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) + 319 | 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)); 320 | } else { 321 | return 1 / (Math.cos(thetaPrime) + 322 | 3.07768 * Math.sin(thetaPrime)); 323 | } 324 | }; 325 | break; 326 | } 327 | } 328 | 329 | /* Make sure gridSize is a whole number and is not smaller than 4px */ 330 | settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); 331 | 332 | /* shorthand */ 333 | var g = settings.gridSize; 334 | var maskRectWidth = g - settings.maskGapWidth; 335 | 336 | /* normalize rotation settings */ 337 | var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); 338 | var rotationSteps = Math.abs(Math.floor(settings.rotationSteps)); 339 | var minRotation = Math.min(settings.maxRotation, settings.minRotation); 340 | 341 | /* information/object available to all functions, set when start() */ 342 | var grid, // 2d array containing filling information 343 | ngx, ngy, // width and height of the grid 344 | center, // position of the center of the cloud 345 | maxRadius; 346 | 347 | /* timestamp for measuring each putWord() action */ 348 | var escapeTime; 349 | 350 | /* function for getting the color of the text */ 351 | var getTextColor; 352 | 353 | function random_hsl_color(min, max) { 354 | return 'hsl(' + 355 | (Math.random() * 360).toFixed() + ',' + 356 | (Math.random() * 30 + 70).toFixed() + '%,' + 357 | (Math.random() * (max - min) + min).toFixed() + '%)'; 358 | } 359 | switch (settings.color) { 360 | case 'random-dark': 361 | getTextColor = function getRandomDarkColor() { 362 | return random_hsl_color(10, 50); 363 | }; 364 | break; 365 | 366 | case 'random-light': 367 | getTextColor = function getRandomLightColor() { 368 | return random_hsl_color(50, 90); 369 | }; 370 | break; 371 | 372 | default: 373 | if (typeof settings.color === 'function') { 374 | getTextColor = settings.color; 375 | } 376 | break; 377 | } 378 | 379 | /* function for getting the font-weight of the text */ 380 | var getTextFontWeight; 381 | if (typeof settings.fontWeight === 'function') { 382 | getTextFontWeight = settings.fontWeight; 383 | } 384 | 385 | /* function for getting the classes of the text */ 386 | var getTextClasses = null; 387 | if (typeof settings.classes === 'function') { 388 | getTextClasses = settings.classes; 389 | } 390 | 391 | /* Interactive */ 392 | var interactive = false; 393 | var infoGrid = []; 394 | var hovered; 395 | 396 | var getInfoGridFromMouseTouchEvent = 397 | function getInfoGridFromMouseTouchEvent(evt) { 398 | var canvas = evt.currentTarget; 399 | var rect = canvas.getBoundingClientRect(); 400 | var clientX; 401 | var clientY; 402 | /** Detect if touches are available */ 403 | if (evt.touches) { 404 | clientX = evt.touches[0].clientX; 405 | clientY = evt.touches[0].clientY; 406 | } else { 407 | clientX = evt.clientX; 408 | clientY = evt.clientY; 409 | } 410 | var eventX = clientX - rect.left; 411 | var eventY = clientY - rect.top; 412 | 413 | var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g); 414 | var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g); 415 | 416 | return infoGrid[x][y]; 417 | }; 418 | 419 | var wordcloudhover = function wordcloudhover(evt) { 420 | var info = getInfoGridFromMouseTouchEvent(evt); 421 | 422 | if (hovered === info) { 423 | return; 424 | } 425 | 426 | hovered = info; 427 | if (!info) { 428 | settings.hover(undefined, undefined, evt); 429 | 430 | return; 431 | } 432 | 433 | settings.hover(info.item, info.dimension, evt); 434 | 435 | }; 436 | 437 | var wordcloudclick = function wordcloudclick(evt) { 438 | var info = getInfoGridFromMouseTouchEvent(evt); 439 | if (!info) { 440 | return; 441 | } 442 | 443 | settings.click(info.item, info.dimension, evt); 444 | evt.preventDefault(); 445 | }; 446 | 447 | /* Get points on the grid for a given radius away from the center */ 448 | var pointsAtRadius = []; 449 | var getPointsAtRadius = function getPointsAtRadius(radius) { 450 | if (pointsAtRadius[radius]) { 451 | return pointsAtRadius[radius]; 452 | } 453 | 454 | // Look for these number of points on each radius 455 | var T = radius * 8; 456 | 457 | // Getting all the points at this radius 458 | var t = T; 459 | var points = []; 460 | 461 | if (radius === 0) { 462 | points.push([center[0], center[1], 0]); 463 | } 464 | 465 | while (t--) { 466 | // distort the radius to put the cloud in shape 467 | var rx = 1; 468 | if (settings.shape !== 'circle') { 469 | rx = settings.shape(t / T * 2 * Math.PI); // 0 to 1 470 | } 471 | 472 | // Push [x, y, t]; t is used solely for getTextColor() 473 | points.push([ 474 | center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), 475 | center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * 476 | settings.ellipticity, 477 | t / T * 2 * Math.PI 478 | ]); 479 | } 480 | 481 | pointsAtRadius[radius] = points; 482 | return points; 483 | }; 484 | 485 | /* Return true if we had spent too much time */ 486 | var exceedTime = function exceedTime() { 487 | return ((settings.abortThreshold > 0) && 488 | ((new Date()).getTime() - escapeTime > settings.abortThreshold)); 489 | }; 490 | 491 | /* Get the deg of rotation according to settings, and luck. */ 492 | var getRotateDeg = function getRotateDeg() { 493 | if (settings.rotateRatio === 0) { 494 | return 0; 495 | } 496 | 497 | if (Math.random() > settings.rotateRatio) { 498 | return 0; 499 | } 500 | 501 | if (rotationRange === 0) { 502 | return minRotation; 503 | } 504 | 505 | if (rotationSteps > 0) { 506 | // Min rotation + zero or more steps * span of one step 507 | return minRotation + 508 | Math.floor(Math.random() * rotationSteps) * 509 | rotationRange / (rotationSteps - 1); 510 | } else { 511 | return minRotation + Math.random() * rotationRange; 512 | } 513 | }; 514 | 515 | var getTextInfo = function getTextInfo(word, weight, rotateDeg) { 516 | // calculate the acutal font size 517 | // fontSize === 0 means weightFactor function wants the text skipped, 518 | // and size < minSize means we cannot draw the text. 519 | var debug = false; 520 | var fontSize = settings.weightFactor(weight); 521 | if (fontSize <= settings.minSize) { 522 | return false; 523 | } 524 | 525 | // Scale factor here is to make sure fillText is not limited by 526 | // the minium font size set by browser. 527 | // It will always be 1 or 2n. 528 | var mu = 1; 529 | if (fontSize < minFontSize) { 530 | mu = (function calculateScaleFactor() { 531 | var mu = 2; 532 | while (mu * fontSize < minFontSize) { 533 | mu += 2; 534 | } 535 | return mu; 536 | })(); 537 | } 538 | 539 | // Get fontWeight that will be used to set fctx.font 540 | var fontWeight; 541 | if (getTextFontWeight) { 542 | fontWeight = getTextFontWeight(word, weight, fontSize); 543 | } else { 544 | fontWeight = settings.fontWeight; 545 | } 546 | 547 | var fcanvas = document.createElement('canvas'); 548 | var fctx = fcanvas.getContext('2d', { 549 | willReadFrequently: true 550 | }); 551 | 552 | fctx.font = fontWeight + ' ' + 553 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 554 | 555 | // Estimate the dimension of the text with measureText(). 556 | var fw = fctx.measureText(word).width / mu; 557 | var fh = Math.max(fontSize * mu, 558 | fctx.measureText('m').width, 559 | fctx.measureText('\uFF37').width) / mu; 560 | 561 | // Create a boundary box that is larger than our estimates, 562 | // so text don't get cut of (it sill might) 563 | var boxWidth = fw + fh * 2; 564 | var boxHeight = fh * 3; 565 | var fgw = Math.ceil(boxWidth / g); 566 | var fgh = Math.ceil(boxHeight / g); 567 | boxWidth = fgw * g; 568 | boxHeight = fgh * g; 569 | 570 | // Calculate the proper offsets to make the text centered at 571 | // the preferred position. 572 | 573 | // This is simply half of the width. 574 | var fillTextOffsetX = -fw / 2; 575 | // Instead of moving the box to the exact middle of the preferred 576 | // position, for Y-offset we move 0.4 instead, so Latin alphabets look 577 | // vertical centered. 578 | var fillTextOffsetY = -fh * 0.4; 579 | 580 | // Calculate the actual dimension of the canvas, considering the rotation. 581 | var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + 582 | boxHeight * Math.abs(Math.cos(rotateDeg))) / g); 583 | var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + 584 | boxHeight * Math.abs(Math.sin(rotateDeg))) / g); 585 | var width = cgw * g; 586 | var height = cgh * g; 587 | 588 | fcanvas.setAttribute('width', width); 589 | fcanvas.setAttribute('height', height); 590 | 591 | if (debug) { 592 | // Attach fcanvas to the DOM 593 | document.body.appendChild(fcanvas); 594 | // Save it's state so that we could restore and draw the grid correctly. 595 | fctx.save(); 596 | } 597 | 598 | // Scale the canvas with |mu|. 599 | fctx.scale(1 / mu, 1 / mu); 600 | fctx.translate(width * mu / 2, height * mu / 2); 601 | fctx.rotate(-rotateDeg); 602 | 603 | // Once the width/height is set, ctx info will be reset. 604 | // Set it again here. 605 | fctx.font = fontWeight + ' ' + 606 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 607 | 608 | // Fill the text into the fcanvas. 609 | // XXX: We cannot because textBaseline = 'top' here because 610 | // Firefox and Chrome uses different default line-height for canvas. 611 | // Please read https://bugzil.la/737852#c6. 612 | // Here, we use textBaseline = 'middle' and draw the text at exactly 613 | // 0.5 * fontSize lower. 614 | fctx.fillStyle = '#000'; 615 | fctx.textBaseline = 'middle'; 616 | fctx.fillText(word, fillTextOffsetX * mu, 617 | (fillTextOffsetY + fontSize * 0.5) * mu); 618 | 619 | // Get the pixels of the text 620 | var imageData = fctx.getImageData(0, 0, width, height).data; 621 | 622 | if (exceedTime()) { 623 | return false; 624 | } 625 | 626 | if (debug) { 627 | // Draw the box of the original estimation 628 | fctx.strokeRect(fillTextOffsetX * mu, 629 | fillTextOffsetY, fw * mu, fh * mu); 630 | fctx.restore(); 631 | } 632 | 633 | // Read the pixels and save the information to the occupied array 634 | var occupied = []; 635 | var gx = cgw, 636 | gy, x, y; 637 | var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; 638 | while (gx--) { 639 | gy = cgh; 640 | while (gy--) { 641 | y = g; 642 | singleGridLoop: { 643 | while (y--) { 644 | x = g; 645 | while (x--) { 646 | if (imageData[((gy * g + y) * width + 647 | (gx * g + x)) * 4 + 3]) { 648 | occupied.push([gx, gy]); 649 | 650 | if (gx < bounds[3]) { 651 | bounds[3] = gx; 652 | } 653 | if (gx > bounds[1]) { 654 | bounds[1] = gx; 655 | } 656 | if (gy < bounds[0]) { 657 | bounds[0] = gy; 658 | } 659 | if (gy > bounds[2]) { 660 | bounds[2] = gy; 661 | } 662 | 663 | if (debug) { 664 | fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; 665 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 666 | } 667 | break singleGridLoop; 668 | } 669 | } 670 | } 671 | if (debug) { 672 | fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; 673 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 674 | } 675 | } 676 | } 677 | } 678 | 679 | if (debug) { 680 | fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; 681 | fctx.fillRect(bounds[3] * g, 682 | bounds[0] * g, 683 | (bounds[1] - bounds[3] + 1) * g, 684 | (bounds[2] - bounds[0] + 1) * g); 685 | } 686 | 687 | // Return information needed to create the text on the real canvas 688 | return { 689 | mu: mu, 690 | occupied: occupied, 691 | bounds: bounds, 692 | gw: cgw, 693 | gh: cgh, 694 | fillTextOffsetX: fillTextOffsetX, 695 | fillTextOffsetY: fillTextOffsetY, 696 | fillTextWidth: fw, 697 | fillTextHeight: fh, 698 | fontSize: fontSize 699 | }; 700 | }; 701 | 702 | /* Determine if there is room available in the given dimension */ 703 | var canFitText = function canFitText(gx, gy, gw, gh, occupied) { 704 | // Go through the occupied points, 705 | // return false if the space is not available. 706 | var i = occupied.length; 707 | while (i--) { 708 | var px = gx + occupied[i][0]; 709 | var py = gy + occupied[i][1]; 710 | 711 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 712 | if (!settings.drawOutOfBound) { 713 | return false; 714 | } 715 | continue; 716 | } 717 | 718 | if (!grid[px][py]) { 719 | return false; 720 | } 721 | } 722 | return true; 723 | }; 724 | 725 | /* Actually draw the text on the grid */ 726 | var drawText = function drawText(gx, gy, info, word, weight, 727 | distance, theta, rotateDeg, attributes) { 728 | 729 | var fontSize = info.fontSize; 730 | var color; 731 | if (getTextColor) { 732 | color = getTextColor(word, weight, fontSize, distance, theta); 733 | } else { 734 | color = settings.color; 735 | } 736 | 737 | // get fontWeight that will be used to set ctx.font and font style rule 738 | var fontWeight; 739 | if (getTextFontWeight) { 740 | fontWeight = getTextFontWeight(word, weight, fontSize); 741 | } else { 742 | fontWeight = settings.fontWeight; 743 | } 744 | 745 | var classes; 746 | if (getTextClasses) { 747 | classes = getTextClasses(word, weight, fontSize); 748 | } else { 749 | classes = settings.classes; 750 | } 751 | 752 | var dimension; 753 | var bounds = info.bounds; 754 | dimension = { 755 | x: (gx + bounds[3]) * g, 756 | y: (gy + bounds[0]) * g, 757 | w: (bounds[1] - bounds[3] + 1) * g, 758 | h: (bounds[2] - bounds[0] + 1) * g 759 | }; 760 | 761 | elements.forEach(function (el) { 762 | if (el.getContext) { 763 | var ctx = el.getContext('2d'); 764 | var mu = info.mu; 765 | 766 | // Save the current state before messing it 767 | ctx.save(); 768 | ctx.scale(1 / mu, 1 / mu); 769 | 770 | ctx.font = fontWeight + ' ' + 771 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 772 | ctx.fillStyle = color; 773 | 774 | // Translate the canvas position to the origin coordinate of where 775 | // the text should be put. 776 | ctx.translate((gx + info.gw / 2) * g * mu, 777 | (gy + info.gh / 2) * g * mu); 778 | 779 | if (rotateDeg !== 0) { 780 | ctx.rotate(-rotateDeg); 781 | } 782 | 783 | // Finally, fill the text. 784 | 785 | // XXX: We cannot because textBaseline = 'top' here because 786 | // Firefox and Chrome uses different default line-height for canvas. 787 | // Please read https://bugzil.la/737852#c6. 788 | // Here, we use textBaseline = 'middle' and draw the text at exactly 789 | // 0.5 * fontSize lower. 790 | ctx.textBaseline = 'middle'; 791 | ctx.fillText(word, info.fillTextOffsetX * mu, 792 | (info.fillTextOffsetY + fontSize * 0.5) * mu); 793 | 794 | // The below box is always matches how s are positioned 795 | /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, 796 | info.fillTextWidth, info.fillTextHeight); */ 797 | 798 | // Restore the state. 799 | ctx.restore(); 800 | } else { 801 | // drawText on DIV element 802 | var span = document.createElement('span'); 803 | var transformRule = ''; 804 | transformRule = 'rotate(' + (-rotateDeg / Math.PI * 180) + 'deg) '; 805 | if (info.mu !== 1) { 806 | transformRule += 807 | 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + 808 | 'scale(' + (1 / info.mu) + ')'; 809 | } 810 | var styleRules = { 811 | 'position': 'absolute', 812 | 'display': 'block', 813 | 'font': fontWeight + ' ' + 814 | (fontSize * info.mu) + 'px ' + settings.fontFamily, 815 | 'left': ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px', 816 | 'top': ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px', 817 | 'width': info.fillTextWidth + 'px', 818 | 'height': info.fillTextHeight + 'px', 819 | 'lineHeight': fontSize + 'px', 820 | 'whiteSpace': 'nowrap', 821 | 'transform': transformRule, 822 | 'webkitTransform': transformRule, 823 | 'msTransform': transformRule, 824 | 'transformOrigin': '50% 40%', 825 | 'webkitTransformOrigin': '50% 40%', 826 | 'msTransformOrigin': '50% 40%' 827 | }; 828 | if (color) { 829 | styleRules.color = color; 830 | } 831 | span.textContent = word; 832 | for (var cssProp in styleRules) { 833 | span.style[cssProp] = styleRules[cssProp]; 834 | } 835 | if (attributes) { 836 | for (var attribute in attributes) { 837 | span.setAttribute(attribute, attributes[attribute]); 838 | } 839 | } 840 | if (classes) { 841 | span.className += classes; 842 | } 843 | el.appendChild(span); 844 | } 845 | }); 846 | }; 847 | 848 | /* Help function to updateGrid */ 849 | var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) { 850 | if (x >= ngx || y >= ngy || x < 0 || y < 0) { 851 | return; 852 | } 853 | 854 | grid[x][y] = false; 855 | 856 | if (drawMask) { 857 | var ctx = elements[0].getContext('2d'); 858 | ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth); 859 | } 860 | 861 | if (interactive) { 862 | infoGrid[x][y] = { 863 | item: item, 864 | dimension: dimension 865 | }; 866 | } 867 | }; 868 | 869 | /* Update the filling information of the given space with occupied points. 870 | Draw the mask on the canvas if necessary. */ 871 | var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { 872 | var occupied = info.occupied; 873 | var drawMask = settings.drawMask; 874 | var ctx; 875 | if (drawMask) { 876 | ctx = elements[0].getContext('2d'); 877 | ctx.save(); 878 | ctx.fillStyle = settings.maskColor; 879 | } 880 | 881 | var dimension; 882 | if (interactive) { 883 | var bounds = info.bounds; 884 | dimension = { 885 | x: (gx + bounds[3]) * g, 886 | y: (gy + bounds[0]) * g, 887 | w: (bounds[1] - bounds[3] + 1) * g, 888 | h: (bounds[2] - bounds[0] + 1) * g 889 | }; 890 | } 891 | 892 | var i = occupied.length; 893 | while (i--) { 894 | var px = gx + occupied[i][0]; 895 | var py = gy + occupied[i][1]; 896 | 897 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 898 | continue; 899 | } 900 | 901 | fillGridAt(px, py, drawMask, dimension, item); 902 | } 903 | 904 | if (drawMask) { 905 | ctx.restore(); 906 | } 907 | }; 908 | 909 | /* putWord() processes each item on the list, 910 | calculate it's size and determine it's position, and actually 911 | put it on the canvas. */ 912 | var putWord = function putWord(item) { 913 | var word, weight, attributes; 914 | if (Array.isArray(item)) { 915 | word = item[0]; 916 | weight = item[1]; 917 | } else { 918 | word = item.word; 919 | weight = item.weight; 920 | attributes = item.attributes; 921 | } 922 | var rotateDeg = getRotateDeg(); 923 | 924 | // get info needed to put the text onto the canvas 925 | var info = getTextInfo(word, weight, rotateDeg); 926 | 927 | // not getting the info means we shouldn't be drawing this one. 928 | if (!info) { 929 | return false; 930 | } 931 | 932 | if (exceedTime()) { 933 | return false; 934 | } 935 | 936 | // If drawOutOfBound is set to false, 937 | // skip the loop if we have already know the bounding box of 938 | // word is larger than the canvas. 939 | if (!settings.drawOutOfBound) { 940 | var bounds = info.bounds; 941 | if ((bounds[1] - bounds[3] + 1) > ngx || 942 | (bounds[2] - bounds[0] + 1) > ngy) { 943 | return false; 944 | } 945 | } 946 | 947 | // Determine the position to put the text by 948 | // start looking for the nearest points 949 | var r = maxRadius + 1; 950 | 951 | var tryToPutWordAtPoint = function (gxy) { 952 | var gx = Math.floor(gxy[0] - info.gw / 2); 953 | var gy = Math.floor(gxy[1] - info.gh / 2); 954 | var gw = info.gw; 955 | var gh = info.gh; 956 | 957 | // If we cannot fit the text at this position, return false 958 | // and go to the next position. 959 | if (!canFitText(gx, gy, gw, gh, info.occupied)) { 960 | return false; 961 | } 962 | 963 | // Actually put the text on the canvas 964 | drawText(gx, gy, info, word, weight, 965 | (maxRadius - r), gxy[2], rotateDeg, attributes); 966 | 967 | // Mark the spaces on the grid as filled 968 | updateGrid(gx, gy, gw, gh, info, item); 969 | 970 | // Return true so some() will stop and also return true. 971 | return true; 972 | }; 973 | 974 | while (r--) { 975 | var points = getPointsAtRadius(maxRadius - r); 976 | 977 | if (settings.shuffle) { 978 | points = [].concat(points); 979 | shuffleArray(points); 980 | } 981 | 982 | // Try to fit the words by looking at each point. 983 | // array.some() will stop and return true 984 | // when putWordAtPoint() returns true. 985 | // If all the points returns false, array.some() returns false. 986 | var drawn = points.some(tryToPutWordAtPoint); 987 | 988 | if (drawn) { 989 | // leave putWord() and return true 990 | return true; 991 | } 992 | } 993 | if (settings.shrinkToFit) { 994 | if (Array.isArray(item)) { 995 | item[1] = item[1] * 3 / 4; 996 | } else { 997 | item.weight = item.weight * 3 / 4; 998 | } 999 | return putWord(item); 1000 | } 1001 | // we tried all distances but text won't fit, return false 1002 | return false; 1003 | }; 1004 | 1005 | /* Send DOM event to all elements. Will stop sending event and return 1006 | if the previous one is canceled (for cancelable events). */ 1007 | var sendEvent = function sendEvent(type, cancelable, details) { 1008 | if (cancelable) { 1009 | return !elements.some(function (el) { 1010 | var event = new CustomEvent(type, { 1011 | detail: details || {} 1012 | }); 1013 | return !el.dispatchEvent(event); 1014 | }, this); 1015 | } else { 1016 | elements.forEach(function (el) { 1017 | var event = new CustomEvent(type, { 1018 | detail: details || {} 1019 | }); 1020 | el.dispatchEvent(event); 1021 | }, this); 1022 | } 1023 | }; 1024 | 1025 | /* Start drawing on a canvas */ 1026 | var start = function start() { 1027 | // For dimensions, clearCanvas etc., 1028 | // we only care about the first element. 1029 | var canvas = elements[0]; 1030 | 1031 | if (canvas.getContext) { 1032 | ngx = Math.ceil(canvas.width / g); 1033 | ngy = Math.ceil(canvas.height / g); 1034 | } else { 1035 | var rect = canvas.getBoundingClientRect(); 1036 | ngx = Math.ceil(rect.width / g); 1037 | ngy = Math.ceil(rect.height / g); 1038 | } 1039 | 1040 | // Sending a wordcloudstart event which cause the previous loop to stop. 1041 | // Do nothing if the event is canceled. 1042 | if (!sendEvent('wordcloudstart', true)) { 1043 | return; 1044 | } 1045 | 1046 | // Determine the center of the word cloud 1047 | center = (settings.origin) ? [settings.origin[0] / g, settings.origin[1] / g] : [ngx / 1048 | 2, ngy / 2 1049 | ]; 1050 | 1051 | // Maxium radius to look for space 1052 | maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); 1053 | 1054 | /* Clear the canvas only if the clearCanvas is set, 1055 | if not, update the grid to the current canvas state */ 1056 | grid = []; 1057 | 1058 | var gx, gy, i; 1059 | if (!canvas.getContext || settings.clearCanvas) { 1060 | elements.forEach(function (el) { 1061 | if (el.getContext) { 1062 | var ctx = el.getContext('2d'); 1063 | ctx.fillStyle = settings.backgroundColor; 1064 | ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1065 | ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1066 | } else { 1067 | el.textContent = ''; 1068 | el.style.backgroundColor = settings.backgroundColor; 1069 | el.style.position = 'relative'; 1070 | } 1071 | }); 1072 | 1073 | /* fill the grid with empty state */ 1074 | gx = ngx; 1075 | while (gx--) { 1076 | grid[gx] = []; 1077 | gy = ngy; 1078 | while (gy--) { 1079 | grid[gx][gy] = true; 1080 | } 1081 | } 1082 | } else { 1083 | /* Determine bgPixel by creating 1084 | another canvas and fill the specified background color. */ 1085 | var bctx = document.createElement('canvas').getContext('2d'); 1086 | 1087 | bctx.fillStyle = settings.backgroundColor; 1088 | bctx.fillRect(0, 0, 1, 1); 1089 | var bgPixel = bctx.getImageData(0, 0, 1, 1).data; 1090 | 1091 | /* Read back the pixels of the canvas we got to tell which part of the 1092 | canvas is empty. 1093 | (no clearCanvas only works with a canvas, not divs) */ 1094 | var imageData = 1095 | canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data; 1096 | 1097 | gx = ngx; 1098 | var x, y; 1099 | while (gx--) { 1100 | grid[gx] = []; 1101 | gy = ngy; 1102 | while (gy--) { 1103 | y = g; 1104 | singleGridLoop: while (y--) { 1105 | x = g; 1106 | while (x--) { 1107 | i = 4; 1108 | while (i--) { 1109 | if (imageData[((gy * g + y) * ngx * g + 1110 | (gx * g + x)) * 4 + i] !== bgPixel[i]) { 1111 | grid[gx][gy] = false; 1112 | break singleGridLoop; 1113 | } 1114 | } 1115 | } 1116 | } 1117 | if (grid[gx][gy] !== false) { 1118 | grid[gx][gy] = true; 1119 | } 1120 | } 1121 | } 1122 | 1123 | imageData = bctx = bgPixel = undefined; 1124 | } 1125 | 1126 | // fill the infoGrid with empty state if we need it 1127 | if (settings.hover || settings.click) { 1128 | 1129 | interactive = true; 1130 | 1131 | /* fill the grid with empty state */ 1132 | gx = ngx + 1; 1133 | while (gx--) { 1134 | infoGrid[gx] = []; 1135 | } 1136 | 1137 | if (settings.hover) { 1138 | canvas.addEventListener('mousemove', wordcloudhover); 1139 | } 1140 | 1141 | if (settings.click) { 1142 | canvas.addEventListener('click', wordcloudclick); 1143 | canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; 1144 | } 1145 | 1146 | canvas.addEventListener('wordcloudstart', function stopInteraction() { 1147 | canvas.removeEventListener('wordcloudstart', stopInteraction); 1148 | 1149 | canvas.removeEventListener('mousemove', wordcloudhover); 1150 | canvas.removeEventListener('click', wordcloudclick); 1151 | hovered = undefined; 1152 | }); 1153 | } 1154 | 1155 | i = 0; 1156 | var loopingFunction, stoppingFunction; 1157 | if (settings.wait !== 0) { 1158 | loopingFunction = window.setTimeout; 1159 | stoppingFunction = window.clearTimeout; 1160 | } else { 1161 | loopingFunction = window.setImmediate; 1162 | stoppingFunction = window.clearImmediate; 1163 | } 1164 | 1165 | var addEventListener = function addEventListener(type, listener) { 1166 | elements.forEach(function (el) { 1167 | el.addEventListener(type, listener); 1168 | }, this); 1169 | }; 1170 | 1171 | var removeEventListener = function removeEventListener(type, listener) { 1172 | elements.forEach(function (el) { 1173 | el.removeEventListener(type, listener); 1174 | }, this); 1175 | }; 1176 | 1177 | var anotherWordCloudStart = function anotherWordCloudStart() { 1178 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1179 | stoppingFunction(timer); 1180 | }; 1181 | 1182 | addEventListener('wordcloudstart', anotherWordCloudStart); 1183 | 1184 | var timer = loopingFunction(function loop() { 1185 | if (i >= settings.list.length) { 1186 | stoppingFunction(timer); 1187 | sendEvent('wordcloudstop', false); 1188 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1189 | 1190 | return; 1191 | } 1192 | escapeTime = (new Date()).getTime(); 1193 | var drawn = putWord(settings.list[i]); 1194 | var canceled = !sendEvent('wordclouddrawn', true, { 1195 | item: settings.list[i], 1196 | drawn: drawn 1197 | }); 1198 | if (exceedTime() || canceled) { 1199 | stoppingFunction(timer); 1200 | settings.abort(); 1201 | sendEvent('wordcloudabort', false); 1202 | sendEvent('wordcloudstop', false); 1203 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1204 | return; 1205 | } 1206 | i++; 1207 | timer = loopingFunction(loop, settings.wait); 1208 | }, settings.wait); 1209 | }; 1210 | 1211 | // All set, start the drawing 1212 | start(); 1213 | }; 1214 | 1215 | WordCloud.isSupported = isSupported; 1216 | WordCloud.minFontSize = minFontSize; 1217 | 1218 | // Expose the library as an AMD module 1219 | if (typeof define === 'function' && define.amd) { 1220 | global.WordCloud = WordCloud; 1221 | define('wordcloud', [], function () { 1222 | return WordCloud; 1223 | }); 1224 | } else if (typeof module !== 'undefined' && module.exports) { 1225 | module.exports = WordCloud; 1226 | } else { 1227 | global.WordCloud = WordCloud; 1228 | } 1229 | 1230 | })(this); //jshint ignore:line 1231 | -------------------------------------------------------------------------------- /pics/api结果示例1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/api结果示例1.png -------------------------------------------------------------------------------- /pics/api结果示例2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/api结果示例2.png -------------------------------------------------------------------------------- /pics/api结果示例3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/api结果示例3.png -------------------------------------------------------------------------------- /pics/书籍.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/书籍.png -------------------------------------------------------------------------------- /pics/分词.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/分词.png -------------------------------------------------------------------------------- /pics/命名实体识别.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/命名实体识别.png -------------------------------------------------------------------------------- /pics/基础.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/基础.png -------------------------------------------------------------------------------- /pics/屏蔽.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/屏蔽.png -------------------------------------------------------------------------------- /pics/情感识别.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/情感识别.png -------------------------------------------------------------------------------- /pics/架构图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/架构图.png -------------------------------------------------------------------------------- /pics/词云1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/词云1.png -------------------------------------------------------------------------------- /pics/词云2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/词云2.png -------------------------------------------------------------------------------- /pics/词云3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/词云3.png -------------------------------------------------------------------------------- /pics/词云4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/词云4.png -------------------------------------------------------------------------------- /pics/词性标注.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/词性标注.png -------------------------------------------------------------------------------- /pics/首页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keleqnma/flask-vuejs-nlp/005b9be2ea19e865c96ebe9f459d32cd8db4be9d/pics/首页.png --------------------------------------------------------------------------------