【Python】CentOS7 安裝 Python3

參考資料 ----
How to install Python 3 on CentOS 7
軟件選集(SCL)軟件庫
How can I make a Red Hat Software Collection persist after a reboot/logout?


想在 CentOS7 使用 Python3 是有點小麻煩的。

RedHat 的套件政策是:主版本週期定為 10 年,主版本釋出後,所包含的套件都會維持主版本不變;例如:CentOS7 內含的 Python 版本是 2.7,在這 10 年間,Python 的版本就會是 2.7.x 的更新。

Python 因為太好用,所以許多套件將 Python 綁在一起,如果想要移除 Python2,會因為套件的相依性而連帶移除了其他原本不想或不能移除的套件!例如:重要的套件管理 yum

但是,10 年是會有滄海滄田的變化的,雖然 RedHat / CentOS 的主要對象是企業,企業重視系統的穩定使用,同時也希望在下一個主版本釋出前,可以對新工具 / 新功能進行 開發 / 測試 / 研究 / 實驗。

RedHat 聽到了企業的聲音,推出了 Software Collections(SCL)SCL 包含了新的套件可供安裝,其中 Python3 的版本,目前更新到 3.6


[root]# vi  /etc/yum.repos.d/CentOS-Base.repo
→ 找到 [extras]
將 enabled 值改成 1
:x 存檔離開


[root]# yum  install  centos-release-scl


偷懶,安裝所有以 rh-python36 字首的套件  :-P

[root]# yum  install  rh-python36*


此時 Python2Python3並存 CentOS7 內的,這時若查詢系統預設執行的 Python 版本

[root]# python  --version

會回應顯示 python 2.7.5



若要以 Python3 執行,則要先下指令:

[root]# scl  enable  rh-python36  bash

再查詢一次

[root]# python  --version

這次就會回應 python 3.6.3

但因為 SCL  原意就是讓我們可以同時使用 Python2Python3,所以當您 登出 或 重新開機,預設要執行的 Python 版本又會回到 2.7


若要每一次登入,預設執行的 Python 版本都是 Python3,方法如下(不過 RedHat 也說這是解套方法,他們還沒想出正解):


建立 rh-python36.sh

[root]# vi /etc/profile.d/rh-python36.sh
 
#!/bin/bash
source  scl_source  enable  rh-python36
 
:x 存檔離開

登出系統,再重新登入

[root]# python  --version
系統就會回應

python 3.6.3



方法二:

Python3 會安裝在下述的路徑

/opt/rh/rh-python36/root/bin/python

在您撰寫的 python 程式的第一行指定要執行的 Python 直譯器

例如,您的程式名為 test.py
 
#!/opt/rh/rh-python36/root/bin/python
import sys
import platform

print(platform.python_version())
 

然後,變更 test.py 的屬性為可執行

[root]# chmod  a+x  test.py

這樣,您的程式會變成可執行檔

執行方式為:切換到 test.py 所在的目錄下,直接執行 test.py

[root]# ./test.py

【Android】AdMob 的測試帳號

參考資料 ----
Sample ad units for Android
Test Ads for iOS


Android
Ad format Sample ad unit ID
Banner ca-app-pub-3940256099942544/6300978111
Interstitial ca-app-pub-3940256099942544/1033173712
Interstitial Video ca-app-pub-3940256099942544/8691691433
Rewarded Video ca-app-pub-3940256099942544/5224354917
Native Advanced ca-app-pub-3940256099942544/2247696110
Native Advanced Video ca-app-pub-3940256099942544/1044960115


iOS
Ad format Sample ad unit ID
Banner ca-app-pub-3940256099942544/2934735716
Interstitial ca-app-pub-3940256099942544/4411468910
Interstitial Video ca-app-pub-3940256099942544/5135589807
Rewarded Video ca-app-pub-3940256099942544/1712485313
Native Advanced ca-app-pub-3940256099942544/3986624511
Native Advanced Video ca-app-pub-3940256099942544/2521693316

【Android Studio】All com.android.support libraries must use the exact same version specification 的解決方法


