【问题标题】:Django: Parse HTML (containing form) to dictionaryDjango:将 HTML(包含表单)解析为字典
【发布时间】:2021-04-10 16:52:20
【问题描述】:

我在服务器端创建了一个 html 表单。

<form action="." method="POST">
 <input type="text" name="foo" value="bar">
 <textarea name="area">long text</textarea>
 <select name="your-choice">
  <option value="a" selected>A</option>
  <option value="b">B</option>
 </select>
</form>

想要的结果:

{
 "foo": "bar",
 "area": "long text",
 "your-choice": "a",
}

我正在寻找的方法(parse_form())可以这样使用:

response = client.get('/foo/')

# response contains <form> ...</form>

data = parse_form(response.content)

data['my-input']='bar'

response = client.post('/foo/', data)

如何在Python中实现parse_form()

这个和django无关,不过django里面有个feature request,但是几年前就被拒绝了:https://code.djangoproject.com/ticket/11797

更新

我围绕基于 lxml 的答案编写了一个小型 Python 库:html_form_to_dict

【问题讨论】:

  • 您需要使用 Selenium 来执行此操作。
  • @markwalker_ 需要 Selenium 来将 html 表单与字典配对?
  • 你在谈论测试你的前端。最好用硒来完成。
  • @markwalker_ 到目前为止,我喜欢编写测试。几年前我用过 selenium,而且速度很慢。我想保持我的编辑测试周期快。我开发是因为我喜欢它,而不是因为我必须这样做。

标签: python django html-parsing


【解决方案1】:
from collections import UserDict

class FormData(UserDict):
    def __init__(self, *args, **kwargs):
        self.frozen = False
        super().__init__(*args, **kwargs)
        self.frozen = True
        
    def __setitem__(self, key, value):
        if self.frozen and key not in self:
            raise ValueError('Key %s is not in the dict. Available: %s' % (
                key, self.keys()
            ))
        super().__setitem__(key, value)

def parse_form(content):
    """
    Parse the first form in the html in content.
    """
    
    import lxml.html
    tree = lxml.html.fromstring(content)
    return FormData(tree.forms[0].fields)

示例用法:

def test_foo_form(user_client):
    url = reverse('foo')
    response = user_client.get(url)
    assert response.status_code == 200
    data = parse_form(response.content)
    response = user_client.post(url, data)
    assert response.status_code == 302

以上代码不完整。请不要复制+粘贴,而是使用库:https://github.com/guettli/html_form_to_dict

【讨论】:

  • @guetty ...请问:为什么不使用 BeautifulSoup 代替?一般来说,我喜欢极简主义的方法,并且讨厌在非严格要求的情况下添加工具,尤其是在单元测试中。然而,似乎每个人都同意 BeautifulSoup 是这种情况下 HTML 解析的事实标准,这可能就是为什么 Django 测试客户端没有添加解析支持的原因
  • @MarioOrlandi AFAIK BeautifulSoup 用于解析可能损坏/无效的 html。在我的情况下,html应该是干净的。如果没有,那么我想知道,因为 html 是由我创建的。 AFAIK 以上 BeautifulSoup 解决方案仅解析 input 元素,而不是
  • 我对BeautifulSoap没有什么重要的经验,但是前几天我需要在单元测试中解析响应内容;就我而言,我必须检查 HTML 的类;这就是我所做的:gist.github.com/morlandi/898f355b70a2845d2dd66e4bc686ff71 我喜欢用于搜索 DOM 的类似 jquery 的语法,因为我已经熟悉它了;我想您可以类似地从 textarea 或 select 中提取值;为什么你认为 BS 应该仅限于输入元素?
  • 由于在对前端进行单元测试时,HTML 解析是一种常见的需求,我正在寻找一种解决方案,它不会强迫我用太多的辅助函数使代码混乱,因为它们可能会分散注意力。
  • @MarioOrlandi 我找到了一个简单的 lxml 解决方案(见上面的答案)。如果您认为 BS 更好,请提供适用于 textareas 和 select 元素的解决方案。
【解决方案2】:

mechanize 工具提供了一个类似浏览器的界面来测试表单:

Python 中的有状态程序化网页浏览。使用简单的 HTML 表单填写和单击链接以编程方式浏览页面。

文档中的这个 sn-p 包含一个如何填写表格的示例:

import re
import mechanize

br = mechanize.Browser()
br.open("http://www.example.com/")
# follow second link with element text matching regular expression
response1 = br.follow_link(text_regex=r"cheese\s*shop", nr=1)
print(br.title())
print(response1.geturl())
print(response1.info())  # headers
print(response1.read())  # body

