i++

プログラム系のメモ書きなど

golang : gmail でメールを送信する

#golang メールを送信するには の通りにやれば良い。

ただし、2段階認証を有効にしている場合には <password> の部分にアプリパスワードを使う必要がある。アプリパスワードの発行 はリンク先から。アプリパスワードについての説明は アプリ パスワードでログイン 辺りを参照。

また、Subject などを含めるには smtp.SendMailmsg に指定する。これについては smtp - The Go Programming Language を参照。

func sendMail(subject, message string) error {
    auth := smtp.PlainAuth(
        "",
        "<sender>@gmail.com", // 送信に使うアカウント
        "<password>", // アカウントのパスワード or アプリケーションパスワード
        "smtp.gmail.com",
    )

    return smtp.SendMail(
        "smtp.gmail.com:587",
        auth,
        "<sender>@gmail.com", // 送信元
        []string{"<recipient>@gmail.com"}, // 送信先
        []byte(
            "To: <recipient>@gmail.com\r\n" +
            "Subject:" + subject + "\r\n" +
            "\r\n" +
            message),
    )
}

golang : 見た目が同じ長さの文字列を作成する(等幅フォント)

等幅フォントで表示する時に同じ長さに見えるような文字列を生成する。

// "github.com/mattn/go-runewidth" を使用。

func makeFixedWidthString(str string, length int) string {
    var buffer bytes.Buffer
    l := 0
    for _, c := range str {
        cl := runewidth.RuneWidth(c)
        if l + cl > length {
            break
        }
        buffer.WriteRune(c)
        l += cl
    }
    for i := 0; i < length - l; i++ {
        buffer.WriteRune(' ')
    }
    return buffer.String()
}

以下に関連しているものの、日本語と英語が交じると見た目上の長さがずれてしまったので、別途対応。あまりきちんと調べていないので、対応出来ていないケースがあるかも。

increment.hatenablog.com

golang : csv.Reader の "wrong number of fields in line" を無視する

Read() を実行する前に FieldsPerRecord を -1 に設定する。

reader := csv.NewReader(f)
reader.Comma = '\t'
reader.FieldsPerRecord = -1 // これ
for {
    record, err := reader.Read()
    if err == io.EOF {
        break
    }
    if err != nil {
        return err
    }
    // 処理...

csv - The Go Programming Language の Reader の FieldsPerRecord の説明を参照。

golang : 固定長文字列出力フォーマット

%(文字数).(文字数)s のような形で、最大精度と最小精度を指定する。. の左側の数字の前に - を付けると左詰め、付けないと右詰め。

// 例えば50文字(左詰め)で固定。
fixedLengthString = fmt.Sprintf("%-50.50s", s)

gcloud : ERROR: (gcloud.auth.git-helper) Invalid input line format: [path=].

gcloud source repos clone (リポジトリ名) --project=(プロジェクト名)Google Cloud Platform 上のソースコードをクローンしようとしたときに発生。

ERROR: (gcloud.auth.git-helper) Invalid input line format: [path=].
fatal: remote error:

解決方法

git config --system --unset credential.helper の実行で解決。

ただし

error: could not lock config file C:\Program Files\Git\mingw64/etc/gitconfig: Permission denied

と初回実行時には出ていたので、該当のフォルダ(C:\Program Files\Git\mingw64/)を右クリック → プロパティ → セキュリティでアクセス許可を編集する必要があった。

参考

gcloud cli - On Windows git pull and clone for Google cloud repository pops credential manager dialog box - Stack Overflow

Robobrowser + α によるスクレイピングの忘備録

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-』を読んでしばらく Robobrowser を使ってみようと思うので、その忘備録。

edx.hatenablog.com

基本

browser = RoboBrowser(
    parser='html.parser',
    # Cookie が使用できないと表示されてログインできない問題を回避するため、通常のブラウザの User-Agent を使う。
    user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecho/20100101 Firefox/45.0)'
)
broswer.open(スクレイピング対象のページのURL)
# 取得したページの中身の確認。デバッグ時以外は不要。
# print(broswer.parsed)

