هل str.replace (..). استبدال (..) AD Nauseam تعبير قياسي في Python؟

StackOverflow https://stackoverflow.com/questions/2484156

سؤال

على سبيل المثال ، قل أنني أردت وظيفة للهروب من سلسلة للاستخدام في HTML (كما في Django's مرشح الهروب):

    def escape(string):
        """
        Returns the given string with ampersands, quotes and angle 
        brackets encoded.
        """
        return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

يعمل هذا ، لكنه يصبح قبيحًا بسرعة ويبدو أنه يحتوي على أداء خوارزمي ضعيف (في هذا المثال ، يتم اجتياز السلسلة مرارًا وتكرارًا 5 مرات). ما سيكون أفضل هو شيء من هذا القبيل:

    def escape(string):
        """
        Returns the given string with ampersands, quotes and angle 
        brackets encoded.
        """
        # Note that ampersands must be escaped first; the rest can be escaped in 
        # any order.
        return replace_multi(string.replace('&', '&amp;'),
                             {'<': '&lt;', '>': '&gt;', 
                              "'": '&#39;', '"': '&quot;'})

هل توجد هذه الوظيفة ، أم أن المصطلح القياسي للبيثون لاستخدام ما كتبته من قبل؟

هل كانت مفيدة؟

المحلول

هل لديك تطبيق يعمل بطيئًا للغاية وقمت بتعيينه لتجد أن خطًا مثل هذا المقتطف يسبب ذلك بطيئًا؟ تحدث الزجاجة في أماكن غير متوقعة.

يعبر المقتطف الحالي السلسلة 5 مرات ، ويقوم بعمل واحد في كل مرة. أنت تقترح اجتيازها مرة واحدة ، وربما تفعل خمسة أشياء في كل مرة (أو على الأقل القيام بشيء في كل مرة). ليس من الواضح أن هذا سيقوم تلقائيًا بعمل أفضل بالنسبة لي. حاليا الخوارزمية المستخدمة هي o (nM) (على افتراض أن طول السلسلة أطول من الأشياء الموجودة في القواعد) ، حيث N هو طول السلسلة و M هو عدد قواعد الإحلال. أعتقد ، في اعتقادي ، تقليل التعقيد الخوارزمي إلى شيء مثل O (نسجل (M)) وفي الحالة المحددة التي نحن فيها - حيث تكون كل الأشياء الأصلية شخصية واحدة فقط (ولكن ليس في حالة مكالمات متعددة إلى replace بشكل عام) - O (n) ، لكن هذا لا يهم منذ ذلك الحين م 5 لكن ن غير محدود.

إذا كان M ثابتًا ، فإن تعقيد كلا الحلين يذهب حقًا إلى O (N). ليس من الواضح بالنسبة لي أنه سيكون من الممكن محاولة تحويل خمس تمريرات بسيطة إلى واحدة معقدة ، والتي لا يمكنني تخمينها في اللحظة الحالية. إذا كان هناك شيء ما يمكن أن يجعله أفضل ، فقد اعتقدت أنها كانت مهمة أكثر أهمية.

يتطلب القيام بكل شيء على مرور واحد بدلاً من تصاريح متتالية أيضًا الإجابة على أسئلة حول ما يجب القيام به بشأن القواعد المتضاربة وكيفية تطبيقها. قرار هذه الأسئلة واضح مع سلسلة من replace.

نصائح أخرى

ماذا عننا فقط نختبر طرقًا مختلفة للقيام بذلك ومعرفة ما الذي يخرج بشكل أسرع (على افتراض أننا نرعى فقط بأسرع طريقة للقيام بذلك).

def escape1(input):
        return input.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

translation_table = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    "'": '&#39;',
    '"': '&quot;',
}

def escape2(input):
        return ''.join(translation_table.get(char, char) for char in input)

import re
_escape3_re = re.compile(r'[&<>\'"]')
def _escape3_repl(x):
    s = x.group(0)
    return translation_table.get(s, s)
