end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

install php-fpm , apache httpd to oracle linux

参考url

install apache httpd php php-fpm

$ sudo yum install httpd
$ sudo yum install php php-fpm

$ httpd -v
Server version: Apache/2.4.37 (Oracle Linux)
Server built:   Oct 24 2023 23:52:21

$ php-fpm -v
PHP 7.2.24 (fpm-fcgi) (built: Oct 22 2019 08:28:36)
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

$ sudo vi /etc/httpd/conf.modules.d/00-mpm.conf

00-mpm.conf に以下を追記

<IfModule mpm_event_module>
   StartServers            3
   MinSpareThreads         24
   MaxSpareThreads         48
   ThreadsPerChild         64
   MaxRequestWorkers       128
   MaxConnectionsPerChild  0
  <FilesMatch \.php$>
     SetHandler "proxy:fcgi://127.0.0.1:9000"
  </FilesMatch>
</IfModule>

php-fpm , apache httpd 起動

$ sudo systemctl restart php-fpm
$ sudo systemctl restart httpd

接続テスト

$ sudo vi /var/www/html/test.php
  <?php echo phpinfo(); ?>

↑このような test.php を用意し、ブラウザでアクセス

sqlachemy + psycopg2 +fastapi for python で postgres への sql実行

先日のentryの続きとして、更に fastapi を経由し、 postgres への 生sqlを実行します

import os
import sys
sys.path.append( os.path.dirname(__file__)+"/../lib" )

from fastapi.middleware.cors import CORSMiddleware
import fastapi
import json
import sqlalchemy
import sqlalchemy.orm

conf_src = os.path.join(os.path.dirname(__file__),
                        '../../resources/app_py_conf.json')
conf = json.load( open(conf_src) )

# https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine
db_url_tmpl = "postgresql+psycopg2://{user}:{pass}@{host}:{port}/{db}"
engine = sqlalchemy.create_engine( db_url_tmpl.format(**conf["db"] ) )

session_local = sqlalchemy.orm.sessionmaker(bind=engine,
                                            autocommit=False,
                                            autoflush =False )
def get_db():
    db = session_local() # 実際のdbへの接続は、このタイミング
    try:
        yield db
    finally:
        # context manager(例:with)により、index()終了時に closeが呼ばれます
        db.close()

app = fastapi.FastAPI()

# FastAPIは defaultでは localhostからのrequestのみ許可する為
# CORSMiddlewareにより * からのrequestを許可化
# origins = ["http://localhost",
#            "http://localhost:8080" ]
app.add_middleware(CORSMiddleware,
                   allow_origins=["*"],
                   #allow_origins=origins,
                   allow_credentials=True,
                   allow_methods=["*"],
                   allow_headers=["*"] )

@app.get("/")
def index(db: sqlalchemy.orm.Session = fastapi.Depends(get_db) ):

    sql = sqlalchemy.text("SELECT * FROM city WHERE code = :code")
    sql_vals = {"code":"11002"}
    results = db.execute( sql.bindparams( **sql_vals ) ).mappings()
    for result in results:
        print(result)
        return result
    return None

sqlachemy + psycopg2 for python で postgres への sql実行

# -*- coding: utf-8 -*-
import sqlalchemy

db_conf = {"user":"postgres",
           "passwd":"",
           "host"  :"localhost",
           "port"  :"5432",
           "db"    :"saawo" }

def main():

    sql = sqlalchemy.text("SELECT * FROM city WHERE code = :code")
    
    engine = sqlalchemy.create_engine(
        "postgresql://{user}:{passwd}@{host}:{port}/{db}".format(**db_conf) )

    sql_vals = {"code":"11002"}
    
    with engine.connect() as db_conn:
        results = db_conn.execute( sql.bindparams( **sql_vals ) ).mappings()
        for result in results:
            print( result )
    
if __name__ == '__main__':
    main()

↑こう書くと、↓こう表示されます

{'code': '11002', 'pref': '北海道', 'city': '札幌市', 'lng': 42.9853361, 'lat': 141.2479725}

FastAPI for python における CORS設定( Cross-Origin Resource Sharing )

async load_disp_date_range(vue_obj){
    let req_url = this.server_api_base() + "DispDateRange";
    let res = await fetch(req_url);
    let disp_dates = await res.json();
    vue_obj.disp_date_min = disp_dates[0];
    vue_obj.disp_date_max = disp_dates[1];
    vue_obj.disp_date     = disp_dates[1];
}