user_agent の指定はサイトによっては不要。少なくとも Amazon.co.jp については必要(設定しない状態で open すると、取得できる html で Cookie を有効にして下さい、という旨のメッセージが入っていることがわかる)。

以降のコード例は基本的に上記の続きで broswer が Robobrowser インスタンスを表すものとする。

CSSセレクターを使った要素の取得

select 関数で CSSセレクターを使用可能。条件に一致する要素の配列が取得できる。

条件を指定して要素を取得する関数としては find_all 関数も利用できるので、好みで。find_all の方が柔軟かもしれない。

実際に使用したことのある一部の例のみ記載。

探したい要素 書き方
特定のクラスを持つ要素 要素.クラス a.active
特定の要素の子孫 要素 要素 body h1
特定の要素の直接の子 要素 > 要素 body > h1
特定のID属性 #ID #main
属性が特定の値 要素[属性=“値”] a[href=“/sample/index.html”]
属性が特定の値で始まる 要素[属性^=“値”] span[id^=“itemPrice_” ]
属性が特定の値で終わる 要素[属性$=“値”] img[src$=“.jpg” ]

以下の例では g-item-details というクラスを持った div 要素を取得し、取得した各要素に更に select を実行して必要な値を取得している

for item_info_element in browser.select('div.g-item-details'):
    title = item_info_element.select('a[id^="itemName_"]')
    assert len(title) == 1
    # text でテキストを取得。
    title = title[0].text.strip()
    # href 属性をキーにリンク先を取得
    # 要 from urllib.parse import urljoin
    url = urljoin(browser.url, title[0]['href'])
    # URL の ref= 以降は不要。
    url = item.url[:item.url.find('ref=')]
    price = item_info_element.select('span[id^="itemPrice_"]')
    # 必要な処理を続ける

特定のテキストを持ったリンク

get_link 関数の第一引数でテキストを指定する。取得したリンクを開くには follow_link 関数を使用する。

# この例ではリンク先(href の値)が特定の URL パターンに一致することも確認している
sign_in_link = browser.get_link('サインイン', href=re.compile(r'https://www\.amazon\.co\.jp/ap/signin\?'))
# assert sing_in_link is not None
# browser.follow_link(sign_in_link)

フォーム

action をキーに get_form 関数を使って取得。 name 要素をキーに各入力欄にアクセスして値を設定し、submit_form 関数で実行

browser.follow_link(sign_in_link)
form = browser.get_form(action='https://www.amazon.co.jp/ap/signin')
if form is None:
    raise Exception('Failed to get signin link.')
form['email'] = '.....@gmail.com'
form['password'] = 'secret, of course!'
browser.submit_form(form, headers={
    'Referer': browser.url,
    'Accept-Language': 'ja,en-US;q=0.7;q=0.3'
})

Amazon では Referer と Accept-Language の設定が必要。必要ないサイトもある。この辺りの必要性は実際に確かめるしか無い?

関数のリトライ設定

Retrying パッケージを使用して、関数に @retry デコレータを使用する

@retry(stop_max_attempt_number=3, wait_exponential_multiplier=1000)
def signin() -> RoboBrowser:
    # 処理に失敗した場合は raise Exception('失敗の理由') などで例外を発生させる

github.com

golang : eval (数式の evaluation)を行う

go/token パッケージ、go/types パッケージ、go/constant パッケージを使用します。

import (
    "fmt"
    "go/token"
    "go/types"
    "go/constant"
)

fs := token.NewFileSet()
tv, err := types.Eval(fs, nil, token.NoPos, "1 + 2 * 3 % 4")
if err != nil {
    fmt.Println(err)
    return
}
val, ok := constant.Int64Val(tv.Value)
if !ok {
    fmt.Println("Failed to get Int64Val")
    return
}
fmt.Println(val)

// 結果は 3。

(Go 言語で プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問 をトライ中に 2問目を解くために方法を探しただけで、中身はあまりよくわかっていません。)