From 31843316bc787cc2bfdf29c93ffcd9f9a81f1cbb Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Fri, 14 May 2010 14:33:26 -0300 Subject: [PATCH] Added new instance based learning extraction library in scrapy.contrib.ibl. Documentation and tools will be added later. --- scrapy/contrib/ibl/__init__.py | 10 + scrapy/contrib/ibl/descriptor.py | 51 ++ scrapy/contrib/ibl/extraction/__init__.py | 126 +++ scrapy/contrib/ibl/extraction/pageobjects.py | 230 +++++ scrapy/contrib/ibl/extraction/pageparsing.py | 318 +++++++ .../contrib/ibl/extraction/regionextract.py | 651 ++++++++++++++ scrapy/contrib/ibl/extraction/similarity.py | 136 +++ scrapy/contrib/ibl/extractors.py | 156 ++++ scrapy/contrib/ibl/htmlpage.py | 212 +++++ scrapy/tests/test_contrib_ibl/__init__.py | 2 + .../test_contrib_ibl/samples_htmlpage.json.gz | Bin 0 -> 105488 bytes .../samples_pageparsing.json.gz | Bin 0 -> 9013 bytes .../tests/test_contrib_ibl/test_extraction.py | 819 ++++++++++++++++++ .../tests/test_contrib_ibl/test_htmlpage.py | 134 +++ .../test_contrib_ibl/test_htmlpage_data.py | 227 +++++ .../test_contrib_ibl/test_pageparsing.py | 283 ++++++ 16 files changed, 3355 insertions(+) create mode 100644 scrapy/contrib/ibl/__init__.py create mode 100644 scrapy/contrib/ibl/descriptor.py create mode 100644 scrapy/contrib/ibl/extraction/__init__.py create mode 100644 scrapy/contrib/ibl/extraction/pageobjects.py create mode 100644 scrapy/contrib/ibl/extraction/pageparsing.py create mode 100644 scrapy/contrib/ibl/extraction/regionextract.py create mode 100644 scrapy/contrib/ibl/extraction/similarity.py create mode 100644 scrapy/contrib/ibl/extractors.py create mode 100644 scrapy/contrib/ibl/htmlpage.py create mode 100644 scrapy/tests/test_contrib_ibl/__init__.py create mode 100644 scrapy/tests/test_contrib_ibl/samples_htmlpage.json.gz create mode 100644 scrapy/tests/test_contrib_ibl/samples_pageparsing.json.gz create mode 100644 scrapy/tests/test_contrib_ibl/test_extraction.py create mode 100644 scrapy/tests/test_contrib_ibl/test_htmlpage.py create mode 100644 scrapy/tests/test_contrib_ibl/test_htmlpage_data.py create mode 100644 scrapy/tests/test_contrib_ibl/test_pageparsing.py diff --git a/scrapy/contrib/ibl/__init__.py b/scrapy/contrib/ibl/__init__.py new file mode 100644 index 000000000..3179c6e63 --- /dev/null +++ b/scrapy/contrib/ibl/__init__.py @@ -0,0 +1,10 @@ +""" +This contrib implements an automatic extraction library based on an Instance +Based Learning (IBL) algorithm, as described in the following papers: + +A hierarchical approach to wrapper induction + http://portal.acm.org/citation.cfm?id=301191 + +Extracting web data using instance based learning + http://portal.acm.org/citation.cfm?id=1265174 +""" diff --git a/scrapy/contrib/ibl/descriptor.py b/scrapy/contrib/ibl/descriptor.py new file mode 100644 index 000000000..9780ee0cf --- /dev/null +++ b/scrapy/contrib/ibl/descriptor.py @@ -0,0 +1,51 @@ +""" +Extended types for IBL extraction +""" +from itertools import chain + +from scrapy.contrib.ibl.extractors import text + +class FieldDescriptor(object): + """description of a scraped attribute""" + __slots__ = ('name', 'description', 'extractor', 'required', 'allow_markup') + + def __init__(self, name, description, extractor=text, required=False, + allow_markup=False): + self.name = name + self.description = description + self.extractor = extractor + self.required = required + self.allow_markup = allow_markup + + def __str__(self): + return "FieldDescriptor(%s)" % self.name + +class ItemDescriptor(object): + """Simple auto scraping item descriptor. + + This used to describe type-specific operations and may be overridden where + necessary. + """ + + def __init__(self, name, description, attribute_descriptors): + self.name = name + self.attribute_map = dict((d.name, d) for d in attribute_descriptors) + self._required_attributes = [d.name for d in attribute_descriptors \ + if d.required] + + def validated(self, data): + """Only return the items in the data that are valid""" + return [d for d in data if self._item_validates(d)] + + def _item_validates(self, item): + """simply checks that all mandatory attributes are present""" + variant_attrs = set(chain(* + [v.keys() for v in item.get('variants', [])])) + return all([(name in item or name in variant_attrs) \ + for name in self._required_attributes]) + + def get_required_attributes(self): + return self._required_attributes + + def __str__(self): + return "ItemDescriptor(%s)" % self.name diff --git a/scrapy/contrib/ibl/extraction/__init__.py b/scrapy/contrib/ibl/extraction/__init__.py new file mode 100644 index 000000000..4d5cf6559 --- /dev/null +++ b/scrapy/contrib/ibl/extraction/__init__.py @@ -0,0 +1,126 @@ +""" +IBL module + +This contains an extraction algorithm based on the paper Extracting Web Data +Using Instance-Based Learning by Yanhong Zhai and Bing Liu. + +It defines the InstanceBasedLearningExtractor class, which implements this +extraction algorithm. + +Main departures from the original algorithm: + * there is no limit in prefix or suffix size + * we have "attribute adaptors" that allow generic post processing and may + affect the extraction process. For example, a price field may require a + numeric value to be present. + * tags can be inserted to extract regions not wrapped by html tags. These + regions are then identified using the longest unique character prefix and + suffix. +""" +from operator import itemgetter +from .regionextract import build_extraction_tree +from .pageparsing import parse_template, parse_extraction_page +from .pageobjects import TokenDict, AnnotationText +from .similarity import common_prefix + +class InstanceBasedLearningExtractor(object): + """Implementation of the instance based learning algorithm to + extract data from web pages. + """ + + def __init__(self, templates, type_descriptor=None, trace=False): + """Initialise this extractor + + templates should contain a sequence of strings, each containing + annotated html that will be used as templates for extraction. + + Tags surrounding areas to be extracted must contain a + 'data-scrapy-annotate' attribute and the value must be the name + of the attribute. If the tag was inserted and was not present in the + original page, the data-scrapy-generated attribute must be present. + + type_descriptor may contain a type descriptor describing the item + to be extracted. + + if trace is true, the returned extracted data will have a 'trace' + property that contains a trace of the extraction execution. + """ + self.token_dict = TokenDict() + parsed_plus_templates = [(parse_template(self.token_dict, t), t) for t in templates] + parsed_plus_epages = [(p, parse_extraction_page(self.token_dict, t)) for p, t \ + in parsed_plus_templates if _annotation_count(p)] + parsed_templates = map(itemgetter(0), parsed_plus_epages) + + # calculate common text prefixes for annotations of same field across all templates + extracted_text = {} + extraction_pages = map(itemgetter(1), parsed_plus_epages) + for i, parsed in enumerate(parsed_templates): + for annot in parsed.annotations: + if annot.match_common_prefix: + field = annot.surrounds_attribute + if field is not None: + descriptor = type_descriptor.attribute_map.get(field) if type_descriptor else None + allow_markup = descriptor.allow_markup if descriptor else False + start, end = annot.start_index, annot.end_index + if allow_markup: + text = extraction_pages[i].html_between_tokens(start, end) + else: + text = extraction_pages[i].text_between_tokens(start, end) + extracted_text.setdefault(field, []).append(text) + common_prefixes = {} + for field, data in extracted_text.iteritems(): + if len(data) > 1: + cprefix = common_prefix(*data) + if cprefix: + common_prefixes[field] = "".join(cprefix).strip() + # now apply common prefixes to annotations + for i, parsed in enumerate(parsed_templates): + for annot in parsed.annotations: + for field, prefix in common_prefixes.iteritems(): + if annot.surrounds_attribute == field and not annot.annotation_text: + annot.annotation_text = AnnotationText(prefix) + + # templates with more attributes are considered first + sorted_templates = sorted(parsed_templates, key=_annotation_count, reverse=True) + self.extraction_trees = [build_extraction_tree(t, type_descriptor, + trace) for t in sorted_templates] + self.validated = type_descriptor.validated if type_descriptor else \ + self._filter_not_none + + def extract(self, html, pref_template_id=None, useone=False): + """extract data from an html page + + If pref_template_url is specified, the template with that url will be + used first. + if useone is True and no data was extracted, no additional template will + be tried. If False and no data was extracted, try with rest of item templates + """ + extraction_page = parse_extraction_page(self.token_dict, html) + if pref_template_id is not None: + if useone: + extraction_trees = [x for x in self.extraction_trees if x.template.id == pref_template_id] + else: + extraction_trees = sorted(self.extraction_trees, + key=lambda x: x.template.id != pref_template_id) + else: + extraction_trees = self.extraction_trees + + for extraction_tree in extraction_trees: + extracted = extraction_tree.extract(extraction_page) + correctly_extracted = self.validated(extracted) + extra_required = extraction_tree.template.extra_required_attrs + correctly_extracted = [c for c in correctly_extracted if \ + extra_required.intersection(c.keys()) == extra_required ] + if len(correctly_extracted) > 0: + return correctly_extracted, extraction_tree.template.id + return None, None + + def __str__(self): + return "InstanceBasedLearningExtractor[\n%s\n]" % \ + (',\n'.join(map(str, self.extraction_trees))) + + @staticmethod + def _filter_not_none(items): + return [d for d in items if d is not None] + +def _annotation_count(template): + return len(template.annotations) diff --git a/scrapy/contrib/ibl/extraction/pageobjects.py b/scrapy/contrib/ibl/extraction/pageobjects.py new file mode 100644 index 000000000..c57cdfa75 --- /dev/null +++ b/scrapy/contrib/ibl/extraction/pageobjects.py @@ -0,0 +1,230 @@ +""" +Page objects + +This module contains objects representing pages and parts of pages (e.g. tokens +and annotations) used in the instance based learning algorithm. +""" +from numpy import array, ndarray + +from scrapy.contrib.ibl.htmlpage import HtmlTagType + +class TokenType(object): + """constants for token types""" + WORD = 0 + OPEN_TAG = HtmlTagType.OPEN_TAG + CLOSE_TAG = HtmlTagType.CLOSE_TAG + NON_PAIRED_TAG = HtmlTagType.UNPAIRED_TAG + +class TokenDict(object): + """Mapping from parse tokens to integers + + >>> d = TokenDict() + >>> d.tokenid('i') + 0 + >>> d.tokenid('b') + 1 + >>> d.tokenid('i') + 0 + + Tokens can be searched for by id + >>> d.find_token(1) + 'b' + + The lower 24 bits store the token reference and the higher bits the type. + """ + + def __init__(self): + self.token_ids = {} + + def tokenid(self, token, token_type=TokenType.WORD): + """create an integer id from the token and token type passed""" + tid = self.token_ids.setdefault(token, len(self.token_ids)) + return tid | (token_type << 24) + + @staticmethod + def token_type(token): + """extract the token type from the token id passed""" + return token >> 24 + + def find_token(self, tid): + """Search for a tag with the given ID + + This is O(N) and is only intended for debugging + """ + tid &= 0xFFFFFF + if tid >= len(self.token_ids) or tid < 0: + raise ValueError("tag id %s out of range" % tid) + + for (token, token_id) in self.token_ids.items(): + if token_id == tid: + return token + assert False, "token dictionary is corrupt" + + def token_string(self, tid): + """create a string representation of a token + + This is O(N). + """ + templates = ["%s", "<%s>", "", "<%s/>"] + return templates[tid >> 24] % self.find_token(tid) + +class Page(object): + """Basic representation of a page. This consists of a reference to a + dictionary of tokens and an array of raw token ids + """ + + __slots__ = ('token_dict', 'page_tokens') + + def __init__(self, token_dict, page_tokens): + self.token_dict = token_dict + # use a numpy array becuase we can index/slice easily and efficiently + if not isinstance(page_tokens, ndarray): + page_tokens = array(page_tokens) + self.page_tokens = page_tokens + +class TemplatePage(Page): + __slots__ = ('annotations', 'id', 'ignored_regions', 'extra_required_attrs') + + def __init__(self, token_dict, page_tokens, annotations, template_id=None, \ + ignored_regions=None, extra_required=None): + Page.__init__(self, token_dict, page_tokens) + # ensure order is the same as start tag order in the original page + annotations = sorted(annotations, key=lambda x: x.end_index, reverse=True) + self.annotations = sorted(annotations, key=lambda x: x.start_index) + self.id = template_id + self.ignored_regions = ignored_regions or [] + self.extra_required_attrs = set(extra_required or []) + + def __str__(self): + summary = [] + for index, token in enumerate(self.page_tokens): + text = "%s: %s" % (index, self.token_dict.find_token(token)) + summary.append(text) + return "TemplatePage\n============\nTokens: (index, token)\n%s\nAnnotations: %s\n" % \ + ('\n'.join(summary), '\n'.join(map(str, self.annotations))) + +class ExtractionPage(Page): + """Parsed data belonging to a web page upon which we wish to perform + extraction. + """ + __slots__ = ('text', + 'token_start_indexes', # index in text of the start of a token + 'token_follow_indexes', # index in text of data following token + 'tag_attributes' # a map from token index to tag attributes + ) + + def __init__(self, text, token_dict, page_tokens, token_start_indexes, + token_follow_indexes, tag_attributes): + Page.__init__(self, token_dict, page_tokens) + self.text = text + self.token_start_indexes = token_start_indexes + self.token_follow_indexes = token_follow_indexes + self.tag_attributes = tag_attributes + + def token_html(self, token_index): + """The raw html for a page token at the given index in the page_tokens + list + """ + text_start = self.token_start_indexes[token_index] + text_end = self.token_follow_indexes[token_index] + return self.text[text_start:text_end] + + def html_between_tokens(self, start_token_index, end_token_index): + """The raw html between the tokens at the specified indexes in the + page_tokens list + + This assumes start_token_index <= end_token_index + """ + text_start = self.token_follow_indexes[start_token_index] + text_end = self.token_start_indexes[end_token_index] + return self.text[text_start:text_end] + + def text_between_tokens(self, start_token_index, end_token_index, + tag_replacement=u' '): + """The text between the the tokens at the specified indexes in the + page_tokens list. Tags are replaced by tag_replacement (default one space + character) + """ + return tag_replacement.join([self.text[ + self.token_follow_indexes[i]:self.token_start_indexes[i+1]] \ + for i in xrange(start_token_index, end_token_index)]) + + def tag_attribute(self, token_index, attribute): + """The value of a tag attribute. The tag is identified by its + corresponding token index. + + If the tag or attribute is not present, None is returned + """ + return self.tag_attributes.get(token_index, {}).get(attribute) + + def __str__(self): + summary = [] + for (token, start, follow) in zip(self.page_tokens, self.token_start_indexes, + self.token_follow_indexes): + text = "%s %s-%s (%s)" % (self.token_dict.find_token(token), start, follow, + self.text[start:follow]) + summary.append(text) + return "ExtractionPage\n==============\nTokens: %s\n\nRaw text: %s\n\n" \ + "Tag attributes: %s\n" % ('\n'.join(summary), self.text, + self.tag_attributes) + +class AnnotationText(object): + __slots__ = ('start_text', 'follow_text') + + def __init__(self, start_text=None, follow_text=None): + self.start_text = start_text + self.follow_text = follow_text + + def __str__(self): + return "AnnotationText(%s..%s)" % \ + (repr(self.start_text), repr(self.follow_text)) + +class AnnotationTag(object): + """A tag that annotates part of the document + + It has the following properties: + start_index - index of the token for the opening tag + end_index - index of the token for the closing tag + surrounds_attribute - the attribute name surrounded by this tag + tag_attributes - list of (tag attribute, extracted attribute) tuples + for each item to be extracted from a tag attribute + annotation_text - text prefix and suffix for the attribute to be extracted + match_common_prefix - use this annotation for calculating across-template prefixes + """ + __slots__ = ('surrounds_attribute', 'start_index', 'end_index', + 'tag_attributes', 'annotation_text', 'variant_id', + 'surrounds_variant','match_common_prefix') + + def __init__(self, start_index, end_index, surrounds_attribute=None, + annotation_text=None, tag_attributes=None, variant_id=None, + surrounds_variant=None, match_common_prefix=False): + self.start_index = start_index + self.end_index = end_index + self.surrounds_attribute = surrounds_attribute + self.annotation_text = annotation_text + self.tag_attributes = tag_attributes or [] + self.variant_id = variant_id + self.surrounds_variant = surrounds_variant + self.match_common_prefix = match_common_prefix + + def __str__(self): + return "AnnotationTag(%s)" % ", ".join( + ["%s=%s" % (s, getattr(self, s)) \ + for s in self.__slots__ if getattr(self, s)]) + + def __repr__(self): + return str(self) + +class LabelledRegion(object): + __slots__ = ('start_index', 'end_index') + + def __init__(self, start, end): + self.start_index = start + self.end_index = end + + def __str__(self): + return "LabelledRegion (%s, %s)" % (self.start_index, self.end_index) + + def __repr__(self): + return str(self) + diff --git a/scrapy/contrib/ibl/extraction/pageparsing.py b/scrapy/contrib/ibl/extraction/pageparsing.py new file mode 100644 index 000000000..486862ae5 --- /dev/null +++ b/scrapy/contrib/ibl/extraction/pageparsing.py @@ -0,0 +1,318 @@ +""" +Page parsing + +Parsing of web pages for extraction task. +""" +from collections import defaultdict +from numpy import array + +from scrapy.utils.py26 import json + +from scrapy.contrib.ibl.htmlpage import HtmlTagType, HtmlTag, HtmlPage +from scrapy.contrib.ibl.extraction.pageobjects import (AnnotationTag, + TemplatePage, ExtractionPage, AnnotationText, TokenDict) + +def parse_strings(template_html, extraction_html): + """Create a template and extraction page from raw strings + + this is useful for testing purposes + """ + t = TokenDict() + template_page = HtmlPage(body=template_html) + extraction_page = HtmlPage(body=extraction_html) + return (parse_template(t, template_page), + parse_extraction_page(t, extraction_page)) + +def parse_template(token_dict, template_html): + """Create an TemplatePage object by parsing the annotated html""" + parser = TemplatePageParser(token_dict) + parser.feed(template_html) + return parser.to_template() + +def parse_extraction_page(token_dict, page_html): + """Create an ExtractionPage object by parsing the html""" + parser = ExtractionPageParser(token_dict) + parser.feed(page_html) + return parser.to_extraction_page() + +class InstanceLearningParser(object): + """Base parser for instance based learning algorithm + + This does not require correct HTML and the parsing method should not alter + the original tag order. It is important that parsing results do not vary. + """ + def __init__(self, token_dict): + self.token_dict = token_dict + self.token_list = [] + + def _add_token(self, token, token_type, start, end): + tid = self.token_dict.tokenid(token, token_type) + self.token_list.append(tid) + + def feed(self, html_page): + self.html_page = html_page + self.previous_element_class = None + for data in html_page.parsed_body: + if isinstance(data, HtmlTag): + self._add_token(data.tag, data.tag_type, data.start, data.end) + self.handle_tag(data) + else: + self.handle_data(data) + self.previous_element_class = data.__class__ + + def handle_data(self, html_data_fragment): + pass + + def handle_tag(self, html_tag): + pass + +_END_UNPAIREDTAG_TAGS = ["form", "div", "p", "table", "tr", "td"] + +class TemplatePageParser(InstanceLearningParser): + """Template parsing for instance based learning algorithm""" + + def __init__(self, token_dict): + InstanceLearningParser.__init__(self, token_dict) + self.annotations = [] + self.ignored_regions = [] + self.extra_required_attrs = [] + self.ignored_tag_stacks = defaultdict(list) + # tag names that have not been completed + self.labelled_tag_stacks = defaultdict(list) + self.replacement_stacks = defaultdict(list) + self.unpairedtag_stack = [] + self.variant_stack = [] + self.prev_data = None + self.last_text_region = None + self.next_tag_index = 0 + + def handle_tag(self, html_tag): + if self.last_text_region: + self._process_text('') + + if html_tag.tag_type == HtmlTagType.OPEN_TAG: + self._handle_open_tag(html_tag) + elif html_tag.tag_type == HtmlTagType.CLOSE_TAG: + self._handle_close_tag(html_tag) + else: + # the tag is not paired, it can contain only attribute annotations + self._handle_unpaired_tag(html_tag) + + @staticmethod + def _read_template_annotation(html_tag): + template_attr = html_tag.attributes.get('data-scrapy-annotate') + if template_attr is None: + return None + unescaped = template_attr.replace('"', '"') + return json.loads(unescaped) + + @staticmethod + def _read_bool_template_attribute(html_tag, attribute): + return html_tag.attributes.get("data-scrapy-" + attribute) == "true" + + def _close_unpaired_tag(self): + self.unpairedtag_stack[0].end_index = self.next_tag_index + self.unpairedtag_stack = [] + + def _handle_unpaired_tag(self, html_tag): + if self._read_bool_template_attribute(html_tag, "ignore") and html_tag.tag == "img": + self.ignored_regions.append((self.next_tag_index, self.next_tag_index + 1)) + elif self._read_bool_template_attribute(html_tag, "ignore-beneath") and html_tag.tag == "img": + self.ignored_regions.append((self.next_tag_index, None)) + jannotation = self._read_template_annotation(html_tag) + if jannotation: + if self.unpairedtag_stack: + self._close_unpaired_tag() + + annotation = AnnotationTag(self.next_tag_index, self.next_tag_index + 1) + attribute_annotations = jannotation.get('annotations', {}).items() + for extract_attribute, tag_value in attribute_annotations: + if extract_attribute == 'content': + annotation.surrounds_attribute = tag_value + self.unpairedtag_stack.append(annotation) + else: + annotation.tag_attributes.append((extract_attribute, tag_value)) + self.annotations.append(annotation) + if jannotation.get('common_prefix', False): + annotation.match_common_prefix = True + + self.extra_required_attrs.extend(jannotation.get('required', [])) + + self.next_tag_index += 1 + + def _handle_open_tag(self, html_tag): + if self._read_bool_template_attribute(html_tag, "ignore"): + if html_tag.tag == "img": + self.ignored_regions.append((self.next_tag_index, self.next_tag_index + 1)) + else: + self.ignored_regions.append((self.next_tag_index, None)) + self.ignored_tag_stacks[html_tag.tag].append(html_tag) + + elif self.ignored_tag_stacks.get(html_tag.tag): + self.ignored_tag_stacks[html_tag.tag].append(None) + if self._read_bool_template_attribute(html_tag, "ignore-beneath"): + self.ignored_regions.append((self.next_tag_index, None)) + + replacement = html_tag.attributes.pop("data-scrapy-replacement", None) + if replacement: + self.token_list.pop() + self._add_token(replacement, html_tag.tag_type, html_tag.start, html_tag.end) + self.replacement_stacks[html_tag.tag].append(replacement) + elif html_tag.tag in self.replacement_stacks: + self.replacement_stacks[html_tag.tag].append(None) + + if self.unpairedtag_stack: + if html_tag.tag in _END_UNPAIREDTAG_TAGS: + self._close_unpaired_tag() + else: + self.unpairedtag_stack.append(html_tag.tag) + + # can't be a p inside another p. Also, an open p element closes + # a previous open p element. + if html_tag.tag == "p" and html_tag.tag in self.labelled_tag_stacks: + annotation = self.labelled_tag_stacks.pop(html_tag.tag)[0] + annotation.end_index = self.next_tag_index + self.annotations.append(annotation) + + jannotation = self._read_template_annotation(html_tag) + if not jannotation: + if html_tag.tag in self.labelled_tag_stacks: + # add this tag to the stack to match correct end tag + self.labelled_tag_stacks[html_tag.tag].append(None) + self.next_tag_index += 1 + return + + annotation = AnnotationTag(self.next_tag_index, None) + if jannotation.get('generated', False): + self.token_list.pop() + annotation.start_index -= 1 + if self.previous_element_class == HtmlTag: + annotation.annotation_text = AnnotationText('') + else: + annotation.annotation_text = AnnotationText(self.prev_data) + if self._read_bool_template_attribute(html_tag, "ignore") \ + or self._read_bool_template_attribute(html_tag, "ignore-beneath"): + ignored = self.ignored_regions.pop() + self.ignored_regions.append((ignored[0]-1, ignored[1])) + + if jannotation.get('common_prefix', False): + annotation.match_common_prefix = True + + self.extra_required_attrs.extend(jannotation.get('required', [])) + + variant_id = jannotation.get('variant', 0) + if variant_id > 0: + self.variant_stack.append(variant_id) + annotation.surrounds_variant = variant_id + attribute_annotations = jannotation.get('annotations', {}).items() + for extract_attribute, tag_value in attribute_annotations: + if extract_attribute == 'content': + annotation.surrounds_attribute = tag_value + else: + annotation.tag_attributes.append((extract_attribute, tag_value)) + + if annotation.annotation_text is None: + self.next_tag_index += 1 + if self.variant_stack: + variant_id = self.variant_stack[-1] + if variant_id == '0': + variant_id = None + annotation.variant_id = variant_id + + # look for a closing tag if the content is important + if annotation.surrounds_attribute or annotation.surrounds_variant: + self.labelled_tag_stacks[html_tag.tag].append(annotation) + else: + annotation.end_index = annotation.start_index + 1 + self.annotations.append(annotation) + + def _handle_close_tag(self, html_tag): + + if self.unpairedtag_stack: + if html_tag.tag == self.unpairedtag_stack[-1]: + self.unpairedtag_stack.pop() + else: + self._close_unpaired_tag() + ignored_tags = self.ignored_tag_stacks.get(html_tag.tag) + if ignored_tags is not None: + tag = ignored_tags.pop() + if isinstance(tag, HtmlTag): + for i in range(-1, -len(self.ignored_regions) - 1, -1): + if self.ignored_regions[i][1] is None: + self.ignored_regions[i] = (self.ignored_regions[i][0], self.next_tag_index) + break + if len(ignored_tags) == 0: + del self.ignored_tag_stacks[html_tag.tag] + + if html_tag.tag in self.replacement_stacks: + replacement = self.replacement_stacks[html_tag.tag].pop() + if replacement: + self.token_list.pop() + self._add_token(replacement, html_tag.tag_type, html_tag.start, html_tag.end) + if len(self.replacement_stacks[html_tag.tag]) == 0: + del self.replacement_stacks[html_tag.tag] + + labelled_tags = self.labelled_tag_stacks.get(html_tag.tag) + if labelled_tags is None: + self.next_tag_index += 1 + return + annotation = labelled_tags.pop() + if annotation is None: + self.next_tag_index += 1 + else: + annotation.end_index = self.next_tag_index + self.annotations.append(annotation) + if annotation.annotation_text is not None: + self.token_list.pop() + self.last_text_region = annotation + else: + self.next_tag_index += 1 + if len(labelled_tags) == 0: + del self.labelled_tag_stacks[html_tag.tag] + if annotation.surrounds_variant and self.variant_stack: + prev = self.variant_stack.pop() + if prev != annotation.surrounds_variant: + raise ValueError("unbalanced variant annotation tags") + + def handle_data(self, html_data_fragment): + fragment_text = self.html_page.fragment_data(html_data_fragment) + self._process_text(fragment_text) + + def _process_text(self, text): + if self.last_text_region is not None: + self.last_text_region.annotation_text.follow_text = text + self.last_text_region = None + self.prev_data = text + + def to_template(self): + """create a TemplatePage from the data fed to this parser""" + return TemplatePage(self.token_dict, self.token_list, self.annotations, + self.html_page.page_id, self.ignored_regions, self.extra_required_attrs) + +class ExtractionPageParser(InstanceLearningParser): + """Parse an HTML page for extraction using the instance based learning + algorithm + + This needs to extract the tokens in a similar way to LabelledPageParser, + it needs to also maintain a mapping from token index to the original content + so that once regions are identified, the original content can be extracted. + """ + def __init__(self, token_dict): + InstanceLearningParser.__init__(self, token_dict) + self.page_data = [] + self.token_start_index = [] + self.token_follow_index = [] + self.tag_attrs = {} + + def _add_token(self, token, token_type, start, end): + InstanceLearningParser._add_token(self, token, token_type, start, end) + self.token_start_index.append(start) + self.token_follow_index.append(end) + + def handle_tag(self, html_tag): + if html_tag.attributes: + self.tag_attrs[len(self.token_list) - 1] = html_tag.attributes + + def to_extraction_page(self): + return ExtractionPage(self.html_page.body, self.token_dict, array(self.token_list), + self.token_start_index, self.token_follow_index, self.tag_attrs) diff --git a/scrapy/contrib/ibl/extraction/regionextract.py b/scrapy/contrib/ibl/extraction/regionextract.py new file mode 100644 index 000000000..9b5aab166 --- /dev/null +++ b/scrapy/contrib/ibl/extraction/regionextract.py @@ -0,0 +1,651 @@ +""" +Region Extract + +Custom extraction for regions in a document +""" +import operator +import copy +import pprint +import cStringIO +from itertools import groupby + +import nltk +from numpy import array + +from scrapy.contrib.ibl.descriptor import FieldDescriptor +from scrapy.contrib.ibl.extraction.similarity import (similar_region, + longest_unique_subsequence, common_prefix) +from scrapy.contrib.ibl.extraction.pageobjects import AnnotationTag, LabelledRegion + +def build_extraction_tree(template, type_descriptor, trace=True): + """Build a tree of region extractors corresponding to the + template + """ + attribute_map = type_descriptor.attribute_map if type_descriptor else None + extractors = BasicTypeExtractor.create(template.annotations, attribute_map) + if trace: + extractors = TraceExtractor.apply(template, extractors) + for cls in (RepeatedDataExtractor, AdjacentVariantExtractor, RepeatedDataExtractor, + RecordExtractor): + extractors = cls.apply(template, extractors) + if trace: + extractors = TraceExtractor.apply(template, extractors) + + return TemplatePageExtractor(template, extractors) + +_ID = lambda x: x +_DEFAULT_DESCRIPTOR = FieldDescriptor('none', None) + +def _labelled(obj): + """ + Returns labelled element of the object (extractor or labelled region) + """ + if hasattr(obj, "annotation"): + return obj.annotation + return obj + +def _compose(f, g): + """given unary functions f and g, return a function that computes f(g(x)) + """ + def _exec(x): + ret = g(x) + return f(ret) if ret is not None else None + return _exec + +class BasicTypeExtractor(object): + """The BasicTypeExtractor extracts single attributes corresponding to + annotations. + + For example: + >>> from scrapy.contrib.ibl.extraction.pageparsing import parse_strings + >>> template, page = parse_strings( \ + u'

