Pythonでマクロ:スクロールバーの実装

Pythonでマクロ:チェックボックス再論ではダイアログ上にチェックボックスを自動的に作成する問題を考えた。チェックボックスが多くなるとチェックボックスがダイアログ画面に納まらなくなる。(垂直)スクロールバーの出番である。

スクロールバーのモデルの詳細はここにある。

スクロールバーのツマミを動かすと
イヴェントが発生するがこのイヴェントの監視とイヴェント処理メッソド(adjustmentValueChanged)の詳細はここにある。

チェックボックスは上図のようにダイアログ画面に納まらない縦長の紙に書かれていると想像してみる。このイヴェント処理メッソドではスクロールバーのツマミの位置の値(ScrollValue)を使ってこの縦長の紙の方を動かして紙のどの部分をダイアログ画面で見せるかを計算し個々のチェックボックスのダイアログ画面上の座標を変えてやる。

実行例の画像

最初のダイアログ画面
最後の外アログ画面

スクロール・ツマミの動きに対してダイアログ画面をスムーズに変化させるにはスクロール属性LiveScrollをtrueにしておくとよい。

最後にマクロ本体を載せておく:


#coding: utf-8
import uno
import screen_io as ui
import unohelper
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XAdjustmentListener

#「スクロールバー」の処理
class MyAdjustmentListener(unohelper.Base, XAdjustmentListener):
  def __init__(self, dlgHgt, ckbxsHgt, ckbxs):
    self.ckbxs = ckbxs
    self.scrollVMax=100
    self.dlgHgt=dlgHgt
    self.ckbxsHgt=ckbxsHgt
    self.ckbxPositionY0 = []
    for ckbx in ckbxs:
      print(ckbx.PositionY)
      self.ckbxPositionY0.append(ckbx.PositionY)
  def adjustmentValueChanged(self, event):
    ratio = event.Value/self.scrollVMax
    print(ratio)
    for i,ckbx in enumerate(self.ckbxs):
      ckbx.PositionY = self.ckbxPositionY0[i]-(self.ckbxsHgt - self.dlgHgt)*ratio
      print( i,  self.ckbxPositionY0[i], ckbx.PositionY)
    #print(event.Type)
    #print(event.Value)
#「完了」ボタンの処理
class FinishedListener(unohelper.Base, XActionListener):
  def __init__(self, ckbxs, sheet):
    self.ckbxs = ckbxs
    self.sheet=sheet
  def actionPerformed(self, evnt):
    for i,ckbx in enumerate(self.ckbxs):
      if(ckbx.State):
        self.sheet.getCellByPosition(1, i+1 ).String = '○'
      #ui.Print(ckbx.State)
#「リセット」ボタンの処理
class ResetListener(unohelper.Base, XActionListener):
  def __init__(self, ckbxs):
    self.ckbxs = ckbxs
  def actionPerformed(self, evnt):
    for ckbx in self.ckbxs:
      ckbx.State=0

def python_macro_scbar(*arg):
#
#シート
  doc = XSCRIPTCONTEXT.getDocument()
  sheet = doc.Sheets[0]
  #データのある行数を調べる
  sRange = sheet.getCellRangeByName("A1")
  sCursor=sheet.createCursorByRange(sRange)
  sCursor.collapseToCurrentRegion()
  MaxDataRow = sCursor.Rows.Count-1
  ui.Print(MaxDataRow)
  items=[]
  for  i in range(MaxDataRow):
    item = sheet.getCellByPosition(0, i+1 ).String
    #ui.Print(item)
    items.append(item)
#
#ダイアログ
  ctx = XSCRIPTCONTEXT.getComponentContext()
  smgr = ctx.getServiceManager()
  dialogM = smgr.createInstance('com.sun.star.awt.UnoControlDialogModel')
  # Size of DialogM
  dlgWth = 150
  dlgHgt = 150
  dialogM.Width = dlgWth
  dialogM.Height = dlgHgt
  dialogM.Title = "選択ダイアログ"
