起因
事情的起因是这样的,我是全栈营的会员,可以在全栈营的网站进行学习。但是网站的运维时间到今年2月份就结束了,意味着2月份之后我将不能再在上面继续学习了。
要知道那上面的知识含金量是特别高的,我萌生将全栈营网站的内容抓取下来,制作成PDF供以后学习的想法。
行动
考虑到这件事很有价值,我从本周一就开始琢磨这件事,由于周内白天都在上班,所以只能在晚上空闲时间到google兜兜转转、找找方法。还好,今天周六折腾了一天终于让我找到了方法啦。嘻嘻!
先来看看我的成果吧!截图如下:
方法
在说具体步骤之前,我先说下,大致的思路:
第一步:先用爬虫的方法,将网页内容抓取下来,写入本地文件;
第二步:利用在线网站将抓取的html文件转化成pdf;
一、 抓取网页html、写入文件
脚本初探
首先这里需要先装两个gem:
gem install rest-client
用于发送请求
gem install nokogiri
用于解析html
然后发送请求时,由于全栈营的网站是要求要登录验证身份的,简单的处理方法是,发送请求时带上cookie参数
有同学反应不知道哪找cookie参数,下面简单介绍下:
浏览器右键进入检查(inspect)
下面我们新建一个脚本文件(任意一个.rb
文件),试试看能不能成功抓取数据。
# test.rb
require 'rest-client'
require 'nokogiri'
url = 'https://fullstack.qzy.camp/posts/860' # 随意测试一个url
cookie = '_quanzhan_session=你复制的cookie值放这里' # 你在登录全栈营时,浏览器中cookie值
response = RestClient.get url, {Cookie: cookie} # 必须传cookie参数(如果需要登录)
doc = Nokogiri::HTML.parse(response.body) # 解析
puts doc
终端执行:ruby test.rb
如果看到下面画面,表示成功了。
分析网页源码,确定抓取部分
让我们先来看看全栈营网页的源码:
# 第一个片段,发现这里的大标题(Web API 设计实作)
<div class="left-block hidden-xs">
<h1><a href="/courses/38/syllabus">Web API 设计实作</a></h1>
</div>
# 第二个片段,发现这里有小标题(所属章节:7. Jbuilder 用法)
<div class="des-text">
<h4>所属章节:7. Jbuilder 用法</h4>
<p><p>本章预计学习时间: 1小时半以内</p></p>
<p><p>再学习5节就可以完成本章了</p></p>
</div>
# 第三个片段,主体内容
<div class="post group">
<div class="post-content markdown">
<p>新增 <code>app/views/api/v1/trains/show.json.jbuilder</code> 档案,这就是 JBuilder 样板,用来定义 JSON 长什么样子:</p>
....略
<p>用浏览器浏览 <code>http://localhost:3000/api/v1/trains/0822</code> 确认正常。</p>
</div>
</div>
好了确定了要抓取的主要内容就可以进入下一步,完善脚本,并写入文件
require 'rest-client'
require 'nokogiri'
url = 'https://fullstack.qzy.camp/posts/860' # 随意测试一个url
cookie = '_quanzhan_session=你复制的cookie值放这里' # 你在登录全栈营时,浏览器中cookie值
response = RestClient.get url, {Cookie: cookie} # 必须传cookie参数(如果需要登录)
doc = Nokogiri::HTML.parse(response.body) # 解析
+ # 分解html
+ them = doc.css("h1")[0].to_s # 大标题
+ chapt = doc.css(".des-text h4").to_s # 小标题
+ post = doc.css(".post").to_s # 主体内容
+ content = them + chapt + post # 组合
+ # 文件写入
+ file = File.new("page.erb", 'w')
+ file.write(content)
- puts doc
终端运行:ruby test.rb
如果你的本地文件page.erb中有html正常写入,则表示正常。
批量抓取多个页面
现在我们能抓取单个页面了,但是我想要的效果是一下子抓取多个页面,怎么办呢?
让我们看看全栈营网址规律:
稍加比较我们就可以知道,只需改变请求最后的数字就可以批量抓取了。
比如抓取web api这部分内容,代码如下:
require 'rest-client'
require 'nokogiri'
basic_url = 'https://fullstack.qzy.camp/posts/' # 基础url
cookie = '_quanzhan_session=你复制的cookie值放这里'
(825..865).each do |p|
url = basic_url + p.to_s
response = RestClient.get url, {Cookie: cookie} # 必须传cookie参数(如果需要登录)
doc = Nokogiri::HTML.parse(response.body)
# 分解html
them = doc.css("h1")[0].to_s # 大标题
chapt = doc.css(".des-text h4").to_s # 小标题
post = doc.css(".post").to_s # 主体内容
content = them + chapt + post # 组合
# 文件写入
file = File.new("page.erb", 'w')
file.write(content)
puts "#{url}------已成功抓取"
end
执行后画面如下
完善脚本
做到这里你的确可以抓取数据了,但是还有三点需要完善:
1、 页面现在是没有带样式的,需要美化(定义css)
2、 前面通过输入一个连续的post编号的方式,批量获取数据的方式,并非万能的(有的地方post 号是不连续的),完善为抓取页面下页的链接
3、 导出的pdf让目录正常
require 'rest-client'
require 'nokogiri'
style = "<style>.frame {
margin-left: 30px;
margin-right: 30px;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
}
.view-count {
float: right;
margin-top: -54px;
color: #9B9B9B;
}
.markdown h2, .markdown h3, .markdown h4 {
text-align: left;
font-weight: 800;
font-size: 16px !important;
line-height: 100%;
margin: 0;
color: #555;
margin-top: 16px;
margin-bottom: 16px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.markdown .figure-code figcaption {
background-color: #e6e6e6;
font: 100%/2.25 Monaco, Menlo, Consolas, 'Courier New', monospace;
text-indent: 10.5px;
-moz-border-radius: 0.25em 0.25em 0 0;
-webkit-border-radius: 0.25em;
border-radius: 0.25em 0.25em 0 0;
-moz-box-shadow: inset 0 0 0 1px #d9d9d9;
-webkit-box-shadow: inset 0 0 0 1px #d9d9d9;
box-shadow: inset 0 0 0 1px #d9d9d9;
}
.markdown {
position: relative;
line-height: 1.8em;
font-size: 14px;
text-overflow: ellipsis;
word-wrap: break-word;
font-family: 'PT Serif', Georgia, Times, 'Times New Roman', serif !important;
}
.markdown ol li, .markdown ul li {
line-height: 1.6em;
padding: 2px 0;
color: #333;
font-size: 16px;
}
.markdown .figure-code {
margin: 20px 0;
}
.post-content {
padding-top: 5px;
padding-bottom: 5px;
}
.markdown code {
background-color: #ececec;
color: #d14;
font-size: 85%;
text-shadow: 0 1px 0 rgba(255,255,255,0.9);
border: 1px solid #d9d9d9;
padding: 0.15em 0.3em;
}
div {
display: block;
}
.markdown figure.code pre {
background-color: #ffffcc !important;
}
.code .gi {
color: #859900;
line-height: 1.2em;
}
.code .err {
color: #93A1A1;
}
.markdown a:link, .markdown a:visited {
color: #0069D6 !important;
text-decoration: none !important;
}
.markdown p {
font-size: 16px;
line-height: 1.5em;
}
.markdown blockquote {
margin-left: 0 !important;
margin-right: 0 !important;
padding: 12px;
border-left: 5px solid #50AF51;
background-color: #F3F8F3;
clear: both;
display: block;
}
.markdown blockquote>*:first-child {
margin-top: 0 !important;
}
.markdown blockquote>*:last-child {
margin-bottom: 0 !important;
}
.markdown blockquote p {
color: #222;
}
* {
outline: none !important;
}
a:active, a:hover, a:link, a:visited {
text-decoration: none;
}
pre {
margin: 0;
}
.markdown img {
vertical-align: top;
max-width:100%;
height:auto;
}
h1 a {
color: #071A52;
}
h4 {
color: #734488;
}
hr {
border-color: #DEDEDE;
border-width: 0.8px;
margin-bottom: auto;
}
.end {
height: 400px;
}
.end img {
clear: both;
display: block;
margin:auto;
margin-top: -70px;
}
.end p {
margin-left: 300px;
margin-top: -100px;
color: #FF9D76;
}
</style>"
print "-----------请输入一个开始页面的post编号:"
# 获取开始页面的编码 和文件名
start_page = gets.chop
print "--------------------请输入保存的文件名:"
file_name = gets.chop
# 结束画面
page_end = "<div class='end'>
<img src='https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1515845318684&di=399b355dd05f4eeb015b087061656115&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253D580%2Fsign%3Dc775b978013b5bb5bed720f606d2d523%2F248ea813632762d018421c6ca2ec08fa503dc64c.jpg'>
<p>又学完一篇好开森!</p>
</div>"
# 写入样式
file = File.new("#{file_name}.html", 'w')
file.write(style)
# 基础链接
basic_url = 'https://fullstack.qzy.camp'
url = basic_url + '/posts/' + start_page
cookie = '_quanzhan_session=你复制的cookie值放这里'
puts "---------------------------已开始抓取数据:请耐心等候"
while (url != 'end')
# 请求数据
response = RestClient.get url, {Cookie: cookie}
doc = Nokogiri::HTML.parse(response.body)
# 当post存在时,解析
if !doc.css(".post").to_s.empty?
title = doc.css(".post-title-h1.markdown h1").to_s
chapt = doc.css(".des-text h4").to_s + '<hr>'
post = doc.css(".post").to_s + page_end
content = title + chapt + post
page = "<div class='frame'>#{content}</div>"
# 写入本page数据
file.write(page)
puts "#{url}----------中数据已成功抓取"
# 计算下一个请求url
next_relative_path = doc.css("li.next a")[0]['href'].to_s
# 如果解析出来是 /dashboard 则代表本课结束
url = next_relative_path == '/dashboard' ? 'end' : (basic_url + next_relative_path)
end
end
puts "---------------------------本课数据已全部抓取 😊"
二、 将抓取html转化成pdf
这里利用 在线转换工具
把抓取的html文件上传到在线工具,转换成pdf后下载即可。
PS: 最后如果觉得简书显示代码的方式不太友好,欢迎访问我的博客:
http://dmy-blog.logdown.com/posts/4739981-how-does-ruby-crawl-web-content-and-make-it-into-pdf