simtoolsz 0.2.13__tar.gz → 0.2.14__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.
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/PKG-INFO +1 -1
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/pyproject.toml +1 -1
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/__init__.py +3 -1
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/reader.py +257 -1
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.github/workflows/publish.yml +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.github/workflows/test.yml +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.gitignore +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.python-version +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/LICENSE +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_EN.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_countrycode.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_countrycode_en.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/DATETIME_CONVERSION.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/iso3166-1.xlsx +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/mail_usage_guide.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/special2db_usage.md +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/conversion_examples.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/mail_examples.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/special2db_example.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/today_examples.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/zip2db_example.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/requirements-dev.lock +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/requirements.lock +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/columns_info +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/country.parquet +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/countrycode.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/datetime.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/db.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/mail.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/utils.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_conversion.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_countrycode.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_countrycode_optimization.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_iso_comprehensive.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_iso_format.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_optimized_reader.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_simple.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_smoke.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_special2db.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_special2db_simple.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_today_optimized.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_which_format.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_zip2db.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_zip2db_simple.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/verify_unicode_fix.py +0 -0
- {simtoolsz-0.2.13 → simtoolsz-0.2.14}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: simtoolsz
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: A simple and convenient toolkit containing useful functions, classes, and methods.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SidneyLYZhang/simtoolsz
|
|
6
6
|
Project-URL: Repository, https://github.com/SidneyLYZhang/simtoolsz.git
|
|
@@ -25,7 +25,9 @@ import simtoolsz.countrycode as countrycode
|
|
|
25
25
|
try:
|
|
26
26
|
__version__ = importlib.metadata.version("simtoolsz")
|
|
27
27
|
except importlib.metadata.PackageNotFoundError:
|
|
28
|
-
__version__ = "0.2.
|
|
28
|
+
__version__ = "0.2.14"
|
|
29
|
+
|
|
30
|
+
__author__ = "Sidney Zhang <zly@lyzhang.me>"
|
|
29
31
|
|
|
30
32
|
__all__ = [
|
|
31
33
|
'__version__', 'mail', 'utils', 'datetime',
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import warnings
|
|
29
|
+
import io
|
|
29
30
|
import re
|
|
30
31
|
import polars as pl
|
|
31
32
|
|
|
@@ -40,7 +41,7 @@ from tempfile import TemporaryDirectory
|
|
|
40
41
|
|
|
41
42
|
__all__ = [
|
|
42
43
|
"read_tsv", "scan_tsv", "getreader", "load_data", "read_archive",
|
|
43
|
-
"is_archive_file", "excel_sheet_names", "load_excel"
|
|
44
|
+
"is_archive_file", "excel_sheet_names", "load_excel", "read_csv_advanced"
|
|
44
45
|
]
|
|
45
46
|
|
|
46
47
|
|
|
@@ -651,3 +652,258 @@ def load_excel(
|
|
|
651
652
|
else:
|
|
652
653
|
raise ValueError(f"Sheet {sheet_name} not found in Excel file")
|
|
653
654
|
return df
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def read_csv_advanced(
|
|
658
|
+
path: str | Path,
|
|
659
|
+
csv_name: Optional[str] = None,
|
|
660
|
+
start_marker: Optional[str] = "#-------------------------",
|
|
661
|
+
end_marker: Optional[str] = None,
|
|
662
|
+
encoding: str = "utf-8",
|
|
663
|
+
**read_kwargs
|
|
664
|
+
) -> pl.DataFrame:
|
|
665
|
+
"""
|
|
666
|
+
从 ZIP 压缩包或文件夹中读取被标记行包裹的 CSV 数据。
|
|
667
|
+
|
|
668
|
+
匹配规则:只要行首(忽略前导空格)以标识符字符串开头即视为边界,
|
|
669
|
+
例如 "#------------------------- 数据开始" 会被正确识别。
|
|
670
|
+
支持不同的起始和结束标识符,标识符只需匹配行的前缀即可。
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
path: ZIP 文件路径或文件夹路径
|
|
674
|
+
csv_name: ZIP 内或文件夹中的 CSV 文件名(可选,单 CSV 文件时可自动检测)
|
|
675
|
+
start_marker: 起始边界标记的前缀字符串(默认 "#-------------------------")
|
|
676
|
+
设为 None 则从文件开头读取
|
|
677
|
+
end_marker: 结束边界标记的前缀字符串(默认 None,表示与 start_marker 相同)
|
|
678
|
+
设为 None 则使用 start_marker 作为结束标记
|
|
679
|
+
encoding: 文件编码(默认 utf-8,中文环境常见 gbk/utf-8-sig)
|
|
680
|
+
**read_kwargs: 传递给 polars.read_csv 的额外参数(如 separator, header 等)
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
pl.DataFrame: 解析后的数据帧
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
FileNotFoundError: 路径不存在
|
|
687
|
+
ValueError: 未找到 CSV 文件或数据区域为空
|
|
688
|
+
|
|
689
|
+
Examples:
|
|
690
|
+
>>> # 从 ZIP 文件读取(默认标记)
|
|
691
|
+
>>> df = read_csv_from_zip("data.zip", encoding="gbk")
|
|
692
|
+
|
|
693
|
+
>>> # 从文件夹读取
|
|
694
|
+
>>> df = read_csv_from_zip("./data_folder", separator="|")
|
|
695
|
+
|
|
696
|
+
>>> # 使用不同的起始和结束标记
|
|
697
|
+
>>> df = read_csv_from_zip("data.zip", start_marker="=== BEGIN ===", end_marker="=== END ===")
|
|
698
|
+
|
|
699
|
+
>>> # 无标记,读取整个文件
|
|
700
|
+
>>> df = read_csv_from_zip("data.zip", start_marker=None)
|
|
701
|
+
"""
|
|
702
|
+
path = Path(path)
|
|
703
|
+
if not path.exists():
|
|
704
|
+
raise FileNotFoundError(f"路径不存在: {path}")
|
|
705
|
+
|
|
706
|
+
if end_marker is None:
|
|
707
|
+
end_marker = start_marker
|
|
708
|
+
|
|
709
|
+
content = _read_csv_content(path, csv_name, encoding)
|
|
710
|
+
data_lines = _extract_data_lines(content, start_marker, end_marker)
|
|
711
|
+
|
|
712
|
+
if not data_lines:
|
|
713
|
+
raise ValueError("提取的数据区域为空,请检查标记是否正确")
|
|
714
|
+
|
|
715
|
+
csv_content = "\n".join(data_lines)
|
|
716
|
+
|
|
717
|
+
return pl.read_csv(
|
|
718
|
+
io.BytesIO(csv_content.encode(encoding)),
|
|
719
|
+
**read_kwargs
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _read_csv_content(path: Path, csv_name: Optional[str], encoding: str) -> str:
|
|
724
|
+
"""
|
|
725
|
+
从 ZIP 文件或文件夹中读取 CSV 文件的文本内容。
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
path: ZIP 文件路径或文件夹路径
|
|
729
|
+
csv_name: CSV 文件名(可选)
|
|
730
|
+
encoding: 文件编码
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
str: CSV 文件的文本内容
|
|
734
|
+
"""
|
|
735
|
+
if path.is_file() and path.suffix.lower() == '.zip':
|
|
736
|
+
return _read_from_zip_file(path, csv_name, encoding)
|
|
737
|
+
elif path.is_dir():
|
|
738
|
+
return _read_from_directory(path, csv_name, encoding)
|
|
739
|
+
else:
|
|
740
|
+
raise ValueError(f"路径必须是 ZIP 文件或文件夹: {path}")
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _read_from_zip_file(zip_path: Path, csv_name: Optional[str], encoding: str) -> str:
|
|
744
|
+
"""
|
|
745
|
+
从 ZIP 文件中读取 CSV 内容。
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
zip_path: ZIP 文件路径
|
|
749
|
+
csv_name: CSV 文件名(可选)
|
|
750
|
+
encoding: 文件编码
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
str: CSV 文件的文本内容
|
|
754
|
+
"""
|
|
755
|
+
with ZipFile(zip_path, "r") as zf:
|
|
756
|
+
csv_name = _resolve_csv_name_zip(zf, csv_name, zip_path)
|
|
757
|
+
with zf.open(csv_name) as f:
|
|
758
|
+
actual_encoding = "utf-8-sig" if encoding == "utf-8" else encoding
|
|
759
|
+
return f.read().decode(actual_encoding)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _read_from_directory(dir_path: Path, csv_name: Optional[str], encoding: str) -> str:
|
|
763
|
+
"""
|
|
764
|
+
从文件夹中读取 CSV 内容。
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
dir_path: 文件夹路径
|
|
768
|
+
csv_name: CSV 文件名(可选)
|
|
769
|
+
encoding: 文件编码
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
str: CSV 文件的文本内容
|
|
773
|
+
"""
|
|
774
|
+
csv_file = _resolve_csv_name_dir(dir_path, csv_name)
|
|
775
|
+
actual_encoding = "utf-8-sig" if encoding == "utf-8" else encoding
|
|
776
|
+
return csv_file.read_text(encoding=actual_encoding)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _resolve_csv_name_zip(zf, csv_name: Optional[str], zip_path: Path) -> str:
|
|
780
|
+
"""
|
|
781
|
+
解析 ZIP 文件中的 CSV 文件名。
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
zf: ZipFile 对象
|
|
785
|
+
csv_name: 指定的 CSV 文件名(可选)
|
|
786
|
+
zip_path: ZIP 文件路径(用于错误信息)
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
str: CSV 文件名
|
|
790
|
+
"""
|
|
791
|
+
if csv_name is not None:
|
|
792
|
+
if csv_name not in zf.namelist():
|
|
793
|
+
raise ValueError(f"ZIP 中不存在文件: {csv_name}")
|
|
794
|
+
return csv_name
|
|
795
|
+
|
|
796
|
+
candidates = [
|
|
797
|
+
n for n in zf.namelist()
|
|
798
|
+
if n.lower().endswith(".csv") and not n.startswith("__MACOSX")
|
|
799
|
+
]
|
|
800
|
+
|
|
801
|
+
if len(candidates) == 0:
|
|
802
|
+
raise ValueError(f"在 {zip_path} 中未找到 CSV 文件")
|
|
803
|
+
if len(candidates) > 1:
|
|
804
|
+
raise ValueError(f"存在多个 CSV 文件,请指定 csv_name: {candidates}")
|
|
805
|
+
|
|
806
|
+
return candidates[0]
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _resolve_csv_name_dir(dir_path: Path, csv_name: Optional[str]) -> Path:
|
|
810
|
+
"""
|
|
811
|
+
解析文件夹中的 CSV 文件路径。
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
dir_path: 文件夹路径
|
|
815
|
+
csv_name: 指定的 CSV 文件名(可选)
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
Path: CSV 文件路径
|
|
819
|
+
"""
|
|
820
|
+
if csv_name is not None:
|
|
821
|
+
csv_file = dir_path / csv_name
|
|
822
|
+
if not csv_file.exists():
|
|
823
|
+
raise ValueError(f"文件夹中不存在文件: {csv_name}")
|
|
824
|
+
return csv_file
|
|
825
|
+
|
|
826
|
+
candidates = [
|
|
827
|
+
f for f in dir_path.iterdir()
|
|
828
|
+
if f.is_file() and f.suffix.lower() == ".csv"
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
if len(candidates) == 0:
|
|
832
|
+
raise ValueError(f"在 {dir_path} 中未找到 CSV 文件")
|
|
833
|
+
if len(candidates) > 1:
|
|
834
|
+
raise ValueError(f"存在多个 CSV 文件,请指定 csv_name: {[f.name for f in candidates]}")
|
|
835
|
+
|
|
836
|
+
return candidates[0]
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _extract_data_lines(
|
|
840
|
+
content: str,
|
|
841
|
+
start_marker: Optional[str],
|
|
842
|
+
end_marker: Optional[str]
|
|
843
|
+
) -> list[str]:
|
|
844
|
+
"""
|
|
845
|
+
从内容中提取数据行。
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
content: 文件内容
|
|
849
|
+
start_marker: 起始标记(None 表示从开头开始)
|
|
850
|
+
end_marker: 结束标记(None 表示到结尾结束)
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
list[str]: 数据行列表
|
|
854
|
+
"""
|
|
855
|
+
lines = content.splitlines()
|
|
856
|
+
|
|
857
|
+
if start_marker is None and end_marker is None:
|
|
858
|
+
return lines
|
|
859
|
+
|
|
860
|
+
start_idx = _find_marker_index(lines, start_marker, find_first=True)
|
|
861
|
+
end_idx = _find_marker_index(lines, end_marker, find_first=False, start_from=start_idx)
|
|
862
|
+
|
|
863
|
+
if start_idx is None:
|
|
864
|
+
warnings.warn(f"未找到起始标记 '{start_marker}',将解析整个文件内容")
|
|
865
|
+
return lines
|
|
866
|
+
|
|
867
|
+
data_start = start_idx + 1
|
|
868
|
+
data_end = end_idx if end_idx is not None else len(lines)
|
|
869
|
+
|
|
870
|
+
return lines[data_start:data_end]
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _find_marker_index(
|
|
874
|
+
lines: list[str],
|
|
875
|
+
marker: Optional[str],
|
|
876
|
+
find_first: bool = True,
|
|
877
|
+
start_from: Optional[int] = None
|
|
878
|
+
) -> Optional[int]:
|
|
879
|
+
"""
|
|
880
|
+
查找标记行的索引。
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
lines: 行列表
|
|
884
|
+
marker: 标记字符串(匹配行的前缀)
|
|
885
|
+
find_first: True 返回第一个匹配,False 返回最后一个匹配
|
|
886
|
+
start_from: 开始搜索的索引
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
Optional[int]: 标记行的索引,未找到返回 None
|
|
890
|
+
"""
|
|
891
|
+
if marker is None:
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
search_range = range(
|
|
895
|
+
start_from + 1 if start_from is not None else 0,
|
|
896
|
+
len(lines)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
if find_first:
|
|
900
|
+
for i in search_range:
|
|
901
|
+
if lines[i].lstrip().startswith(marker):
|
|
902
|
+
return i
|
|
903
|
+
return None
|
|
904
|
+
else:
|
|
905
|
+
result = None
|
|
906
|
+
for i in search_range:
|
|
907
|
+
if lines[i].lstrip().startswith(marker):
|
|
908
|
+
result = i
|
|
909
|
+
return result
|
|
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
|
|
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
|
|
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
|
|
File without changes
|