欢迎回到有关构建网络刮板的系列文章。 在本教程中,我将通过一个示例从我自己的播客站点中抓取数据。 我将详细介绍如何提取数据,辅助方法和实用程序方法如何完成其工作以及所有难题之类如何组合在一起。
主题
- 刮我的播客
- 撬
- 刮刀
- 辅助方法
- 写文章
刮我的播客
让我们将到目前为止所学的知识付诸实践。 由于各种原因,请重新设计我的播客。 屏幕早就该过期了。 当我早上醒来时,有些问题使我尖叫。 因此,我决定建立一个由Middleman构建并由GitHub Pages托管的全新静态站点。
在根据自己的需要调整了Middleman博客之后,我在新设计上投入了大量时间。 剩下要做的就是从我的数据库支持的Sinatra应用程序中导入内容,因此我需要抓取现有内容并将其转移到新的静态站点中。
因为我可以依靠我的朋友Nokogiri和Mechanize来为我做这份工作,所以以schmuck方式手工完成此任务不是摆在桌子上,甚至不是问题。 摆在您面前的是一个相当小的刮刮工作,它不太复杂,但是提供了一些有趣的转折,对于那些在网上刮刮新手的工作应该是有教益的。
以下是我的播客的两个屏幕截图。
截图旧播客
截图新播客
让我们分解一下我们想要完成的工作。 我们希望从139个情节(分布在21个分页的索引站点中)中提取以下数据:
- 标题
- 受访者
- 带有主题列表的子标题
- 每个剧集的SoundCloud曲目号
- 日期
- 剧集编号
- 演出笔记中的文字
- 显示笔记中的链接
我们遍历分页,让Mechanize单击情节的每个链接。 在下面的详细信息页面上,我们将在上面找到所需的所有信息。 使用刮的数据,我们要填充的扉页每集降价文件和“身体”。
您可以在下面预览如何将新的降价文件与提取的内容组合在一起。 我认为这将使您对我们未来的范围有个好主意。 这是我们小脚本中的最后一步。 不用担心,我们将更详细地介绍它。
def compose_markdown
def compose_markdown(options={})
<<-HEREDOC
---
title: #{options[:interviewee]}
interviewee: #{options[:interviewee]}
topic_list: #{options[:title]}
tags: #{options[:tags]}
soundcloud_id: #{options[:sc_id]}
date: #{options[:date]}
episode_number: #{options[:episode_number]}
---
#{options[:text]}
HEREDOC
end
我还想添加一些旧站点无法播放的技巧。 对我来说,拥有定制的,全面的标记系统至关重要。 我希望听众拥有一个深入的发现工具。 因此,我需要为每个受访者提供标签,并将子标题也分成标签。 由于仅在第一季就制作了139集,所以我不得不为网站准备一段时间,以使内容的梳理变得更加困难。 带有智能放置推荐的深度标记系统是必经之路。 这使我能够保持网站的轻巧和快速。
让我们看一下抓取网站内容的完整代码。 环顾四周,尝试找出正在发生的一切。 由于我希望您是事物的初学者,因此我避免过多地进行抽象,而在清晰性方面犯了错误。 我做了一些旨在帮助代码清晰的重构,但是当您完成本文时,我还为您留了一些肉。 毕竟,当您不仅仅阅读和玩弄一些代码时,就会进行高质量的学习。
在此过程中,我强烈建议您开始考虑如何改善自己面前的代码。 这将是本文结尾的最后任务。 我的提示:将大型方法分解为较小的方法始终是一个很好的起点。 一旦了解了代码的工作原理,就应该在重构过程中花点时间。
我已经从将一堆方法提取到专注的小型助手中开始了。 您应该可以轻松地应用从我以前的文章中所学到的关于代码气味及其重构的知识 。 如果现在您仍然无法解决这个问题,请不要担心-我们都去过那里。 只要坚持下去,某些时候事情就会开始加快点击速度。
完整代码
require 'Mechanize'
require 'Pry'
require 'date'
# Helper Methods
# (Extraction Methods)
def extract_interviewee(detail_page)
interviewee_selector = '.episode_sub_title span'
detail_page.search(interviewee_selector).text.strip
end
def extract_title(detail_page)
title_selector = ".episode_title"
detail_page.search(title_selector).text.gsub(/[?#]/, '')
end
def extract_soundcloud_id(detail_page)
sc = detail_page.iframes_with(href: /soundcloud.com/).to_s
sc.scan(/\d{3,}/).first
end
def extract_shownotes_text(detail_page)
shownote_selector = "#shownote_container > p"
detail_page.search(shownote_selector)
end
def extract_subtitle(detail_page)
subheader_selector = ".episode_sub_title"
detail_page.search(subheader_selector).text
end
def extract_episode_number(episode_subtitle)
number = /[#]\d*/.match(episode_subtitle)
clean_episode_number(number)
end
# (Utility Methods)
def clean_date(episode_subtitle)
string_date = /[^|]*([,])(.....)/.match(episode_subtitle).to_s
Date.parse(string_date)
end
def build_tags(title, interviewee)
extracted_tags = strip_pipes(title)
"#{interviewee}"+ ", #{extracted_tags}"
end
def strip_pipes(text)
tags = text.tr('|', ',')
tags = tags.gsub(/[@?#&]/, '')
tags.gsub(/[w\/]{2}/, 'with')
end
def clean_episode_number(number)
number.to_s.tr('#', '')
end
def dasherize(text)
text.lstrip.rstrip.tr(' ', '-')
end
def extract_data(detail_page)
interviewee = extract_interviewee(detail_page)
title = extract_title(detail_page)
sc_id = extract_soundcloud_id(detail_page)
text = extract_shownotes_text(detail_page)
episode_subtitle = extract_subtitle(detail_page)
episode_number = extract_episode_number(episode_subtitle)
date = clean_date(episode_subtitle)
tags = build_tags(title, interviewee)
options = {
interviewee: interviewee,
title: title,
sc_id: sc_id,
text: text,
tags: tags,
date: date,
episode_number: episode_number
}
end
def compose_markdown(options={})
<<-HEREDOC
---
title: #{options[:interviewee]}
interviewee: #{options[:interviewee]}
topic_list: #{options[:title]}
tags: #{options[:tags]}
soundcloud_id: #{options[:sc_id]}
date: #{options[:date]}
episode_number: #{options[:episode_number]}
---
#{options[:text]}
HEREDOC
end
def write_page(link)
detail_page = link.click
extracted_data = extract_data(detail_page)
markdown_text = compose_markdown(extracted_data)
date = extracted_data[:date]
interviewee = extracted_data[:interviewee]
episode_number = extracted_data[:episode_number]
File.open("#{date}-#{dasherize(interviewee)}-#{episode_number}.html.erb.md", 'w') { |file| file.write(markdown_text) }
end
def scrape
link_range = 1
agent ||= Mechanize.new
until link_range == 21
page = agent.get("https://between-screens.herokuapp.com/?page=#{link_range}")
link_range += 1
page.links[2..8].map do |link|
write_page(link)
end
end
end
scrape
我们为什么不require "Nokogiri"
? 机械化为我们提供了所有刮削需求。 正如我们在上一篇文章中讨论的那样,Mechanize建立在Nokogiri之上,并且还允许我们提取内容。 但是,在第一篇文章中覆盖该宝石非常重要,因为我们需要在此基础上进行构建。
撬
首先是第一件事。 在我们进入本文的代码之前,我认为有必要向您展示如何有效检查代码的每一步是否按预期工作。 正如您肯定已经注意到的那样,我在组合中添加了另一个工具。 除其他外, Pry
非常方便调试。
如果将Pry.start(binding)
放置在代码中的任何位置,则可以在该点检查应用程序。 您可以在应用程序中的特定位置撬入对象。 这对于一步一步地完成应用程序而不会引起您的麻烦很有帮助。 例如,让我们将其放在我们的write_page
函数之后,并检查link
是否符合我们的期望。
撬
...
def scrape
link_range = 1
agent ||= Mechanize.new
until link_range == 21
page = agent.get("https://between-screens.herokuapp.com/?page=#{link_range}")
link_range += 1
page.links[2..8].map do |link|
write_page(link)
Pry.start(binding)
end
end
end
...
如果您运行脚本,我们将得到类似的信息。
输出量
»$ ruby noko_scraper.rb
321: def scrape
322: link_range = 1
323: agent ||= Mechanize.new
324:
326: until link_range == 21
327: page = agent.get("https://between-screens.herokuapp.com/?page=#{link_range}")
328: link_range += 1
329:
330: page.links[2..8].map do |link|
331: write_page(link)
=> 332: Pry.start(binding)
333: end
334: end
335: end
[1] pry(main)>
然后,当我们请求link
对象时,我们可以在继续其他实现细节之前检查我们是否处在正确的轨道上。
终奌站
[2] pry(main)> link
=> #<Mechanize::Page::Link
"Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values"
"/episodes/139">
看起来像我们需要的。 太好了,我们可以继续。 逐步遍历整个应用程序是确保您不会迷路并且真正了解其工作原理的重要实践。 我将在这里不再详细介绍Pry,因为这样做至少需要我再写一篇完整的文章。 我只能建议将其用作标准IRB外壳的替代产品。 回到我们的主要任务。
刮刀
现在您已经有机会熟悉适当的拼图,我建议我们逐一介绍它们,并在此处和此处阐明一些有趣的观点。 让我们从核心部分开始。
podcast_scraper.rb
...
def write_page(link)
detail_page = link.click
extracted_data = extract_data(detail_page)
markdown_text = compose_markdown(extracted_data)
date = extracted_data[:date]
interviewee = extracted_data[:interviewee]
episode_number = extracted_data[:episode_number]
file_name = "#{date}-#{dasherize(interviewee)}-#{episode_number}.html.erb.md"
File.open(file_name, 'w') { |file| file.write(markdown_text) }
end
def scrape
link_range = 1
agent ||= Mechanize.new
until link_range == 21
page = agent.get("https://between-screens.herokuapp.com/?page=#{link_range}")
link_range += 1
page.links[2..8].map do |link|
write_page(link)
end
end
end
...
scrape
方法会发生什么? 首先,我遍历旧播客中的每个索引页面。 我正在使用Heroku应用中的旧网址,因为新网站已经在betweenscreens.fm上在线了。 我需要翻阅20页的剧集。
我通过link_range
变量对循环定界,该变量随每个循环更新。 分页就像在每个页面的URL中使用此变量一样简单。 简单有效。
def scrape
page = agent.get("https://between-screens.herokuapp.com/?page=#{link_range}")
然后,每当我要抓取一个新页面时又要抓取八个情节时,我就使用page.links
来标识我们要单击的链接,并转到每个情节的详细信息页面。 我决定使用一系列链接( links[2..8]
),因为它在每个页面上都是一致的。 这也是从每个索引页面定位我需要的链接的最简单方法。 无需在这里随意使用CSS选择器。
然后,我们将详细页面的链接输入到write_page
方法。 这是完成大部分工作的地方。 我们单击该链接,然后单击它,然后将其转到详细信息页面,在此我们可以开始提取其数据。 在该页面上,我们找到了为新网站撰写新的降价集所需的所有信息。
def write_page
extracted_data = extract_data(detail_page)
def extract_data
def extract_data(detail_page)
interviewee = extract_interviewee(detail_page)
title = extract_title(detail_page)
sc_id = extract_soundcloud_id(detail_page)
text = extract_shownotes_text(detail_page)
episode_subtitle = extract_subtitle(detail_page)
episode_number = extract_episode_number(episode_subtitle)
date = clean_date(episode_subtitle)
tags = build_tags(title, interviewee)
options = {
interviewee: interviewee,
title: title,
sc_id: sc_id,
text: text,
tags: tags,
date: date,
episode_number: episode_number
}
end
正如您在上面看到的,我们采用了detail_page
并对其应用了一系列提取方法。 我们提取interviewee
, title
, sc_id
, text
, episode_title
和episode_number
。 我重构了一系列专注于这些提取职责的辅助方法。 让我们快速看一下它们:
辅助方法
提取方法
之所以提取这些帮助程序是因为它使我总体上可以使用较小的方法。 封装他们的行为也很重要。 该代码也阅读得更好。 他们中的大多数人都将detail_page
作为参数,并提取一些我们中间人帖子所需的特定数据。
def extract_interviewee(detail_page)
interviewee_selector = '.episode_sub_title span'
detail_page.search(interviewee_selector).text.strip
end
我们在页面上搜索特定的选择器,并获得没有多余空格的文本。
def extract_title(detail_page)
title_selector = ".episode_title"
detail_page.search(title_selector).text.gsub(/[?#]/, '')
end
我们拿了标题并删除了?
和#
因为它们与我们剧集帖子中的头条内容搭配得不好。 以下是有关前端问题的更多信息。
def extract_soundcloud_id(detail_page)
sc = detail_page.iframes_with(href: /soundcloud.com/).to_s
sc.scan(/\d{3,}/).first
end
在这里,我们需要更加努力地提取托管轨道的SoundCloud ID。 首先,我们需要带有soundcloud.com
的href
的Mechanize iframe,并将其设置为扫描字符串...
"[#<Mechanize::Page::Frame\n nil\n \"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/221003494&auto_play=false&hide_related=false&show_comments=false&show_user=true&show_reposts=false&visual=true\">\n]"
然后将正则表达式与其音轨ID的数字匹配-我们的soundcloud_id
"221003494"
。
def extract_shownotes_text(detail_page)
shownote_selector = "#shownote_container > p"
detail_page.search(shownote_selector)
end
提取演出笔记也非常简单。 我们只需要在详细信息页面中查找展示笔记的段落即可。 这里没有惊喜。
def extract_subtitle(detail_page)
subheader_selector = ".episode_sub_title"
detail_page.search(subheader_selector).text
end
副标题也是如此,只是它只是从中干净地提取剧集编号的一种准备。
def extract_episode_number(episode_subtitle)
number = /[#]\d*/.match(episode_subtitle)
clean_episode_number(number)
end
在这里,我们需要另一轮正则表达式。 让我们看看应用正则表达式之前和之后。
Episode_subtitle
" João Ferreira | 12 Minutes | Aug 26, 2015 | Episode #139 "
数
"#139"
再走一步,直到我们得到一个干净的数字。
def clean_episode_number(number)
number.to_s.tr('#', '')
end
我们用井号#
删除该数字。 Voilà,我们也提取了第一集139
。 我建议我们在将所有实用程序组合在一起之前,先看看其他实用程序方法。
实用方法
完成所有提取工作后,我们需要做一些清理工作。 我们已经可以开始准备构成降价的数据了。 例如,我将episode_subtitle
切得episode_subtitle
一些,以得到一个干净的日期,并使用build_tags
方法构建tags
。
def clean_date
def clean_date(episode_subtitle)
string_date = /[^|]*([,])(.....)/.match(episode_subtitle).to_s
Date.parse(string_date)
end
我们运行另一个正则表达式,查找日期如下: " Aug 26, 2015"
。 如您所见,这还不是很有帮助。 从字幕获得的string_date
,我们需要创建一个真实的Date
对象。 否则,创建中间人帖子将毫无用处。
string_date
" Aug 26, 2015"
因此,我们采用该字符串并执行Date.parse
。 结果看起来更有希望。
日期
2015-08-26
def build_tags
def build_tags(title, interviewee)
extracted_tags = strip_pipes(title)
"#{interviewee}"+ ", #{extracted_tags}"
end
这将使用我们在extract_data
方法中建立的title
和interviewee
者,并删除所有管道字符和垃圾内容。 我们用逗号@
,?代替竖线字符?
, #
和&
与一个空字符串,最后要注意with
的缩写。
def strip_pipes
def strip_pipes(text)
tags = text.tr('|', ',')
tags = tags.gsub(/[@?#&]/, '')
tags.gsub(/[w\/]{2}/, 'with')
end
最后,我们还要在标签列表中包括受访者的姓名,并用逗号分隔每个标签。
之前
"Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values"
后
"João Ferreira, Masters Work , Subvisual , Deadlines , Design personality , Design problems , Team , Pushing envelopes , Delightful experiences , Perfecting details , Company values"
这些标签中的每一个最终都将成为指向该主题的帖子集合的链接。 所有这些都发生在extract_data
方法内部。 让我们再看一下我们的位置:
def extract_data
def extract_data(detail_page)
interviewee = extract_interviewee(detail_page)
title = extract_title(detail_page)
sc_id = extract_soundcloud_id(detail_page)
text = extract_shownotes_text(detail_page)
episode_subtitle = extract_subtitle(detail_page)
episode_number = extract_episode_number(episode_subtitle)
date = clean_date(episode_subtitle)
tags = build_tags(title, interviewee)
options = {
interviewee: interviewee,
title: title,
sc_id: sc_id,
text: text,
tags: tags,
date: date,
episode_number: episode_number
}
end
剩下要做的就是返回一个包含我们提取的数据的选项哈希。 我们可以将此哈希值输入compose_markdown
方法,该方法可以将我们的数据准备好写为新站点所需的文件。
写文章
def compose_markdown
def compose_markdown(options={})
<<-HEREDOC
---
title: #{options[:interviewee]}
interviewee: #{options[:interviewee]}
topic_list: #{options[:title]}
tags: #{options[:tags]}
soundcloud_id: #{options[:sc_id]}
date: #{options[:date]}
episode_number: #{options[:episode_number]}
---
#{options[:text]}
HEREDOC
end
为了在我的Middleman网站上发布播客节目,我选择重新调整其博客系统的用途。 我没有创建“纯”博客文章,而是为我的剧集创建了显示注释,这些事件通过iframe显示了SoundCloud托管的剧集。 在索引网站上,我只显示该iframe以及标题和内容。
我需要的工作格式由称为前题的东西组成。 这基本上是我的静态网站的键/值存储。 它取代了我以前的Sinatra网站上的数据库需求。
诸如受访者姓名,日期,SoundCloud曲目ID,情节编号等数据在情节文件顶部位于三个破折号( ---
)之间。 以下是每集的内容-问题,链接,赞助商之类的东西。
前事
---
key: value
key: value
key: value
key: value
---
Episode content goes here.
在compose_markdown
方法中,我使用HEREDOC
在循环播放的每个情节HEREDOC
文件及其HEREDOC
进行组合。 从选项哈希中,我们为该方法提供数据,然后提取在extract_data
helper方法中收集的所有数据。
def compose_markdown
...
<<-HEREDOC
---
title: #{options[:interviewee]}
interviewee: #{options[:interviewee]}
topic_list: #{options[:title]}
tags: #{options[:tags]}
soundcloud_id: #{options[:sc_id]}
date: #{options[:date]}
episode_number: #{options[:episode_number]}
---
#{options[:text]}
HEREDOC
...
这是在那里播出新播客的蓝图。 这就是我们的目的。 也许您想知道这种特定的语法: #{options[:interviewee]}
。 我像往常一样使用字符串进行插值,但是由于我已经在<<-HEREDOC
,因此可以<<-HEREDOC
双引号。
只是为了调整自己的方向,我们仍然处于循环中,在write_page
函数内,每个单击到链接的详细信息页面的链接都带有单个情节的注释。 接下来发生的事情是准备将此蓝图写入文件系统。 换句话说,我们通过提供文件名和组成的markdown_text
创建实际情节。
对于最后一步,我们只需要准备以下内容:日期,受访者姓名和剧集编号。 加上markdown_text
当然是我们刚从compose_markdown
。
def write_page
...
markdown_text = compose_markdown(extracted_data)
date = extracted_data[:date]
interviewee = extracted_data[:interviewee]
episode_number = extracted_data[:episode_number]
file_name = "#{date}-#{dasherize(interviewee)}-#{episode_number}.html.erb.md"
...
然后,我们只需要使用file_name
和markdown_text
并写入文件。
def write_page
...
File.open(file_name, 'w') { |file| file.write(markdown_text) }
...
让我们也分解一下。 对于每个帖子,我都需要一种特定的格式:类似于2016-10-25-Avdi-Grimm-120
。 我想写出以日期开头的文件,其中包括受访者的姓名和剧集编号。
为了匹配格式中间人预计,新的职位,我需要把受访者的名字,并把它通过我的辅助方法dasherize
我的名字,从Avdi Grimm
到Avdi-Grimm
。 没什么魔术,但是值得一看:
def dasherize
def dasherize(text)
text.lstrip.rstrip.tr(' ', '-')
end
它从我们为被访者姓名抓取的文本中删除了空格,并用破折号替换了Avdi和Grimm之间的空格。 文件名的其余部分在字符串本身中"date-interviewee-name-episodenumber"
划线: "date-interviewee-name-episodenumber"
。
def write_page
...
"#{date}-#{dasherize(interviewee)}-#{episode_number}.html.erb.md"
...
由于提取的内容直接来自HTML网站,因此我不能简单地使用.md
或.markdown
作为文件扩展名。 我决定使用.html.erb.md
。 对于以后撰写而无需抓取的情节,我可以.html.erb
部分,只需要.md
。
完成此步骤后, scrape
函数中的循环结束,我们应该有一个看起来像这样的情节:
2014-12-01-Avdi-Grimm-1.html.erb.md
---
title: Avdi Grimm
interviewee: Avdi Grimm
topic_list: What is Rake | Origins | Jim Weirich | Common use cases | Advantages of Rake
tags: Avdi Grimm, What is Rake , Origins , Jim Weirich , Common use cases , Advantages of Rake
soundcloud_id: 179619755
date: 2014-12-01
episode_number: 1
---
Questions:
- What is Rake?
- What can you tell us about the origins of Rake?
- What can you tell us about Jim Weihrich?
- What are the most common use cases for Rake?
- What are the most notable advantages of Rake?
Links:
In">http://www.youtube.com/watch?v=2ZHJSrF52bc">In memory of the great Jim Weirich
Rake">https://github.com/jimweirich/rake">Rake on GitHub
Jim">https://github.com/jimweirich">Jim Weirich on GitHub
Basic">http://www.youtube.com/watch?v=AFPWDzHWjEY">Basic Rake talk by Jim Weirich
Power">http://www.youtube.com/watch?v=KaEqZtulOus">Power Rake talk by Jim Weirich
Learn">http://devblog.avdi.org/2014/04/30/learn-advanced-rake-in-7-episodes/">Learn advanced Rake in 7 episodes - from Avdi Grimm ( free )
Avdi">http://about.avdi.org/">Avdi Grimm
Avdi Grimm’s screencasts: Ruby">http://www.rubytapas.com/">Ruby Tapas
Ruby">http://devchat.tv/ruby-rogues/">Ruby Rogues podcast with Avdi Grimm
Great ebook: Rake">http://www.amazon.com/Rake-Management-Essentials-Andrey-Koleshko/dp/1783280778">Rake Task Management Essentials fromhttps://twitter.com/ka8725"> Andrey Koleshko
当然,此抓取工具将从最后一集开始,一直循环到第一集。 出于演示目的,第1集与任何第1集一样好。 您可以在最前面看到我们提取的数据。
所有这些以前都已锁定在我的Sinatra应用程序的数据库中-剧集编号,日期,受访者姓名等。 现在,我们已经准备好成为我的新静态Middleman网站的一部分。 两个三横线( ---
)下方的所有内容都是显示注释中的文字:主要是问题和链接。
最后的想法
我们完成了。 我的新播客已经启动并正在运行。 我很高兴花时间从头开始重新设计它。 现在发布新剧集要酷得多。 对于用户来说,发现新内容也应该更加顺畅。
正如我之前提到的,这是您应该进入代码编辑器以获得一些乐趣的时候。 采取这段代码,并与之搏斗。 尝试找到使它更简单的方法。 有一些机会可以重构代码。
总体而言,我希望这个小例子能使您对使用新的Web抓取扒条能做什么有个好主意。 当然,您可以解决更复杂的挑战-我相信利用这些技能甚至可以创造许多小型商业机会。
但是,与往常一样,一次只迈出一步,如果事情没有立即点击,也不要感到沮丧。 这不仅对大多数人来说是正常的,而且是可以预料的。 这是旅程的一部分。 刮刮乐!
翻译自: https://code.tutsplus.com/articles/building-your-first-web-scraper-part-3--cms-27599