br.select_form(name="order")
# Browser passes through unknown attributes (including methods)
# to the selected HTMLForm.
br["cheeses"] = ["mozzarella", "caerphilly"]  # (the method here is __setitem__)
# Submit current form.  Browser calls .close() on the current response on
# navigation, so this closes response1
response2 = br.submit()

如果不需要 JavaScript 支持,这可以正常工作。

【讨论】:

    【解决方案3】:

    这个和django无关,只和html解析有关。标准工具是 BeautifulSoup (bs4) 库。

    它解析任意 HTML,并且经常用于网络爬虫(包括我自己的)。这个问题涵盖了解析 html 表单:Python beautiful soup form input parsing,几乎所有你需要的东西都在这里某个地方得到了回答:)

    from bs4 import BeautifulSoup
    
    def selected_option(select):
        option = select.find("option", selected=True)
        if option: 
            return option['value']
    
    # tag name => how to extract its value
    tags = {  
        "input": lambda t: t['value'],
        "textarea": lambda t: t.text,
        "select": selected_option
    }
    
    
    def parse_form(html):
        soup = BeautifulSoup(html, 'html.parser')
        form = soup.find("form")
        return {
            e['name']: tags[e.name](e)
            for e in form.find_all(tags.keys())
        }
    

    这会为您的输入提供以下输出:

    {
        "foo": "bar",
        "area": "long text",
        "your-choice": "a"
    }
    

    对于生产,您将需要添加大量错误检查,对于未找到表单、没有名称的输入等。这取决于具体需要什么。

    【讨论】:

    • 我猜到现在sn-p上面还没有读取textareas和select元素的值。
    • @guettli 当然,它没有。所以可以修改它来做到这一点,你的意思是什么?给出的唯一示例是input,如果这是需要的,那么这非常好。你的似乎很复杂,只需使用 BS 并结合一些单行来制作你理想的 dict 输出。你甚至可以使用 lxml 作为后端 :D
    • 也许你可以用一个有代表性的输入和输出来更新你的问题,否则这看起来就像你在玩一个奇怪的游戏。对于选择,您想如何处理输出的命名,因为您有“输入名称+选项值”并且多个选择可以具有相同的值,等等。有没有您想分享的完整示例?
    • 该问题包含 应该返回一个列表。
    • 新示例中的错字。 &lt;/texarea&gt; 缺少第二个 t
    【解决方案4】:

    为什么不只是这个?:

    def parse_form(content):
        import lxml.html
        tree = lxml.html.fromstring(content)
        return dict(tree.forms[0].fields)
    

    我猜不出使用 UserDict 的原因

    一点警告:我注意到当表单包含

    【讨论】:

    【解决方案5】:

    为了好玩,我尝试用 BeatifulSoap 复制 guettli 提出的解决方案。

    这是我出来的:

    from bs4 import BeautifulSoup
    
    
    def parse_form(content):
        data = {}
        html = BeautifulSoup(content, features="lxml")
        form = html.find('form', recursive=True)
        fields = form.find_all(('input', 'select', 'textarea'))
        for field in fields:
            name = field.get('name')
            if name:
                if field.name == 'input':
                    value = field.get('value')
                elif field.name == 'select':
                    try:
                        value = field.find_all('option', selected=True)[0].get('value')
                    except:
                        value = None
                elif field.name == 'textarea':
                    value = field.text
                else:
                    # checkbox ? radiobutton ? file ? 
                    continue
                data[name] = value
        return data
    

    这是一个更好的结果吗?

    老实说,我不这么认为;另一方面,如果您碰巧使用 BS 以其他方式解析响应内容,这可能是一种选择。

    【讨论】:

      【解决方案6】:

      首先,考虑使用response.context 而不是response.content。正如here 所记录的那样,它为您提供了用于渲染response.content 的模板参数。如果您将它们作为参数提供给渲染器,您需要的表单属性(名称和值)可能就在其中。

      如果你必须使用response.content,那么我认为 Django 没有提供解析 HTML 响应的方法。您可以使用像 beautifulsoup 这样的 HTML 解析器,或者使用正则表达式。

      【讨论】:

      • 好点。如果查看上下文并验证该值是否已按预期传递给模板就足够了,那么检查 context[var] 就足够了。
      • 对于这个特定的问题需要response.content。也许html是通过其他方式创建的。可能是format_html(),那么就没有上下文了。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-25
      相关资源
      最近更新 更多