def escape3(x):
    return _escape3_re.sub(_escape3_repl, x)

def escape4(x):
    return unicode(x).translate(translation_table)

test_strings = (
    'Nothing in there.',
    '<this is="not" a="tag" />',
    'Something & Something else',
    'This one is pretty long. ' * 50
)

import time

for test_i, test_string in enumerate(test_strings):
    print repr(test_string)
    for func in escape1, escape2, escape3, escape4:
        start_time = time.time()
        for i in xrange(1000):
            x = func(test_string)
        print '\t%s done in %.3fms' % (func.__name__, (time.time() - start_time))
    print

تشغيل هذا يمنحك:

'Nothing in there.'
    escape1 done in 0.002ms
    escape2 done in 0.009ms
    escape3 done in 0.001ms
    escape4 done in 0.005ms

'<this is="not" a="tag" />'
    escape1 done in 0.002ms
    escape2 done in 0.012ms
    escape3 done in 0.009ms
    escape4 done in 0.007ms

'Something & Something else'
    escape1 done in 0.002ms
    escape2 done in 0.012ms
    escape3 done in 0.003ms
    escape4 done in 0.007ms

'This one is pretty long. <snip>'
    escape1 done in 0.008ms
    escape2 done in 0.386ms
    escape3 done in 0.011ms
    escape4 done in 0.310ms

يبدو أن استبدالهم واحدًا تلو الآخر يذهب إلى الأسرع.

تعديل: إن إجراء الاختبارات مرة أخرى باستخدام 1000000 تكرار يعطي ما يلي للأوتار الثلاثة الأولى (الرابع سيستغرق وقتًا طويلاً على جهازي ليتمكن من الانتظار = P):

'Nothing in there.'
    escape1 done in 0.001ms
    escape2 done in 0.008ms
    escape3 done in 0.002ms
    escape4 done in 0.005ms

'<this is="not" a="tag" />'
    escape1 done in 0.002ms
    escape2 done in 0.011ms
    escape3 done in 0.009ms
    escape4 done in 0.007ms

'Something & Something else'
    escape1 done in 0.002ms
    escape2 done in 0.011ms
    escape3 done in 0.003ms
    escape4 done in 0.007ms

الأرقام متشابهة إلى حد كبير. في الحالة الأولى ، تكون في الواقع أكثر اتساقًا لأن استبدال السلسلة المباشر هو الأسرع الآن.

أفضل شيء نظيف مثل:

substitutions = [
    ('<', '&lt;'),
    ('>', '&gt;'),
    ...]

for search, replacement in substitutions:
    string = string.replace(search, replacement)

وهذا ما Django يفعل:

def escape(html):
    """Returns the given HTML with ampersands, quotes and carets encoded."""
    return mark_safe(force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))

يمكنك استخدام تقليل:

reduce(lambda s,r: s.replace(*r),
       [('&', '&amp;'),
        ('<', '&lt;'),
        ('>', '&gt;'),
        ("'", '&#39;'),
        ('"', '&quot;')],
       string)

وفقًا لاقتراح بيبرا ، إليك ما انتهى بي الأمر (في وحدة منفصلة ، بالطبع):

import re

class Subs(object):
    """
    A container holding strings to be searched for and replaced in
    replace_multi().

    Holds little relation to the sandwich.
    """
    def __init__(self, needles_and_replacements):
        """
        Returns a new instance of the Subs class, given a dictionary holding 
        the keys to be searched for and the values to be used as replacements.
        """
        self.lookup = needles_and_replacements
        self.regex = re.compile('|'.join(map(re.escape,
                                             needles_and_replacements)))

def replace_multi(string, subs):
    """
    Replaces given items in string efficiently in a single-pass.

    "string" should be the string to be searched.
    "subs" can be either:
        A.) a dictionary containing as its keys the items to be
            searched for and as its values the items to be replaced.
        or B.) a pre-compiled instance of the Subs class from this module
               (which may have slightly better performance if this is
                called often).
    """
    if not isinstance(subs, Subs): # Assume dictionary if not our class.
        subs = Subs(subs)
    lookup = subs.lookup
    return subs.regex.sub(lambda match: lookup[match.group(0)], string)

