Python - SMTP

前情提要

痞客邦最近「新增文章」API,不斷地出現未知的伺服器錯誤,而且從回應的時間來猜測的話,
感覺像是他們自己本身內部的錯誤導致 Time out 之類的。
礙於目前還無法在短時間內處理好一台伺服器建構多個 WordPress,
所以只好先將內容發佈到 Blogger 來解決當前的問題。
而我們只需要使用到發文的功能,所以就可以簡單利用電子郵件來發文;

  1. Blogger 設定的「以電子郵件傳送」
  2. 使用電子郵件張貼填入電子信箱,並開啟「立即發佈電子郵件」
  3. 儲存設定

這樣只要寄信給設定的地址的話,Blogger 便會自動發文。

SMTP

首先在 Python 當中,我們可以使用 smtplib 來發送信件,
下列使用 gmail 為例:

import smtplib
smtpserver = smtplib.SMTP_SSL("smtp.gmail.com",465)
smtpserver.ehlo()
smtpserver.login(username, password)

若你直接執行的話,可能會收到登入失敗的訊息;
由於 Google 在安全性設定上,會阻擋安全性較低的應用程式,
所以若要使用的話,則需要開啟相關設定:
登入與安全性
啟用
這樣的話,就可以順利地登入 gmail。
再來準備信件的內容並寄出:

from email.mime.text import MIMEText
from email.header import Header
from_address = 'Archie.Chang.iOS@gmail.com'
to_address = ['Archie.Chang.iOS@gmail.com']
message = MIMEText(html_body, 'html', 'utf-8')
message['From'] = from_address
message['To'] = to_address[0]
message['Subject'] = subject
smtpserver.sendmail(from_address, to_address, message.as_string())
smtpserver.quit()

小雷

其中,sendmail 的 to_address 為 list 型態;
之前測試的過程中,我將 sendmail 改為

smtpserver.sendmail(from_address, to_address[0], message.asString())

結果導致它不斷地寄同一封信,Blogger 文章就大爆發了。

Firebase Cloud Functions with Database and Messaging

Cloud Functions

Firebase 一直以來便是以 serverless 為主要的方向,
而 Functions 則是一個十分有趣的功能;
它可以自動地隨著事件的回應,如資料庫的異動或是收到 HTTP 的 requests 時,執行程式碼。
其中一個重點是,我們也不需要去管理或是 scale 伺服器。

Triggers

  • Cloud Firestore Triggers
  • Realtime Database Triggers
  • Firebase Authentication Triggers
  • Google Analytics for Firebase Triggers
  • Crashlytics Triggers
  • Cloud Storage Triggers
  • Cloud Pub/Sub Triggers
  • HTTP Triggers

範例

需求

Realtime Database 底下的資料有異動的話,進行推播

直接上 Code
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.updateTodayNews = functions.database.ref('/today/{newsCategory}/updatedTime').onWrite(event => {
    const newsCategory = event.params.newsCategory;
    return admin.database().ref(`/today/${newsCategory}`).once('value').then((snapshot) => {
        if (!snapshot.hasChildren()) {
            return console.log('There are no notification tokens to send to.');
        }
        const title = snapshot.child('title').val();
        const body = snapshot.child('body').val();
        // Notification details.
        const payload = {
          notification: {
            title: title,
            body: body
          }
        };
        if (!snapshot.child('fcmTokens').hasChildren()) {
            return console.log('There are no notification tokens to send to.');
        }
        const tokens = Object.keys(snapshot.child('fcmTokens').val());
        const pushTokens = tokens.filter(key => {
            return snapshot.child('fcmTokens').child(key).val();
        });
        return admin.messaging().sendToDevice(pushTokens, payload)
        .then(function (response) {
            console.log('successfully sent message:', response);
        })
        .catch(function (error) {
            console.log('Error sending message:', error);
        });
    });
});
簡單解說

exports 後面加上這支 function 的名稱,如我們這邊則為 updateTodayNews
而 {newsCategory} 則是變數,以範例來說:
只要 /today/ 底下的任何物件 /updatedTime 又被寫入的話,則會觸發 function。
宣告 admin 是為了使用 Firebase 其他的功能,如這邊是使用到 messaging 和 database。
當 admin.database 得到資料回來後,再使用 admin.messaging().sendToDevice 來進行推播的發送。

