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