مثال الاستخدام:

def escape(string):
    """
    Returns the given string with ampersands, quotes and angle 
    brackets encoded.
    """
    # Note that ampersands must be escaped first; the rest can be escaped in 
    # any order.
    escape.subs = Subs({'<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'})
    return replace_multi(string.replace('&', '&amp;'), escape.subs)

أفضل بكثير :). شكرا للمساعدة.

تعديل

فما باللك ، كان مايك جراهام على حق. لقد قمت بتقييمها وينتهي الاستبدال في الواقع كثيراً أبطأ.

شفرة:

from urllib2 import urlopen
import timeit

def escape1(string):
    """
    Returns the given string with ampersands, quotes and angle
    brackets encoded.
    """
    return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("'", '&#39;').replace('"', '&quot;')

def escape2(string):
    """
    Returns the given string with ampersands, quotes and angle
    brackets encoded.
    """
    # Note that ampersands must be escaped first; the rest can be escaped in
    # any order.
    escape2.subs = Subs({'<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'})
    return replace_multi(string.replace('&', '&amp;'), escape2.subs)

# An example test on the stackoverflow homepage.
request = urlopen('http://stackoverflow.com')
test_string = request.read()
request.close()

test1 = timeit.Timer('escape1(test_string)',
                     setup='from __main__ import escape1, test_string')
test2 = timeit.Timer('escape2(test_string)',
                     setup='from __main__ import escape2, test_string')
print 'multi-pass:', test1.timeit(2000)
print 'single-pass:', test2.timeit(2000)

انتاج:

multi-pass: 15.9897229671
single-pass: 66.5422530174

الكثير من من أجل هذا.

يبدو أنه من الشائع تنفيذ ذلك عبر Regex. يمكنك العثور على مثال على ذلك في ASPN و هنا.

إذا كنت تعمل مع سلاسل غير unicode و Python <3.0 ، جرب بديل translate طريقة:

# Python < 3.0
import itertools

def escape(a_string):
    replacer= dict( (chr(c),chr(c)) for c in xrange(256))
    replacer.update(
        {'&': '&amp;',
         '<': '&lt;',
         '>': '&gt;',
         '"': '&quot;',
         "'": '&#39;'}
    )
    return ''.join(itertools.imap(replacer.__getitem__, a_string))

if __name__ == "__main__":
    print escape('''"Hello"<i> to George's friend&co.''')

$ python so2484156.py 
&quot;Hello&quot;&lt;i&gt; to George&#39;s friend&amp;co.

هذا أقرب إلى "فحص واحد" لسلسلة الإدخال ، حسب رغبتك.

تعديل

كان نيتي إنشاء ملف unicode.translate ما يعادل ذلك لم يقتصر على بدائل الأحرف ، لذلك توصلت إلى الإجابة أعلاه ؛ حصلت على تعليق من قبل "تدفق" المستخدم الذي كان خارج السياق بالكامل تقريبًا ، مع نقطة واحدة صحيحة: الرمز أعلاه ، كما هو ، يهدف إلى العمل مع سلاسل البايت و لا سلاسل يونيكود. هناك تحديث واضح (أي unichr () ... Xrange (sys.maxunicode+1)) الذي لا أحبه بشدة ، لذلك توصلت إلى وظيفة أخرى تعمل على كل من خيوط Unicode و Byte ، بالنظر إلى أن Python ضمانات:

all( (chr(i)==unichr(i) and hash(chr(i))==hash(unichr(i)))
    for i in xrange(128)) is True

تتبع الوظيفة الجديدة:

def escape(a_string):
    replacer= {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
    }
    return ''.join(
        itertools.starmap(
            replacer.get, # .setdefault *might* be faster
            itertools.izip(a_string, a_string)
        )
    )

لاحظ استخدام starmap مع سلسلة من tuples: لأي حرف ليس في قوله البديل ، Return saile saile.