x

', u'

a name

') + >>> ex = BasicTypeExtractor(template.annotations[0]) + >>> ex.extract(page, 0, 1, None) + [(u'name', u' a name')] + + It supports attribute descriptors + >>> descriptor = FieldDescriptor('name', None, lambda x: x.strip()) + >>> ex = BasicTypeExtractor(template.annotations[0], {'name': descriptor}) + >>> ex.extract(page, 0, 1, None) + [(u'name', u'a name')] + + It supports ignoring regions + >>> template, page = parse_strings(\ + u'
x xx
',\ + u'
a name id-9
') + >>> ex = BasicTypeExtractor(template.annotations[0]) + >>> ex.extract(page, 0, 3, [LabelledRegion(*(1,2))]) + [(u'name', u'a name')] + """ + + def __init__(self, annotation, attribute_descriptors=None): + self.annotation = annotation + if attribute_descriptors is None: + attribute_descriptors = {} + + if annotation.surrounds_attribute: + descriptor = attribute_descriptors.get(annotation.surrounds_attribute) + if descriptor: + self.content_validate = descriptor.extractor + self.allow_markup = descriptor.allow_markup + else: + self.content_validate = _ID + self.allow_markup = False + self.extract = self._extract_content + + if annotation.tag_attributes: + self.tag_data = [] + for (tag_attr, extraction_attr) in annotation.tag_attributes: + descriptor = attribute_descriptors.get(extraction_attr) + extractf = descriptor.extractor if descriptor else _ID + self.tag_data.append((extractf, tag_attr, extraction_attr)) + + self.extract = self._extract_both if \ + annotation.surrounds_attribute else self._extract_attribute + + def _extract_both(self, page, start_index, end_index, ignored_regions=None): + return self._extract_content(page, start_index, end_index, ignored_regions) + \ + self._extract_attribute(page, start_index, end_index, ignored_regions) + + def _extract_content(self, extraction_page, start_index, end_index, ignored_regions=None): + # we might want to add opening/closing ul/ol/table if we have the + # middle of a region. This would require support in the scrapy + # cleansing. + complete_data = "" + start = start_index + end = ignored_regions[0].start_index if ignored_regions else end_index + while start is not None: + if self.allow_markup: + data = extraction_page.html_between_tokens(start, end) + else: + data = extraction_page.text_between_tokens(start, end) + complete_data += data + if ignored_regions: + start = ignored_regions[0].end_index + ignored_regions.pop(0) + end = ignored_regions[0].start_index if ignored_regions else end_index + else: + start = None + complete_data = self.content_validate(complete_data) + return [(self.annotation.surrounds_attribute, complete_data)] if complete_data else [] + + def _extract_attribute(self, extraction_page, start_index, end_index, ignored_regions=None): + data = [] + for (f, ta, ea) in self.tag_data: + tag_value = extraction_page.tag_attribute(start_index, ta) + if tag_value: + extracted = f(tag_value) + if extracted is not None: + data.append((ea, extracted)) + return data + + @classmethod + def create(cls, annotations, attribute_descriptors=None): + """Create a list of basic extractors from the given annotations + and attribute descriptors + """ + if attribute_descriptors is None: + attribute_descriptors = {} + return [cls._create_basic_extractor(annotation, attribute_descriptors) \ + for annotation in annotations \ + if annotation.surrounds_attribute or annotation.tag_attributes] + + @staticmethod + def _create_basic_extractor(annotation, attribute_descriptors): + """Create a basic type extractor for the annotation""" + text_region = annotation.annotation_text + if text_region is not None: + if annotation.match_common_prefix: + region_extract = TextPrefixRegionDataExtractor(text_region.start_text).extract + else: + region_extract = TextRegionDataExtractor(text_region.start_text, + text_region.follow_text).extract + # copy attribute_descriptors and add the text extractor + descriptor_copy = dict(attribute_descriptors) + attr_descr = descriptor_copy.get(annotation.surrounds_attribute, + _DEFAULT_DESCRIPTOR) + attr_descr = copy.copy(attr_descr) + attr_descr.extractor = _compose(attr_descr.extractor, region_extract) + descriptor_copy[annotation.surrounds_attribute] = attr_descr + attribute_descriptors = descriptor_copy + return BasicTypeExtractor(annotation, attribute_descriptors) + + def extracted_item(self): + """key used to identify the item extracted""" + return (self.annotation.surrounds_attribute, self.annotation.tag_attributes) + + def __repr__(self): + return str(self) + + def __str__(self): + messages = ['BasicTypeExtractor('] + if self.annotation.surrounds_attribute: + messages += [self.annotation.surrounds_attribute, ': ', + 'html content' if self.allow_markup else 'text content', + ] + if self.content_validate != _ID: + messages += [', extracted with \'', + self.content_validate.__name__, '\''] + + if self.annotation.tag_attributes: + if self.annotation.surrounds_attribute: + messages.append(';') + for (f, ta, ea) in self.tag_data: + messages += [ea, ': tag attribute "', ta, '"'] + if f != _ID: + messages += [', validated by ', str(f)] + messages.append(", template[%s:%s])" % \ + (self.annotation.start_index, self.annotation.end_index)) + return ''.join(messages) + +class RepeatedDataExtractor(object): + """Data extractor for handling repeated data""" + + def __init__(self, prefix, suffix, extractors): + self.prefix = array(prefix) + self.suffix = array(suffix) + self.extractor = copy.copy(extractors[0]) + self.annotation = copy.copy(self.extractor.annotation) + self.annotation.end_index = extractors[-1].annotation.end_index + + def extract(self, page, start_index, end_index, ignored_regions): + """repeatedly find regions bounded by the repeated + prefix and suffix and extract them + """ + prefixlen = len(self.prefix) + suffixlen = len(self.suffix) + index = max(0, start_index - prefixlen) + max_index = min(len(page.page_tokens) - suffixlen, end_index + len(self.suffix)) + max_start_index = max_index - prefixlen + extracted = [] + while index <= max_start_index: + prefix_end = index + prefixlen + if (page.page_tokens[index:prefix_end] == self.prefix).all(): + for peek in xrange(prefix_end, max_index): + if (page.page_tokens[peek:peek + suffixlen] \ + == self.suffix).all(): + extracted += self.extractor.extract(page, + prefix_end - 1, peek, ignored_regions) + index = max(peek, index + 1) + break + else: + break + else: + index += 1 + return extracted + + @staticmethod + def apply(template, extractors): + tokens = template.page_tokens + output_extractors = [] + group_key = lambda x: x.extracted_item() + for extr_key, extraction_group in groupby(extractors, group_key): + extraction_group = list(extraction_group) + if extr_key is None or len(extraction_group) == 1: + output_extractors += extraction_group + continue + + separating_tokens = [ \ + tokens[x.annotation.end_index:y.annotation.start_index+1] \ + for (x, y) in zip(extraction_group[:-1], extraction_group[1:])] + + # calculate the common prefix + group_start = extraction_group[0].annotation.start_index + prefix_start = max(0, group_start - len(separating_tokens[0])) + first_prefix = tokens[prefix_start:group_start+1] + prefixes = [first_prefix] + separating_tokens + prefix_pattern = list(reversed( + common_prefix(*map(reversed, prefixes)))) + + # calculate the common suffix + group_end = extraction_group[-1].annotation.end_index + last_suffix = tokens[group_end:group_end + \ + len(separating_tokens[-1])] + suffixes = separating_tokens + [last_suffix] + suffix_pattern = common_prefix(*suffixes) + + # create a repeated data extractor, if there is a suitable + # prefix and suffix. (TODO: tune this heuristic) + matchlen = len(prefix_pattern) + len(suffix_pattern) + if matchlen >= len(separating_tokens): + group_extractor = RepeatedDataExtractor(prefix_pattern, + suffix_pattern, extraction_group) + output_extractors.append(group_extractor) + else: + output_extractors += extraction_group + return output_extractors + + def extracted_item(self): + """key used to identify the item extracted""" + return self.extractor.extracted_item() + + def __repr__(self): + return "Repeat(%r)" % self.extractor + + def __str__(self): + return "Repeat(%s)" % self.extractor + +class TransposedDataExtractor(object): + """ """ + pass + +_namef = operator.itemgetter(0) +_valuef = operator.itemgetter(1) +def _attrs2dict(attributes): + """convert a list of attributes (name, value) tuples + into a dict of lists. + + For example: + >>> l = [('name', 'sofa'), ('colour', 'red'), ('colour', 'green')] + >>> _attrs2dict(l) == {'name': ['sofa'], 'colour': ['red', 'green']} + True + """ + grouped_data = groupby(sorted(attributes, key=_namef), _namef) + return dict((name, map(_valuef, data)) for (name, data) in grouped_data) + +class RecordExtractor(object): + """The RecordExtractor will extract records given annotations. + + It looks for a similar region in the target document, using the ibl + similarity algorithm. The annotations are partitioned by the first similar + region found and searched recursively. + + Records are represented as dicts mapping attribute names to lists + containing their values. + + For example: + >>> from scrapy.contrib.ibl.extraction.pageparsing import parse_strings + >>> template, page = parse_strings( \ + u'