fetch() for javascript を用い、他のサーバへ、ajax requestすると、以下のエラー。

Access to fetch at 'http://localhost:8080/api/newbuild/DispDateRange'
from origin 'http://172.18.129.236' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
If an opaque response serves your needs,
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

以下のように fastapi.middleware.cors.CORSMiddleware の設定を加えることで解消。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# origins = ["http://localhost",
#            "http://localhost:8080" ]

app.add_middleware(CORSMiddleware,
                   allow_origins=["*"],
                   #allow_origins=origins,
                   allow_credentials=True,
                   allow_methods=["*"],
                   allow_headers=["*"] )

@app.get("/api/newbuild/DispDateRange")
async def disp_date_range():
    return ["2024-03-31","2024-04-14"]

fetch() for javascript 側にも、{mode:"cors"} を必要とする情報も 見かけましたが、私の環境ではこれがなくても問題なく動作しました。

let res = await fetch(req_url,{mode: "cors"});

sklearn.feature_extraction.text.TfidfVectorizer for python によるTF-IDF特徴語算出

更に前回entryの続きとして以下

from sklearn.feature_extraction.text import TfidfVectorizer
from sudachipy import dictionary
import csv
import pandas
import re
import unicodedata

qa_sys_src_csv    = "qa_srcs_full.csv"
sudachi_conf_json = "c:/Users/end0t/tmp/QA_SGST/sudachi_user_dic/sudachi.json"

def main():
    # TF-IDF対象のテキストをload
    qas = load_qa_sys_src()

    tokenizer_obj = dictionary.Dictionary(config_path=sudachi_conf_json).create()

    docs = []
    
    for qa_src in ( qas ):
        org_txt = "".join([
            qa_src["表題"],"。",qa_src["相談内容"]
        ])
        # 形態素解析
        tokens = tokenizer_obj.tokenize( org_txt )

        doc = []
        for token in tokens:
            if token.part_of_speech()[1] != "普通名詞":
                continue
            doc.append( token.normalized_form() )
        docs.append( " ".join( doc ) )

    # TF-IDF処理
    vectorizer = TfidfVectorizer(smooth_idf=False)
    # TF-IDF処理結果の取り出し
    tfidf_matrix = vectorizer.fit_transform(docs)
    tf_idf = tfidf_matrix.toarray() # TF-IDF 行列を表示

    # 対応する単語を表示
    feature_names = vectorizer.get_feature_names_out()
    # pandasの data frame化
    tf_idf_df = pandas.DataFrame(tf_idf, columns=feature_names).T

    # 各document毎に上位10個の特徴語を表示
    for doc_no in range(100):
        print( qas[doc_no]["表題"], qas[doc_no]["相談内容"] )
        print( tf_idf_df.sort_values(doc_no, ascending=False)[:10][doc_no] )


def load_qa_sys_src():
    ret_datas = []
    with open(qa_sys_src_csv, encoding="cp932") as f:
        reader = csv.DictReader(f)
        for row in reader:
            row["相談No"]   = normalize_word( row["相談No"] )
            row["表題"]     = normalize_word( row["表題"] )
            row["相談内容"] = normalize_word( row["相談内容"] )
            row["回答内容"] = normalize_word( row["回答内容"] )
            ret_datas.append(row)
    return ret_datas


# Sudachiのユーザー辞書には文字正規化が必要
# https://zenn.dev/sorami/articles/6bdb4bf6c7f207
def normalize_word( word ):
    word = re.sub("[\s\n ]+","",word)
    word = unicodedata.normalize('NFKC', word)
    word = word.lower().replace(",","").replace("--","")
    return word
    
if __name__ == '__main__':
    main()

sudachipy for python による sudachiユーザ辞書の利用 (形態素解析)

先程のentryの続きです

sudachi.json

元の sudachi.json をコピーし、「"userDict" : [~] 」を追加しています