Selenium

Selenium

需要在 Python 上操作瀏覽器的話,我會選擇使用 Selenium;
以爬蟲來說,和 requests 不同的地方在於,
像是在讀取網頁時,使用 Selenium 開啟瀏覽器的話可以幫我們處理掉渲染的問題。
這篇主要會以 Selenium 的方式來取得痞客邦的 Access Token。

先搞懂痞客邦的流程

首先,我們先到痞客邦的開發者網頁
PIXNET
或是可以直接到 API Explorer 的畫面
API Explorer
接下來會需要進行登入的動作
Login
登入成功後,便是授權給 API Explorer 權限來取得 Access token
Granted
最後可以在 API Explorer 的畫面上看到 Access Token。
AccessToken

程式方面

我們會使用到 Selenium 的這些元件

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def get_token():
    url = 'https://devtool.pixnet.pro/#/'
    # 使用 Firefox 來開啟網頁
    driver = webdriver.Firefox()
    # 打開 API Explorer 的畫面
    driver.get(url)
    # 進行登入
    driver.get('https://panel.pixnet.cc/login/openid?done=https%3A%2F%2Fdevtool.pixnet.pro&openid=https%3A%2F%2Fmember.pixnet.cc%2Flogin&easy_login=1')
    # 選擇臉書登入
    driver.get('https://panel.pixnet.cc/login/facebooklogin?done=https%3A%2F%2Fdevtool.pixnet.pro&easy_login=1&register_url=%2F%2Fmember.pixnet.cc%2Fregister')
    # 在 Email 和 Password 欄位填上值,並按下登入按鈕
    email = driver.find_element_by_id('email_container')
    password = driver.find_element_by_id('pass')
    login_button = driver.find_element_by_id('loginbutton')
    email.send_keys('Your Facebook email')
    password.send_keys('Your Facebook password')
    login_button.click()
    # 跳轉至授權畫面並按下同意
    driver.get('https://emma.pixnet.cc/oauth2/authorize?redirect_uri=https://devtool.pixnet.pro/index/cb&client_id='Your client id'&response_type=code')
    driver.find_element_by_id('send-Allow').click()
    time.sleep(5)
    # 使用 WebDriverWait 來等候標題出現 EMMA API Explorer(這邊我只寫 EMMA)
    try:
        WebDriverWait(driver, 10).until(EC.title_contains('EMMA'))
    except TimeoutException:
        print('time out')
    finally:
        # 接著 Parse 出 Access token 並且 return。
        soup = BeautifulSoup(driver.page_source, 'html.parser')
        ps = soup.find_all('p', 'form-control-static ng-binding')
        token = ps[0].string.replace(' ', '').replace('n', '')
        driver.close()
        return token

可能遇到的問題

geckodriver

由於我是使用 Ubuntu + Firefox 來執行,而在 Firefox 後續的版本中,
並沒有內建 geckodriver,需要手動安裝到電腦之中。

Crontab

若你是使用 Crontab 來跑的話,由於 Crontab 本身並不會有 output 的輸出,
意思是指跑 Crontab 時,也不會自己跳出 Firefox 的畫面;
會導致 Selenium 發生錯誤。
解決的方式是使用 pyvirtualdisplay 來弄一個虛擬的畫面,
給 Firefox 作為使用。
上面的程式碼則補上:

...
from pyvirtualdisplay import Display
def get_token():
    # 先準備好虛擬的畫面
    display = Display(visible=0, size=(800, 600))
    display.start()
    ...
    # 最後記得關掉畫面
    display.stop()
    return token

大致上是這樣,就可以利用 Selenium 的方式取得痞客邦的 Access Token!

Twitter Follow all users script

追隨所有 Twitter 用戶

