【问题标题】:Impyla Insert SQL from Flask: Syntax error (Identifier Binding)Impyla 从 Flask 插入 SQL:语法错误(标识符绑定)
【发布时间】:2019-05-13 07:47:09
【问题描述】:

最近我设置了一个 Flask POST 端点,通过 Impyla 模块将数据写入 Impala DB。

环境:CentOS 上的 Python 3.6.5。

Impala 版本:impalad 版本 2.6.0-cdh5.8.0

api.py:

from flask import Flask, request, abort, Response
from flask_cors import CORS
import json
from impala.dbapi import connect
import sys
import re
from datetime import datetime


app = application = Flask(__name__)
CORS(app)


conn = connect(host='datanode2', port=21050,
            user='user', database='testdb')


@app.route("/api/endpoint", methods=['POST'])
def post_data():
    # if not request.json:
    #     abort(400)

    params = request.get_json(force=True)  # getting request data
    print(">>>>>> ", params, flush=True)

    params['log_time'] = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
    # params['page_url'] = re.sub(
    #     '[^a-zA-Z0-9-_*.]', '', re.sub(':', '_', params['page_url']))

    try:
        cursor = conn.cursor()

        sql = "INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES (%s, %s, %s, %s, %s, %s)"
        values = (params['page_title'], params['page_url'], params['log_time'],
                params['machine'], params['clicks'], params['id'])
        print(">>>>>> " + sql % values, file=sys.stderr, flush=True)

        cursor.execute(sql, values)

        print(
            f">>>>>> Data Written Successfully", file=sys.stderr, flush=True)
        return Response(json.dumps({'success': True}), 201, mimetype="application/json")
    except Exception as e:
        print(e, file=sys.stderr, flush=True)
        return Response(json.dumps({'success': False}), 400, mimetype="application/json")


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5008, debug=True)

req.py:

import requests as r

url = "http://123.234.345.456:30001/"
# url =  "https://stackoverflow.com/questions/ask"

res = r.post('http://localhost:5008/api/endpoint', 
            json={             
                "page_title": "Home",   
                "page_url": url,
                "machine": "Mac OS",
                "clicks": 16,
                "id": "60cd1d79-eda7-44c2-a4ec-ffdd5d6ac3db"         
            }
        )

if res.ok:
    print(res.json())
else:
    print('Error!')

我使用python api.py 运行flask api,然后使用python req.py 对其进行测试。

烧瓶服务器给出这个错误:

