前端工程師後端之旅(6) - 利用爬蟲取得網站原始商品資料

為了盡可能重現目標網站,我們會需要原來的商品資料,但總不可能一筆一筆複製貼上,這時候我們就會需要爬蟲,爬蟲就是一隻去造訪網站並且分析網站回傳的資料,我們就可以只拿出我們需要部分的資料,透過爬蟲我們可以做很多數據統計的分析,比如:分析某某競爭對手電商的產品趨勢、分析PTT八卦版最常出現的用詞…等。

Python是最常見用於撰寫爬蟲的語言之一,但其實大多數語言都可以用來撰寫爬蟲,因為目前正在學習Rails,而且Ruby還能夠透過’active-record’使用ORM跟資料庫溝通,非常方便,所以今天我會用ruby來示範如何撰寫一隻爬蟲。

Outline - 流程

爬蟲從獲取資料到分析完資料之間可細分為幾個步驟:

  1. 觀察url規律
  2. 模擬送出HTTP Request
  3. 取得網站Response
  4. 分析html內容結構
  5. 取出需要的部分並整理
  6. 與資料庫取得連線
  7. 整理後存入資料庫

使用工具

要進行以上幾個動作,我們會需要幾個套件,分別是:

  • nokogiri:可以讓我們使用與jquery選擇器一樣的方式去選出html內容
  • rest-client: 可以模擬並發出http請求
  • active-record :讓我們可以使用ORM與資料庫溝通
  • pry :讓我可以下中斷並且觀察資料

觀察Url規律

既然是要拿到所有相同類型的資料,只要觀察不同筆資料間url的相異之處就可以了,如

https://www.leisurecosmetics.com/index.php?route=product/product&product_id=75
https://www.leisurecosmetics.com/index.php?route=product/product&product_id=77 

可看出改網站用product_id來識別商品。

模擬送出HTTP Request並取回html網頁結構