x

' + \ + u'

y

', \ + u'

name

description

') + >>> basic_extractors = map(BasicTypeExtractor, template.annotations) + >>> ex = RecordExtractor.apply(template, basic_extractors)[0] + >>> ex.extract(page) + [{u'description': [u'description'], u'name': [u'name']}] + """ + + def __init__(self, extractors, template_tokens): + """Construct a RecordExtractor for the given annotations and their + corresponding region extractors + """ + self.extractors = extractors + self.template_tokens = template_tokens + self.template_ignored_regions = [] + start_index = min(e.annotation.start_index for e in extractors) + end_index = max(e.annotation.end_index for e in extractors) + self.annotation = AnnotationTag(start_index, end_index) + + def extract(self, page, start_index=0, end_index=None, ignored_regions=None): + """extract data from an extraction page + + The region in the page to be extracted from may be specified using + start_index and end_index + """ + ignored_regions = [LabelledRegion(*i) for i in (ignored_regions or [])] + region_elements = sorted(self.extractors + ignored_regions, key=lambda x: _labelled(x).start_index) + _, _, attributes = self._doextract(page, region_elements, start_index, + end_index) + # collect variant data, maintaining the order of variants + variant_ids = []; variants = {}; items = [] + for k, v in attributes: + if isinstance(k, int): + if k in variants: + variants[k] += v + else: + variant_ids.append(k) + variants[k] = v + else: + items.append((k, v)) + + variant_records = [('variants', _attrs2dict(variants[vid])) \ + for vid in variant_ids] + items += variant_records + return [_attrs2dict(items)] + + def _doextract(self, page, region_elements, start_index, end_index, nested_regions=None, ignored_regions=None): + """Carry out extraction of records using the given annotations + in the page tokens bounded by start_index and end_index + """ + # reorder extractors leaving nested ones for the end and separating + # ignore regions + nested_regions = nested_regions or [] + ignored_regions = ignored_regions or [] + first_region, following_regions = region_elements[0], region_elements[1:] + while following_regions and _labelled(following_regions[0]).start_index \ + < _labelled(first_region).end_index: + region = following_regions.pop(0) + labelled = _labelled(region) + if isinstance(labelled, AnnotationTag) or (nested_regions and \ + _labelled(nested_regions[-1]).start_index < labelled.start_index \ + < _labelled(nested_regions[-1]).end_index): + nested_regions.append(region) + else: + ignored_regions.append(region) + extracted_data = [] + # end_index is inclusive, but similar_region treats it as exclusive + end_region = None if end_index is None else end_index + 1 + labelled = _labelled(first_region) + score, pindex, sindex = \ + similar_region(page.page_tokens, self.template_tokens, + labelled, start_index, end_region) + if score > 0: + if isinstance(labelled, AnnotationTag): + similar_ignored_regions = [] + start = pindex + for i in ignored_regions: + s, p, e = similar_region(page.page_tokens, self.template_tokens, \ + i, start, sindex) + if s > 0: + similar_ignored_regions.append(LabelledRegion(*(p, e))) + start = e or start + extracted_data = first_region.extract(page, pindex, sindex, similar_ignored_regions) + if extracted_data: + if first_region.annotation.variant_id: + extracted_data = [(first_region.annotation.variant_id, extracted_data)] + + if nested_regions: + _, _, nested_data = self._doextract(page, nested_regions, pindex, sindex) + extracted_data += nested_data + if following_regions: + _, _, following_data = self._doextract(page, following_regions, sindex or start_index, end_region) + extracted_data += following_data + + elif following_regions: + end_index, _, following_data = self._doextract(page, following_regions, start_index, end_region) + if end_index is not None: + pindex, sindex, extracted_data = self._doextract(page, [first_region], start_index, end_index - 1, nested_regions, ignored_regions) + extracted_data += following_data + elif nested_regions: + _, _, nested_data = self._doextract(page, nested_regions, start_index, end_region) + extracted_data += nested_data + return pindex, sindex, extracted_data + + @classmethod + def apply(cls, template, extractors): + return [cls(extractors, template.page_tokens)] + + def extracted_item(self): + return [self.__class__.__name__] + \ + sorted(e.extracted_item() for e in self.extractors) + + def __repr__(self): + return str(self) + + def __str__(self): + stream = cStringIO.StringIO() + pprint.pprint(self.extractors, stream) + stream.seek(0) + template_data = stream.read() + if template_data: + return "%s[\n%s\n]" % (self.__class__.__name__, template_data) + return "%s[none]" % (self.__class__.__name__) + +class AdjacentVariantExtractor(RecordExtractor): + """Extractor for variants + + This simply extends the RecordExtractor to output data in a "variants" + attribute. + + The "apply" method will only apply to variants whose items are all adjacent and + it will appear as one record so that it can be handled by the RepeatedDataExtractor. + """ + + def extract(self, page, start_index=0, end_index=None, ignored_regions=None): + records = RecordExtractor.extract(self, page, start_index, end_index, ignored_regions) + return [('variants', r['variants'][0]) for r in records if r] + + @classmethod + def apply(cls, template, extractors): + adjacent_variants = set([]) + variantf = lambda x: x.annotation.variant_id + for vid, egroup in groupby(extractors, variantf): + if not vid: + continue + if vid in adjacent_variants: + adjacent_variants.remove(vid) + elif len(list(egroup)) > 1: + adjacent_variants.add(vid) + + new_extractors = [] + for variant, group_seq in groupby(extractors, variantf): + group_seq = list(group_seq) + if variant in adjacent_variants: + record_extractor = AdjacentVariantExtractor(group_seq, template.page_tokens) + new_extractors.append(record_extractor) + else: + new_extractors += group_seq + return new_extractors + + def __repr__(self): + return str(self) + +class TraceExtractor(object): + """Extractor that wraps other extractors and prints an execution + trace of the extraction process to aid debugging + """ + + def __init__(self, traced, template): + self.traced = traced + self.annotation = traced.annotation + tstart = traced.annotation.start_index + tend = traced.annotation.end_index + self.tprefix = " ".join([template.token_dict.token_string(t) + for t in template.page_tokens[tstart-4:tstart+1]]) + self.tsuffix = " ".join([template.token_dict.token_string(t) + for t in template.page_tokens[tend:tend+5]]) + + def summarize_trace(self, page, start, end, ret): + text_start = page.token_follow_indexes[start] + text_end = page.token_start_indexes[end or -1] + page_snippet = "(...%s)%s(%s...)" % ( + page.text[text_start-50:text_start].replace('\n', ' '), + page.text[text_start:text_end], + page.text[text_end:text_end+50].replace('\n', ' ')) + pre_summary = "\nstart %s page[%s:%s]\n" % (self.traced.__class__.__name__, start, end) + post_summary = """ +%s page[%s:%s] + +html +%s + +annotation +...%s +%s +%s... + +extracted +%s + """ % (self.traced.__class__.__name__, start, end, page_snippet, + self.tprefix, self.annotation, self.tsuffix, [r for r in ret if 'trace' not in r]) + return pre_summary, post_summary + + def extract(self, page, start, end, ignored_regions): + ret = self.traced.extract(page, start, end, ignored_regions) + if not ret: + return [] + + # handle records by inserting a trace and combining with variant traces + if len(ret) == 1 and isinstance(ret[0], dict): + item = ret[0] + trace = item.pop('trace', []) + variants = item.get('variants', ()) + for variant in variants: + trace += variant.pop('trace', []) + pre_summary, post_summary = self.summarize_trace(page, start, end, ret) + item['trace'] = [pre_summary] + trace + [post_summary] + return ret + + pre_summary, post_summary = self.summarize_trace(page, start, end, ret) + return [('trace', pre_summary)] + ret + [('trace', post_summary)] + + @staticmethod + def apply(template, extractors): + output = [] + for extractor in extractors: + if not isinstance(extractor, TraceExtractor): + extractor = TraceExtractor(extractor, template) + output.append(extractor) + return output + + def extracted_item(self): + return self.traced.extracted_item() + + def __repr__(self): + return "Trace(%s)" % repr(self.traced) + +class TemplatePageExtractor(object): + """Top level extractor for a template page""" + + def __init__(self, template, extractors): + # fixme: handle multiple items per page + self.extractor = extractors[0] + self.template = template + + def extract(self, page, start_index=0, end_index=None): + return self.extractor.extract(page, start_index, end_index, self.template.ignored_regions) + + def __repr__(self): + return repr(self.extractor) + + def __str__(self): + return str(self.extractor) + +_tokenize = nltk.tokenize.WordPunctTokenizer().tokenize + +class TextRegionDataExtractor(object): + """Data Extractor for extracting text fragments from within a larger + body of text. It extracts based on the longest unique prefix and suffix. + + for example: + >>> extractor = TextRegionDataExtractor('designed by ', '.') + >>> extractor.extract("by Marc Newson.") + 'Marc Newson' + + Both prefix and suffix are optional: + >>> extractor = TextRegionDataExtractor('designed by ') + >>> extractor.extract("by Marc Newson.") + 'Marc Newson.' + >>> extractor = TextRegionDataExtractor(suffix='.') + >>> extractor.extract("by Marc Newson.") + 'by Marc Newson' + + It requires a minimum match of at least one word or punctuation character: + >>> extractor = TextRegionDataExtractor('designed by') + >>> extractor.extract("y Marc Newson.") is None + True + """ + def __init__(self, prefix=None, suffix=None): + self.prefix = (prefix or '')[::-1] + self.suffix = suffix or '' + self.minprefix = self.minmatch(self.prefix) + self.minsuffix = self.minmatch(self.suffix) + + @staticmethod + def minmatch(matchstring): + """the minimum number of characters that should match in order + to consider it a match for that string. + + This uses the last word of punctuation character + """ + tokens = _tokenize(matchstring or '') + return len(tokens[0]) if tokens else 0 + + def extract(self, text): + """attempt to extract a substring from the text""" + pref_index = 0 + if self.minprefix > 0: + rev_idx, plen = longest_unique_subsequence(text[::-1], self.prefix) + if plen < self.minprefix: + return None + pref_index = -rev_idx + if self.minsuffix == 0: + return text[pref_index:] + sidx, slen = longest_unique_subsequence(text[pref_index:], self.suffix) + if slen < self.minsuffix: + return None + return text[pref_index:pref_index + sidx] + +class TextPrefixRegionDataExtractor(object): + """ + Data extractor for extracting text fragment from within a + larger body of text, based on a fixed prefix. + >>> extractor = TextPrefixRegionDataExtractor("£s;") + >>> extractor.extract("£s; 17.00") + ' 17.00' + >>> extractor.extract("€ 17.00") is None + True + >>> extractor.extract("$ 17.00") is None + True + >>> extractor.extract(" £s; 17.00 ") + ' 17.00 ' + """ + def __init__(self, prefix): + self.prefix = prefix + def extract(self, text): + text = text.lstrip() + # attempt to extract a substring from the text + if text.startswith(self.prefix): + return text.replace(self.prefix, '') + diff --git a/scrapy/contrib/ibl/extraction/similarity.py b/scrapy/contrib/ibl/extraction/similarity.py new file mode 100644 index 000000000..9bb8e24c3 --- /dev/null +++ b/scrapy/contrib/ibl/extraction/similarity.py @@ -0,0 +1,136 @@ +""" +Similarity calculation for Instance based extraction algorithm. +""" +from itertools import izip, count +from operator import itemgetter +from heapq import nlargest + +def common_prefix_length(a, b): + """Calculate the length of the common prefix in both sequences passed. + + For example, the common prefix in this example is [1, 3] + >>> common_prefix_length([1, 3, 4], [1, 3, 5, 1]) + 2 + + If there is no common prefix, 0 is returned + >>> common_prefix_length([1], []) + 0 + """ + i = -1 + for i, x, y in izip(count(), a, b): + if x != y: + return i + return i + 1 + +def common_prefix(*sequences): + """determine the common prefix of all sequences passed + + For example: + >>> common_prefix('abcdef', 'abc', 'abac') + ['a', 'b'] + """ + prefix = [] + for sample in izip(*sequences): + first = sample[0] + if all(x == first for x in sample[1:]): + prefix.append(first) + else: + break + return prefix + +def longest_unique_subsequence(to_search, subsequence, range_start=0, + range_end=None): + """Find the longest unique subsequence of items in a list or array. This + searches the to_search list or array looking for the longest overlapping + match with subsequence. If the largest match is unique (there is no other + match of equivalent length), the index and length of match is returned. If + there is no match, (None, None) is returned. + + Please see section 3.2 of Extracting Web Data Using Instance-Based + Learning by Yanhong Zhai and Bing Liu + + For example, the longest match occurs at index 2 and has length 3 + >>> to_search = [6, 3, 2, 4, 3, 2, 5] + >>> longest_unique_subsequence(to_search, [2, 4, 3]) + (2, 3) + + When there are two equally long subsequences, it does not generate a match + >>> longest_unique_subsequence(to_search, [3, 2]) + (None, None) + + range_start and range_end specify a range in which the match must begin + >>> longest_unique_subsequence(to_search, [3, 2], 3) + (4, 2) + >>> longest_unique_subsequence(to_search, [3, 2], 0, 2) + (1, 2) + """ + startval = subsequence[0] + if range_end is None: + range_end = len(to_search) + + # the comparison to startval ensures only matches of length >= 1 and + # reduces the number of calls to the common_length function + matches = ((i, common_prefix_length(to_search[i:], subsequence)) \ + for i in xrange(range_start, range_end) if startval == to_search[i]) + best2 = nlargest(2, matches, key=itemgetter(1)) + # if there is a single unique best match, return that + if len(best2) == 1 or len(best2) == 2 and best2[0][1] != best2[1][1]: + return best2[0] + return None, None + +def similar_region(extracted_tokens, template_tokens, labelled_region, + range_start=0, range_end=None): + """Given a labelled section in a template, identify a similar region + in the extracted tokens. + + The start and end index of the similar region in the extracted tokens + is returned. + + This will return a tuple containing: + (match score, start index, end index) + where match score is the sum of the length of the matching prefix and + suffix. If there is no unique match, (0, None, None) will be returned. + + start_index and end_index specify a range in which the match must begin + """ + data_length = len(extracted_tokens) + if range_end is None: + range_end = data_length + # calculate the prefix score by finding a longest subsequence in + # reverse order + reverse_prefix = template_tokens[labelled_region.start_index::-1] + reverse_tokens = extracted_tokens[::-1] + (rpi, pscore) = longest_unique_subsequence(reverse_tokens, reverse_prefix, + data_length - range_end, data_length - range_start) + + # None means nothing exracted. Index 0 means there cannot be a suffix. + if not rpi: + return 0, None, None + + # convert to an index from the start instead of in reverse + prefix_index = len(extracted_tokens) - rpi - 1 + + if labelled_region.end_index is None: + return pscore, prefix_index, None + + suffix = template_tokens[labelled_region.end_index:] + + # if it's not a paired tag, use the best match between prefix & suffix + if labelled_region.start_index == labelled_region.end_index: + (match_index, sscore) = longest_unique_subsequence(extracted_tokens, + suffix, prefix_index, range_end) + if match_index == prefix_index: + return (pscore + sscore, prefix_index, match_index) + elif pscore > sscore: + return pscore, prefix_index, prefix_index + elif sscore > pscore: + return sscore, match_index, match_index + return 0, None, None + + # calculate the suffix match on the tokens following the prefix. We could + # consider the whole page and require a good match. + (match_index, sscore) = longest_unique_subsequence(extracted_tokens, + suffix, prefix_index + 1, range_end) + if match_index is None: + return 0, None, None + return (pscore + sscore, prefix_index, match_index) diff --git a/scrapy/contrib/ibl/extractors.py b/scrapy/contrib/ibl/extractors.py new file mode 100644 index 000000000..9c460df10 --- /dev/null +++ b/scrapy/contrib/ibl/extractors.py @@ -0,0 +1,156 @@ +""" +Extractors for attributes +""" +import re +import urlparse + +from scrapy.utils.markup import remove_entities +from scrapy.utils.url import safe_url_string + +#FIXME: the use of "." needs to be localized +_NUMERIC_ENTITIES = re.compile("&#([0-9]+)(?:;|\s)", re.U) +_PRICE_NUMBER_RE = re.compile('(?:^|[^a-zA-Z0-9])(\d+(?:\.\d+)?)(?:$|[^a-zA-Z0-9])') +_NUMBER_RE = re.compile('(\d+(?:\.\d+)?)') + +_IMAGES = ( + 'mng', 'pct', 'bmp', 'gif', 'jpg', 'jpeg', 'png', 'pst', 'psp', 'tif', + 'tiff', 'ai', 'drw', 'dxf', 'eps', 'ps', 'svg', +) + +_IMAGES_TYPES = '|'.join(_IMAGES) +_CSS_IMAGERE = re.compile("background(?:-image)?\s*:\s*url\((.*?)\)", re.I) +_BASE_PATH_RE = "/?(?:[^/]+/)*(?:.+%s)" +_IMAGE_PATH_RE = re.compile(_BASE_PATH_RE % '\.(?:%s)' % _IMAGES_TYPES, re.I) +_GENERIC_PATH_RE = re.compile(_BASE_PATH_RE % '', re.I) + +def text(txt): + stripped = txt.strip() if txt else None + if stripped: + return stripped + +def contains_any_numbers(txt): + """text that must contain at least one number + >>> contains_any_numbers('foo') + >>> contains_any_numbers('$67 at 15% discount') + '$67 at 15% discount' + """ + if _NUMBER_RE.search(txt) is not None: + return txt + +def contains_prices(txt): + """text must contain a number that is not joined to text""" + if _PRICE_NUMBER_RE.findall(txt) is not None: + return txt + +def contains_numbers(txt, count=1): + """Must contain a certain amount of numbers + + >>> contains_numbers('foo', 2) + >>> contains_numbers('this 1 has 2 numbers', 2) + 'this 1 has 2 numbers' + """ + numbers = _NUMBER_RE.findall(txt) + if len(numbers) == count: + return txt + +def extract_number(txt): + """Extract a numeric value. + + This will fail if more than one numeric value is present. + + >>> extract_number(' 45.3') + '45.3' + >>> extract_number(' 45.3, 7') + + It will handle unescaped entities: + >>> extract_number(u'£129.99') + u'129.99' + """ + txt = _NUMERIC_ENTITIES.sub(lambda m: unichr(int(m.groups()[0])), txt) + numbers = _NUMBER_RE.findall(txt) + if len(numbers) == 1: + return numbers[0] + +def url(txt): + """convert text to a url + + this is quite conservative, since relative urls are supported + """ + txt = txt.strip("\t\r\n '\"") + if txt: + return txt + +def image_url(txt): + """convert text to a url + + this is quite conservative, since relative urls are supported + Example: + + >>> image_url('') + + >>> image_url(' ') + + >>> image_url(' \\n\\n ') + + >>> image_url('foo-bar.jpg') + ['foo-bar.jpg'] + >>> image_url('/images/main_logo12.gif') + ['/images/main_logo12.gif'] + >>> image_url("http://www.image.com/image.jpg") + ['http://www.image.com/image.jpg'] + >>> image_url("http://www.domain.com/path1/path2/path3/image.jpg") + ['http://www.domain.com/path1/path2/path3/image.jpg'] + >>> image_url("/path1/path2/path3/image.jpg") + ['/path1/path2/path3/image.jpg'] + >>> image_url("path1/path2/image.jpg") + ['path1/path2/image.jpg'] + >>> image_url("background-image : url(http://www.site.com/path1/path2/image.jpg)") + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url("background-image : url('http://www.site.com/path1/path2/image.jpg')") + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url('background-image : url("http://www.site.com/path1/path2/image.jpg")') + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url("background : url(http://www.site.com/path1/path2/image.jpg)") + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url("background : url('http://www.site.com/path1/path2/image.jpg')") + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url('background : url("http://www.site.com/path1/path2/image.jpg")') + ['http://www.site.com/path1/path2/image.jpg'] + >>> image_url('/getimage.php?image=totalgardens/outbbq2_400.jpg&type=prod&resizeto=350') + ['/getimage.php?image=totalgardens/outbbq2_400.jpg&type=prod&resizeto=350'] + >>> image_url('http://www.site.com/getimage.php?image=totalgardens/outbbq2_400.jpg&type=prod&resizeto=350') + ['http://www.site.com/getimage.php?image=totalgardens/outbbq2_400.jpg&type=prod&resizeto=350'] + >>> image_url('http://s7d4.scene7.com/is/image/Kohler/jaa03267?hei=425&wid=457&op_usm=2,1,2,1&qlt=80') + ['http://s7d4.scene7.com/is/image/Kohler/jaa03267?hei=425&wid=457&op_usm=2,1,2,1&qlt=80'] + >>> image_url('../image.aspx?thumb=true&boxSize=175&img=Unknoportrait[1].jpg') + ['../image.aspx?thumb=true&boxSize=175&img=Unknoportrait%5B1%5D.jpg'] + >>> image_url('http://www.sundancecatalog.com/mgen/catalog/test.ms?args=%2245932|MERIDIAN+PENDANT|.jpg%22&is=336,336,0xffffff') + ['http://www.sundancecatalog.com/mgen/catalog/test.ms?args=%2245932|MERIDIAN+PENDANT|.jpg%22&is=336,336,0xffffff'] + >>> image_url('http://www.site.com/image.php') + ['http://www.site.com/image.php'] + >>> image_url('background-image:URL(http://s7d5.scene7.com/is/image/wasserstrom/165133?wid=227&hei=227&defaultImage=noimage_wasserstrom)') + ['http://s7d5.scene7.com/is/image/wasserstrom/165133?wid=227&hei=227&defaultImage=noimage_wasserstrom'] + + """ + txt = url(txt) + imgurl = None + if txt: + # check if the text is style content + m = _CSS_IMAGERE.search(txt) + txt = m.groups()[0] if m else txt + parsed = urlparse.urlparse(txt) + path = None + m = _IMAGE_PATH_RE.search(parsed.path) + if m: + path = m.group() + elif parsed.query: + m = _GENERIC_PATH_RE.search(parsed.path) + if m: + path = m.group() + if path is not None: + parsed = list(parsed) + parsed[2] = path + imgurl = urlparse.urlunparse(parsed) + if not imgurl: + imgurl = txt + return [safe_url_string(remove_entities(url(imgurl)))] if imgurl else None diff --git a/scrapy/contrib/ibl/htmlpage.py b/scrapy/contrib/ibl/htmlpage.py new file mode 100644 index 000000000..daeef4053 --- /dev/null +++ b/scrapy/contrib/ibl/htmlpage.py @@ -0,0 +1,212 @@ +""" +htmlpage + +Container object for representing html pages in the IBL system. This +encapsulates page related information and prevents parsing multiple times. +""" +import re +import hashlib + +from scrapy.utils.python import str_to_unicode + +def create_page_from_jsonpage(jsonpage, body_key): + """Create an HtmlPage object from a dict object conforming to the schema + for a page + + `body_key` is the key where the body is stored and can be either 'body' + (original page with annotations - if any) or 'original_body' (original + page, always). Classification typically uses 'original_body' to avoid + confusing the classifier with annotated pages, while extraction uses 'body' + to pass the annotated pages. + """ + url = jsonpage['url'] + headers = jsonpage.get('headers') + body = str_to_unicode(jsonpage[body_key]) + page_id = jsonpage.get('page_id') + return HtmlPage(url, headers, body, page_id) + +class HtmlPage(object): + def __init__(self, url=None, headers=None, body=None, page_id=None): + assert isinstance(body, unicode), "unicode expected, got: %s" % type(body).__name__ + self.headers = headers or {} + self.body = body + self.url = url or u'' + if page_id is None and url: + self.page_id = hashlib.sha1(url).hexdigest() + else: + self.page_id = page_id + + + def _set_body(self, body): + self._body = body + self.parsed_body = list(parse_html(body)) + + body = property(lambda x: x._body, _set_body) + + def fragment_data(self, data_fragment): + return self.body[data_fragment.start:data_fragment.end] + +class HtmlTagType(object): + OPEN_TAG = 1 + CLOSE_TAG = 2 + UNPAIRED_TAG = 3 + +class HtmlDataFragment(object): + __slots__ = ('start', 'end') + + def __init__(self, start, end): + self.start = start + self.end = end + + def __str__(self): + return "" % (self.start, self.end) + + def __repr__(self): + return str(self) + +class HtmlTag(HtmlDataFragment): + __slots__ = ('tag_type', 'tag', 'attributes') + + def __init__(self, tag_type, tag, attributes, start, end): + HtmlDataFragment.__init__(self, start, end) + self.tag_type = tag_type + self.tag = tag + self.attributes = attributes + + def __str__(self): + return "" % (self.tag, ', '.join(sorted\ + (["%s: %s" % (k, repr(v)) for k, v in self.attributes.items()])), self.start, self.end) + + def __repr__(self): + return str(self) + +_ATTR = "((?:[^=/>\s]|/(?!>))+)(?:\s*=(?:\s*\"(.*?)\"|\s*'(.*?)'|([^>\s]+))?)?" +_TAG = "<(\/?)(\w+(?::\w+)?)((?:\s+" + _ATTR + ")+\s*|\s*)(\/?)>" +_DOCTYPE = r"" + +_ATTR_REGEXP = re.compile(_ATTR, re.I | re.DOTALL) +_HTML_REGEXP = re.compile(_TAG, re.I | re.DOTALL) +_DOCTYPE_REGEXP = re.compile("(?:%s)" % _DOCTYPE) +_COMMENT_RE = re.compile("()", re.DOTALL) +_SCRIPT_RE = re.compile("().*?()", re.DOTALL | re.I) + +def parse_html(text): + """Higher level html parser. Calls lower level parsers and joins sucesive + HtmlDataFragment elements in a single one. + """ + script_layer = lambda x: _parse_clean_html(x, _SCRIPT_RE, HtmlTag, _simple_parse_html) + comment_layer = lambda x: _parse_clean_html(x, _COMMENT_RE, HtmlDataFragment, script_layer) + delayed_element = None + for element in comment_layer(text): + if isinstance(element, HtmlTag): + if delayed_element is not None: + yield delayed_element + delayed_element = None + yield element + else:# element is HtmlDataFragment + if delayed_element is not None: + delayed_element.start = min(element.start, delayed_element.start) + delayed_element.end = max(element.end, delayed_element.end) + else: + delayed_element = element + if delayed_element is not None: + yield delayed_element + +def _parse_clean_html(text, regex, htype, func): + """ + Removes regions from text, passes the cleaned text to the lower parse layer, + and reinserts removed regions. + regex - regular expression that defines regions to be removed/re inserted + htype - the html parser type of the removed elements + func - function that performs the lower parse layer + """ + removed = [[m.start(), m.end(), m.groups()] for m in regex.finditer(text)] + + cleaned = regex.sub("", text) + shift = 0 + for element in func(cleaned): + element.start += shift + element.end += shift + while removed: + if element.end <= removed[0][0]: + yield element + break + else: + start, end, groups = removed.pop(0) + add = end - start + element.end += add + shift += add + if element.start >= start: + element.start += add + elif isinstance(element, HtmlTag): + yield element + break + + if element.start < start: + yield HtmlDataFragment(element.start, start) + element.start = end + + if htype == HtmlTag: + begintag = _parse_tag(_HTML_REGEXP.match(groups[0])) + endtag = _parse_tag(_HTML_REGEXP.match(groups[1])) + begintag.start = start + begintag.end += start + + endtag.start = end - endtag.end + endtag.end = end + content = None + if begintag.end < endtag.start: + content = HtmlDataFragment(begintag.end, endtag.start) + yield begintag + if content is not None: + yield content + yield endtag + else: + yield htype(start, end) + else: + yield element + +def _simple_parse_html(text): + """Simple html parse. It returns a sequence of HtmlTag and HtmlDataFragment + objects. Does not ignore any region. + """ + # If have doctype remove it. + start_pos = 0 + match = _DOCTYPE_REGEXP.match(text) + if match: + start_pos = match.end() + prev_end = start_pos + for match in _HTML_REGEXP.finditer(text, start_pos): + start = match.start() + end = match.end() + + if start > prev_end: + yield HtmlDataFragment(prev_end, start) + + yield _parse_tag(match) + prev_end = end + textlen = len(text) + if prev_end < textlen: + yield HtmlDataFragment(prev_end, textlen) + +def _parse_tag(match): + """ + parse a tag matched by _HTML_REGEXP + """ + data = match.groups() + closing, tag, attr_text = data[:3] + # if tag is None then the match is a comment + if tag is not None: + unpaired = data[-1] + if closing: + tag_type = HtmlTagType.CLOSE_TAG + elif unpaired: + tag_type = HtmlTagType.UNPAIRED_TAG + else: + tag_type = HtmlTagType.OPEN_TAG + attributes = [] + for attr_match in _ATTR_REGEXP.findall(attr_text): + name = attr_match[0].lower() + values = [v for v in attr_match[1:] if v] + attributes.append((name, values[0] if values else None)) + return HtmlTag(tag_type, tag.lower(), dict(attributes), match.start(), match.end()) diff --git a/scrapy/tests/test_contrib_ibl/__init__.py b/scrapy/tests/test_contrib_ibl/__init__.py new file mode 100644 index 000000000..f5fa69d7a --- /dev/null +++ b/scrapy/tests/test_contrib_ibl/__init__.py @@ -0,0 +1,2 @@ +import sys +path = sys.modules[__name__].__path__[0] diff --git a/scrapy/tests/test_contrib_ibl/samples_htmlpage.json.gz b/scrapy/tests/test_contrib_ibl/samples_htmlpage.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..fe4d1894c28718146c560b5dced991cbc775ecdb GIT binary patch literal 105488 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

