More cleanup, yey!
[python_utils.git] / profanity_filter.py
1 #!/usr/bin/env python3
2
3 """A helper to identify and optionally obscure some bad words."""
4
5 import logging
6 import random
7 import re
8 import string
9 import sys
10
11 import nltk
12 from nltk.stem import PorterStemmer
13
14 import decorator_utils
15 import string_utils
16
17 logger = logging.getLogger(__name__)
18
19
20 @decorator_utils.singleton
21 class ProfanityFilter(object):
22     """A helper to identify and optionally obscure some bad words."""
23
24     def __init__(self):
25         self.bad_words = set(
26             [
27                 'acrotomophilia',
28                 'anal',
29                 'analingus',
30                 'anally',
31                 'anilingus',
32                 'anus',
33                 'arsehol',
34                 'arsehole',
35                 'ass',
36                 'asses',
37                 'asshol',
38                 'asshole',
39                 'assmunch',
40                 'auto erot',
41                 'auto erotic',
42                 'autoerotic',
43                 'babeland',
44                 'babi batter',
45                 'baby batter',
46                 'ball gag',
47                 'ball gravi',
48                 'ball gravy',
49                 'ball kick',
50                 'ball kicking',
51                 'ball lick',
52                 'ball licking',
53                 'ball sack',
54                 'ball suck',
55                 'ball sucking',
56                 'ball zack',
57                 'bangbro',
58                 'bangbros',
59                 'bare legal',
60                 'bareback',
61                 'barely legal',
62                 'barenak',
63                 'barenaked',
64                 'bastardo',
65                 'bastinado',
66                 'bbc',
67                 'bbw',
68                 'bdsm',
69                 'beaver cleaver',
70                 'beaver lip',
71                 'beaver lips',
72                 'bestial',
73                 'bestiality',
74                 'bi curiou',
75                 'bi curious',
76                 'big black',
77                 'big breasts',
78                 'big knocker',
79                 'big knockers',
80                 'big tit',
81                 'big tits',
82                 'bimbo',
83                 'birdlock',
84                 'bitch',
85                 'bitches',
86                 'black cock',
87                 'blond action',
88                 'blond on blond',
89                 'blonde action',
90                 'blow j',
91                 'blow job',
92                 'blowjob',
93                 'blow my',
94                 'blow me',
95                 'blow ourselv',
96                 'blow ourselves',
97                 'blow your load',
98                 'blue waffl',
99                 'blue waffle',
100                 'blumpkin',
101                 'bollock',
102                 'bollocks',
103                 'bondag',
104                 'bondage',
105                 'boner',
106                 'boob',
107                 'boobs',
108                 'booti call',
109                 'booty call',
110                 'breast',
111                 'breasts',
112                 'brown shower',
113                 'brown showers',
114                 'brunett action',
115                 'brunette action',
116                 'bukkak',
117                 'bukkake',
118                 'bulldyk',
119                 'bulldyke',
120                 'bullet vibe',
121                 'bullshit',
122                 'bung hole',
123                 'bunghol',
124                 'bunghole',
125                 'busti',
126                 'busty',
127                 'butt',
128                 'buttcheek',
129                 'buttcheeks',
130                 'butthol',
131                 'butthole',
132                 'camel toe',
133                 'camgirl',
134                 'camslut',
135                 'camwhore',
136                 'carpet muncher',
137                 'carpetmuncher',
138                 'chocol rosebud',
139                 'chocolate rosebuds',
140                 'circlejerk',
141                 'chink',
142                 'cleveland steamer',
143                 'clit',
144                 'clitori',
145                 'clitoris',
146                 'clover clamp',
147                 'clover clamps',
148                 'clusterfuck',
149                 'cock',
150                 'cocks',
151                 'coprolagnia',
152                 'coprophilia',
153                 'cornhol',
154                 'cornhole',
155                 'cream pie',
156                 'creampi',
157                 'creampie',
158                 'cum',
159                 'cumming',
160                 'cunnilingu',
161                 'cunnilingus',
162                 'cunt',
163                 'damn',
164                 'darki',
165                 'darkie',
166                 'date rape',
167                 'daterap',
168                 'daterape',
169                 'deep throat',
170                 'deepthroat',
171                 'dick',
172                 'dildo',
173                 'dirti pillow',
174                 'dirti sanchez',
175                 'dirty pillow',
176                 'dirty sanchez',
177                 'dog style',
178                 'doggi style',
179                 'doggie style',
180                 'doggiestyl',
181                 'doggiestyle',
182                 'doggystyle',
183                 'dolcett',
184                 'domination',
185                 'dominatrix',
186                 'domm',
187                 'dommes',
188                 'donkey punch',
189                 'doubl dick',
190                 'doubl dong',
191                 'doubl penetr',
192                 'double dick',
193                 'double dong',
194                 'double penetration',
195                 'dp action',
196                 'dtf',
197                 'eat my ass',
198                 'ecchi',
199                 'ejacul',
200                 'erection',
201                 'erotic',
202                 'erotism',
203                 'escort',
204                 'ethical slut',
205                 'eunuch',
206                 'faggot',
207                 'fecal',
208                 'felch',
209                 'fellatio',
210                 'feltch',
211                 'female squirting',
212                 'femdom',
213                 'figging',
214                 'fingered',
215                 'fingering',
216                 'fingers',
217                 'fisted',
218                 'fisting',
219                 'fists',
220                 'foot fetish',
221                 'footjob',
222                 'frotting',
223                 'fuck button',
224                 'fuck',
225                 'fucked',
226                 'fucker',
227                 'fuckhead',
228                 'fuckin',
229                 'fucking',
230                 'fudge packer',
231                 'fudgepack',
232                 'fudgepacker',
233                 'futanari',
234                 'g spot',
235                 'g-spot',
236                 'gang bang',
237                 'gay sex',
238                 'gee spot',
239                 'genital',
240                 'giant cock',
241                 'girl gone wild',
242                 'girl on top',
243                 'girl on',
244                 'give head',
245                 'giving head',
246                 'gave head',
247                 'goatcx',
248                 'goatse',
249                 'goddamn',
250                 'gokkun',
251                 'golden shower',
252                 'goo girl',
253                 'goodpoop',
254                 'goregasm',
255                 'grope',
256                 'group sex',
257                 'gspot',
258                 'guro',
259                 'hand job',
260                 'handjob',
261                 'hard core',
262                 'hardcore',
263                 'hentai',
264                 'homoerotic',
265                 'honkey',
266                 'hooker',
267                 'horni',
268                 'horny',
269                 'hot chick',
270                 'how to kill',
271                 'how to murder',
272                 'huge fat',
273                 'humped',
274                 'humping',
275                 'humps',
276                 'incest',
277                 'intercourse',
278                 'jack off',
279                 'jail bait',
280                 'jailbait',
281                 'jerk off',
282                 'jigaboo',
283                 'jiggaboo',
284                 'jiggerboo',
285                 'jizz',
286                 'jugg',
287                 'kike',
288                 'kinbaku',
289                 'kinkster',
290                 'kinky',
291                 'knobbing',
292                 'leather restraint',
293                 'lemon party',
294                 'lolita',
295                 'lovemaking',
296                 'make me come',
297                 'male squirting',
298                 'masturb',
299                 'menage a trois',
300                 'milf',
301                 'missionary position',
302                 'motherfuck',
303                 'mound of venus',
304                 'mr hand',
305                 'muff diver',
306                 'muffdiv',
307                 'muffdiving',
308                 'nambla',
309                 'nawashi',
310                 'negro',
311                 'neonazi',
312                 'nig nog',
313                 'nigga',
314                 'nigger',
315                 'nimphomania',
316                 'nipple',
317                 'nip',
318                 'not safe for',
319                 'nsfl',
320                 'nsfw',
321                 'nude',
322                 'nudes',
323                 'nudity',
324                 'nut sack',
325                 'nutsack',
326                 'nympho',
327                 'nymphomania',
328                 'octopussy',
329                 'omorashi',
330                 'one night stand',
331                 'orgasm',
332                 'orgy',
333                 'paedophil',
334                 'paedophile',
335                 'panties',
336                 'panty',
337                 'pedobear',
338                 'pedophil',
339                 'pedophile',
340                 'pee',
341                 'pegging',
342                 'peni',
343                 'penis',
344                 'phone sex',
345                 'pigfucker',
346                 'piss pig',
347                 'piss',
348                 'pissing',
349                 'pisspig',
350                 'playboy',
351                 'pleasure chest',
352                 'pole smoker',
353                 'ponyplay',
354                 'poof',
355                 'poop chute',
356                 'poopchute',
357                 'porn',
358                 'pron',
359                 'pornhub',
360                 'porno',
361                 'pornographi',
362                 'pornography',
363                 'prince albert',
364                 'pthc',
365                 'pube',
366                 'pussi',
367                 'pussies',
368                 'pussy',
369                 'queaf',
370                 'queer',
371                 'raghead',
372                 'raging boner',
373                 'rape',
374                 'raping',
375                 'rapist',
376                 'rectum',
377                 'reverse cowgirl',
378                 'rimjob',
379                 'rimming',
380                 'rosy palm',
381                 'rusty trombone',
382                 's & m',
383                 's&m',
384                 's+m',
385                 'sadism',
386                 'scat',
387                 'schlong',
388                 'scissoring',
389                 'semen',
390                 'sex',
391                 'sexi',
392                 'sexo',
393                 'sexy',
394                 'shaved beaver',
395                 'shaved pussy',
396                 'shemale',
397                 'shibari',
398                 'shit',
399                 'shota',
400                 'shrimping',
401                 'slanteye',
402                 'slut',
403                 'smut',
404                 'snatch',
405                 'snm',
406                 'snowballing',
407                 'sodomi',
408                 'sodomize',
409                 'sodomy',
410                 'spic',
411                 'spooge',
412                 'spread legs',
413                 'squirting',
414                 'strap on',
415                 'strapon',
416                 'strappado',
417                 'strip club',
418                 'style doggy',
419                 'suck',
420                 'suicide girls',
421                 'sultry women',
422                 'swastika',
423                 'swinger',
424                 'taint',
425                 'tainted love',
426                 'taste my',
427                 'tea bagging',
428                 'threesome',
429                 'throating',
430                 'tied up',
431                 'tight white',
432                 'tit',
433                 'tits',
434                 'titti',
435                 'titties',
436                 'titty',
437                 'tongue in',
438                 'topless',
439                 'tosser',
440                 'towelhead',
441                 'tranny',
442                 'tribadism',
443                 'tub girl',
444                 'tubgirl',
445                 'tushy',
446                 'twat',
447                 'twink',
448                 'twinki',
449                 'twinkie',
450                 'undress',
451                 'upskirt',
452                 'urethra play',
453                 'urophilia',
454                 'vag',
455                 'vagina',
456                 'venus mound',
457                 'vibrator',
458                 'violet blue',
459                 'violet wand',
460                 'vorarephilia',
461                 'voyeur',
462                 'vulva',
463                 'wank',
464                 'wet dream',
465                 'wetback',
466                 'white power',
467                 'whore',
468                 'women rapping',
469                 'wrapping men',
470                 'wrinkled starfish',
471                 'xx',
472                 'xxx',
473                 'yaoi',
474                 'yellow shower',
475                 'yiffy',
476                 'zoophilia',
477             ]
478         )
479         self.stemmer = PorterStemmer()
480
481     def _normalize(self, text: str) -> str:
482         """Normalize text.
483
484         >>> _normalize('Tittie5')
485         'titties'
486
487         >>> _normalize('Suck a Dick!')
488         'suck a dick'
489
490         >>> _normalize('fucking a whore')
491         'fuck a whore'
492
493         """
494         result = text.lower()
495         result = result.replace("_", " ")
496         result = result.replace('0', 'o')
497         result = result.replace('1', 'l')
498         result = result.replace('4', 'a')
499         result = result.replace('5', 's')
500         result = result.replace('3', 'e')
501         for x in string.punctuation:
502             result = result.replace(x, "")
503         chunks = [self.stemmer.stem(word) for word in nltk.word_tokenize(result)]
504         return ' '.join(chunks)
505
506     @staticmethod
507     def tokenize(text: str):
508         for x in nltk.word_tokenize(text):
509             for y in re.split(r'\W+', x):
510                 yield y
511
512     def contains_bad_word(self, text: str) -> bool:
513         """Returns True if text contains a bad word (or more than one)
514         and False if no bad words were detected.
515
516         >>> contains_bad_word('fuck you')
517         True
518
519         >>> contains_bad_word('FucK u')
520         True
521
522         >>> contains_bad_word('FuK U')
523         False
524
525         """
526         words = list(self.tokenize(text))
527         for word in words:
528             if self.is_bad_word(word):
529                 logger.debug('"%s" is profanity', word)
530                 return True
531
532         if len(words) > 1:
533             for bigram in string_utils.ngrams_presplit(words, 2):
534                 bigram = ' '.join(bigram)
535                 if self.is_bad_word(bigram):
536                     logger.debug('"%s" is profanity', bigram)
537                     return True
538
539         if len(words) > 2:
540             for trigram in string_utils.ngrams_presplit(words, 3):
541                 trigram = ' '.join(trigram)
542                 if self.is_bad_word(trigram):
543                     logger.debug('"%s" is profanity', trigram)
544                     return True
545         return False
546
547     def is_bad_word(self, word: str) -> bool:
548         return word in self.bad_words or self._normalize(word) in self.bad_words
549
550     def obscure_bad_words(self, text: str) -> str:
551         """Obscure bad words that are detected by inserting random punctuation
552         characters.
553
554         """
555
556         def obscure(word: str):
557             out = ''
558             last = ''
559             for letter in word:
560                 if letter.isspace():
561                     out += letter
562                 else:
563                     while True:
564                         char = random.choice(['#', '%', '!', '@', '&', '*'])
565                         if last != char:
566                             last = char
567                             out += char
568                             break
569             return out
570
571         words = list(self.tokenize(text))
572         words.append('')
573         words.append('')
574         words.append('')
575         out = ''
576
577         cursor = 0
578         while cursor < len(words) - 3:
579             word = words[cursor]
580             bigram = word + ' ' + words[cursor + 1]
581             trigram = bigram + ' ' + words[cursor + 2]
582             if self.is_bad_word(trigram):
583                 out += obscure(trigram) + ' '
584                 cursor += 3
585             elif self.is_bad_word(bigram):
586                 out += obscure(bigram) + ' '
587                 cursor += 2
588             elif self.is_bad_word(word):
589                 out += obscure(word) + ' '
590                 cursor += 1
591             else:
592                 out += word + ' '
593                 cursor += 1
594         return out.strip()
595
596
597 def main() -> None:
598     import doctest
599
600     doctest.testmod()
601     pf = ProfanityFilter()
602     phrase = ' '.join(sys.argv[1:])
603     print(pf.contains_bad_word(phrase))
604     print(pf.obscure_bad_words(phrase))
605     sys.exit(0)
606
607
608 if __name__ == '__main__':
609     main()