Construyendo su primer Web scraper , parte 2
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En este tutorial, aprenderá cómo puede utilizar Mechanize para hacer clic en vínculos, completar formularios y cargar archivos. También aprenderá cómo dividir Mechanize objetos de página y cómo automatizar una búsqueda de Google y guardar sus resultados.
Temas
- Pagina Única vs. Paginación
- Mechanize
- Agente
- Página
- Métodos de Nokogiri
- Enlaces
- Click
- Formularios
Pagina Única vs. Paginación
Hasta ahora hemos pasado algún tiempo averiguando cómo podemos raspar la pantalla de una sola página usando Nokogiri. Esta fue una buena base para avanzar un paso y aprender a extraer contenido de varias páginas.
Después de todo, el problema que estamos tratando de resolver implica obtener el contenido de más de 140 episodios, que es más contenido que puede encajar razonablemente en una sola página web. Tenemos que trabajar con la paginación y la necesidad de averiguar cómo seguir el contenido por el agujero de conejo.
Aquí es donde Nokogiri se detiene y otra joya útil
llamada Mechanize entra en juego.
Mechanize
Mechanize es otra herramienta poderosa que tiene un montón de cosas que ofrecer. Esencialmente te permite automatizar las interacciones con los sitios web de los que necesitas extraer contenido. En ese sentido, me recuerda un poco de algunas funcionalidades que usted podría saber de las pruebas con Capybara.
No
me malinterpretes, jugar con Nokogiri en una sola página es
impresionante en sí mismo, pero para trabajos de extracción de datos más
picantes, necesitamos un poco más de potencia. Esencialmente
podemos rastrear las páginas que necesitamos e interactuar con sus
elementos, imitando y automatizando el comportamiento humano. ¡Cosas
bastante poderosas!
Esta joya le permite seguir vínculos, rellenar campos de formulario y presentar que los datos, incluso tratar con las cookies está en la mesa. Esto significa que también puede imitar el inicio de sesión de los usuarios en sesiones privadas y obtener contenido de un sitio solo al que tiene acceso.
Usted llena el inicio de sesión con sus credenciales y
decirle a Mechanize cómo seguir adelante. Como puedes hacer clic en
vínculos y enviar formularios, hay muy poco que no puedas hacer con esta
herramienta. Tiene una estrecha relación con Nokogiri y también depende
de ello. Aaron Patterson es de nuevo uno de los autores de esta
preciosa joya.
Instanciar un agente de Mechanize
Antes de que podamos empezar a mecanizar las cosas, necesitamos instanciar un agente de Mechanize.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
Este agente agent se usará para buscar una página, similar a lo que hicimos con Nokogiri.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
Lo que sucede aquí es que el agente de Mechanize obtuvo la página de podcast y sus cookies.
Extracción del contenido de la página
Ahora tenemos una página lista para
la extracción. Antes de hacerlo, recomiendo que echemos un vistazo bajo
el capó usando el método de inspección inspect .
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
puts page.inspect |
La salida es bastante sustantiva. Echa un vistazo y verás por ti mismo lo que consiste en un objeto
Mechanize::Page. Aquí puede ver todos los atributos de esa página.Para
mí, este es un objeto muy práctico para rebanar los datos que desea
extraer. Aquí puede ver todos los atributos de esa página.
Para mí, este es un objeto muy práctico para rebanar los datos que desea extraer.
Salida
1 |
#<Mechanize::Page
|
2 |
{url #http://betweenscreens.fm/>} |
3 |
{meta_refresh} |
4 |
{title "Between | Screens "} |
5 |
{iframes
|
6 |
#<Mechanize::Page::Frame
|
7 |
nil |
8 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/290328784&color=ff0000&auto...>
|
9 |
#<Mechanize::Page::Frame
|
10 |
nil
|
11 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/290126141&color=ff0000&auto...> |
12 |
#<Mechanize::Page::Frame
|
13 |
nil |
14 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/289018386&color=ff0000&auto...>
|
15 |
#<Mechanize::Page::Frame
|
16 |
nil
|
17 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/287425105&color=ff0000&auto...> |
18 |
#<Mechanize::Page::Frame
|
19 |
nil |
20 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/287105342&color=ff0000&auto...>
|
21 |
#<Mechanize::Page::Frame
|
22 |
nil
|
23 |
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/221003494&color=ff0000&auto...> |
24 |
#<Mechanize::Page::Frame
|
25 |
nil |
26 |
"">https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/218101809&color=ff0000&auto...} |
27 |
{frames} |
28 |
{links
|
29 |
#<Mechanize::Page::Link "Logo cube" "/">
|
30 |
#https://github.com/vis-kid/betweenscreens">
|
31 |
#<Mechanize::Page::Link "about" "pages/about/">
|
32 |
#<Mechanize::Page::Link "design" "design/">
|
33 |
#<Mechanize::Page::Link "code" "code/">
|
34 |
#<Mechanize::Page::Link "Randy J. Hunt" "episodes/144/">
|
35 |
#<Mechanize::Page::Link "Jason Long" "episodes/143/">
|
36 |
#<Mechanize::Page::Link "David Heinemeier Hansson" "episodes/142/">
|
37 |
#<Mechanize::Page::Link "Zach Holman" "episodes/141/">
|
38 |
#<Mechanize::Page::Link "Joel Glovier" "episodes/140/">
|
39 |
#<Mechanize::Page::Link "João Ferreira" "episodes/139/">
|
40 |
#<Mechanize::Page::Link "Corwin Harrell" "episodes/138/">
|
41 |
#<Mechanize::Page::Link "Older Stuff »" "page/2/">
|
42 |
#<Mechanize::Page::Link "Exercise" "/tags/exercise/">
|
43 |
#<Mechanize::Page::Link "Company benefits" "/tags/company-benefits/">
|
44 |
#<Mechanize::Page::Link "Tmux" "/tags/tmux/">
|
45 |
#<Mechanize::Page::Link "FileTask" "/tags/filetask/">
|
46 |
#<Mechanize::Page::Link "Decision making" "/tags/decision-making/">
|
47 |
#<Mechanize::Page::Link "Favorite feature" "/tags/favorite-feature/">
|
48 |
#<Mechanize::Page::Link "Working out" "/tags/working-out/">
|
49 |
#<Mechanize::Page::Link "Scott Savarie" "/tags/scott-savarie/">
|
50 |
#<Mechanize::Page::Link "Titles" "/tags/titles/">
|
51 |
#<Mechanize::Page::Link "Erik Spiekermann" "/tags/erik-spiekermann/">
|
52 |
#<Mechanize::Page::Link "Newbie mistakes" "/tags/newbie-mistakes/">
|
53 |
#<Mechanize::Page::Link "Playbook" "/tags/playbook/">
|
54 |
#<Mechanize::Page::Link "Delegation" "/tags/delegation/">
|
55 |
#<Mechanize::Page::Link "Heat maps" "/tags/heat-maps/">
|
56 |
#<Mechanize::Page::Link "Europe" "/tags/europe/">
|
57 |
#<Mechanize::Page::Link "Sizing type" "/tags/sizing-type/">
|
58 |
#<Mechanize::Page::Link "Focus" "/tags/focus/">
|
59 |
#<Mechanize::Page::Link "Virtual assistants" "/tags/virtual-assistants/">
|
60 |
#<Mechanize::Page::Link "Writing" "/tags/writing/">
|
61 |
#<Mechanize::Page::Link "Hacking" "/tags/hacking/">
|
62 |
#<Mechanize::Page::Link "Joel Glovier" "/tags/joel-glovier/">
|
63 |
#<Mechanize::Page::Link "Corwin Harrell" "/tags/corwin-harrell/">
|
64 |
#<Mechanize::Page::Link "Mario C. Delgado" "/tags/mario-c-delgado/">
|
65 |
#<Mechanize::Page::Link "Tom Dale" "/tags/tom-dale/">
|
66 |
#<Mechanize::Page::Link "Obie Fernandez" "/tags/obie-fernandez/">
|
67 |
#<Mechanize::Page::Link "Chad Pytel" "/tags/chad-pytel/">
|
68 |
#<Mechanize::Page::Link "Zach Holman" "/tags/zach-holman/">
|
69 |
#<Mechanize::Page::Link "Max Luster" "/tags/max-luster/">
|
70 |
#<Mechanize::Page::Link "Kyle Fiedler" "/tags/kyle-fiedler/">
|
71 |
#<Mechanize::Page::Link "Roberto Machado" "/tags/roberto-machado/">}
|
72 |
{forms}> |
Si desea echar un vistazo a la página HTML en sí, puede marcar en el cuerpo body o métodos de contenido content .
some_scraper.rb
1 |
...
|
2 |
|
3 |
print page.body |
4 |
|
5 |
...
|
Salida
1 |
<!doctype html>
|
2 |
|
3 |
<html>
|
4 |
<head>
|
5 |
<meta charset="utf-8" /> |
6 |
<meta http-equiv='X-UA-Compatible' content='IE=edge;chrome=1' /> |
7 |
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> |
8 |
<meta name="viewport" content="initial-scale=1"> |
9 |
<title>Between | Screens </title> |
10 |
<link rel="alternate" type="application/atom+xml" title="Atom Feed" href="/feed.xml" /> |
11 |
<link href="stylesheets/all-11b45acc.css" rel="stylesheet" /> |
12 |
<script src="javascripts/all-4c20da82.js"></script> |
13 |
</head>
|
14 |
|
15 |
<body>
|
16 |
<header>
|
17 |
<div id="logo"> |
18 |
<a href="/"><img src="images/Between_Screens_Logo_Cube_Up-539d6997.svg" alt="Logo cube" /></a> |
19 |
</div>
|
20 |
<nav class="navigation"> |
21 |
<ul class="nav-list"> |
22 |
fork">https://github.com/vis-kid/betweenscreens">fork! |
23 |
<li><a href="pages/about/">about</a></li> |
24 |
<li><a href="design/">design</a></li> |
25 |
<li><a href="code/">code</a></li> |
26 |
</ul>
|
27 |
</nav>
|
28 |
</header>
|
29 |
|
30 |
<div id="main" role="main"> |
31 |
<div class='posts'> |
32 |
<ul>
|
33 |
<li>
|
34 |
<article class="index-article"> |
35 |
<span class='post-date'>Oct 27 | 2016</span><h2 class='post-title'><a href="episodes/144/">Randy J. Hunt</a></h2> |
36 |
<h3 class='topic-list'>Organizing teams | Diversity | Desires | Pizza rule | Effective over clever | Novel solutions | Straightforwardness | Research | Coffeeshop test | Small changes | Reducing errors | Granular diffs</h3> |
37 |
<div class='soundcloud-player-small'> |
38 |
<iframe width="100%" |
39 |
height="166" |
40 |
scrolling="no" |
41 |
frameborder="no" |
42 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/290328784&color=ff0000&...> |
43 |
</div>
|
44 |
</article>
|
45 |
</li>
|
46 |
|
47 |
<li>
|
48 |
<article class="index-article"> |
49 |
<span class='post-date'>Oct 25 | 2016</span><h2 class='post-title'><a href="episodes/143/">Jason Long</a></h2> |
50 |
<h3 class='topic-list'>Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3> |
51 |
<div class='soundcloud-player-small'> |
52 |
<iframe width="100%" |
53 |
height="166" |
54 |
scrolling="no" |
55 |
frameborder="no" |
56 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/290126141&color=ff0000&...> |
57 |
</div>
|
58 |
</article>
|
59 |
</li>
|
60 |
|
61 |
<li>
|
62 |
<article class="index-article"> |
63 |
<span class='post-date'>Oct 18 | 2016</span><h2 class='post-title'><a href="episodes/142/">David Heinemeier Hansson</a></h2> |
64 |
<h3 class='topic-list'>Rails community | Tone | Technical disagreements | Community policing | Ungratefulness | No assholes allowed | Basecamp | Open source persona | Aspirations | Guarding motivations | Dealing with audiences | Pressure | Honesty | Diverse opinions | Small talk</h3> |
65 |
<div class='soundcloud-player-small'> |
66 |
<iframe width="100%" |
67 |
height="166" |
68 |
scrolling="no" |
69 |
frameborder="no" |
70 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/289018386&color=ff0000&...> |
71 |
</div>
|
72 |
</article>
|
73 |
</li>
|
74 |
|
75 |
<li>
|
76 |
<article class="index-article"> |
77 |
<span class='post-date'>Oct 12 | 2016</span><h2 class='post-title'><a href="episodes/141/">Zach Holman</a></h2> |
78 |
<h3 class='topic-list'>Getting Fired | Taboo | Transparency | Different Perspectives | Timing | Growth Stages | Employment & Dating | Managers | At-will Employment | Tech Industry | Europe | Low hanging Fruits | Performance Improvement Plans | Meeting Goals | Surprise Firings | Firing Fast | Mistakes | Company Culture | Communication</h3> |
79 |
<div class='soundcloud-player-small'> |
80 |
<iframe width="100%" |
81 |
height="166" |
82 |
scrolling="no" |
83 |
frameborder="no" |
84 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/287425105&color=ff0000&...> |
85 |
</div>
|
86 |
</article>
|
87 |
</li>
|
88 |
|
89 |
<li>
|
90 |
<article class="index-article"> |
91 |
<span class='post-date'>Oct 10 | 2016</span><h2 class='post-title'><a href="episodes/140/">Joel Glovier</a></h2> |
92 |
<h3 class='topic-list'>Digital Product Design | Product Design @ GitHub | Loving Design | Order & Chaos | Drawing | Web Design | HospitalRun | Diversity | Startup Culture | Improving Lives | CURE International | Ember | Offline First | Hospital Information System | Designers & Open Source</h3> |
93 |
<div class='soundcloud-player-small'> |
94 |
<iframe width="100%" |
95 |
height="166" |
96 |
scrolling="no" |
97 |
frameborder="no" |
98 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/287105342&color=ff0000&...> |
99 |
</div>
|
100 |
</article>
|
101 |
</li>
|
102 |
|
103 |
<li>
|
104 |
<article class="index-article"> |
105 |
<span class='post-date'>Aug 26 | 2015</span><h2 class='post-title'><a href="episodes/139/">João Ferreira</a></h2> |
106 |
<h3 class='topic-list'>Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values</h3> |
107 |
<div class='soundcloud-player-small'> |
108 |
<iframe width="100%" |
109 |
height="166" |
110 |
scrolling="no" |
111 |
frameborder="no" |
112 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/221003494&color=ff0000&...> |
113 |
</div>
|
114 |
</article>
|
115 |
</li>
|
116 |
|
117 |
<li>
|
118 |
<article class="index-article"> |
119 |
<span class='post-date'>Aug 06 | 2015</span><h2 class='post-title'><a href="episodes/138/">Corwin Harrell</a></h2> |
120 |
<h3 class='topic-list'>Q&A | 01 | University | Graphic design | Design setup | Sublime | Atom | thoughtbot | Working location | Collaboration & pairing | Vim advocates | Daily routine | Standups | Clients | Coffee walks | Investment Fridays |</h3> |
121 |
<div class='soundcloud-player-small'> |
122 |
<iframe width="100%" |
123 |
height="166" |
124 |
scrolling="no" |
125 |
frameborder="no" |
126 |
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/218101809&color=ff0000&...> |
127 |
</div>
|
128 |
</article>
|
129 |
</li>
|
130 |
</ul>
|
131 |
</div>
|
132 |
|
133 |
<section>
|
134 |
<div class='pagination-link'><a href="page/2/">Older Stuff »</a></div> |
135 |
</section>
|
136 |
</div>
|
137 |
|
138 |
<footer>
|
139 |
<div class='footer-tags'> |
140 |
<h3>Random Tags</h3> |
141 |
<ul class='random-tag-list'> |
142 |
<li><a href="/tags/exercise/">Exercise</a></li> |
143 |
<li><a href="/tags/company-benefits/">Company benefits</a></li> |
144 |
<li><a href="/tags/tmux/">Tmux</a></li> |
145 |
<li><a href="/tags/filetask/">FileTask</a></li> |
146 |
<li><a href="/tags/decision-making/">Decision making</a></li> |
147 |
<li><a href="/tags/favorite-feature/">Favorite feature</a></li> |
148 |
<li><a href="/tags/working-out/">Working out</a></li> |
149 |
<li><a href="/tags/scott-savarie/">Scott Savarie</a></li> |
150 |
<li><a href="/tags/titles/">Titles</a></li> |
151 |
<li><a href="/tags/erik-spiekermann/">Erik Spiekermann</a></li> |
152 |
<li><a href="/tags/newbie-mistakes/">Newbie mistakes</a></li> |
153 |
<li><a href="/tags/playbook/">Playbook</a></li> |
154 |
<li><a href="/tags/delegation/">Delegation</a></li> |
155 |
<li><a href="/tags/heat-maps/">Heat maps</a></li> |
156 |
<li><a href="/tags/europe/">Europe</a></li> |
157 |
<li><a href="/tags/sizing-type/">Sizing type</a></li> |
158 |
<li><a href="/tags/focus/">Focus</a></li> |
159 |
<li><a href="/tags/virtual-assistants/">Virtual assistants</a></li> |
160 |
<li><a href="/tags/writing/">Writing</a></li> |
161 |
<li><a href="/tags/hacking/">Hacking</a></li> |
162 |
</ul>
|
163 |
</div>
|
164 |
|
165 |
<div class='recent-posts'> |
166 |
<h3>Random Interviewees</h3> |
167 |
<ul>
|
168 |
<li><a href="/tags/joel-glovier/">Joel Glovier</a></li> |
169 |
<li><a href="/tags/corwin-harrell/">Corwin Harrell</a></li> |
170 |
<li><a href="/tags/mario-c-delgado/">Mario C. Delgado</a></li> |
171 |
<li><a href="/tags/tom-dale/">Tom Dale</a></li> |
172 |
<li><a href="/tags/obie-fernandez/">Obie Fernandez</a></li> |
173 |
<li><a href="/tags/chad-pytel/">Chad Pytel</a></li> |
174 |
<li><a href="/tags/zach-holman/">Zach Holman</a></li> |
175 |
<li><a href="/tags/max-luster/">Max Luster</a></li> |
176 |
<li><a href="/tags/kyle-fiedler/">Kyle Fiedler</a></li> |
177 |
<li><a href="/tags/roberto-machado/">Roberto Machado</a></li> |
178 |
</ul>
|
179 |
</div>
|
180 |
</footer>
|
181 |
</body>
|
182 |
</html>
|
Dado
que este podcast tiene sólo un pequeño número de elementos diferentes
en la página, aquí está el Mechanize::Page que se devuelve de
github.com. Tiene una mayor variedad de contenido para echar un vistazo. Creo que esto es importante para tener una idea.
Salida
1 |
#<Mechanize::Page
|
2 |
{url #https://github.com/>} |
3 |
{meta_refresh} |
4 |
{title "How people build software · GitHub"} |
5 |
{iframes} |
6 |
{frames} |
7 |
{links
|
8 |
#<Mechanize::Page::Link "Skip to content" "#start-of-content">
|
9 |
#https://github.com/">
|
10 |
#<Mechanize::Page::Link "\n Personal\n" "/personal">
|
11 |
#<Mechanize::Page::Link "\n Open source\n" "/open-source">
|
12 |
#<Mechanize::Page::Link "\n Business\n" "/business">
|
13 |
#<Mechanize::Page::Link "\n Explore\n" "/explore">
|
14 |
#<Mechanize::Page::Link "Sign up" "/join?source=header-home">
|
15 |
#<Mechanize::Page::Link "Sign in" "/login">
|
16 |
#<Mechanize::Page::Link "Pricing" "/pricing">
|
17 |
#<Mechanize::Page::Link "Blog" "/blog">
|
18 |
#https://help.github.com">
|
19 |
#https://github.com/search">
|
20 |
#https://help.github.com/terms">
|
21 |
#https://help.github.com/privacy">
|
22 |
#<Mechanize::Page::Link "Sign up for GitHub" "/join?source=button-home">
|
23 |
#<Mechanize::Page::Link
|
24 |
"\n \n \n \n \n A whole new Universe\n \n Learn about the exciting features and announcements revealed at this year's GitHub Universe conference.\n \n \n " |
25 |
"/universe-2016"> |
26 |
#<Mechanize::Page::Link "Individuals " "/personal">
|
27 |
#<Mechanize::Page::Link "Communities " "/open-source">
|
28 |
#<Mechanize::Page::Link "Businesses " "/business">
|
29 |
#<Mechanize::Page::Link "NASA" "//github.com/nasa">
|
30 |
#<Mechanize::Page::Link "Sign up for GitHub" "/join?source=button-home">
|
31 |
#https://github.com/contact">
|
32 |
#https://developer.github.com">
|
33 |
#https://training.github.com">
|
34 |
#https://shop.github.com">
|
35 |
#https://github.com/blog">
|
36 |
#https://github.com/about">
|
37 |
#https://github.com">
|
38 |
#https://github.com/site/terms">
|
39 |
#https://github.com/site/privacy">
|
40 |
#https://github.com/security">
|
41 |
#https://status.github.com/">
|
42 |
#https://help.github.com">
|
43 |
#<Mechanize::Page::Link "Reload" "">
|
44 |
#<Mechanize::Page::Link "Reload" "">}
|
45 |
{forms
|
46 |
#<Mechanize::Form
|
47 |
{name nil} |
48 |
{method "GET"} |
49 |
{action "/search"} |
50 |
{fields
|
51 |
[hidden:0x3feb90f8297c type: hidden name: utf8 value: ✓] |
52 |
[text:0x3feb90f827d8 type: text name: q value: ]} |
53 |
{radiobuttons} |
54 |
{checkboxes} |
55 |
{file_uploads} |
56 |
{buttons}> |
57 |
#<Mechanize::Form
|
58 |
{name nil} |
59 |
{method "POST"} |
60 |
{action "/join"} |
61 |
{fields
|
62 |
[hidden:0x3feb90f7be38 type: hidden name: utf8 value: ✓] |
63 |
[hidden:0x3feb90f7bbb8 type: hidden name: authenticity_token value: vjRATKj7smXreq6Lt02r+MzW+ewWoi+fRzQXPedFAlOZgwzxQ0dZnChirhDfd7vyWZZZBO+ZFydLNedjIEDsrQ==] |
64 |
[text:0x3feb90f7b9d8 type: text name: user[login] value: ] |
65 |
[text:0x3feb90f7b7f8 type: text name: user[email] value: ] |
66 |
[field:0x3feb90f7b654 type: password name: user[password] value: ] |
67 |
[hidden:0x3feb90f7b474 type: hidden name: source value: form-home]} |
68 |
{radiobuttons} |
69 |
{checkboxes} |
70 |
{file_uploads} |
71 |
{buttons [button:0x3feb90f7a038 type: submit name: value: ]}>}> |
De
nuevo al podcast, también puedes ver cosas como codificaciones, el
código de respuesta HTTP, el URI o los encabezados de respuesta.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
puts 'Encodings' |
10 |
puts page.encodings |
11 |
puts 'Repsonse Headers' |
12 |
puts page.response |
13 |
puts 'HTTP response code' |
14 |
puts page.code |
15 |
puts 'URI' |
16 |
puts page.uri |
Salida
1 |
Encodings |
2 |
EUC-JP |
3 |
utf-8 |
4 |
utf-8 |
5 |
|
6 |
Repsonse Headers |
7 |
{"server"=>"GitHub.com", "date"=>"Sat, 29 Oct 2016 17:56:00 GMT", "content-type"=>"text/html; charset=utf-8", "transfer-encoding"=>"chunked", "last-modified"=>"Fri, 28 Oct 2016 01:48:56 GMT", "access-control-allow-origin"=>"*", "expires"=>"Sat, 29 Oct 2016 18:06:00 GMT", "cache-control"=>"max-age=600", "content-encoding"=>"gzip", "x-github-request-id"=>"501C936D:C723:1631523C:5814E2B0"} |
8 |
|
9 |
HTTP response code |
10 |
200 |
11 |
|
12 |
URI |
13 |
http://betweenscreens.fm/ |
Hay muchas más cosas si quieres profundizar. Lo dejaré en eso.
Métodos de Nokogiri
atsearch
Mechanize utiliza Nokogiri para raspar los datos de las páginas. Usted puede aplicar lo que aprendió sobre Nokogiri en el primer artículo y utilizarlo en las páginas de Mechanize también. Eso significa que generalmente usa Mechanize para navegar páginas y métodos de Nokogiri para sus necesidades de scraping.
Por
ejemplo, si desea buscar un solo objeto, puede utilizar at, mientras
que la búsqueda search devuelve todos los objetos que coincidan con un selector
en una página en particular. Para reformular esto, estos métodos
funcionarán tanto en objetos de documento Nokogiri como en objetos de
página Mechanize.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
first_title = page.at('h2.post-title') |
10 |
|
11 |
all_titles = page.search('h2.post-title') |
12 |
|
13 |
all_titles.each do |title| |
14 |
puts title |
15 |
end
|
16 |
|
17 |
puts " * "*33 |
18 |
|
19 |
puts first_title |
Salida
1 |
<h2 class="post-title"><a href="episodes/144/">Randy J. Hunt</a></h2> |
2 |
<h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2> |
3 |
<h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> |
4 |
<h2 class="post-title"><a href="episodes/141/">Zach Holman</a></h2> |
5 |
<h2 class="post-title"><a href="episodes/140/">Joel Glovier</a></h2> |
6 |
<h2 class="post-title"><a href="episodes/139/">João Ferreira</a></h2> |
7 |
<h2 class="post-title"><a href="episodes/138/">Corwin Harrell</a></h2> |
8 |
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * |
9 |
<h2 class="post-title"><a href="episodes/144/">Randy J. Hunt</a></h2> |
Links
linkslink_withlinks_with
También podemos navegar por todo el sitio a nuestro gusto. Probablemente la parte más importante de Mechanize es su capacidad para
permitirle jugar con enlaces. De lo contrario, podría bastante palo con
Nokogiri por su cuenta. Echemos un vistazo a lo que nos devuelven si le
pedimos una página para sus enlaces.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
puts "#{page.links}" |
Salida
1 |
[#<Mechanize::Page::Link "Logo cube" "/"> |
2 |
, #https://github.com/vis-kid/betweenscreens">
|
3 |
, #<Mechanize::Page::Link "about" "pages/about/">
|
4 |
, #<Mechanize::Page::Link "design" "design/">
|
5 |
, #<Mechanize::Page::Link "code" "code/">
|
6 |
, #<Mechanize::Page::Link "Randy J. Hunt" "episodes/144/">
|
7 |
, #<Mechanize::Page::Link "Jason Long" "episodes/143/">
|
8 |
, #<Mechanize::Page::Link "David Heinemeier Hansson" "episodes/142/">
|
9 |
, #<Mechanize::Page::Link "Zach Holman" "episodes/141/">
|
10 |
, #<Mechanize::Page::Link "Joel Glovier" "episodes/140/">
|
11 |
, #<Mechanize::Page::Link "João Ferreira" "episodes/139/">
|
12 |
, #<Mechanize::Page::Link "Corwin Harrell" "episodes/138/">
|
13 |
, #<Mechanize::Page::Link "Older Stuff »" "page/2/">
|
14 |
, #<Mechanize::Page::Link "Exercise" "/tags/exercise/">
|
15 |
, #<Mechanize::Page::Link "Company benefits" "/tags/company-benefits/">
|
16 |
, #<Mechanize::Page::Link "Tmux" "/tags/tmux/">
|
17 |
, #<Mechanize::Page::Link "FileTask" "/tags/filetask/">
|
18 |
, #<Mechanize::Page::Link "Decision making" "/tags/decision-making/">
|
19 |
, #<Mechanize::Page::Link "Favorite feature" "/tags/favorite-feature/">
|
20 |
, #<Mechanize::Page::Link "Working out" "/tags/working-out/">
|
21 |
, #<Mechanize::Page::Link "Scott Savarie" "/tags/scott-savarie/">
|
22 |
, #<Mechanize::Page::Link "Titles" "/tags/titles/">
|
23 |
, #<Mechanize::Page::Link "Erik Spiekermann" "/tags/erik-spiekermann/">
|
24 |
, #<Mechanize::Page::Link "Newbie mistakes" "/tags/newbie-mistakes/">
|
25 |
, #<Mechanize::Page::Link "Playbook" "/tags/playbook/">
|
26 |
, #<Mechanize::Page::Link "Delegation" "/tags/delegation/">
|
27 |
, #<Mechanize::Page::Link "Heat maps" "/tags/heat-maps/">
|
28 |
, #<Mechanize::Page::Link "Europe" "/tags/europe/">
|
29 |
, #<Mechanize::Page::Link "Sizing type" "/tags/sizing-type/">
|
30 |
, #<Mechanize::Page::Link "Focus" "/tags/focus/">
|
31 |
, #<Mechanize::Page::Link "Virtual assistants" "/tags/virtual-assistants/">
|
32 |
, #<Mechanize::Page::Link "Writing" "/tags/writing/">
|
33 |
, #<Mechanize::Page::Link "Hacking" "/tags/hacking/">
|
34 |
, #<Mechanize::Page::Link "Joel Glovier" "/tags/joel-glovier/">
|
35 |
, #<Mechanize::Page::Link "Corwin Harrell" "/tags/corwin-harrell/">
|
36 |
, #<Mechanize::Page::Link "Mario C. Delgado" "/tags/mario-c-delgado/">
|
37 |
, #<Mechanize::Page::Link "Tom Dale" "/tags/tom-dale/">
|
38 |
, #<Mechanize::Page::Link "Obie Fernandez" "/tags/obie-fernandez/">
|
39 |
, #<Mechanize::Page::Link "Chad Pytel" "/tags/chad-pytel/">
|
40 |
, #<Mechanize::Page::Link "Zach Holman" "/tags/zach-holman/">
|
41 |
, #<Mechanize::Page::Link "Max Luster" "/tags/max-luster/">
|
42 |
, #<Mechanize::Page::Link "Kyle Fiedler" "/tags/kyle-fiedler/">
|
43 |
, #<Mechanize::Page::Link "Roberto Machado" "/tags/roberto-machado/">
|
44 |
]
|
Santo Moly, vamos a romper esto. Como no hemos dicho a Mechanize que busque en otra parte, tenemos una serie de enlaces desde esa primera página. Mechanize pasa por esa página en orden descendente y le devuelve esta lista de enlaces de arriba a abajo. He creado una pequeña imagen con punteros verdes a los varios acoplamientos que puedes ver en la salida.
Por cierto, esto ya está
mostrando el resultado final del rediseño para mi podcast. Creo que esta
versión es un poco mejor para fines de demostración. También se echa un
vistazo a cómo se ve el resultado final y por qué necesitaba scrape mi
antiguo sitio de Sinatra.
Captura de pantalla



Como siempre, también podemos extraer sólo el texto de eso.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
page.links.each do |link| |
10 |
puts link.text |
11 |
end
|
Salida
1 |
Logo cube |
2 |
fork! |
3 |
about |
4 |
design |
5 |
code |
6 |
Randy J. Hunt |
7 |
Jason Long |
8 |
David Heinemeier Hansson |
9 |
Zach Holman |
10 |
Joel Glovier |
11 |
João Ferreira |
12 |
Corwin Harrell |
13 |
Older Stuff » |
14 |
Exercise |
15 |
Company benefits |
16 |
Tmux |
17 |
FileTask |
18 |
Decision making |
19 |
Favorite feature |
20 |
Working out |
21 |
Scott Savarie |
22 |
Titles |
23 |
Erik Spiekermann |
24 |
Newbie mistakes |
25 |
Playbook |
26 |
Delegation |
27 |
Heat maps |
28 |
Europe |
29 |
Sizing type
|
30 |
Focus |
31 |
Virtual assistants |
32 |
Writing |
33 |
Hacking |
34 |
Joel Glovier |
35 |
Corwin Harrell |
36 |
Mario C. Delgado |
37 |
Tom Dale |
38 |
Obie Fernandez |
39 |
Chad Pytel |
40 |
Zach Holman |
41 |
Max Luster |
42 |
Kyle Fiedler |
43 |
Roberto Machado |
Obtener todos estos enlaces a granel puede ser muy útil o simplemente tedioso. Por suerte para nosotros, tenemos algunas herramientas en su lugar para afinar lo que necesitamos.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
focus_link = agent.page.links.find { |link| link.text == 'Focus' } |
10 |
|
11 |
puts focus_link |
Salida
1 |
Focus |
¡Auge! ¡Ahora estamos llegando a algún sitio! Podemos hacer zoom en enlaces
específicos como ese. Podemos
segmentar vínculos que coincidan con ciertos criterios -como su texto,
por ejemplo- con una API más agradable como links_with o link_with. Además, si tenemos múltiples vínculos de Focus, podríamos acercar un
número determinado de la página usando corchetes [].
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
focus_link = agent.page.links_with(:text => 'Focus')[2] |
10 |
|
11 |
puts focus_link |
Si
usted no está después del texto del enlace, pero el enlace en sí, sólo
tiene que especificar un href particular para encontrar ese enlace. Mechanize no se interpondrá en su camino. En lugar de texto text, se alimentan los métodos con href
some_scraper.rb
1 |
page = agent.page.link_with(href: '/episodes/95/') |
2 |
|
3 |
page = agent.page.links_with(href: '/episodes/95/') |
Si sólo desea encontrar el primer enlace con el texto deseado, también puede hacer uso de esta sintaxis. Muy práctico y un poco más legible.
some_scraper.rb
1 |
focus_links = agent.page.link_with(:text => 'Focus') |
¿Qué tal seguir a ese tipo y ver lo que se esconde detrás de este enlace de Focus? ¡Vamos a hacer clic click en él!
Click
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
focus_links = agent.page.links.find { |link| link.text == 'Focus' }.click.links |
10 |
|
11 |
puts focus_links |
Esto nos daría otra larga lista de enlaces como antes. Vea lo fácil que era combinar .click.links. Mechanize hace clic en el enlace y sigue la página al nuevo destino. Puesto
que también solicitamos una lista de enlaces, obtendremos todos los
enlaces que Mechanize pueda encontrar en esa nueva página.
Digamos
que tengo dos enlaces de texto del mismo entrevistado-uno que enlaza
con etiquetas y uno con un episodio reciente- y quiero obtener los
enlaces de cada una de estas páginas.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
podcast_url = "http://betweenscreens.fm/" |
6 |
|
7 |
page = agent.get(podcast_url) |
8 |
|
9 |
links = agent.page.links_with(text: "Some interviewee") |
10 |
|
11 |
links.each do |link| |
12 |
puts link.click.links |
13 |
end
|
Esto le daría una lista de enlaces para ambas páginas. Usted
itera sobre cada acoplamiento para el entrevistado, y Mechanize sigue
el acoplamiento chascado y recoge los acoplamientos que encuentra en la
nueva página para usted. A continuación puede encontrar algunos ejemplos donde puede comparar combinaciones para empezar.
some_scraper.rb
1 |
agent.page.links.find { |l| l.text == 'Focus' } |
2 |
agent.page.links.find { |l| l.text == 'Focus' }.click |
3 |
agent.page.link_with(text: 'Focus') |
4 |
agent.page.links_with(text: 'Focus')[0] |
5 |
agent.page.links_with(text: 'Focus')[1].click |
6 |
agent.page.links_with(text: 'Focus')[2].click.links |
7 |
agent.page.link_with(href: '/some-href') |
8 |
agent.page.link_with(href: '/some-href').click |
9 |
agent.page.links_with(href: '/some-href') |
10 |
agent.page.links_with(href: '/some-href').click |
Formularios
submitfield_withcheckbox_withradiobuttons_withfile_uploads
¡Echemos un vistazo a los formularios!
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
google_url = "http://google.com/" |
6 |
|
7 |
page = agent.get(google_url) |
8 |
|
9 |
forms = page.forms |
10 |
|
11 |
puts forms.inspect |
Salida
1 |
[#<Mechanize::Form |
2 |
# Attention!!
|
3 |
{name "f"} |
4 |
# Attention!!
|
5 |
{method "GET"} |
6 |
{action "/search"} |
7 |
{fields
|
8 |
[hidden:0x3fea91d2eb08 type: hidden name: ie value: ISO-8859-1] |
9 |
[hidden:0x3fea91d2e964 type: hidden name: hl value: es] |
10 |
[hidden:0x3fea91d2e7e8 type: hidden name: source value: hp] |
11 |
[hidden:0x3fea91d2e5f4 type: hidden name: biw value: ] |
12 |
[hidden:0x3fea91d2e428 type: hidden name: bih value: ] |
13 |
# Attention!!
|
14 |
[text:0x3fea91d2e248 type: name: q value: ] |
15 |
# Attention!!
|
16 |
[hidden:0x3fea91d2bcb4 type: hidden name: gbv value: 1]} |
17 |
{radiobuttons} |
18 |
{checkboxes} |
19 |
{file_uploads} |
20 |
{buttons
|
21 |
[submit:0x3fea91d2e0f4 type: submit name: btnG value: Buscar con Google] |
22 |
[submit:0x3fea91d2be80 type: submit name: btnI value: Voy a tener suerte]}> |
23 |
]
|
Debido
a que usamos el método de formularios forms, obtenemos una matriz devuelta,
incluso cuando sólo tenemos un formulario devuelto a nosotros. Ahora que sabemos que la forma tiene el nombre "f", podemos usar la forma de versión singular form para afinar en que uno.
1 |
... |
2 |
|
3 |
{name "f"} |
4 |
|
5 |
... |
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
google_url = "http://google.com/" |
6 |
|
7 |
page = agent.get(google_url) |
8 |
|
9 |
search_form = page.form('f') |
10 |
|
11 |
puts search_form.inspect |
Usando el form('f'), señalamos la forma particular con la que queremos trabajar. Como resultado, no obtendremos una matriz devuelta.
Salida
1 |
#<Mechanize::Form
|
2 |
# Attention!!
|
3 |
{name "f"} |
4 |
# Attention!!
|
5 |
{method "GET"} |
6 |
{action "/search"} |
7 |
{fields
|
8 |
[hidden:0x3ffe9ce85ba4 type: hidden name: ie value: ISO-8859-1] |
9 |
[hidden:0x3ffe9ce859d8 type: hidden name: hl value: es] |
10 |
[hidden:0x3ffe9ce857bc type: hidden name: source value: hp] |
11 |
[hidden:0x3ffe9ce85618 type: hidden name: biw value: ] |
12 |
[hidden:0x3ffe9ce853e8 type: hidden name: bih value: ] |
13 |
# Attention!!
|
14 |
[text:0x3ffe9ce851cc type: name: q value: ] |
15 |
# Attention!!
|
16 |
[hidden:0x3ffe9ce84bdc type: hidden name: gbv value: 1]} |
17 |
{radiobuttons} |
18 |
{checkboxes} |
19 |
{file_uploads} |
20 |
{buttons
|
21 |
[submit:0x3ffe9ce85078 type: submit name: btnG value: Buscar con Google] |
22 |
[submit:0x3ffe9ce84e48 type: submit name: btnI value: Voy a tener suerte]}> |
También podemos identificar el nombre del campo de entrada de texto (q).
1 |
... |
2 |
|
3 |
[text:0x3ffe9ce851cc type: name: q value: ] |
4 |
|
5 |
... |
Podemos apuntarla por ese nombre y establecer su valor como atributos Ruby. Todo lo que necesitamos hacer es proporcionarle un nuevo valor. Puede
ver en el ejemplo de salida anterior que está vacía por defecto.
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
google_url = "http://google.com/" |
6 |
|
7 |
page = agent.get(google_url) |
8 |
|
9 |
search_form = page.form('f') |
10 |
search_form.q = 'New Google Search' |
11 |
|
12 |
puts search_form.inspect |
Salida
1 |
#<Mechanize::Form
|
2 |
{name "f"} |
3 |
{method "GET"} |
4 |
{action "/search"} |
5 |
{fields
|
6 |
[hidden:0x3fcb85b6a784 type: hidden name: ie value: ISO-8859-1] |
7 |
[hidden:0x3fcb85b6a57c type: hidden name: hl value: es] |
8 |
[hidden:0x3fcb85b6a3b0 type: hidden name: source value: hp] |
9 |
[hidden:0x3fcb85b6a16c type: hidden name: biw value: ] |
10 |
[hidden:0x3fcb85b67f20 type: hidden name: bih value: ] |
11 |
# Attention!!
|
12 |
[text:0x3fcb85b67d18 type: name: q value: New Google Search] |
13 |
# Attention!!
|
14 |
[hidden:0x3fcb85b67728 type: hidden name: gbv value: 1]} |
15 |
{radiobuttons} |
16 |
{checkboxes} |
17 |
{file_uploads} |
18 |
{buttons
|
19 |
[submit:0x3fcb85b67b9c type: submit name: btnG value: Buscar con Google] |
20 |
[submit:0x3fcb85b67994 type: submit name: btnI value: Voy a tener suerte]}> |
Como se puede observar anteriormente, el valor del campo de texto ha cambiado a Nueva búsqueda de Google New Google Search Ahora solo necesitamos enviar el formulario submit y recopilar los resultados
de la página que Google devuelve. No podría ser más fácil. ¡Busquemos
algo más esta vez!
some_scraper.rb
1 |
require 'mechanize' |
2 |
|
3 |
agent = Mechanize.new |
4 |
|
5 |
google_url = "http://google.com/" |
6 |
page = agent.get(google_url) |
7 |
|
8 |
search_form = page.form('f') |
9 |
search_form.q = 'GitHub TouchFart' |
10 |
|
11 |
page = agent.submit(search_form) |
12 |
|
13 |
pp page.search('h3.r').map(&:text) |
Aquí
identifiqué el encabezado de los resultados de búsqueda usando un
selector de CSS h3.r, mapeado su texto text, y bastante impreso los
resultados. ¿No fue tan difícil, verdad? Ese es un ejemplo fácil, claro, pero ¡piensa
en las infinitas posibilidades que tienes a tu disposición con esto!
Salida
1 |
["GitHub - hungtruong/TouchFart: A fart app for the new Macbook ...", |
2 |
"TouchFart/TouchFart at master · hungtruong/TouchFart · GitHub",
|
3 |
"Commits · hungtruong/TouchFart · GitHub",
|
4 |
"Projects · hungtruong/TouchFart · GitHub",
|
5 |
"Pull Requests · hungtruong/TouchFart · GitHub",
|
6 |
"Issues · hungtruong/TouchFart · GitHub",
|
7 |
"TouchFart/license.txt at master · hungtruong/TouchFart · GitHub",
|
8 |
"Add autoplay attribute to <audio> tag and touchfart (er ... - GitHub",
|
9 |
"Find file - File Finder · GitHub",
|
10 |
"Fart app for the new Macbook Pro's Touch... #3860 on topic touchfart ..."] |
Mechanize tiene diferentes campos de entrada disponibles para jugar. ¡Incluso puede subir archivos!
field_withcheckbox_withradiobuttons_withfile_uploads
También
puede identificar los botones de radio y las casillas de verificación
por su nombre y comprobarlos con—lo has adivinado—check.
some_scraper.rb
1 |
form.radiobuttons_with(:name => 'gender')[3].check |
2 |
|
3 |
form.checkbox_with(:name => 'coder').check |
Las etiquetas de opción ofrecen a los usuarios seleccionar un elemento de una lista desplegable. Nuevamente, los seleccionamos por nombre y seleccionamos el número de opción que deseamos.
some_scraper.rb
1 |
form.field_with(:name => 'countries').options[22].select |
Las
subidas de archivos funcionan de forma similar a la introducción de
texto en los formularios al establecerlo como atributos de Ruby. Identifica el campo de carga y luego especifica la ruta del archivo
(nombre del archivo) que deseas transferir. Suena más complicado de lo
que es. ¡Echemos un vistazo!
some_scraper.rb
1 |
form.file_uploads.first.file_name = "some-path/some-image.jpg" |
Pensamientos finales
Ves, ¡no hay magia después de todo! Ahora está bien equipado para
divertirse por su cuenta. Sin duda hay un poco más para aprender acerca
de Nokogiri y Mechanize,
pero en lugar de pasar demasiado tiempo en aspectos innecesarios, jugar
con él y buscar en más documentación cuando se encuentra con problemas
más allá del alcance de un artículo para principiantes.
Espero que usted
pueda ver cómo maravillosamente simple esta gema es y cuánto energía
ofrece. Como todos sabemos de la cultura popular por ahora, esto también
tiene responsabilidades. Úselo dentro de los marcos legales y cuando no
tenga acceso a una API. Usted probablemente no tendrá un uso frecuente
para estas
herramientas, pero el muchacho no vienen en práctico cuando usted tiene
algunas necesidades que raspan verdaderas delante de usted.
Como se
prometió, en el próximo artículo cubriremos un ejemplo del mundo real
donde rasparé datos de mi sitio de podcast. Lo
extraeré de un antiguo sitio de Sinatra y lo transferiré a mi nuevo
sitio de Middleman que usa archivos .markdown para cada episodio. Extraeremos las fechas, los números de los episodios, los nombres de los
entrevistados, los encabezados, los subtítulos, etc. ¡Te veo allí!