8Z6POS9R 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�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 V`hWeu|Lgzy{{Wd6i^`Vu3IJ=3NCW@? literal 0 HcmV?d00001 diff --git a/scrapy/tests/test_contrib_ibl/samples_pageparsing.json.gz b/scrapy/tests/test_contrib_ibl/samples_pageparsing.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..20a2eb09a2f0a6918fafe602bd4328a498e3cd85 GIT binary patch literal 9013 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 +

COMPANY - Item Title

+

introduction

+
+ +

+This is such a nice item
Everybody likes it. +

+
+
+

click here for other items

+ +""" + +EXTRACT_PAGE1 = u""" + +

Scrapy - Nice Product

+

introduction

+
+a nice product image +

wonderful product

+
+
+ +""" + +# single tag with multiple items extracted +ANNOTATED_PAGE2 = u""" +xx +xxx + +""" +EXTRACT_PAGE2 = u"""product 1 is great""" + +# matching must match the second attribute in order to find the first +ANNOTATED_PAGE3 = u""" +

xx

+
xx
+""" +EXTRACT_PAGE3 = u""" +

description

+
delivery
+

this is not the description

+""" + +# test inferring repeated elements +ANNOTATED_PAGE4 = u""" +
    +
  • feature1
  • +
  • feature2
  • +