#

#コントロールの作成・登録
#ラベル
  lab1 = dialogM.createInstance('com.sun.star.awt.UnoControlFixedTextModel')
  tabIndex = 0
  lab1.Name = 'FixedLabel'
  lab1.TabIndex = tabIndex
  lab1.PositionX =0
  lab1.PositionY =0
  lab1.Width = dlgWth - 10
  lab1.Height = 10
  lab1.Label = "選択(複数)してください。"
  lab1.Align = 1					# 0 : Left / 1 : Center / 2 : Right
  lab1.Border = 0
  lab1.TextColor = 0xff0000
  lab1.Enabled = 1
  dialogM.insertByName('FixedLabel', lab1)
#スクロールバー
  scb1 = dialogM.createInstance('com.sun.star.awt.UnoControlScrollBarModel')
  tabIndex += 1
  scrollVMin=0
  scrollVMax=100
  scb1.Name = 'ScrollBar'
  scb1.TabIndex = tabIndex
  scb1.PositionX = dlgWth-10
  scb1.PositionY = 0
  scb1.Width = 10
  scb1.Height = dlgHgt
  scb1.Orientation=1
  scb1.ScrollValueMin = scrollVMin
  scb1.ScrollValueMax = scrollVMax
  scb1.LiveScroll= 1
  dialogM.insertByName('ScrollBar', scb1)
#ボタン
  btn1 = dialogM.createInstance('com.sun.star.awt.UnoControlButtonModel')
  tabIndex += 1
  btn1.Name = 'OkBtn'
  btn1.TabIndex = tabIndex
  btn1.PositionX = dlgWth-50
  btn1.PositionY = dlgHgt/2 - 20
  btn1.Width = 32
  btn1.Height = 10
  btn1.Label = '完了'
  btn1.PushButtonType = 0		# 1 : OK
  dialogM.insertByName('OkBtn', btn1)
#
  btn2 = dialogM.createInstance('com.sun.star.awt.UnoControlButtonModel')
  tabIndex += 1
  btn2.Name = 'ResetBtn'
  btn2.TabIndex = tabIndex
  btn2.PositionX = dlgWth-50
  btn2.PositionY = dlgHgt/2 + 20
  btn2.Width = 32
  btn2.Height = 10
  btn2.Label = 'リセット'
  btn2.PushButtonType = 0		# 1 : OK
  dialogM.insertByName('ResetBtn', btn2)
#チェックボックス
  ckbxs=[]
  for i in range(MaxDataRow):
    ckbx = dialogM.createInstance('com.sun.star.awt.UnoControlCheckBoxModel')
    tabIndex += 1
    ckbxsHgt = 15+15*i+10
    ckbx.Name = items[i]
    ckbx.TabIndex = tabIndex
    ckbx.PositionX = 10
    ckbx.PositionY = ckbxsHgt
    ckbx.Width = 100
    ckbx.Height = 15
    ckbx.Label = items[i]
    # Dialog Modelの仕様に CheckBox Button1 の仕様を設定
    dialogM.insertByName(items[i], ckbx)
    ckbxs.append(ckbx)
  #最終的なチェックボックス表の高さ
  ckbxsHgt=ckbxsHgt+15

# Create the dialog and set the model
#ダイアログの生成とモデルの登録
  dialog = smgr.createInstance('com.sun.star.awt.UnoControlDialog')
  dialog.setModel(dialogM)
#コントロールの登録
  cmdBtn1 = dialog.getControl('OkBtn')
  cmdBtn2 = dialog.getControl('ResetBtn')
  cmdScb1 = dialog.getControl('ScrollBar')
#エヴェント監視(ボタンが押されたとき)
  btn1_listener = FinishedListener(ckbxs, sheet)
  cmdBtn1.addActionListener(btn1_listener)
  btn2_listener = ResetListener(ckbxs)
  cmdBtn2.addActionListener(btn2_listener)