{
    "systemDict" : null,
    "characterDefinitionFile" : "char.def",
    "userDict" : ["c:/Users/end0t/tmp/QA_SGST/sudachi_user_dic/user.dic"],
    "inputTextPlugin" : [
        { "class" : "com.worksap.nlp.sudachi.DefaultInputTextPlugin" },
        { "class" : "com.worksap.nlp.sudachi.ProlongedSoundMarkPlugin",
          "prolongedSoundMarks": ["ー", "-", "⁓", "〜", "〰"],
          "replacementSymbol": "ー"},
        { "class": "com.worksap.nlp.sudachi.IgnoreYomiganaPlugin",
          "leftBrackets": ["(", "("],
          "rightBrackets": [")", ")"],
          "maxYomiganaLength": 4}
    ],
    "oovProviderPlugin" : [
        { "class" : "com.worksap.nlp.sudachi.MeCabOovPlugin",
          "charDef" : "char.def",
          "unkDef" : "unk.def" },
        { "class" : "com.worksap.nlp.sudachi.SimpleOovPlugin",
          "oovPOS" : [ "補助記号", "一般", "*", "*", "*", "*" ],
          "leftId" : 5968,
          "rightId" : 5968,
          "cost" : 3857 }
    ],
    "pathRewritePlugin" : [
        { "class" : "com.worksap.nlp.sudachi.JoinNumericPlugin",
          "enableNormalize" : true },
        { "class" : "com.worksap.nlp.sudachi.JoinKatakanaOovPlugin",
          "oovPOS" : [ "名詞", "普通名詞", "一般", "*", "*", "*" ],
          "minLength" : 3
        }
    ]
}

python

#from sudachipy import tokenizer
from sudachipy import dictionary

sudachi_conf_json = "c:/Users/end0t/tmp/QA_SGST/sudachi_user_dic/sudachi.json"
def main():
    # ↓sudachipy.tokenizer.Tokenizer
    tokenizer_obj = dictionary.Dictionary(config_path=sudachi_conf_json).create()
    print( tokenizer_obj )
    
    text = "ユーザと、ユーザー、USERは、同義語です"
    tokens = tokenizer_obj.tokenize(text)
    for token in tokens:
        print( token.surface(),
               token.part_of_speech(),
               token.reading_form(),
               token.normalized_form() )

if __name__ == '__main__':
    main()

実行結果

ユーザ ('名詞', '普通名詞', '一般', '*', '*', '*') ユーザ ユーザー
と ('助詞', '格助詞', '*', '*', '*', '*') ト と
、 ('補助記号', '読点', '*', '*', '*', '*') 、 、
ユーザー ('名詞', '普通名詞', '一般', '*', '*', '*') ユーザー ユーザー
、 ('補助記号', '読点', '*', '*', '*', '*') 、 、
USER ('名詞', '普通名詞', '一般', '*', '*', '*') ユーザー ユーザー
は ('助詞', '係助詞', '*', '*', '*', '*') ハ は
、 ('補助記号', '読点', '*', '*', '*', '*') 、 、
同義語 ('名詞', '普通名詞', '一般', '*', '*', '*') ドウギゴ 同義語
です ('助動詞', '*', '*', '*', '助動詞-デス', '終止形-一般') デス です

sudachipy for python (miniconda for win)による sudachiユーザ辞書作成

以下の通りです

sudachipy.exe コマンドを呼ぶのではなく、 https://github.com/WorksApplications/SudachiPy/blob/develop/sudachipy/dictionarylib/userdictionarybuilder.py にある UserDictionaryBuilder クラスを 内部的に呼びたかったのですが、userdictionarybuilder.py は pip install sudachipy によるインストールの対象外のようでしたので、 以下のようにしています

import csv
import datetime
import os
import re
import sys
import subprocess
import sudachipy
import unicodedata

use_dic_dir  = os.path.dirname(os.path.abspath(__file__))

dic_src_path = use_dic_dir + "/user.dic.src.txt"
dic_csv_path = use_dic_dir + "/user.dic.csv"
user_dic_path= use_dic_dir + "/user.dic"
sudachi_cmd  = "C:/Users/end0t/miniconda3/Scripts/sudachipy.exe"
sys_dic_path = "C:/Users/end0t/miniconda3/Lib/site-packages/sudachidict_core/resources/system.dic"

def main():

    dic_words = load_dic_src( dic_src_path )
    dic_csv_path = save_dic_csv( dic_words )
    
    # 古いユーザ辞書fileのbackup
    global user_dic_path
    if os.path.exists( user_dic_path ):
        bakup_path = user_dic_path + "." + datetime.datetime.now().strftime('%Y%m%d')
        os.rename(user_dic_path, bakup_path)
        
    user_dic_path = make_user_dic( dic_csv_path )
    print( user_dic_path )

# sudachipy.exe ubuild コマンドによるユーザ辞書の作成
def make_user_dic( dic_csv_path ):

    cmd_line = "{} ubuild -s {} -o {} {}".format(
        sudachi_cmd, sys_dic_path, user_dic_path, dic_csv_path )
    print( cmd_line )
    proc = subprocess.Popen(
        cmd_line,
        shell  = True,
        stdin  = subprocess.PIPE,
        stdout = subprocess.PIPE,
        stderr = subprocess.PIPE)
    stdout, stderr = proc.communicate()
    return user_dic_path
    
