2018年9月28日 星期五

[Django] 如何將Django專案與MariaDB、mysql連線

一般Django專案會使用內建的sqlite3當作資料庫
只要安裝相關套件 Django也是可以支援mariaDB、mysql

一、環境建置
首先安裝需要的軟體,mysql和mariadb二擇一即可。
sudo apt-get update
# mysql
sudo apt-get install mysqldb-server libmysqlclient-dev
# mariadb
sudo apt-get install mariadb-server libmariadbclient-dev

sudo apt-get install python-pip python-dev
pip install mysqlclient

登入資料庫
sudo mysql
新增一個資料庫和使用者,並將資料庫的存取權限授權給使用者。
> CREATE DATABASE `djangoDB`;
> CREATE USER 'myuser' IDENTIFIED BY 'mypassword';
> GRANT ALL privileges ON `djangoDB`.* TO 'myuser'@localhost;
> FLUSH PRIVILEGES;'

完成設定後,指令exit即可離開資料庫
用新使用者的身分登入試試看設定是否正確
-p代表要輸入密碼(不加會跳error)
mysql -u myuser -p


二、Django專案設定
請記得剛剛設定的資料庫名稱和使用者帳密
打開settings.py將原本設定的sqlite3修改成mysql
# project/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'djangoDB', # DB名稱
        'USER': 'myuser', # 使用者帳號
        'PASSWORD': 'mypassword', # 使用者密碼
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}

option如果沒有設定的話,執行migrate會出現警告 相關資料請見 https://docs.djangoproject.com/en/2.1/ref/databases/#mysql-sql-mode


三、設定生效
設定完成後就可以執行migrate
python manage.py migrate
這時就可以打開資料庫確認table有沒有建好,就大功告成囉!

[Django] 如何使外來鍵指向多個模型


[本文翻譯自Bhrigu Srivastava-Django: How to add ForeignKey to multiple models]
https://medium.com/@bhrigu/django-how-to-add-foreignkey-to-multiple-models-394596f06e84

如何用一個外來鍵(Foreign Key)和1種以上的模型(Model)進行關聯。

先假設你有兩個模型,Article和Post
class Article(models.Model):
    content = models.CharField(max_length=100)
class Post(models.Model):
    content = models.CharField(max_length=100)

現在,我們新增一個Comment的模型,且Article和Post皆與此關聯。因此,我們要如何在Comment中新增一個FK,並指向上述模型中其一。

能達到這功能的概念即為通用關係(Generic Relation),Django包含一種contenttypes的應用,你的模型和content type模型在應用中的關係,能使一個模型物件與任何你建立的模型物件之間啟用通用關係,

Comment模型的格式如下:
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

class Comment(models.Model):
    comm = models.CharField(max_length=50)
    content_type =   models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object=GenericForeignKey('content_type', 'object_id')
(建立Generic Relations時,固定加上最後三行)

ContentType是一種模型,一個ContentType的物件儲存了關於專案中的模型資訊,提供方法來回傳他們代表的模型或進行搜尋。

因此,我們能將Comment與任何種類的模型建立關聯,儲存在content_type。(content_type回傳模型種類,object_id回傳此模型物件的id)

接下來,我們新增一個Comment的物件(instance),並將此指向一個Articel物件或Post物件。

將Comment物件指向Articel物件:
>>> art = Article.objects.get(id=1)
>>> c = Comment(content_object=art, comm='asdf')
>>> c.save()
>>> c.content_object
<Post: post1>
將Comment物件指向Post物件:
>>> pos= Post.objects.get(id=1)
>>> c= Comment(content_object=pos, comm='new comment')
>>> c.save()
>>> c.content_object
<Post: post1>

逆向Generic Relations
再來,為了取得所有與Article和Post關聯的Comment物件,我們能利用GenericRelations來達成。在Article和Post中定義一個新的欄位,在你的模型中新增一個欄位來逆向搜尋。
from django.contrib.contenttypes.fields import GenericRelation

class Article(models.Model):
    content = models.CharField(max_length=100)
    comments = GenericRelation(Comment)
class Post(models.Model):
    content = models.CharField(max_length=100)
    comments = GenericRelation(Comment)

接下來,指令model_object.comments.all()會取得所有指向這個Comment物件,舉例:art是一個Article物件;post是一個Post物件。
>>> art.comments.all()
<QuerySet [<Comment: asdf>, <Comment: test>]>
>>> pos.comments.all()
<QuerySet [<Comment: new_comment>, <Comment: test2>]>
以上就是「物件指向多個模型」你所有需要知道的基本使用方式。

請參考The contenttypes framework,能知道更多關於contenttypes的應用。

2018年9月27日 星期四

[Python] 用Selenium訂台鐵車票

首先說明Selenium如何抓到指定的欄位
假設有個HTML原始碼
<input id="my_name" type="text" name="fname"/>
代表可以透過id my_name來填入資料,寫成:
input = browser.find_element_by_xpath("//input[@id='my_name']")
input.send_keys('Amy')

就可以在指定的欄位中填入你的名字
但如果剛好網站不像這樣這麼單純的話
還有一個簡單的方法可以取得xpath

檢視原始碼找到元件,點原始碼按右鍵→Copy→Copy XPath
再到程式碼中填上就可以了


訂購車票程式範例
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException, WebDriverException
import time

if __name__ == '__main__':
    try:
        browser = webdriver.Chrome()
        browser.get('http://railway.hinet.net/Foreign/TW/etno1.html')

        person_id = browser.find_element_by_xpath("//input[@id='person_id']")
        person_id.send_keys('A123456789') # 身分證字號

        date = browser.find_element_by_xpath("//select[@id='getin_date']")
        date.send_keys('2018/09/27') # 日期

        from_station = Select(browser.find_element_by_id('from_station'))
        from_station.select_by_value('100') # 起站代碼
        to_station = Select(browser.find_element_by_id("to_station"))
        to_station.select_by_value('149') # 到站代碼

        train_no = browser.find_element_by_xpath("//input[@id='train_no']")
        train_no.send_keys('181') # 車次

        browser.find_element_by_css_selector('button.btn.btn-primary').click()

        rand = browser.find_element_by_xpath("//input[@id='randInput']")
        input_str = input('請輸入圖形中的英數字: ') # [!!] 手動輸入圖形驗證碼
        rand.send_keys(input_str)

        browser.find_element_by_css_selector('button.btn.btn-primary').click()

        print('close brower after 10s...')
        time.sleep(10)
        browser.close()
    except NoSuchElementException as e:
        print(e)
    except WebDriverException as e:
        print(e)
其中幾個參數請根據情形修改
(1) 身分證字號
(2) 乘車日期(格式YYYY/MM/DD)
(3) 起站代碼、到站代碼
(4) 車次 代碼請參考
http://railway.hinet.net/Foreign/TW/etno1.html

圖形驗證碼其實就是防止自動程式搶購
因此這部分需要自己看圖輸入答案才會真正完成訂購
雖然如此能自動填寫資料還是可以節省不少時間
程式未針對資料錯誤進行防呆(像是車次不存在、代碼填錯)
資料正確的情況下都能成功訂購一般車次

如果是普悠瑪的話
因為還要填人數所以不太一樣
需要的話記得要修改一下才適用