Extrayendo datos de AnimeFLV con Python y Scrapy

Publicado por Jack el 19 de Julio de 2019 a las 21:27


Introducción

Hoy en dia las grandes empresa son dueñas de mucha de nuestra información, en los ultimos años se ha vuelto muy popular el análisis de datos, pudiendo así hacer todo tipo de análisis y estadísticas, sin duda hoy en día es fácil encontrar información sobre cualquier tema, esto gracias al Internet, existen millones de paginas en Internet que pueden proporcionarnos información valiosa, pero no todas las Web nos dan los datos de una forma que sea "fácil" de manejar, para ello hoy enseñare una herramienta que nos ayudara a extraer información de prácticamente cualquier pagina web.

¿Qué es Web scraping?

Según Wikipedia "Web scraping es una técnica utilizada mediante programas de software para extraer información de sitios web. Usualmente, estos programas simulan la navegación de un humano en la World Wide Web ya sea utilizando el protocolo HTTP manualmente, o incrustando un navegador en una aplicación." Articulo completo

Scrapy

Scrapy es un framework(marco de trabajo) que nos permite hacer web scraping de forma automática, al estar basado en Tiwsted es capaz de hacer múltiples consultas de forma simultanea. Se instala de la siguiente forma:

pip install scrapy

cfscrape

Cuando entramos a la pagina de AnimeFLV podemos ver un banner de la pagina cloudflare, la cual una de sus utilidades es brindar una proteccion anti DDoS (ataque de denagación de servicio), por lo que nos puede limitar a la hora de usar Scrapy, por lo que usaremos cfscrape para burlar esta autenticación, mas que burlarla, obtendremos un baypass para no tener que generar uno nuevo cada que se consulte alguna pagina de AnimeFLV. Se instala de la siguiente forma:

Es necesario tener instalado NodeJs>=4.5
Ubuntu
apt-get install nodejs
Instalar cfscrape
pip install cfscrape

Identificando datos

La pagina de AnimeFLV nos provee de un un buscador el cual podemos filtrar por distintos parámetros, ya que quiero extraer la información de todos los animes, el unico parametro que le pondre será ordenarlos por fecha en la que se agrego, puedes verlo en el siguiente enlace.

Usando la función de inspector podemos ver el código HTML de la pagina, lo primero que buscamos es el cuadro que engloba un solo anime, vemos que este esta dentro la etiqueta article el cual tiene como clase Anime,alt y B.


<article class="Anime alt B">
    <a href="/anime/5615/toaru-kagaku-no-accelerator">
        <div class="Image fa-play-circle-o">
            <figure><img src="https://animeflv.net/uploads/animes/covers/3176.jpg" alt="Toaru Kagaku no Accelerator"></figure>
            <span class="Type tv">Anime</span>
        </div>
        <h3 class="Title">Toaru Kagaku no Accelerator</h3>
    </a>
    <div class="Description">
        <div class="Title"><strong>Toaru Kagaku no Accelerator</strong></div>
        <p><span class="Type tv">Anime</span> <span class="Vts fa-star">4.6</span></p>
        <p>Spin-Off de To Aru Majutsu no Index enfocándose en el personaje Accelerator con Last Order, Luego de la derrota de Accelerator en manos de Touma.</p>
        <span class="Flwrs fa-user"><span>611</span> Seguidores</span>
        <a class="Button Vrnmlk" href="/anime/5615/toaru-kagaku-no-accelerator">VER ANIME</a>
    </div>
</article>

Usando xpath podremos acceder a esta parte del código HTML, esto se hará para cada uno de los animes de cada pagina del buscador de anime, pero ademas como hay múltiples paginas de anime es necesario encontrar el "url" de la siguiente pagina, esto para que scrapy sepa a que pagina ir después de haber analizado la actual. Esta url se encuentra en esta etiqueta:

<li><a href="/browse?order=added&amp;page=2" rel="next">»</a></li>

Armando el scraper

Ya que hemos identificado los primeros elementos que hay que obtener, es hora de armar el scraper, nos basaremos del ejemplo que viene en su pagina oficial

Importamos scrapy y cfscrape

import scrapy
from scrapy.crawler import CrawlerProcess
import cfscrape
import logging
logging.getLogger('scrapy').propagate = False #Deshabilitamos los mensajes

Ahora crearemos la clase en base a la documentación de Scrapy, añadiremos un contador, para solo obtener las primeras dos paginas. Ya que scrapy funciona con request, cuando analizamos la primer pagina usamos el método follow para darle continuidad a mas peticiones, es decir leer la información de la siguiente pagina. Vemos que usando xpath podemos obtener la información de las etiquetas previamente identificadas.