#エヴェント監視(スクロールバー)
  scb1_listener = MyAdjustmentListener(dlgHgt, ckbxsHgt, ckbxs)
  cmdScb1.addAdjustmentListener(scb1_listener)
#
#窓の生成そしてこの窓をダイアログ画面として使う
  window = smgr.createInstance('com.sun.star.awt.Toolkit')
  dialog.createPeer(window,None)		# None : OK / none : NG
#
  dialog.execute()
  dialog.dispose()

Pythonでマクロ:チェックボックス再論

アンケート調査のように多数の質問項目がありそれにチェックを入れるような状況を想定したLibreofficeのマクロをPythonで作る問題である。

以前にはチェック項目が少ないばあいのマクロを紹介した。チェック項目が多くなると手動でチェック欄を設定するが大変になる。

問題はこうだ:

Calcシートサンプル
Calcシートサンプル

画像のようにLibreofficeのCalcのシート上に多くのチェック項目が書かれているとする。これらにチェック欄を設定(フォームやダイアログ上のチェックボックス)することである。

ここでは

ダイアログのマクロでCalcシート上のチェック項目を読み込み必要な個数のチェックボックスをダイアログの中に自動的に作る。ユーザはこのチェックボックスに必要なチェックを入れ「完了」ボタンを押すとその結果がシートに反映される。「リセット」ボタンは全てのチェック欄のリセットに使う。

マクロの実行画面
マクロの実行画面

マクロの本体は長いので最後に載せた。このマクロはシート上のチェック項目の多寡に拘わらず使えるが、チェック項目は多くなったときにはチェックボックスがダイアログ画面をはみ出してしまう。ダイアログに縦スクロールを付けてダイアログ画面を制御できればよいわけであるがこれは未実装である。

【マクロ】

#coding: utf-8>

import uno
import screen_io as ui
import unohelper
from com.sun.star.awt import XActionListener

#「完了」ボタンの処理
class FinishedListener(unohelper.Base, XActionListener):
  def __init__(self, ckbxs, sheet):
    self.ckbxs = ckbxs
    self.sheet=sheet
  def actionPerformed(self, evnt):
    for i,ckbx in enumerate(self.ckbxs):
      if(ckbx.State):
        self.sheet.getCellByPosition(1, i+1 ).String = '○'
      #ui.Print(ckbx.State)
#「リセット」ボタンの処理
class ResetListener(unohelper.Base, XActionListener):
  def __init__(self, ckbxs):
    self.ckbxs = ckbxs
  def actionPerformed(self, evnt):
    for ckbx in self.ckbxs:
      ckbx.State=0

def python_macro_ckbx(*arg):
#
#シート
  doc = XSCRIPTCONTEXT.getDocument()
  sheet = doc.Sheets[0]
  #データのある行数を調べる
  sRange = sheet.getCellRangeByName("A1")
  sCursor=sheet.createCursorByRange(sRange)
  sCursor.collapseToCurrentRegion()
  MaxDataRow = sCursor.Rows.Count-1
  ui.Print(MaxDataRow)
  items=[]
  for  i in range(MaxDataRow):
    item = sheet.getCellByPosition(0, i+1 ).String
    #ui.Print(item)
    items.append(item)
#
#ダイアログ
  ctx = XSCRIPTCONTEXT.getComponentContext()
  smgr = ctx.getServiceManager()
  dialogM = smgr.createInstance('com.sun.star.awt.UnoControlDialogModel')
  # Size of DialogM
  dlgWth = 150
  dlgHgt = 200
  dialogM.Width = dlgWth
  dialogM.Height = dlgHgt
  dialogM.Title = "選択ダイアログ"
#