+""" + +EXTRACT_PAGE4 = u""" +
    +
  • feature1
  • ignore this +
  • feature2
  • +
  • feature3
  • +
+""" + +# test variant handling with identical repeated variant +ANNOTATED_PAGE5 = u""" +

description

+ + + + + + + + + +
colour 1price 1
colour 2price 2
+""" + +EXTRACT_PAGE5 = u""" +

description

+ + + + + + + + + + + + + +
colour 1price 1
colour 2price 2
colour 3price 3
+""" + +# test variant handling with irregular structure and some non-variant +# attributes +ANNOTATED_PAGE6 = u""" +

description

+

name 1

+
name 3
+

name 2

+""" +EXTRACT_PAGE6 = u""" +

description

+

name 1

+
name 3
+

name 2

+""" + +# test repeating variants at the table column level +ANNOTATED_PAGE7 = u""" + + + + + + + + + +
colour 1colour 2
price 1price 2
+""" + +EXTRACT_PAGE7 = u""" + + + + + + + + + + + +
colour 1colour 2colour 3
price 1price 2price 3
+""" + +ANNOTATED_PAGE8 = u""" + +

A product

+
+

XXXX XXXX xxxxx

+ +
+
+10.00

13

+
+ +""" + +EXTRACT_PAGE8 = u""" + +

