end0tknr's kipple - web写経開発

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

bcp import from utf-8 csv to SQL Server 2022 for Linux

install SQL Server to Oracle Linux 8 - end0tknr's kipple - web写経開発

先日の上記entryの続きです。

今回は、SQL Server 2022 for Linux に対し、 utf-8で記載されたcsvファイルをbcpコマンドでインポートします。

目次

utf-8のデータベース作成と、その確認

sql serverで、日本語のutf-8を扱う場合、 「COLLATE Japanese_XJIS_140_CI_AS_UTF8」を指定するようですので 以下のようにデータベース作成します。

$ sqlcmd -S localhost -U sa -P ????24k! -C -s\| -W

SQL> CREATE DATABASE xserial COLLATE Japanese_XJIS_140_CI_AS_UTF8
go

SQL> SELECT db.name, db.create_date,
            pcpl.name as owner, collation_name
     FROM sys.databases db
     JOIN sys.server_principals pcpl
     ON db.owner_sid=pcpl.sid
go

ログインユーザとデータベースユーザの追加

sql serverでは、db接続用の「ログインユーザ」と、 db操作用の「データベースユーザ」の2つが必要ですので、追加します。

まず、ログインユーザの追加と、追加結果の確認

SQL> CREATE LOGIN end0tknr
WITH PASSWORD = 'end0tknr',
     CHECK_EXPIRATION= OFF,
     CHECK_POLICY    = OFF
go

SQL> SELECT name, sid, type_desc,
  FORMAT(create_date, 'yyyy-MM-dd') as create_date,
  FORMAT(modify_date, 'yyyy-MM-dd') as modify_date
FROM  sys.server_principals
WHERE type IN ('S','U')
go

name    |sid         |type_desc|create_date|modify_date
--------|------------|---------|-----------|-----------
sa      |0x01        |SQL_LOGIN|2003-04-08 |2024-04-23
end0tknr|0xC5AE<略>|SQL_LOGIN|2024-04-27 |2024-04-27

次に先程作成したデータベース用のユーザ追加と、確認

USE xserial
go

CREATE USER end0tknr FOR LOGIN end0tknr
go

SELECT
  name, sid, type_desc,
  FORMAT(create_date, 'yyyy-MM-dd') as create_date,
  FORMAT(modify_date, 'yyyy-MM-dd') as modify_date
FROM  sys.database_principals
WHERE type IN ('S','U')
go

name              |sid    |type_desc|create_date|modify_date
------------------|-------|---------|-----------|-----------
dbo               |0x01   |SQL_USER |2003-04-08 |2024-04-27
guest             |0x00   |SQL_USER |2003-04-08 |2003-04-08
INFORMATION_SCHEMA|NULL   |SQL_USER |2009-04-13 |2009-04-13
sys               |NULL   |SQL_USER |2009-04-13 |2009-04-13
end0tknr          |0xC5 略|SQL_USER |2024-04-27 |2024-04-27

ロールへのユーザの追加

use xserial
go

ALTER ROLE db_owner ADD MEMBER end0tknr
go

SELECT DP1.name AS DatabaseRoleName,   
   isnull (DP2.name, 'No members') AS DatabaseUserName   
 FROM sys.database_role_members AS DRM  
 RIGHT OUTER JOIN sys.database_principals AS DP1  
   ON DRM.role_principal_id = DP1.principal_id  
 LEFT OUTER JOIN sys.database_principals AS DP2  
   ON DRM.member_principal_id = DP2.principal_id  
WHERE DP1.type = 'R'
ORDER BY DP1.name
go

DatabaseRoleName |DatabaseUserName
-----------------|----------------
db_accessadmin   |No members
db_backupoperator|No members
db_datareader    |No members
db_datawriter    |No members
db_ddladmin      |No members
db_denydatareader|No members
db_denydatawriter|No members
db_owner         |dbo
db_owner         |end0tknr
db_securityadmin |No members
public           |No members

参考url

https://learn.microsoft.com/ja-jp/sql/relational-databases/system-catalog-views/sys-database-role-members-transact-sql

次に sysadmin ロールにも追加します。

sql serverには、bulkadmin というバルクインサート用のロールがありますが、 ドキュメントによれば、

SQL Server on Linux では、ADMINISTER BULK OPERATIONS アクセス許可
または bulkadmin ロールはサポートされていません。
SQL Server on Linux に対して一括挿入を実行できるのは、sysadmin だけです。

https://learn.microsoft.com/ja-jp/sql/t-sql/statements/bulk-insert-transact-sql

のようですので、次の手順でロールにメンバーを追加します。

SQL> use master
go

SQL> ALTER SERVER ROLE sysadmin ADD MEMBER end0tknr
go

SQL> SELECT rp.name, mp.name
FROM sys.server_role_members srm
JOIN sys.server_principals rp ON srm.role_principal_id = rp.principal_id
JOIN sys.server_principals mp ON srm.member_principal_id = mp.principal_id
WHERE mp.name = 'end0tknr'
go