class AnimeSpider(scrapy.Spider):
    name = "AnimeSpider"
    base_url = "https://animeflv.net/"
    def start_requests(self):
        url = self.base_url+"browse?order=added" # url inicial (lista anime)
        #Bypass para cloudflare
        token,agent = cfscrape.get_tokens(url=url)
        self.token = token
        self.agent = agent
        self.max = 2
        self.pages = 0
        yield scrapy.Request(url=url,callback=self.parse,
                             cookies=token,
                             headers={'User-Agent': agent})
    def parse(self,response):
        for a in response.xpath('.//article[@class="Anime alt B"]'):
            anime_url = a.xpath(".//a/@href").extract_first()
            print(anime_url)
        # Obtenemos el url de la siguiente pagina
        next_page = response.xpath('//a[@rel="next"]/@href').extract()
        if next_page and self.pages<self.max:
            self.pages+=1
            yield response.follow(self.base_url+next_page[0],callback=self.parse,
                                  cookies=self.token,
                                  headers={'User-Agent': self.agent})

Por ultimo solo mandamos a llamar al script de la siguiente forma y obtenemos:

proc = CrawlerProcess()
proc.crawl(AnimeSpider)
proc.start()

Nos devuelve el siguiente resultado


/anime/5618/bem
/anime/5617/tsuujou-kougeki-ga-zentai-kougeki-de-nikai-kougeki-no-okaasan-wa-suki-desu-ka
/anime/5616/dungeon-ni-deai-wo-motomeru-no-wa-machigatteiru-darou-ka-ii
/anime/5615/toaru-kagaku-no-accelerator
/anime/5614/given
/anime/5613/machikado-mazoku
/anime/5612/isekai-cheat-magician
...
/anime/5548/kono-yo-no-hate-de-koi-wo-utau-shoujo-yuno /anime/5547/cinderella-girls-gekijou-climax-season

Ya que tenemos ahora el link de cada pagina de anime, podemos crear otro método que se llamará AnimeData. Este metodo tendrá muchas lineas de código, pero es básicamente por que son varios elementos los que podemos extraer de cada pagina de anime.

    def AnimeData(self,res):
        data = {}
        data["id"] = int(re.findall("\/[0-9]+\/",res.request.url)[0][1:-1])
        data["rating"] = float(res.xpath('//span[@id="votes_prmd"]/text()').extract_first())
        data["description"] = res.xpath('//div[@class="Description"]/p/text()').extract_first()
        data["img"] = res.xpath('//figure//img/@src').extract_first()
        data["genre"] = [g.xpath('text()').extract_first() for g in res.xpath('//nav[@class="Nvgnrs"]//a')]
        data["type"] = res.xpath('//span[contains(@class,"Type")]/text()').extract_first()
        data["web_state"] = res.xpath('//p[contains(@class,"AnmStts")]//span/text()').extract_first()
        data["votes"] = int(res.xpath('//span[@id="votes_nmbr"]/text()').extract_first())
        script = res.xpath('//script[contains(., "var anime_info")]/text()').extract_first()

Esta información esta bien, pero queremos obtener información que se encuentra en una variable en javascript, en la pagina de AnimeFLV hay un script que contiene la información extra del anime, para obtener los capítulos, el nombre del anime y otro dato para construir el path para obtener los links de las imágenes de cada capitulo. Por lo que usaremos js2xml, la cual es una herramienta que nos permite evaluar un código como string de javascript.
Importamos js2xml (recuerda instalarlo usando: pip install js2xml)

import js2xml
from js2xml.utils.vars import get_vars

Una vez hecho esto podemos usar js2xml para evaluar el script que contiene la variable que queremos, usamos xpath nuevamente para encontrar el script. El script de la pagina de animeFLV se ve así:

<script>
    var anime_info = ["3179","Bem","bem","2019-07-21"];
    var episodes = [[1,52393]];
    var last_seen = 0;
    $(document).ready(function(){
        renderEpisodes(1);
    });
</script>
Lo evaluamos de la siguiente forma:
script = es.xpath('//script[contains(., "var anime_info")]/text()').extract_first() #Obtenemos el script como string
script_vars = get_vars(js2xml.parse(script)) #Parseamos y evaluamos
 #Devuelve lo siguiente
"""{'anime_info': ['3179', 'Bem', 'bem', '2019-07-21'],
 'episodes': [[1, 52393]],
 'last_seen': 0}"""