在我的 app 層級build.gradle
 
...
...

dependencies {
    implementation 'com.android.support:support-v4:27.1.1'

    ...
    ...
}

出現錯誤訊息
 
All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes). Found versions 27.1.1, 26.1.0. Examples include com.android.support:recyclerview-v7:27.1.1 and com.android.support:customtabs:26.1.0 less... (Ctrl+F1)

There are some combinations of libraries, or tools and libraries, that are incompatible, or can lead to bugs. One such incompatibility is compiling with a version of the Android support libraries that is not the latest version (or in particular, a version lower than your targetSdkVersion).
 

關鍵在於
com.android.support:customtabs:26.1.0

com.android.support:support-v4:27.1.1
版本不同

所以再加入 customtabs 並指定版本為 27.1.1 即可,如下:
 
...
...

dependencies {
    implementation 'com.android.support:support-v4:27.1.1'
    implementation 'com.android.support:customtabs:27.1.1'

    ...
    ...
}

【Delphi】Delphi7 透過 HTTPS 傳 LINE 訊息

參考資料 ----
download SSL
LINE Messaging API
Indy 10.6 HTTPS POST example with JSON body


這篇筆記有 2 個重點:
1. Delphi7 以 HTTPS 傳遞 post 資料
2. 透過 LINE Messaging API 傳送訊息


開發環境 ----
WinXP 32bit, Delphi7




申請 LINE 開發者帳號(關於 LINE 的相關流程,是老人家的同事處理的,老人家只是依據同事的說法概略的描述流程,更進一步詳細的做法請上 LINE 官網),這是免費的,不過只能傳給 50 個對象,這 50 個對象可以是 個人群組,因為只是做公司內部消息佈達用,所以我們的做法是建立一個群組(省著點用),再將相關的人拉進群組內,發訊息給這個群組就可以了。



在您的官網放置 line.php,目的是要接收取得所建立的 LINE 群組的 ID,並寫入 line.txt
 
<?php
echo "LINE";
$fp = fopen('line.txt' , "a+");
fwrite($fp , print_r( apache_request_headers() , true));

fwrite($fp , print_r(  file_get_contents('php://input'), true));
fclose($fp);
?>
 
 

隨意發個訊息給群組,然後打開 line.txt,取得 LINE groupID,這是要給 Delphi7 程式用的。


1. 建立專案目錄

2. download SSL 下載  indy_OpenSSL096m.zip (經嘗試錯誤,要這個檔案才能用),解壓後,將 ssleay32.dll, libeay32.dll 放入專案的目錄內

3. FormidHTTP(Indy Clients 頁籤 )idSSLIOHandlerSocket (Indy I/O Handlers 頁籤) 2 個元件,這 2 個是主角;再放 Label(name=lblMessage)、Edit(name=txtMessage)、Memo、Button(name=btnLine),如下圖:




4. idHTTP 元件的 name 屬性改為 idHttpidSSLIOHandlerSocketname 屬性改為 IdSSL

5. IdSSLMethod 屬性改為 sslvSSLv23

6. idHttpIOHandler 屬性指向 IdSSL


 
...
...

procedure TfrmMain.FormCreate(Sender: TObject);
begin
    // 這段的目的是讓程式一啟動先載入 ssleay32.dll, libeay32.dll
    // 但第一次執行會出現錯誤
    // 所以用 try 把錯誤訊息壓下來不顯示
    try
        idHttp.Get('https://隨便打個網址');    // 重點是網址要支援 https
    except

    end;
end;

...
...

procedure TfrmMain.btnLineClick(Sender: TObject);
const
    url = 'https://api.line.me/v2/bot/message/push';
var
    RequestBody: TStream;
    ResponseBody: string;