#コントロールの作成・登録
#ラベル
  lab1 = dialogM.createInstance('com.sun.star.awt.UnoControlFixedTextModel')
  tabIndex = 0
  lab1.Name = 'FixedLabel'
  lab1.TabIndex = tabIndex
  lab1.PositionX = 10
  lab1.PositionY = 10
  lab1.Width = 100
  lab1.Height = 10
  lab1.Label = "選択(複数)してください。"
  lab1.Align = 1					# 0 : Left / 1 : Center / 2 : Right
  lab1.Border = 0
  lab1.TextColor = 0xff0000
  lab1.Enabled = 1
  dialogM.insertByName('FixedLabel', lab1)
#ボタン
  btn1 = dialogM.createInstance('com.sun.star.awt.UnoControlButtonModel')
  tabIndex += 1
  btn1.Name = 'OkBtn'
  btn1.TabIndex = tabIndex
  btn1.PositionX = dlgWth-50
  btn1.PositionY = dlgHgt/2 - 20
  btn1.Width = 32
  btn1.Height = 10
  btn1.Label = '完了'
  btn1.PushButtonType = 0		# 1 : OK
  dialogM.insertByName('OkBtn', btn1)
#
  btn2 = dialogM.createInstance('com.sun.star.awt.UnoControlButtonModel')
  tabIndex += 1
  btn2.Name = 'ResetBtn'
  btn2.TabIndex = tabIndex
  btn2.PositionX = dlgWth-50
  btn2.PositionY = dlgHgt/2 + 20
  btn2.Width = 32
  btn2.Height = 10
  btn2.Label = 'リセット'
  btn2.PushButtonType = 0		# 1 : OK
  dialogM.insertByName('ResetBtn', btn2)
#チェックボックス
  ckbxs=[]
  for i in range(MaxDataRow):
    ckbx = dialogM.createInstance('com.sun.star.awt.UnoControlCheckBoxModel')
    tabIndex += 1
    ckbx.Name = items[i]
    ckbx.TabIndex = tabIndex
    ckbx.PositionX = 10
    ckbx.PositionY = 15+15*i+10
    ckbx.Width = 100
    ckbx.Height = 15
    ckbx.Label = items[i]
    # Dialog Modelの仕様に CheckBox Button1 の仕様を設定
    dialogM.insertByName(items[i], ckbx)
    ckbxs.append(ckbx)

# Create the dialog and set the model
#ダイアログの生成とモデルの登録
  dialog = smgr.createInstance('com.sun.star.awt.UnoControlDialog')
  dialog.setModel(dialogM)
#コントロールの登録
  cmdBtn1 = dialog.getControl('OkBtn')
  cmdBtn2 = dialog.getControl('ResetBtn')
#エヴェント監視(ボタンが押されたとき)
  btn1_listener = FinishedListener(ckbxs, sheet)
  cmdBtn1.addActionListener(btn1_listener)
  btn2_listener = ResetListener(ckbxs)
  cmdBtn2.addActionListener(btn2_listener)
#
#窓の生成そしてこの窓をダイアログ画面として使う
  window = smgr.createInstance('com.sun.star.awt.Toolkit')
  dialog.createPeer(window,None)		# None : OK / none : NG
#
  dialog.execute()
  dialog.dispose()

Pythonで正規表現:Unicodeの扱い

正規表現は元々ASCIIコードの文字列を念頭に置いた処理であったがUnicodeへの拡張がされている。

  • \d:これは任意の 10 進数字にマッチする。Unicodeへの拡張で半角数字、全角数字にマッチする。
>>> print(re.findall(r'\d+','2021年6月十五日'))
['2021', '6']

漢数字にはマッチしない。半角数字のみでは[0-9]の範囲指定、全角数字のみでは[0-9]の範囲指定を使う。

  • \D:これは\dの補集合にマッチする。
  • \w:英数字にマッチする。Unicodeへの拡張では殆んど全ての文字にマッチする。ひらがなのみにマッチさせるのは[あ-ん]を使う。カタカナは[ア-ン]を使う。
