end0tknr's kipple - web写経開発

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

kuromoji.js へのユーザ辞書追加

先程のentryの続きです

参考url

nodejsはver.10.16.3 で

参考urlによれば、v10.16.3 以外ではエラーになるようですので

$ nvm install 10.16.3
$ nvm use 10.16.3

$ node -v
v10.16.3

$ npm -v
6.9.0

install kuromoji.js

$ npm install kuromoji
$ cd node_modules/kuromoji
$ npm install
$ npm run build-dict

もし「npm run build-dict」で、out of memmory のエラーとなる場合、以下

$ NODE_OPTIONS="--max-old-space-size=4096" npm run build-dict

ユーザ辞書用csvの作成

$ cd /home/end0tknr/tmp/myproj/node_modules/kuromoji
$ vi node_modules/mecab-ipadic-seed/lib/dict/userdic.csv

上記csvのファイル名は任意で、内容は例えば以下

快感エア,1285,1285,5402,名詞,一般,*,*,*,*,快感エア,カイカンエア,カイカンエア

辞書データのbuild

以下の通りで、完成した *.dat.gz は、先程のentryのように利用できます

$ cd /home/end0tknr/tmp/myproj/node_modules/kuromoji
$ npm run build-dict

> kuromoji@0.1.2 build-dict /home/end0tknr/tmp/myproj/node_modules/kuromoji
> gulp build-dict

[14:38:43] Using gulpfile ~/tmp/myproj/node_modules/kuromoji/gulpfile.js
[14:38:43] Starting 'clean'...
[14:38:43] Starting 'clean-dict'...
[14:38:43] Finished 'clean' after 21 ms
[14:38:43] Starting 'build'...
[14:38:43] Finished 'clean-dict' after 22 ms
[14:38:43] Finished 'build' after 168 ms
[14:38:43] Starting 'build-dict'...
[14:38:43] Starting 'create-dat-files'...
[14:38:43] Finished 'build-dict' after 9.12 ms
Finishied to read token info dics
Finishied to read unk.def
Finishied to read char.def
Finishied to read matrix.def
Finishied to read all seed dictionary files
Building binary dictionary ...
[14:38:47] Finished 'create-dat-files' after 4.25 s
[14:38:47] Starting 'compress-dict'...
[14:38:50] Finished 'compress-dict' after 3.03 s
[14:38:50] Starting 'clean-dat-files'...
[14:38:50] Finished 'clean-dat-files' after 11 ms

$ ls -lh dict 
total 17M
-rw-rw-r-- 1 end0tknr end0tknr 3.8M May 14 14:38 base.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 1.7M May 14 14:38 cc.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 3.0M May 14 14:38 check.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 1.6M May 14 14:38 tid.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 1.5M May 14 14:38 tid_map.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 5.7M May 14 14:38 tid_pos.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr  11K May 14 14:38 unk.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr  306 May 14 14:38 unk_char.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr  338 May 14 14:38 unk_compat.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 1.2K May 14 14:38 unk_invoke.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr 1.2K May 14 14:38 unk_map.dat.gz
-rw-rw-r-- 1 end0tknr end0tknr  11K May 14 14:38 unk_pos.dat.gz

kuromoji.js - javascript版 形態素解析

python版の形態素解析は、以前、上記entryにある sudachipy を使用しましたが、 今回は、javascript版の形態素解析である kuromoji.js を node.jsを用いず、ブラウザで動作させます。

github.com