begin
    idHttp.Request.ContentType := 'application/json';
    idHttp.Request.CustomHeaders.Text := 'Authorization:Bearer  jj/dAZOPVE/blablabla很長的字串=';
    try
        try
            RequestBody := TStringStream.Create('{"to":"群組 ID", "messages": [{"type": "text","text": "'+UTF8Encode(txtMessage.Text)+'"}]}');    // UTF8Encode 很重要, 尤其訊息內有中文
            try
                ResponseBody := idHttp.Post(url, RequestBody);
                memo1.lines.add(ResponseBody);
            finally
                RequestBody.Free;
            end;
        except on E: EIdHTTPProtocolException do
                begin
                    memo1.Lines.Add(e.Message);
                    memo1.Lines.Add(e.ErrorMessage);
                end;
                on E: Exception do
                begin
                    memo1.Lines.Add(e.Message);
                end;
        end;
    finally
        
    end;
end;

 


這個程式有個小問題,就是第一次執行 idHttp 時會跳出錯誤訊息,所以才在 FormCreate

try

except

end;

將錯誤訊息欄截不顯示。如果看倌有更好的解法,也請您分享。謝謝啦~




【Python】CentOS 7 使用 cx_Oracle 連線 Oracle 9i

參考資料 ----
Python Oracle drivers


Python 連線 Oracle 的驅動程式有很多種,因為 CentOS7 預設使用 cx_Oracle,並且其他驅動有涉及到版權規範,因此老人家還是選擇 cx_Oracle


* 以 root 身份登入 CentOS 7


* 到 Oracle 官網下載並安裝  oracle-instantclient11.2-basic.x86_64.rpm,   oracle-instantclient11.2-devel.x86_64.rpm, oracle-instantclient11.2-sqlplus.x86_64.rpm



* 新增並編輯 oracle.sh
[root]# vi  /etc/profile.d/oracle.sh
 
ORACLE_HOME="/usr/lib/oracle/11.2/client64"
PATH=$PATH:$ORACLE_HOME/bin
export PATH
LD_LIBRARY_PATH=$ORACLE_HOME/lib:/usr/lib:/usr/local/lib
export LD_LIBRARY_PATH
 
:x 存檔離開

重開機, 讓 oracle.sh 設定生效


新增 Python 的測試程式 oratest.py
內容如下:
 
# -*- coding: utf-8 -*-
import cx_Oracle
conn = cx_Oracle.connect("帳號/密碼@IP/資料庫名", encoding = "UTF-8", nencoding = "UTF-8")
curs = conn.cursor()
curs.execute('SELECT  *  FROM  資料表名')


# 原生方式是以欄位順序取得欄位值
# 例如 row[0], row[1], ...
# 我們要很明確知道 row[0], row[1] 各代表什麼欄位
# 可讀性不高

# 我們可以繞個彎
# 將資料集的欄位讀出並存入 list
columns = [i[0] for i in curs.description]

# 這裡要注意
# row 對欄位名並沒有大小寫分別
# 但因我們以 curs.description 讀出的欄位名是大寫
for row in curs:  
    print 'field1 = ' + row[columns.index('FIELD1')]

conn.close()
 


附帶看一下 curs.description 的內容:

[('FIELD1', , 2, 2, None, None, 0), ('FIELD2', , 8, 8, None, None, 0), ...]


========================================

要注意的是,若您的 Python 程式要放入排程跑,則要再加環境變數的設定:

[root]# vi /etc/crontab
 
SHELL=/bin/bash

# 設置 oracle 環境變數
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/lib/oracle/11.2/client64/bin
ORACLE_HOME="/usr/lib/oracle/11.2/client64"
LD_LIBRARY_PATH=/usr/lib/oracle/11.2/client64/lib:/usr/lib:/usr/local/lib

...
...

要執行的 python 程式
 
:x 存檔離開


不然您會發現,crontab 到指定時間有執行,資料卻沒存入 Oracle

【jQuery】$.ajax() 在 smarty 內注意事項

當網頁套用 smarty 時,若有用到 $.ajax() 時,要注意程式碼格式
 
$.ajax({    // 左大括號之後必須空白
        url: 'index.php',    // 資料參數從這一列開始
        async: false,
        type: "POST",
        dataType: 'json',
        data: { ...
                ...
                'DATA': JSON.stringify(aData)
            },
        success: function(obj_inf)
                {
                    if(obj_inf.Code>0)
                        {
                            $('#form').submit();
                        }
                    else
                        {
                            alert(obj_inf.ResponseStr);
                        }
                }
        });
 