# ユーザ辞書用csvの作成  https://qiita.com/sakamoto_mi/items/c1787973dd1a591c9957
# https://github.com/WorksApplications/Sudachi/blob/develop/docs/user_dict.md
def save_dic_csv( dic_words ):

    dic_csv_tmpl  = \
        "{word},4789,4789,5000,{word},名詞,普通名詞,一般,*,*,*,*,{caption},*,*,*,*,*"

    with open(dic_csv_path, mode="w",encoding='utf-8') as f:
        for word, caption in dic_words.items():
            csv_line = dic_csv_tmpl.format( word=word, caption=caption )
            f.write( csv_line +"\n" )
            
    return dic_csv_path

# ユーザ辞書用csvの元となるtsvのload    縦軸:見出し語、横軸:類似語
def load_dic_src( dic_src_path ):
    ret_datas = {}
    
    with open(dic_src_path, encoding='utf-8') as f:
        for tsv_line in f:
            words = tsv_line.strip().split("\t")
            caption = None
            for i, word in enumerate(words):
                word = normalize_word( word )
                
                if word in ret_datas:
                    print( f"WARN duplicate word exist : {word}",file=sys.stderr )
                    continue
                if i == 0:
                    caption = word
                if not caption:
                    continue
                ret_datas[word] = caption
        return ret_datas

# Sudachiのユーザー辞書には文字正規化が必要
# https://zenn.dev/sorami/articles/6bdb4bf6c7f207
def normalize_word( word ):
    word = re.sub("[\s\n ]+","",word)
    word = unicodedata.normalize('NFKC', word)
    word = word.lower().replace(",","")
    return word

if __name__ == '__main__':
    main()

psycopg2.DatabaseError: error with status PGRES_TUPLES_OK and no message from the libpq

pythonの自前コード内で、postgresへの connection poolを行い、更に、 concurrent.futures.ProcessPoolExecutor for python の並列処理による影響でしょうか

psycopg2.DatabaseError: error with status PGRES_TUPLES_OK and no message from the libpq

のようなエラーが発生。

なので、以下のように connectionを使いまわす部分をcomment化したところ、 解消したみたい。

def db_connect(self):
    # global db_conn
    
    # if db_conn:
    #     return db_conn
    
    db_conn = psycopg2.connect(
        database    = conf["db"]["db_name"],
        user        = conf["db"]["db_user"],
        password    = conf["db"]["db_pass"],
        host        = conf["db"]["db_host"],
        port        = conf["db"]["db_port"] )
    return db_conn

TortoiseGitで repository path 'X:/tmp/saawo/' is not owned by current user エラー

wslで起動した oracle linuxをsambaで Xドライブをマウントし、 そこに TortoiseGitで git cloneし、commit等を行ったところ、以下のエラーが発生

---------------------------
TortoiseGit
---------------------------
Could not get HEAD hash.
libgit2 returned: repository path 'X:/tmp/???/' is not owned by current user.

To add an exception for this directory, call:
git config --global --add safe.directory 'X:/tmp/???'
---------------------------
OK   
---------------------------

ダイアログに 「git config --global --add safe.directory 'X:/tmp/???'」とありますので、 TortoiseGitの設定から「Edit global .gitconfig」を開き、 ドライブ名を「x→X」のように大文字にすることで解消

[safe]
directory = x            ##★ ココ
directory = x:/tmp/???   ##★ ココ
directory = %(prefix)///172.18.129.236/end0tknr/tmp/??? ##★ ココ
directory = 'x:/tmp/???' ##★ ココ

emacs 27 for windows への jedi(python 自動補完)の install

ポイントは

  • minicondaをinstallし、windowsのPATHを通す
  • pip installで jediに加え、virtualenv もインストール

かと思います。

step1 - miniconda (python11)の install

2024/3時点で、minicondaの最新は python12版ですが、 emacsのjediが対応していない為、python11版をinstall。

その上で、windows環境変数 PATHに以下を追加

  • c:\Users\end0t\AppData\Local\miniconda3
  • c:\Users\end0t\AppData\Local\miniconda3\Scripts

step2 - pip install jedi & virtualenv

(conda base)> pip install jedi
(conda base)> pip install virtualenv

step3 - emacs package install