Por ultimo podemos guardar toda esta información en un archivo, base de datos,etc. En este caso usare elasticsearch para guardar la información.

try:
  res = self.es.index(index="animeflv2",doc_type="anime",body=data,id=data["id"])
except Exception as e:
  with open("animes/%s.json"%data["id"],'w') as f:
    json.dump(data,f)
    f.write(str(e))

Conclusión

Scrapy es un herramienta muy potente para hacer web scraping, ya que como permite multiple conexiones es posible obtener información mucho más rápido, anteriormente intente hacer el mismo script con selenium pero no obtuve buenos resultados.
Cada vez son más paginas las que implementan protección para que sea mas difícil hacer web scraping, por lo que seguramente dependiendo la pagina se necesita hacer algún otro método intermedio, en nuestro caso obtener el bypass de cloudflare. Sin duda saber extraer datos de alguna pagina web es algo que cualquier analista de datos debería aprender, ya que es bastante sencillo con Scrapy.

Apéndice

El script completo queda así

import scrapy
from scrapy.crawler import CrawlerProcess
import cfscrape
from elasticsearch import Elasticsearch
import js2xml
from js2xml.utils.vars import get_vars
import json
import re
class AnimeSpider(scrapy.Spider):
    name = "AnimeSpider"
    base_url = "https://animeflv.net/"
    es = Elasticsearch(hosts="localhost")
    def start_requests(self):
        url = self.base_url+"browse?order=added"
        token,agent = cfscrape.get_tokens(url=url)
        self.token = token
        self.agent = agent
        yield scrapy.Request(url=url,callback=self.parse, cookies=token, headers={'User-Agent': agent})
    def parse(self,response):
        for a in response.xpath('.//article[@class="Anime alt B"]'):
            name = a.xpath(".//a/@href").extract_first()
            yield response.follow(self.base_url+name,callback=self.AnimeData,
                                cookies=self.token,
                                headers={'User-Agent': self.agent})
        next_page = response.xpath('//a[@rel="next"]/@href').extract()
        if next_page:
            yield response.follow(self.base_url+next_page[0],callback=self.parse,
                                  cookies=self.token,
                                  headers={'User-Agent': self.agent})
    def AnimeData(self,res):
        data = {}
        data["id"] = int(re.findall("\/[0-9]+\/",res.request.url)[0][1:-1])
        data["rating"] = float(res.xpath('//span[@id="votes_prmd"]/text()').extract_first())
        data["description"] = res.xpath('//div[@class="Description"]/p/text()').extract_first()
        data["img"] = res.xpath('//figure//img/@src').extract_first()
        data["genre"] = [g.xpath('text()').extract_first() for g in res.xpath('//nav[@class="Nvgnrs"]//a')]
        data["type"] = res.xpath('//span[contains(@class,"Type")]/text()').extract_first()
        data["web_state"] = res.xpath('//p[contains(@class,"AnmStts")]//span/text()').extract_first()
        data["votes"] = int(res.xpath('//span[@id="votes_nmbr"]/text()').extract_first())
        script = res.xpath('//script[contains(., "var anime_info")]/text()').extract_first() #Obtenemos el script como string
        script_vars = get_vars(js2xml.parse(script)) #Parseamos y evaluamos
        anime_info = script_vars["anime_info"]
        data["name"] = anime_info[1]
        episodes_info = sorted(script_vars["episodes"])
        data["episodes_num"] = len(episodes_info)
        episodes = {}
        for e in episodes_info:
            episodes[e[0]] = {"link":"https://animeflv.net/ver/%s/%s-%s"%(e[1],anime_info[2],e[0]),
                             "img": "https://cdn.animeflv.net/screenshots/%s/%s/th_3.jpg"%(anime_info[0],e[0])}
        animeRel = []
        for a in res.xpath('//ul[contains(@class,"ListAnmRel")]//li'):
            aid = re.findall("\/[0-9]+\/",a.xpath('a/@href').extract_first())[0][1:-1]
            atype = a.xpath('text()').extract_first()
            name = a.xpath('a/text()').extract_first()
            animeRel.append({"id":aid,"name":name,"type":atype})
        data["animeRel"] = animeRel
        data["episodes"] = episodes
        try:
            res = self.es.index(index="animeflv2",doc_type="anime",body=data,id=data["id"])
        except Exception as e:
            with open("animes/%s.json"%data["id"],'w') as f:
                json.dump(data,f)
                f.write(str(e))
proc = CrawlerProcess()
proc.crawl(AnimeSpider)
proc.start()