如何从现有 Python 应用程序公开带有 REST API 和 HTML/JavaScript 应用程序的 Web 服务器?

How to expose a web server with REST API and HTML/JavaScript applications from an existing Python application?

我有一个现有的 Python 应用程序可以连续抓取 Internet。它使用 requests 包向各种 Internet 网站(例如 GitHub、Twitter 等)发出 HTTP 请求,并将可用数据下载到文件系统。它还向 GitHub 存储库和 Twitter 的 REST API 发出 HTTP 请求,并下载大量元数据。它一直在无限循环中执行此操作。每次迭代后,它调用 time.sleep(3600) 在下一次迭代之前休眠 1 小时。

现在我想从此应用程序的端口 80 上公开一个 HTTP 服务器,以便任何客户端都可以连接到此应用程序的端口 80 以查询其内部状态。例如,如果某人 运行s curl http://myapp/status 它应该用 {"status": "crawling"}{"status": "sleeping"} 来响应。如果有人使用他们的网络浏览器访问 http://myapp/status,它应该显示一个显示状态的 HTML 页面。根据检测到的用户代理,它将提供 REST API 响应或 HTML 页面。如果出于任何原因,我的应用程序出现故障或崩溃,对端口 80 的 HTTP 请求当然应该失败。

如何从应用程序公开这样的 HTTP 服务器?我想到使用 Django,因为随着项目的进行,它必须做很多繁重的工作,例如身份验证、防止 CSRF 攻击、接受用户输入和查询它拥有的数据等等。 Django 似乎很适合这个目的。但是 Django 的问题是我不能在我当前的应用程序中嵌入 Django。我必须 运行 一个单独的 uwsgi 服务器来为 Django 应用程序提供服务。 Flask 也存在同样的问题。

在 Python 中解决此类问题的正确方法是什么?

在我看来,您有两种解决此问题的高级方法:

  1. 有单独的应用程序(一个 "server" 和一个 "crawler"),它们有一些共享的数据存储(数据库、Redis 等)。每个应用程序都将独立运行,爬虫只需更新其在共享数据存储中的状态。这种方法可能可以更好地扩展:如果你在 Docker Swarm 之类的东西中旋转它,你可以在你负担得起的范围内尽可能多地扩展爬虫实例。
  2. 有一个应用程序为爬虫和服务器生成单独的线程。因为它们在同一个进程中,所以您可以更快地在它们之间共享信息(尽管如果只是爬虫状态应该无关紧要)。此选项的优势似乎只是难以启动它——您不需要共享数据存储,也不需要管理多个服务。

我个人更倾向于(1),因为每一部分都比较简单。接下来是 (1) 的解决方案,以及 (2) 的快速而肮脏的解决方案。

1。使用共享数据存储分离进程

我会使用 Docker Compose 来处理启动所有服务。它增加了一层额外的复杂性(因为您需要安装 Docker),但它极大地简化了服务管理。

整个Docker组合栈

基于示例配置 here 我会制作一个 ./docker-compose.yaml 文件,看起来像

version: '3'
services:
  server:
    build: ./server
    ports:
      - "80:80"
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
  crawler:
    build: ./crawler
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
  redis:
    image: "redis/alpine"
    container_name: cache
    expose: 
      - 6379

我会将应用程序组织到单独的目录中,例如 ./server./crawler,但这不是唯一的方法。无论您如何组织它们,上面配置中的 build 参数都应该匹配。

服务器

我会在 ./server/app.py 中编写一个简单的服务器来做类似

的事情
import os

from flask import Flask
import redis

app = Flask(__name__)
r_conn = redis.Redis(
    host=os.environ.get('REDIS_HOST'),
    port=6379
)

@app.route('/status')
def index():
    stat = r_conn.get('crawler_status')
    try:
        return stat.decode('utf-8')
    except:
        return 'error getting status', 500

app.run(host='0.0.0.0', port=8000)

