Update profanity.
[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                 'gave you head',
248                 'gave him head',
249                 'gave them head',
250                 'gave us head',
251                 'goatcx',
252                 'goatse',
253                 'goddamn',
254                 'gokkun',
255                 'golden shower',
256                 'goo girl',
257                 'goodpoop',
258                 'goregasm',
259                 'grope',
260                 'group sex',
261                 'gspot',
262                 'guro',
263                 'hand job',
264                 'handjob',
265                 'hard core',
266                 'hardcore',
267                 'hentai',
268                 'homoerotic',
269                 'honkey',
270                 'hooker',
271                 'horni',
272                 'horny',
273                 'hot chick',
274                 'how to kill',
275                 'how to murder',
276                 'huge fat',
277                 'humped',
278                 'humping',
279                 'humps',
280                 'incest',
281                 'intercourse',
282                 'jack off',
283                 'jail bait',
284                 'jailbait',
285                 'jerk off',
286                 'jigaboo',
287                 'jiggaboo',
288                 'jiggerboo',
289                 'jizz',
290                 'jugg',
291                 'kike',
292                 'kinbaku',
293                 'kinkster',
294                 'kinky',
295                 'knobbing',
296                 'leather restraint',
297                 'lemon party',
298                 'lolita',
299                 'lovemaking',
300                 'make me come',
301                 'male squirting',
302                 'masturb',
303                 'menage a trois',
304                 'milf',
305                 'missionary position',
306                 'motherfuck',
307                 'mound of venus',
308                 'mr hand',
309                 'muff diver',
310                 'muffdiv',
311                 'muffdiving',
312                 'nambla',
313                 'nawashi',
314                 'negro',
315                 'neonazi',
316                 'nig nog',
317                 'nigga',
318                 'nigger',
319                 'nimphomania',
320                 'nipple',
321                 'nip',
322                 'not safe for',
323                 'nsfl',
324                 'nsfw',
325                 'nude',
326                 'nudes',
327                 'nudity',
328                 'nut sack',
329                 'nutsack',
330                 'nympho',
331                 'nymphomania',
332                 'octopussy',
333                 'omorashi',
334                 'one night stand',
335                 'orgasm',
336                 'orgy',
337                 'paedophil',
338                 'paedophile',
339                 'panties',
340                 'panty',
341                 'pedobear',
342                 'pedophil',
343                 'pedophile',
344                 'pee',
345                 'pegging',
346                 'peni',
347                 'penis',
348                 'phone sex',
349                 'pigfucker',
350                 'piss pig',
351                 'piss',
352                 'pissing',
353                 'pisspig',
354                 'playboy',
355                 'pleasure chest',
356                 'pole smoker',
357                 'ponyplay',
358                 'poof',
359                 'poop chute',
360                 'poopchute',
361                 'porn',
362                 'pron',
363                 'pornhub',
364                 'porno',
365                 'pornographi',
366                 'pornography',
367                 'prince albert',
368                 'pthc',
369                 'pube',
370                 'pussi',
371                 'pussies',
372                 'pussy',
373                 'queaf',
374                 'queer',
375                 'raghead',
376                 'raging boner',
377                 'rape',
378                 'raping',
379                 'rapist',
380                 'rectum',
381                 'reverse cowgirl',
382                 'rimjob',
383                 'rimming',
384                 'rosy palm',
385                 'rusty trombone',
386                 's & m',
387                 's&m',
388                 's+m',
389                 'sadism',
390                 'scat',
391                 'schlong',
392                 'scissoring',
393                 'semen',
394                 'sex',
395                 'sexi',
396                 'sexo',
397                 'sexy',
398                 'shaved beaver',
399                 'shaved pussy',
400                 'shemale',
401                 'shibari',
402                 'shit',
403                 'shota',
404                 'shrimping',
405                 'slanteye',
406                 'slut',
407                 'smut',
408                 'snatch',
409                 'snm',
410                 'snowballing',
411                 'sodomi',
412                 'sodomize',
413                 'sodomy',
414                 'spic',
415                 'spooge',
416                 'spread legs',
417                 'squirting',
418                 'strap on',
419                 'strapon',
420                 'strappado',
421                 'strip club',
422                 'style doggy',
423                 'suck',
424                 'suicide girls',
425                 'sultry women',
426                 'swastika',
427                 'swinger',
428                 'taint',
429                 'tainted love',
430                 'taste my',
431                 'tea bagging',
432                 'threesome',
433                 'throating',
434                 'tied up',
435                 'tight white',
436                 'tit',
437                 'tits',
438                 'titti',
439                 'titties',
440                 'titty',
441                 'tongue in',
442                 'topless',
443                 'tosser',
444                 'towelhead',
445                 'tranny',
446                 'tribadism',
447                 'tub girl',
448                 'tubgirl',
449                 'tushy',
450                 'twat',
451                 'twink',
452                 'twinki',
453                 'twinkie',
454                 'undress',
455                 'upskirt',
456                 'urethra play',
457                 'urophilia',
458                 'vag',
459                 'vagina',
460                 'venus mound',
461                 'vibrator',
462                 'violet blue',
463                 'violet wand',
464                 'vorarephilia',
465                 'voyeur',
466                 'vulva',
467                 'wank',
468                 'wet dream',
469                 'wetback',
470                 'white power',
471                 'whore',
472                 'women rapping',
473                 'wrapping men',
474                 'wrinkled starfish',
475                 'xx',
476                 'xxx',
477                 'yaoi',
478                 'yellow shower',
479                 'yiffy',
480                 'zoophilia',
481             ]
482         )
483         self.stemmer = PorterStemmer()
484
485     def _normalize(self, text: str) -> str:
486         """Normalize text.
487
488         >>> _normalize('Tittie5')
489         'titties'
490
491         >>> _normalize('Suck a Dick!')
492         'suck a dick'
493
494         >>> _normalize('fucking a whore')
495         'fuck a whore'
496
497         """
498         result = text.lower()
499         result = result.replace("_", " ")
500         result = result.replace('0', 'o')
501         result = result.replace('1', 'l')
502         result = result.replace('4', 'a')
503         result = result.replace('5', 's')
504         result = result.replace('3', 'e')
505         for x in string.punctuation:
506             result = result.replace(x, "")
507         chunks = [self.stemmer.stem(word) for word in nltk.word_tokenize(result)]
508         return ' '.join(chunks)
509
510     @staticmethod
511     def tokenize(text: str):
512         for x in nltk.word_tokenize(text):
513             for y in re.split(r'\W+', x):
514                 yield y
515
516     def contains_bad_word(self, text: str) -> bool:
517         """Returns True if text contains a bad word (or more than one)
518         and False if no bad words were detected.
519
520         >>> contains_bad_word('fuck you')
521         True
522
523         >>> contains_bad_word('FucK u')
524         True
525
526         >>> contains_bad_word('FuK U')
527         False
528
529         """
530         words = list(self.tokenize(text))
531         for word in words:
532             if self.is_bad_word(word):
533                 logger.debug('"%s" is profanity', word)
534                 return True
535
536         if len(words) > 1:
537             for bigram in string_utils.ngrams_presplit(words, 2):
538                 bigram = ' '.join(bigram)
539                 if self.is_bad_word(bigram):
540                     logger.debug('"%s" is profanity', bigram)
541                     return True
542
543         if len(words) > 2:
544             for trigram in string_utils.ngrams_presplit(words, 3):
545                 trigram = ' '.join(trigram)
546                 if self.is_bad_word(trigram):
547                     logger.debug('"%s" is profanity', trigram)
548                     return True
549         return False
550
551     def is_bad_word(self, word: str) -> bool:
552         return word in self.bad_words or self._normalize(word) in self.bad_words
553
554     def obscure_bad_words(self, text: str) -> str:
555         """Obscure bad words that are detected by inserting random punctuation
556         characters.
557
558         """
559
560         def obscure(word: str):
561             out = ''
562             last = ''
563             for letter in word:
564                 if letter.isspace():
565                     out += letter
566                 else:
567                     while True:
568                         char = random.choice(['#', '%', '!', '@', '&', '*'])
569                         if last != char:
570                             last = char
571                             out += char
572                             break
573             return out
574
575         words = list(self.tokenize(text))
576         words.append('')
577         words.append('')
578         words.append('')
579         out = ''
580
581         cursor = 0
582         while cursor < len(words) - 3:
583             word = words[cursor]
584             bigram = word + ' ' + words[cursor + 1]
585             trigram = bigram + ' ' + words[cursor + 2]
586             if self.is_bad_word(trigram):
587                 out += obscure(trigram) + ' '
588                 cursor += 3
589             elif self.is_bad_word(bigram):
590                 out += obscure(bigram) + ' '
591                 cursor += 2
592             elif self.is_bad_word(word):
593                 out += obscure(word) + ' '
594                 cursor += 1
595             else:
596                 out += word + ' '
597                 cursor += 1
598         return out.strip()
599
600
601 def main() -> None:
602     import doctest
603
604     doctest.testmod()
605     pf = ProfanityFilter()
606     phrase = ' '.join(sys.argv[1:])
607     print(pf.contains_bad_word(phrase))
608     print(pf.obscure_bad_words(phrase))
609     sys.exit(0)
610
611
612 if __name__ == '__main__':
613     main()