حسنًا ، جلست وفعلت الرياضيات. الثابتة والمتنقلة لا تغضب مني أنا أجب على وجه التحديد أن ناقش حل τζωτζιου ، لكن هذا سيكون من الصعب إلى حد ما أن أحضره داخل التعليق ، لذلك اسمحوا لي أن أفعل ذلك بهذه الطريقة. في الواقع ، سأبث أيضًا بعض الاعتبارات ذات الصلة بسؤال البروتوكول الاختياري.

أولاً ، لقد ناقشت مع τζωτζιου الأناقة وصحتها وقابليتها على مقاربه. اتضح أنه يبدو أن الاقتراح ، في حين أنه يستخدم قاموسًا (غير مرتبة بطبيعته) كسجل لتخزين أزواج الاستبدال ، في الواقع يعيد باستمرار النتائج الصحيحة ، حيث زعمت أنه لن يفعل ذلك. هذا لأن الدعوة إلى itertools.starmap() في السطر 11 ، أدناه ، يحصل كوسيطة ثانية على ذلك على أزواج من الأحرف/البايتات (المزيد عن ذلك لاحقًا) [ ( 'h', 'h', ), ( 'e', 'e', ), ( 'l', 'l', ), ... ]. هذه الأزواج من الشخصيات/البايتات هي ما هي الوسيطة الأولى ، replacer.get, ، يسمى مرارا وتكرارا مع. ليست هناك فرصة للركض في موقف حيث أولاً '>' يتحول إلى '&gt;' ثم عن غير قصد مرة أخرى في '&amp;gt;', ، لأن كل حرف/بايت يعتبر مرة واحدة فقط للاستبدال. لذلك هذا الجزء من حيث المبدأ على غرار وصحيح الخوارزمية.

السؤال التالي هو الجدوى ، وسيشمل ذلك نظرة على الأداء. إذا تم الانتهاء من مهمة حيوية بشكل صحيح في 0.01s باستخدام رمز محرج ولكن 1s باستخدام رمز رائع ، فقد يعتبر محرجًا مفضلاً في الممارسة (ولكن فقط إذا كانت الخسارة الثانية لا تطاق في الواقع). هنا هو الكود الذي استخدمته للاختبار ؛ ويشمل عدد من التطبيقات المختلفة. إنه مكتوب في Python 3.1 حتى نتمكن من استخدام أحرف يونانية Unicode للمعرفات التي في حد ذاتها رائعة (zip في PY3K يعيد نفس الشيء itertools.izip في PY2):

import itertools                                                                  #01
                                                                                  #02
_replacements = {                                                                 #03
  '&': '&amp;',                                                                   #04
  '<': '&lt;',                                                                    #05
  '>': '&gt;',                                                                    #06
  '"': '&quot;',                                                                  #07
  "'": '&#39;', }                                                                 #08
                                                                                  #09
def escape_ΤΖΩΤΖΙΟΥ( a_string ):                                                  #10
  return ''.join(                                                                 #11
    itertools.starmap(                                                            #12
      _replacements.get,                                                          #13
      zip( a_string, a_string ) ) )                                               #14
                                                                                  #15
def escape_SIMPLE( text ):                                                        #16
  return ''.join( _replacements.get( chr, chr ) for chr in text )                 #17
                                                                                  #18
def escape_SIMPLE_optimized( text ):                                              #19
  get = _replacements.get                                                         #20
  return ''.join( get( chr, chr ) for chr in text )                               #21
                                                                                  #22
def escape_TRADITIONAL( text ):                                                   #23
  return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\    #24
    .replace("'", '&#39;').replace('"', '&quot;')                                 #25

هذه هي نتائج التوقيت:

escaping with SIMPLE            took 5.74664253sec for 100000 items
escaping with SIMPLE_optimized  took 5.11457801sec for 100000 items
escaping TRADITIONAL in-situ    took 0.57543013sec for 100000 items
escaping with TRADITIONAL       took 0.62347413sec for 100000 items
escaping a la ΤΖΩΤΖΙΟΥ          took 2.66592320sec for 100000 items