>>>>>>  {'page_title': 'Home', 'page_url': 'http://123.234.345.456:30001/', 'machine': 'Mac OS', 'clicks': 16, 'id': '60cd1d79-eda7-44c2-a4ec-ffdd5d6ac3db'}
>>>>>> INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES (Home, http://123.234.345.456:30001/, 2018-12-12 16-14-04, Mac OS, 16, 60cd1d79-eda7-44c2-a4ec-ffdd5d6ac3db)
AnalysisException: Syntax error in line 1:
..., 'http://123.234.345.456'2018-12-12 16-14-04'0001/', ...
                         ^
Encountered: INTEGER LITERAL
Expected: AND, AS, ASC, BETWEEN, CROSS, DESC, DIV, ELSE, END, FOLLOWING, FROM, FULL, GROUP, HAVING, ILIKE, IN, INNER, IREGEXP, IS, JOIN, LEFT, LIKE, LIMIT, NOT, NULLS, OFFSET, OR, ORDER, PRECEDING, RANGE, REGEXP, RIGHT, RLIKE, ROWS, THEN, UNION, WHEN, WHERE, COMMA, IDENTIFIER

CAUSED BY: Exception: Syntax error

这个错误有点烦人:

  1. 我试过直接在impala-shell里面插入sql命令,效果很好。

  2. 当 page_url 是唯一的参数时,它也可以正常工作。

所以这是某种条件字符转义问题?我设法通过使用一些正则表达式(取消注释第 27 - 28 行)调整 url 来绕过这个问题。但这真的很烦人,我不想因此而清理我的数据。

当我检查其他人的试验时,认为为每个插入值添加一对引号会起作用。但是,在使用字符串格式时我该如何做到这一点,并且必须在cursor.execute(sql, values) 之前进行?

【问题讨论】:

    标签: python impala impyla


    【解决方案1】:

    经过一番努力,在@Scratch'N'Purr 和@msafiullahParameter substitution issue #317 的大力帮助下,我设法让它工作了。这有点复杂,所以我将发布完整的文档代码:

    错误原因:通过 Impyla API 出现冒号转义问题。

    解决方案:使用自定义转义函数处理数据,采用sql注入(Python的字符串格式化方式替换参数)代替标准的Python DB API e.g. cursor.execute(sql, values).

    api.py:

    from flask import Flask, request, abort, Response
    from flask_cors import CORS
    import json
    from impala.dbapi import connect
    from impala.util import _escape
    import sys    
    from datetime import datetime
    import six
    
    app = application = Flask(__name__)
    CORS(app)
    
    
    conn = connect(host='datanode2', port=21050,
                user='user', database='testdb')
    
    
    def parameterize(value): # by msafiullah
        if value is None:
            return "NULL"
        elif isinstance(value, six.string_types):
            return "'" + _escape(value) + "'"
        else:
            return str(value)
    
    
    @app.route("/api/endpoint", methods=['POST'])
    def post_data():
        if not request.json:
            abort(400)
    
        params = request.get_json(force=True)  # getting request data
        print(">>>>>> ", params, flush=True)
    
        params['log_time'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
        try:
            cursor = conn.cursor()
    
            sql = 'INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES ( CAST({} AS VARCHAR(64)), {}, {}, CAST({} AS VARCHAR(32)) , {}, CAST({} AS VARCHAR(32)))'\
                    .format(parameterize(params['page_title']), parameterize(params['page_url']), parameterize(params['log_time']), parameterize(params['machine']), params['clicks'], parameterize(params['id']))
            print(">>>>>> " + sql, file=sys.stderr, flush=True)
    
            cursor.execute(sql)
    
            print(
                f">>>>>> Data Written Successfully", file=sys.stderr, flush=True)
            return Response(json.dumps({'success': True}), 201, mimetype="application/json")
        except Exception as e:
            print(e, file=sys.stderr, flush=True)
            return Response(json.dumps({'success': False}), 400, mimetype="application/json")
    
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=5008, debug=True)
    

    req.py 与 Question 相同。

    table 架构:

    CREATE TABLE if not exists table (
        id VARCHAR(36),
        machine VARCHAR(32),
        clicks INT,
        page_title VARCHAR(64),
        page_url STRING,
        log_time TIMESTAMP
    );
    

    Flask 的服务器输出:

    >>>>>>  {'page_title': 'Home', 'page_url': 'http://123.234.345.456:30001/', 'machine': 'Mac OS', 'clicks': 16, 'id': '60cd1d79-eda7-44c2-a4ec-ffdd5d6ac3db'}
    >>>>>> INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES ( CAST('Home' AS VARCHAR(64)), 'http://123.234.345.456:30001/', '2018-12-14 17:27:29', CAST('Mac OS' AS VARCHAR(32)) , 16, CAST('60cd1d79-eda7-44c2-a4ec-ffdd5d6ac3db' AS VARCHAR(32)))
    >>>>>> Data Written Successfully
    127.0.0.1 - - [14/Dec/2018 17:27:29] "POST /api/endpoint HTTP/1.1" 201 -
    

    在 Impala-shell 中,select * from table 将给出:

    +----------------------------------+--------+--------------+------------+----------------------------------------------------------------------+---------------------+
    | id                               | machine | clicks      | page_title | page_url                                                             | log_time            |
    +----------------------------------+--------+--------------+------------+----------------------------------------------------------------------+---------------------+
    | 60cd1d79-eda7-44c2-a4ec-ffdd5d6a | Mac OS | 16           | Home       | http://123.234.345.456:30001/                                        | 2018-12-14 17:27:29 |
    +----------------------------------+--------+--------------+------------+----------------------------------------------------------------------+---------------------+
    

    基本上,只有数字(例如INT 类型)不需要经过parameterize() 清理/转义过程。其他类型如VARCHARCHARSTRINGTIMESTAMP(由于冒号)应正确转义以通过 Impyla API 安全插入。

    【讨论】:

    • 感谢分享!
    【解决方案2】:

    Impala 或其他基于 impala 的 python 库不支持参数化查询,传统 SQL db 支持的方式。如果值被定义为字符串/时间戳,我遇到的唯一解决方案是用引号括住插入值。

    您提到在执行查询之前使用字符串格式时如何执行此操作?很简单,只需应用字符串格式,然后插入格式化的值。

    在您的示例中,假设您的表具有以下类型定义:

    CREATE TABLE table (
        page_title VARCHAR(64),
        page_url STRING,
        log_time TIMESTAMP,
        machine VARCHAR(64),
        clicks INT,
        id CHAR(36)
    )
    

    那么您的插入语句将是:

    sql = "INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES ('%s', '%s', '%s', '%s', %s, '%s')"  # note the single quotes around the string/timestamp types
    

    现在由于log_time 是时间戳类型,您必须将datetime.now() 格式化为yyyy-MM-dd HH:mm:ss 格式。

    params['log_time'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    

    如果您已将 log_time 定义为 STRING 而不是 TIMESTAMP,那么您的 %Y-%m-%d %H-%M-%S 格式将有效。

    最后,执行:

    values = (params['page_title'], params['page_url'], params['log_time'],
              params['machine'], params['clicks'], params['id'])
    cursor.execute(sql, values)
    

    请注意,此方法仅适用于处理基本数据类型(例如数字或字符串)时。任何复杂的东西,比如数组或结构都行不通。

    【讨论】:

    • 但 Impyla 明确确实支持参数化查询,请参阅 github.com/cloudera/impyla/blob/master/impala/tests/…
    • @DanielRoseman 嗯,自从我上次使用 impyla 以来,这一定是新的。然后在这种情况下,不需要进行任何上游字符串格式化或将值括在引号中。我想知道它是否仍然适用于参数化插入。
    • @Scratch'N'Purr 非常感谢您的解决方案!由于转义问题,我实际上不得不为我的log_time 使用 STRING ......!现在我正试图将我的桌子改回正轨
    • @DanielRoseman 很有趣,也没有注意到这一点。如果这行得通,那就太好了。代码变得更简洁易读。
    • 嗯,确实很有趣。似乎参数化插入将 url 的 :3 视为占位符 ('http://123.234.345.456'2018-12-12 16-14-04'0001/')。也许唯一的其他选择是使用 sql 注入(例如sql = "INSERT INTO table ( page_title, page_url, log_time, machine, clicks, id ) VALUES ('{}', '{}', '{}', '{}', {}, '{}')".format(params['page_title'], params['page_url'], ...)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-16
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 2020-06-16
    • 1970-01-01
    相关资源
    最近更新 更多