ruby如何抓取html并转换成pdf?

起因

事情的起因是这样的,我是全栈营的会员,可以在全栈营的网站进行学习。但是网站的运维时间到今年2月份就结束了,意味着2月份之后我将不能再在上面继续学习了。

要知道那上面的知识含金量是特别高的,我萌生将全栈营网站的内容抓取下来,制作成PDF供以后学习的想法。

行动

考虑到这件事很有价值,我从本周一就开始琢磨这件事,由于周内白天都在上班,所以只能在晚上空闲时间到google兜兜转转、找找方法。还好,今天周六折腾了一天终于让我找到了方法啦。嘻嘻!

先来看看我的成果吧!截图如下:


Snip20180113_3.png

Snip20180114_4.png

Snip20180114_5.png

方法

在说具体步骤之前,我先说下,大致的思路:

第一步:先用爬虫的方法,将网页内容抓取下来,写入本地文件;
第二步:利用在线网站将抓取的html文件转化成pdf;

一、 抓取网页html、写入文件

脚本初探
首先这里需要先装两个gem:
gem install rest-client 用于发送请求
gem install nokogiri 用于解析html

然后发送请求时,由于全栈营的网站是要求要登录验证身份的,简单的处理方法是,发送请求时带上cookie参数
有同学反应不知道哪找cookie参数,下面简单介绍下:

浏览器右键进入检查(inspect)


Snip20180115_11.png

Snip20180115_12.png

Snip20180115_14.png

下面我们新建一个脚本文件(任意一个.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
如果看到下面画面,表示成功了。

Snip20180113_5.png

分析网页源码,确定抓取部分
让我们先来看看全栈营网页的源码:

# 第一个片段,发现这里的大标题(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正常写入,则表示正常。

Snip20180113_7.png

批量抓取多个页面
现在我们能抓取单个页面了,但是我想要的效果是一下子抓取多个页面,怎么办呢?
让我们看看全栈营网址规律:

Snip20180113_9.png

Snip20180113_10.png

Snip20180113_8.png

稍加比较我们就可以知道,只需改变请求最后的数字就可以批量抓取了。
比如抓取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

执行后画面如下


Snip20180113_11.png

完善脚本
做到这里你的确可以抓取数据了,但是还有三点需要完善:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343