A product

+
+

A very nice product for all intelligent people

+ +
+
+12.00

ID 15

+(VAT exc.)
+ +""" + +ANNOTATED_PAGE9 = ANNOTATED_PAGE8 + +EXTRACT_PAGE9 = u""" + + +

A product

+
+

A very nice product for all intelligent people

+ +
+
+12.00

ID 16

+(VAT exc.)
+ +""" + +ANNOTATED_PAGE10a = u""" + + + + + + + + + + + +
SKUL345
Size10cmx20cm
Price£s;99.00
+ +""" + +ANNOTATED_PAGE10b = u""" + + + + + + + + + + + +
SKUS220
Size20cmx20cm
Price£s;85.00
+ +""" + +EXTRACT_PAGE10a = u""" + + + + + + + + + + + +
OfferFrom $2500.00
DescriptionElectrorheological Cyborgs
Series:T2000
+ +""" + +EXTRACT_PAGE10b = u""" + + + + + + + + + + + +
SKUK80
Size50cm
Price&euros;85.00
+ +""" + +ANNOTATED_PAGE11 = u""" + +

+ +SL342 + +
+Nice product for ladies +
+£s;85.00 + +

+ +£s;100.00 + + +""" + +EXTRACT_PAGE11 = u""" + +

+SL342 +
+Nice product for ladies +
+£s;85.00 +