تبين أن اهتمام الملصق الأصلي بأن الطريقة "التقليدية" تصبح قبيحة بسرعة ويبدو أن لها أداء خوارزمي ضعيف "لا مبرر له جزئيًا عند وضعه في هذا السياق. في الواقع يؤدي بشكل أفضل. عندما يتم التخلي عن مكالمة وظيفية ، فإننا نرى عقوبة أداء بنسبة 8 ٪ ("طرق الاتصال باهظة الثمن" ، ولكن بشكل عام لا يزال يتعين عليك القيام بذلك). بالمقارنة ، يستغرق تطبيق τζωτζιου حوالي 5 مرات طالما أن الطريقة التقليدية ، والتي ، بالنظر إلى تعقيد أعلى يجب أن يتنافس مع أساليب السلسلة المحسّنة الطويلة للبيثون.

هناك خوارزمية أخرى هنا ، البسيطة. بقدر ما أستطيع أن أرى ، فإن هذا يفعل تمامًا ما تفعله طريقة τζωτζιου: إنها تكرر على الشخصيات/البايتات في النص وتؤدي بحثًا عن كل منها ، ثم ينضم إلى جميع الأحرف/البايت معًا ويعيد النص الناتج الناتج. يمكنك أن ترى أنه عندما تتضمن إحدى الطرق للقيام بذلك صياغة طويلة ومطالبة إلى حد ما ، يكون التنفيذ البسيط مفهومًا بالفعل في لمحة.

ما يوجهني حقًا هنا ، على الرغم من ذلك ، هو مدى سوء الأداء في الأداء: إنه حوالي 10 أضعاف البطيء مثل الطريقة التقليدية ، وأيضًا ضعف طريقة τζωτζιου. أنا في حيرة هنا تمامًا ، ربما يمكن لشخص ما التوصل إلى فكرة عن سبب ذلك. إنه يستخدم فقط لبنات البناء الأساسية من Python ويعمل مع تكرارين ضمنيين ، لذلك فهو يتجنب بناء قوائم التبعية وكل شيء ، لكنه لا يزال بطيئًا ، ولا أعرف السبب.

اسمحوا لي أن أختتم مراجعة الكود هذه مع ملاحظة حول ميزة حل τζωτζιου. لقد أوضحت أنه من الواضح بما فيه الكفاية ، أجد أن الكود يصعب قراءته وتفوقه على المهمة المطروحة. ومع ذلك ، أجد أن الطريقة التي يعامل بها الشخصيات وتتأكد من أنه بالنسبة لمجموعة صغيرة من الشخصيات ، فإنهم سيتصرفون بطريقة تشبه البايت قليلاً. من المؤكد أنها تعمل للمهمة المطروحة ، ولكن بمجرد أن أكرر على سبيل المثال ، ما فوق "ما أقوم به" ما أقوم به هو التكرار على البايتات المجاورة التي تمثل أحرفًا واحدة. في معظم المواقف ، هذا بالضبط ما يجب تجنبه ؛ هذا هو بالضبط السبب في أن "الأوتار" في Py3K هي الآن "كائنات أحادية" القديمة ، وأصبحت "الأوتار" القديمة "بايت" و "Bytearray". إذا أردت ترشيح الميزة الواحدة لـ PY3K والتي يمكن أن تضمن ترحيلًا مكلفًا للرمز من السلسلة 2 إلى السلسلة 3 ، فستكون هذه الخاصية الفردية لـ PY3K. 98 ٪ من جميع مشكلات الترميز الخاصة بي قد حلت للتو منذ ذلك الحين ، ولا يوجد اختراق ذكي يمكن أن يجعلني أشك بجدية في حركتي. الخوارزمية المذكورة ليست "من الناحية المفاهيمية 8Bit-Clean و Unicode Safe" ، والتي بالنسبة لي هي مختصرة بشكل خطير ، بالنظر إلى أن هذا هو 2010.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top