name    |name
--------|--------
sysadmin|end0tknr

インポート先のテーブル作成と確認

SQL> CREATE TABLE dteam_attr (
dteam_code      char(3)         DEFAULT ' '     NOT NULL,
dteam_name      varchar(40)     DEFAULT ' '     NOT NULL ,
entry_day       char(8)         DEFAULT ' '     NOT NULL,
renew_day       char(8)         DEFAULT ' '     NOT NULL,
constraint dteam_attr_uidx  primary key clustered (dteam_code)
)
go
SQL> SELECT name, type, type_desc,
       FORMAT(create_date, 'yyyy-MM-dd') as create_date,
       FORMAT(modify_date, 'yyyy-MM-dd') as modify_date
FROM sys.objects
WHERE type = 'U'
go

name      |type|type_desc |create_date|modify_date
----------|----|----------|-----------|-----------
dteam_attr|U   |USER_TABLE|2024-04-27 |2024-04-27


SQL> SELECT
       TABLE_CATALOG as CATALOG,
       TABLE_SCHEMA as SCHEMA, TABLE_NAME,
       COLUMN_NAME,
       ORDINAL_POSITION as POS,
       COLUMN_DEFAULT   as DETAULT,
       IS_NULLABLE      as NULLABLE,
       DATA_TYPE,
       CHARACTER_MAXIMUM_LENGTH as MAX_LEN
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'dteam_attr'
go

CATALOG|SCHEMA|TABLE_NAME|COLUMN_NAME|POS|DEFAULT|NULLABLE|DATA_TYPE|MAX_LEN
-------|------|----------|-----------|---|-------|--------|---------|-------
xserial|dbo   |dteam_attr|dteam_code |1  |(' ')  |NO      |char     |3
xserial|dbo   |dteam_attr|dteam_name |2  |(' ')  |NO      |varchar  |40
xserial|dbo   |dteam_attr|entry_day  |3  |(' ')  |NO      |char     |8
xserial|dbo   |dteam_attr|renew_day  |4  |(' ')  |NO      |char     |8


SQL> SELECT *
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_NAME = 'dteam_attr'
go

CONSTRAINT_CATALOG|CONSTRAINT_SCHEMA|CONSTRAINT_NAME|TABLE_CATALOG|TABLE_SCHEMA|TABLE_NAME|COLUMN_NAME
------------------|-----------------|---------------|-------------|------------|----------|-----------
xserial           |dbo              |dteam_attr_uidx|xserial      |dbo         |dteam_attr|dteam_code 

SQL> SELECT name, collation_name FROM sys.columns WHERE name = 'dteam_name'

name      |collation_name
----------|-----------------------------
dteam_name|Japanese_XJIS_140_CI_AS_UTF8

/opt/mssql/lib/mssql-conf/mssqlsettingsmanager.py の編集

この後、実行する bcp コマンドで、sslのエラーになったので、 以下のように FORCE_ENCRYPTION を Falseに。

(更に後から実行すると、sslエラーにならなかったので、実は不要だったのかも)

    supportedSettingsList.append(mssqlsettings.BooleanSetting("forceencryption",
        "MSSQL_FORCE_ENCRYPTION",
        mssqlsettings.SettingValueType.boolean,
        _("Force encryption of incoming client connections"),
        mssqlsettings.SectionForSetting.network,
        False,
#        True,
        "1", # true value
        "0")) # false value

上記の編集後、sql server再起動

$ sudo systemctl restart mssql-server

utf-8で記載されたcsvファイルをbcpコマンドでインポート

$ bcp dteam_attr in /home/end0tknr/tmp/BCP/dteam_attr.bcp
    -S localhost -d xserial -U end0tknr -P end0tknr -q -c -t "," -u

Starting copy...
SQLState = 22001, NativeError = 0
Error = [Microsoft][ODBC Driver 18 for SQL Server]String data, right truncation

28 rows copied.
Network packet size (bytes): 4096
Clock Time (ms.) Total     : 30     Average : (933.3 rows per sec.)

インポート結果の確認

$ sqlcmd -S localhost -U end0tknr -P end0tknr -C -s\| -W
1> use xserial
2> go
Changed database context to 'xserial'.

1> select * from dteam_attr
2> go
dteam_code|dteam_name|entry_day|renew_day
----------|----------|---------|---------
ele       |電気      |19910528 |20240227
<略>

(28 rows affected)

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 を用意し、ブラウザでアクセス

install SQL Server to Oracle Linux 8

RHEL:Linux 上に SQL Server をインストールする - SQL Server | Microsoft Learn

に記載の通りで全くOK。

以下は、インストールした sql serverのバージョン

1> SELECT @@VERSION
2> go
 -
Microsoft SQL Server 2022 (RTM-CU12-GDR) (KB5036343) - 16.0.4120.1 (X64) 
        Mar 18 2024 12:02:14 
        Copyright (C) 2022 Microsoft Corporation
        Developer Edition (64-bit) on Linux (Oracle Linux Server 8.7) <X64>
(1 rows affected)

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 )