之前在弄一個自動發文的機器人,便想要在 Twitter 上同步發送;
而苦無沒有人追隨的情況下,決定先來追隨別人,
看看對方是否會反追隨回來(?
以結果來看,有一些些功用,互相追隨的機率大概 5 % 左右。
PTT 爆報機器人
儘管多數可能也是機器人加機器人好友,不過至少稍稍提升了痞客邦的流量。

Javascript

var FOLLOW_PAUSE = 1250;
var FOLLOW_RAND = 250;
var PAGE_WAIT = 2000;
__cnt__ = 0;
var f;
f = function() {
        var eles;
        var __lcnt__ = 0;
        eles = jQuery('.Grid-cell .not-following .follow-text').each(function(i, ele) {
                    ele = jQuery(ele);
                    if (ele.css('display') != 'block') {
                        console.trace('Already following: ' + i);
                        return;
                    }
                    setTimeout(function() {
                              console.trace("Following " + i + " of " + eles.length);
                            ele.click();
                            if ((eles.length - 1) == i) {
                                console.trace("Scrolling...");
                                window.scrollTo(0, document.body.scrollHeight);
                                setTimeout(function() {
                                    f();
                                }, PAGE_WAIT);
                            }
                    }, __lcnt__++ * FOLLOW_PAUSE + Math.random()*(FOLLOW_RAND) - FOLLOW_RAND/2);
                    __cnt__++;
        });
}
f();

小提點

  • Twitter 單一用戶追隨的上限為 5000 名(不過不知道為什麼,我的機器人追了 5001 位)
  • 一天(24 小時內)追隨的上限為 1000 名

不過有趣的是,我有因為用 Twitter 的 iOS App 一直點追隨被 ban 過一次帳號,
但用 Script 跑卻沒事。
另外 Twitter 的電話認證解鎖竟然是語音,而且是不太清楚的英文;
反覆弄了兩天才解鎖成功,語音內容約是
Twitter@ 6 個數字
「@」又常常和第一個數字含糊在一塊!加上有時間內的驗證次數上限!
總之,現在沒事就好!超!麻!煩!
來自 Stack Overflow

Crontab

Crontab

Crontab 在 Ubuntu 裏頭是預載的,其功能是可以根據時間參數來執行工作排程;
它的格式如下:
* * * * * command to be executed
依序分別是 分鐘[0-59]小時[0-23]日期[1-31]月份[1-12]星期[0-6]command
其中 星期 0 = 天的意思。

符號

「*」:不設限
「,」:分隔時段。例如:30 10,20 * * * command,代表早上十點半和下午八點半執行。
「-」:一段時間範圍。例如:15 9-12 * * * command,代表從九點到十二點的每個 15 分都執行一次。
「/n」:表示每個 n 單位間隔。例如:*/5 * * * * command,代表每隔 5 分鐘執行一次。
* * * * *:每隔一分鐘執行一次。
你也可以使用 @ 來取代五個參數:
@reboot:僅在開機的時候執行一次。
@yearly:一年執行一次,和0 0 1 1 * command效果一樣。
@annually:(和@yearly一樣)
@monthly:一個月執行一次,和0 0 1 * * command效果一樣。
@weekly:一個星期執行一次,和0 0 * * 0 command效果一樣。
@daily:每天執行,和0 0 * * * command效果一樣。
@midnight:(和@daily一樣)
@hourly :每小時執行,和0 * * * * command效果一樣。

輸出

另外,需要設立 command 輸出的地點或方式;
如使用 Postfix 或是直接在 command 後方加上 >> /file_path,

sudo apt-get install postfix

設定完後,可以在下列位置查看 output

sudo tail -f /var/mail/<user>

而如果需要清空 mail 內容的話,
則利用

> /var/mail/<user>

除錯

Ubuntu 的話,cron log 會和 syslog 寫在一塊,使用下列的 function 來區隔:

grep CRON /var/log/syslog

編輯

crontab -l
crontab -e
crontab -r

-l:列出所有的 cron
-e:編輯
-r:移除
並可以使用

/etc/init.d/cron restart

來重新啟用 crontab

Facebook 隱私權條款問題

由於之前上架的 App 有使用到 Facebook 的登入功能,
於是這幾天一直收到這類的信件:
Demo
但是由於本身對這方面可以說是完全沒有涉略,
所以便找找有沒有什麼辦法解決。

Free Privacy Policy

於是找到了這個,回答五種題型便可以得到它們產生的條款,
再找個地方貼上補連結到 Facebook 即可!

Bitnami