>>> print(re.findall(r'[あ-ん]+','雨ニモ負ケズ、風にも負けず'))
['にも', 'けず']
>>> print(re.findall(r'[ア-ン]+','雨ニモ負ケズ、風にも負けず'))
['ニモ', 'ケズ']

注)Unicode表に沿えばひらがなの範囲は[ぁ(小書きのあ)-ゖ(小書きのけ)]であるが伝統的にはひらがなは「あ」で始まり「ん」で終わるべきだ。

ひらがなカタカナをマッチングから外すには補集合表示が使える。

>>> print(re.findall(r'[^あ-んア-ン]+','雨ニモ負ケズ、風にも負けず'))
['雨', '負', '、風', '負']
  • \W:これは\wの補集合にマッチする。
  • 小さい「っ」・大きい「つ」の区別
>>> print(re.findall(r'.っ','はっきり、つめる'))
['はっ']
>>> print(re.findall(r'.つ','はっきり、つめる'))
['、つ']
  • 全角空白・半角空白
>>> print(re.findall(r' .','これは 全角、これは 半角'))
[' 半']
>>> print(re.findall(r' .','これは 全角、これは 半角'))
['\u3000全']
  • 句読点
>>> print(re.findall(r'.。|.、','おお、寒い。'))
['お、', 'い。']

以上、Unicodeの扱いを纏めてみた。

Pythonで正規表現:raw_stringを使うわけ

正規表現ではバックスラッシュ(\)が多用される。一方Pythonの文字列ではこのバックスラッシュが特別な意味を持つことがある。例を挙げる:

>>> len('\section')
8
>>> len('\next')
4
>>> 

文字列でバックスラッシュに続けてnがあるとそれらの2個で特別な意味を持つ1文字となる。だから文字列’\section’では文字数が8文字であることと対照的に文字列’\next’は4文字となる。この特別な意味を持つ1文字をエスケープシーケンスと呼ぶ(なぜエスケープシーケンスと呼ぶかは最初<Esc>キーをエスケープ文字にした歴史的な経緯による)。

正規表現もPython文字列である。しかもバックスラッシュが多用される。そのバックスラッシュがエスケープ文字として働くと厄介なことが起きる。

これを回避する一つの方法が通常の文字列の代わりにraw_stringを用いるものである。この記法は通常の文字列の前にrを付ける。実例を示す:

>>> len(r'\next')
5
>>> 

この記法ではエスケープシーケンスはその特別な意味を失い、単なる普通の二文字となる。

この記法で書けばエスケープシーケンスが紛れ込むことを気にしないで正規表現を記述できる。例を示す:


#coding: utf-8
import re
text = r'次の節は\nextで始める'
m = re.search(r'\\next', text)
print(m.group(0))

正規表現の中の\\は正規表現の特別文字、バックスラッシュを普通の文字とするためである。

Pythonで正規表現:グループ化を使ってみる

正規表現でマッチした文字列の全てに興味がある訳ではないことがしばしば起こる。例えば固定電話番号のマッチングでは市外局番・市内局番・加入者番号がマッチすることを要求するが興味があるのは加入者番号だけであるというようなばあいである。このばあいはマッチした文字列全体ではなく、加入者番号のみを表示すれば足りる。このようなばあいにグループ化が使える。

グループ化は(正規表現)とグループ化したい正規表現の部分を丸括弧()で括ってやる。グループ化は入れ子(表現1(表現2))でも直列(表現1)(表現2)でも構わない。グループには番号が振られる。正規表現全体はグループ0、正規表現を右に検索し左丸括弧に出会うと番号はインクリメントし、対応する右丸括弧までがこのグループに所属することになる。実例を示す:


