xython 4.5.1__tar.gz → 4.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xython-4.5.1 → xython-4.5.2}/MANIFEST.in +1 -0
- {xython-4.5.1/src/xython.egg-info → xython-4.5.2}/PKG-INFO +3 -2
- {xython-4.5.1 → xython-4.5.2}/pyproject.toml +3 -3
- {xython-4.5.1 → xython-4.5.2}/src/xython/__init__.py +1 -1
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_chrome.py +2 -1
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_color.py +10 -12
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_common.py +1 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_edge.py +98 -46
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_excel.py +30 -11
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_outlook.py +4 -1
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_re.py +4 -3
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_time.py +5 -5
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_util.py +1 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_word.py +5 -5
- {xython-4.5.1 → xython-4.5.2/src/xython.egg-info}/PKG-INFO +3 -2
- {xython-4.5.1 → xython-4.5.2}/README.md +0 -0
- {xython-4.5.1 → xython-4.5.2}/requirements.txt +0 -0
- {xython-4.5.1 → xython-4.5.2}/setup.cfg +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/_easy_start.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_auto.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_db.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_excel_event.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_hwp.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_list.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython/xy_map.py +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/SOURCES.txt +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/dependency_links.txt +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/requires.txt +0 -0
- {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xython
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.2
|
|
4
4
|
Summary: xython package
|
|
5
5
|
Author-email: "SJ.Park" <sjpkorea@naver.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://blog.naver.com/xython
|
|
8
|
+
Project-URL: Documentation, https://sjpkorea.github.io/xython.github.io/
|
|
8
9
|
Requires-Python: >=3.8
|
|
9
10
|
Description-Content-Type: text/markdown
|
|
10
11
|
Requires-Dist: korean_lunar_calendar
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xython"
|
|
7
|
-
version = "4.5.
|
|
7
|
+
version = "4.5.2"
|
|
8
8
|
description = "xython package"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -26,8 +26,8 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
|
-
Homepage = "https://
|
|
30
|
-
|
|
29
|
+
Homepage = "https://blog.naver.com/xython"
|
|
30
|
+
Documentation = "https://sjpkorea.github.io/xython.github.io/"
|
|
31
31
|
[tool.setuptools.packages.find]
|
|
32
32
|
where = ["src"]
|
|
33
33
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import copy
|
|
2
3
|
from html.parser import HTMLParser
|
|
3
4
|
from xython import xy_util, xy_excel, xy_re
|
|
@@ -1123,7 +1124,7 @@ class xy_chrome:
|
|
|
1123
1124
|
print(f"[테이블 추출] URL: {url}")
|
|
1124
1125
|
print(f" → 총 {len(tables_3d)}개 테이블 발견")
|
|
1125
1126
|
for i, table in enumerate(tables_3d):
|
|
1126
|
-
print(f" 테이블[{i}]: {len(table)}행
|
|
1127
|
+
print(f" 테이블[{i}]: {len(table)}행 x {max(len(r) for r in table) if table else 0}열")
|
|
1127
1128
|
|
|
1128
1129
|
return tables_3d
|
|
1129
1130
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import re, math # 내장모듈
|
|
2
3
|
import win32api, win32gui
|
|
3
4
|
from xython import xy_re, xy_common # xython 모듈
|
|
4
5
|
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
with patch("ctypes.windll.user32.SetProcessDPIAware", autospec=True):
|
|
8
|
+
import pyautogui
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
class xy_color:
|
|
7
12
|
"""
|
|
@@ -140,7 +145,7 @@ class xy_color:
|
|
|
140
145
|
hsl값을 미세조정하는 부분
|
|
141
146
|
|
|
142
147
|
pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
|
|
143
|
-
숫자일 때: 50 기준으로
|
|
148
|
+
숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
|
|
144
149
|
"""
|
|
145
150
|
if type(pm100) == type(123):
|
|
146
151
|
l_value = pm100 - 50 # 50 기준 차이 (음수 가능 → 어둡게도 동작)
|
|
@@ -162,7 +167,7 @@ class xy_color:
|
|
|
162
167
|
hsl값을 미세조정하는 부분
|
|
163
168
|
|
|
164
169
|
pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
|
|
165
|
-
숫자일 때: 50 기준으로
|
|
170
|
+
숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
|
|
166
171
|
"""
|
|
167
172
|
if type(pm100) == type(123):
|
|
168
173
|
l_value = pm100 - 50 # 50 기준 차이 (음수 가능 → 어둡게도 동작)
|
|
@@ -996,7 +1001,7 @@ class xy_color:
|
|
|
996
1001
|
|
|
997
1002
|
def get_analogous_colors(self, input_xcolor, angle=30, count=2):
|
|
998
1003
|
"""
|
|
999
|
-
유사색 생성 (색상환에서
|
|
1004
|
+
유사색 생성 (색상환에서 +-angle도 이내의 색)
|
|
1000
1005
|
count=2이면 양쪽 1개씩, count=4이면 양쪽 2개씩
|
|
1001
1006
|
반환: [[r,g,b], ...]
|
|
1002
1007
|
"""
|
|
@@ -1291,7 +1296,7 @@ class xy_color:
|
|
|
1291
1296
|
|
|
1292
1297
|
def get_split_complementary_colors(self, input_xcolor, angle=150):
|
|
1293
1298
|
"""
|
|
1294
|
-
분리 보색 배색 (보색에서
|
|
1299
|
+
분리 보색 배색 (보색에서 +-30도 위치의 두 색)
|
|
1295
1300
|
반환: [원색rgb, 분리보색1rgb, 분리보색2rgb]
|
|
1296
1301
|
"""
|
|
1297
1302
|
hsl = self.to_hsl(input_xcolor)
|
|
@@ -1616,7 +1621,7 @@ class xy_color:
|
|
|
1616
1621
|
def hsl_to_rgb_by_pm100(self, input_hsl, pm100):
|
|
1617
1622
|
"""
|
|
1618
1623
|
pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
|
|
1619
|
-
숫자일 때: 50 기준으로
|
|
1624
|
+
숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
|
|
1620
1625
|
|
|
1621
1626
|
:param input_hsl: [h,s,l]값
|
|
1622
1627
|
:param pm100:
|
|
@@ -1994,8 +1999,6 @@ class xy_color:
|
|
|
1994
1999
|
result = '#{:02x}{:02x}{:02x}'.format(r, g, b)
|
|
1995
2000
|
return result
|
|
1996
2001
|
|
|
1997
|
-
def rgb_to_hex(self, input_rgb, option="#"):
|
|
1998
|
-
return self.rgb_to_hex(input_rgb, option)
|
|
1999
2002
|
|
|
2000
2003
|
def rgb_to_hex_rgb(self, r, g, b):
|
|
2001
2004
|
"""
|
|
@@ -2279,11 +2282,6 @@ class xy_color:
|
|
|
2279
2282
|
result = self.rgb_to_close_56color_no(rgb_value)
|
|
2280
2283
|
return result
|
|
2281
2284
|
|
|
2282
|
-
def to_hex(self, input_xcolor):
|
|
2283
|
-
"""
|
|
2284
|
-
사용의 편의성을 위해 만듦
|
|
2285
|
-
"""
|
|
2286
|
-
return self.to_hex(input_xcolor)
|
|
2287
2285
|
|
|
2288
2286
|
def to_hex(self, input_xcolor):
|
|
2289
2287
|
"""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import os, time, winreg, socket,re
|
|
2
3
|
from html.parser import HTMLParser
|
|
3
4
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
@@ -168,9 +169,9 @@ class xy_edge:
|
|
|
168
169
|
result.append(p)
|
|
169
170
|
|
|
170
171
|
if result:
|
|
171
|
-
print(f"
|
|
172
|
+
print(f" Edge 경로 발견: {result[0]}")
|
|
172
173
|
else:
|
|
173
|
-
print("
|
|
174
|
+
print(" Edge 경로를 자동으로 찾지 못했습니다.")
|
|
174
175
|
|
|
175
176
|
return result
|
|
176
177
|
|
|
@@ -306,10 +307,10 @@ class xy_edge:
|
|
|
306
307
|
"""
|
|
307
308
|
input_value 에 따라 탭을 선택하고, is_active=True 이면 활성화합니다.
|
|
308
309
|
|
|
309
|
-
None / "" / "pass"
|
|
310
|
-
"visible" / "active" / "activate"
|
|
311
|
-
int
|
|
312
|
-
str
|
|
310
|
+
None / "" / "pass" 현재 상태 유지
|
|
311
|
+
"visible" / "active" / "activate" 화면에 보이는 탭
|
|
312
|
+
int 해당 번호 탭 (1-based)
|
|
313
|
+
str 제목 부분 일치 탭
|
|
313
314
|
"""
|
|
314
315
|
if input_value in (None, "", "pass"):
|
|
315
316
|
pass
|
|
@@ -526,7 +527,7 @@ class xy_edge:
|
|
|
526
527
|
def clear_and_input(self, selector, text):
|
|
527
528
|
"""
|
|
528
529
|
기존 값을 지우고 새 값을 입력합니다.
|
|
529
|
-
Ctrl+A
|
|
530
|
+
Ctrl+A Delete 방식으로 확실하게 지웁니다.
|
|
530
531
|
"""
|
|
531
532
|
try:
|
|
532
533
|
element = self.page.ele(selector, timeout=5)
|
|
@@ -568,7 +569,7 @@ class xy_edge:
|
|
|
568
569
|
def click_by_full_css_path(self, css_path, timeout = 5.0, scroll_to = True):
|
|
569
570
|
"""
|
|
570
571
|
CSS 전체 경로(Full CSS Selector)로 요소를 찾아 클릭합니다.
|
|
571
|
-
DevTools
|
|
572
|
+
DevTools Copy Copy selector 결과를 그대로 사용할 수 있습니다.
|
|
572
573
|
"""
|
|
573
574
|
try:
|
|
574
575
|
element = self._active.ele(f'css:{css_path}', timeout=timeout)
|
|
@@ -582,7 +583,7 @@ class xy_edge:
|
|
|
582
583
|
print(f"[click_by_full_css_path] 클릭 성공: {css_path}")
|
|
583
584
|
return True
|
|
584
585
|
except Exception as e:
|
|
585
|
-
print(f"[click_by_full_css_path] 오류: {css_path}
|
|
586
|
+
print(f"[click_by_full_css_path] 오류: {css_path} {e}")
|
|
586
587
|
return False
|
|
587
588
|
|
|
588
589
|
def click_by_full_path(self, full_xpath):
|
|
@@ -666,7 +667,7 @@ class xy_edge:
|
|
|
666
667
|
time.sleep(wait_time)
|
|
667
668
|
return True
|
|
668
669
|
|
|
669
|
-
print(f"
|
|
670
|
+
print(f"X '{target_text}' 클릭 실패")
|
|
670
671
|
return False
|
|
671
672
|
|
|
672
673
|
except Exception as e:
|
|
@@ -716,7 +717,7 @@ class xy_edge:
|
|
|
716
717
|
print(f"[iframe_click] 요소 없음: {locator}")
|
|
717
718
|
return False
|
|
718
719
|
el.click()
|
|
719
|
-
print(f"
|
|
720
|
+
print(f" iframe 클릭 완료: {locator}")
|
|
720
721
|
return True
|
|
721
722
|
except Exception as e:
|
|
722
723
|
print(f"[iframe_click] 오류: {e}")
|
|
@@ -735,7 +736,7 @@ class xy_edge:
|
|
|
735
736
|
time.sleep(wait_time)
|
|
736
737
|
|
|
737
738
|
if len(self.page.tabs) > initial_tab_count:
|
|
738
|
-
print("
|
|
739
|
+
print(" 새 탭 생성됨")
|
|
739
740
|
self.select_page_by_latest_page()
|
|
740
741
|
time.sleep(0.5)
|
|
741
742
|
|
|
@@ -835,7 +836,7 @@ class xy_edge:
|
|
|
835
836
|
if self.page and (self.new_browser_opened or force):
|
|
836
837
|
try:
|
|
837
838
|
self.page.quit()
|
|
838
|
-
print("
|
|
839
|
+
print(" 브라우저를 종료했습니다.")
|
|
839
840
|
except Exception:
|
|
840
841
|
pass
|
|
841
842
|
|
|
@@ -939,7 +940,7 @@ class xy_edge:
|
|
|
939
940
|
closed += 1
|
|
940
941
|
except Exception:
|
|
941
942
|
pass
|
|
942
|
-
print(f"
|
|
943
|
+
print(f" {closed}개 탭/팝업 닫음 (유지: {current_url})")
|
|
943
944
|
return closed
|
|
944
945
|
except Exception as e:
|
|
945
946
|
print(f"[close_other_tabs] 오류: {e}")
|
|
@@ -1020,13 +1021,12 @@ class xy_edge:
|
|
|
1020
1021
|
if (btn && isVisible(btn)) return btn;
|
|
1021
1022
|
}
|
|
1022
1023
|
|
|
1023
|
-
// 우선순위 3: 텍스트가
|
|
1024
|
+
// 우선순위 3: 텍스트가 닫기 인 버튼
|
|
1024
1025
|
var allBtns = root.querySelectorAll('button, a, span, div, td');
|
|
1025
1026
|
for (var btn of allBtns) {
|
|
1026
1027
|
if (!isVisible(btn)) continue;
|
|
1027
1028
|
var txt = (btn.innerText || btn.textContent || '').trim();
|
|
1028
|
-
if (txt === '닫기' || txt === 'X' || txt === 'x' ||
|
|
1029
|
-
txt === '×' || txt === '✕' || txt === '✖' || txt === 'Close') {
|
|
1029
|
+
if (txt === '닫기' || txt === 'X' || txt === 'x' || txt === 'Close') {
|
|
1030
1030
|
return btn;
|
|
1031
1031
|
}
|
|
1032
1032
|
}
|
|
@@ -1115,7 +1115,7 @@ class xy_edge:
|
|
|
1115
1115
|
closed_count = page.run_js(dhx_close_js)
|
|
1116
1116
|
total_closed = closed_count or 0
|
|
1117
1117
|
|
|
1118
|
-
# 재시도: 0.5초 간격
|
|
1118
|
+
# 재시도: 0.5초 간격 x 최대 5회 (팝업이 순차적으로 뜨는 경우 대비)
|
|
1119
1119
|
for retry in range(5):
|
|
1120
1120
|
time.sleep(0.5)
|
|
1121
1121
|
|
|
@@ -1565,19 +1565,19 @@ class xy_edge:
|
|
|
1565
1565
|
# 1단계: 기존 디버그 브라우저 확인
|
|
1566
1566
|
if self._is_port_open(port):
|
|
1567
1567
|
try:
|
|
1568
|
-
print(f"
|
|
1568
|
+
print(f" 포트 {port}에서 실행 중인 디버그 브라우저 발견! 연결 시도...")
|
|
1569
1569
|
self.page = ChromiumPage(addr_or_opts=f'127.0.0.1:{port}')
|
|
1570
1570
|
self.browser = self.page.browser
|
|
1571
|
-
print("
|
|
1571
|
+
print(" 기존 브라우저 연결 성공!\n")
|
|
1572
1572
|
if self.url:
|
|
1573
|
-
print(f"
|
|
1573
|
+
print(f" URL로 이동: {self.url}")
|
|
1574
1574
|
self.page.get(self.url)
|
|
1575
1575
|
return
|
|
1576
1576
|
except Exception as e:
|
|
1577
|
-
print(f"
|
|
1577
|
+
print(f"X 연결 실패: {e}\n 새 브라우저를 실행합니다...\n")
|
|
1578
1578
|
|
|
1579
1579
|
# 2단계: 새 Edge 실행
|
|
1580
|
-
print(f"
|
|
1580
|
+
print(f" 포트 {port}에 실행 중인 브라우저가 없습니다. 새 Edge를 실행합니다...\n")
|
|
1581
1581
|
|
|
1582
1582
|
co = ChromiumOptions()
|
|
1583
1583
|
co.set_local_port(port)
|
|
@@ -1585,10 +1585,10 @@ class xy_edge:
|
|
|
1585
1585
|
|
|
1586
1586
|
# Edge 경로 설정 — 중복 호출 제거
|
|
1587
1587
|
if self.edge_paths:
|
|
1588
|
-
print(f"
|
|
1588
|
+
print(f" Edge 경로 설정: {self.edge_paths[0]}")
|
|
1589
1589
|
co.set_browser_path(self.edge_paths[0])
|
|
1590
1590
|
else:
|
|
1591
|
-
print("
|
|
1591
|
+
print(" Edge 경로를 찾지 못해 시스템 기본 브라우저를 사용합니다.")
|
|
1592
1592
|
|
|
1593
1593
|
co.set_argument('--test-type')
|
|
1594
1594
|
co.set_argument('--start-maximized')
|
|
@@ -1605,12 +1605,12 @@ class xy_edge:
|
|
|
1605
1605
|
self.page = ChromiumPage(co)
|
|
1606
1606
|
self.browser = self.page.browser
|
|
1607
1607
|
self.new_browser_opened = True
|
|
1608
|
-
print("
|
|
1608
|
+
print(" 새 Edge 브라우저 실행 성공!\n")
|
|
1609
1609
|
if self.url:
|
|
1610
|
-
print(f"
|
|
1610
|
+
print(f" URL로 이동: {self.url}")
|
|
1611
1611
|
self.page.get(self.url)
|
|
1612
1612
|
except Exception as e:
|
|
1613
|
-
print(f"
|
|
1613
|
+
print(f"X Edge 실행 실패: {e}")
|
|
1614
1614
|
raise
|
|
1615
1615
|
|
|
1616
1616
|
def count_iframe(self):
|
|
@@ -1654,7 +1654,7 @@ class xy_edge:
|
|
|
1654
1654
|
|
|
1655
1655
|
def drag_and_drop(self, source_selector, target_selector, timeout = 5.0):
|
|
1656
1656
|
"""
|
|
1657
|
-
요소
|
|
1657
|
+
요소 요소 드래그 앤 드롭 (드롭존이 file input 이 아닌 경우).
|
|
1658
1658
|
파일 드롭은 drop_file_to_element() 를 사용하세요.
|
|
1659
1659
|
"""
|
|
1660
1660
|
try:
|
|
@@ -1861,8 +1861,8 @@ class xy_edge:
|
|
|
1861
1861
|
print(
|
|
1862
1862
|
f" [{idx}] <{info['tag']}> '{info['text'][:40]}' "
|
|
1863
1863
|
f"| 중심=({center_x},{center_y}) "
|
|
1864
|
-
f"| {rect['width']}
|
|
1865
|
-
f"| {'
|
|
1864
|
+
f"| {rect['width']}x{rect['height']} "
|
|
1865
|
+
f"| {'' if is_visible else 'X'}"
|
|
1866
1866
|
)
|
|
1867
1867
|
except Exception as e:
|
|
1868
1868
|
print(f" [{idx}] 정보 수집 오류: {e}")
|
|
@@ -1955,7 +1955,7 @@ class xy_edge:
|
|
|
1955
1955
|
|
|
1956
1956
|
# 모든 항목이 포함된 경우
|
|
1957
1957
|
if not missing:
|
|
1958
|
-
print(f" 완전 매칭!
|
|
1958
|
+
print(f" 완전 매칭! iframe_id: '{iframe_id}'")
|
|
1959
1959
|
matched_ids.append(iframe_id)
|
|
1960
1960
|
|
|
1961
1961
|
except Exception as e:
|
|
@@ -2216,10 +2216,10 @@ class xy_edge:
|
|
|
2216
2216
|
}})()
|
|
2217
2217
|
"""
|
|
2218
2218
|
tables_3d = self.page.run_js(js) or []
|
|
2219
|
-
print(f"[테이블 추출] URL: {url}
|
|
2219
|
+
print(f"[테이블 추출] URL: {url} -> 총 {len(tables_3d)}개 테이블")
|
|
2220
2220
|
for i, table in enumerate(tables_3d):
|
|
2221
2221
|
cols = max(len(r) for r in table) if table else 0
|
|
2222
|
-
print(f" 테이블[{i}]: {len(table)}행
|
|
2222
|
+
print(f" 테이블[{i}]: {len(table)}행 x {cols}열")
|
|
2223
2223
|
return tables_3d
|
|
2224
2224
|
|
|
2225
2225
|
def get_all_text_by_iframe_id(self, iframe_id):
|
|
@@ -2378,7 +2378,7 @@ class xy_edge:
|
|
|
2378
2378
|
}
|
|
2379
2379
|
print(
|
|
2380
2380
|
f"[캐럿 위치] 태그={info['element_tag']} 인덱스={info['caret_index']} "
|
|
2381
|
-
f"스크린({sx},{sy})
|
|
2381
|
+
f"스크린({sx},{sy}) 모니터{monitor} 로컬({local_x},{sy})"
|
|
2382
2382
|
)
|
|
2383
2383
|
return info
|
|
2384
2384
|
|
|
@@ -2443,9 +2443,9 @@ class xy_edge:
|
|
|
2443
2443
|
|
|
2444
2444
|
Args:
|
|
2445
2445
|
path: [grand_idx, papa_idx, son_idx, ...] 형태의 정수 리스트.
|
|
2446
|
-
예) [0]
|
|
2447
|
-
[0, 1]
|
|
2448
|
-
[0, 1, 0]
|
|
2446
|
+
예) [0] 최상위 첫 번째 iframe
|
|
2447
|
+
[0, 1] 최상위 첫 번째 iframe 안의 두 번째 자식 iframe
|
|
2448
|
+
[0, 1, 0] 그 아래 첫 번째 손자 iframe
|
|
2449
2449
|
|
|
2450
2450
|
Returns:
|
|
2451
2451
|
frame 객체 또는 None (경로가 잘못된 경우)
|
|
@@ -2529,7 +2529,7 @@ class xy_edge:
|
|
|
2529
2529
|
local_x = x if monitor == 1 else x - monitor_width
|
|
2530
2530
|
|
|
2531
2531
|
info = {"x": x, "y": y, "monitor": monitor, "local_x": local_x, "local_y": y}
|
|
2532
|
-
print(f"[마우스 위치] 절대({x}, {y})
|
|
2532
|
+
print(f"[마우스 위치] 절대({x}, {y}) 모니터 {monitor}번 로컬({local_x}, {y})")
|
|
2533
2533
|
return info
|
|
2534
2534
|
|
|
2535
2535
|
def get_page_data_by_xpath_v2(self, xpath, tab_name=None):
|
|
@@ -2805,7 +2805,7 @@ class xy_edge:
|
|
|
2805
2805
|
button = self.page.ele(selector, timeout=timeout)
|
|
2806
2806
|
if button:
|
|
2807
2807
|
button.click()
|
|
2808
|
-
print("
|
|
2808
|
+
print(" '무시하고 보내기' 버튼 클릭 완료")
|
|
2809
2809
|
time.sleep(0.5)
|
|
2810
2810
|
return True
|
|
2811
2811
|
except Exception:
|
|
@@ -2895,7 +2895,7 @@ class xy_edge:
|
|
|
2895
2895
|
cursor_mark = " ★★★ [클릭가능]" if el.get('cursor') in ['pointer', 'hand'] else ""
|
|
2896
2896
|
close_mark = " ★ [CLOSE관련]" if any(
|
|
2897
2897
|
k in (el.get('className', '') + el.get('text', '')).lower()
|
|
2898
|
-
for k in ['close', '닫기', '
|
|
2898
|
+
for k in ['close', '닫기', 'X', 'x', 'btn', 'button']
|
|
2899
2899
|
) else ""
|
|
2900
2900
|
|
|
2901
2901
|
print(f" [{el.get('rootClass', '')}]")
|
|
@@ -3179,7 +3179,7 @@ class xy_edge:
|
|
|
3179
3179
|
"""
|
|
3180
3180
|
try:
|
|
3181
3181
|
frame_obj.get_screenshot(path=save_path)
|
|
3182
|
-
print(f"
|
|
3182
|
+
print(f" iframe 스크린샷 저장: {save_path}")
|
|
3183
3183
|
return True
|
|
3184
3184
|
except Exception as e:
|
|
3185
3185
|
print(f"[iframe_screenshot] 오류: {e}")
|
|
@@ -3208,7 +3208,7 @@ class xy_edge:
|
|
|
3208
3208
|
"""
|
|
3209
3209
|
result = self.page.run_js(js)
|
|
3210
3210
|
time.sleep(0.4)
|
|
3211
|
-
print(f"[scroll_to_make_visible_by_coord] 뷰포트({viewport_x}, {viewport_y})
|
|
3211
|
+
print(f"[scroll_to_make_visible_by_coord] 뷰포트({viewport_x}, {viewport_y}) {result}px")
|
|
3212
3212
|
return True
|
|
3213
3213
|
except Exception as e:
|
|
3214
3214
|
print(f"[scroll_to_make_visible_by_coord] 오류: {e}")
|
|
@@ -3286,7 +3286,7 @@ class xy_edge:
|
|
|
3286
3286
|
print(f"select 요소를 찾을 수 없음 (id: {select_id})")
|
|
3287
3287
|
return False
|
|
3288
3288
|
|
|
3289
|
-
print(f"
|
|
3289
|
+
print(f" select 요소 발견!")
|
|
3290
3290
|
|
|
3291
3291
|
options = select_element.eles('tag:option')
|
|
3292
3292
|
print(f"옵션 개수: {len(options)}")
|
|
@@ -3403,7 +3403,7 @@ class xy_edge:
|
|
|
3403
3403
|
if opt.attr("value") == option_value:
|
|
3404
3404
|
opt.click()
|
|
3405
3405
|
break
|
|
3406
|
-
print(f"
|
|
3406
|
+
print(f" iframe select 완료: {locator}")
|
|
3407
3407
|
return True
|
|
3408
3408
|
except Exception as e:
|
|
3409
3409
|
print(f"[iframe_select_option] 오류: {e}")
|
|
@@ -3687,10 +3687,62 @@ class xy_edge:
|
|
|
3687
3687
|
if clear_first:
|
|
3688
3688
|
el.clear()
|
|
3689
3689
|
el.input(value)
|
|
3690
|
-
print(f"
|
|
3690
|
+
print(f" iframe 입력 완료: {locator} ← '{value}'")
|
|
3691
3691
|
return True
|
|
3692
3692
|
except Exception as e:
|
|
3693
3693
|
print(f"[iframe_write] 오류: {e}")
|
|
3694
3694
|
return False
|
|
3695
3695
|
|
|
3696
3696
|
|
|
3697
|
+
def downLoad_attachments_for_1st(self):
|
|
3698
|
+
#회사의 첨부화일을 다운 받는 함수
|
|
3699
|
+
attach_area = self.page.ele('#AttFileInfo')
|
|
3700
|
+
if not attach_area:
|
|
3701
|
+
print("첨부파일 없음")
|
|
3702
|
+
return
|
|
3703
|
+
inks = attach_area.eles('tag:a')
|
|
3704
|
+
print(f'총 {len(links)}개 파일 발견\n')
|
|
3705
|
+
|
|
3706
|
+
for link in enumerate(Links):
|
|
3707
|
+
print(f"[{i+1}/{len(links)}] {link.text.strip()}클릭")
|
|
3708
|
+
|
|
3709
|
+
try:
|
|
3710
|
+
Link.click()
|
|
3711
|
+
except Exception as e:
|
|
3712
|
+
print(f'?? 클릭 오류: {e}')
|
|
3713
|
+
continue
|
|
3714
|
+
time.sleep(0.5) #클릭 간 짧은 간격만 줌
|
|
3715
|
+
|
|
3716
|
+
print('*클릭 완료! 브라우저가 알아서 다운로드 중...')
|
|
3717
|
+
|
|
3718
|
+
def save_page_as_pdf(self, filename=None, folder=None):
|
|
3719
|
+
# 현재 연결된 페이지를 PDF로
|
|
3720
|
+
if not filename.endswith(".pdf"): filename +=".pdf"
|
|
3721
|
+
|
|
3722
|
+
if not folder:
|
|
3723
|
+
download_folder = Path.home() / "Downloads"
|
|
3724
|
+
else:
|
|
3725
|
+
download_folder = folder
|
|
3726
|
+
|
|
3727
|
+
save_path = os.path.join(download_folder, filename)
|
|
3728
|
+
pdf_params = {
|
|
3729
|
+
"printBackground": True,
|
|
3730
|
+
"paperWidth": 8.27, # A4 L₩ (inch) - 210mm
|
|
3731
|
+
"aperHeight": 11.69, # A4 ₩0l (inch) - 297mm
|
|
3732
|
+
"marginTop": 0.5,
|
|
3733
|
+
"marginBottom":0.5,
|
|
3734
|
+
"marginleft": 0.5,
|
|
3735
|
+
"marginRight": 0.5,
|
|
3736
|
+
"displayHeaderFooter": False, # 혜더/푸터 술길
|
|
3737
|
+
"scale": 1.0,
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
# PDF 생성 및 지장
|
|
3741
|
+
pdf_data = self.page.run_cdp("Page.printToPDF", **pdf_params)
|
|
3742
|
+
# Base64 디코딩 후 파일 제품
|
|
3743
|
+
|
|
3744
|
+
pdf_bytes = base64.b64decode(pdf_data["data"])
|
|
3745
|
+
with open(save_path, "wb") as f:
|
|
3746
|
+
f.write(pdf_bytes)
|
|
3747
|
+
print(f"PDF 저장 완료: (save_path)")
|
|
3748
|
+
return save_path
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import re, math, string, random, os, itertools, copy, time
|
|
2
3
|
import pywintypes, webbrowser, psutil
|
|
3
4
|
import ctypes
|
|
@@ -118,12 +119,16 @@ class xy_excel:
|
|
|
118
119
|
def add_text_at_left(self, sheet_name='', xyxy='', input_value=None):
|
|
119
120
|
sheet, [x1, y1, x2, y2], rng = self.check_sheet_n_xyxy(sheet_name, xyxy)
|
|
120
121
|
for x, y in itertools.product(range(x1, x2 + 1), range(y1, y2 + 1)):
|
|
121
|
-
|
|
122
|
+
current_value = sheet.Cells(x, y).Value
|
|
123
|
+
if current_value == None: current_value = ""
|
|
124
|
+
sheet.Cells(x, y).Value = str(input_value) + str(current_value)
|
|
122
125
|
|
|
123
126
|
def add_text_at_right(self, sheet_name='', xyxy='', input_value=None):
|
|
124
127
|
sheet, [x1, y1, x2, y2], rng = self.check_sheet_n_xyxy(sheet_name, xyxy)
|
|
125
128
|
for x, y in itertools.product(range(x1, x2 + 1), range(y1, y2 + 1)):
|
|
126
|
-
|
|
129
|
+
current_value = sheet.Cells(x, y).Value
|
|
130
|
+
if current_value == None: current_value = ""
|
|
131
|
+
sheet.Cells(x, y).Value = str(current_value) + str(input_value)
|
|
127
132
|
|
|
128
133
|
def all_sheet_name(self):
|
|
129
134
|
"""get_all_sheet_name 별칭 (하위 호환)."""
|
|
@@ -10671,7 +10676,7 @@ class xy_excel:
|
|
|
10671
10676
|
sheet.Cells(write_x, write_y).Value = write_x, write_y
|
|
10672
10677
|
|
|
10673
10678
|
def wait_ready(self, timeout_sec=30, interval=0.2):
|
|
10674
|
-
"""
|
|
10679
|
+
"""계산 : 화면 갱신 대기"""
|
|
10675
10680
|
end = time.time() + timeout_sec
|
|
10676
10681
|
while time.time() < end:
|
|
10677
10682
|
try:
|
|
@@ -11380,32 +11385,32 @@ class xy_excel:
|
|
|
11380
11385
|
if data_type == "l1d":
|
|
11381
11386
|
data = self.varx["data"][input_value]
|
|
11382
11387
|
self.new_sheet()
|
|
11383
|
-
self.write("", [1, 1], data)
|
|
11388
|
+
self.write("", [1, 1], data, True)
|
|
11384
11389
|
elif data_type == "l2d":
|
|
11385
11390
|
data = self.varx["data"][input_value]
|
|
11386
11391
|
self.new_sheet()
|
|
11387
|
-
self.write("", [1, 1], data)
|
|
11392
|
+
self.write("", [1, 1], data, True)
|
|
11388
11393
|
elif data_type == "t2d":
|
|
11389
11394
|
data = self.varx["data"][input_value]
|
|
11390
11395
|
changed_data = data.splitlines()
|
|
11391
11396
|
self.new_sheet()
|
|
11392
|
-
self.write("", [1, 1], changed_data)
|
|
11397
|
+
self.write("", [1, 1], changed_data, True)
|
|
11393
11398
|
elif data_type == "table2d":
|
|
11394
11399
|
data = self.varx["data"][input_value]
|
|
11395
11400
|
self.new_sheet()
|
|
11396
|
-
self.write("", [1, 1], data)
|
|
11401
|
+
self.write("", [1, 1], data, True)
|
|
11397
11402
|
elif data_type == "d1d":
|
|
11398
11403
|
data = self.varx["data"][input_value]
|
|
11399
11404
|
self.new_sheet()
|
|
11400
11405
|
for index, key in enumerate(data.keys()):
|
|
11401
|
-
self.write("", [1 + index, 1], key)
|
|
11402
|
-
self.write("", [1 + index, 2], str(data[key]))
|
|
11406
|
+
self.write("", [1 + index, 1], key, True)
|
|
11407
|
+
self.write("", [1 + index, 2], str(data[key]), True)
|
|
11403
11408
|
elif data_type == "d2d":
|
|
11404
11409
|
data = self.varx["data"][input_value]
|
|
11405
11410
|
self.new_sheet()
|
|
11406
11411
|
for index, key in enumerate(data.keys()):
|
|
11407
|
-
self.write("", [1 + index, 1], key)
|
|
11408
|
-
self.write("", [1 + index, 2], str(data[key]))
|
|
11412
|
+
self.write("", [1 + index, 1], key, True)
|
|
11413
|
+
self.write("", [1 + index, 2], str(data[key]), True)
|
|
11409
11414
|
|
|
11410
11415
|
def write_title_from_first_line(self, sheet_name='', xyxy=''):
|
|
11411
11416
|
"""
|
|
@@ -11527,4 +11532,18 @@ class xy_excel:
|
|
|
11527
11532
|
merged_wb.Close()
|
|
11528
11533
|
|
|
11529
11534
|
|
|
11535
|
+
def all_data_for_quote_no(self, gijun_no):
|
|
11536
|
+
# 엑셀의 관리시트에서 관리 번호를 입력하면 그에대한 정보를 갖고오는것
|
|
11537
|
+
data_start_x= 2
|
|
11538
|
+
xy = self.get_address_for_bottom_end("", [3,5])
|
|
11539
|
+
if xy[0]-50 < data_start_x:
|
|
11540
|
+
xy[0] =50+ data_start_x
|
|
11541
|
+
l2d_15 = self.read_range("", [xy[0]-50, 1, xy[0], 40])
|
|
11542
|
+
|
|
11543
|
+
#맨앞부분에 엑셀의 가로본호를 넣는다 (엑셀의 위치를 알고싶어서)
|
|
11544
|
+
for index, item in enumerate(l2d_15):
|
|
11545
|
+
item.append(index+xy[0]-50)
|
|
11546
|
+
# 읽어온 15개의 자료중에 기준번호와 같은것만 필터링하는 것
|
|
11547
|
+
l2d_filtered = self.xyutil.filter_12d_for_same_with_yline_and_value(l2d_15, 3, gijun_no)
|
|
11548
|
+
return l2d_filtered
|
|
11530
11549
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import os, datetime, csv # 내장모듈
|
|
2
3
|
import win32com.client # pywin32의 모듈
|
|
3
4
|
from collections import Counter
|
|
@@ -23,6 +24,8 @@ class xy_outlook:
|
|
|
23
24
|
'Importance', 'Sensitivity', 'Categories', 'Attachments',
|
|
24
25
|
'ConversationTopic', 'Size', 'UnRead', 'EntryID', ] # Importance 중복 제거, 유용한 속성 추가
|
|
25
26
|
|
|
27
|
+
|
|
28
|
+
def start(self):
|
|
26
29
|
self.outlook_program = win32com.client.dynamic.Dispatch('Outlook.Application')
|
|
27
30
|
self.outlook = self.outlook_program.GetNamespace("MAPI")
|
|
28
31
|
|
|
@@ -687,7 +690,7 @@ class xy_outlook:
|
|
|
687
690
|
|
|
688
691
|
def get_attachment_info_for_mail(self, one_mail):
|
|
689
692
|
"""
|
|
690
|
-
메일 첨부파일의
|
|
693
|
+
메일 첨부파일의 이름/크기를 딕셔너리 리스트로 반환
|
|
691
694
|
[{"name": "file.xlsx", "size": 12345}, ...]
|
|
692
695
|
"""
|
|
693
696
|
result = []
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
import re
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -567,7 +568,7 @@ class xy_re:
|
|
|
567
568
|
return bool(re.match(r"^[0-9.]+$", input_text))
|
|
568
569
|
|
|
569
570
|
def is_resident_id(self, input_text):
|
|
570
|
-
"""주민등록번호 형식(13자리,
|
|
571
|
+
"""주민등록번호 형식(13자리, 날짜/성별 자릿수)."""
|
|
571
572
|
text = str(input_text).replace("-", "").strip()
|
|
572
573
|
return bool(
|
|
573
574
|
re.fullmatch(r"\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{7}", text)
|
|
@@ -604,7 +605,7 @@ class xy_re:
|
|
|
604
605
|
return self.search_with_resql(resql, input_text)
|
|
605
606
|
|
|
606
607
|
def normalize_whitespace(self, input_text):
|
|
607
|
-
"""연속
|
|
608
|
+
"""연속 공백/빈줄 정리."""
|
|
608
609
|
text = re.sub(r"[ \t]+", " ", input_text)
|
|
609
610
|
return self.delete_over_2_empty_lines(text).strip()
|
|
610
611
|
|
|
@@ -896,7 +897,7 @@ class xy_re:
|
|
|
896
897
|
return self.search_with_resql(self.change_xsql_to_resql(input_xsql), input_text)
|
|
897
898
|
|
|
898
899
|
def setup_begin_end(self, begin=True, end=True):
|
|
899
|
-
"""전체 일치용 [시작]/[끝]
|
|
900
|
+
"""전체 일치용 [시작]/[끝] 접두/접미 설정"""
|
|
900
901
|
self.setup["begin"] = begin
|
|
901
902
|
self.setup["end"] = end
|
|
902
903
|
|
|
@@ -7,8 +7,8 @@ from datetime import datetime, timezone, timedelta, date
|
|
|
7
7
|
|
|
8
8
|
from xython import xy_re, xy_common # xython 모듈
|
|
9
9
|
"""
|
|
10
|
-
공휴일 = 법정 공휴일로 지정된 날 (신정, 설날, 추석 등). 그날이
|
|
11
|
-
주말 =
|
|
10
|
+
공휴일 = 법정 공휴일로 지정된 날 (신정, 설날, 추석 등). 그날이 토/일요일과 겹쳐도 여전히 공휴일입니다.
|
|
11
|
+
주말 = 토/일요일 자체. 법정 공휴일이 아닌 일반 토/일요일은 "주말"이지 "공휴일"이 아닙니다.
|
|
12
12
|
겹치는 경우 = 예를 들어 현충일(6/6)이 토요일이면 → 공휴일이면서 동시에 주말입니다.
|
|
13
13
|
"""
|
|
14
14
|
|
|
@@ -1764,7 +1764,7 @@ class xy_time():
|
|
|
1764
1764
|
def check_temp_holiday(self, base_date: date, rule: str, already: set):
|
|
1765
1765
|
"""
|
|
1766
1766
|
대체공휴일 계산.
|
|
1767
|
-
rule: "" → 없음 / "일" → 일요일 겹침 / "토일" →
|
|
1767
|
+
rule: "" → 없음 / "일" → 일요일 겹침 / "토일" → 토/일요일 겹침
|
|
1768
1768
|
"""
|
|
1769
1769
|
if not rule:
|
|
1770
1770
|
return None
|
|
@@ -2209,7 +2209,7 @@ class xy_time():
|
|
|
2209
2209
|
|
|
2210
2210
|
분류 기준:
|
|
2211
2211
|
- holiday : 법정 공휴일 (주말과 겹쳐도 공휴일 우선)
|
|
2212
|
-
- weekend : 공휴일이 아닌 순수
|
|
2212
|
+
- weekend : 공휴일이 아닌 순수 토/일요일
|
|
2213
2213
|
- workday : 근무일(평일 + 비공휴일)
|
|
2214
2214
|
|
|
2215
2215
|
Parameters
|
|
@@ -2713,7 +2713,7 @@ class xy_time():
|
|
|
2713
2713
|
|
|
2714
2714
|
def get_holidays_in_range_include_weekend(self, start, end) -> list:
|
|
2715
2715
|
"""
|
|
2716
|
-
start ~ end 사이의 공휴일 + 주말(
|
|
2716
|
+
start ~ end 사이의 공휴일 + 주말(토/일)을 날짜 순서대로 반환합니다.
|
|
2717
2717
|
(공휴일과 주말을 모두 포함, 근무일만 제외)
|
|
2718
2718
|
|
|
2719
2719
|
Parameters
|
|
@@ -6434,7 +6434,7 @@ class xy_word:
|
|
|
6434
6434
|
|
|
6435
6435
|
|
|
6436
6436
|
def normalize(self, text: str) -> str:
|
|
6437
|
-
"""
|
|
6437
|
+
"""공백/줄바꿈/탭 등을 모두 제거한 순수 문자열 반환"""
|
|
6438
6438
|
return "".join(text.split())
|
|
6439
6439
|
|
|
6440
6440
|
|
|
@@ -6490,7 +6490,7 @@ class xy_word:
|
|
|
6490
6490
|
total_sections = len(section_ranges)
|
|
6491
6491
|
print(f"[1단계] 총 {total_sections}개 섹션 발견")
|
|
6492
6492
|
|
|
6493
|
-
# 제목도 normalize() 적용하여
|
|
6493
|
+
# 제목도 normalize() 적용하여 공백/줄바꿈 제거 후 그룹화
|
|
6494
6494
|
title_sections = defaultdict(list)
|
|
6495
6495
|
for idx, section in enumerate(section_ranges):
|
|
6496
6496
|
normalized_title = self.normalize(section["prev_title"])
|
|
@@ -6508,7 +6508,7 @@ class xy_word:
|
|
|
6508
6508
|
if dup_title_count > 0:
|
|
6509
6509
|
print("\n [중복 제목 목록]")
|
|
6510
6510
|
for title, indices in duplicates_to_check.items():
|
|
6511
|
-
print(f"
|
|
6511
|
+
print(f" '{title}' — {len(indices)}개 발견")
|
|
6512
6512
|
print()
|
|
6513
6513
|
|
|
6514
6514
|
ranges_to_delete = []
|
|
@@ -6520,7 +6520,7 @@ class xy_word:
|
|
|
6520
6520
|
section = section_ranges[idx]
|
|
6521
6521
|
content_range = self.doc.Range(section['start'], section['end'])
|
|
6522
6522
|
|
|
6523
|
-
# 내용도 normalize() 적용 —
|
|
6523
|
+
# 내용도 normalize() 적용 — 공백/줄바꿈 완전 제거 후 순수 값만 비교
|
|
6524
6524
|
content = self.normalize(content_range.Text)
|
|
6525
6525
|
|
|
6526
6526
|
if content in content_map:
|
|
@@ -6535,7 +6535,7 @@ class xy_word:
|
|
|
6535
6535
|
if actual_dup_count > 0:
|
|
6536
6536
|
print("\n [삭제 예정 항목]")
|
|
6537
6537
|
for title, count in deleted_titles.items():
|
|
6538
|
-
print(f"
|
|
6538
|
+
print(f" '{title}' — {count}개 삭제")
|
|
6539
6539
|
print()
|
|
6540
6540
|
|
|
6541
6541
|
# 역순 삭제
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xython
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.2
|
|
4
4
|
Summary: xython package
|
|
5
5
|
Author-email: "SJ.Park" <sjpkorea@naver.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://blog.naver.com/xython
|
|
8
|
+
Project-URL: Documentation, https://sjpkorea.github.io/xython.github.io/
|
|
8
9
|
Requires-Python: >=3.8
|
|
9
10
|
Description-Content-Type: text/markdown
|
|
10
11
|
Requires-Dist: korean_lunar_calendar
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|