连同一个 ./server/requirements.txt 具有依赖关系的文件

Flask
redis

最后 ./server/Dockerfile 告诉 Docker 如何构建您的服务器

FROM alpine:latest
# install Python
RUN apk add --no-cache python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache
# copy the app and make it your current directory
RUN mkdir -p /opt/server
COPY ./ /opt/server
WORKDIR /opt/server
# install deps and run server
RUN pip3 install -qr requirements.txt
EXPOSE 8000
CMD ["python3", "app.py"]

停下来检查一切是否正常

此时,如果您在 ./docker-compose.yaml 目录中打开 CMD 提示符或终端,您应该能够 运行 docker-compose build && docker-compose up 检查所有内容是否已构建,并且 运行 很高兴。您将需要禁用 YAML 文件的 crawler 部分(因为它尚未写入),但您应该能够启动与 Redis 通信的服务器。如果您对此感到满意,请取消注释 YAML 的 crawler 部分并继续。

爬虫进程

由于 Docker 处理重新启动爬虫进程,您实际上可以将其编写为一个非常简单的 Python 脚本。像 ./crawler/app.py 这样的东西可能看起来像

from time import sleep
import os
import sys

import redis

TIMEOUT = 3600  # seconds between runs
r_conn = redis.Redis(
    host=os.environ.get('REDIS_HOST'),
    port=6379
)

# ... update status and then do the work ...
r_conn.set('crawler_status', 'crawling')
sleep(60)
# ... okay, it's done, update status ...
r_conn.set('crawler_status', 'sleeping')

# sleep for a while, then exit so Docker can restart
sleep(TIMEOUT)
sys.exit(0)

然后像之前一样需要 ./crawler/requirements.txt 文件

redis

还有一个(非常类似于服务器的)./crawler/Dockerfile

FROM alpine:latest
# install Python
RUN apk add --no-cache python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache
# copy the app and make it your current directory
RUN mkdir -p /opt/crawler
COPY ./ /opt/crawler
WORKDIR /opt/crawler
# install deps and run server
RUN pip3 install -qr requirements.txt
# NOTE that no port is exposed
CMD ["python3", "app.py"]

总结

在 7 个文件中,您有两个由 Docker 管理的独立应用程序以及一个 Redis 实例。如果你想缩放它,你可以查看 --scale option for docker-compose up. This is not necessarily the simplest solution, but it manages some of the unpleasant bits about process management. For reference, I also made a Git repo for it here.

为了 运行 它作为一个无头服务,只是 运行 docker-compose up -d.

从这里开始,您可以并且应该向搜寻器添加更好的日志记录。您当然可以使用 Django 而不是 Flask 作为服务器(尽管我更熟悉 Flask,Django 可能会引入新的依赖项)。当然,你总是可以让它变得更复杂。

2。带线程的单进程

此解决方案不需要任何 Docker,并且应该只需要一个 Python 文件来管理。除非 OP 需要,否则我不会编写完整的解决方案,但基本草图类似于

import threading
import time

from flask import Flask

STATUS = ''

# run the server on another thread
def run_server():
    app = Flask(__name__)
    @app.route('/status')
    def index():
        return STATUS
server_thread = threading.Thread(target=run_server)
server_thread.start()

# run the crawler on another thread
def crawler_loop():
    while True:
        STATUS = 'crawling'
        # crawl and wait
        STATUS = 'sleeping'
        time.sleep(3600)
crawler_thread = threading.Thread(target=crawler_loop)
crawler_thread.start()

# main thread waits until the app is killed
try:
    while True:
        sleep(1)
except:
    server_thread.kill()
    crawler_thread.kill()

此解决方案不处理与保持服务活动有关的任何事情,实际上与错误处理有关,最后的块不会很好地处理来自 OS 的信号。也就是说,这是一个快速而肮脏的解决方案,可以让您脱离实际。