#coding: utf-8
import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (\d{4}) #仙台(22)
    | \d{3}-\d{2}- (\d{4]) #松本(263)
    | \d{4}-\d-    (\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('022-345-9876')
if m :
    print(m.groups())
else:
    print(m)

この例では正規表現に六個のグループが使われている。メソッドgroups()はこの六個のグループに対応する表示をタプルの形で取り出すことができる。この実例の結果は

('22-345-9876', None, '9876', None, None, None)

グループ1とグループ3でマッチする表現があることが分る。Noneは該当するグループにマッチする文字列がなかったことを意味する。

このグループ化のPython拡張としてグループに名前を付けることができる。それには(?P<名前>…)というふうに左丸括弧の次に?P<名前>を挿入する。実例を示す:


#coding: utf-8
import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(?P<電話番号>
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (?P<固定0>\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (?P<固定1>\d{4}) #仙台(22)
    | \d{3}-\d{2}- (?P<固定2>\d{4]) #松本(263)
    | \d{4}-\d-    (?P<固定3>\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (?P<携帯>\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('070-2345-9876')
if m :
    print(m.groupdict())
else:
    print(m)

メソッドgroupdict()によってグループ名をキーとしマッチした文字列を値とする辞書で結果を取り出すことができる。結果は

{'電話番号': '70-2345-9876','固定0': None,'固定1': None,
 '固定2': None, '固定3': None, '携帯': '5-9876'}

となり、この電話番号は携帯で加入者番号が5-9876であることが表示される。

Pythonで正規表現:冗長(Verbose)な表現

正規表現はとかく呪文のような読み難い表現になり勝ちであるごが、Pythonでは少し工夫がされていて冗長な表現ができる。実例を示す:


import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (\d{4}) #仙台(22)
    | \d{3}-\d{2}- (\d{4]) #松本(263)
    | \d{4}-\d-    (\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('070-2345-9876')
if m :
    print(m.group(0))
else:
    print(m)

日本の電話番号のマッチングのための正規表現であるが、コメント(#)や空白・改行を使って見やすくできる。

Pythonで正規表現:キャレットの役割(続き)

前のブログの続きである。

正規表現上でキャレットは前述の二つの位置以外では特別な意味は持たないが特殊記号にはちがいない。従って単なる文字としてキャレットとマッチしたいときには

\^

とバックスラッシュ(\)を使う。

[^^]

これは先頭のキャレットが補集合キャレットで二番目のキャレットは単なる文字としてのキャレットである。和集合のなかでは補集合キャレットを例外として全ての特殊記号は単なる文字として振舞う。上の正規表現はキャレットでない任意の一文字でマッチする。

Pythonで正規表現:キャレットの役割

正規表現ではキャレット(^)は特別な役割をしている。

  • 正規表現の先頭にキャレットがあるとそれは対象になる文字列の先頭でマッチングを検証することを意味する。
^abc

の正規表現では文字列abcdはマッチするが、dabcはマッチしない。

  • 文字クラス(角括弧で括られた文字の和集合、例[0-9abc]。この位置で和集合のどれかの文字であるとマッチする)の先頭にキャレットがあるとその補集合、つまりこの和集合のどの文字でもないとマッチすることを意味する。
[^0-9abc]

この正規表現ではこの和集合以外の文字であるとマッチする。

[^a]

これも有効な表現である。

  • この二つの位置以外ではキャレットは特別な意味を持たない。因みに
^a

は先頭の文字がaである文字列でマッチする。

^[^a]

は先頭の文字がa以外である文字列でマッチする。

Pythonで正規表現:先読みアサーション(再論)

具体的な例から考察しよう。ファイル名は基幹名(basename)と拡張子(extension)をドット(.)で区切って表現する。例えば、ファイル名abc.txtはabcが基幹名で、txtが拡張子である。

例えば多くのファイル名のマッチングのタスクで、特定の拡張子を持つファイル名をマッチングから外したいとする(例えば拡張子bat)。

この逆、つまり特定の拡張子を持つファイル名のみマッチングさせたいという処理は極めて簡単である。正規表現で書くと

.*[.]bat$

区別に関心のない基幹名(ドットを含めて)はドットで終わる任意の長さの任意の文字列とのマッチだから.*[.]となる。
この処理の逆、つまり特定の拡張子を持つファイル名をマッチングから除外しそれ以外の多くの拡張子を持つファイル名をマッチさせたいというのが本来のタスクである。このようなばあいに使えるのが「先読みアサーション」である。「先読みアサーション」には否定と肯定とがあるが、ここでは否定を使う。

パターンAパターンB

パターンAまでマッチングが進んだ時点でこれ以降がパターンBであるかどうかをその時点で検証し、であるとマッチングは失敗とする。ないとマッチングは継続する。問題に即して正規表現で書くと

.*[.](?!bat$)

先読みアサーション(否定)は(?!正規表現)と書く。この時点以降の文字列がこの正規表現とマッチするか検証する。それ以降で例えばbatchではマッチしない。rebatもマッチしない。マッチするものはbatのみである。これがマッチするとマッチング作業は失敗である。それがマッチしないと、後はなんの制限もなしにマッチさせればよいので最終的な正規表現は以下のようになる:

.*[.](?!bat$).*$

後読みアサーションというものもある。これも否定と肯定がある。この否定は(?<!..)と書く。例えば「ファイル名の基幹名がabc以外であるとマッチする」というような処理で使える。ファイルの基幹名がabc以外の全てのファイル名にマッチする正規表現は以下のようになる。

(?<!^abc)[.].*$

これは左からドットの位置まで探索を進め、そこで振り返って先頭からドットまでの間の文字列がabcであるとマッチングは失敗とする。それ以外では探査を右に進め通常のマッチングを続ける。この例では.*$となっているので文字列の最後まで任意の長さの任意の文字列がマッチされる。しかし結果としてマッチされる文字列は[.].*$の部分のみである。基幹名もマッチさせるには以下のようにする:

.*(?<!^abc)[.].*$

これで基幹名がabc以外の全てのファイル名がマッチされる。拡張子がなんであれ、基幹名がabcのファイル名はマッチに失敗する。

先読み(後読み)アサーションはこのように上手く使うと大変の便利な機能である。

 

Pythonで正規表現:先読みアサーション

拡張子を持ったファイル名の全てにマッチした正規表現は簡単で


.*[.].*$

となる。

次に拡張子batを持つファイル名以外のファイル名の全てにマッチした正規表現を見てみよう。


.*[.]bat$

と書くと目的に反対に拡張子batを持つファイル名のみにマッチする正規表現になる。


.*[.]([^b]..|.[^a].|..[^t])$

と書くと拡張子の部分は「先頭の文字bがない三文字か、真ん中に文字bがない3文字か、末尾に文字tがない三文字か」のマッチ要求になりよさそうだ。しかし拡張子は三文字とは限らない。

もっとすっきりと表現できる正規表現がある。それが先読みアサーションである。詳細は正規表現 HOWTOにある。


.*[.](?!bat$)[^.]*$

ここで(?!..)は否定先読みアサーションと呼ばれる記述で、今のばあいこの記述位置に文字列bat$があるとマッチは失敗となる(つまり最後尾がbatなっていると失敗)。ないと先に進む。[^.]*$は最後尾から文字.を含まない任意の長さの文字列という意味である。

正規表現 HOWTOには「このパターンで [^.]* を使うことで、ファイル名に複数のドットがあったときにも上手くいくようになります。」とあるが意味不明である。例えば、abc.cd.exeのようなファイル名を考える。このばあいabc.cd.までが.*[.]によってマッチングがされ、[^.]*$によってexeをマッチングする。

 
.*[.]      abc.cd.
[^.*]$      exe

従って複数のドットを処理しているのは.*[.]の部分のように思える。