1313from itertools import chain , product
1414from locale import getlocale
1515from math import log
16+ from pkgutil import iter_modules
1617from platform import system
1718from random import randint
1819from six import binary_type , string_types , text_type , BytesIO
3940 "DARWIN" , "LANG" , "LINUX" , "MASKS" , "PY3" , "UNIX" , "WINDOWS" ]
4041CODECS_REGISTRY = None
4142CODECS_CATEGORIES = ["native" , "custom" ]
42- LANG = getlocale ()[0 ][:2 ].lower ()
43+ try :
44+ LANG = getlocale ()[0 ][:2 ].lower ()
45+ except TypeError :
46+ LANG = None
4347MASKS = {
4448 'a' : printable ,
4549 'b' : "" .join (chr (i ) for i in range (256 )),
6468UNIX = DARWIN or LINUX
6569WINDOWS = system () == "Windows"
6670
67- LANG_BACKEND = None
68- for lib in ["langid" , "langdetect" , "pycld2" , "cld3" , "textblob" ]:
69- try :
70- globals ()[lib ] = __import__ (lib )
71- LANG_BACKEND = lib
72- break
73- except ImportError :
74- pass
75- CLD3_LANGUAGES = "af|am|ar|bg|bn|bs|ca|ce|co|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|fy|ga|gd|gl|gu|ha|hi|hm|hr|ht|hu|" \
76- "hy|id|ig|is|it|iw|ja|jv|ka|kk|km|kn|ko|ku|ky|la|lb|lo|lt|lv|mg|mi|mk|ml|mn|mr|ms|mt|my|ne|nl|no|ny|" \
77- "pa|pl|ps|pt|ro|ru|sd|si|sk|sl|sm|sn|so|sq|sr|st|su|sv|sw|ta|te|tg|th|tr|uk|ur|uz|vi|xh|yi|yo|zh|zu" \
78- .split ("|" )
79- TEXTBLOB_LANGUAGES = "af|ar|az|be|bg|bn|ca|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|ga|gl|gu|hi|hr|ht|hu|id|is|it|iw|" \
80- "ja|ka|kn|ko|la|lt|lv|mk|ms|mt|nl|no|pl|pt|ro|ru|sk|sl|sq|sr|sv|sw|ta|te|th|tl|tr|uk|ur|vi|yi|zh" \
81- .split ("|" )
82-
8371entropy = lambda s : - sum ([p * log (p , 2 ) for p in [float (s .count (c )) / len (s ) for c in set (s )]])
8472
8573isb = lambda s : isinstance (s , binary_type )
@@ -307,7 +295,8 @@ class StreamReader(Codec, codecs.StreamReader):
307295 ci .parameters ['guess' ] = kwargs .get ('guess' , glob .get ('__guess__' , [ename ])) or []
308296 ci .parameters ['module' ] = kwargs .get ('module' , glob .get ('__name__' ))
309297 ci .parameters .setdefault ("scoring" , {})
310- for attr in ["bonus_func" , "entropy" , "len_charset" , "penalty" , "printables_rate" , "padding_char" ]:
298+ for attr in ["bonus_func" , "entropy" , "expansion_factor" , "len_charset" , "penalty" , "printables_rate" ,
299+ "padding_char" ]:
311300 a = kwargs .pop (attr , None )
312301 if a is not None :
313302 ci .parameters ['scoring' ][attr ] = a
@@ -1068,6 +1057,7 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
10681057
10691058
10701059# guess feature objects
1060+ __module_exists = lambda n : n in [x [1 ] for x in iter_modules ()]
10711061stopfunc = ModuleType ("stopfunc" , """
10721062 Predefined stop functions
10731063 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1077,6 +1067,8 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
10771067 - `flag`: searches for the pattern "[Ff][Ll1][Aa4@][Gg9]" (either UTF-8 or UTF-16)
10781068 - `lang_**`: checks if the given lang (any from the PROFILES_DIRECTORY of the langdetect module) is detected
10791069 - `printables`: checks that every output character is in the set of printables
1070+ - `regex`: takes one argument, the regular expression, for checking a string against the given pattern
1071+ - `text`: checks for printables and an entropy less than 4.6 (empirically determined)
10801072""" )
10811073stopfunc .printables = lambda s : all (c in printable for c in ensure_str (s ))
10821074stopfunc .printables .__name__ = stopfunc .printables .__qualname__ = "printables"
@@ -1086,13 +1078,27 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
10861078stopfunc .text .__name__ = stopfunc .text .__qualname__ = "text"
10871079stopfunc .flag = lambda x : re .search (r"[Ff][Ll1][Aa4@][Gg96]" , ensure_str (x )) is not None
10881080stopfunc .flag .__name__ = stopfunc .flag .__qualname__ = "flag"
1089- stopfunc .default = stopfunc .printables
1081+ stopfunc .default = stopfunc .text
1082+
1083+ stopfunc .LANG_BACKEND = None
1084+ stopfunc .LANG_BACKENDS = [n for n in ["langid" , "langdetect" , "pycld2" , "cld3" , "textblob" ] if __module_exists (n )]
1085+ if len (stopfunc .LANG_BACKENDS ) > 0 :
1086+ stopfunc .LANG_BACKEND = stopfunc .LANG_BACKENDS [0 ]
1087+ if "cld3" in stopfunc .LANG_BACKENDS :
1088+ stopfunc .CLD3_LANGUAGES = "af|am|ar|bg|bn|bs|ca|ce|co|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|fy|ga|gd|gl|gu|ha|" \
1089+ "hi|hm|hr|ht|hu|hy|id|ig|is|it|iw|ja|jv|ka|kk|km|kn|ko|ku|ky|la|lb|lo|lt|lv|mg|mi|mk|" \
1090+ "ml|mn|mr|ms|mt|my|ne|nl|no|ny|pa|pl|ps|pt|ro|ru|sd|si|sk|sl|sm|sn|so|sq|sr|st|su|sv|" \
1091+ "sw|ta|te|tg|th|tr|uk|ur|uz|vi|xh|yi|yo|zh|zu" .split ("|" )
1092+ if "textblob" in stopfunc .LANG_BACKENDS :
1093+ stopfunc .TEXTBLOB_LANGUAGES = "af|ar|az|be|bg|bn|ca|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|ga|gl|gu|hi|hr|ht|hu|" \
1094+ "id|is|it|iw|ja|ka|kn|ko|la|lt|lv|mk|ms|mt|nl|no|pl|pt|ro|ru|sk|sl|sq|sr|sv|sw|ta|" \
1095+ "te|th|tl|tr|uk|ur|vi|yi|zh" .split ("|" )
10901096
10911097
10921098def _detect (text ):
1093- _lb , t = LANG_BACKEND , ensure_str (text )
1099+ _lb , t = stopfunc . LANG_BACKEND , ensure_str (text )
10941100 if _lb is None :
1095- raise ValueError ("No language backend installed" )
1101+ raise ValueError ("No language backend %s" % [ "selected" , " installed"][ len ( stopfunc . LANG_BACKENDS ) == 0 ] )
10961102 return langid .classify (t )[0 ] if _lb == "langid" else \
10971103 langdetect .detect (t ) if _lb == "langdetect" else \
10981104 pycld2 .detect (t )[2 ][0 ][1 ] if _lb == "pycld2" else \
@@ -1110,23 +1116,42 @@ def _test(s):
11101116 return False
11111117 return _test
11121118
1113- if LANG_BACKEND :
1114- _lb = LANG_BACKEND
1115- if _lb == "langid" :
1116- langid .langid .load_model ()
1117- for lang in (
1118- langid .langid .identifier .nb_classes if _lb == "langid" else \
1119- [p .replace ("-" , "" ) for p in os .listdir (langdetect .PROFILES_DIRECTORY )] if _lb == "langdetect" else \
1120- list (set (x [1 ][:2 ] for x in pycld2 .LANGUAGES if x [0 ] in pycld2 .DETECTED_LANGUAGES )) if _lb == "pycld2" else \
1121- CLD3_LANGUAGES if _lb == "cld3" else \
1122- TEXTBLOB_LANGUAGES if _lb == "textblob" else \
1123- []):
1124- n = "lang_%s" % lang
1125- setattr (stopfunc , n , _lang (lang ))
1126- getattr (stopfunc , n ).__name__ = getattr (stopfunc , n ).__qualname__ = n
1127- flng = "lang_%s" % LANG
1128- if getattr (stopfunc , flng , None ):
1129- stopfunc .default = getattr (stopfunc , flng )
1119+
1120+ def _load_lang_backend (backend = None ):
1121+ # import the requested backend library if not imported yet
1122+ if backend is None or backend in stopfunc .LANG_BACKENDS :
1123+ stopfunc .LANG_BACKEND = backend
1124+ if backend :
1125+ globals ()[backend ] = __import__ (backend )
1126+ else :
1127+ raise ValueError ("Unsupported language detection backend" )
1128+ # remove language-related stop functions
1129+ for attr in dir (stopfunc ):
1130+ if attr .startswith ("_" ) or not isinstance (getattr (stopfunc , attr ), FunctionType ):
1131+ continue
1132+ if re .match (r"lang_[a-z]{2}$" , attr ):
1133+ delattr (stopfunc , attr )
1134+ # rebind applicable language-related stop functions
1135+ if stopfunc .LANG_BACKEND :
1136+ _lb = stopfunc .LANG_BACKEND
1137+ if _lb == "langid" :
1138+ langid .langid .load_model ()
1139+ for lang in (
1140+ langid .langid .identifier .nb_classes if _lb == "langid" else \
1141+ list (set (p [:2 ] for p in os .listdir (langdetect .PROFILES_DIRECTORY ))) if _lb == "langdetect" else \
1142+ list (set (x [1 ][:2 ] for x in pycld2 .LANGUAGES if x [0 ] in pycld2 .DETECTED_LANGUAGES )) if _lb == "pycld2" else \
1143+ stopfunc .CLD3_LANGUAGES if _lb == "cld3" else \
1144+ stopfunc .TEXTBLOB_LANGUAGES if _lb == "textblob" else \
1145+ []):
1146+ n = "lang_%s" % lang
1147+ setattr (stopfunc , n , _lang (lang ))
1148+ getattr (stopfunc , n ).__name__ = getattr (stopfunc , n ).__qualname__ = n
1149+ if LANG :
1150+ flng = "lang_%s" % LANG
1151+ if getattr (stopfunc , flng , None ):
1152+ stopfunc .default = getattr (stopfunc , flng )
1153+ _load_lang_backend (stopfunc .LANG_BACKEND )
1154+ stopfunc ._reload_lang = _load_lang_backend
11301155
11311156
11321157def __develop (encodings ):
@@ -1202,6 +1227,8 @@ def __rank(prev_input, input, codecs, heuristic=False, extended=False, yield_sco
12021227
12031228
12041229class _Text (object ):
1230+ __slots__ = ["entropy" , "lcharset" , "len" , "padding" , "printables" ]
1231+
12051232 def __init__ (self , text , pad_char = None ):
12061233 self .len = len (text )
12071234 self .lcharset = len (set (text ))
@@ -1247,13 +1274,24 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
12471274 elif not pad and obj .padding :
12481275 s -= .1 # it could arise a padding character is encountered while not being padding => small penalty
12491276 # give a bonus when the rate of printable characters is greater or equal than expected and a penalty when
1250- # lower only for codecs that tolerate errors (otherwise, the printables rate can be biased)
1277+ # lower only for codecs that DO NOT tolerate errors (otherwise, the printables rate can be biased)
12511278 if not ci .parameters .get ('no_error' , False ):
12521279 pr = sc .get ('printables_rate' , 0 )
12531280 if isinstance (pr , type (lambda : None )):
12541281 pr = float (pr (obj .printables ))
12551282 if obj .printables - pr <= .05 :
12561283 s += .1
1284+ expf = sc .get ('expansion_factor' )
1285+ if expf :
1286+ f = float (len (new_input )) / obj .len
1287+ if isinstance (expf , type (lambda : None )):
1288+ expf = expf (f )
1289+ elif isinstance (expf , (int , float )):
1290+ epxf = f - .1 <= expf <= f + .1
1291+ elif isinstance (expf , (tuple , list )) and len (expf ) == 2 :
1292+ expf = f - expf [1 ] <= expf [0 ] <= expf [1 ] + .1
1293+ if expf :
1294+ s += .1
12571295 # afterwards, if the input text has an entropy close to the expected one, give a bonus weighted on the
12581296 # number of input characters to take bad entropies of shorter strings into account
12591297 entr = sc .get ('entropy' , {})
@@ -1265,7 +1303,7 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
12651303 entr = entr (obj .entropy )
12661304 if entr is not None :
12671305 # use a quadratic heuristic to compute a weight for the entropy delta, aligned on (100w,.1) and (200w,1)
1268- d_entr = min (4e-05 * obj .len ** 2 - .003 * obj .len , 1 ) * abs (entr - obj . entropy )
1306+ d_entr = min (4e-05 * obj .len ** 2 - .003 * obj .len , 1 ) * abs (entr - entropy ( new_input ) )
12691307 if d_entr <= .5 :
12701308 s += .5 - d_entr
12711309 # finally, if relevant, apply a custom bonus (e.g. when a regex pattern is matched)
@@ -1283,7 +1321,7 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
12831321
12841322
12851323def guess (input , stop_func = stopfunc .default , min_depth = 0 , max_depth = 5 , codec_categories = None , exclude = None , found = (),
1286- stop = True , show = False , scoring_heuristic = False , extended = False , debug = False ):
1324+ stop = True , show = False , scoring_heuristic = True , extended = False , debug = False ):
12871325 """ Try decoding without the knowledge of the encoding(s). """
12881326 if max_depth <= 0 :
12891327 raise ValueError ("Depth must be a non-null positive integer" )
@@ -1309,6 +1347,8 @@ def guess(input, stop_func=stopfunc.default, min_depth=0, max_depth=5, codec_cat
13091347
13101348def rank (input , extended = False , limit = - 1 , codec_categories = None , exclude = None ):
13111349 """ Rank the most probable encodings based on the given input. """
1350+ if isinstance (codec_categories , string_types ):
1351+ codec_categories = (codec_categories , )
13121352 codecs = list_encodings (* (codec_categories or ()))
13131353 for e in __develop (exclude ):
13141354 try :
0 commit comments