+£s;100.00 + +""" + +ANNOTATED_PAGE12 = u""" + +

A product

+
+

XXXX XXXX xxxxx

+ +
+10.00

13

+
+
+ +""" + +EXTRACT_PAGE12a = u""" + +

A product

+
+

A very nice product for all intelligent people

+ +
+12.00

ID 15

+(VAT exc.) +
+ +""" + +EXTRACT_PAGE12b = u""" + +

A product

+
+

A very nice product for all intelligent people

+ +
+12.00

ID 15

+(VAT exc.) +
+
    +Features +
  • Feature A
  • +
  • Feature B
  • +
+
+ +""" + +# Ex1: nested annotation with token sequence replica outside exterior annotation +# and a possible sequence pattern can be extracted only with +# correct handling of nested annotations +ANNOTATED_PAGE13a = u""" + + +

+


+

A product

+$50.00 +This product is excelent. Buy it! +

+
+ +

+

See other products:

+Product b +

+
+
+ +""" + +EXTRACT_PAGE13a = u""" + + +

+

A product

+$50.00 +This product is excelent. Buy it! +
+

+
+ +

+

See other products:

+Product B +

+
+ +""" + +# Ex2: annotation with token sequence replica inside a previous nested annotation +# and a possible sequence pattern can be extracted only with +# correct handling of nested annotations +ANNOTATED_PAGE13b = u""" + + +

+

A product

+Previous price: $50.00 +This product is excelent. Buy it! +

+
+ +

+

Save 10%!!

+$45.00 +

+
+ +""" + +EXTRACT_PAGE13b = u""" + + +

+

A product

+$50.00 +This product is excelent. Buy it! +

+
+ +
+

+

Save 10%!!

+$45.00 +

+
+
+ +""" + +ANNOTATED_PAGE14 = u""" + + +

+ +""" + +EXTRACT_PAGE14 = u""" + + +""" + +ANNOTATED_PAGE15 = u""" + +
Short +
892342
+
+
+

Description +90.00 +

+ +""" + +EXTRACT_PAGE15 = u""" + +
+

Description +80.00 +

+ +""" + +ANNOTATED_PAGE16 = u""" + +
+Description +

+name

+

+80.00

+
+ +""" + +EXTRACT_PAGE16 = u""" + +

product name

+

90.00

+ +""" + +ANNOTATED_PAGE17 = u""" + + +

+This product is excelent. Buy it! +

+
+
+ + +

See other products:

+

Product b +

+
+ +""" + +EXTRACT_PAGE17 = u""" + + +

+This product is excelent. Buy it! +

+
+ + +

See other products:

+

Product B +

+
+ +""" + +ANNOTATED_PAGE18 = u""" + +
+Item Id +
+Description +
+ +""" + +EXTRACT_PAGE18 = u""" + +
+Item Id +
+Description +
+ +""" + +ANNOTATED_PAGE19 = u""" + +
+

Product name

+

60.00

+ +

description

+
+ +""" + +EXTRACT_PAGE19a = u""" + +
+

Product name

+

60.00

+ +

description

+
+ +""" + +EXTRACT_PAGE19b = u""" + +
+

Range

+

from 20.00

+ +

+
+