https://monologu.com/add-melpa/ を参考に、init.el へ以下を追記

(when (require 'package nil t)
  (add-to-list 'package-archives
     '("melpa" .      "http://melpa.org/packages/"))
  (package-initialize))

その上で、

  • M-x package-install jedi
  • M-x jedi:install-server

step4 - emacs の init.el に jediの設定

https://tkf.github.io/emacs-jedi/latest/ を参考に以下を追記

(add-hook 'python-mode-hook 'jedi:setup)
(setq jedi:complete-on-dot t)

すると、*.py ファイルの編集時、以下のように自動補完されます

requests for python による大容量レスポンスの分割ダウンロード

requests for python で、巨大なfileをhttp getする場合、stream=True で chunk化 - end0tknr's kipple - web写経開発

以前の上記entryの修正版です。

国土交通省で公開している不動産取引価格情報取得API ( https://www.land.mlit.go.jp/webland/api.html )に対し、 python3標準の urllib.request を用い

import urllib.request
http_conf = {"retry_limit":3, "retry_sleep":5 }

def get_http_requests(self, req_or_url,req_timeout=5):
    i = 0
    while i < http_conf["retry_limit"]:
        i += 1
        try:
            http_res = urllib.request.urlopen(req_or_url, timeout=req_timeout)
            html_content = http_res.read()
            return html_content
        
        except Exception as e:
            if "404: Not Found" in str(e):
                logger.error("{} {}".format(req_or_url, e))
                return None
            
            logger.warning(e)
            logger.warning("retry {} {}".format(i,req_or_url))
            time.sleep(http_conf["retry_sleep"])

    return None

のように http getを行うと、本来、数MBの要領がある影響でしょうか、 レスポンスが途中で途切れてしまいます。

https://docs.python.org/ja/3/library/urllib.request.html を見ると、 「 より高水準のHTTPクライアントインターフェースとして Requestsパッケージ がお奨めです。」 という記載がありましたので、

「pip install requests」した上で、以下のように requests を用い、かつ、「stream=True」で分割ダウンロードすることで解消

import requests

def get_http_requests(self,req_url):
    i = 0
    while i < 3: # 最大3回 retry
        i += 1
        try: # 先方サーバによっては http response codeを返さない為、try-except
            res = requests.get(req_url, timeout=(5,60), stream=True)
        except Exception as e:
            logger.warning(e)
            logger.warning("retry {} {}".format(i,req_url))
            time.sleep(10)

        if res.status_code == 404:
            logger.error( "404 error {}".format(req_url) )
            return

        try:
            res.raise_for_status()
        except Exception as e:
            logger.warning(e)
            logger.warning("retry {} {}".format(i,req_url))
            time.sleep(10)

    # 大容量の為か urllib.request.urlopen()では
    # response contentを取得できなかった為、stream=True で chunk化
    chunks = []
    for chunk in res.iter_content(chunk_size=1024*1024):
        chunks.append(chunk)
      
    content = b"".join(chunks).decode()
    return json.loads( content )

Oracle Linux 8.7 への日本語フォント インストール

$ sudo yum install ipa-gothic-fonts ipa-mincho-fonts \
                   ipa-pgothic-fonts ipa-pmincho-fonts

以前、centosに対しては上記のようにipaのフォントをインストールしていましたが、 redhat?には、このフォントがないみたい。

なので、以下

$ sudo yum search japan font
Repository google-chrome is listed more than once in the configuration
Last metadata expiration check: 4:46:37 ago on Thu Mar 21 02:47:33 2024.
======================== Summary & Name Matched: japan, font ========================
adobe-source-han-sans-jp-fonts.noarch : Adobe OpenType Pan-CJK font family for
                                      : Japanese
adobe-source-han-sans-jp-fonts.src : Adobe OpenType Pan-CJK font family for Japanese
google-noto-sans-cjk-jp-fonts.noarch : Japanese Multilingual Sans OTF font files for
                                     : google-noto-cjk-fonts
vlgothic-fonts.noarch : Japanese TrueType font
vlgothic-fonts.src : Japanese TrueType font
vlgothic-p-fonts.noarch : Proportional Japanese TrueType font
=========================== Summary Matched: japan, font ============================
texlive-wadalab.noarch : Wadalab (Japanese) font packages
$ sudo yum install \
  adobe-source-han-sans-jp-fonts \
  google-noto-sans-cjk-jp-fonts \
  vlgothic-fonts.noarch \
  vlgothic-p-fonts