Merge remote-tracking branch 'origin/master'

# Conflicts:
#	spacy/en/tokenizer_exceptions.py
This commit is contained in:
Ines Montani 2017-01-03 18:20:01 +01:00
commit d3b181cdf1
7 changed files with 117 additions and 12 deletions

View File

@ -12,8 +12,6 @@
<!--- Include details of your testing environment, and the tests you ran too --> <!--- Include details of your testing environment, and the tests you ran too -->
<!--- How were other areas of the code affected? --> <!--- How were other areas of the code affected? -->
## Screenshots (if appropriate):
## Types of changes ## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all applicable boxes.: --> <!--- What types of changes does your code introduce? Put an `x` in all applicable boxes.: -->
- [ ] Bug fix (non-breaking change fixing an issue) - [ ] Bug fix (non-breaking change fixing an issue)
@ -27,4 +25,4 @@
- [ ] My change requires a change to spaCy's documentation. - [ ] My change requires a change to spaCy's documentation.
- [ ] I have updated the documentation accordingly. - [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes. - [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed. - [ ] All new and existing tests passed.

View File

@ -67,6 +67,8 @@ class BaseDefaults(object):
@classmethod @classmethod
def create_tokenizer(cls, nlp=None): def create_tokenizer(cls, nlp=None):
rules = cls.tokenizer_exceptions rules = cls.tokenizer_exceptions
if cls.token_match:
token_match = cls.token_match
if cls.prefixes: if cls.prefixes:
prefix_search = util.compile_prefix_regex(cls.prefixes).search prefix_search = util.compile_prefix_regex(cls.prefixes).search
else: else:
@ -82,7 +84,7 @@ class BaseDefaults(object):
vocab = nlp.vocab if nlp is not None else cls.create_vocab(nlp) vocab = nlp.vocab if nlp is not None else cls.create_vocab(nlp)
return Tokenizer(vocab, rules=rules, return Tokenizer(vocab, rules=rules,
prefix_search=prefix_search, suffix_search=suffix_search, prefix_search=prefix_search, suffix_search=suffix_search,
infix_finditer=infix_finditer) infix_finditer=infix_finditer, token_match=token_match)
@classmethod @classmethod
def create_tagger(cls, nlp=None): def create_tagger(cls, nlp=None):
@ -142,6 +144,8 @@ class BaseDefaults(object):
pipeline.append(nlp.entity) pipeline.append(nlp.entity)
return pipeline return pipeline
token_match = language_data.TOKEN_MATCH
prefixes = tuple(language_data.TOKENIZER_PREFIXES) prefixes = tuple(language_data.TOKENIZER_PREFIXES)
suffixes = tuple(language_data.TOKENIZER_SUFFIXES) suffixes = tuple(language_data.TOKENIZER_SUFFIXES)

View File

@ -3,3 +3,4 @@ from .punctuation import *
from .tag_map import * from .tag_map import *
from .entity_rules import * from .entity_rules import *
from .util import * from .util import *
from .tokenizer_exceptions import *

View File

@ -0,0 +1,11 @@
from __future__ import unicode_literals
import re
_URL_PATTERN = r'''
^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w\-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$
'''.strip()
TOKEN_MATCH = re.compile(_URL_PATTERN).match
__all__ = ['TOKEN_MATCH']

View File

@ -0,0 +1,77 @@
from __future__ import unicode_literals
import pytest
URLS = [
u"http://www.nytimes.com/2016/04/20/us/politics/new-york-primary-preview.html?hp&action=click&pgtype=Homepage&clickSource=story-heading&module=a-lede-package-region&region=top-news&WT.nav=top-news&_r=0",
u"www.google.com?q=google",
u"google.com",
u"www.red-stars.com",
pytest.mark.xfail(u"red-stars.com"),
u"http://foo.com/blah_(wikipedia)#cite-1",
u"http://www.example.com/wpstyle/?bar=baz&inga=42&quux",
u"mailto:foo.bar@baz.com",
u"mailto:foo-bar@baz-co.com"
]
# Punctuation we want to check is split away before the URL
PREFIXES = [
"(", '"', "...", ">"
]
# Punctuation we want to check is split away after the URL
SUFFIXES = [
'"', ":", ">"]
@pytest.mark.parametrize("text", URLS)
def test_simple_url(en_tokenizer, text):
tokens = en_tokenizer(text)
assert tokens[0].orth_ == text
assert len(tokens) == 1
@pytest.mark.parametrize("prefix", PREFIXES)
@pytest.mark.parametrize("url", URLS)
def test_prefixed_url(en_tokenizer, prefix, url):
tokens = en_tokenizer(prefix + url)
assert tokens[0].text == prefix
assert tokens[1].text == url
assert len(tokens) == 2
@pytest.mark.parametrize("suffix", SUFFIXES)
@pytest.mark.parametrize("url", URLS)
def test_suffixed_url(en_tokenizer, url, suffix):
tokens = en_tokenizer(url + suffix)
assert tokens[0].text == url
assert tokens[1].text == suffix
assert len(tokens) == 2
@pytest.mark.parametrize("prefix", PREFIXES)
@pytest.mark.parametrize("suffix", SUFFIXES)
@pytest.mark.parametrize("url", URLS)
def test_surround_url(en_tokenizer, prefix, suffix, url):
tokens = en_tokenizer(prefix + url + suffix)
assert tokens[0].text == prefix
assert tokens[1].text == url
assert tokens[2].text == suffix
assert len(tokens) == 3
@pytest.mark.parametrize("prefix1", PREFIXES)
@pytest.mark.parametrize("prefix2", PREFIXES)
@pytest.mark.parametrize("url", URLS)
def test_two_prefix_url(en_tokenizer, prefix1, prefix2, url):
tokens = en_tokenizer(prefix1 + prefix2 + url)
assert tokens[0].text == prefix1
assert tokens[1].text == prefix2
assert tokens[2].text == url
assert len(tokens) == 3
@pytest.mark.parametrize("suffix1", SUFFIXES)
@pytest.mark.parametrize("suffix2", SUFFIXES)
@pytest.mark.parametrize("url", URLS)
def test_two_prefix_url(en_tokenizer, suffix1, suffix2, url):
tokens = en_tokenizer(url + suffix1 + suffix2)
assert tokens[0].text == url
assert tokens[1].text == suffix1
assert tokens[2].text == suffix2
assert len(tokens) == 3

View File

@ -16,6 +16,7 @@ cdef class Tokenizer:
cdef PreshMap _specials cdef PreshMap _specials
cpdef readonly Vocab vocab cpdef readonly Vocab vocab
cdef public object token_match
cdef public object prefix_search cdef public object prefix_search
cdef public object suffix_search cdef public object suffix_search
cdef public object infix_finditer cdef public object infix_finditer

View File

@ -29,7 +29,7 @@ cdef class Tokenizer:
"""Segment text, and create Doc objects with the discovered segment boundaries.""" """Segment text, and create Doc objects with the discovered segment boundaries."""
@classmethod @classmethod
def load(cls, path, Vocab vocab, rules=None, prefix_search=None, suffix_search=None, def load(cls, path, Vocab vocab, rules=None, prefix_search=None, suffix_search=None,
infix_finditer=None): infix_finditer=None, token_match=None):
'''Load a Tokenizer, reading unsupplied components from the path. '''Load a Tokenizer, reading unsupplied components from the path.
Arguments: Arguments:
@ -39,6 +39,8 @@ cdef class Tokenizer:
A storage container for lexical types. A storage container for lexical types.
rules (dict): rules (dict):
Exceptions and special-cases for the tokenizer. Exceptions and special-cases for the tokenizer.
token_match:
A boolean function matching strings that becomes tokens.
prefix_search: prefix_search:
Signature of re.compile(string).search Signature of re.compile(string).search
suffix_search: suffix_search:
@ -65,10 +67,9 @@ cdef class Tokenizer:
with (path / 'tokenizer' / 'infix.txt').open() as file_: with (path / 'tokenizer' / 'infix.txt').open() as file_:
entries = file_.read().split('\n') entries = file_.read().split('\n')
infix_finditer = util.compile_infix_regex(entries).finditer infix_finditer = util.compile_infix_regex(entries).finditer
return cls(vocab, rules, prefix_search, suffix_search, infix_finditer) return cls(vocab, rules, prefix_search, suffix_search, infix_finditer, token_match)
def __init__(self, Vocab vocab, rules, prefix_search, suffix_search, infix_finditer, token_match=None):
def __init__(self, Vocab vocab, rules, prefix_search, suffix_search, infix_finditer):
'''Create a Tokenizer, to create Doc objects given unicode text. '''Create a Tokenizer, to create Doc objects given unicode text.
Arguments: Arguments:
@ -85,10 +86,13 @@ cdef class Tokenizer:
infix_finditer: infix_finditer:
A function matching the signature of re.compile(string).finditer A function matching the signature of re.compile(string).finditer
to find infixes. to find infixes.
token_match:
A boolean function matching strings that becomes tokens.
''' '''
self.mem = Pool() self.mem = Pool()
self._cache = PreshMap() self._cache = PreshMap()
self._specials = PreshMap() self._specials = PreshMap()
self.token_match = token_match
self.prefix_search = prefix_search self.prefix_search = prefix_search
self.suffix_search = suffix_search self.suffix_search = suffix_search
self.infix_finditer = infix_finditer self.infix_finditer = infix_finditer
@ -100,9 +104,10 @@ cdef class Tokenizer:
def __reduce__(self): def __reduce__(self):
args = (self.vocab, args = (self.vocab,
self._rules, self._rules,
self._prefix_re, self._prefix_re,
self._suffix_re, self._suffix_re,
self._infix_re) self._infix_re,
self.token_match)
return (self.__class__, args, None, None) return (self.__class__, args, None, None)
@ -216,6 +221,8 @@ cdef class Tokenizer:
cdef unicode minus_suf cdef unicode minus_suf
cdef size_t last_size = 0 cdef size_t last_size = 0
while string and len(string) != last_size: while string and len(string) != last_size:
if self.token_match and self.token_match(string):
break
last_size = len(string) last_size = len(string)
pre_len = self.find_prefix(string) pre_len = self.find_prefix(string)
if pre_len != 0: if pre_len != 0:
@ -226,6 +233,8 @@ cdef class Tokenizer:
string = minus_pre string = minus_pre
prefixes.push_back(self.vocab.get(mem, prefix)) prefixes.push_back(self.vocab.get(mem, prefix))
break break
if self.token_match and self.token_match(string):
break
suf_len = self.find_suffix(string) suf_len = self.find_suffix(string)
if suf_len != 0: if suf_len != 0:
suffix = string[-suf_len:] suffix = string[-suf_len:]
@ -263,7 +272,11 @@ cdef class Tokenizer:
tokens.push_back(prefixes[0][i], False) tokens.push_back(prefixes[0][i], False)
if string: if string:
cache_hit = self._try_cache(hash_string(string), tokens) cache_hit = self._try_cache(hash_string(string), tokens)
if not cache_hit: if cache_hit:
pass
elif self.token_match and self.token_match(string):
tokens.push_back(self.vocab.get(tokens.mem, string), not suffixes.size())
else:
matches = self.find_infix(string) matches = self.find_infix(string)
if not matches: if not matches:
tokens.push_back(self.vocab.get(tokens.mem, string), False) tokens.push_back(self.vocab.get(tokens.mem, string), False)