若緊接著寫會造成 smarty 編譯錯誤
 
// 這種寫法會造成 smarty 錯誤!!
$.ajax({url: 'index.php',
        async: false,
        type: "POST",
        dataType: 'json',
        data: { ...
                ...
                'DATA': JSON.stringify(aData)
            },
        success: function(obj_inf)
                {
                    if(obj_inf.Code>0)
                        {
                            $('#form').submit();
                        }
                    else
                        {
                            alert(obj_inf.ResponseStr);
                        }
                }
        });
 

錯誤訊息
 
Fatal error: Uncaught exception 'SmartyCompilerException' with message 'Syntax Error in template mysmarty.tpl" on line xxx "$.ajax({url: 'index.php'," - Unexpected ": ", expected one of: "}" , " " , 
 


【Android Studio】標記書籤

Ctrl + F11,再按(主鍵盤區的) 數字,

例如:
Ctrl + F11  → 1,這樣就標記了 1 號書籤

當插入線(鍵盤游標) 在程式碼的其他地方,要回到 1 號書籤,則按

Ctrl + 1

【Android Studio】執行時期為 APP 取得權限(授權)

參考資料 ----
Request App Permissions


在執行時期取得權限,是自 Marshmallow(6.0, API23) 加入的功能,為使用者提供更多保護;回想一下,您在安裝 APP 時,會仔細看 APP 要求取得哪些權限嗎?自 Android6.0(含) 起,在 APP 第一次執行時,會提示使用者,您的 APP 需要取得哪些權限,並必須得使用者允許。

所以在 Android6.0(含) 以上的裝置,如果沒有撰寫相關的程式碼,為 APP 執行時期取得權限,嚴重時甚至 APPcrash 掉!

AndroidManifest.xml (宣告 APP 必須取得的權限)
 
<!-- 要求取得相機權限 -->
<uses-permission android:name="android.permission.CAMERA"/>

...
...

<application
        ...
        ...
 

編輯 build.gradle(Module: app)
 
dependencies {
    
    ...
    ...

    implementation 'com.android.support:design:27.1.1'      // Snackbar 需要
}
 


activity_main.xml (記得要給 layout ID,不然 Snackbar 不會顯示)
 
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="開啟相機"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="showCameraPreview"/>

</android.support.constraint.ConstraintLayout>
 

Mainactivity.java
 
public class MainActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback
{
    private static final int PERMISSION_REQUEST_CAMERA = 0;
    private View mLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLayout = findViewById(R.id.main_layout);
    }

    public void showCameraPreview(View v)
    {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
            {
                Snackbar.make(mLayout, "已取得相機權限, 開始預覽", Snackbar.LENGTH_SHORT).show();
                startCamera();
            }
        else
            {
                requestCameraPermission();
            }
    }

    private void startCamera()
    {
        // 開啟相機, 在此寫您自己的程式
    }

    private void requestCameraPermission()
    {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA))
            {
                Snackbar.make(mLayout, "需要取得相機權限", Snackbar.LENGTH_INDEFINITE)
                        .setAction("OK", new View.OnClickListener()
                                            {
                                                @Override
                                                public void onClick(View view)
                                                {
                                                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
                                                }
                                            })
                        .show();
            }
        else
            {
                Snackbar.make(mLayout, "無法開啟相機,可能有其他 APP 正在使用中 或 本 APP 尚未取得授權", Snackbar.LENGTH_SHORT).show();
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
            }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
    {
        if (requestCode == PERMISSION_REQUEST_CAMERA)
        {
            if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                {
                    Snackbar.make(mLayout, "已取得相機權限, 開始預覽", Snackbar.LENGTH_SHORT)
                            .show();
                    startCamera();
                }
            else
                {
                    Snackbar.make(mLayout, "請求相機權限被拒", Snackbar.LENGTH_SHORT)
                            .show();
                }
        }
    }
}
 


APP 安裝到實體裝置後,先關閉不執行,到 Android 的 "設定" 進一步觀察:

安裝在 KitKat4.4(API19),就已經取得權限



安裝在 Oreo8.0(API26),尚未取得權限



執行 APP,點擊 "開啟相機" 鈕,會出現請求授權的畫面



選取 "拒絕",就會跳出 "請求相機權限被拒" 的 Snackbar



再按一次 "開啟相機",這次 Snackbar 會提示您 APP 需要取得相機權限,選取 "OK"



打勾 "不要再詢問",並再次拒絕



這麼一來,就再也不會出現請求授權的畫面了,只能到 "設定" → 應用程式與通知應用程式資訊 → 找到 APP → 點擊 "權限" 去手動開啟授權

或是將 APP 移除再重新安裝了



若是在 請求授權畫面選擇 "允許" 或 手動進 "設定" 開啟授權 後,執行 APP 點擊 "開啟相機" 鈕,就會跳出已取得授權的 Snackbar




相關筆記 ----
Snackbar -- 比 Toast 更多功能的 跳出式訊息

【VisualStudioCode】設定高亮度語法顯示 .tpl 檔

功能表的 檔案
喜好設定
設定
→ 在畫面右半部  使用者設定 輸入下面內容

 
    "files.associations": 
    {
        "*.tpl": "html"
    }
 

再重新啟動 VisualStudioCode 就行了。

【Android Studio】Snackbar -- 比 Toast 更多功能的 跳出式訊息

參考資料 ----
Building and Displaying a Pop-Up Message


Snackbar 是自 API22 新加入的功能,類似 Toast,也是跳出式的訊息視窗 (是不是要取代 Toast? 官方沒有明確表示) 。


SnackbarCoordinatorLayout 內顯示時,還能有額外的特殊功能:

1. 使用者能以 左/右 滑動的方式移除 Snackbar

2. 如果在 Layout 內有諸如 FloatingActionButton 的元件時,當跳出 Snackbar 顯示,Snackbar 會將 FloatingActionButton 往上推;等 Snackbar 退出畫面,FloatingActionButton 會再回到原位。


如果您的畫面是 FramLayout,您甚至可以直接以 CoordinatorLayout 取代,就能充分運用 Snackbar 的功能。


先看傳統的程式使用 Snackbar 的方式



 build.gradle(Module: app)
 
...
...

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'      // 注意:要加上這行
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
 


activity_main.xml(採用 RelativeLayout)
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mv"    注意:Layout 要有 id
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="Button"
        tools:layout_editor_absoluteX="16dp"
        tools:layout_editor_absoluteY="16dp"
        android:onClick="ShowSnackBar"/>

</RelativeLayout>
 


MainActivity.java
 
...
...

public void ShowSnackBar(View v)
{
    Snackbar.make(findViewById(R.id.mv), "顯示 Snackbar", Snackbar.LENGTH_SHORT)
            .show();
}
 





稍微修改 activity_mail.xml
 
<!-- 在 RelativeLayout 外層包上 CoordinatorLayout, 並微調 -->
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    android:id="@+id/mv"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<RelativeLayout
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="Button"
        tools:layout_editor_absoluteX="16dp"
        tools:layout_editor_absoluteY="16dp"
        android:onClick="ShowSnackBar"/>

</RelativeLayout>

</android.support.design.widget.CoordinatorLayout>
 



Snackbar 顯示時,往右滑移除。



Snackbar 還可以加入 文字按鈕,並在使用者點擊時執行指定的動作。
 
public void ShowSnackBar(View v)
{
    Snackbar.make(findViewById(R.id.mv), "顯示 Snackbar", Snackbar.LENGTH_LONG)
            .setAction("點擊執行", new View.OnClickListener()
                                    {
                                        @Override
                                        public void onClick(View v)
                                        {
                                            // 點擊訊息時要執行的動作
                                            Toast.makeText(MainActivity.this, "點擊了 Snackbar", Toast.LENGTH_SHORT).show();
                                        }
                                    })
            .setActionTextColor(Color.YELLOW)    // 還可以指定 文字顏色
            .show();
}