···11+# Config file for automatic testing at travis-ci.org
22+33+language: python
44+python: 2.7
55+66+# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
77+install:
88+ - pip install Scrapy docopt
99+1010+# command to run tests, e.g. python setup.py test
1111+script:
1212+ - nosetests tests
1313+1414+notifications:
1515+ slack: descartes2:6sgCzx3PvrO9IIMwKxj12dDM
+1-3
FourmiCrawler/items.py
···11-# Define here the models for your scraped items
22-#
33-# See documentation in:
11+# For more information on item definitions, see the Scrapy documentation in:
42# http://doc.scrapy.org/en/latest/topics/items.html
5364from scrapy.item import Item, Field
+13-12
FourmiCrawler/pipelines.py
···11-# Define your item pipelines here
22-#
33-# Don't forget to add your pipeline to the ITEM_PIPELINES setting
44-# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
11+# For more information on item pipelines, see the Scrapy documentation in:
22+# http://doc.scrapy.org/en/latest/topics/item-pipeline.html
53import re
44+65from scrapy.exceptions import DropItem
7688-class RemoveNonePipeline(object):
9788+class RemoveNonePipeline(object):
109 def __init__(self):
1111- self.known_values = set()
1010+ pass
12111313- def process_item(self, item, spider):
1212+ @staticmethod
1313+ def process_item(item, spider):
1414 """
1515 Processing the items so None values are replaced by empty strings
1616 :param item: The incoming item
···2222 item[key] = ""
2323 return item
24242525-class DuplicatePipeline(object):
26252626+class DuplicatePipeline(object):
2727 def __init__(self):
2828 self.known_values = set()
2929···3636 """
3737 value = (item['attribute'], item['value'], item['conditions'])
3838 if value in self.known_values:
3939- raise DropItem("Duplicate item found: %s" % item) # #[todo] append sources of first item.
3939+ raise DropItem("Duplicate item found: %s" % item) # [todo] append sources of first item.
4040 else:
4141 self.known_values.add(value)
4242 return item
43434444-class AttributeSelectionPipeline(object):
45444545+class AttributeSelectionPipeline(object):
4646 def __init__(self):
4747- pass;
4747+ pass
48484949- def process_item(self, item, spider):
4949+ @staticmethod
5050+ def process_item(item, spider):
5051 """
5152 The items are processed using the selected attribute list available in the spider,
5253 items that don't match the selected items are dropped.
+1-1
FourmiCrawler/settings.py
···33# For simplicity, this file contains only the most important settings by
44# default. All the other settings are documented here:
55#
66-# http://doc.scrapy.org/en/latest/topics/settings.html
66+# http://doc.scrapy.org/en/latest/topics/settings.html
77#
8899BOT_NAME = 'FourmiCrawler'
+6-5
FourmiCrawler/sources/ChemSpider.py
···11-from source import Source
11+import re
22+23from scrapy import log
34from scrapy.http import Request
45from scrapy.selector import Selector
66+77+from source import Source
58from FourmiCrawler.items import Result
66-import re
99+710811# [TODO] - Maybe clean up usage of '.extract()[0]', because of possible IndexError exception.
912···5861 prop_conditions = ''
59626063 # Test for properties without values, with one hardcoded exception
6161- if (not re.match(r'^\d', prop_value) or
6262- (prop_name == 'Polarizability' and
6363- prop_value == '10-24cm3')):
6464+ if not re.match(r'^\d', prop_value) or (prop_name == 'Polarizability' and prop_value == '10-24cm3'):
6465 continue
65666667 # Match for condition in parentheses
+13-10
FourmiCrawler/sources/NIST.py
···11-from source import Source
11+import re
22+23from scrapy import log
34from scrapy.http import Request
45from scrapy.selector import Selector
66+77+from source import Source
58from FourmiCrawler.items import Result
66-import re
99+710811# [TODO]: values can be '128.', perhaps remove the dot in that case?
912# [TODO]: properties have references and comments which do not exist in the
1010-# Result item, but should be included eventually.
1313+# Result item, but should be included eventually.
11141215class NIST(Source):
1316 """NIST Scraper plugin
···1518 This plugin manages searching for a chemical on the NIST website
1619 and parsing the resulting page if the chemical exists on NIST.
1720 """
1818- website = "http://webbook.nist.gov/*"
2121+ website = "http://webbook.nist.gov/*"
19222023 search = 'cgi/cbook.cgi?Name=%s&Units=SI&cTP=on'
2124···7578 requests.extend(self.parse_generic_data(table, summary))
7679 else:
7780 log.msg('NIST table: NOT SUPPORTED', level=log.WARNING)
7878- continue #Assume unsupported
8181+ continue # Assume unsupported
7982 return requests
80838184 def parse_generic_info(self, sel):
···103106 data['IUPAC Standard InChI'] = raw_inchi.extract()[0]
104107105108 raw_inchikey = ul.xpath('li[strong="IUPAC Standard InChIKey:"]'
106106- '/tt/text()')
109109+ '/tt/text()')
107110 data['IUPAC Standard InChIKey'] = raw_inchikey.extract()[0]
108111109112 raw_cas_number = ul.xpath('li[strong="CAS Registry Number:"]/text()')
···129132 results = []
130133 for tr in table.xpath('tr[td]'):
131134 extra_data_url = tr.xpath('td[last()][a="Individual data points"]'
132132- '/a/@href').extract()
135135+ '/a/@href').extract()
133136 if extra_data_url:
134137 request = Request(url=self.website[:-1] + extra_data_url[0],
135135- callback=self.parse_individual_datapoints)
138138+ callback=self.parse_individual_datapoints)
136139 results.append(request)
137140 continue
138141 data = []
···179182 'conditions': '%s K, (%s -> %s)' % (tds[1], tds[2], tds[3])
180183 })
181184 results.append(result)
182182-183185184186 return results
185187···228230229231 return results
230232231231- def parse_individual_datapoints(self, response):
233233+ @staticmethod
234234+ def parse_individual_datapoints(response):
232235 """Parses the page linked from aggregate data"""
233236 sel = Selector(response)
234237 table = sel.xpath('//table[@class="data"]')[0]
+5-3
FourmiCrawler/sources/WikipediaParser.py
···11+import re
22+13from scrapy.http import Request
24from scrapy import log
33-from source import Source
45from scrapy.selector import Selector
66+77+from source import Source
58from FourmiCrawler.items import Result
66-import re
79810911class WikipediaParser(Source):
···3638 """ scrape data from infobox on wikipedia. """
3739 items = []
38403939- #be sure to get chembox (wikipedia template)
4141+ # be sure to get chembox (wikipedia template)
4042 tr_list = sel.xpath('.//table[@class="infobox bordered"]//td[not(@colspan)]'). \
4143 xpath('normalize-space(string())')
4244 prop_names = tr_list[::2]
+19-2
FourmiCrawler/sources/source.py
···77 _spider = None
8899 def __init__(self):
1010+ """
1111+ Initiation of a new Source
1212+ """
1013 pass
11141212- def parse(self, reponse):
1313- log.msg("The parse function of the empty parser was used.", level=log.WARNING)
1515+ def parse(self, response):
1616+ """
1717+ This function should be able to parse all Scrapy Response objects with a URL matching the website Regex.
1818+ :param response: A Scrapy Response object
1919+ :return: A list of Result items and new Scrapy Requests
2020+ """
2121+ log.msg("The parse function of the empty source was used.", level=log.WARNING)
1422 pass
15231624 def new_compound_request(self, compound):
2525+ """
2626+ This function should return a Scrapy Request for the given compound request.
2727+ :param compound: A compound name.
2828+ :return: A new Scrapy Request
2929+ """
1730 # return Request(url=self.website[:-1] + compound, callback=self.parse)
1831 pass
19322033 def set_spider(self, spider):
3434+ """
3535+ A Function to save the associated spider.
3636+ :param spider: A FourmiSpider object
3737+ """
2138 self._spider = spider
+54-20
FourmiCrawler/spider.py
···11+import re
22+13from scrapy.spider import Spider
24from scrapy import log
33-import re
455667class FourmiSpider(Spider):
88+ """
99+ A spider writen for the Fourmi Project which calls upon all available sources to request and scrape data.
1010+ """
711 name = "FourmiSpider"
88- __parsers = []
99- synonyms = []
1212+ _sources = []
1313+ synonyms = set()
10141115 def __init__(self, compound=None, selected_attributes=[".*"], *args, **kwargs):
1616+ """
1717+ Initiation of the Spider
1818+ :param compound: compound that will be searched.
1919+ :param selected_attributes: A list of regular expressions that the attributes should match.
2020+ """
1221 super(FourmiSpider, self).__init__(*args, **kwargs)
1313- self.synonyms.append(compound)
1414- self.selected_attributes = selected_attributes;
2222+ self.synonyms.add(compound)
2323+ self.selected_attributes = selected_attributes
15241616- def parse(self, reponse):
1717- for parser in self.__parsers:
1818- if re.match(parser.website, reponse.url):
1919- log.msg("Url: " + reponse.url + " -> Source: " + parser.website, level=log.DEBUG)
2020- return parser.parse(reponse)
2525+ def parse(self, response):
2626+ """
2727+ The function that is called when a response to a request is available. This function distributes this to a
2828+ source which should be able to handle parsing the data.
2929+ :param response: A Scrapy Response object that should be parsed
3030+ :return: A list of Result items and new Request to be handled by the scrapy core.
3131+ """
3232+ for source in self._sources:
3333+ if re.match(source.website, response.url):
3434+ log.msg("Url: " + response.url + " -> Source: " + source.website, level=log.DEBUG)
3535+ return source.parse(response)
2136 return None
22372338 def get_synonym_requests(self, compound):
3939+ """
4040+ A function that generates new Scrapy Request for each source given a new synonym of a compound.
4141+ :param compound: A compound name
4242+ :return: A list of Scrapy Request objects
4343+ """
2444 requests = []
2525- for parser in self.__parsers:
2626- parser_requests = parser.new_compound_request(compound)
2727- if parser_requests is not None:
2828- requests.append(parser_requests)
4545+ if compound not in self.synonyms:
4646+ self.synonyms.add(compound)
4747+ for parser in self._sources:
4848+ parser_requests = parser.new_compound_request(compound)
4949+ if parser_requests is not None:
5050+ requests.append(parser_requests)
2951 return requests
30523153 def start_requests(self):
5454+ """
5555+ The function called by Scrapy for it's first Requests
5656+ :return: A list of Scrapy Request generated from the known synonyms using the available sources.
5757+ """
3258 requests = []
3359 for synonym in self.synonyms:
3460 requests.extend(self.get_synonym_requests(synonym))
3561 return requests
36623737- def add_parsers(self, parsers):
3838- for parser in parsers:
3939- self.add_parser(parser)
6363+ def add_sources(self, sources):
6464+ """
6565+ A function to add a new Parser objects to the list of available sources.
6666+ :param sources: A list of Source Objects.
6767+ """
6868+ for parser in sources:
6969+ self.add_source(parser)
40704141- def add_parser(self, parser):
4242- self.__parsers.append(parser)
4343- parser.set_spider(self)7171+ def add_source(self, source):
7272+ """
7373+ A function add a new Parser object to the list of available parsers.
7474+ :param source: A Source Object
7575+ """
7676+ self._sources.append(source)
7777+ source.set_spider(self)
+4
README.md
···11# Fourmi
2233+**Master branch**: [](https://travis-ci.org/Recondor/Fourmi)
44+55+**Developing branch**: [](https://travis-ci.org/Recondor/Fourmi)
66+37Fourmi is an web scraper for chemical substances. The program is designed to be
48used as a search engine to search multiple chemical databases for a specific
59substance. The program will produce all available attributes of the substance
+28-6
fourmi.py
···11-#!/usr/bin/env python
11+# !/usr/bin/env python
22"""
33Fourmi, a web scraper build to search specific information for a given compound (and it's pseudonyms).
44···3333from sourceloader import SourceLoader
343435353636-def setup_crawler(searchable, settings, source_loader, attributes):
3737- spider = FourmiSpider(compound=searchable, selected_attributes=attributes)
3838- spider.add_parsers(source_loader.sources)
3636+def setup_crawler(compound, settings, source_loader, attributes):
3737+ """
3838+ This function prepares and start the crawler which starts the actual search on the internet
3939+ :param compound: The compound which should be searched
4040+ :param settings: A scrapy settings object
4141+ :param source_loader: A fully functional SourceLoader object which contains only the sources that should be used.
4242+ :param attributes: A list of regular expressions which the attribute names should match.
4343+ """
4444+ spider = FourmiSpider(compound=compound, selected_attributes=attributes)
4545+ spider.add_sources(source_loader.sources)
3946 crawler = Crawler(settings)
4047 crawler.signals.connect(reactor.stop, signal=signals.spider_closed)
4148 crawler.configure()
···445145524653def scrapy_settings_manipulation(docopt_arguments):
5454+ """
5555+ This function manipulates the Scrapy settings that normally would be set in the settings file. In the Fourmi
5656+ project these are command line arguments.
5757+ :param docopt_arguments: A dictionary generated by docopt containing all CLI arguments.
5858+ """
4759 settings = get_project_settings()
4848- # [todo] - add at least a warning for files that already exist
6060+4961 if docopt_arguments["--output"] != 'result.*format*':
5062 settings.overrides["FEED_URI"] = docopt_arguments["--output"]
5163 elif docopt_arguments["--format"] == "jsonlines":
···607261736274def start_log(docopt_arguments):
7575+ """
7676+ This function starts the logging functionality of Scrapy using the settings given by the CLI.
7777+ :param docopt_arguments: A dictionary generated by docopt containing all CLI arguments.
7878+ """
6379 if docopt_arguments["--log"] is not None:
6480 if docopt_arguments["--verbose"]:
6581 log.start(logfile=docopt_arguments["--log"], logstdout=False, loglevel=log.DEBUG)
···738974907591def search(docopt_arguments, source_loader):
9292+ """
9393+ The function that facilitates the search for a specific compound.
9494+ :param docopt_arguments: A dictionary generated by docopt containing all CLI arguments.
9595+ :param source_loader: An initiated SourceLoader object pointed at the directory with the sources.
9696+ """
7697 start_log(docopt_arguments)
7798 settings = scrapy_settings_manipulation(docopt_arguments)
7899 setup_crawler(docopt_arguments["<compound>"], settings, source_loader, docopt_arguments["--attributes"].split(','))
79100 reactor.run()
8010181102103103+# The start for the Fourmi Command Line interface.
82104if __name__ == '__main__':
8383- arguments = docopt.docopt(__doc__, version='Fourmi - V0.4.0')
105105+ arguments = docopt.docopt(__doc__, version='Fourmi - V0.4.1')
84106 loader = SourceLoader()
8510786108 if arguments["--include"]:
+18
setup.py
···11+import sys
22+from cx_Freeze import setup, Executable
33+44+# After running the setup file (python setup.py build) the scrapy/VERSION file has to be manually put into the
55+# library.zip, also the FourmiCrawler map has to be copied to both the library and the exe.win32-2.7 folder. after
66+# putting the files in the library the library has to be zipped and replace the old library.
77+# Dependencies are automatically detected, but it might need fine tuning.
88+build_exe_options = {"packages": ["os", "scrapy", "lxml", "w3lib", "pkg_resources", "zope.interface", "twisted.internet"], "excludes": []}
99+1010+# GUI applications require a different base on Windows (the default is for a
1111+# console application).
1212+base = None
1313+1414+setup( name = "Scrapy",
1515+ version = "0.1",
1616+ description = "My GUI application!",
1717+ options = {"build_exe": build_exe_options},
1818+ executables = [Executable("fourmi.py", base=base)])
+23-4
sourceloader.py
···11import inspect
22+import sys
23import os
34import re
55+46from FourmiCrawler.sources.source import Source
5768···810 sources = []
9111012 def __init__(self, rel_dir="FourmiCrawler/sources"):
1111- path = os.path.dirname(os.path.abspath(__file__))
1313+1414+ if hasattr(sys,'frozen'):
1515+ path = os.path.dirname(sys.executable)
1616+ else:
1717+ path = os.path.dirname(os.path.abspath(__file__))
1818+1219 path += "/" + rel_dir
1320 known_parser = set()
14211522 for py in [f[:-3] for f in os.listdir(path) if f.endswith('.py') and f != '__init__.py']:
1616- mod = __import__('.'.join([rel_dir.replace("/", "."), py]), fromlist=[py])
2323+ mod = __import__('.'.join([rel_dir.replace('/', "."), py]), fromlist=[py])
1724 classes = [getattr(mod, x) for x in dir(mod) if inspect.isclass(getattr(mod, x))]
1825 for cls in classes:
1926 if issubclass(cls, Source) and cls not in known_parser:
2020- self.sources.append(cls()) # [review] - Would we ever need arguments for the parsers?
2121- known_parser.add(cls)
2727+ self.sources.append(cls()) # [review] - Would we ever need arguments for the parsers?
2828+ # known_parser.add(cls)
22292330 def include(self, source_names):
3131+ """
3232+ This function excludes all sources that don't match the given regular expressions.
3333+ :param source_names: A list of regular expression (strings)
3434+ """
2435 new = set()
2536 for name in source_names:
2637 new.update([src for src in self.sources if re.match(name, src.__class__.__name__)])
2738 self.sources = list(new)
28392940 def exclude(self, source_names):
4141+ """
4242+ This function excludes all sources that match the given regular expressions.
4343+ :param source_names: A list of regular expression (strings)
4444+ """
3045 exclude = []
3146 for name in source_names:
3247 exclude.extend([src for src in self.sources if re.match(name, src.__class__.__name__)])
3348 self.sources = [src for src in self.sources if src not in exclude]
34493550 def __str__(self):
5151+ """
5252+ This function returns a string with all sources currently available in the SourceLoader.
5353+ :return: a string with all available sources.
5454+ """
3655 string = ""
3756 for src in self.sources:
3857 string += "Source: " + src.__class__.__name__