#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

import random
import urllib2
import mutex
import threading
import re, time
from optparse import OptionParser

default_page = "http://poisson.phc.unipi.it"

__author__ = "Leonardo Robol <leo@robol.it>"

mtx_url_dict = mutex.mutex()

size = 1000
url_dict = {}
url_counter = range(size)

max_steps = 5

debug = False
outfile = "connections.txt"


def get_links(page):
    """Restituisce una lista con i link
    presenti nella pagina data, in forma canonica"""
    content, real_url = get_content(page.url)

    if(content == -1):
        return -1

    links = re.findall(r"<a href=\"(\S*)\"[^>]*>",content)
    ret = []
    for link in links:
        # Espando il link in modo da (speriamo!)
        # garantire l'unicità
        ret.append(expand_url(real_url, link))

    return ret


def expand_url(parent, url):
    """Questa funzione prende l'url della pagina parent
    e l'url del link e dà all'url del link una forma unica
    e canonica, del tipo

      http://www.example.com/pagina
      http://www.example.com/pagina.html
    """

    ## Controllo che l'url non cominci con un punto
    ## nel qual caso cerchiamo di rimediare subito,
    ## ma non cadiamo nel tranello di ignorare i ..
    if url[0] == ".":
        if len(url) == 1:
            url = parent

        else:
            if(url[1] != "."):
                url = url[1:]

    ## Se all'inizio dell'url c'è uno slash non ci serve tutto
    ## il parent, ma solo la prima parte
    if url.startswith("/"):
        parent = re.search(".+//[^/]*", parent).group(0)
    else:
        # in caso contrario dobbiamo assicurarci di troncare
        # l'ultima parte dell'url dopo il /, a meno che non
        # finisca senza estensione (in quel caso aggiungiamo un /)
        if re.search("\.[^/]*$", parent):
            parent = re.sub("[^/]*$", "", parent)
        else:
            if not parent.endswith("/"):
                parent += "/"



    ## Controlliamo prima di tutto se nell'url c'è un
    ## protocollo
    protocol = re.search(r"(\w+):", url)
    if protocol == None:
        url = parent + url
    return url

def get_content(url):
    """Cerca di scaricare l'url dato e restituisce
    -1 se non ce la fa, il contenuto altrimenti"""
    try:
        req = urllib2.urlopen(url)
    except:
        return (-1, None)

    return (req.read(), req.geturl())

class Page():
    """Una pagina web. Questa classe, quando viene istanziata,
    controlla se una pagina con quel nome è già presente (una
    pagina è unica!) e se lo è restituisce lo stesso oggetto,
    altrimenti ne crea uno nuovo con un nuovo ID"""

    def __repr__(self):
        return "<Page object: %s>" % self.url

    def __init__(self, url=""):

        if(url != ""):
            mtx_url_dict.lock(self.__new_page, url)
            mtx_url_dict.unlock()
        else:
            mtx_url_dict.lock(self.__get_page, 0)
            mtx_url_dict.unlock()

    def __get_page(self, num):

        if(len(url_counter) == 0):
            self.exhausted = True
            return

        page_found = False

        while(not page_found):

            for url in url_dict:
                page = Page(url)
                if not page.analyzed:
                    page_found = True
                    self.url = url
                    break

            if not page_found:
                time.sleep(1)



        self.ID = page.ID
        self.analyzed = page.analyzed
        self.exhausted = False
        url_dict[url].analyzed = True

    def __new_page(self, url):
        # Questo ci serve per tenere il
        # conto di tutti gli url
        global url_dict
        global url_counter

        self.exhausted = False
        self.analyzed = False
        self.url = url

        if(url_dict.has_key(url)):
            # Preservo i parametri che esistono già!
            self.ID = url_dict[url].ID
            self.analyzed = url_dict[url].analyzed

        else:
            try:
                self.ID = url_counter.pop()
            except IndexError:
                self.exhausted = True


            url_dict[url] = self
            url_dict[url].links = []

    def add_link(self, page):

        if(page.exhausted):
            return -1
        if debug:
            print " => Adding link to %s" % page.url
        mtx_url_dict.lock(self.__add_link, page.ID)
        mtx_url_dict.unlock()
        return 0

    def __add_link(self, ID):
        url_dict[self.url].links.append(ID)

    def links(self):
        return url_dict[self.url].links




class Crawler(threading.Thread):
    """Partendo da startpage, segue tutti i link registrando
    i vari collegamenti fra le pagine. Una volta raggiunto il
    limite di pagine da controllare termina"""

    def __init__(self, startpage=default_page):
        threading.Thread.__init__(self)
        self.start_page = startpage


    def run(self):

        step_counter = 0

        # Capiamo che pagina ci serve
        page = Page(self.start_page)

        while(not page.exhausted):

            if(step_counter > max_steps):
                page = Page(self.start_page)
                step_counter = 0
            else:
                page = Page()
                step_counter += 1

            if page.exhausted:
                break

            # Come prima cosa devo fare il parsing dei
            # link che ci sono nella pagina
            # Diamo una mixata per simulare meglio
            # il caso.. dato che tanto è probabile che
            # alcuni link rimarranno non visitati!
            links = get_links(page)

            ## A questo punto io che mi occupo della pagina devo
            ## aggiungere tutti i link alla pagina

            if not links == -1:
                random.shuffle(links)
                for l in links:
                    lpage = Page(l)

                    if not lpage.exhausted:
                        page.add_link(lpage)
                    else:
                        break




if __name__ == "__main__":

    parser = OptionParser()
    parser.add_option("-c", "--concurrency", dest="concurrency", action="store",
                      help="Set level of concurrency (i.e. how many threads)", default=3)
    parser.add_option("-d", "--debug", dest="debug", action="store_true",
                      help="Activate debug mode", default=False)
    parser.add_option("-o", "--output", dest="outfile", action="store",
                      help="Name of the output file for the connection matrix", default="connections.txt")
    parser.add_option("-n", "--number", dest="size", action="store",
                      help="Number of pages to analyze", default=1000)
    parser.add_option("-m", "--max-steps", dest="max_steps", action="store",
                      help="Max steps to walk from the starting page", default=5)
    parser.add_option("-s", "--start-page", dest="start_page", default="http://poisson.phc.unipi.it",
                      action="store")


    (option, args) = parser.parse_args()

    concurrency = int(option.concurrency)
    debug = bool(option.debug)
    outfile = option.outfile
    size = int(option.size)
    url_counter = range(size)
    max_steps = int(option.max_steps)
    default_page = option.start_page


    l = time.localtime(time.time())
    print " => Starting with this configuration at %s:%s:%s\n\
    %d thread(s)\n\
    %d pages to analyze\n\
    %d max steps from the start page, %s\n\
    Writing on file %s\n\
    " % (l.tm_hour,l.tm_min,l.tm_sec, concurrency, size, max_steps, default_page, outfile)


    threads = []
    for i in range(0, concurrency):
        threads.append(Crawler(default_page))
        threads[i].start()


    ## Qui non c'è modo umano di terminare il
    ## suo lavoro, bisognerà studiarci sopra
    for i in range(0, concurrency):
        threads[i].join()


    ## A questo punto mi devo preoccupare di salvare
    ## la matrice in un formato soddisfacente

    out = open(outfile, 'w')
    out.write(str(size) + "\n")



    for page in url_dict:
        for link in url_dict[page].links:
            out.write(page + "\t" + str(url_dict[page].ID) + "\t" + str(link) + "\n")

    l = time.localtime(time.time())
    print " => Work completed at %s:%s:%s " % (l.tm_hour,l.tm_min,l.tm_sec)



ViewGit