Skip to content

Commit 64a53cc

Browse files
committed
Add legal indicators whitelist to gibberish detection
1 parent c05a13d commit 64a53cc

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

src/textcode/gibberish.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/python
2+
#
3+
# From: https://raw.githubusercontent.com/yapus/gibberish/01637fe1fda827529ca76b8d6fee2de9100719f1/gibberish/gibberish.py
4+
#
5+
# 12Jun2017 Petr Janata - added srcfile and outfile
6+
# 17Jun2107 Petr Janata - expanded set of accepted characters to include digits and hyphen
7+
#
8+
# whch is based off of:
9+
# https://raw.githubusercontent.com/rrenaud/Gibberish-Detector/aa1d4e4555362b3dada97ebe6ecc23a84fc470fe/gib_detect_train.py
10+
#
11+
12+
import math
13+
import pickle
14+
from pathlib import Path
15+
16+
data_dir = Path(__file__).parent / 'data' / 'gibberish'
17+
model_path = data_dir / 'gib_model.pki'
18+
big_file_path = data_dir / 'big.txt'
19+
good_file_path = data_dir / 'good.txt'
20+
bad_file_path = data_dir / 'bad.txt'
21+
22+
accepted_chars = 'abcdefghijklmnopqrstuvwxyz0123456789- '
23+
pos = dict([(char, idx) for idx, char in enumerate(accepted_chars)])
24+
25+
26+
class Gibberish(object):
27+
def __init__(self):
28+
if model_path.exists():
29+
self.load_persisted_model()
30+
else:
31+
self.train()
32+
33+
def persist_model(self):
34+
with open(model_path, mode='wb') as f:
35+
pickle.dump(vars(self), f)
36+
37+
def load_persisted_model(self):
38+
with open(model_path, mode='rb') as f:
39+
persisted_model = pickle.load(f)
40+
for key, value in persisted_model.items():
41+
setattr(self, key, value)
42+
43+
def normalize(self, line):
44+
""" Return only the subset of chars from accepted_chars.
45+
This helps keep the model relatively small by ignoring punctuation,
46+
infrequenty symbols, etc. """
47+
return [c.lower() for c in line if c.lower() in accepted_chars]
48+
49+
def ngram(self, n, l):
50+
""" Return all n grams from l after normalizing """
51+
filtered = self.normalize(l)
52+
for start in range(0, len(filtered) - n + 1):
53+
yield ''.join(filtered[start:start + n])
54+
55+
def avg_transition_prob(self, l, log_prob_mat):
56+
""" Return the average transition prob from l through log_prob_mat. """
57+
log_prob = 0.0
58+
transition_ct = 0
59+
for a, b in self.ngram(2, l):
60+
log_prob += log_prob_mat[pos[a]][pos[b]]
61+
transition_ct += 1
62+
# The exponentiation translates from log probs to probs.
63+
return math.exp(log_prob / (transition_ct or 1))
64+
65+
def train(self, bigfile=big_file_path, goodfile=good_file_path,
66+
badfile=bad_file_path):
67+
""" Write a simple model as a pickle file """
68+
k = len(accepted_chars)
69+
# Assume we have seen 10 of each character pair. This acts as a kind of
70+
# prior or smoothing factor. This way, if we see a character transition
71+
# live that we've never observed in the past, we won't assume the entire
72+
# string has 0 probability.
73+
counts = [[10 for i in range(k)] for i in range(k)]
74+
75+
# Count transitions from big text file, taken
76+
# from http://norvig.com/spell-correct.html
77+
for line in open(bigfile, encoding='utf-8'):
78+
for a, b in self.ngram(2, line):
79+
counts[pos[a]][pos[b]] += 1
80+
81+
# Normalize the counts so that they become log probabilities.
82+
# We use log probabilities rather than straight probabilities to avoid
83+
# numeric underflow issues with long texts.
84+
# This contains a justification:
85+
# http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/
86+
for i, row in enumerate(counts):
87+
s = float(sum(row))
88+
for j in range(len(row)):
89+
row[j] = math.log(row[j] / s)
90+
91+
# Find the probability of generating a few arbitrarily choosen good and
92+
# bad phrases.
93+
good_probs = [self.avg_transition_prob(l, counts) for l in open(goodfile, encoding='utf-8')]
94+
bad_probs = [self.avg_transition_prob(l, counts) for l in open(badfile, encoding='utf-8')]
95+
96+
# Assert that we actually are capable of detecting the junk.
97+
assert min(good_probs) > max(bad_probs)
98+
99+
# And pick a threshold halfway between the worst good and best bad inputs.
100+
thresh = (min(good_probs) + max(bad_probs)) / 2
101+
self.mat = counts
102+
self.thresh = thresh
103+
self.persist_model()
104+
105+
def detect_gibberish(self, text):
106+
COPYRIGHT_INDICATORS = (
107+
'copyright', '(c)', 'c)', '©', '@copyright',
108+
'author:', 'commit', 'portions:', 'rights reserved',
109+
'(p)', 'trademark', 'intellectual property'
110+
)
111+
112+
text_lower = text.lower()
113+
if any(indicator in text_lower for indicator in COPYRIGHT_INDICATORS):
114+
return False
115+
116+
text_normalized = ''.join(self.normalize(text))
117+
return self.avg_transition_prob(text_normalized, self.mat) < self.thresh
118+
119+
def percent_gibberish(self, text):
120+
text = ''.join(self.normalize(text))
121+
text = text.strip()
122+
words = text.split(' ')
123+
if len(words) == 0:
124+
return 0
125+
126+
gibberish_count = 0
127+
for word in words:
128+
if self.detect_gibberish(word):
129+
gibberish_count += 1
130+
131+
return float(gibberish_count) / float(len(words))
132+
133+
def gibberish_pct(self, text):
134+
text = ''.join(self.normalize(text))
135+
return self.avg_transition_prob(text, self.mat)

0 commit comments

Comments
 (0)