+ +""" + +SAMPLE_DESCRIPTOR1 = ItemDescriptor('test', 'product test', [ + A('name', "Product name", required=True), + A('price', "Product price, including any discounts and tax or vat", + contains_any_numbers, True), + A('image_urls', "URLs for one or more images", image_url, True), + A('description', "The full description of the product", allow_markup=True), + ] +) + +# A list of (test name, [templates], page, extractors, expected_result) +TEST_DATA = [ + # extract from a similar page + ('similar page extraction', [ANNOTATED_PAGE1], EXTRACT_PAGE1, None, + {u'title': [u'Nice Product'], u'description': [u'wonderful product'], + u'image_url': [u'nice_product.jpg']} + ), + # strip the first 5 characters from the title + ('extractor test', [ANNOTATED_PAGE1], EXTRACT_PAGE1, + ItemDescriptor('test', 'product test', + [A('title', "something about a title", lambda x: x[5:])]), + {u'title': [u'Product'], u'description': [u'wonderful product'], + u'image_url': [u'nice_product.jpg']} + ), + # compilicated tag (multiple attributes and annotation) + ('multiple attributes and annotation', [ANNOTATED_PAGE2], EXTRACT_PAGE2, None, + {'name': [u'product 1'], 'image_url': [u'http://example.com/product1.jpg'], + 'description': [u'product 1 is great']} + ), + # can only work out correct placement by matching the second attribute first + ('ambiguous description', [ANNOTATED_PAGE3], EXTRACT_PAGE3, None, + {'description': [u'description'], 'delivery': [u'delivery']} + ), + # infer a repeated structure + ('repeated elements', [ANNOTATED_PAGE4], EXTRACT_PAGE4, None, + {'features': [u'feature1', u'feature2', u'feature3']} + ), + # identical variants with a repeated structure + ('repeated identical variants', [ANNOTATED_PAGE5], EXTRACT_PAGE5, None, + { + 'description': [u'description'], + 'variants': [ + {u'colour': [u'colour 1'], u'price': [u'price 1']}, + {u'colour': [u'colour 2'], u'price': [u'price 2']}, + {u'colour': [u'colour 3'], u'price': [u'price 3']} + ] + } + ), + # variants with an irregular structure + ('irregular variants', [ANNOTATED_PAGE6], EXTRACT_PAGE6, None, + { + 'description': [u'description'], + 'variants': [ + {u'name': [u'name 1']}, + {u'name': [u'name 3']}, + {u'name': [u'name 2']} + ] + } + ), + + # discovering repeated variants in table columns +# ('variants in table columns', [ANNOTATED_PAGE7], EXTRACT_PAGE7, None, +# {'variants': [ +# {u'colour': [u'colour 1'], u'price': [u'price 1']}, +# {u'colour': [u'colour 2'], u'price': [u'price 2']}, +# {u'colour': [u'colour 3'], u'price': [u'price 3']} +# ]} +# ), + + + # ignored regions + ( + 'ignored_regions', [ANNOTATED_PAGE8], EXTRACT_PAGE8, None, + { + 'description': [u'\n A very nice product for all intelligent people \n\n'], + 'price': [u'\n12.00\n(VAT exc.)'], + } + ), + # shifted ignored regions (detected by region similarity) + ( + 'shifted_ignored_regions', [ANNOTATED_PAGE9], EXTRACT_PAGE9, None, + { + 'description': [u'\n A very nice product for all intelligent people \n\n'], + 'price': [u'\n12.00\n(VAT exc.)'], + } + ), + # detection of common prefixes across templates, all templates marked + (# wrong extraction + 'without_match_common_prefix', [ANNOTATED_PAGE10a], EXTRACT_PAGE10a, None, + { + 'price': [u'\n Series: T2000 \n'], + 'dimensions': [u'\n Description Electrorheological Cyborgs \n'], + 'site_id': [u'\n Offer From $2500.00 \n'], + } + ), + (# right extraction + 'with_match_common_prefix', [ANNOTATED_PAGE10a, ANNOTATED_PAGE10b], EXTRACT_PAGE10a, None, + {} + ), + (# another example + 'match_common_prefix', [ANNOTATED_PAGE10a, ANNOTATED_PAGE10b], EXTRACT_PAGE10b, None, + { + 'dimensions': [u'50cm'], + 'site_id': [u'K80'], + } + ), + (# common_prefix with allow_markup attribute + 'common_prefix_allow_markup', [ANNOTATED_PAGE10a, ANNOTATED_PAGE10b], EXTRACT_PAGE10b, + ItemDescriptor('test', 'product test', + [A('dimensions', "something about dimensions", allow_markup=True)]), + { + 'dimensions': [u'50cm'], + 'site_id': [u'K80'], + } + ), + (# special case with partial annotations + 'special_partial_annotation', [ANNOTATED_PAGE11], EXTRACT_PAGE11, None, + { + 'name': [u'SL342'], + 'description': [u'\nSL342\n \nNice product for ladies\n \n£s;85.00\n'], + 'price': [u'£s;85.00'], + 'price_before_discount': [u'£s;100.00'], + } + ), + (# with ignore-beneath feature + 'ignore-beneath', [ANNOTATED_PAGE12], EXTRACT_PAGE12a, None, + { + 'description': [u'\n A very nice product for all intelligent people \n'], + } + ), + (# ignore-beneath with extra tags + 'ignore-beneath with extra tags', [ANNOTATED_PAGE12], EXTRACT_PAGE12b, None, + { + 'description': [u'\n A very nice product for all intelligent people \n'], + } + ), + ('nested annotation with replica outside', [ANNOTATED_PAGE13a], EXTRACT_PAGE13a, None, + {'description': [u'\n A product \n $50.00 \nThis product is excelent. Buy it!\n \n'], + 'price': ["$50.00"], + 'name': [u'A product']} + ), + ('outside annotation with nested replica', [ANNOTATED_PAGE13b], EXTRACT_PAGE13b, None, + {'description': [u'\n A product \n $50.00 \nThis product is excelent. Buy it!\n'], + 'price': ["$45.00"], + 'name': [u'A product']} + ), + ('consistency check', [ANNOTATED_PAGE14], EXTRACT_PAGE14, None, + {}, + ), + ('consecutive nesting', [ANNOTATED_PAGE15], EXTRACT_PAGE15, None, + {'description': [u'Description\n\n'], + 'price': [u'80.00']}, + ), + ('nested inside not found', [ANNOTATED_PAGE16], EXTRACT_PAGE16, None, + {'price': [u'90.00'], + 'name': [u'product name']}, + ), + ('ignored region helps to find attributes', [ANNOTATED_PAGE17], EXTRACT_PAGE17, None, + {'description': [u'\nThis product is excelent. Buy it!\n']}, + ), + ('ignored region in partial annotation', [ANNOTATED_PAGE18], EXTRACT_PAGE18, None, + {u'site_id': [u'Item Id'], + u'description': [u'\nDescription\n']}, + ), + ('extra required attribute product', [ANNOTATED_PAGE19], EXTRACT_PAGE19a, + SAMPLE_DESCRIPTOR1, + {u'price': [u'60.00'], + u'description': [u'description'], + u'image_urls': [['http://example.com/image.jpg']], + u'name': [u'Product name']}, + ), + ('extra required attribute no product', [ANNOTATED_PAGE19], EXTRACT_PAGE19b, + SAMPLE_DESCRIPTOR1, + None, + ), +] + +class TestExtraction(TestCase): + + def _run(self, name, templates, page, extractors, expected_output): + self.trace = None + template_pages = [HtmlPage(None, {}, t) for t in templates] + extractor = InstanceBasedLearningExtractor(template_pages, extractors, True) + actual_output, _ = extractor.extract(HtmlPage(None, {}, page)) + if not actual_output: + if expected_output is None: + return + assert False, "failed to extract data for test '%s'" % name + actual_output = actual_output[0] + self.trace = ["Extractor:\n%s" % extractor] + actual_output.pop('trace', []) + expected_names = set(expected_output.keys()) + actual_names = set(actual_output.keys()) + + missing_in_output = filter(None, expected_names - actual_names) + error = "attributes '%s' were expected but were not present in test '%s'" % \ + ("', '".join(missing_in_output), name) + assert len(missing_in_output) == 0, error + + unexpected = actual_names - expected_names + error = "unexpected attributes %s in test '%s'" % \ + (', '.join(unexpected), name) + assert len(unexpected) == 0, error + + for k, v in expected_output.items(): + extracted = actual_output[k] + assert v == extracted, "in test '%s' for attribute '%s', " \ + "expected value '%s' but got '%s'" % (name, k, v, extracted) + + def test_expected_outputs(self): + try: + for data in TEST_DATA: + self._run(*data) + except AssertionError: + if self.trace: + print "Trace:" + for line in self.trace: + print "\n---\n%s" % line + raise diff --git a/scrapy/tests/test_contrib_ibl/test_htmlpage.py b/scrapy/tests/test_contrib_ibl/test_htmlpage.py new file mode 100644 index 000000000..f502c6364 --- /dev/null +++ b/scrapy/tests/test_contrib_ibl/test_htmlpage.py @@ -0,0 +1,134 @@ +""" +htmlpage.py tests +""" +import os +from gzip import GzipFile +from unittest import TestCase + +from scrapy.utils.py26 import json +from scrapy.tests.test_contrib_ibl import path +from scrapy.contrib.ibl.htmlpage import parse_html, HtmlTag, HtmlDataFragment +from scrapy.tests.test_contrib_ibl.test_htmlpage_data import * + +SAMPLES_FILE = "samples_htmlpage.json.gz" + +def _encode_element(el): + """ + jsonize parse element + """ + if isinstance(el, HtmlTag): + return {"tag": el.tag, "attributes": el.attributes, + "start": el.start, "end": el.end, "tag_type": el.tag_type} + if isinstance(el, HtmlDataFragment): + return {"start": el.start, "end": el.end} + raise TypeError + +def _decode_element(dct): + """ + dejsonize parse element + """ + if "tag" in dct: + return HtmlTag(dct["tag_type"], dct["tag"], \ + dct["attributes"], dct["start"], dct["end"]) + if "start" in dct: + return HtmlDataFragment(dct["start"], dct["end"]) + return dct + +def add_sample(source): + """ + Method for adding samples to test samples file + (use from console) + """ + samples = [] + if os.path.exists(SAMPLES_FILE): + for line in GzipFile(os.path.join(path, SAMPLES_FILE), "r").readlines(): + samples.append(json.loads(line)) + + new_sample = {"source": source} + new_sample["parsed"] = list(parse_html(source)) + samples.append(new_sample) + samples_file = GzipFile(os.path.join(path, SAMPLES_FILE), "wb") + for sample in samples: + samples_file.write(json.dumps(sample, default=_encode_element) + "\n") + samples_file.close() + +class TestParseHtml(TestCase): + """Test for parse_html""" + def _test_sample(self, sample): + source = sample["source"] + expected_parsed = sample["parsed"] + parsed = parse_html(source) + count_element = 0 + count_expected = 0 + for element in parsed: + if type(element) == HtmlTag: + count_element += 1 + expected = expected_parsed.pop(0) + if type(expected) == HtmlTag: + count_expected += 1 + element_text = source[element.start:element.end] + expected_text = source[expected.start:expected.end] + if element.start != expected.start or element.end != expected.end: + assert False, "[%s,%s] %s != [%s,%s] %s" % (element.start, \ + element.end, element_text, expected.start, \ + expected.end, expected_text) + if type(element) != type(expected): + assert False, "(%s) %s != (%s) %s for text\n%s" % (count_element, \ + repr(type(element)), count_expected, repr(type(expected)), element_text) + if type(element) == HtmlTag: + self.assertEqual(element.tag, expected.tag) + self.assertEqual(element.attributes, expected.attributes) + + def test_parse(self): + """simple parse_html test""" + parsed = [_decode_element(d) for d in PARSED] + sample = {"source": PAGE, "parsed": parsed} + self._test_sample(sample) + + def test_site_samples(self): + """test parse_html from real cases""" + samples = [] + for line in GzipFile(os.path.join(path, SAMPLES_FILE), "r").readlines(): + samples.append(json.loads(line, object_hook=_decode_element)) + for sample in samples: + self._test_sample(sample) + + def test_bad(self): + """test parsing of bad html layout""" + parsed = [_decode_element(d) for d in PARSED2] + sample = {"source": PAGE2, "parsed": parsed} + self._test_sample(sample) + + def test_comments(self): + """test parsing of tags inside comments""" + parsed = [_decode_element(d) for d in PARSED3] + sample = {"source": PAGE3, "parsed": parsed} + self._test_sample(sample) + + def test_script_text(self): + """test parsing of tags inside scripts""" + parsed = [_decode_element(d) for d in PARSED4] + sample = {"source": PAGE4, "parsed": parsed} + self._test_sample(sample) + + def test_sucessive(self): + """test parsing of sucesive cleaned elements""" + parsed = [_decode_element(d) for d in PARSED5] + sample = {"source": PAGE5, "parsed": parsed} + self._test_sample(sample) + + def test_sucessive2(self): + """test parsing of sucesive cleaned elements (variant 2)""" + parsed = [_decode_element(d) for d in PARSED6] + sample = {"source": PAGE6, "parsed": parsed} + self._test_sample(sample) + + def test_special_cases(self): + """some special cases tests""" + parsed = list(parse_html("")) + self.assertEqual(parsed[0].attributes, {'content': 'no-cache', 'http-equiv': 'Pragma'}) + parsed = list(parse_html("")) + self.assertEqual(parsed[0].attributes, {'xmlns': 'http://www.w3.org/1999/xhtml', 'xml:lang': 'en', 'lang': 'en'}) + parsed = list(parse_html("")) + self.assertEqual(parsed[0].attributes, {'src': 'http://images.play.com/banners/SAM550a.jpg', \ + 'align': 'left', 'hspace': '5', '/': None}) diff --git a/scrapy/tests/test_contrib_ibl/test_htmlpage_data.py b/scrapy/tests/test_contrib_ibl/test_htmlpage_data.py new file mode 100644 index 000000000..2eac24f26 --- /dev/null +++ b/scrapy/tests/test_contrib_ibl/test_htmlpage_data.py @@ -0,0 +1,227 @@ +PAGE = u""" + + +