寫過前端ajax的人,一定常用axios.get(‘http://example.com/api/xxx’) 去送出get請求拿回資料,其實一般我們進入瀏覽器時,就是在對該網站的server送出Http的get請求,只不過跟ajax不同的是,拿回來的不是api資料,而是網站的畫面,這就是常聽到的Server Side Rendering,圖為進入youtube時所送出的Get請求。
螢幕快照 2019-01-23 上午10.42.09

接下來利用rest-client去模擬http get請求到該目標網站:

html = RestClient.get('https://www.leisurecosmetics.com/index.php?route=product/product&product_id=75')

binding.pry 下中斷並觀察取回的資料
螢幕快照 2019-01-23 上午10.54.54
我們已經把網頁的html拿到手了。

分析Html結構

透過RestClient取回資料後,接下來我們需要Nokigiri去讓我們可以很方便找到我們要的資料,他的使用方式其實就很跟jquery選擇元素的時候一樣,例如,想要選到商品title,我先觀察,商品title上面是否有唯一且具有識別性的元素?

<h2 class="text-primary">BS03 輕透底光 - 面部化妝刷具套裝</h2>

觀察到商品名是用h2包起來並且有 .text-primary 這個class, 我可以先用瀏覽器確認是不是用h2.text-primary可以只選到標題:
螢幕快照 2019-01-23 上午11.09.25

取出需要的部分並整理

太棒了!上面正好是我要的,之後我們只要用Nokogiri以同樣的方式選出該元素就行:

html = RestClient.get('https://www.leisurecosmetics.com/index.php?route=product/product&product_id=75')
doc = Nokogiri::HTML(html)
doc.css('h2.text-primary').text //===> get product title

螢幕快照 2019-01-23 上午11.16.50

用gsub方法把空白字元去掉,ruby的gsub就跟js的replace一樣都是用來代換字串:

doc.css('h2.text-primary').first.text()
        .gsub("\n","")
        .gsub("\t","")

螢幕快照 2019-01-23 上午11.22.05
拿到我要的資料。
也可以用正規表達式來匹配資料:

doc.css('.price h2').first().text.match(/(\w+)\$([\d,]+)/)[2] //get product price 

與資料庫取得連線

Ruby 的active record 其實不一定只能跟著Rails 搭配使用,反過來說,Rails也不一定要使用這套ORM(也有其他的)。這裡我們就把active record單獨拿出來使用,作為與資料庫溝通的橋樑。因為我們上面已經有require了,這邊只要直接使用他的class就可以:

ActiveRecord::Base.establish_connection({
    adapter: 'mysql2',
    encoding: 'utf8',
    database: 'leisure_development',
    username: 'your_db_username',
    password: 'your_db_password',
  })

記得資料庫的資訊要設好,否則會連不上。

存入資料庫

這邊用的資料庫要是跟Rails專案同樣的資料庫,才能夠直接讓專案使用。
最後就是依照資料表格式整理成Hash然後把需要的資料包成Hash,我把整段程式碼包成method,這個method 直接回傳商品資料:

  def parse_page(doc)
        product_name = doc.css('h2.text-primary').first.text()
        .gsub("\n","")
        .gsub("\t","")
        price = doc.css('.price h2').first().text.match(/(\w+)\$([\d,]+)/)[2]
        price_origin  = doc.css('.price .strike').first().text.match(/(\w+)\$([\d,]+)/)[2]
        discount_value = price_origin.to_i - price.to_i
        tab_content = doc.css('.tab-content')
        sku = doc.css('.condition li').first.text
        .gsub("\t","")
        .gsub("\n","").match(/:(\w+)/)[1]

        
        return {
            :name => product_name, 
            :price => price,
            :content=>tab_content,
            :discount_value=>discount_value, 
            :sku=>sku,
            :stock=> 100 , 
        }
    end 

接下來我只要用迴圈一筆一筆去送出request拿回html並且找到我要的商品訊息,就可以放進資料庫了,這裡因為不確定商品有幾筆,所以試試看從第一筆到第一百筆抓抓看,記得嘗試抓資料時在迴圈裡面要做例外處理,避免沒有資料而發生錯誤:

  def run  
        (1..100).each do |product_id|
            puts "Parsing product id : #{product_id} \n"
            product_url = "#{BASE_URL}?route=product/product&product_id=#{product_id}"
            begin
                

                html = RestClient.get(product_url)
                doc = Nokogiri::HTML(html)
                product_data = parse_page(doc)
                Product.create!(product_data)
            rescue => exception
                puts "#{exception.message}"
            end
            sleep 0.1
        end
    end

你可以在這裡看到我的網站實作成果

前端工程師後端之旅(7) - 爬上穹頂的最後一哩路:部署

什麼是部署

所有功能實作完之後,為了讓網站可以被一般使用者看到,我們必須要將程式碼放到一台有網路連接的電腦,只要有跟網路連接,就可以透過IP位址找到網站的所在位置,以前要做到這件事情還必須購買硬體主機等等昂貴的設備。

不過現在已經有許多網路主機商提供雲端虛擬主機的服務,只要花一點相對非常便宜的費用就可以連線到該主機去設定系統並且安裝需要的套件跟程式碼,這個複雜且充滿未知挑戰的最後一步我們稱為「部署」。

Outline

  1. 雲端Server 選擇
  2. 新增使用者
  3. 更新資源庫
  4. 安裝mysql
  5. 安裝rvm
  6. 使用rvm 安裝 並使用ruby
  7. 安裝Rails
  8. Clone 要部署的專案
  9. 安裝 Passenger 跟 nginx
  10. Nginx Server設定
  11. 參考教學

雲端Server 選擇

基本上這類服務都差不多,最基本就是用價錢跟使用時間來做考量,在這裡我使用的是Google的GCP服務,在GCP剛註冊的會員會給予300塊美金,並且在一年內可以使用,非常適合拿來做練習,而且我記得Google每個人最多可以開五個左右的帳號,一組用不夠…可以開兩個啊

對於怎麼建立一台主機,網路上應該有很多類似的教學,看一下教學應該半小時內可以完成,在此不多做說明。裝完主機後,透過GCP介面提供的指令,應該可以很快讓你從電腦連到該機器。

新增使用者

我們通常不會一直用root去操作跟安裝,而是會新增一個使用者後,在給予最高權限,因為每個帳號有自己個別的資料夾,如此一來可以讓系統比較好維護,而最高權限意味著可以做任何事情,當然包括把自己刪掉,所以在使用上要非常小心。

sudo adduser mujing
sudo adduser mujing sudo

切換帳號

su mujing

更新資源庫

我使用的是ubuntu系統,因此新系統安裝完時,最好更新一下系統內預設的套件管理器:

sudo apt-get update 

安裝mysql

這個Rails專案是搭配mysql資料庫開發的,因此必須安裝:

sudo apt install mysql-server

如果是在unbuntu上使用mysql 的話記得多裝個mysql client :

sudo apt-get install libmysqlclient-dev

安裝rvm

rvm 是Ruby 語言版本的管理套件,可以讓你在不同版本之間快速切換,方便管理:

curl -L https://get.rvm.io | bash

使用rvm 安裝 並使用ruby

使用的ruby是2.4.1 ,可以根據需求安裝不同版本

rvm install 2.2.3

設成預設

rvm use 2.4.1 --default

安裝Rails

走了那麼多步驟,終於可以安裝Rails啦

gem install rails --no-ri --no-rdoc 

Clone 要部署的專案

把程式碼從git server 下載到這台主機上,一般會放在github,
如這個專案的程式碼位置 https://github.com/moojing/rails-cart

下載完之後記得進入專案資料夾下 bundle install 指令,安裝專案所需要的套件,並透過rails指令把資料表產出來,記得專案裡面database.yml的資料庫使用者帳號密碼要設定對,否則會連不上Sql Server:

bundle install
rake db:create
rake db:migrate

安裝 Passenger 跟 nginx

Nginx 是一套http Server,可以讓管理者設定許多網頁相關的功能,我們這裡只需要最簡單的監聽某個port,可以讓資料跟外面溝通就行。

Passenger可以想成是Nginx的擴充模組,透過設定,讓你再啟動Nginx時一起把Rails 開起來,透過Nginx監聽某port,來與外界傳送資料跟溝通。

安裝以上兩個工具基本上不會很複雜,Passenger已經寫好教學等你去看了,只要照著操作就能安裝完Nginx跟Passenger:

https://www.phusionpassenger.com/library/install/nginx/install/oss/xenial

Nginx Server設定

安裝完之後開啟nginx 設定檔

sudo nano /etc/nginx/nginx.conf

應該可以看到passenger幫你加入這兩行,把註解打開就行:

# passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
# passenger_ruby /usr/bin/ruby;

由於nginx預設監聽80port,所有Server都是預設80port,平常你上網直接打網址的時候其實就是對到80port,
而且Server的port同時只能被一個程序佔用,所以如果想讓80port空下來,我們必須改掉他預設的設定:

打開下列的檔案,這裡是nginx預設的設定檔

sudo vim /etc/nginx/sites-available/default

把 listen 80 default_server;相關的設定註解掉:

# listen 80 default_server;
# listen [::]:80 default_server ipv6only=on;

在同樣的資料夾新增一個屬於你網站的設定檔:

sudo vim /etc/nginx/sites-available/mysite

放入相關設定:

server {
 listen 80 default_server;
 server_name http://34.80.215.175; //如果沒有domain 就放網址
 passenger_enabled on; //passenger 設定,要有才能串連rails
 passenger_app_env development; // 環境變數
 root /home/rails/rails-cart/public; // 你的rails專案資料夾的public
}

設定軟連結,

sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite

然後再重啟Nginx Server 就大功告成:

sudo service nginx start 

終於成功了!系統部署一般屬於DevOps的工作,當然以上都只用到非常基礎的知識而已,DevOps領域可說是博大精深,學也學不完,這只能算是淺嘗輒止,總之你可以在這裡看到我的實作成果。

參考教學

Luka’s notes - Deploy Redmine to VPS by Rails way in Ubuntu 14.04

How To Deploy a Rails App with Passenger and Nginx on Ubuntu 14.04

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×