| 3103 | LIFO = "lifo" # Last In, First Out (= FILO). |
| 3104 | |
| 3105 | class Crawler(object): |
| 3106 | |
| 3107 | def __init__(self, links=[], domains=[], delay=20.0, parser=HTMLLinkParser().parse, sort=FIFO): |
| 3108 | """ A crawler can be used to browse the web in an automated manner. |
| 3109 | It visits the list of starting URLs, parses links from their content, visits those, etc. |
| 3110 | - Links can be prioritized by overriding Crawler.priority(). |
| 3111 | - Links can be ignored by overriding Crawler.follow(). |
| 3112 | - Each visited link is passed to Crawler.visit(), which can be overridden. |
| 3113 | """ |
| 3114 | self.parse = parser |
| 3115 | self.delay = delay # Delay between visits to the same (sub)domain. |
| 3116 | self.domains = domains # Domains the crawler is allowed to visit. |
| 3117 | self.history = {} # Domain name => time last visited. |
| 3118 | self.visited = {} # URLs visited. |
| 3119 | self._queue = [] # URLs scheduled for a visit: (priority, time, Link). |
| 3120 | self._queued = {} # URLs scheduled so far, lookup dictionary. |
| 3121 | self.QUEUE = 10000 # Increase or decrease according to available memory. |
| 3122 | self.sort = sort |
| 3123 | # Queue given links in given order: |
| 3124 | for link in (isinstance(links, basestring) and [links] or links): |
| 3125 | self.push(link, priority=1.0, sort=FIFO) |
| 3126 | |
| 3127 | @property |
| 3128 | def done(self): |
| 3129 | """ Yields True if no further links are scheduled to visit. |
| 3130 | """ |
| 3131 | return len(self._queue) == 0 |
| 3132 | |
| 3133 | def push(self, link, priority=1.0, sort=FILO): |
| 3134 | """ Pushes the given link to the queue. |
| 3135 | Position in the queue is determined by priority. |
| 3136 | Equal ranks are sorted FIFO or FILO. |
| 3137 | With priority=1.0 and FILO, the link is inserted to the queue. |
| 3138 | With priority=0.0 and FIFO, the link is appended to the queue. |
| 3139 | """ |
| 3140 | if not isinstance(link, Link): |
| 3141 | link = Link(url=link) |
| 3142 | dt = time.time() |
| 3143 | dt = sort == FIFO and dt or 1 / dt |
| 3144 | bisect.insort(self._queue, (1 - priority, dt, link)) |
| 3145 | self._queued[link.url] = True |
| 3146 | |
| 3147 | def pop(self, remove=True): |
| 3148 | """ Returns the next Link queued to visit and removes it from the queue. |
| 3149 | Links on a recently visited (sub)domain are skipped until Crawler.delay has elapsed. |
| 3150 | """ |
| 3151 | now = time.time() |
| 3152 | for i, (priority, dt, link) in enumerate(self._queue): |
| 3153 | if self.delay <= now - self.history.get(base(link.url), 0): |
| 3154 | if remove is True: |
| 3155 | self._queue.pop(i) |
| 3156 | self._queued.pop(link.url, None) |
| 3157 | return link |
| 3158 | |
| 3159 | @property |
| 3160 | def next(self): |
| 3161 | """ Returns the next Link queued to visit (without removing it). |
| 3162 | """ |
no outgoing calls
no test coverage detected
searching dependent graphs…