https://github.com/takuyaa/kuromoji.js にある 「Browser You only need the build/kuromoji.js and dict/*.dat.gz files」の通り、 これらをダウンロードし、以下のhtml + javascriptを作成

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>
<style>
table  { border-collapse: collapse; }
th, td { border: 1px solid #555;  padding: 5px; }
</style>
</head>
<body>
  <input id="org_text" type="text" style="width:400px;"
         value="ここに形態素解析対象の文を入力してください。"/>
  <button type="button" onClick="do_tokenize()">形態素解析</button>
  <table>
    <thead>
      <tr>
        <th>word_id</th><th>word_type</th><th>word_position</th>
        <th>surface_form</th><th>pos</th><th>固有値</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  
  <template id="tmpl_tr">
    <tr> <td></td><td></td><td></td><td></td><td></td><td></td> </tr>
  </template>
  
  <script src="./kuromoji.js"></script>
  <script>
    function buildTokenizer() {
        return new Promise((resolve, reject) => {
            kuromoji.builder({ dicPath: "./dict" }).build((err,tokenizer)=>{
                if (err) {
                    reject(err);
                } else {
                    resolve(tokenizer);
                }
            });
        });
    }
    let tokenizer;
    let org_txt_elm = document.querySelector("#org_text");
    let tmpl_tr     = document.querySelector("#tmpl_tr");
    let tbody       = document.querySelector("table tbody");
    
    function do_tokenize() {
        let org_text = org_txt_elm.value.trim();
        let tokens = tokenizer.tokenize(org_text);
        tbody.innerHTML = "";
        
        for (let token of tokens ){
            //"true" is "deep copy".
            let clone = tmpl_tr.content.cloneNode(true);
            let tds = clone.querySelectorAll("td");
            tds[0].textContent = token["word_id"];
            tds[1].textContent = token["word_type"];
            tds[2].textContent = token["word_position"];
            tds[3].textContent = token["surface_form"];
            tds[4].textContent = token["pos"];

            if(token.pos == "名詞" && token.pos_detail_1 == "固有名詞"){
                tds[5].textContent = "固有表現";
            }
            tbody.appendChild(clone);
        }
    }

    async function main() {
        tokenizer = await buildTokenizer();
    }
    
    main();
  </script>
</body>
</html>

すると、以下のように表示され、実行できます

microsoft listに添付されたexcelファイルをseleniumでダウンロード

Office365 (microsoft365)のmicrosoft listに添付されたexcelのリンクをクリックすると、 ファイルダウンロードにはならず、まずは、excel for webで起動されます。

selenium for pythonexcel for web からexcelファイルのダウンロードを 試みましたが、なぜか find_elements(By.CSS_SELECTOR)で elementを取得できない。

どうやら、excel for web の画面で用いられている iframe の影響らしい。

そこで、switch_to.frame() で iframe内へ切り替えることで解消。

python scriptとしては、おおよそ以下のような感じです。

#!python
# -*- coding: utf-8 -*-
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.common.by    import By
from selenium.webdriver.common.keys  import Keys
from selenium.webdriver.edge.options import Options
from selenium.webdriver.edge.service import Service
from selenium.webdriver.support.ui   import Select
import subprocess
import sys
import time

CONF = {
    "browser": {
        "browser_driver" : ".\\msedgedriver.exe",
        "implicitly_wait": 10,
        "browser_options": [
            #"headless",
        ],
        "prefs": {
          #ファイルのダウンロード先
            "download.default_directory":"c:\\Users\\xcend0tknr\\tmp"
        }
    },
    "xmile_sso":{
        "url"      :"https://portaltop.xmile.sexy.co.jp/",
        "user_id"  :"ないしょ@id.sexy-g.com",
        "user_pw"  :"ないしょ",
        "proxy_pac":"http://portal.xmile.sexy.co.jp/proxy.pac"
    },
    "o365":{
        "qa_list_url":
        "https://sexyglobal-my.sharepoint.com/personal/ないしょ_id_sexy-g_com/Lists/A/AllItems.aspx"
}
}

def main():
    set_proxy_pac("off")
    browser = init_browser()

    login_to_xmile(browser)
    time.sleep(5)
    
    # microsoft listsにある問合せ一覧へ
    browser.get( CONF["o365"]["qa_list_url"] )
    time.sleep(10)

    # 各問合せへのurlを取得
    a_href_elms = browser.find_elements(By.CSS_SELECTOR,
                                        "div.field-_x8cea__x554f__x5185__x5bb9_ a")
    qa_urls = []
    for a_href_elm in a_href_elms:
        qa_urls.append( a_href_elm.get_attribute("href") )

    # 各問合せにある添付fileへののurlを取得
    for qa_url in qa_urls:
        browser.get( qa_url )
        time.sleep(5)

        attach_elms = browser.find_elements(
            By.CSS_SELECTOR, "div.ReactFieldEditor-Attachments-Renderer a")
        
        attach_urls = []
        for attach_elm in attach_elms:
            attach_url = attach_elm.get_attribute("href")
            attach_urls.append(attach_url)

        for attach_url in attach_urls:
            browser.get( attach_url )
            time.sleep(5)
            # excel for webは iframeで構成される為、そのiframeへ切替え
            browser.switch_to.frame(0)
            time.sleep(5)
            # excel for web画面の「ファイル」タブclick
            btn_elms =browser.find_elements(By.CSS_SELECTOR,"span#id__3")
            btn_elms[0].click()
            time.sleep(5)
            
            #browser.switch_to.default_content()
            
            # 「名前を付けて保存」click
            btn_elms =browser.find_elements(By.CSS_SELECTOR,"#FileSaveAsPage")
            btn_elms[0].click()
            time.sleep(5)
            # 「ダウンロード」click
            btn_elms =browser.find_elements(By.CSS_SELECTOR,"#DownloadACopy")
            btn_elms[0].click()
            time.sleep(5)
            break
    
def login_to_xmile(browser):
    browser.get( CONF["xmile_sso"]["url"] )

    button_elms = browser.find_elements(By.CSS_SELECTOR,"button.aad_login_button")
    button_elms[0].click()
    
    time.sleep( 2 )
    input_elms = browser.find_elements(By.CSS_SELECTOR,"input[name='loginfmt']")
    input_elms[0].send_keys( CONF["xmile_sso"]["user_id"] )
    time.sleep( 2 )
    
    input_elms[0].send_keys(Keys.ENTER)
    time.sleep( 2 )

    input_elms = browser.find_elements(By.CSS_SELECTOR,"input[name='passwd']")
    input_elms[0].send_keys( CONF["xmile_sso"]["user_pw"] )
    time.sleep( 2 )
    
    input_elms[0].send_keys(Keys.ENTER)
    time.sleep( 10 )
    return browser


def init_browser():
    browser = None
    try:
        browser_service = Service(
            executable_path=CONF["browser"]["browser_driver"] )

        browser_opts = Options()
        for tmp_opt in CONF["browser"]["browser_options"]:
            browser_opts.add_argument( tmp_opt )

        browser_opts.experimental_options["prefs"]=CONF["browser"]["prefs"]

        browser = webdriver.Edge(service = browser_service,
                                 options = browser_opts )
        # 要素が見つかるまで、最大 ?秒 待つ
        browser.implicitly_wait( CONF["browser"]["implicitly_wait"] )
    except Exception as e:
        print(e)
    return browser


# cf. https://qiita.com/fetaro/items/a3b3bd4ea197b600ac45
def set_proxy_pac(on_off):
    reg_key = \
        '"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"'

    if on_off == "off":
        off_cmd_cols = [
            'reg delete',
            reg_key,
            '/v "AutoConfigURL"',
            '/f']
        off_cmd_str = " ".join( off_cmd_cols )
        return exec_subprocess( off_cmd_str )

    if on_off == "on":
        on_cmd_cols = [
            'reg add',
            reg_key,
            '/v "AutoConfigURL"',
            '/t REG_SZ',
            '/d "%s"' % ( CONF["xmile_sso"]["proxy_pac"] ),
            '/f']
        on_cmd_str = " ".join( on_cmd_cols )
        return exec_subprocess( on_cmd_str )

def exec_subprocess(cmd:str, raise_error=True):
    child = subprocess.Popen( cmd,
                              shell=True,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE )
    stdout, stderr = child.communicate()
    rt = child.returncode
    if rt != 0 and raise_error:
        print("ERROR",stderr,file=sys.stderr)
        return (None,None,None)

    return stdout, stderr, rt

if __name__ == '__main__':
    main()

Re:区再編に伴う全国地方公共団体コードの変更について

「区名・区域に変更のない天竜区のコードも変更」のようなケースで 市区町村コードが変わっていて驚きました

https://www.city.hamamatsu.shizuoka.jp/ksh/imf/tkdk.html

CODE 名称
old 22140-6 静岡県 浜松市天竜区
new 22137-6

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()