diff --git "a/boss\345\237\216\345\270\202\347\274\226\347\240\201.xlsx" "b/boss\345\237\216\345\270\202\347\274\226\347\240\201.xlsx" new file mode 100644 index 00000000..ff010a13 Binary files /dev/null and "b/boss\345\237\216\345\270\202\347\274\226\347\240\201.xlsx" differ diff --git "a/boss\350\241\214\344\270\232\347\274\226\347\240\201.xlsx" "b/boss\350\241\214\344\270\232\347\274\226\347\240\201.xlsx" new file mode 100644 index 00000000..3b75897d Binary files /dev/null and "b/boss\350\241\214\344\270\232\347\274\226\347\240\201.xlsx" differ diff --git a/build.gradle.kts b/build.gradle.kts index 36d1f09e..701da9d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,8 @@ dependencies { // 不在 BOM 中的依赖(写版本) implementation("com.microsoft.playwright:playwright:1.51.0") + implementation("org.seleniumhq.selenium:selenium-java:4.31.0") + implementation("org.seleniumhq.selenium:selenium-devtools-v135:4.31.0") implementation("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.9") implementation("org.xerial:sqlite-jdbc:3.45.1.0") // 代码生成器(MyBatis-Plus Generator + Freemarker 模板) @@ -75,4 +77,4 @@ tasks.named("bootRun") { systemProperty("LOG_DATE", LocalDate.now().toString()) // 可选:对齐端口 // systemProperty("server.port", "8888") -} \ No newline at end of file +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/html/DroidSansMonoDotted.ttf b/html/DroidSansMonoDotted.ttf new file mode 100644 index 00000000..e56a5ca7 Binary files /dev/null and b/html/DroidSansMonoDotted.ttf differ diff --git a/html/bootstrap.min.css b/html/bootstrap.min.css new file mode 100644 index 00000000..92791740 --- /dev/null +++ b/html/bootstrap.min.css @@ -0,0 +1,12 @@ +@charset "UTF-8";/*! + * Bootswatch v5.3.3 (https://bootswatch.com) + * Theme: sketchy + * Copyright 2012-2024 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import url(https://fonts.googleapis.com/css?family=Neucha|Cabin+Sketch&display=swap);:root,[data-bs-theme=light]{--bs-blue:#007bff;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#28a745;--bs-teal:#20c997;--bs-cyan:#17a2b8;--bs-black:#000;--bs-white:#fff;--bs-gray:#868e96;--bs-gray-dark:#333;--bs-gray-100:#f8f9fa;--bs-gray-200:#f7f7f9;--bs-gray-300:#dee2e6;--bs-gray-400:#ccc;--bs-gray-500:#aaa;--bs-gray-600:#868e96;--bs-gray-700:#555;--bs-gray-800:#333;--bs-gray-900:#212529;--bs-primary:#333;--bs-secondary:#555;--bs-success:#28a745;--bs-info:#17a2b8;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#fff;--bs-dark:#555;--bs-primary-rgb:51,51,51;--bs-secondary-rgb:85,85,85;--bs-success-rgb:40,167,69;--bs-info-rgb:23,162,184;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:255,255,255;--bs-dark-rgb:85,85,85;--bs-primary-text-emphasis:#141414;--bs-secondary-text-emphasis:#222222;--bs-success-text-emphasis:#10431c;--bs-info-text-emphasis:#09414a;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#555;--bs-dark-text-emphasis:#555;--bs-primary-bg-subtle:#d6d6d6;--bs-secondary-bg-subtle:#dddddd;--bs-success-bg-subtle:#d4edda;--bs-info-bg-subtle:#d1ecf1;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ccc;--bs-primary-border-subtle:#adadad;--bs-secondary-border-subtle:#bbbbbb;--bs-success-border-subtle:#a9dcb5;--bs-info-border-subtle:#a2dae3;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#f7f7f9;--bs-dark-border-subtle:#aaa;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:Neucha,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:700;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#f7f7f9;--bs-secondary-bg-rgb:247,247,249;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#333;--bs-link-color-rgb:51,51,51;--bs-link-decoration:underline;--bs-link-hover-color:#292929;--bs-link-hover-color-rgb:41,41,41;--bs-code-color:#e83e8c;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:2px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:25px;--bs-border-radius-sm:15px;--bs-border-radius-lg:35px;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(51, 51, 51, 0.25);--bs-form-valid-color:#28a745;--bs-form-valid-border-color:#28a745;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#333;--bs-secondary-bg-rgb:51,51,51;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2a2c2e;--bs-tertiary-bg-rgb:42,44,46;--bs-primary-text-emphasis:#858585;--bs-secondary-text-emphasis:#999999;--bs-success-text-emphasis:#7eca8f;--bs-info-text-emphasis:#74c7d4;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#0a0a0a;--bs-secondary-bg-subtle:#111111;--bs-success-bg-subtle:#08210e;--bs-info-bg-subtle:#052025;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#333;--bs-dark-bg-subtle:#1a1a1a;--bs-primary-border-subtle:#1f1f1f;--bs-secondary-border-subtle:#333333;--bs-success-border-subtle:#186429;--bs-info-border-subtle:#0e616e;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#555;--bs-dark-border-subtle:#333;--bs-heading-color:inherit;--bs-link-color:#858585;--bs-link-hover-color:#9d9d9d;--bs-link-color-rgb:133,133,133;--bs-link-hover-color-rgb:157,157,157;--bs-code-color:#f18bba;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#555;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#7eca8f;--bs-form-valid-border-color:#7eca8f;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-family:"Cabin Sketch",cursive;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:15px}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#868e96}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:#333;--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:#fff;width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:2px;box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(2px * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:2px 0}.table-bordered>:not(caption)>*>*{border-width:0 2px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#d6d6d6;--bs-table-border-color:#ababab;--bs-table-striped-bg:#cbcbcb;--bs-table-striped-color:#fff;--bs-table-active-bg:#c1c1c1;--bs-table-active-color:#fff;--bs-table-hover-bg:#c6c6c6;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#dddddd;--bs-table-border-color:#b1b1b1;--bs-table-striped-bg:#d2d2d2;--bs-table-striped-color:#000;--bs-table-active-bg:#c7c7c7;--bs-table-active-color:#fff;--bs-table-hover-bg:#cccccc;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d4edda;--bs-table-border-color:#aabeae;--bs-table-striped-bg:#c9e1cf;--bs-table-striped-color:#000;--bs-table-active-bg:#bfd5c4;--bs-table-active-color:#000;--bs-table-hover-bg:#c4dbca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#d1ecf1;--bs-table-border-color:#a7bdc1;--bs-table-striped-bg:#c7e0e5;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd4d9;--bs-table-active-color:#000;--bs-table-hover-bg:#c1dadf;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#fff;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#fff;--bs-table-border-color:#cccccc;--bs-table-striped-bg:#f2f2f2;--bs-table-striped-color:#000;--bs-table-active-bg:#e6e6e6;--bs-table-active-color:#000;--bs-table-hover-bg:#ececec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#555;--bs-table-border-color:#777777;--bs-table-striped-bg:#5e5e5e;--bs-table-striped-color:#fff;--bs-table-active-bg:#666666;--bs-table-active-color:#fff;--bs-table-hover-bg:#626262;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:700;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid #333;border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#333;outline:0;box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);border-color:#868e96;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:700;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid #333;border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#333;outline:0;box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:#868e96;background-color:var(--bs-secondary-bg);border-color:#868e96}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#333;outline:0;box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}.form-check-input:checked{background-color:#333;border-color:#333}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#333;border-color:#333;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23333'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(51,51,51,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(51,51,51,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#333;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#c2c2c2}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#333;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#c2c2c2}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#868e96}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:700;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid #333;border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#333;--bs-btn-border-color:#333;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2b2b2b;--bs-btn-hover-border-color:#292929;--bs-btn-focus-shadow-rgb:82,82,82;--bs-btn-active-color:#fff;--bs-btn-active-bg:#292929;--bs-btn-active-border-color:#262626;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#333;--bs-btn-disabled-border-color:#333}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#555;--bs-btn-border-color:#555;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#484848;--bs-btn-hover-border-color:#444444;--bs-btn-focus-shadow-rgb:111,111,111;--bs-btn-active-color:#fff;--bs-btn-active-bg:#444444;--bs-btn-active-border-color:#404040;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#555;--bs-btn-disabled-border-color:#555}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#28a745;--bs-btn-border-color:#28a745;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#228e3b;--bs-btn-hover-border-color:#208637;--bs-btn-focus-shadow-rgb:72,180,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#208637;--bs-btn-active-border-color:#1e7d34;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#28a745;--bs-btn-disabled-border-color:#28a745}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#17a2b8;--bs-btn-border-color:#17a2b8;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#148a9c;--bs-btn-hover-border-color:#128293;--bs-btn-focus-shadow-rgb:58,176,195;--bs-btn-active-color:#fff;--bs-btn-active-bg:#128293;--bs-btn-active-border-color:#117a8a;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#17a2b8;--bs-btn-disabled-border-color:#17a2b8}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d9a406;--bs-btn-hover-border-color:#cc9a06;--bs-btn-focus-shadow-rgb:255,202,44;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cc9a06;--bs-btn-active-border-color:#bf9105;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#fff;--bs-btn-border-color:#fff;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d9d9d9;--bs-btn-hover-border-color:#cccccc;--bs-btn-focus-shadow-rgb:217,217,217;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cccccc;--bs-btn-active-border-color:#bfbfbf;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#fff;--bs-btn-disabled-border-color:#fff}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#555;--bs-btn-border-color:#555;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6f6f6f;--bs-btn-hover-border-color:#666666;--bs-btn-focus-shadow-rgb:111,111,111;--bs-btn-active-color:#fff;--bs-btn-active-bg:#777777;--bs-btn-active-border-color:#666666;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#555;--bs-btn-disabled-border-color:#555}.btn-outline-primary{--bs-btn-color:#333;--bs-btn-border-color:#333;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#333;--bs-btn-hover-border-color:#333;--bs-btn-focus-shadow-rgb:51,51,51;--bs-btn-active-color:#fff;--bs-btn-active-bg:#333;--bs-btn-active-border-color:#333;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#333;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#333;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#555;--bs-btn-border-color:#555;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#555;--bs-btn-hover-border-color:#555;--bs-btn-focus-shadow-rgb:85,85,85;--bs-btn-active-color:#fff;--bs-btn-active-bg:#555;--bs-btn-active-border-color:#555;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#555;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#555;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#28a745;--bs-btn-border-color:#28a745;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#28a745;--bs-btn-hover-border-color:#28a745;--bs-btn-focus-shadow-rgb:40,167,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#28a745;--bs-btn-active-border-color:#28a745;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#28a745;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#28a745;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#17a2b8;--bs-btn-border-color:#17a2b8;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#17a2b8;--bs-btn-hover-border-color:#17a2b8;--bs-btn-focus-shadow-rgb:23,162,184;--bs-btn-active-color:#fff;--bs-btn-active-bg:#17a2b8;--bs-btn-active-border-color:#17a2b8;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#17a2b8;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#17a2b8;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#fff;--bs-btn-border-color:#fff;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:#fff;--bs-btn-focus-shadow-rgb:255,255,255;--bs-btn-active-color:#000;--bs-btn-active-bg:#fff;--bs-btn-active-border-color:#fff;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#fff;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#555;--bs-btn-border-color:#555;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#555;--bs-btn-hover-border-color:#555;--bs-btn-focus-shadow-rgb:85,85,85;--bs-btn-active-color:#fff;--bs-btn-active-bg:#555;--bs-btn-active-border-color:#555;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#555;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#555;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#868e96;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:82,82,82;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:#333;--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:#333;--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:#fff;--bs-dropdown-link-hover-bg:#333;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#333;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#868e96;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#333;--bs-dropdown-border-color:#333;--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:#333;--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#333;--bs-dropdown-link-disabled-color:#aaa;--bs-dropdown-header-color:#aaa}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:#333;--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:#333;--bs-nav-tabs-link-active-color:#333;--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:#333;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#333}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:2px;--bs-card-border-color:#333;--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - 2px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23141414' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(51, 51, 51, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23858585'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23858585'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0.75rem;--bs-breadcrumb-padding-y:0.375rem;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius:0.25rem;--bs-breadcrumb-divider-color:#333;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#333;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:#333;--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:#fff;--bs-pagination-hover-bg:#333;--bs-pagination-hover-border-color:#333;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(51, 51, 51, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#333;--bs-pagination-active-border-color:#333;--bs-pagination-disabled-color:#ccc;--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:#333;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:1.2em;--bs-badge-padding-y:0.5em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#fff;--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#ccc;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:#333;--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#333;--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:#dee2e6;--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#333;--bs-list-group-active-border-color:#333;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:inherit;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='inherit'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:none;--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:25px;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:2px;--bs-toast-border-color:#333;--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:#333;--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:#333;width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:#333;--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:#333;--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:#333;--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:#333;--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:#333;--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(41,41,41,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(41,41,41,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(41,41,41,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(68,68,68,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(68,68,68,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(68,68,68,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(32,134,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(32,134,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(32,134,55,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(18,130,147,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(18,130,147,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(18,130,147,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(204,154,6,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(255,255,255,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(68,68,68,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(68,68,68,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(68,68,68,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}.navbar{border-style:solid;border-width:2px;border-radius:25px 25px 55px 5px/5px 55px 25px 25px}.navbar.bg-light{border-color:#333}.navbar.fixed-top{border-width:0 0 2px;border-radius:0 25px 225px 0/25px 0 25px 255px}.navbar.fixed-bottom{border-width:2px 0 0;border-radius:255px 25px 0 25px/25px 225px 25px 0}.navbar-brand{font-family:"Cabin Sketch",cursive;font-weight:400;text-decoration:none}.btn{text-decoration:none;border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.btn-group-lg>.btn,.btn-lg{border-radius:55px 225px 15px 25px/25px 25px 35px 355px}.btn-group-sm>.btn,.btn-sm{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.btn-check{display:inline-block;opacity:0}button,input,optgroup,select,textarea{font-family:Neucha,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}b,strong{font-family:"Cabin Sketch",cursive}blockquote{border-radius:15px 27px 25px 25px/25px 25px 305px 635px}table td,table th{background-color:#fff}.table-bordered{overflow:hidden;border-spacing:0;border-collapse:separate;background-color:#333;border-radius:5px 25px 5px 25px/25px 5px 25px 5px}.table-bordered td,.table-bordered th{border-radius:5px 5px 25px 4px/5px 4px 3px 5px}.table-bordered .table-success td,.table-bordered .table-success th,.table-bordered .table-success:hover td,.table-bordered .table-success:hover th{color:#fff;background-color:#28a745}.table-bordered .table-info td,.table-bordered .table-info th,.table-bordered .table-info:hover td,.table-bordered .table-info:hover th{color:#fff;background-color:#17a2b8}.table-bordered .table-warning td,.table-bordered .table-warning th,.table-bordered .table-warning:hover td,.table-bordered .table-warning:hover th{color:#fff;background-color:#ffc107}.table-bordered .table-danger td,.table-bordered .table-danger th,.table-bordered .table-danger:hover td,.table-bordered .table-danger:hover th{color:#fff;background-color:#dc3545}.table-dark td,.table-dark th,.table-dark.table-hover .table-active:hover>td,.table-dark.table-hover .table-active:hover>th{background-color:#333}.form-control,.input-group-text,input{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}select,select.form-control,textarea,textarea.form-control{border-radius:55px 225px 15px 25px/25px 25px 35px 355px!important}[type=checkbox]{position:relative;width:0;height:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;border:none}[type=checkbox]::before{position:absolute;top:-.1em;left:0;display:inline-block;width:15px;height:16px;content:"";border:2px solid #333;border-radius:2px 8px 2px 4px/5px 3px 5px 3px}[type=checkbox]:focus::before{box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}[type=checkbox]:checked::after,[type=checkbox]:indeterminate::after{position:absolute;top:0;left:.1em;font-size:1.5rem;line-height:.5;color:#333}[type=checkbox]:checked::after{content:"x"}[type=checkbox]:indeterminate::after{top:.1em;content:"-"}[type=checkbox]:disabled::before{border:2px solid #aaa}[type=radio]{position:relative;width:0;height:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;border:none}[type=radio]::before{position:absolute;top:-.1em;left:0;display:inline-block;width:16px;height:16px;content:"";border:2px solid #333;border-radius:50% 45% 40% 50%/40% 50% 50% 45%}[type=radio]:focus::before{box-shadow:0 0 0 .25rem rgba(51,51,51,.25)}[type=radio]:checked::before{background-color:#333}[type=radio]:disabled::before{border:2px solid #aaa}.form-check-input:focus{box-shadow:none}.form-switch{padding-left:0}.form-switch .form-check-input{position:relative;margin-left:0}.form-switch .form-check-input::before{width:32px;border-radius:30% 35% 30% 30%/30% 50% 30% 45%}.form-switch .form-check-input::after{position:absolute;top:0;left:0;display:inline-block;width:12px;height:12px;content:"";background-color:#fff;border:2px solid #333;border-radius:50% 45% 40% 50%/40% 50% 50% 45%;transition:left .15s ease-in-out}.form-switch .form-check-input:checked::after{top:0;left:18px;background-color:#333}.form-switch .form-check-label{margin-left:.5em}.dropdown-menu{overflow:hidden;border-radius:555px 25px 25px 25px/25px 25px 25px 555px}.dropdown-divider{border-top-width:2px}.list-group{overflow:hidden;background-color:#333;border:2px solid #333;border-radius:45px 15px 35px 5px/15px 5px 15px 65px}.list-group-item{border-top:2px solid #333;border-right:none;border-left:none;border-radius:255px 5px 225px 5px/25px 225px 25px 255px}.list-group-item:first-child{border-top:none}.list-group-item:last-child{border-bottom:none}.nav-pills .nav-link{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.dropdown-item,.list-group-item,.nav-link,.page-link{text-decoration:none}.nav-tabs .nav-link{border-radius:45px 15px 225px 5px/25px 225px 25px 255px}.breadcrumb{border:2px solid #333;border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.pagination .page-link{border-radius:425px 255px 25px 25px/25px 25px 5px 25px}.badge{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.badge-pill{border-radius:7rem 8rem 8rem 8rem/4rem 5rem 6rem 6rem}.badge.bg-light{color:#555}.alert{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.alert .btn-close::before{color:inherit}.progress{border:2px solid #333;border-radius:255px 25px 225px 25px/25px 225px 25px 255px}.card{border-radius:5px 5px 5px 5px/25px 25px 25px 5px}.card-outline-danger,.card-outline-info,.card-outline-primary,.card-outline-success,.card-outline-warning{border-width:2px}.card-header{border-color:inherit;border-bottom-width:2px}.card-header:first-child{border-radius:3px 3px 0 0/23px 23px 0 0}.card-footer{border-top-width:2px}.toast{border-radius:10px 10px 15px 5px/5px 15px 5px 15px}.toast-header{font-family:"Cabin Sketch",cursive}.modal-content{border-radius:15px 5px 5px 25px/5px 25px 25px 5px}.popover{padding:0;border-radius:45px 85px 15px 25px/15px 10px 35px 555px}.popover-title{border-bottom:2px solid #333}.popover.bs-popover-auto[data-popper-placement^=left]::before,.popover.bs-popover-start::before,.popover.bs-tether-element-attached-right::before{right:-13px}.popover.bs-popover-auto[data-popper-placement^=top]::before,.popover.bs-popover-top::before,.popover.bs-tether-element-attached-bottom::before{bottom:-13px}.popover.bs-popover-auto[data-popper-placement^=bottom]::before,.popover.bs-popover-bottom::before,.popover.bs-tether-element-attached-top::before{top:-13px}.popover.bs-popover-auto[data-popper-placement^=right]::before,.popover.bs-popover-end::before,.popover.bs-tether-element-attached-left::before{left:-13px}.tooltip-inner{border-radius:255px 25px 225px 25px/25px 225px 25px 255px}pre{border:2px solid #333;border-radius:15px 5px 5px 25px/5px 25px 25px 5px}.btn-close{background-image:none}.btn-close::before{position:absolute;top:.8rem;right:1rem;content:"X"}.img-thumbnail{border-radius:255px 25px 225px 25px/25px 225px 25px 255px} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/html/index.html b/html/index.html new file mode 100644 index 00000000..8852d3dd --- /dev/null +++ b/html/index.html @@ -0,0 +1,290 @@ + + + + get_jobs + + + Title + + + + +

🍀Get Jobs【工作无忧】

+
+ + Stars + QQ交流群 + License Issues closed + Forks +
+
+

🌴源码地址

+ + +

🌞 特色功能

+ + + +

🔞️ 注意事项

+ + + +
+

已经有人在交流群里 发广告 等与本项目无关的信息

+

如果带着不同目的或者没想清楚就进群的

+

一经发现群主会对您的家人及朋友进行亲切(没有素质)的问候

+

并将您请出群聊,请珍惜交流的机会,谢谢!

+
+
+

🚀 如何使用?


+

1️⃣ 使用git拉取代码

+ +
+    git clone https://github.com/loks666/get_jobs.git
+    cd get_jobs
+    
+ +

2️⃣ 环境配置:JDK17+、Maven、Chrome、ChromeDriver

+ +
+

目前driver版本号:123.0.6312.122

+

chrome需要版本为:124.0.6367.61及以上(默认最新即可)

+ +
    +
  • 目前程序自动判断系统环境,使用对应的chromedriver,无需手动下载
  • +
  • 但是你的Chrome版本必须是在Chrome官网下载的,并且为最新版本,才可使用
  • +
  • 如果你是mac m1芯片的版本,需要解压【chromedriver-mac-arm64.zip】后才能使用 +
  • +
+ + 更多环境配置详情请点击:📚 环境配置 +
+ +

3️⃣ 修改配置文件(一般默认即可,需要修改自己的地区和岗位)

+ +

4️⃣ 最后一步:运行代码

+ +
+

✍🏼 例:Boss投递日志

+ Boss投递日志 +

✍🏼 猎聘投递日志

+ 猎聘投递日志 +

✍🏼 获取城市码

+ 获取城市码 +

📧 联系方式

+ +

👨🏻‍🔧 QQ群

+

扫码添加:加群答案为本项目仓库名【get_jobs】

+ qq群 + 微信群 +

点击下面的链接可直接加群

+ QQ交流群 + +

🚩 付费部署

+

本项目文档已相对完善,如仍需付费部署,请添加QQ群或微信联系群主

+ + +

📑 更新日志

+

2024-4-15 01:52:18

+
    +
  1. 新增config.yaml,目前仅需修改配置文件即可,已全平台支持
  2. +
  3. cookie有效期延长,保持至少一周(拉勾平台除外)
  4. +
+ +

🤝 参与贡献

+

我们非常欢迎各种形式的贡献

+

如果你对贡献代码感兴趣

+

可以查看我们的 Issuesdiscussions

+

期待你的大展身手,向我们展示你的奇思妙想。

+ +

📰 开源协议

+
+

📝 License

+ FOSSA Status +
+

☕️ 请我喝杯咖啡

+ 支付宝付款码 + 微信付款码 +
+ + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..301a1d0b --- /dev/null +++ b/pom.xml @@ -0,0 +1,195 @@ + + + 4.0.0 + + com.superxiang + get_jobs + v2.0.1 + get_jobs + + + org.springframework.boot + spring-boot-starter-parent + 2.5.0 + + + + UTF-8 + 21 + 21 + + + + + org.json + json + 20231013 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + 1.18.30 + + + + org.slf4j + slf4j-api + 1.7.30 + + + + ch.qos.logback + logback-classic + 1.2.3 + + + + com.microsoft.playwright + playwright + 1.51.0 + + + + org.seleniumhq.selenium + selenium-java + + 4.31.0 + + + org.seleniumhq.selenium + selenium-api + + + org.seleniumhq.selenium + selenium-remote-driver + + + org.seleniumhq.selenium + selenium-support + + + org.seleniumhq.selenium + selenium-devtools-v133 + + + org.seleniumhq.selenium + selenium-devtools-v134 + + + org.seleniumhq.selenium + selenium-chrome-driver + + + org.seleniumhq.selenium + selenium-chromium-driver + + + org.seleniumhq.selenium + selenium-edge-driver + + + org.seleniumhq.selenium + selenium-firefox-driver + + + org.seleniumhq.selenium + selenium-ie-driver + + + org.seleniumhq.selenium + selenium-safari-driver + + + + + + + org.seleniumhq.selenium + selenium-api + 4.31.0 + + + + org.seleniumhq.selenium + selenium-devtools-v135 + 4.31.0 + + + + org.seleniumhq.selenium + selenium-chrome-driver + 4.31.0 + + + + org.seleniumhq.selenium + selenium-remote-driver + 4.31.0 + + + + org.seleniumhq.selenium + selenium-support + 4.31.0 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.12.3 + + + + + org.apache.httpcomponents.client5 + httpclient5-fluent + 5.1 + + + + + io.github.cdimascio + dotenv-java + 2.2.0 + + + + + + get_jobs + get_jobs + https://maven.pkg.github.com/loks666/get_jobs + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + StartAll + + + + + \ No newline at end of file diff --git a/run_startall.bat b/run_startall.bat new file mode 100644 index 00000000..46d2767b --- /dev/null +++ b/run_startall.bat @@ -0,0 +1,4 @@ +@echo off +:: 设置代码页为 UTF-8 +chcp 65001 >nul +mvn exec:java \ No newline at end of file diff --git a/src/main/java/StartAll.java b/src/main/java/StartAll.java new file mode 100644 index 00000000..6a41b39d --- /dev/null +++ b/src/main/java/StartAll.java @@ -0,0 +1,119 @@ +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class StartAll { + // 存储所有子进程的引用 + private static final List childProcesses = new ArrayList<>(); + + public static void main(String[] args) { + + // Create a ScheduledExecutorService for Boss + ScheduledExecutorService bossScheduler = Executors.newSingleThreadScheduledExecutor(); + + // 定义Boss任务 + Runnable bossTask = () -> { + try { + log.info("正在执行 Boss 任务,线程名称: {}", Thread.currentThread().getName()); + executeTask("boss.Boss"); + log.info("Boss 任务已完成,完成时间: {}", java.time.LocalDateTime.now()); + } catch (Exception e) { + log.error("Boss 任务执行过程中发生错误: {}", e.getMessage(), e); + } + }; + + // 创建一个统一的线程池来执行所有任务 + ExecutorService executorService = Executors.newFixedThreadPool(2); + + // 定义Liepin任务 + Runnable liepinTask = () -> { + try { + log.info("正在执行 Liepin 任务,线程名称: {}", Thread.currentThread().getName()); + executeTask("liepin.Liepin"); + log.info("Liepin 任务已完成,完成时间: {}", java.time.LocalDateTime.now()); + } catch (Exception e) { + log.error("Liepin 任务执行过程中发生错误: {}", e.getMessage(), e); + } + }; + + // 定义Job51任务 + Runnable job51Task = () -> { + try { + log.info("正在执行 Job51 任务,线程名称: {}", Thread.currentThread().getName()); + executeTask("job51.Job51"); + log.info("Job51 任务已完成,完成时间: {}", java.time.LocalDateTime.now()); + } catch (Exception e) { + log.error("Job51 任务执行过程中发生错误: {}", e.getMessage(), e); + } + }; + + // 提交所有任务到线程池执行 +// executorService.submit(liepinTask); +// executorService.submit(job51Task); + + // 添加关闭钩子,优雅地关闭线程池和子进程 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("正在关闭线程池和子进程..."); + + // 关闭所有子进程 + synchronized (childProcesses) { + for (Process process : childProcesses) { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + } + childProcesses.clear(); + } + + executorService.shutdown(); + bossScheduler.shutdown(); + try { + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("强制关闭线程池..."); + executorService.shutdownNow(); + bossScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + log.error("关闭线程池时发生错误: {}", e.getMessage(), e); + executorService.shutdownNow(); + bossScheduler.shutdownNow(); + } + })); + } + + /** + * 使用独立进程运行指定的类 + * + * @param className 要执行的类名 + * @throws Exception 如果发生错误 + */ + private static void executeTask(String className) throws Exception { + ProcessBuilder processBuilder = new ProcessBuilder( + "java", "-cp", System.getProperty("java.class.path"), className + ); + processBuilder.inheritIO(); // 将子进程的输入/输出重定向到当前进程 + Process process = processBuilder.start(); + + // 将进程添加到管理列表中 + synchronized (childProcesses) { + childProcesses.add(process); + } + + int exitCode = process.waitFor(); + + // 进程结束后从列表中移除 + synchronized (childProcesses) { + childProcesses.remove(process); + } + + if (exitCode != 0) { + throw new RuntimeException(className + " 执行失败,退出代码: " + exitCode); + } + } +} diff --git a/src/main/java/ai/AiConfig.java b/src/main/java/ai/AiConfig.java new file mode 100644 index 00000000..0aac284f --- /dev/null +++ b/src/main/java/ai/AiConfig.java @@ -0,0 +1,36 @@ +package ai; + +import lombok.Data; +import utils.JobUtils; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class AiConfig { + + /** + * 介绍语 + */ + private String introduce; + + /** + * 提示词 + */ + private String prompt; + + public AiConfig() { + } + + public AiConfig(String introduce, String prompt) { + this.introduce = introduce; + this.prompt = prompt; + } + + public static AiConfig init() { + AiConfig config = JobUtils.getConfig(AiConfig.class); + return new AiConfig(config.introduce, config.prompt); + } + +} diff --git a/src/main/java/ai/AiFilter.java b/src/main/java/ai/AiFilter.java new file mode 100644 index 00000000..729354d6 --- /dev/null +++ b/src/main/java/ai/AiFilter.java @@ -0,0 +1,31 @@ +package ai; + +import lombok.Data; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class AiFilter { + + /** + * ai检测结果 + */ + private Boolean result; + + /** + * 如果匹配,则返回的打招呼语 + */ + private String message; + + public AiFilter(Boolean result) { + this.result = result; + } + + public AiFilter(Boolean result, String message) { + this.result = result; + this.message = message; + } + +} diff --git a/src/main/java/ai/AiService.java b/src/main/java/ai/AiService.java new file mode 100644 index 00000000..58fb469c --- /dev/null +++ b/src/main/java/ai/AiService.java @@ -0,0 +1,170 @@ +package ai; + +import io.github.cdimascio.dotenv.Dotenv; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.*; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class AiService { + + private static final Dotenv dotenv = Dotenv.load(); + private static final String BASE_URL = dotenv.get("BASE_URL") + "/v1/chat/completions"; + private static final String API_KEY = dotenv.get("API_KEY"); + private static final String MODEL = dotenv.get("MODEL"); + + + public static String sendRequest(String content) { + // 设置超时时间,单位:秒 + int timeoutInSeconds = 60; // 你可以修改这个变量来设置超时时间 + + // 创建 HttpClient 实例并设置超时 + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(timeoutInSeconds)) // 设置连接超时 + .build(); + + // 构建 JSON 请求体 + JSONObject requestData = new JSONObject(); + requestData.put("model", MODEL); + requestData.put("temperature", 0.5); + + // 添加消息内容 + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.put("role", "user"); + message.put("content", content); + messages.put(message); + + requestData.put("messages", messages); + + // 构建 HTTP 请求 + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + API_KEY) + .POST(HttpRequest.BodyPublishers.ofString(requestData.toString())) + .build(); + + // 创建线程池用于执行请求 + ExecutorService executor = Executors.newSingleThreadExecutor(); + Callable> task = () -> client.send(request, HttpResponse.BodyHandlers.ofString()); + + // 提交请求并控制超时 + Future> future = executor.submit(task); + try { + // 使用 future.get 设置超时 + HttpResponse response = future.get(timeoutInSeconds, TimeUnit.SECONDS); + + if (response.statusCode() == 200) { + // 解析响应体 + log.info(response.body()); + JSONObject responseObject = new JSONObject(response.body()); + String requestId = responseObject.getString("id"); + long created = responseObject.getLong("created"); + String model = responseObject.getString("model"); + + // 解析返回的内容 + JSONObject messageObject = responseObject.getJSONArray("choices") + .getJSONObject(0) + .getJSONObject("message"); + String responseContent = messageObject.getString("content"); + + // 解析 usage 部分 + JSONObject usageObject = responseObject.getJSONObject("usage"); + int promptTokens = usageObject.getInt("prompt_tokens"); + int completionTokens = usageObject.getInt("completion_tokens"); + int totalTokens = usageObject.getInt("total_tokens"); + + // 格式化时间 + LocalDateTime createdTime = Instant.ofEpochSecond(created) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String formattedTime = createdTime.format(formatter); + + log.info("请求ID: {}, 创建时间: {}, 模型名: {}, 提示词: {}, 补全: {}, 总用量: {}", requestId, formattedTime, model, promptTokens, completionTokens, totalTokens); + return responseContent; + } else { + log.error("AI请求失败!状态码: {}", response.statusCode()); + } + } catch (TimeoutException e) { + log.error("请求超时!超时设置为 {} 秒", timeoutInSeconds); + } catch (Exception e) { + log.error("AI请求异常!", e); + } finally { + executor.shutdownNow(); // 关闭线程池 + } + return ""; + } + + + public static void main(String[] args) { + System.out.println(cleanBossDesc(".EwyXFHpFfseN{display:inline-block;width:0.1px;height:0.1px;overflow:hidden;visibility: hidden;}.FxpRjMznwNS{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.QTsRdnap{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.spBzTCGii{display:inline-block;font-size:0!important;width:1em;height:1em;visibility:hidden;line-height:0;}.DXpfskbRdfn{display:inline-block;width:0.1px;height:0.1px;overflow:hidden;visibility: hidden;}.snNcSPFFs{font-style:normal;font-weight:normal}.zjziXGAdnjK{font-style:normal;font-weight:normal}.CjmzfkfTmx{font-style:normal;font-weight:normal}.YYTWRZHhrm{font-style:normal;font-weight:normal}.cfAzXEKs{font-style:normal;font-weight:normal}岗位职责:\n" + + "kanzhun一、boss产品+AI的实BOSS直聘施与落地能力\n" + + "1、负责公司产品+AI的实施与落地,熟悉各基础大模型性能,熟练应用大模型关键技术,面向隧道股份各需求,协同规划产品落地路径,具备实施能力;\n" + + "2、负责聚焦场景和产品的大模型相关的训练工作,包括:需求分析、功能设计、数据集构建、模型训练、评估及优化等;\n" + + "3、熟悉包括RAG、指令数据构建、Prompt工程、模型Fine-tuning、Prompt Engineering等环节,实现大模型技术在领域内垂直场景的落地应用;\n" + + "4、熟悉NLP、CV、多模态等领域大模型结构、算法,具备追踪大模型领域内前沿的技术研究成果,包括但不局限预训练、强化学习、知识增强、分布式训练等,同时提出创新思路来推动升级的能力;\n" + + "5、具备优化计算、存储和网络性能的能力,以满足业务系统的资源需求,并设定具体的性能优化目标,如延迟、吞吐量、资源利用率等;在满足性能需求的前提下,优化计算、存储和网络资源的使用,降低总成本;\n" + + "6、对业务系统的效果进行持续调优,通过数据分析和系统改进,提升系统的性能和用户体验。\n" + + "二、项目管理与协作\n" + + "1、参与制定核心业务项目计划和需求分析,确保项目按时交付和达到高质量标准。\n" + + "带领项目成员进行端对端开发,制定项目计划、分配任务并指导项目成员完成开发工作。\n" + + "2、与跨部门团队紧密合作,包括开发人员、测试人员、产品经理等,共同推动项目的顺利进行。\n" + + "四、技术能力提升\n" + + "1、负责相关技术文档的撰写与整理。\n" + + "2、协同团队成员进行技术分享,促进***学习于经验交流。\n" + + "3、建立公司知识库,沉淀技术文档、***实践、案例分享等,方便企业内部日常学习与参考。\n" + + "\n" + + "任职资格:\n" + + "一、教育背景\n" + + "本科及以上学历,电子工程、计算机科学、人工智能等相关领域专业。\n" + + "二、工作经验\n" + + "1、具备3年以上AI相关开发经验或5年以上软件开发经验(优秀者可适当放宽工作年限要求)。\n" + + "2、具备Rerank、Embedding、Langchain、RAG等服务开发及部署经验者优先。\n" + + "具备大模型应用开发经验,在智能问答、代码review、代码续写、测试用例生成等方向有成功经验者优先。\n" + + "4、有大型互联网公司大规模机器学习平台相关研发落地经验者优先。\n" + + "三、专业知识\n" + + "1、熟悉主流大模型如GPT、Gemini、LLaMA、ChatGLM等及其原理,并能进行针对性模型开发工作;\n" + + "2、了解深度学习等技术,熟悉大模型训练、推理、量化和部署者优先;\n" + + "3、了解主流AI应用框架者(如TensorFlow、PyTorch、longchain等)优先;\n" + + "4、熟悉JAVA/C++/Go/Python任一语言,有完整的项目开发经验,具备核心模块设计和维护经验。有一定的算法工程化能力,能够实现算法/模型的工程化与应用部署;具有NLP相关技术经验者更佳;\n" + + "熟悉Agent框架,有优化能力,包括planing、action、tools use、memory等核心Agent能力的提升者优先;、了解深度学习等技术,熟悉大模型训练、推理、量化和部署者优先;\n" + + "四、能力要求\n" + + "1、具备良好的问题解决能力和逻辑思维,能独立分析并解决技术难题。\n" + + "2、具备良好的团队合作精神和沟通能力,能在跨部门协作中发挥积极作用。\n" + + "3、具备良好的学习能力和适应能力,能够快速掌握新技术和新知识。\n" + + "4、具备较强的抗压能力,能够适应一定频率的出差与加班,满足工作中的紧急任务需求。")); + try { + // 示例:发送请求 + String content = "你好"; + String response = sendRequest(content); + System.out.println("AI回复: " + response); + } catch (Exception e) { + log.error("AI异常!"); + } + } + + public static String cleanBossDesc(String raw) { + return raw.replaceAll("kanzhun|BOSS直聘|来自BOSS直聘", "") + .replaceAll("[\\u200b-\\u200d\\uFEFF]", "") + .replaceAll("<[^>]+>", "") // 如果有HTML标签就用 + .replaceAll("\\s+", " ") + .trim(); + } +} diff --git a/src/main/java/boss/Boss.java b/src/main/java/boss/Boss.java new file mode 100644 index 00000000..8e5eb515 --- /dev/null +++ b/src/main/java/boss/Boss.java @@ -0,0 +1,935 @@ +package boss; + +import ai.AiConfig; +import ai.AiFilter; +import ai.AiService; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import lombok.SneakyThrows; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import utils.*; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; + +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.Scanner; + +import static boss.Locators.*; +import static utils.Bot.sendMessageByTime; +import static utils.JobUtils.formatDuration; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + * Boss直聘自动投递 + */ +public class Boss { + static { + // 在类加载时就设置日志文件名,确保Logger初始化时能获取到正确的属性 + System.setProperty("log.name", "boss"); + } + + private static final Logger log = LoggerFactory.getLogger(Boss.class); + static String homeUrl = "https://www.zhipin.com"; + static String baseUrl = "https://www.zhipin.com/web/geek/job?"; + static Set blackCompanies; + static Set blackRecruiters; + static Set blackJobs; + static List resultList = new ArrayList<>(); + static String dataPath = "src/main/java/boss/data.json"; + static String cookiePath = "src/main/java/boss/cookie.json"; + static Date startDate; + static BossConfig config = BossConfig.init(); + + static { + try { + // 检查dataPath文件是否存在,不存在则创建 + File dataFile = new File(dataPath); + if (!dataFile.exists()) { + // 确保父目录存在 + if (!dataFile.getParentFile().exists()) { + dataFile.getParentFile().mkdirs(); + } + // 创建文件并写入初始JSON结构 + Map> initialData = new HashMap<>(); + initialData.put("blackCompanies", new HashSet<>()); + initialData.put("blackRecruiters", new HashSet<>()); + initialData.put("blackJobs", new HashSet<>()); + String initialJson = customJsonFormat(initialData); + Files.write(Paths.get(dataPath), initialJson.getBytes()); + log.info("创建数据文件: {}", dataPath); + } + + // 检查cookiePath文件是否存在,不存在则创建 + File cookieFile = new File(cookiePath); + if (!cookieFile.exists()) { + // 确保父目录存在 + if (!cookieFile.getParentFile().exists()) { + cookieFile.getParentFile().mkdirs(); + } + // 创建空的cookie文件 + Files.write(Paths.get(cookiePath), "[]".getBytes()); + log.info("创建cookie文件: {}", cookiePath); + } + } catch (IOException e) { + log.error("创建文件时发生异常: {}", e.getMessage()); + } + } + + public static void main(String[] args) { + loadData(dataPath); + // 使用 PlayWright 获取岗位 + PlaywrightUtil.init(); + startDate = new Date(); + login(); + config.getCityCode().forEach(Boss::postJobByCity); + log.info(resultList.isEmpty() ? "未发起新的聊天..." : "新发起聊天公司如下:\n{}", + resultList.stream().map(Object::toString).collect(Collectors.joining("\n"))); + if (!config.getDebugger()) { + printResult(); + } + } + + private static void printResult() { + String message = String.format("\nBoss投递完成,共发起%d个聊天,用时%s", resultList.size(), + formatDuration(startDate, new Date())); + log.info(message); + sendMessageByTime(message); + saveData(dataPath); + resultList.clear(); + if (!config.getDebugger()) { + PlaywrightUtil.close(); + } + + // 确保所有日志都被刷新到文件 + try { + Thread.sleep(1000); // 等待1秒确保日志写入完成 + // 强制刷新日志 - 使用正确的方法 + ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void postJobByCity(String cityCode) { + String searchUrl = getSearchUrl(cityCode); + for (String keyword : config.getKeywords()) { + int postCount = 0; + // 使用 URLEncoder 对关键词进行编码 + String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8); + + String url = searchUrl + "&query=" + encodedKeyword; + log.info("投递地址:{}", searchUrl + "&query=" + keyword); + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate(url); + + // 1. 滚动到底部,加载所有岗位卡片 + int lastCount = -1; + while (true) { + // 滑动到底部 + page.evaluate("window.scrollTo(0, document.body.scrollHeight);"); + PlaywrightUtil.sleep(1); // 等待加载(可根据速度调整) + + // 获取所有卡片数 + Locator cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + int currentCount = cards.count(); + + // 判断是否继续滑动 + if (currentCount == lastCount) { + break; // 没有新内容,跳出循环 + } + lastCount = currentCount; + } + log.info("【{}】岗位已全部加载,总数:{}", keyword, lastCount); + + // 2. 回到页面顶部 + page.evaluate("window.scrollTo(0, 0);"); + PlaywrightUtil.sleep(1); + + // 3. 逐个遍历所有岗位 + Locator cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + int count = cards.count(); + for (int i = 0; i < count; i++) { + // 重新获取卡片,避免元素过期 + cards = page.locator("//ul[contains(@class, 'rec-job-list')]//li[contains(@class, 'job-card-box')]"); + cards.nth(i).click(); + PlaywrightUtil.sleep(1); + + // 等待详情内容加载 + page.waitForSelector("div[class*='job-detail-box']", new Page.WaitForSelectorOptions().setTimeout(4000)); + Locator detailBox = page.locator("div[class*='job-detail-box']"); + + // 岗位名称 + String jobName = safeText(detailBox, "span[class*='job-name']"); + if (blackJobs.stream().anyMatch(jobName::contains)) continue; + // 薪资(原始) + String jobSalaryRaw = safeText(detailBox, "span.job-salary"); + String jobSalary = decodeSalary(jobSalaryRaw); + // 城市/经验/学历 + List tags = safeAllText(detailBox, "ul[class*='tag-list'] > li"); + // 标签 (暂时不使用) + // List jobLabels = safeAllText(detailBox, "ul[class*='job-label-list'] > li"); + // 岗位描述 + String jobDesc = safeText(detailBox, "p.desc"); + // Boss姓名、活跃 + String bossNameRaw = safeText(detailBox, "h2[class*='name']"); + String[] bossInfo = splitBossName(bossNameRaw); + String bossName = bossInfo[0]; + String bossActive = bossInfo[1]; + if (config.getDeadStatus().stream().anyMatch(bossActive::contains)) continue; + // Boss公司/职位 + String bossTitleRaw = safeText(detailBox, "div[class*='boss-info-attr']"); + String[] bossTitleInfo = splitBossTitle(bossTitleRaw); + String bossCompany = bossTitleInfo[0]; + if (blackCompanies.stream().anyMatch(bossCompany::contains)) continue; + String bossJobTitle = bossTitleInfo[1]; + if (blackRecruiters.stream().anyMatch(bossJobTitle::contains)) continue; + + // 创建Job对象 + Job job = new Job(); + job.setJobName(jobName); + job.setSalary(jobSalary); + job.setJobArea(String.join(", ", tags)); + job.setCompanyName(bossCompany); + job.setRecruiter(bossName); + job.setJobInfo(jobDesc); + + // 输出 +// log.info("正在投递:第{}条 | 岗位名称:{} | 薪资:{} | 城市/经验/学历:{} | Boss姓名:{} | 活跃状态:{} | 公司:{} | 职位:{}", (i + 1), jobName, jobSalary, tags, bossName, bossActive, bossCompany, bossJobTitle); + resumeSubmission(page, keyword, job); + postCount++; + } + log.info("【{}】岗位已投递完毕!已投递岗位数量:{}", keyword, postCount); + } + } + + public static String decodeSalary(String text) { + Map fontMap = new HashMap<>(); + fontMap.put('', '0'); + fontMap.put('', '1'); + fontMap.put('', '2'); + fontMap.put('', '3'); + fontMap.put('', '4'); + fontMap.put('', '5'); + fontMap.put('', '6'); + fontMap.put('', '7'); + fontMap.put('', '8'); + fontMap.put('', '9'); + StringBuilder result = new StringBuilder(); + for (char c : text.toCharArray()) { + result.append(fontMap.getOrDefault(c, c)); + } + return result.toString(); + } + + // 安全获取单个文本内容 + public static String safeText(Locator root, String selector) { + Locator node = root.locator(selector); + try { + if (node.count() > 0 && node.innerText() != null) { + return node.innerText().trim(); + } + } catch (Exception e) { + // ignore + } + return ""; + } + + // 安全获取多个文本内容 + public static List safeAllText(Locator root, String selector) { + try { + return root.locator(selector).allInnerTexts(); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + // Boss姓名+活跃状态拆分 + public static String[] splitBossName(String raw) { + String[] bossParts = raw.trim().split("\\s+"); + String bossName = bossParts[0]; + String bossActive = bossParts.length > 1 ? String.join(" ", Arrays.copyOfRange(bossParts, 1, bossParts.length)) : ""; + return new String[]{bossName, bossActive}; + } + + // Boss公司+职位拆分 + public static String[] splitBossTitle(String raw) { + String[] parts = raw.trim().split(" · "); + String company = parts[0]; + String job = parts.length > 1 ? parts[1] : ""; + return new String[]{company, job}; + } + + private static boolean isJobsPresent() { + try { + // 判断页面是否存在岗位的元素 + PlaywrightUtil.waitForElement(JOB_LIST_CONTAINER); + return true; + } catch (Exception e) { + log.error("加载岗位区块失败:{}", e.getMessage()); + return false; + } + } + + private static String getSearchUrl(String cityCode) { + return baseUrl + JobUtils.appendParam("city", cityCode) + + JobUtils.appendParam("jobType", config.getJobType()) + + JobUtils.appendParam("salary", config.getSalary()) + + JobUtils.appendListParam("experience", config.getExperience()) + + JobUtils.appendListParam("degree", config.getDegree()) + + JobUtils.appendListParam("scale", config.getScale()) + + JobUtils.appendListParam("industry", config.getIndustry()) + + JobUtils.appendListParam("stage", config.getStage()); + } + + private static void saveData(String path) { + try { + updateListData(); + Map> data = new HashMap<>(); + data.put("blackCompanies", blackCompanies); + data.put("blackRecruiters", blackRecruiters); + data.put("blackJobs", blackJobs); + String json = customJsonFormat(data); + Files.write(Paths.get(path), json.getBytes()); + } catch (IOException e) { + log.error("保存【{}】数据失败!", path); + } + } + + private static void updateListData() { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate("https://www.zhipin.com/web/geek/chat"); + PlaywrightUtil.sleep(3); + + boolean shouldBreak = false; + while (!shouldBreak) { + try { + Locator bottomLocator = page.locator(FINISHED_TEXT); + if (bottomLocator.count() > 0 && "没有更多了".equals(bottomLocator.textContent())) { + shouldBreak = true; + } + } catch (Exception ignore) { + } + + Locator items = page.locator(CHAT_LIST_ITEM); + int itemCount = items.count(); + + for (int i = 0; i < itemCount; i++) { + try { + Locator companyElements = page.locator(COMPANY_NAME_IN_CHAT); + Locator messageElements = page.locator(LAST_MESSAGE); + + if (i >= companyElements.count() || i >= messageElements.count()) { + break; + } + + String companyName = null; + String message = null; + int retryCount = 0; + + while (retryCount < 2) { + try { + companyName = companyElements.nth(i).textContent(); + message = messageElements.nth(i).textContent(); + break; + } catch (Exception e) { + retryCount++; + if (retryCount >= 2) { + log.info("尝试获取元素文本2次失败,放弃本次获取"); + break; + } + log.info("页面元素已变更,正在重试第{}次获取元素文本...", retryCount); + PlaywrightUtil.sleep(1); + } + } + + if (companyName != null && message != null) { + boolean match = message.contains("不") || message.contains("感谢") || message.contains("但") + || message.contains("遗憾") || message.contains("需要本") || message.contains("对不"); + boolean nomatch = message.contains("不是") || message.contains("不生"); + if (match && !nomatch) { + log.info("黑名单公司:【{}】,信息:【{}】", companyName, message); + if (blackCompanies.stream().anyMatch(companyName::contains)) { + continue; + } + companyName = companyName.replaceAll("\\.{3}", ""); + if (companyName.matches(".*(\\p{IsHan}{2,}|[a-zA-Z]{4,}).*")) { + blackCompanies.add(companyName); + } + } + } + } catch (Exception e) { + log.error("寻找黑名单公司异常...", e); + } + } + + try { + Locator scrollElement = page.locator(SCROLL_LOAD_MORE); + if (scrollElement.count() > 0) { + scrollElement.scrollIntoViewIfNeeded(); + } else { + page.evaluate("window.scrollTo(0, document.body.scrollHeight);"); + } + } catch (Exception e) { + log.error("滚动元素出错", e); + break; + } + } + log.info("黑名单公司数量:{}", blackCompanies.size()); + } + + private static String customJsonFormat(Map> data) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + for (Map.Entry> entry : data.entrySet()) { + sb.append(" \"").append(entry.getKey()).append("\": [\n"); + sb.append(entry.getValue().stream().map(s -> " \"" + s + "\"").collect(Collectors.joining(",\n"))); + + sb.append("\n ],\n"); + } + sb.delete(sb.length() - 2, sb.length()); + sb.append("\n}"); + return sb.toString(); + } + + private static void loadData(String path) { + try { + String json = new String(Files.readAllBytes(Paths.get(path))); + parseJson(json); + } catch (IOException e) { + log.error("读取【{}】数据失败!", path); + } + } + + private static void parseJson(String json) { + JSONObject jsonObject = new JSONObject(json); + blackCompanies = jsonObject.getJSONArray("blackCompanies").toList().stream().map(Object::toString) + .collect(Collectors.toSet()); + blackRecruiters = jsonObject.getJSONArray("blackRecruiters").toList().stream().map(Object::toString) + .collect(Collectors.toSet()); + blackJobs = jsonObject.getJSONArray("blackJobs").toList().stream().map(Object::toString) + .collect(Collectors.toSet()); + } + + @SneakyThrows + private static void resumeSubmission(com.microsoft.playwright.Page page, String keyword, Job job) { + PlaywrightUtil.sleep(1); + + // 1. 查找“查看更多信息”按钮(必须存在且新开页) + Locator moreInfoBtn = page.locator("a.more-job-btn"); + if (moreInfoBtn.count() == 0) { + log.warn("未找到“查看更多信息”按钮,跳过..."); + return; + } + // 强制用js新开tab + String href = moreInfoBtn.first().getAttribute("href"); + if (href == null || !href.startsWith("/job_detail/")) { + log.warn("未获取到岗位详情链接,跳过..."); + return; + } + String detailUrl = "https://www.zhipin.com" + href; + + // 2. 新开详情页 + com.microsoft.playwright.Page detailPage = page.context().newPage(); + detailPage.navigate(detailUrl); + PlaywrightUtil.sleep(1); // 页面加载 + + // 3. 查找“立即沟通”按钮 + Locator chatBtn = detailPage.locator("a.btn-startchat, a.op-btn-chat"); + boolean foundChatBtn = false; + for (int i = 0; i < 5; i++) { + if (chatBtn.count() > 0 && (chatBtn.first().textContent().contains("立即沟通"))) { + foundChatBtn = true; + break; + } + PlaywrightUtil.sleep(1); + } + if (!foundChatBtn) { + log.warn("未找到立即沟通按钮,跳过岗位: {}", job.getJobName()); + detailPage.close(); + return; + } + chatBtn.first().click(); + PlaywrightUtil.sleep(1); + + // 4. 等待聊天输入框 + Locator inputLocator = detailPage.locator("div#chat-input.chat-input[contenteditable='true'], textarea.input-area"); + boolean inputReady = false; + for (int i = 0; i < 10; i++) { + if (inputLocator.count() > 0 && inputLocator.first().isVisible()) { + inputReady = true; + break; + } + PlaywrightUtil.sleep(1); + } + if (!inputReady) { + log.warn("聊天输入框未出现,跳过: {}", job.getJobName()); + detailPage.close(); + return; + } + + // 5. AI智能生成打招呼语 + AiFilter aiResult = null; + if (config.getEnableAI()) { + String jd = job.getJobInfo(); + if (jd != null && !jd.isEmpty()) { + aiResult = checkJob(keyword, job.getJobName(), jd); + } + } + String sayHi = config.getSayHi().replaceAll("[\\r\\n]", ""); + String message = (aiResult != null && aiResult.getResult() && isValidString(aiResult.getMessage())) + ? aiResult.getMessage() : sayHi; + + // 6. 输入打招呼语 + Locator input = inputLocator.first(); + input.click(); + if (input.evaluate("el => el.tagName.toLowerCase()") instanceof String tag && tag.equals("textarea")) { + input.fill(message); + } else { + input.evaluate("(el, msg) => el.innerText = msg", message); + } + + // 7. 发送图片简历(可选) + boolean imgResume = false; + if (config.getSendImgResume()) { + try { + URL resourceUrl = Boss.class.getResource("/resume.jpg"); + if (resourceUrl != null) { + File imageFile = new File(resourceUrl.toURI()); + Locator fileInput = detailPage.locator("//div[@aria-label='发送图片']//input[@type='file']"); + if (fileInput.count() > 0) { + fileInput.setInputFiles(imageFile.toPath()); + imgResume = true; + } + } + } catch (Exception e) { + log.error("发送图片简历失败: {}", e.getMessage()); + } + } + + // 8. 点击发送按钮(div.send-message 或 button.btn-send) + Locator sendBtn = detailPage.locator("div.send-message, button[type='send'].btn-send, button.btn-send"); + boolean sendSuccess = false; + if (sendBtn.count() > 0) { + sendBtn.first().click(); + PlaywrightUtil.sleep(1); + sendSuccess = true; + } else { + log.warn("未找到发送按钮,自动跳过!岗位:{}", job.getJobName()); + } + + log.info("投递完成 | 岗位:{} | 招呼语:{} | 图片简历:{}", job.getJobName(), message, imgResume ? "已发送" : "未发送"); + + // 9. 关闭详情页,回到主页面 + detailPage.close(); + PlaywrightUtil.sleep(1); + + // 10. 成功投递加入结果 + if (sendSuccess) { + resultList.add(job); + } + } + + public static boolean isValidString(String str) { + return str != null && !str.isEmpty(); + } + + public static Boolean sendResume(String company) { + log.warn("sendResume方法已废弃,请直接在主逻辑中使用playwright实现文件上传"); + return false; + } + + /** + * 检查岗位薪资是否符合预期 + * + * @return boolean + * true 不符合预期 + * false 符合预期 + * 期望的最低薪资如果比岗位最高薪资还小,则不符合(薪资给的太少) + * 期望的最高薪资如果比岗位最低薪资还小,则不符合(要求太高满足不了) + */ + private static boolean isSalaryNotExpected(String salary) { + try { + // 1. 如果没有期望薪资范围,直接返回 false,表示"薪资并非不符合预期" + List expectedSalary = config.getExpectedSalary(); + if (!hasExpectedSalary(expectedSalary)) { + return false; + } + + // 2. 清理薪资文本(比如去掉 "·15薪") + salary = removeYearBonusText(salary); + + // 3. 如果薪资格式不符合预期(如缺少 "K" / "k"),直接返回 true,表示"薪资不符合预期" + if (!isSalaryInExpectedFormat(salary)) { + return true; + } + + // 4. 进一步清理薪资文本,比如去除 "K"、"k"、"·" 等 + salary = cleanSalaryText(salary); + + // 5. 判断是 "月薪" 还是 "日薪" + String jobType = detectJobType(salary); + salary = removeDayUnitIfNeeded(salary); // 如果是按天,则去除 "元/天" + + // 6. 解析薪资范围并检查是否超出预期 + Integer[] jobSalaryRange = parseSalaryRange(salary); + return isSalaryOutOfRange(jobSalaryRange, + getMinimumSalary(expectedSalary), + getMaximumSalary(expectedSalary), + jobType); + + } catch (Exception e) { + log.error("岗位薪资获取异常!薪资文本【{}】,异常信息【{}】", salary, e.getMessage(), e); + // 出错时,您可根据业务需求决定返回 true 或 false + // 这里假设出错时无法判断,视为不满足预期 => 返回 true + return true; + } + } + + /** + * 是否存在有效的期望薪资范围 + */ + private static boolean hasExpectedSalary(List expectedSalary) { + return expectedSalary != null && !expectedSalary.isEmpty(); + } + + /** + * 去掉年终奖信息,如 "·15薪"、"·13薪"。 + */ + private static String removeYearBonusText(String salary) { + if (salary.contains("薪")) { + // 使用正则去除 "·任意数字薪" + return salary.replaceAll("·\\d+薪", ""); + } + return salary; + } + + /** + * 判断是否是按天计薪,如发现 "元/天" 则认为是日薪 + */ + private static String detectJobType(String salary) { + if (salary.contains("元/天")) { + return "day"; + } + return "mouth"; + } + + /** + * 如果是日薪,则去除 "元/天" + */ + private static String removeDayUnitIfNeeded(String salary) { + if (salary.contains("元/天")) { + return salary.replaceAll("元/天", ""); + } + return salary; + } + + private static Integer getMinimumSalary(List expectedSalary) { + return expectedSalary != null && !expectedSalary.isEmpty() ? expectedSalary.get(0) : null; + } + + private static Integer getMaximumSalary(List expectedSalary) { + return expectedSalary != null && expectedSalary.size() > 1 ? expectedSalary.get(1) : null; + } + + private static boolean isSalaryInExpectedFormat(String salaryText) { + return salaryText.contains("K") || salaryText.contains("k") || salaryText.contains("元/天"); + } + + private static String cleanSalaryText(String salaryText) { + salaryText = salaryText.replace("K", "").replace("k", ""); + int dotIndex = salaryText.indexOf('·'); + if (dotIndex != -1) { + salaryText = salaryText.substring(0, dotIndex); + } + return salaryText; + } + + private static boolean isSalaryOutOfRange(Integer[] jobSalary, Integer miniSalary, Integer maxSalary, + String jobType) { + if (jobSalary == null) { + return true; + } + if (miniSalary == null) { + return false; + } + if (Objects.equals("day", jobType)) { + // 期望薪资转为平均每日的工资 + maxSalary = BigDecimal.valueOf(maxSalary).multiply(BigDecimal.valueOf(1000)) + .divide(BigDecimal.valueOf(21.75), 0, RoundingMode.HALF_UP).intValue(); + miniSalary = BigDecimal.valueOf(miniSalary).multiply(BigDecimal.valueOf(1000)) + .divide(BigDecimal.valueOf(21.75), 0, RoundingMode.HALF_UP).intValue(); + } + // 如果职位薪资下限低于期望的最低薪资,返回不符合 + if (jobSalary[1] < miniSalary) { + return true; + } + // 如果职位薪资上限高于期望的最高薪资,返回不符合 + return maxSalary != null && jobSalary[0] > maxSalary; + } + + private static void RandomWait() { + PlaywrightUtil.sleep(JobUtils.getRandomNumberInRange(3, 20)); + } + + private static void simulateWait() { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + for (int i = 0; i < 3; i++) { + page.keyboard().press(" "); + PlaywrightUtil.sleep(1); + } + page.keyboard().press("Control+Home"); + PlaywrightUtil.sleep(1); + } + + private static boolean isDeadHR(com.microsoft.playwright.Page page) { + if (!config.getFilterDeadHR()) { + return false; + } + try { + // 尝试获取 HR 的活跃时间 + Locator activeTimeLocator = page.locator(HR_ACTIVE_TIME); + if (activeTimeLocator.count() > 0) { + String activeTimeText = activeTimeLocator.textContent(); + log.info("{}:{}", getCompanyAndHR(page), activeTimeText); + // 如果 HR 活跃状态符合预期,则返回 true + return containsDeadStatus(activeTimeText, config.getDeadStatus()); + } + } catch (Exception e) { + log.info("没有找到【{}】的活跃状态, 默认此岗位将会投递...", getCompanyAndHR(page)); + } + return false; + } + + public static boolean containsDeadStatus(String activeTimeText, List deadStatus) { + for (String status : deadStatus) { + if (activeTimeText.contains(status)) { + return true;// 一旦找到包含的值,立即返回 true + } + } + return false;// 如果没有找到,返回 false + } + + private static String getCompanyAndHR(com.microsoft.playwright.Page page) { + Locator recruiterLocator = page.locator(RECRUITER_INFO); + if (recruiterLocator.count() > 0) { + return recruiterLocator.textContent().replaceAll("\n", ""); + } + return "未知公司和HR"; + } + + private static void closeWindow(ArrayList tabs) { + log.warn("closeWindow方法已废弃,请使用playwright的page.close()方法"); + // 该方法已废弃,在playwright中直接使用page.close() + } + + private static AiFilter checkJob(String keyword, String jobName, String jd) { + AiConfig aiConfig = AiConfig.init(); + String requestMessage = String.format(aiConfig.getPrompt(), aiConfig.getIntroduce(), keyword, jobName, jd, + config.getSayHi()); + String result = AiService.sendRequest(requestMessage); + return result.contains("false") ? new AiFilter(false) : new AiFilter(true, result); + } + + private static Integer[] parseSalaryRange(String salaryText) { + try { + return Arrays.stream(salaryText.split("-")).map(s -> s.replaceAll("[^0-9]", "")) // 去除非数字字符 + .map(Integer::parseInt) // 转换为Integer + .toArray(Integer[]::new); // 转换为Integer数组 + } catch (Exception e) { + log.error("薪资解析异常!{}", e.getMessage(), e); + } + return null; + } + + private static boolean isLimit(com.microsoft.playwright.Page page) { + try { + PlaywrightUtil.sleep(1); + Locator dialogLocator = page.locator(DIALOG_CON); + if (dialogLocator.count() > 0) { + String text = dialogLocator.textContent(); + return text.contains("已达上限"); + } + return false; + } catch (Exception e) { + return false; + } + } + + @SneakyThrows + private static void login() { + log.info("打开Boss直聘网站中..."); + + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate(homeUrl); + PlaywrightUtil.sleep(1); + // 检查滑块验证 + waitForSliderVerify(page); + + if (PlaywrightUtil.isCookieValid(cookiePath)) { + PlaywrightUtil.loadCookies(cookiePath); + page.reload(); + PlaywrightUtil.sleep(1); + waitForSliderVerify(page); + // 启用反检测模式 + PlaywrightUtil.initStealth(); + } + + if (isLoginRequired()) { + log.error("cookie失效,尝试扫码登录..."); + scanLogin(); + } + } + + private static void waitForSliderVerify(com.microsoft.playwright.Page page) { + String SLIDER_URL = "https://www.zhipin.com/web/user/safe/verify-slider"; + // 最多等待5分钟(防呆,防止死循环) + long start = System.currentTimeMillis(); + while (true) { + String url = page.url(); + if (url != null && url.startsWith(SLIDER_URL)) { + System.out.println("\n【滑块验证】请手动完成Boss直聘滑块验证,通过后在控制台回车继续…"); + try { + System.in.read(); + } catch (Exception e) { + log.error("等待滑块验证输入异常: {}", e.getMessage()); + } + PlaywrightUtil.sleep(1); + // 验证通过后页面url会变,循环再检测一次 + continue; + } + if ((System.currentTimeMillis() - start) > 5 * 60 * 1000) { + throw new RuntimeException("滑块验证超时!"); + } + break; + } + } + + + private static boolean isLoginRequired() { + try { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + Locator buttonLocator = page.locator(LOGIN_BTNS); + if (buttonLocator.count() > 0 && buttonLocator.textContent().contains("登录")) { + return true; + } + } catch (Exception e) { + try { + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.locator(PAGE_HEADER).waitFor(); + Locator errorLoginLocator = page.locator(ERROR_PAGE_LOGIN); + if (errorLoginLocator.count() > 0) { + errorLoginLocator.click(); + } + return true; + } catch (Exception ex) { + log.info("没有出现403访问异常"); + } + log.info("cookie有效,已登录..."); + return false; + } + return false; + } + + @SneakyThrows + private static void scanLogin() { + // 访问登录页面 + com.microsoft.playwright.Page page = PlaywrightUtil.getPageObject(); + page.navigate(homeUrl + "/web/user/?ka=header-login"); + PlaywrightUtil.sleep(1); + + // 1. 如果已经登录,则直接返回 + try { + Locator loginBtnLocator = page.locator(LOGIN_BTN); + if (loginBtnLocator.count() > 0 && !Objects.equals(loginBtnLocator.textContent(), "登录")) { + log.info("已经登录,直接开始投递..."); + return; + } + } catch (Exception ignored) { + } + + log.info("等待登录..."); + + // 2. 定位二维码登录的切换按钮 + try { + Locator scanButton = page.locator(LOGIN_SCAN_SWITCH); + scanButton.click(); + + // 3. 登录逻辑 + boolean login = false; + + // 4. 记录开始时间,用于判断10分钟超时 + long startTime = System.currentTimeMillis(); + final long TIMEOUT = 10 * 60 * 1000; // 10分钟 + + while (!login) { + // 判断是否超时 + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed >= TIMEOUT) { + log.error("超过10分钟未完成登录,程序退出..."); + System.exit(1); + } + + try { + // 判断页面上是否出现职位列表容器 + Locator jobList = page.locator("div.job-list-container"); + if (jobList.isVisible()) { + login = true; + log.info("用户已登录!"); + // 登录成功,保存Cookie + PlaywrightUtil.saveCookies(cookiePath); + break; + } + } catch (Exception e) { + log.error("检测元素时异常: {}", e.getMessage()); + } + // 每2秒检查一次 + Thread.sleep(2000); + } + + + } catch (Exception e) { + log.error("未找到二维码登录按钮,登录失败", e); + } + } + + /** + * 在指定的毫秒数内等待用户输入回车;若在等待时间内用户按回车则返回 true,否则返回 false。 + * + * @param scanner 用于读取控制台输入 + * @return 用户是否在指定时间内按回车 + */ + private static boolean waitForUserInputOrTimeout(Scanner scanner) { + long end = System.currentTimeMillis() + 2000; + while (System.currentTimeMillis() < end) { + try { + // 判断输入流中是否有可用字节 + if (System.in.available() > 0) { + // 读取一行(用户输入) + scanner.nextLine(); + return true; + } + } catch (IOException e) { + // 读取输入流异常,直接忽略 + } + + // 小睡一下,避免 CPU 空转 + PlaywrightUtil.sleep(1); + } + return false; + } + +} diff --git a/src/main/java/boss/BossConfig.java b/src/main/java/boss/BossConfig.java new file mode 100644 index 00000000..ecf98018 --- /dev/null +++ b/src/main/java/boss/BossConfig.java @@ -0,0 +1,142 @@ +package boss; + +import lombok.Data; +import lombok.SneakyThrows; +import utils.JobUtils; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class BossConfig { + /** + * 用于打招呼的语句 + */ + private String sayHi; + + /** + * 开发者模式 + */ + private Boolean debugger; + + /** + * 搜索关键词列表 + */ + private List keywords; + + /** + * 城市编码 + */ + private List cityCode; + + /** + * 自定义城市编码映射 + */ + private Map customCityCode; + + /** + * 行业列表 + */ + private List industry; + + /** + * 工作经验要求 + */ + private List experience; + + /** + * 工作类型 + */ + private String jobType; + + /** + * 薪资范围 + */ + private String salary; + + /** + * 学历要求列表 + */ + private List degree; + + /** + * 公司规模列表 + */ + private List scale; + + /** + * 公司融资阶段列表 + */ + private List stage; + + /** + * 是否开放AI检测 + */ + private Boolean enableAI; + + /** + * 是否过滤不活跃hr + */ + private Boolean filterDeadHR; + + /** + * 是否发送图片简历 + */ + private Boolean sendImgResume; + + /** + * 目标薪资 + */ + private List expectedSalary; + + /** + * 等待时间 + */ + private String waitTime; + + /** + * HR未上线状态 + */ + private List deadStatus; + + @SneakyThrows + public static BossConfig init() { + BossConfig config = JobUtils.getConfig(BossConfig.class); + + // 转换工作类型 + config.setJobType(BossEnum.JobType.forValue(config.getJobType()).getCode()); + // 转换薪资范围 + config.setSalary(BossEnum.Salary.forValue(config.getSalary()).getCode()); + // 转换城市编码 +// config.setCityCode(config.getCityCode().stream().map(value -> BossEnum.CityCode.forValue(value).getCode()).collect(Collectors.toList())); + List convertedCityCodes = config.getCityCode().stream() + .map(city -> { + // 优先从自定义映射中获取 + if (config.getCustomCityCode() != null && config.getCustomCityCode().containsKey(city)) { + return config.getCustomCityCode().get(city); + } + // 否则从枚举中获取 + return BossEnum.CityCode.forValue(city).getCode(); + }) + .collect(Collectors.toList()); + config.setCityCode(convertedCityCodes); + // 转换工作经验要求 + config.setExperience(config.getExperience().stream().map(value -> BossEnum.Experience.forValue(value).getCode()).collect(Collectors.toList())); + // 转换学历要求 + config.setDegree(config.getDegree().stream().map(value -> BossEnum.Degree.forValue(value).getCode()).collect(Collectors.toList())); + // 转换公司规模 + config.setScale(config.getScale().stream().map(value -> BossEnum.Scale.forValue(value).getCode()).collect(Collectors.toList())); + // 转换公司融资阶段 + config.setStage(config.getStage().stream().map(value -> BossEnum.Financing.forValue(value).getCode()).collect(Collectors.toList())); + // 转换行业 + config.setIndustry(config.getIndustry().stream().map(value -> BossEnum.Industry.forValue(value).getCode()).collect(Collectors.toList())); + + return config; + } + +} diff --git a/src/main/java/boss/BossEnum.java b/src/main/java/boss/BossEnum.java new file mode 100644 index 00000000..30491334 --- /dev/null +++ b/src/main/java/boss/BossEnum.java @@ -0,0 +1,251 @@ +package boss; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class BossEnum { + @Getter + public enum Experience { + NULL("不限", "0"), + STUDENT("在校生", "108"), + GRADUATE("应届毕业生", "102"), + UNLIMITED("经验不限", "101"), + LESS_THAN_ONE_YEAR("1年以下", "103"), + ONE_TO_THREE_YEARS("1-3年", "104"), + THREE_TO_FIVE_YEARS("3-5年", "105"), + FIVE_TO_TEN_YEARS("5-10年", "106"), + MORE_THAN_TEN_YEARS("10年以上", "107"); + + private final String name; + private final String code; + + Experience(String name, String code) { + this.name = name; + this.code = code; + } + + public static Optional getCode(String name) { + return Arrays.stream(Experience.values()).filter(experience -> experience.name.equals(name)).findFirst().map(experience -> experience.code); + } + + @JsonCreator + public static Experience forValue(String value) { + for (Experience experience : Experience.values()) { + if (experience.name.equals(value)) { + return experience; + } + } + return NULL; + } + } + + @Getter + public enum CityCode { + NULL("不限", "0"), + ALL("全国", "100010000"), + BEIJING("北京", "101010100"), + SHANGHAI("上海", "101020100"), + TIANJIN("天津", "101030100"), + HANGZHOU("杭州", "101210100"), + GUANGZHOU("广州", "101280100"), + SHENZHEN("深圳", "101280600"), + WUHAN("武汉", "101200100"), + CHENGDU("成都", "101270100"); + + private final String name; + private final String code; + + CityCode(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static CityCode forValue(String value) { + for (CityCode cityCode : CityCode.values()) { + if (cityCode.name.equals(value)) { + return cityCode; + } + } + return NULL; + } + + } + + @Getter + public enum JobType { + NULL("不限", "0"), + FULL_TIME("全职", "1901"), + PART_TIME("兼职", "1903"); + + private final String name; + private final String code; + + JobType(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static JobType forValue(String value) { + for (JobType jobType : JobType.values()) { + if (jobType.name.equals(value)) { + return jobType; + } + } + return NULL; + } + } + + @Getter + public enum Salary { + NULL("不限", "0"), + BELOW_3K("3K以下", "402"), + FROM_3K_TO_5K("3-5K", "403"), + FROM_5K_TO_10K("5-10K", "404"), + FROM_10K_TO_20K("10-20K", "405"), + FROM_20K_TO_50K("20-50K", "406"), + ABOVE_50K("50K以上", "407"); + + private final String name; + private final String code; + + Salary(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Salary forValue(String value) { + for (Salary salary : Salary.values()) { + if (salary.name.equals(value)) { + return salary; + } + } + return NULL; + } + } + + @Getter + public enum Degree { + NULL("不限", "0"), + BELOW_JUNIOR_HIGH_SCHOOL("初中及以下", "209"), + SECONDARY_VOCATIONAL("中专/中技", "208"), + HIGH_SCHOOL("高中", "206"), + JUNIOR_COLLEGE("大专", "202"), + BACHELOR("本科", "203"), + MASTER("硕士", "204"), + DOCTOR("博士", "205"); + + private final String name; + private final String code; + + Degree(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Degree forValue(String value) { + for (Degree degree : Degree.values()) { + if (degree.name.equals(value)) { + return degree; + } + } + return NULL; + } + } + + @Getter + public enum Scale { + NULL("不限", "0"), + ZERO_TO_TWENTY("0-20人", "301"), + TWENTY_TO_NINETY_NINE("20-99人", "302"), + ONE_HUNDRED_TO_FOUR_NINETY_NINE("100-499人", "303"), + FIVE_HUNDRED_TO_NINE_NINETY_NINE("500-999人", "304"), + ONE_THOUSAND_TO_NINE_NINE_NINE_NINE("1000-9999人", "305"), + TEN_THOUSAND_ABOVE("10000人以上", "306"); + + private final String name; + private final String code; + + Scale(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Scale forValue(String value) { + for (Scale scale : Scale.values()) { + if (scale.name.equals(value)) { + return scale; + } + } + return NULL; + } + } + + @Getter + public enum Financing { + NULL("不限", "0"), + UNFUNDED("未融资", "801"), + ANGEL_ROUND("天使轮", "802"), + A_ROUND("A轮", "803"), + B_ROUND("B轮", "804"), + C_ROUND("C轮", "805"), + D_AND_ABOVE("D轮及以上", "806"), + LISTED("已上市", "807"), + NO_NEED("不需要融资", "808"); + + private final String name; + private final String code; + + Financing(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Financing forValue(String value) { + for (Financing financing : Financing.values()) { + if (financing.name.equals(value)) { + return financing; + } + } + return NULL; + } + } + + @Getter + public enum Industry { + NULL("不限", "0"), + INTERNET("互联网", "100020"), + COMPUTER_SOFTWARE("计算机软件", "100021"), + CLOUD_COMPUTING("云计算", "100029"); + + private final String name; + private final String code; + + Industry(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Industry forValue(String value) { + for (Industry industry : Industry.values()) { + if (industry.name.equals(value)) { + return industry; + } + } + return NULL; + } + } +} diff --git a/src/main/java/boss/BossScheduled.java b/src/main/java/boss/BossScheduled.java new file mode 100644 index 00000000..f2defb64 --- /dev/null +++ b/src/main/java/boss/BossScheduled.java @@ -0,0 +1,30 @@ +package boss; + +import lombok.extern.slf4j.Slf4j; +import utils.JobUtils; +import utils.Platform; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class BossScheduled { + + public static void main(String[] args) { + JobUtils.runScheduled(Platform.BOSS); + } + + public static void postJobs() { + safeRun(() -> Boss.main(null)); + } + + // 任务执行的安全包装,防止异常 + private static void safeRun(Runnable task) { + try { + task.run(); + } catch (Exception e) { + log.error("safeRun异常:{}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/boss/Locators.java b/src/main/java/boss/Locators.java new file mode 100644 index 00000000..3bfdeb63 --- /dev/null +++ b/src/main/java/boss/Locators.java @@ -0,0 +1,63 @@ +package boss; + +/** + * Boss直聘网页元素定位器 + * 集中管理所有页面元素的定位表达式 + */ +public class Locators { + // 主页相关元素 + public static final String LOGIN_BTN = "//li[@class='nav-figure']"; + public static final String LOGIN_SCAN_SWITCH = "//div[@class='btn-sign-switch ewm-switch']"; + + /** + * 搜索结果页相关元素 + */ + // 用于判断岗位列表区块是否加载完成 + public static final String JOB_LIST_CONTAINER = "//div[@class='job-list-container']"; + // 定位一个岗位卡 + public static final String JOB_CARD_BOX = "li.job-card-box"; + + /** + * 岗位列表 + */ + // 定位所有岗位卡片,用于获取当前获取到的岗位总数 + public static final String JOB_LIST_SELECTOR = "ul.rec-job-list li.job-card-box"; + // 岗位名称 + public static final String JOB_NAME = "a.job-name"; + // 公司名称 + public static final String COMPANY_NAME = "span.boss-name"; + // 公司区域 + public static final String JOB_AREA = "span.company-location"; + // 岗位标签 + public static final String TAG_LIST = "ul.tag-list li"; + + // 职位详情页元素 + public static final String CHAT_BUTTON = "[class*='btn btn-startchat']"; + public static final String ERROR_CONTENT = "//div[@class='error-content']"; + public static final String JOB_DETAIL_SALARY = "//div[@class='info-primary']//span[@class='salary']"; + public static final String RECRUITER_INFO = "//div[@class='boss-info-attr']"; + public static final String HR_ACTIVE_TIME = "//span[@class='boss-active-time']"; + public static final String JOB_DESCRIPTION = "//div[@class='job-sec-text']"; + + // 聊天相关元素 + public static final String DIALOG_TITLE = "//div[@class='dialog-title']"; + public static final String DIALOG_CLOSE = "//i[@class='icon-close']"; + public static final String CHAT_INPUT = "//div[@id='chat-input']"; + public static final String DIALOG_CONTAINER = "//div[@class='dialog-container']"; + public static final String SEND_BUTTON = "//button[@type='send']"; + public static final String IMAGE_UPLOAD = "//div[@aria-label='发送图片']//input[@type='file']"; + public static final String DIALOG_CONTENT = "//div[@class='dialog-con']"; + public static final String SCROLL_LOAD_MORE = "//div[contains(text(), '滚动加载更多')]"; + + // 消息列表页元素 + public static final String CHAT_LIST_ITEM = "//li[@role='listitem']"; + public static final String COMPANY_NAME_IN_CHAT = "//div[@class='title-box']/span[@class='name-box']//span[2]"; + public static final String LAST_MESSAGE = "//div[@class='gray last-msg']/span[@class='last-msg-text']"; + public static final String FINISHED_TEXT = "//div[@class='finished']"; + + public static final String DIALOG_CON = ".dialog-con"; + public static final String LOGIN_BTNS = "//div[@class='btns']"; + public static final String PAGE_HEADER = "//h1"; + public static final String ERROR_PAGE_LOGIN = "//a[@ka='403_login']"; + +} \ No newline at end of file diff --git a/src/main/java/boss/data.json b/src/main/java/boss/data.json new file mode 100644 index 00000000..d443e6c4 --- /dev/null +++ b/src/main/java/boss/data.json @@ -0,0 +1,25 @@ +{ + "blackCompanies": [ + "境开科技", + "法本信息", + "正佳科技", + "广州市千兔科技", + "智和瑞成", + "羁绊科技", + "阿里巴巴智能信息事业群", + "海南钦诚", + "头文科技", + "法本", + "三七互娱" + ], + "blackJobs": [ + "视觉", + "设计", + "外包", + "现场", + "驻场" + ], + "blackRecruiters": [ + "猎头" + ] +} \ No newline at end of file diff --git a/src/main/java/job51/Job51.java b/src/main/java/job51/Job51.java new file mode 100644 index 00000000..d69fff2c --- /dev/null +++ b/src/main/java/job51/Job51.java @@ -0,0 +1,238 @@ +package job51; + +import lombok.SneakyThrows; +import org.openqa.selenium.*; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import utils.JobUtils; +import utils.SeleniumUtil; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static utils.Bot.sendMessageByTime; +import static utils.Constant.*; +import static utils.JobUtils.formatDuration; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + * 前程无忧自动投递简历 + */ +public class Job51 { + static { + // 在类加载时就设置日志文件名,确保Logger初始化时能获取到正确的属性 + System.setProperty("log.name", "job51"); + } + + private static final Logger log = LoggerFactory.getLogger(Job51.class); + + static Integer page = 1; + static Integer maxPage = 50; + static String cookiePath = "./src/main/java/job51/cookie.json"; + static String homeUrl = "https://www.51job.com"; + static String loginUrl = "https://login.51job.com/login.php?lang=c&url=https://www.51job.com/&qrlogin=2"; + static String baseUrl = "https://we.51job.com/pc/search?"; + static List resultList = new ArrayList<>(); + static Job51Config config = Job51Config.init(); + static Date startDate; + + public static void main(String[] args) { + String searchUrl = getSearchUrl(); + SeleniumUtil.initDriver(); + startDate = new Date(); + Login(); + config.getKeywords().forEach(keyword -> resume(searchUrl + "&keyword=" + keyword)); + printResult(); + } + + private static void printResult() { + String message = String.format("\n51job投递完成,共投递%d个简历,用时%s", resultList.size(), formatDuration(startDate, new Date())); + log.info(message); + sendMessageByTime(message); + resultList.clear(); + CHROME_DRIVER.close(); + CHROME_DRIVER.quit(); + + // 确保所有日志都被刷新到文件 + try { + Thread.sleep(1000); // 等待1秒确保日志写入完成 + // 强制刷新日志 - 使用正确的方法 + ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static String getSearchUrl() { + return baseUrl + + JobUtils.appendListParam("jobArea", config.getJobArea()) + + JobUtils.appendListParam("salary", config.getSalary()); + } + + private static void Login() { + CHROME_DRIVER.get(homeUrl); + if (SeleniumUtil.isCookieValid(cookiePath)) { + SeleniumUtil.loadCookie(cookiePath); + CHROME_DRIVER.navigate().refresh(); + SeleniumUtil.sleep(1); + } + if (isLoginRequired()) { + log.error("cookie失效,尝试扫码登录..."); + scanLogin(); + } + } + + private static boolean isLoginRequired() { + try { + String text = CHROME_DRIVER.findElement(By.cssSelector("span.login")).getText(); + return text != null && text.contains("登录"); + } catch (Exception e) { + log.info("cookie有效,已登录..."); + return false; + } + } + + @SneakyThrows + private static void resume(String url) { + CHROME_DRIVER.get(url); + SeleniumUtil.sleep(1); + + // 再次判断是否登录 + WebElement login = WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[contains(@class, 'uname')]"))); + if (login != null && isNotNullOrEmpty(login.getText()) && login.getText().contains("登录")) { + login.click(); + WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//i[contains(@class, 'passIcon')]"))).click(); + log.info("请扫码登录..."); + WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[contains(@class, 'joblist')]"))); + SeleniumUtil.saveCookie(cookiePath); + } + + //由于51更新,每投递一页之前,停止10秒 + SeleniumUtil.sleep(10); + + int i = 0; + try { + CHROME_DRIVER.findElements(By.className("ss")).get(i).click(); + } catch (Exception e) { + findAnomaly(); + } + for (int j = page; j <= maxPage; j++) { + while (true) { + try { + WebElement mytxt = WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.id("jump_page"))); + SeleniumUtil.sleep(5); + mytxt.click(); + mytxt.clear(); + mytxt.sendKeys(String.valueOf(j)); + WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#app > div > div.post > div > div > div.j_result > div > div:nth-child(2) > div > div.bottom-page > div > div > span.jumpPage"))).click(); + ACTIONS.keyDown(Keys.CONTROL).sendKeys(Keys.HOME).keyUp(Keys.CONTROL).perform(); + log.info("第 {} 页", j); + break; + } catch (Exception e) { + log.error("mytxt.clear()可能异常..."); + SeleniumUtil.sleep(1); + findAnomaly(); + CHROME_DRIVER.navigate().refresh(); + } + } + postCurrentJob(); + } + } + + public static boolean isNullOrEmpty(String str) { + return str == null || str.isBlank(); + } + + public static boolean isNotNullOrEmpty(String str) { + return !isNullOrEmpty(str); + } + + + @SneakyThrows + private static void postCurrentJob() { + SeleniumUtil.sleep(1); + // 选择所有岗位,批量投递 + List checkboxes = CHROME_DRIVER.findElements(By.cssSelector("div.ick")); + if (checkboxes.isEmpty()) { + return; + } + List titles = CHROME_DRIVER.findElements(By.cssSelector("[class*='jname text-cut']")); + List companies = CHROME_DRIVER.findElements(By.cssSelector("[class*='cname text-cut']")); + JavascriptExecutor executor = CHROME_DRIVER; + for (int i = 0; i < checkboxes.size(); i++) { + WebElement checkbox = checkboxes.get(i); + executor.executeScript("arguments[0].click();", checkbox); + String title = titles.get(i).getText(); + String company = companies.get(i).getText(); + resultList.add(company + " | " + title); + log.info("选中:{} | {} 职位", company, title); + } + SeleniumUtil.sleep(1); + ACTIONS.keyDown(Keys.CONTROL).sendKeys(Keys.HOME).keyUp(Keys.CONTROL).perform(); + boolean success = false; + while (!success) { + try { + // 查询按钮是否存在 + WebElement parent = CHROME_DRIVER.findElement(By.cssSelector("div.tabs_in")); + List button = parent.findElements(By.cssSelector("button.p_but")); + // 如果按钮存在,则点击 + if (button != null && !button.isEmpty()) { + SeleniumUtil.sleep(1); + button.get(1).click(); + success = true; + } + } catch (ElementClickInterceptedException e) { + log.error("失败,1s后重试.."); + SeleniumUtil.sleep(1); + } + } + + try { + SeleniumUtil.sleep(3); + String text = CHROME_DRIVER.findElement(By.xpath("//div[@class='successContent']")).getText(); + if (text.contains("快来扫码下载~")) { + //关闭弹窗 + CHROME_DRIVER.findElement(By.cssSelector("[class*='van-icon van-icon-cross van-popup__close-icon van-popup__close-icon--top-right']")).click(); + } + } catch (Exception ignored) { + log.info("未找到投递成功弹窗!可能为单独投递申请弹窗!"); + } + String particularly = null; + try { + particularly = CHROME_DRIVER.findElement(By.xpath("//div[@class='el-dialog__body']/span")).getText(); + } catch (Exception ignored) { + } + if (particularly != null && particularly.contains("需要到企业招聘平台单独申请")) { + //关闭弹窗 + CHROME_DRIVER.findElement(By.cssSelector("#app > div > div.post > div > div > div.j_result > div > div:nth-child(2) > div > div:nth-child(2) > div:nth-child(2) > div > div.el-dialog__header > button > i")).click(); + log.info("关闭单独投递申请弹窗成功!"); + } + } + + private static void findAnomaly() { + try { + String verify = CHROME_DRIVER.findElement(By.xpath("//p[@class='waf-nc-title']")).getText(); + if (verify.contains("验证")) { + //关闭弹窗 + log.error("出现访问验证了!程序退出..."); + printResult(); + CHROME_DRIVER.close(); + CHROME_DRIVER.quit(); + } + } catch (Exception ignored) { + log.info("未出现访问验证,继续运行..."); + } + } + + private static void scanLogin() { + log.info("等待扫码登陆.."); + CHROME_DRIVER.get(loginUrl); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//a[contains(text(), '在线简历')]"))); + SeleniumUtil.saveCookie(cookiePath); + } + +} diff --git a/src/main/java/job51/Job51Config.java b/src/main/java/job51/Job51Config.java new file mode 100644 index 00000000..83389a56 --- /dev/null +++ b/src/main/java/job51/Job51Config.java @@ -0,0 +1,45 @@ +package job51; + +import lombok.Data; +import lombok.SneakyThrows; +import utils.JobUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + * 前程无忧自动投递简历 + */ +@Data +public class Job51Config { + + + /** + * 搜索关键词列表 + */ + private List keywords; + + /** + * 城市编码 + */ + private List jobArea; + + /** + * 薪资范围 + */ + private List salary; + + + @SneakyThrows + public static Job51Config init() { + Job51Config config = JobUtils.getConfig(Job51Config.class); + // 转换城市编码 + config.setJobArea(config.getJobArea().stream().map(value -> Job51Enum.jobArea.forValue(value).getCode()).collect(Collectors.toList())); + // 转换薪资范围 + config.setSalary(config.getSalary().stream().map(value -> Job51Enum.Salary.forValue(value).getCode()).collect(Collectors.toList())); + return config; + } + +} diff --git a/src/main/java/job51/Job51Enum.java b/src/main/java/job51/Job51Enum.java new file mode 100644 index 00000000..97a28ebc --- /dev/null +++ b/src/main/java/job51/Job51Enum.java @@ -0,0 +1,77 @@ +package job51; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + * 前程无忧自动投递简历 + */ +public class Job51Enum { + + @Getter + public enum jobArea { + NULL("不限", "0"), + BEIJING("北京", "010000"), + SHANGHAI("上海", "020000"), + GUANGZHOU("广州", "030200"), + SHENZHEN("深圳", "040000"), + WUHAN("武汉", "180200"), + CHENGDU("成都", "090200"); + private final String name; + private final String code; + + jobArea(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static jobArea forValue(String value) { + for (jobArea cityCode : jobArea.values()) { + if (cityCode.name.equals(value)) { + return cityCode; + } + } + return NULL; + } + + } + + @Getter + public enum Salary { + NULL("不限", "0"), + BELOW_2K("2千以下", "01"), + FROM_2K_TO_3K("2-3千", "02"), + FROM_3K_TO_4_5K("3-4.5千", "03"), + FROM_4_5K_TO_6K("4.5-6千", "04"), + FROM_6K_TO_8K("6-8千", "05"), + FROM_8K_TO_10K("0.8-1万", "06"), + FROM_10K_TO_15K("1-1.5万", "07"), + FROM_15K_TO_20K("1.5-2万", "08"), + FROM_20K_TO_30K("2-3万", "09"), + FROM_30K_TO_40K("3-4万", "10"), + FROM_40K_TO_50K("4-5万", "11"), + ABOVE_50K("5万以上", "12"); + + private final String name; + private final String code; + + Salary(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static Salary forValue(String value) { + for (Salary salary : Salary.values()) { + if (salary.name.equals(value)) { + return salary; + } + } + return NULL; + } + } + +} diff --git a/src/main/java/job51/Job51Scheduled.java b/src/main/java/job51/Job51Scheduled.java new file mode 100644 index 00000000..394fc9df --- /dev/null +++ b/src/main/java/job51/Job51Scheduled.java @@ -0,0 +1,31 @@ +package job51; + +import lombok.extern.slf4j.Slf4j; +import utils.JobUtils; +import utils.Platform; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + * 前程无忧自动投递简历 + */ +@Slf4j +public class Job51Scheduled { + + public static void main(String[] args) { + JobUtils.runScheduled(Platform.JOB51); + } + + public static void postJobs() { + safeRun(() -> Job51.main(null)); + } + + // 任务执行的安全包装,防止异常 + private static void safeRun(Runnable task) { + try { + task.run(); + } catch (Exception e) { + log.error("safeRun异常:{}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/lagou/Lagou.java b/src/main/java/lagou/Lagou.java new file mode 100644 index 00000000..b46dc5d1 --- /dev/null +++ b/src/main/java/lagou/Lagou.java @@ -0,0 +1,387 @@ +package lagou; + +import lombok.SneakyThrows; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import utils.JobUtils; +import utils.SeleniumUtil; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static utils.Bot.sendMessageByTime; +import static utils.Constant.*; +import static utils.JobUtils.formatDuration; +import static utils.SeleniumUtil.isCookieValid; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class Lagou { + static { + // 在类加载时就设置日志文件名,确保Logger初始化时能获取到正确的属性 + System.setProperty("log.name", "lagou"); + } + + private static final Logger log = LoggerFactory.getLogger(Lagou.class); + + static Integer page = 1; + static Integer maxPage = 4; + static String homeUrl = "https://www.lagou.com?"; + static String wechatUrl = "https://open.weixin.qq.com/connect/qrconnect?appid=wx9d8d3686b76baff8&redirect_uri=https%3A%2F%2Fpassport.lagou.com%2Foauth20%2Fcallback_weixinProvider.html&response_type=code&scope=snsapi_login#wechat_redirect"; + static int oneKeyMaxJob = 20; + static int currentKeyJobNum = 0; + static int jobCount = 0; + static String cookiePath = "./src/main/java/lagou/cookie.json"; + static LagouConfig config = LagouConfig.init(); + static Date startDate; + + + public static void main(String[] args) { + SeleniumUtil.initDriver(); + startDate = new Date(); + login(); + CHROME_DRIVER.get(homeUrl); + homeUrl = "https://www.lagou.com/wn/zhaopin?fromSearch=true"; + config.getKeywords().forEach(keyword -> { + String searchUrl = getSearchUrl(keyword); + CHROME_DRIVER.get(searchUrl); + setMaxPage(); + for (int i = page; i <= maxPage || currentKeyJobNum > oneKeyMaxJob; i++) { + submit(); + try { + getWindow(); + CHROME_DRIVER.findElements(By.className("lg-pagination-item-link")).get(1).click(); + } catch (Exception e) { + break; + } + } + currentKeyJobNum = 0; + }); + printResult(); + } + + private static void printResult() { + String message = String.format("\n拉勾投递完成,共投递%d个岗位,用时%s", jobCount, formatDuration(startDate, new Date())); + log.info(message); + sendMessageByTime(message); + jobCount = 0; + CHROME_DRIVER.close(); + CHROME_DRIVER.quit(); + + // 确保所有日志都被刷新到文件 + try { + Thread.sleep(1000); // 等待1秒确保日志写入完成 + // 强制刷新日志 - 使用正确的方法 + ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static String getSearchUrl(String keyword) { + return homeUrl + + JobUtils.appendParam("city", config.getCityCode()) + + JobUtils.appendParam("kd", keyword) + + JobUtils.appendParam("yx", config.getSalary()) + + JobUtils.appendParam("gj", config.getGj()) + + JobUtils.appendListParam("gm", config.getScale()); + } + + /** + * 设置选项 + */ + private static void setMaxPage() { + // 模拟 Ctrl + End + ACTIONS.keyDown(Keys.CONTROL).sendKeys(Keys.END).keyUp(Keys.CONTROL).perform(); + WebElement secondLastLi = CHROME_DRIVER.findElement(By.xpath("(//ul[@class='lg-pagination']/li)[last()-1]")); + if (secondLastLi != null && secondLastLi.getText().matches("\\d+")) { + maxPage = Integer.parseInt(secondLastLi.getText()); + } + // 模拟 Ctrl + Home + ACTIONS.keyDown(Keys.CONTROL).sendKeys(Keys.HOME).keyUp(Keys.CONTROL).perform(); + } + + @SneakyThrows + private static void submit() { + // 获取所有的元素 + List elements = null; + try { + ACTIONS.sendKeys(Keys.HOME).perform(); + SeleniumUtil.sleep(1); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.id("openWinPostion"))); + elements = CHROME_DRIVER.findElements(By.id("openWinPostion")); + + } catch (Exception ignore) { + } + if (elements != null) { + for (int i = 0; i < elements.size() || currentKeyJobNum > oneKeyMaxJob; i++) { + WebElement element = null; + try { + element = elements.get(i); + } catch (Exception e) { + log.error("获取岗位列表中某个岗位失败,岗位列表数量:{},获取第【{}】个元素失败", i + 1, elements.size()); + } + try { + ACTIONS.moveToElement(element).perform(); + } catch (Exception e) { + getWindow(); + } + if (-1 == tryClick(element, i)) { + continue; + } + TimeUnit.SECONDS.sleep(1); + getWindow(); + String jobName; + WebElement submit; + try { + jobName = CHROME_DRIVER.findElement(By.className("header__HY1Cm")).getText(); + } catch (Exception e) { + try { + jobName = CHROME_DRIVER.findElement(By.className("position-head-wrap-position-name")).getText(); + } catch (Exception ex) { + SeleniumUtil.sleep(10); + continue; + } + + } + if (!(jobName != null && !jobName.isEmpty() && !jobName.contains("销"))) { + CHROME_DRIVER.close(); + getWindow(); + continue; + } + submit = CHROME_DRIVER.findElement(By.className("resume-deliver")); + if ("投简历".equals(submit.getText())) { + String jobTitle = null; + String companyName = null; + String jobInfo = null; + String companyInfo = null; + String salary = null; + String weal = null; + try { + jobTitle = CHROME_DRIVER.findElement(By.cssSelector("span.name__36WTQ")).getText(); + companyName = CHROME_DRIVER.findElement(By.cssSelector("span.company")).getText(); + jobInfo = CHROME_DRIVER.findElements(By.cssSelector("h3.position-tags span")) + .stream() + .map(WebElement::getText) + .collect(Collectors.joining("/")); + companyInfo = CHROME_DRIVER.findElement(By.cssSelector("div.header__HY1Cm")).getText(); + salary = CHROME_DRIVER.findElement(By.cssSelector("span.salary__22Kt_")).getText(); + weal = CHROME_DRIVER.findElement(By.cssSelector("li.labels")).getText(); + } catch (Exception e) { + log.error("获取职位信息失败", e); + try { + jobTitle = CHROME_DRIVER.findElement(By.cssSelector("span.position-head-wrap-position-name")).getText(); + companyName = CHROME_DRIVER.findElement(By.cssSelector("span.company")).getText(); + List jobInfoElements = CHROME_DRIVER.findElements(By.cssSelector("h3.position-tags span:not(.tag-point)")); + jobInfo = jobInfoElements.stream() + .map(WebElement::getText) + .collect(Collectors.joining("/")); + companyInfo = CHROME_DRIVER.findElement(By.cssSelector("span.company")).getText(); + salary = CHROME_DRIVER.findElement(By.cssSelector("span.salary")).getText(); + weal = CHROME_DRIVER.findElement(By.cssSelector("dd.job-advantage p")).getText(); + } catch (Exception ex) { + log.error("第二次获取职位信息失败,放弃了!", ex); + } + } + log.info("投递: {},职位: {},公司: {},职位信息: {},公司信息: {},薪资: {},福利: {}", jobTitle, jobTitle, companyName, jobInfo, companyInfo, salary, weal); + jobCount++; + currentKeyJobNum++; + TimeUnit.SECONDS.sleep(2); + submit.click(); + TimeUnit.SECONDS.sleep(2); + try { + WebElement send = CHROME_DRIVER.findElement(By.cssSelector("body > div:nth-child(45) > div > div.lg-design-modal-wrap.position-modal > div > div.lg-design-modal-content > div.lg-design-modal-footer > button.lg-design-btn.lg-design-btn-default")); + if ("确认投递".equals(send.getText())) { + send.click(); + } + } catch (Exception e) { + log.error("没有【确认投递】的弹窗,继续!"); + } + try { + WebElement confirm = CHROME_DRIVER.findElement(By.cssSelector("button.lg-design-btn.lg-design-btn-primary span")); + String buttonText = confirm.getText(); + if ("我知道了".equals(buttonText)) { + confirm.click(); + } else { + TimeUnit.SECONDS.sleep(1); + } + } catch (Exception e) { + log.error("第一次点击【我知道了】按钮失败...重试xpath点击..."); + TimeUnit.SECONDS.sleep(1); + try { + CHROME_DRIVER.findElement(By.xpath("/html/body/div[7]/div/div[2]/div/div[2]/div[2]/button[2]")).click(); + } catch (Exception ex) { + log.error("第二次点击【我知道了】按钮失败...放弃了!", ex); + TimeUnit.SECONDS.sleep(10); + CHROME_DRIVER.navigate().refresh(); + } + } + try { + TimeUnit.SECONDS.sleep(2); + CHROME_DRIVER.findElement(By.cssSelector("#__next > div:nth-child(3) > div > div > div.feedback_job__3EnWp > div.feedback_job_title__2y8Bj > div.feedback_job_deliver__3UIB5.feedback_job_active__3bbLa")).click(); + } catch (Exception e) { + log.error("这个岗位没有推荐职位..."); + TimeUnit.SECONDS.sleep(1); + } + } else if ("立即沟通".equals(submit.getText())) { + submit.click(); + try { + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//*[@id=\"modalConIm\"]"))).click(); + } catch (Exception e) { + submit.click(); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//*[@id=\"modalConIm\"]"))).click(); + } + } else { + log.info("这个岗位没有投简历按钮...一秒后关闭标签页面!"); + TimeUnit.SECONDS.sleep(1); + } + CHROME_DRIVER.close(); + getWindow(); + } + } + } + + private static void getWindow() { + try { + ArrayList tabs = new ArrayList<>(CHROME_DRIVER.getWindowHandles()); + if (tabs.size() > 1) { + CHROME_DRIVER.switchTo().window(tabs.get(1)); + } else { + CHROME_DRIVER.switchTo().window(tabs.get(0)); + } + } catch (Exception ignore) { + } + } + + private static int tryClick(WebElement element, int i) { + boolean isClicked = false; + int maxRetryCount = 5; + int retryCount = 0; + + try { + element.click(); + isClicked = true; + } catch (Exception e) { + try { + CHROME_DRIVER.findElements(By.id("openWinPostion")).get(i).click(); + isClicked = true; + } catch (Exception ex) { + log.info(ex.getMessage()); + } + } + return 0; + + /* + while (!isClicked && retryCount < maxRetryCount) { + try { + element.click(); + isClicked = true; + } catch (Exception e) { + retryCount++; + log.error("element.click() 点击失败,正在尝试重新点击...(正在尝试:第 {} 次)", retryCount); + TimeUnit.SECONDS.sleep(5); + + try { + CHROME_DRIVER.findElements(By.id("openWinPostion")).get(i).click(); + isClicked = true; + } catch (Exception ex) { + log.error(" get(i).click() 重试失败,尝试使用Actions点击...(正在尝试:第 {} 次)", retryCount); + TimeUnit.SECONDS.sleep(5); + try { + ACTIONS.keyDown(Keys.CONTROL).click(element).keyUp(Keys.CONTROL).build().perform(); + isClicked = true; + } catch (Exception exc) { + log.error("使用Actions点击也失败,等待10秒后再次尝试...(正在尝试:第 {} 次)", retryCount); + TimeUnit.SECONDS.sleep(10); + } + } + } + } + if (!isClicked) { + log.error("已尝试 {} 次,已达最大重试次数,少侠请重新来过!", maxRetryCount); + log.info("已投递 {} 次,正在退出...", jobCount); + CHROME_DRIVER.quit(); + return -1; + } else { + return 0; + } + */ + } + + @SneakyThrows + private static void newTab(int index) { + String windowHandle = CHROME_DRIVER.getWindowHandle(); + String company = CHROME_DRIVER.findElement(By.cssSelector(".company-name__2-SjF a")).getText(); + + String jobTitle = CHROME_DRIVER.findElement(By.cssSelector(".p-top__1F7CL a")).getText(); + CHROME_DRIVER.findElements(By.id("openWinPostion")).get(index).click(); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.className("resume-deliver"))); + + Set windowHandles = CHROME_DRIVER.getWindowHandles(); + windowHandles.remove(windowHandle); + String newWindowHandle = windowHandles.iterator().next(); + CHROME_DRIVER.switchTo().window(newWindowHandle); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.className("resume-deliver"))); + + if (!"已投递".equals(CHROME_DRIVER.findElements(By.className("resume-deliver")).get(0).getText())) { + CHROME_DRIVER.findElements(By.className("resume-deliver")).get(0).click(); + TimeUnit.SECONDS.sleep(1); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("button.lg-design-btn.lg-design-btn-primary"))).click(); + log.info("投递【{}】公司: 【{}】岗位", company, jobTitle); + } + CHROME_DRIVER.close(); + CHROME_DRIVER.switchTo().window(windowHandle); + } + + @SneakyThrows + private static void login() { + log.info("正在打开拉勾..."); + CHROME_DRIVER.get("https://www.lagou.com"); + log.info("拉勾正在登录..."); + if (isCookieValid(cookiePath)) { + SeleniumUtil.loadCookie(cookiePath); + CHROME_DRIVER.navigate().refresh(); + } + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.id("search_button"))); + if (isLoginRequired()) { + log.info("cookie失效,尝试扫码登录..."); + scanLogin(); + SeleniumUtil.saveCookie(cookiePath); + } else { + log.info("cookie有效,准备投递..."); + } + } + + private static boolean isLoginRequired() { + try { + WebElement header = CHROME_DRIVER.findElement(By.id("lg_tbar")); + return header.getText().contains("登录"); + } catch (Exception e) { + return true; + } + } + + private static void scanLogin() { + try { + CHROME_DRIVER.get(wechatUrl); + log.info("等待扫码.."); + WAIT.until(ExpectedConditions.elementToBeClickable(By.id("search_button"))); + } catch (Exception e) { + CHROME_DRIVER.navigate().refresh(); + } + + } + + +} diff --git a/src/main/java/lagou/LagouConfig.java b/src/main/java/lagou/LagouConfig.java new file mode 100644 index 00000000..a7c5ecb4 --- /dev/null +++ b/src/main/java/lagou/LagouConfig.java @@ -0,0 +1,52 @@ +package lagou; + +import lombok.Data; +import lombok.SneakyThrows; +import utils.JobUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class LagouConfig { + /** + * 搜索关键词列表 + */ + private List keywords; + + /** + * 城市编码 + */ + private String cityCode; + + /** + * 薪资范围 + */ + private String salary; + + /** + * 公司规模 + */ + private List scale; + + /** + * 工作年限 + */ + private String gj; + + @SneakyThrows + public static LagouConfig init() { + LagouConfig config = JobUtils.getConfig(LagouConfig.class); + // 转换城市编码 + config.setSalary(Objects.equals("不限", config.getSalary()) ? "0" : config.getSalary()); + List scales = config.getScale(); + config.setScale(scales.stream().map(scale -> "不限".equals(scale) ? "0" : scale).collect(Collectors.toList())); + return config; + } + +} diff --git a/src/main/java/lagou/LagouEnum.java b/src/main/java/lagou/LagouEnum.java new file mode 100644 index 00000000..03ee6565 --- /dev/null +++ b/src/main/java/lagou/LagouEnum.java @@ -0,0 +1,36 @@ +package lagou; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class LagouEnum { + + @Getter + public enum CityCode { + NULL("不限", "0"), + ALL("全国", "0"); + + private final String name; + private final String code; + + CityCode(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static CityCode forValue(String value) { + for (CityCode cityCode : CityCode.values()) { + if (cityCode.name.equals(value)) { + return cityCode; + } + } + return CityCode.valueOf(value); + } + } + +} diff --git a/src/main/java/lagou/LagouScheduled.java b/src/main/java/lagou/LagouScheduled.java new file mode 100644 index 00000000..f90a8856 --- /dev/null +++ b/src/main/java/lagou/LagouScheduled.java @@ -0,0 +1,30 @@ +package lagou; + +import lombok.extern.slf4j.Slf4j; +import utils.JobUtils; +import utils.Platform; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class LagouScheduled { + + public static void main(String[] args) { + JobUtils.runScheduled(Platform.LAGOU); + } + + public static void postJobs() { + safeRun(() -> Lagou.main(null)); + } + + // 任务执行的安全包装,防止异常 + private static void safeRun(Runnable task) { + try { + task.run(); + } catch (Exception e) { + log.error("safeRun异常:{}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/liepin/Liepin.java b/src/main/java/liepin/Liepin.java new file mode 100644 index 00000000..abe29ebe --- /dev/null +++ b/src/main/java/liepin/Liepin.java @@ -0,0 +1,531 @@ +package liepin; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.LoadState; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; +import utils.JobUtils; +import utils.PlaywrightUtil; +import utils.SeleniumUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +import static liepin.Locators.*; +import static utils.Bot.sendMessageByTime; +import static utils.JobUtils.formatDuration; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class Liepin { + static { + // 在类加载时就设置日志文件名,确保Logger初始化时能获取到正确的属性 + System.setProperty("log.name", "liepin"); + } + + private static final Logger log = LoggerFactory.getLogger(Liepin.class); + static String homeUrl = "https://www.liepin.com/"; + static String cookiePath = "./src/main/java/liepin/cookie.json"; + static int maxPage = 50; + static List resultList = new ArrayList<>(); + static String baseUrl = "https://www.liepin.com/zhaopin/?"; + static LiepinConfig config = LiepinConfig.init(); + static Date startDate; + + /** + * 保存页面源码到日志和文件,用于调试 + */ + private static void savePageSource(Page page, String context) { + try { + String pageSource = page.content(); + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + + // 保存完整源码到文件 + Path sourceDir = Paths.get("./target/logs/page_sources"); + Files.createDirectories(sourceDir); + + String fileName = String.format("liepin_page_%s_%s.html", context.replaceAll("[^a-zA-Z0-9]", "_"), timestamp); + Path sourceFile = sourceDir.resolve(fileName); + Files.write(sourceFile, pageSource.getBytes("UTF-8")); + + log.info("完整页面源码已保存到文件: {}", sourceFile.toAbsolutePath()); + + } catch (IOException e) { + log.error("保存页面源码失败: {}", e.getMessage()); + } + } + + + + public static void main(String[] args) { + PlaywrightUtil.init(); + startDate = new Date(); + login(); + for (String keyword : config.getKeywords()) { + submit(keyword); + } + printResult(); + } + + private static void printResult() { + String message = String.format("\n猎聘投递完成,共投递%d个岗位,用时%s", resultList.size(), formatDuration(startDate, new Date())); + log.info(message); + sendMessageByTime(message); + resultList.clear(); + PlaywrightUtil.close(); + + // 确保所有日志都被刷新到文件 + try { + Thread.sleep(1000); // 等待1秒确保日志写入完成 + // 强制刷新日志 - 使用正确的方法 + ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + + @SneakyThrows + private static void submit(String keyword) { + Page page = PlaywrightUtil.getPageObject(); + page.navigate(getSearchUrl() + "&key=" + keyword); + + // 等待分页元素加载 + page.waitForSelector(PAGINATION_BOX, new Page.WaitForSelectorOptions().setTimeout(10000)); + Locator paginationBox = page.locator(PAGINATION_BOX); + Locator lis = paginationBox.locator("li"); + setMaxPage(lis); + + for (int i = 0; i < maxPage; i++) { + try { + // 尝试关闭订阅弹窗 + Locator closeBtn = page.locator(SUBSCRIBE_CLOSE_BTN); + if (closeBtn.count() > 0) { + closeBtn.click(); + } + } catch (Exception ignored) { + } + + // 等待岗位卡片加载 + page.waitForSelector(JOB_CARDS, new Page.WaitForSelectorOptions().setTimeout(10000)); + log.info("正在投递【{}】第【{}】页...", keyword, i + 1); + submitJob(); + log.info("已投递第【{}】页所有的岗位...\n", i + 1); + + // 查找下一页按钮 + paginationBox = page.locator(PAGINATION_BOX); + Locator nextPage = paginationBox.locator(NEXT_PAGE); + if (nextPage.count() > 0 && nextPage.getAttribute("disabled") == null) { + nextPage.click(); + // PlaywrightUtil.sleep(1); // 休息一秒 + } else { + break; + } + } + log.info("【{}】关键词投递完成!", keyword); + } + + private static String getSearchUrl() { + return baseUrl + + JobUtils.appendParam("city", config.getCityCode()) + + JobUtils.appendParam("salary", config.getSalary()) + + JobUtils.appendParam("pubTime", config.getPubTime()) + + "¤tPage=" + 0 + "&dq=" + config.getCityCode(); + } + + + private static void setMaxPage(Locator lis) { + try { + int count = lis.count(); + if (count >= 2) { + String pageText = lis.nth(count - 2).textContent(); + int page = Integer.parseInt(pageText); + if (page > 1) { + maxPage = page; + } + } + } catch (Exception ignored) { + } + } + + private static void submitJob() { + Page page = PlaywrightUtil.getPageObject(); + + // 等待页面完全加载 + // try { + // page.waitForLoadState(LoadState.NETWORKIDLE, new Page.WaitForLoadStateOptions().setTimeout(10000)); + // } catch (Exception e) { + // log.warn("等待页面网络空闲超时,继续执行: {}", e.getMessage()); + // } + + // 获取hr数量 + Locator jobCards = page.locator(JOB_CARDS); + + // 等待岗位卡片加载完成 + // try { + // jobCards.first().waitFor(new Locator.WaitForOptions().setTimeout(10000)); + // } catch (Exception e) { + // log.warn("等待岗位卡片加载超时: {}", e.getMessage()); + // } + + int count = jobCards.count(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + + Locator jobTitleElements = page.locator(JOB_TITLE); + Locator companyNameElements = page.locator(COMPANY_NAME); + Locator salaryElements = page.locator(JOB_SALARY); + + if (i >= jobTitleElements.count() || i >= companyNameElements.count() || i >= salaryElements.count()) { + continue; + } + + String jobName = jobTitleElements.nth(i).textContent().replaceAll("\n", " ").replaceAll("【 ", "[").replaceAll(" 】", "]"); + String companyName = companyNameElements.nth(i).textContent().replaceAll("\n", " "); + String salary = salaryElements.nth(i).textContent().replaceAll("\n", " "); + String recruiterName = null; + + try { + // 获取当前岗位卡片 + Locator currentJobCard = page.locator(JOB_CARDS).nth(i); + + // 使用JavaScript滚动到卡片位置,更稳定 + try { + // 先滚动到卡片位置 + page.evaluate("(element) => element.scrollIntoView({behavior: 'instant', block: 'center'})", currentJobCard.elementHandle()); + // PlaywrightUtil.sleep(1); // 等待滚动完成 + + // 再次确保元素在视窗中 + page.evaluate("(element) => { const rect = element.getBoundingClientRect(); if (rect.top < 0 || rect.bottom > window.innerHeight) { element.scrollIntoView({behavior: 'instant', block: 'center'}); } }", currentJobCard.elementHandle()); + // PlaywrightUtil.sleep(1); + } catch (Exception scrollError) { + log.warn("JavaScript滚动失败,尝试页面滚动: {}", scrollError.getMessage()); + // 备用方案:滚动页面到大概位置 + page.evaluate("window.scrollBy(0, " + (i * 200) + ")"); + // PlaywrightUtil.sleep(1); + } + + // 查找HR区域 - 尝试多种可能的HR标签选择器 + Locator hrArea = null; + String[] hrSelectors = { + ".recruiter-info-box", // 根据页面源码,这是主要的HR区域类名 + ".recruiter-info, .hr-info, .contact-info", + "[class*='recruiter'], [class*='hr-'], [class*='contact']", + ".job-card-footer, .card-footer", + ".job-bottom, .bottom-info" + }; + + for (String selector : hrSelectors) { + Locator tempHrArea = currentJobCard.locator(selector); + if (tempHrArea.count() > 0) { + hrArea = tempHrArea.first(); + log.debug("找到HR区域,使用选择器: {}", selector); + break; + } + } + + // 如果找不到特定的HR区域,使用整个卡片 + if (hrArea == null) { + log.debug("未找到特定HR区域,使用整个岗位卡片"); + hrArea = currentJobCard; + } + + // 鼠标悬停到HR区域,触发按钮显示 - 简化悬停逻辑 + boolean hoverSuccess = false; + int hoverRetries = 3; + for (int retry = 0; retry < hoverRetries; retry++) { + try { + // 检查HR区域是否可见,如果不可见则跳过悬停 + if (!hrArea.isVisible()) { + log.debug("HR区域不可见,跳过悬停操作"); + hoverSuccess = true; // 设为成功,继续后续流程 + break; + } + + // 直接悬停,不再进行复杂的微调 + hrArea.hover(new Locator.HoverOptions().setTimeout(5000)); + hoverSuccess = true; + break; + } catch (Exception hoverError) { + log.warn("第{}次悬停失败: {}", retry + 1, hoverError.getMessage()); + if (retry < hoverRetries - 1) { + // 重试前重新滚动确保元素可见 + try { + page.evaluate("(element) => element.scrollIntoView({behavior: 'instant', block: 'center'})", currentJobCard.elementHandle()); + Thread.sleep(500); // 等待滚动完成 + } catch (Exception e) { + log.warn("重试前滚动失败: {}", e.getMessage()); + } + } + } + } + + if (!hoverSuccess) { + log.warn("悬停操作失败,但继续查找按钮"); + // 不再跳过,而是继续查找按钮,因为有些按钮可能不需要悬停就能显示 + } + + // PlaywrightUtil.sleep(1); // 等待按钮显示 + + // 获取hr名字 + try { + Locator hrNameElement = currentJobCard.locator(".recruiter-name, .hr-name, .contact-name, [class*='recruiter-name'], [class*='hr-name']"); + if (hrNameElement.count() > 0) { + recruiterName = hrNameElement.first().textContent(); + } else { + recruiterName = "HR"; + } + } catch (Exception e) { + log.error("获取HR名字失败: {}", e.getMessage()); + recruiterName = "HR"; + } + + } catch (Exception e) { + log.error("处理岗位卡片失败: {}", e.getMessage()); + continue; + } + + // 查找聊一聊按钮 + Locator button = null; + String buttonText = ""; + try { + // 在当前岗位卡片中查找按钮,尝试多种选择器 + Locator currentJobCard = page.locator(JOB_CARDS).nth(i); + + String[] buttonSelectors = { + "button.ant-btn.ant-btn-primary.ant-btn-round", + "button.ant-btn.ant-btn-round.ant-btn-primary", + "button[class*='ant-btn'][class*='primary']", + "button[class*='ant-btn'][class*='round']", + "button[class*='chat'], button[class*='talk']", + ".chat-btn, .talk-btn, .contact-btn", + "button:has-text('聊一聊')", + "button" // 最后尝试所有按钮 + }; + + for (String selector : buttonSelectors) { + try { + Locator tempButtons = currentJobCard.locator(selector); + int buttonCount = tempButtons.count(); + log.debug("选择器 '{}' 找到 {} 个按钮", selector, buttonCount); + + for (int j = 0; j < buttonCount; j++) { + Locator tempButton = tempButtons.nth(j); + try { + if (tempButton.isVisible()) { + String text = tempButton.textContent(); + log.debug("按钮文本: '{}'", text); + if (text != null && !text.trim().isEmpty()) { + button = tempButton; + buttonText = text.trim(); + // 只关注"聊一聊"按钮 + if (text.contains("聊一聊")) { + log.debug("找到目标按钮: '{}'", text); + break; + } + } + } + } catch (Exception ignore) { + log.debug("获取按钮文本失败: {}", ignore.getMessage()); + } + } + + if (button != null && buttonText.contains("聊一聊")) { + break; + } + } catch (Exception e) { + log.debug("选择器 '{}' 查找失败: {}", selector, e.getMessage()); + } + } + + } catch (Exception e) { + log.error("查找按钮失败: {}", e.getMessage()); + // 保存页面源码用于调试 + savePageSource(page, "button_search_failed"); + continue; + } + + // 检查按钮文本并点击 + if (button != null && buttonText.contains("聊一聊")) { + try { + // 在点击按钮前进行鼠标微调,先向右移动2像素,再向左移动2像素 + try { + var boundingBox = button.boundingBox(); + if (boundingBox != null) { + double centerX = boundingBox.x + boundingBox.width / 2; + double centerY = boundingBox.y + boundingBox.height / 2; + + // 先移动到按钮中心 + page.mouse().move(centerX, centerY); + Thread.sleep(50); + + // 向右移动2像素 + page.mouse().move(centerX + 2, centerY); + Thread.sleep(50); + + // 向左移动2像素(回到中心再向左2像素) + page.mouse().move(centerX - 2, centerY); + Thread.sleep(50); + + // 回到中心位置 + page.mouse().move(centerX, centerY); + Thread.sleep(50); + + log.debug("完成鼠标微调,准备点击按钮"); + } + } catch (Exception moveError) { + log.warn("鼠标微调失败,直接点击按钮: {}", moveError.getMessage()); + } + + button.click(); + // PlaywrightUtil.sleep(1); // 等待点击响应 + + // 猎聘会自动发送打招呼语,所以我们只需要关闭聊天窗口 + try { + // 等待聊天界面加载 + page.waitForSelector(CHAT_HEADER, new Page.WaitForSelectorOptions().setTimeout(3000)); + + // 直接关闭聊天窗口 + Locator close = page.locator(CHAT_CLOSE); + if (close.count() > 0) { + PlaywrightUtil.sleep(1); + close.click(); + } + + resultList.add(sb.append("【").append(companyName).append(" ").append(jobName).append(" ").append(salary).append(" ").append(recruiterName).append(" ").append("】").toString()); + sb.setLength(0); + log.info("成功发起聊天:【{}】的【{}·{}】岗位", companyName, jobName, salary); + + } catch (Exception e) { + log.warn("关闭聊天窗口失败,但投递可能已成功: {}", e.getMessage()); + // 即使关闭失败,也认为投递成功 + resultList.add(sb.append("【").append(companyName).append(" ").append(jobName).append(" ").append(salary).append(" ").append(recruiterName).append(" ").append("】").toString()); + sb.setLength(0); + } + + } catch (Exception e) { + log.error("点击按钮失败: {}", e.getMessage()); + // 保存页面源码用于调试 + savePageSource(page, "button_click_failed"); + } + } else { + if (button != null) { + log.debug("跳过岗位(按钮文本不匹配): 【{}】的【{}·{}】岗位,按钮文本: '{}'", companyName, jobName, salary, buttonText); + } else { +// log.warn("未找到可点击的按钮: 【{}】的【{}·{}】岗位", companyName, jobName, salary); + // 保存页面源码用于调试 + savePageSource(page, "no_button_found"); + } + } + + // 等待一下,避免操作过快 + // PlaywrightUtil.sleep(1); + } + } + + @SneakyThrows + private static void login() { + log.info("正在打开猎聘网站..."); + Page page = PlaywrightUtil.getPageObject(); + page.navigate(homeUrl); + log.info("猎聘正在登录..."); + + if (PlaywrightUtil.isCookieValid(cookiePath)) { + PlaywrightUtil.loadCookies(cookiePath); + page.reload(); + } + + page.waitForSelector(HEADER_LOGO, new Page.WaitForSelectorOptions().setTimeout(10000)); + + if (isLoginRequired()) { + log.info("cookie失效,尝试扫码登录..."); + scanLogin(); + PlaywrightUtil.saveCookies(cookiePath); + } else { + log.info("cookie有效,准备投递..."); + } + } + + private static boolean isLoginRequired() { + Page page = PlaywrightUtil.getPageObject(); + String currentUrl = page.url(); + return !currentUrl.contains("c.liepin.com"); + } + + private static void scanLogin() { + try { + Page page = PlaywrightUtil.getPageObject(); + + // 点击切换登录类型按钮 + Locator switchBtn = page.locator(LOGIN_SWITCH_BTN); + if (switchBtn.count() > 0) { + switchBtn.click(); + } + + log.info("等待扫码.."); + + // 记录开始时间 + long startTime = System.currentTimeMillis(); + long maxWaitTime = 10 * 60 * 1000; // 10分钟,单位毫秒 + + // 主循环,直到登录成功或超时 + while (true) { + try { + // 检查是否已登录 + Locator loginButtons = page.locator(LOGIN_BUTTONS); + if (loginButtons.count() > 0) { + String login = loginButtons.first().textContent(); + if (!login.contains("登录")) { + log.info("用户扫码成功,继续执行..."); + break; + } + } + } catch (Exception ignored) { + try { + Locator userInfo = page.locator(USER_INFO); + if (userInfo.count() > 0) { + String login = userInfo.first().textContent(); + if (login.contains("你好")){ + break; + } + } + } catch (Exception e) { + log.error("获取登录状态失败!"); + } + } + + // 检查是否超过最大等待时间 + long elapsedTime = System.currentTimeMillis() - startTime; + if (elapsedTime > maxWaitTime) { + log.error("登录超时,10分钟内未完成扫码登录,程序将退出。"); + PlaywrightUtil.close(); // 关闭浏览器 + return; // 返回而不是退出整个程序 + } + PlaywrightUtil.sleep(1); + } + + // 登录成功后,保存Cookie + PlaywrightUtil.saveCookies(cookiePath); + log.info("登录成功,Cookie已保存。"); + + } catch (Exception e) { + log.error("scanLogin() 失败: {}", e.getMessage()); + PlaywrightUtil.close(); // 关闭浏览器 + return; // 返回而不是退出整个程序 + } + } + + + +} diff --git a/src/main/java/liepin/LiepinConfig.java b/src/main/java/liepin/LiepinConfig.java new file mode 100644 index 00000000..0f4e30b6 --- /dev/null +++ b/src/main/java/liepin/LiepinConfig.java @@ -0,0 +1,46 @@ +package liepin; + +import lombok.Data; +import lombok.SneakyThrows; +import utils.JobUtils; + +import java.util.List; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class LiepinConfig { + /** + * 搜索关键词列表 + */ + private List keywords; + + /** + * 城市编码 + */ + private String cityCode; + + /** + * 薪资范围 + */ + private String salary; + + + + /** + * 发布时间 + */ + private String pubTime; + + + @SneakyThrows + public static LiepinConfig init() { + LiepinConfig config = JobUtils.getConfig(LiepinConfig.class); + // 转换城市编码 + config.setCityCode(LiepinEnum.CityCode.forValue(config.getCityCode()).getCode()); + return config; + } + +} diff --git a/src/main/java/liepin/LiepinEnum.java b/src/main/java/liepin/LiepinEnum.java new file mode 100644 index 00000000..f95c9dbc --- /dev/null +++ b/src/main/java/liepin/LiepinEnum.java @@ -0,0 +1,43 @@ +package liepin; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class LiepinEnum { + + @Getter + public enum CityCode { + NULL("不限", "0"), + ALL("全国", "410"), + BEIJING("北京", "010"), + SHANGHAI("上海", "020"), + GUANGZHOU("广州", "050020"), + SHENZHEN("深圳", "050090"), + WUHAN("武汉", "170020"), + CHENGDU("成都", "280020"); + + private final String name; + private final String code; + + CityCode(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static CityCode forValue(String value) { + for (CityCode cityCode : CityCode.values()) { + if (cityCode.name.equals(value)) { + return cityCode; + } + } + return NULL; + } + + } + +} diff --git a/src/main/java/liepin/LiepinScheduled.java b/src/main/java/liepin/LiepinScheduled.java new file mode 100644 index 00000000..7e4fc72e --- /dev/null +++ b/src/main/java/liepin/LiepinScheduled.java @@ -0,0 +1,30 @@ +package liepin; + +import lombok.extern.slf4j.Slf4j; +import utils.JobUtils; +import utils.Platform; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class LiepinScheduled { + + public static void main(String[] args) { + JobUtils.runScheduled(Platform.LIEPIN); + } + + public static void postJobs() { + safeRun(() -> Liepin.main(null)); + } + + // 任务执行的安全包装,防止异常 + private static void safeRun(Runnable task) { + try { + task.run(); + } catch (Exception e) { + log.error("safeRun异常:{}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/liepin/Locators.java b/src/main/java/liepin/Locators.java new file mode 100644 index 00000000..a1f9eb3b --- /dev/null +++ b/src/main/java/liepin/Locators.java @@ -0,0 +1,33 @@ +package liepin; + +/** + * 猎聘网页元素定位器 + * 集中管理所有页面元素的定位表达式 + */ +public class Locators { + // 主页相关元素 + public static final String HEADER_LOGO = "#header-logo-box"; + public static final String LOGIN_SWITCH_BTN = "//div[@class='jsx-263198893 btn-sign-switch']"; + public static final String LOGIN_BUTTONS = "//button[@type='button']"; + public static final String USER_INFO = "//div[@id='header-quick-menu-user-info']"; + + // 搜索结果页相关元素 + public static final String PAGINATION_BOX = ".list-pagination-box"; + public static final String PAGINATION_ITEMS = ".list-pagination-box li"; + public static final String NEXT_PAGE = "li[title='Next Page']"; + public static final String SUBSCRIBE_CLOSE_BTN = "//div[contains(@class, 'subscribe-close-btn')]"; + + // 岗位列表相关元素 + public static final String JOB_CARDS = "//div[contains(@class, 'job-card-pc-container')]"; + public static final String JOB_TITLE = "//div[contains(@class, 'job-title-box')]"; + public static final String COMPANY_NAME = "//span[contains(@class, 'company-name')]"; + public static final String JOB_SALARY = "//span[contains(@class, 'job-salary')]"; + + // 聊天相关元素 + public static final String CHAT_BUTTON_PRIMARY = "//button[@class='ant-btn ant-btn-primary ant-btn-round']"; + public static final String CHAT_BUTTON_ALTERNATIVE = "//button[@class='ant-btn ant-btn-round ant-btn-primary']"; + public static final String CHAT_HEADER = ".__im_basic__header-wrap"; + public static final String CHAT_TEXTAREA = "//textarea[contains(@class, '__im_basic__textarea')]"; + public static final String CHAT_CLOSE = "div.__im_basic__contacts-title svg"; + public static final String RECRUITER_INFO = "//div[contains(@class, 'recruiter-info-box')]"; +} \ No newline at end of file diff --git a/src/main/java/utils/Bot.java b/src/main/java/utils/Bot.java new file mode 100644 index 00000000..b0bd4bad --- /dev/null +++ b/src/main/java/utils/Bot.java @@ -0,0 +1,145 @@ +package utils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.github.cdimascio.dotenv.Dotenv; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; + +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class Bot { + + private static final String HOOK_URL; + private static final String BARK_URL; + private static boolean isSend; + private static boolean isBarkSend; + + static { + // 加载环境变量 + Dotenv dotenv = Dotenv + .configure() + .directory("/src/main/resources") + .load(); + HOOK_URL = dotenv.get("HOOK_URL"); + BARK_URL = dotenv.get("BARK_URL"); + + // 使用 Jackson 加载 config.yaml 配置 + try { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + HashMap config = mapper.readValue(new File("/src/main/resources/config.yaml"), new TypeReference>() { + }); + log.info("YAML 配置内容: {}", config); + + // 获取 bot 配置 + HashMap botConfig = safeCast(config.get("bot"), HashMap.class); + if (botConfig != null && botConfig.get("is_send") != null) { + isSend = Boolean.TRUE.equals(safeCast(botConfig.get("is_send"), Boolean.class)); + } else { + log.warn("配置文件中缺少 'bot.is_send' 键或值为空,不发送消息。"); + isSend = false; + } + + // 获取Bark配置 + if (botConfig != null && botConfig.get("is_bark_send") != null) { + isBarkSend = Boolean.TRUE.equals(safeCast(botConfig.get("is_bark_send"), Boolean.class)); + } else { + log.warn("配置文件中缺少 'bot.is_bark_send' 键或值为空,不发送Bark消息。"); + isBarkSend = false; + } + } catch (IOException e) { + log.error("读取 config.yaml 异常:{}", e.getMessage()); + isSend = false; // 如果读取配置文件失败,默认不发送消息 + } + } + + public static void sendMessageByTime(String message) { + if (!isSend && !isBarkSend) { + return; + } + // 格式化当前时间 + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + String formattedMessage = String.format("%s %s", currentTime, message); + sendMessage(formattedMessage); + sendBarkMessage("AI投递提醒", formattedMessage); + } + + public static void sendMessage(String message) { + if (!isSend) { + return; + } + // 发送HTTP请求 + try { + String response = Request.post(HOOK_URL) + .bodyString("{\"msgtype\": \"text\", \"text\": {\"content\": \"" + message + "\"}}", + org.apache.hc.core5.http.ContentType.APPLICATION_JSON) + .execute() + .returnContent() + .asString(); + log.info("消息推送成功: {}", response); + } catch (Exception e) { + log.error("消息推送失败: {}", e.getMessage()); + } + } + + /** + * 发送Bark消息 + * @param title 消息标题 + * @param message 消息内容 + */ + public static void sendBarkMessage(String title, String message) { + if (!isBarkSend || BARK_URL == null || BARK_URL.isEmpty()) { + log.info("Bark消息推送未启用或URL未配置"); + return; + } + + try { + // 构建Bark URL,格式通常为:https://api.day.app/{BARK_KEY}/{TITLE}/{BODY} + String encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString()); + String encodedMessage = URLEncoder.encode(message, StandardCharsets.UTF_8.toString()); + String barkRequestUrl = String.format("%s/%s/%s", BARK_URL, encodedTitle, encodedMessage); + System.out.println("barkRequestUrl = " + barkRequestUrl); + + String response = Request.get(barkRequestUrl) + .execute() + .returnContent() + .asString(); + log.info("Bark消息推送成功: {}", response); + } catch (Exception e) { + log.error("Bark消息推送失败: {}", e.getMessage()); + } + } + + public static void main(String[] args) { + sendMessageByTime("企业微信推送测试消息..."); + } + + /** + * 通用的安全类型转换方法,避免未检查的类型转换警告 + * + * @param obj 要转换的对象 + * @param clazz 目标类型的 Class 对象 + * @param 目标类型 + * @return 如果对象类型匹配,则返回转换后的对象,否则返回 null + */ + @SuppressWarnings("unchecked") + public static T safeCast(Object obj, Class clazz) { + if (clazz.isInstance(obj)) { + return (T) obj; + } else { + return null; + } + } +} diff --git a/src/main/java/utils/Constant.java b/src/main/java/utils/Constant.java new file mode 100644 index 00000000..a53c668e --- /dev/null +++ b/src/main/java/utils/Constant.java @@ -0,0 +1,21 @@ +package utils; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.WebDriverWait; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class Constant { + public static ChromeDriver CHROME_DRIVER; + public static ChromeDriver MOBILE_CHROME_DRIVER; + public static Actions ACTIONS; + public static Actions MOBILE_ACTIONS; + public static WebDriverWait WAIT; + public static WebDriverWait MOBILE_WAIT; + public static int WAIT_TIME = 30; + public static String UNLIMITED_CODE = "0"; +} diff --git a/src/main/java/utils/EncryptDecryptUtil.java b/src/main/java/utils/EncryptDecryptUtil.java new file mode 100644 index 00000000..d25cbf17 --- /dev/null +++ b/src/main/java/utils/EncryptDecryptUtil.java @@ -0,0 +1,58 @@ +package utils; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +@Slf4j +public class EncryptDecryptUtil { + private static final String AES_KEY = "sB1+lkOiIqzMKO2yR/B91A=="; + + private static final String PLAIN_TEXT = ""; + + public static void main(String[] args) { + try { + String encryptedText = encrypt(PLAIN_TEXT, AES_KEY); + System.out.println(encryptedText); + String decryptedText = decrypt(encryptedText, AES_KEY); + System.out.printf(decryptedText); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + public static String encrypt(String plainText, String base64Key) throws Exception { + byte[] decodedKey = Base64.getDecoder().decode(base64Key); + SecretKey secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes()); + + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public static String decrypt(String encryptedText, String base64Key) throws Exception { + byte[] decodedKey = Base64.getDecoder().decode(base64Key); + SecretKey secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText)); + + return new String(decryptedBytes); + } + + public static String decrypt(String encryptedText) { + try { + return decrypt(encryptedText, AES_KEY); + } catch (Exception e) { + log.error("decrypt异常!"); + return PLAIN_TEXT; + } + } + +} diff --git a/src/main/java/utils/Finder.java b/src/main/java/utils/Finder.java new file mode 100644 index 00000000..7a256233 --- /dev/null +++ b/src/main/java/utils/Finder.java @@ -0,0 +1,242 @@ +package utils; + +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +/** + * Boss直聘元素查找工具类 + * 封装Selenium和Playwright元素查找逻辑 + * + * 替代原来代码中直接使用的: + * - CHROME_DRIVER.findElement(By.xpath("...")) + * - CHROME_DRIVER.findElement(By.cssSelector("...")) + * - WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("..."))) + * - SeleniumUtil.findElement("...", "") + */ +public class Finder { + private static final Logger log = LoggerFactory.getLogger(Finder.class); + private static final WebDriver driver = Constant.CHROME_DRIVER; + private static final int DEFAULT_TIMEOUT_SECONDS = 10; + + /** + * 基于Selenium WebDriver查找单个元素 + * + * 替代原来的代码模式: + * WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("..."))) + * + * @param selector 选择器表达式 + * @param timeoutSeconds 超时时间(秒) + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional findElement(String selector, int timeoutSeconds) { + try { + By by = parseSelector(selector); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds)); + return Optional.of(wait.until(ExpectedConditions.presenceOfElementLocated(by))); + } catch (Exception e) { + log.debug("未找到元素: {}, 原因: {}", selector, e.getMessage()); + return Optional.empty(); + } + } + + /** + * 基于Selenium WebDriver查找单个元素,使用默认超时时间 + * + * 替代原来的代码模式: + * CHROME_DRIVER.findElement(By.xpath("...")) + * + * @param selector 选择器表达式 + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional findElement(String selector) { + return findElement(selector, DEFAULT_TIMEOUT_SECONDS); + } + + /** + * 基于Selenium WebDriver查找多个元素 + * + * 替代原来的代码模式: + * CHROME_DRIVER.findElements(By.xpath("...")) + * + * @param selector 选择器表达式 + * @return 找到的元素列表,如果没找到返回空列表 + */ + public static List findElements(String selector) { + try { + By by = parseSelector(selector); + return driver.findElements(by); + } catch (Exception e) { + log.debug("查找元素列表失败: {}, 原因: {}", selector, e.getMessage()); + return List.of(); + } + } + + /** + * 等待元素可见 + * + * 替代原来的代码模式: + * WAIT.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("..."))) + * + * @param selector 选择器表达式 + * @param timeoutSeconds 超时时间(秒) + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional waitForElementVisible(String selector, int timeoutSeconds) { + try { + By by = parseSelector(selector); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds)); + return Optional.of(wait.until(ExpectedConditions.visibilityOfElementLocated(by))); + } catch (Exception e) { + log.debug("等待元素可见超时: {}, 原因: {}", selector, e.getMessage()); + return Optional.empty(); + } + } + + /** + * 等待元素可见,使用默认超时时间 + * + * @param selector 选择器表达式 + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional waitForElementVisible(String selector) { + return waitForElementVisible(selector, DEFAULT_TIMEOUT_SECONDS); + } + + /** + * 等待元素可点击 + * + * 替代原来的代码模式: + * WAIT.until(ExpectedConditions.elementToBeClickable(By.xpath("..."))) + * + * @param selector 选择器表达式 + * @param timeoutSeconds 超时时间(秒) + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional waitForElementClickable(String selector, int timeoutSeconds) { + try { + By by = parseSelector(selector); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds)); + return Optional.of(wait.until(ExpectedConditions.elementToBeClickable(by))); + } catch (Exception e) { + log.debug("等待元素可点击超时: {}, 原因: {}", selector, e.getMessage()); + return Optional.empty(); + } + } + + /** + * 等待元素可点击,使用默认超时时间 + * + * @param selector 选择器表达式 + * @return 找到的元素,如果没找到返回Optional.empty() + */ + public static Optional waitForElementClickable(String selector) { + return waitForElementClickable(selector, DEFAULT_TIMEOUT_SECONDS); + } + + /** + * 判断元素是否存在 + * + * 替代原来的try-catch模式: + * try { + * CHROME_DRIVER.findElement(By.xpath("...")); + * return true; + * } catch (Exception e) { + * return false; + * } + * + * @param selector 选择器表达式 + * @return true如果元素存在,否则false + */ + public static boolean isElementPresent(String selector) { + try { + By by = parseSelector(selector); + driver.findElement(by); + return true; + } catch (NoSuchElementException e) { + return false; + } + } + + /** + * 基于Playwright查找单个元素 + * + * 替代原来的代码模式: + * page.querySelector("...") + * + * @param page Playwright页面对象 + * @param selector 选择器表达式 + * @return 找到的元素,如果没找到返回null + */ + public static ElementHandle findPlaywrightElement(Page page, String selector) { + try { + return page.querySelector(selector); + } catch (Exception e) { + log.debug("Playwright未找到元素: {}, 原因: {}", selector, e.getMessage()); + return null; + } + } + + /** + * 基于Playwright查找多个元素 + * + * 替代原来的代码模式: + * page.querySelectorAll("...") + * + * @param page Playwright页面对象 + * @param selector 选择器表达式 + * @return 找到的元素列表,如果没找到返回空列表 + */ + public static List findPlaywrightElements(Page page, String selector) { + try { + return page.querySelectorAll(selector); + } catch (Exception e) { + log.debug("Playwright查找元素列表失败: {}, 原因: {}", selector, e.getMessage()); + return List.of(); + } + } + + /** + * 获取Playwright的Locator对象 + * + * 替代原来的代码模式: + * page.locator("...") + * + * @param page Playwright页面对象 + * @param selector 选择器表达式 + * @return Locator对象 + */ + public static Locator getPlaywrightLocator(Page page, String selector) { + return page.locator(selector); + } + + /** + * 解析选择器表达式,判断是XPath还是CSS选择器 + * + * 自动判断选择器类型,避免手动区分By.xpath和By.cssSelector + * + * @param selector 选择器表达式 + * @return 解析后的By对象 + */ + private static By parseSelector(String selector) { + if (selector.startsWith("//") || selector.startsWith("(//") || selector.startsWith("/")) { + return By.xpath(selector); + } else if (selector.startsWith("[")) { + return By.cssSelector(selector); + } else { + return By.cssSelector(selector); + } + } +} \ No newline at end of file diff --git a/src/main/java/utils/Job.java b/src/main/java/utils/Job.java new file mode 100644 index 00000000..1ba031f4 --- /dev/null +++ b/src/main/java/utils/Job.java @@ -0,0 +1,74 @@ +package utils; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class Job implements Serializable { + /** + * 岗位链接 + */ + private String href; + + /** + * 岗位名称 + */ + private String jobName; + + /** + * 岗位地区 + */ + private String jobArea; + + /** + * 岗位信息 + */ + private String jobInfo; + + /** + * 岗位薪水 + */ + private String salary; + + /** + * 公司标签 + */ + private String companyTag; + + /** + * HR名称 + */ + private String recruiter; + + /** + * 公司名字 + */ + private String companyName; + + /** + * 公司信息 + */ + private String companyInfo; + + @Override + public String toString() { + return String.format("【%s, %s, %s, %s, %s, %s】", companyName, jobName, jobArea, salary, companyTag, recruiter); + } + + public String toString(Platform platform) { + if (platform == Platform.ZHILIAN) { + return String.format("【%s, %s, %s, %s, %s, %s, %s】", companyName, jobName, jobArea, companyTag, salary, recruiter, href); + } + if (platform == Platform.BOSS) { + return String.format("【%s, %s, %s, %s, %s, %s】", companyName, jobName, jobArea, salary, companyTag, recruiter); + } + return String.format("【%s, %s, %s, %s, %s, %s, %s】", companyName, jobName, jobArea, salary, companyTag, recruiter, href); + } +} + + diff --git a/src/main/java/utils/JobUtils.java b/src/main/java/utils/JobUtils.java new file mode 100644 index 00000000..ee7891fd --- /dev/null +++ b/src/main/java/utils/JobUtils.java @@ -0,0 +1,171 @@ +package utils; + +import static utils.Constant.UNLIMITED_CODE; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import boss.BossScheduled; +import job51.Job51Scheduled; +import lagou.LagouScheduled; +import liepin.LiepinScheduled; +import zhilian.ZhilianScheduled; + +@Slf4j +public class JobUtils { + + public static String appendParam(String name, String value) { + return Optional.ofNullable(value) + .filter(v -> !Objects.equals(UNLIMITED_CODE, v)) + .map(v -> "&" + name + "=" + v) + .orElse(""); + } + + public static String appendListParam(String name, List values) { + return Optional.ofNullable(values) + .filter(list -> !list.isEmpty() && !Objects.equals(UNLIMITED_CODE, list.getFirst())) + .map(list -> "&" + name + "=" + String.join(",", list)) + .orElse(""); + } + + @SneakyThrows + public static T getConfig(Class clazz) { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + InputStream is = clazz.getClassLoader().getResourceAsStream("config.yaml"); + if (is == null) { + throw new FileNotFoundException("无法找到 config.yaml 文件"); + } + JsonNode rootNode = mapper.readTree(is); + String key = clazz.getSimpleName().toLowerCase().replaceAll("config", ""); + JsonNode configNode = rootNode.path(key); + return mapper.treeToValue(configNode, clazz); + } + + public static void runScheduled(Platform platform) { + String platformName = platform.getPlatformName(); + switch (platform) { + case BOSS -> { + BossScheduled.postJobs(); + scheduleTaskAtTime(platformName, 10, 0, BossScheduled::postJobs); + scheduleTaskAtTime(platformName, 15, 0, BossScheduled::postJobs); + } + case JOB51 -> { + Job51Scheduled.postJobs(); + scheduleTaskAtTime(platformName, 10, 0, Job51Scheduled::postJobs); + } + case LIEPIN -> { + LiepinScheduled.postJobs(); + scheduleTaskAtTime(platformName, 10, 0, LiepinScheduled::postJobs); + } + case ZHILIAN -> { + ZhilianScheduled.postJobs(); + scheduleTaskAtTime(platformName, 10, 0, ZhilianScheduled::postJobs); + } + case LAGOU -> { + LagouScheduled.postJobs(); + scheduleTaskAtTime(platformName, 10, 0, LagouScheduled::postJobs); + } + default -> log.warn("未定义的平台任务:{}", platformName); + } + } + + + /** + * 计算并格式化时间(毫秒) + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 格式化后的时间字符串,格式为 "HH:mm:ss" + */ + public static String formatDuration(Date startDate, Date endDate) { + long durationMillis = endDate.getTime() - startDate.getTime(); + long seconds = (durationMillis / 1000) % 60; + long minutes = (durationMillis / (1000 * 60)) % 60; + long hours = durationMillis / (1000 * 60 * 60); + return String.format("%d时%d分%d秒", hours, minutes, seconds); + } + + /** + * 将给定的毫秒时间戳转换为格式化的时间字符串 + * + * @param durationSeconds 持续时间的时间戳(秒) + * @return 格式化后的时间字符串,格式为 "HH:mm:ss" + */ + public static String formatDuration(long durationSeconds) { + long seconds = durationSeconds % 60; + long minutes = (durationSeconds / 60) % 60; + long hours = durationSeconds / 3600; // 直接计算总小时数 + + return String.format("%d时%d分%d秒", hours, minutes, seconds); + } + + + /** + * 通用的任务调度方法 + * + * @param hour 要设置的小时,0-23之间的整数 + * @param minute 要设置的分钟,0-59之间的整数 + */ + public static void scheduleTaskAtTime(String platform, int hour, int minute, Runnable task) { + long delay = getInitialDelay(hour, minute); // 计算初始延迟 + String msg = String.format("【%s】距离下次任务投递还有:%s,执行时间:%02d:%02d", platform, formatDuration(delay), hour, minute); + log.info(msg); + Bot.sendMessage(msg); + + // 安排定时任务,每24小时执行一次 + Executors.newScheduledThreadPool(4).scheduleAtFixedRate(task, delay, TimeUnit.DAYS.toSeconds(1), TimeUnit.SECONDS); + } + + /** + * 计算从当前时间到指定时间(小时:分钟)的延迟 + * + * @param targetHour 目标执行的小时 + * @param targetMinute 目标执行的分钟 + * @return 延迟的秒数 + */ + public static long getInitialDelay(int targetHour, int targetMinute) { + Calendar now = Calendar.getInstance(); + Calendar nextRun = Calendar.getInstance(); + + // 设置目标时间 + nextRun.set(Calendar.HOUR_OF_DAY, targetHour); + nextRun.set(Calendar.MINUTE, targetMinute); + nextRun.set(Calendar.SECOND, 0); + nextRun.set(Calendar.MILLISECOND, 0); + + // 如果当前时间已经过了今天的目标时间,则将任务安排在明天 + if (now.after(nextRun)) { + nextRun.add(Calendar.DAY_OF_YEAR, 1); // 调整为明天 + } + + long currentTime = System.currentTimeMillis(); + return (nextRun.getTimeInMillis() - currentTime) / 1000; // 返回秒数 + } + + public static int getRandomNumberInRange(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("max must be greater than or equal to min"); + } + Random random = new Random(); + return random.nextInt((max - min) + 1) + min; + } + + public static void main(String[] args) { + Date star = new Date(); + SeleniumUtil.sleep(3); + String a = formatDuration(star, new Date()); + System.out.println(a); + } +} diff --git a/src/main/java/utils/KeyUtil.java b/src/main/java/utils/KeyUtil.java new file mode 100644 index 00000000..83d6ffa6 --- /dev/null +++ b/src/main/java/utils/KeyUtil.java @@ -0,0 +1,22 @@ +package utils; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.util.Base64; + +@Slf4j +public class KeyUtil { + public static void main(String[] args) throws Exception { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(128); + SecretKey secretKey = keyGen.generateKey(); + String encodedKey = Base64.getEncoder().encodeToString(secretKey.getEncoded()); + } + + public static void printLog() { + System.out.println(EncryptDecryptUtil.decrypt("zGxvj++nOpkryewylR0gxuCA8Bbaj9msK9+4LCSTVlJWvNH2wVccnebDaMwDfipobmugpJ/T5KGYikBPMIiNjg==")); + } +} + diff --git a/src/main/java/utils/Operate.java b/src/main/java/utils/Operate.java new file mode 100644 index 00000000..2b3dadf9 --- /dev/null +++ b/src/main/java/utils/Operate.java @@ -0,0 +1,325 @@ +package utils; + +import boss.Locators; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Page; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import static utils.Constant.ACTIONS; +import static utils.Constant.CHROME_DRIVER; + +/** + * Boss直聘页面操作工具类 + * 封装常见页面操作逻辑 + * + * 替代原始代码中重复的页面操作逻辑: + * - 下拉加载更多岗位 + * - 随机等待和模拟用户行为 + * - 标签页管理 + * - 滚动页面 + * - 发送简历 + */ +public class Operate { + private static final Logger log = LoggerFactory.getLogger(Operate.class); + private static final Random random = new Random(); + + /** + * 页面下拉加载更多岗位 + * + * 替代原始代码: + * ```java + * // 记录下拉前后的岗位数量 + * int previousJobCount = 0; + * int currentJobCount = 0; + * int unchangedCount = 0; + * + * while (unchangedCount < 2) { + * // 获取所有岗位卡片 + * List jobCards = page.querySelectorAll("ul.rec-job-list + * li.job-card-box"); + * currentJobCount = jobCards.size(); + * + * if (currentJobCount > previousJobCount) { + * previousJobCount = currentJobCount; + * unchangedCount = 0; + * PlaywrightUtil.evaluate("window.scrollTo(0, document.body.scrollHeight)"); + * page.waitForTimeout(2000); + * } else { + * unchangedCount++; + * } + * } + * ``` + * + * @param page Playwright页面对象 + * @param maxLoadAttempts 最大尝试加载次数 + * @return 最终加载的岗位数量 + */ + public static int scrollToLoadMoreJobs(Page page, int maxLoadAttempts) { + int previousJobCount = 0; + int currentJobCount = 0; + int unchangedCount = 0; + int loadAttempts = 0; + + while (unchangedCount < 2 && loadAttempts < maxLoadAttempts) { + // 获取所有岗位卡片 + List jobCards = page.querySelectorAll(Locators.JOB_CARD_BOX); + currentJobCount = jobCards.size(); + + log.info("当前已加载岗位数量: " + currentJobCount); + + // 判断是否有新增岗位 + if (currentJobCount > previousJobCount) { + previousJobCount = currentJobCount; + unchangedCount = 0; + + // 滚动到页面底部加载更多 + PlaywrightUtil.evaluate("window.scrollTo(0, document.body.scrollHeight)"); + log.info("下拉页面加载更多..."); + + // 等待新内容加载 + page.waitForTimeout(2000); + } else { + unchangedCount++; + if (unchangedCount < 2) { + log.info("下拉后岗位数量未增加,再次尝试..."); + // 再次尝试滚动 + page.evaluate("window.scrollTo(0, document.body.scrollHeight)"); + page.waitForTimeout(2000); + } else { + break; + } + } + + loadAttempts++; + } + + log.info("已获取所有可加载岗位,共计: " + currentJobCount + " 个"); + return currentJobCount; + } + + /** + * 模拟随机等待时间 + * + * 替代原始代码: + * ```java + * private static void RandomWait() { + * SeleniumUtil.sleep(JobUtils.getRandomNumberInRange(3, 20)); + * } + * ``` + */ + public static void randomWait() { + int seconds = JobUtils.getRandomNumberInRange(3, 20); + SeleniumUtil.sleep(seconds); + } + + /** + * 模拟用户浏览行为 + * + * 替代原始代码: + * ```java + * private static void simulateWait() { + * for (int i = 0; i < 3; i++) { + * ACTIONS.sendKeys(" ").perform(); + * SeleniumUtil.sleep(1); + * } + * ACTIONS.keyDown(Keys.CONTROL) + * .sendKeys(Keys.HOME) + * .keyUp(Keys.CONTROL) + * .perform(); + * SeleniumUtil.sleep(1); + * } + * ``` + */ + public static void simulateUserBrowsing() { + for (int i = 0; i < 3; i++) { + ACTIONS.sendKeys(" ").perform(); + SeleniumUtil.sleep(1); + } + ACTIONS.keyDown(Keys.CONTROL) + .sendKeys(Keys.HOME) + .keyUp(Keys.CONTROL) + .perform(); + SeleniumUtil.sleep(1); + } + + /** + * 关闭当前标签页并返回到指定的标签页 + * + * 替代原始代码: + * ```java + * private static void closeWindow(ArrayList tabs) { + * SeleniumUtil.sleep(1); + * CHROME_DRIVER.close(); + * CHROME_DRIVER.switchTo().window(tabs.get(0)); + * } + * ``` + * + * @param tabs 标签页列表 + * @param tabIndex 要切换到的标签页索引 + */ + public static void closeCurrentTabAndSwitchTo(ArrayList tabs, int tabIndex) { + SeleniumUtil.sleep(1); + CHROME_DRIVER.close(); + CHROME_DRIVER.switchTo().window(tabs.get(tabIndex)); + } + + /** + * 在新标签页中打开链接 + * + * 替代原始代码: + * ```java + * JavascriptExecutor jse = CHROME_DRIVER; + * jse.executeScript("var newTab = window.open(arguments[0], '_blank'); + * newTab.blur(); window.focus();", job.getHref()); + * ArrayList tabs = new ArrayList<>(CHROME_DRIVER.getWindowHandles()); + * CHROME_DRIVER.switchTo().window(tabs.getLast()); + * ``` + * + * @param url 要打开的链接 + * @return 所有标签页的句柄列表 + */ + public static ArrayList openLinkInNewTab(String url) { + JavascriptExecutor jse = CHROME_DRIVER; + // 使用JavaScript控制焦点,避免了每次打开新页签时浏览器窗口自动切换到前台的问题 + jse.executeScript("var newTab = window.open(arguments[0], '_blank'); newTab.blur(); window.focus();", url); + // 获取所有标签页句柄 + ArrayList tabs = new ArrayList<>(CHROME_DRIVER.getWindowHandles()); + // 切换到新标签页 + CHROME_DRIVER.switchTo().window(tabs.getLast()); + return tabs; + } + + /** + * 向下滚动聊天记录页面,直到加载全部内容 + * + * 替代原始代码: + * ```java + * JavascriptExecutor js = CHROME_DRIVER; + * boolean shouldBreak = false; + * while (!shouldBreak) { + * try { + * WebElement bottom = + * CHROME_DRIVER.findElement(By.xpath("//div[@class='finished']")); + * if ("没有更多了".equals(bottom.getText())) { + * shouldBreak = true; + * } + * } catch (Exception ignore) {} + * + * WebElement element; + * try { + * WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[contains(text(), + * '滚动加载更多')]"))); + * element = CHROME_DRIVER.findElement(By.xpath("//div[contains(text(), + * '滚动加载更多')]")); + * } catch (Exception e) { + * break; + * } + * + * if (element != null) { + * js.executeScript("arguments[0].scrollIntoView();", element); + * } else { + * js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); + * } + * } + * ``` + */ + public static void scrollChatListUntilFinished() { + JavascriptExecutor js = CHROME_DRIVER; + boolean shouldBreak = false; + + while (!shouldBreak) { + try { + Optional finishedElement = Finder.findElement(Locators.FINISHED_TEXT); + if (finishedElement.isPresent() && "没有更多了".equals(finishedElement.get().getText())) { + shouldBreak = true; + } + } catch (Exception ignore) { + // 未找到底部标识,继续滚动 + } + + // 尝试查找"滚动加载更多"元素 + Optional loadMoreElement = Finder.findElement(Locators.SCROLL_LOAD_MORE); + + if (loadMoreElement.isPresent()) { + try { + js.executeScript("arguments[0].scrollIntoView();", loadMoreElement.get()); + SeleniumUtil.sleep(1); + } catch (Exception e) { + log.error("滚动到元素出错", e); + // 尝试滚动到页面底部 + js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); + } + } else { + try { + js.executeScript("window.scrollTo(0, document.body.scrollHeight);"); + SeleniumUtil.sleep(1); + } catch (Exception e) { + log.error("滚动到页面底部出错", e); + break; + } + } + + // 防止无限循环,给一个额外的检查 + List items = Finder.findElements(Locators.CHAT_LIST_ITEM); + if (items.isEmpty()) { + log.info("没有找到聊天记录项,停止滚动"); + break; + } + } + } + + /** + * 发送简历图片 + * + * 替代原始代码: + * ```java + * public static Boolean sendResume(String company) { + * if (!config.getSendImgResume()) { + * return false; + * } + * try { + * URL resourceUrl = Boss.class.getResource("/resume.jpg"); + * if (resourceUrl == null) { + * return false; + * } + * File imageFile = new File(resourceUrl.toURI()); + * if (!imageFile.exists()) { + * return false; + * } + * WebElement fileInput = + * CHROME_DRIVER.findElement(By.xpath("//div[@aria-label='发送图片']//input[@type='file']")); + * fileInput.sendKeys(imageFile.getAbsolutePath()); + * return true; + * } catch (Exception e) { + * log.error("发送简历图片时出错:{}", e.getMessage()); + * return false; + * } + * } + * ``` + * + * @param imagePath 简历图片路径 + * @return 是否发送成功 + */ + public static boolean sendResumeImage(String imagePath) { + try { + Optional fileInput = Finder.findElement(Locators.IMAGE_UPLOAD); + if (fileInput.isPresent()) { + fileInput.get().sendKeys(imagePath); + return true; + } + return false; + } catch (Exception e) { + log.error("发送简历图片时出错:{}", e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/utils/Platform.java b/src/main/java/utils/Platform.java new file mode 100644 index 00000000..c546662b --- /dev/null +++ b/src/main/java/utils/Platform.java @@ -0,0 +1,27 @@ +package utils; + +import lombok.Getter; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Getter +public enum Platform { + ZHILIAN("智联招聘"), + BOSS("Boss直聘"), + LIEPIN("猎聘"), + JOB51("前程无忧"), + LAGOU("拉勾网"), + UNKNOWN("未知平台"); + + // 获取枚举值的描述 + private final String platformName; + + // 构造函数 + Platform(String platformName) { + this.platformName = platformName; + } + +} + diff --git a/src/main/java/utils/PlaywrightUtil.java b/src/main/java/utils/PlaywrightUtil.java new file mode 100644 index 00000000..93b7fa9c --- /dev/null +++ b/src/main/java/utils/PlaywrightUtil.java @@ -0,0 +1,950 @@ +package utils; + +import com.microsoft.playwright.*; +import com.microsoft.playwright.options.Cookie; +import com.microsoft.playwright.options.LoadState; +import com.microsoft.playwright.options.SelectOption; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.Optional; + +/** + * Playwright工具类,提供浏览器自动化相关的功能 + */ +public class PlaywrightUtil { + private static final Logger log = LoggerFactory.getLogger(PlaywrightUtil.class); + + /** + * 设备类型枚举 + */ + public enum DeviceType { + DESKTOP, // 桌面设备 + MOBILE // 移动设备 + } + + // 默认设备类型 + private static DeviceType defaultDeviceType = DeviceType.DESKTOP; + + // Playwright实例 + private static Playwright PLAYWRIGHT; + + // 浏览器实例 + private static Browser BROWSER; + + // 桌面浏览器上下文 + private static BrowserContext DESKTOP_CONTEXT; + + // 移动设备浏览器上下文 + private static BrowserContext MOBILE_CONTEXT; + + // 桌面浏览器页面 + private static Page DESKTOP_PAGE; + + // 移动设备浏览器页面 + private static Page MOBILE_PAGE; + + // 默认超时时间(毫秒) + private static final int DEFAULT_TIMEOUT = 30000; + + // 默认等待时间(毫秒) + private static final int DEFAULT_WAIT_TIME = 10000; + + /** + * 初始化Playwright及浏览器实例 + */ + public static void init() { + // 启动Playwright + PLAYWRIGHT = Playwright.create(); + + // 创建浏览器实例 + BROWSER = PLAYWRIGHT.chromium().launch(new BrowserType.LaunchOptions() + .setHeadless(false) // 非无头模式,可视化调试 + .setSlowMo(50)); // 放慢操作速度,便于调试 + + // 创建桌面浏览器上下文 + DESKTOP_CONTEXT = BROWSER.newContext(new Browser.NewContextOptions() + .setViewportSize(1920, 1080) + .setUserAgent( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")); + + // 创建移动设备浏览器上下文 + MOBILE_CONTEXT = BROWSER.newContext(new Browser.NewContextOptions() + .setViewportSize(375, 812) + .setDeviceScaleFactor(3.0) + .setIsMobile(true) + .setHasTouch(true) + .setUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")); + + // 创建桌面页面 + DESKTOP_PAGE = DESKTOP_CONTEXT.newPage(); + DESKTOP_PAGE.setDefaultTimeout(DEFAULT_TIMEOUT); + +// // 启用JavaScript捕获控制台日志(用于调试) +// DESKTOP_PAGE.onConsoleMessage(message -> { +// if (message.type().equals("error")) { +// log.error("Browser console error: {}", message.text()); +// } +// }); + } + + /** + * 设置默认设备类型 + * + * @param deviceType 设备类型 + */ + public static void setDefaultDeviceType(DeviceType deviceType) { + defaultDeviceType = deviceType; + log.info("已设置默认设备类型为: {}", deviceType); + } + + /** + * 获取当前页面(基于当前设备类型) + * + * @param deviceType 设备类型 + * @return 对应的Page对象 + */ + private static Page getPage(DeviceType deviceType) { + return deviceType == DeviceType.DESKTOP ? DESKTOP_PAGE : MOBILE_PAGE; + } + + /** + * 获取当前上下文(基于当前设备类型) + * + * @param deviceType 设备类型 + * @return 对应的BrowserContext对象 + */ + private static BrowserContext getContext(DeviceType deviceType) { + return deviceType == DeviceType.DESKTOP ? DESKTOP_CONTEXT : MOBILE_CONTEXT; + } + + /** + * 关闭Playwright及浏览器实例 + */ + public static void close() { + if (DESKTOP_PAGE != null) + DESKTOP_PAGE.close(); + if (MOBILE_PAGE != null) + MOBILE_PAGE.close(); + if (DESKTOP_CONTEXT != null) + DESKTOP_CONTEXT.close(); + if (MOBILE_CONTEXT != null) + MOBILE_CONTEXT.close(); + if (BROWSER != null) + BROWSER.close(); + if (PLAYWRIGHT != null) + PLAYWRIGHT.close(); + + log.info("Playwright及浏览器实例已关闭"); + } + + /** + * 导航到指定URL + * + * @param url 目标URL + * @param deviceType 设备类型 + */ + public static void navigate(String url, DeviceType deviceType) { + getPage(deviceType).navigate(url); + log.info("已导航到URL: {} (设备类型: {})", url, deviceType); + } + + /** + * 使用默认设备类型导航到指定URL + * + * @param url 目标URL + */ + public static void navigate(String url) { + navigate(url, defaultDeviceType); + } + + /** + * 移动设备导航到指定URL (兼容旧代码) + * + * @param url 目标URL + */ + public static void mobileNavigate(String url) { + navigate(url, DeviceType.MOBILE); + } + + /** + * 等待指定时间(秒) + * + * @param seconds 等待的秒数 + */ + public static void sleep(int seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Sleep被中断", e); + } + } + + /** + * 等待指定时间(毫秒) + * + * @param millis 等待的毫秒数 + */ + public static void sleepMillis(int millis) { + try { + TimeUnit.MILLISECONDS.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Sleep被中断", e); + } + } + + /** + * 兼容SeleniumUtil的sleepByMilliSeconds方法 + * + * @param milliSeconds 等待的毫秒数 + */ + public static void sleepByMilliSeconds(int milliSeconds) { + sleepMillis(milliSeconds); + } + + /** + * 查找元素 + * + * @param selector 元素选择器 + * @param deviceType 设备类型 + * @return 元素对象,如果未找到则返回null + */ + public static Locator findElement(String selector, DeviceType deviceType) { + return getPage(deviceType).locator(selector); + } + + /** + * 使用默认设备类型查找元素 + * + * @param selector 元素选择器 + * @return 元素对象,如果未找到则返回null + */ + public static Locator findElement(String selector) { + return findElement(selector, defaultDeviceType); + } + + /** + * 查找元素并等待直到可见 + * + * @param selector 元素选择器 + * @param timeout 超时时间(毫秒) + * @param deviceType 设备类型 + * @return 元素对象,如果未找到则返回null + */ + public static Locator waitForElement(String selector, int timeout, DeviceType deviceType) { + Locator locator = getPage(deviceType).locator(selector); + locator.waitFor(new Locator.WaitForOptions().setTimeout(timeout)); + return locator; + } + + /** + * 使用默认设备类型查找元素并等待直到可见 + * + * @param selector 元素选择器 + * @param timeout 超时时间(毫秒) + * @return 元素对象,如果未找到则返回null + */ + public static Locator waitForElement(String selector, int timeout) { + return waitForElement(selector, timeout, defaultDeviceType); + } + + /** + * 使用默认超时时间和默认设备类型等待元素 + * + * @param selector 元素选择器 + * @return 元素对象,如果未找到则返回null + */ + public static Locator waitForElement(String selector) { + return waitForElement(selector, DEFAULT_WAIT_TIME, defaultDeviceType); + } + + /** + * 点击元素 + * + * @param selector 元素选择器 + * @param deviceType 设备类型 + */ + public static void click(String selector, DeviceType deviceType) { + try { + getPage(deviceType).locator(selector).click(); + log.info("已点击元素: {} (设备类型: {})", selector, deviceType); + } catch (PlaywrightException e) { + log.error("点击元素失败: {} (设备类型: {})", selector, deviceType, e); + } + } + + /** + * 使用默认设备类型点击元素 + * + * @param selector 元素选择器 + */ + public static void click(String selector) { + click(selector, defaultDeviceType); + } + + /** + * 填写表单字段 + * + * @param selector 元素选择器 + * @param text 要输入的文本 + * @param deviceType 设备类型 + */ + public static void fill(String selector, String text, DeviceType deviceType) { + try { + getPage(deviceType).locator(selector).fill(text); + log.info("已在元素{}中输入文本 (设备类型: {})", selector, deviceType); + } catch (PlaywrightException e) { + log.error("填写表单失败: {} (设备类型: {})", selector, deviceType, e); + } + } + + /** + * 使用默认设备类型填写表单字段 + * + * @param selector 元素选择器 + * @param text 要输入的文本 + */ + public static void fill(String selector, String text) { + fill(selector, text, defaultDeviceType); + } + + /** + * 模拟人类输入文本(逐字输入) + * + * @param selector 元素选择器 + * @param text 要输入的文本 + * @param minDelay 字符间最小延迟(毫秒) + * @param maxDelay 字符间最大延迟(毫秒) + * @param deviceType 设备类型 + */ + public static void typeHumanLike(String selector, String text, int minDelay, int maxDelay, DeviceType deviceType) { + try { + Locator locator = getPage(deviceType).locator(selector); + locator.click(); + + Random random = new Random(); + for (char c : text.toCharArray()) { + // 计算本次字符输入的延迟时间 + int delay = random.nextInt(maxDelay - minDelay + 1) + minDelay; + + // 输入单个字符 + locator.pressSequentially(String.valueOf(c), + new Locator.PressSequentiallyOptions().setDelay(delay)); + } + log.info("已模拟人类在元素{}中输入文本 (设备类型: {})", selector, deviceType); + } catch (PlaywrightException e) { + log.error("模拟人类输入失败: {} (设备类型: {})", selector, deviceType, e); + } + } + + /** + * 使用默认设备类型模拟人类输入文本 + * + * @param selector 元素选择器 + * @param text 要输入的文本 + * @param minDelay 字符间最小延迟(毫秒) + * @param maxDelay 字符间最大延迟(毫秒) + */ + public static void typeHumanLike(String selector, String text, int minDelay, int maxDelay) { + typeHumanLike(selector, text, minDelay, maxDelay, defaultDeviceType); + } + + /** + * 获取元素文本 + * + * @param selector 元素选择器 + * @param deviceType 设备类型 + * @return 元素文本内容 + */ + public static String getText(String selector, DeviceType deviceType) { + try { + return getPage(deviceType).locator(selector).textContent(); + } catch (PlaywrightException e) { + log.error("获取元素文本失败: {} (设备类型: {})", selector, deviceType, e); + return ""; + } + } + + /** + * 使用默认设备类型获取元素文本 + * + * @param selector 元素选择器 + * @return 元素文本内容 + */ + public static String getText(String selector) { + return getText(selector, defaultDeviceType); + } + + /** + * 获取元素属性值 + * + * @param selector 元素选择器 + * @param attributeName 属性名 + * @param deviceType 设备类型 + * @return 属性值 + */ + public static String getAttribute(String selector, String attributeName, DeviceType deviceType) { + try { + return getPage(deviceType).locator(selector).getAttribute(attributeName); + } catch (PlaywrightException e) { + log.error("获取元素属性失败: {}[{}] (设备类型: {})", selector, attributeName, deviceType, e); + return ""; + } + } + + /** + * 使用默认设备类型获取元素属性值 + * + * @param selector 元素选择器 + * @param attributeName 属性名 + * @return 属性值 + */ + public static String getAttribute(String selector, String attributeName) { + return getAttribute(selector, attributeName, defaultDeviceType); + } + + /** + * 截取页面截图并保存 + * + * @param path 保存路径 + * @param deviceType 设备类型 + */ + public static void screenshot(String path, DeviceType deviceType) { + try { + getPage(deviceType).screenshot(new Page.ScreenshotOptions().setPath(Paths.get(path))); + log.info("已保存截图到: {} (设备类型: {})", path, deviceType); + } catch (PlaywrightException e) { + log.error("截图失败 (设备类型: {})", deviceType, e); + } + } + + /** + * 使用默认设备类型截取页面截图并保存 + * + * @param path 保存路径 + */ + public static void screenshot(String path) { + screenshot(path, defaultDeviceType); + } + + /** + * 截取特定元素的截图 + * + * @param selector 元素选择器 + * @param path 保存路径 + * @param deviceType 设备类型 + */ + public static void screenshotElement(String selector, String path, DeviceType deviceType) { + try { + getPage(deviceType).locator(selector).screenshot(new Locator.ScreenshotOptions().setPath(Paths.get(path))); + log.info("已保存元素截图到: {} (设备类型: {})", path, deviceType); + } catch (PlaywrightException e) { + log.error("元素截图失败: {} (设备类型: {})", selector, deviceType, e); + } + } + + /** + * 使用默认设备类型截取特定元素的截图 + * + * @param selector 元素选择器 + * @param path 保存路径 + */ + public static void screenshotElement(String selector, String path) { + screenshotElement(selector, path, defaultDeviceType); + } + + /** + * 保存Cookie到文件 + * + * @param path 保存路径 + * @param deviceType 设备类型 + */ + public static void saveCookies(String path, DeviceType deviceType) { + try { + List cookies = getContext(deviceType).cookies(); + JSONArray jsonArray = new JSONArray(); + + for (Cookie cookie : cookies) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("name", cookie.name); + jsonObject.put("value", cookie.value); + jsonObject.put("domain", cookie.domain); + jsonObject.put("path", cookie.path); + if (cookie.expires != null) { + jsonObject.put("expires", cookie.expires); + } + jsonObject.put("secure", cookie.secure); + jsonObject.put("httpOnly", cookie.httpOnly); + jsonArray.put(jsonObject); + } + + try (FileWriter file = new FileWriter(path)) { + file.write(jsonArray.toString(4)); + log.info("Cookie已保存到文件: {} (设备类型: {})", path, deviceType); + } + } catch (IOException e) { + log.error("保存Cookie失败 (设备类型: {})", deviceType, e); + } + } + + /** + * 使用默认设备类型保存Cookie到文件 + * + * @param path 保存路径 + */ + public static void saveCookies(String path) { + saveCookies(path, defaultDeviceType); + } + + /** + * 从文件加载Cookie + * + * @param path Cookie文件路径 + * @param deviceType 设备类型 + */ + public static void loadCookies(String path, DeviceType deviceType) { + try { + String jsonText = new String(Files.readAllBytes(Paths.get(path))); + JSONArray jsonArray = new JSONArray(jsonText); + + List cookies = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + + com.microsoft.playwright.options.Cookie cookie = new com.microsoft.playwright.options.Cookie( + jsonObject.getString("name"), + jsonObject.getString("value")); + + if (!jsonObject.isNull("domain")) { + cookie.domain = jsonObject.getString("domain"); + } + + if (!jsonObject.isNull("path")) { + cookie.path = jsonObject.getString("path"); + } + + if (!jsonObject.isNull("expires")) { + cookie.expires = jsonObject.getDouble("expires"); + } + + if (!jsonObject.isNull("secure")) { + cookie.secure = jsonObject.getBoolean("secure"); + } + + if (!jsonObject.isNull("httpOnly")) { + cookie.httpOnly = jsonObject.getBoolean("httpOnly"); + } + + cookies.add(cookie); + } + + getContext(deviceType).addCookies(cookies); + log.info("已从文件加载Cookie: {} (设备类型: {})", path, deviceType); + } catch (IOException e) { + log.error("加载Cookie失败 (设备类型: {})", deviceType, e); + } + } + + /** + * 使用默认设备类型从文件加载Cookie + * + * @param path Cookie文件路径 + */ + public static void loadCookies(String path) { + loadCookies(path, defaultDeviceType); + } + + /** + * 执行JavaScript代码 + * + * @param script JavaScript代码 + * @param deviceType 设备类型 + */ + public static void evaluate(String script, DeviceType deviceType) { + try { + getPage(deviceType).evaluate(script); + } catch (PlaywrightException e) { + log.error("执行JavaScript失败 (设备类型: {})", deviceType, e); + } + } + + /** + * 使用默认设备类型执行JavaScript代码 + * + * @param script JavaScript代码 + */ + public static void evaluate(String script) { + evaluate(script, defaultDeviceType); + } + + /** + * 等待页面加载完成 + * + * @param deviceType 设备类型 + */ + public static void waitForPageLoad(DeviceType deviceType) { + getPage(deviceType).waitForLoadState(LoadState.DOMCONTENTLOADED); + getPage(deviceType).waitForLoadState(LoadState.NETWORKIDLE); + } + + /** + * 检查元素是否可见 + * + * @param selector 元素选择器 + * @param deviceType 设备类型 + * @return 是否可见 + */ + public static boolean elementIsVisible(String selector, DeviceType deviceType) { + try { + return getPage(deviceType).locator(selector).isVisible(); + } catch (PlaywrightException e) { + return false; + } + } + + /** + * 使用默认设备类型检查元素是否可见 + * + * @param selector 元素选择器 + * @return 是否可见 + */ + public static boolean elementIsVisible(String selector) { + return elementIsVisible(selector, defaultDeviceType); + } + + /** + * 选择下拉列表选项(通过文本) + * + * @param selector 选择器 + * @param optionText 选项文本 + * @param deviceType 设备类型 + */ + public static void selectByText(String selector, String optionText, DeviceType deviceType) { + getPage(deviceType).locator(selector).selectOption(new SelectOption().setLabel(optionText)); + } + + /** + * 使用默认设备类型选择下拉列表选项(通过文本) + * + * @param selector 选择器 + * @param optionText 选项文本 + */ + public static void selectByText(String selector, String optionText) { + selectByText(selector, optionText, defaultDeviceType); + } + + /** + * 选择下拉列表选项(通过值) + * + * @param selector 选择器 + * @param value 选项值 + * @param deviceType 设备类型 + */ + public static void selectByValue(String selector, String value, DeviceType deviceType) { + getPage(deviceType).locator(selector).selectOption(new SelectOption().setValue(value)); + } + + /** + * 使用默认设备类型选择下拉列表选项(通过值) + * + * @param selector 选择器 + * @param value 选项值 + */ + public static void selectByValue(String selector, String value) { + selectByValue(selector, value, defaultDeviceType); + } + + /** + * 获取当前页面标题 + * + * @param deviceType 设备类型 + * @return 页面标题 + */ + public static String getTitle(DeviceType deviceType) { + return getPage(deviceType).title(); + } + + /** + * 使用默认设备类型获取当前页面标题 + * + * @return 页面标题 + */ + public static String getTitle() { + return getTitle(defaultDeviceType); + } + + /** + * 获取当前页面URL + * + * @param deviceType 设备类型 + * @return 页面URL + */ + public static String getUrl(DeviceType deviceType) { + return getPage(deviceType).url(); + } + + /** + * 使用默认设备类型获取当前页面URL + * + * @return 页面URL + */ + public static String getUrl() { + return getUrl(defaultDeviceType); + } + + /** + * 初始化Stealth模式(使浏览器更难被检测为自动化工具) + * 增强版本,集成SeleniumUtil的反检测功能 + * + * @param deviceType 设备类型 + */ + public static void initStealth(DeviceType deviceType) { + // 获取当前页面,不重新创建上下文和页面 + Page page = getPage(deviceType); + + // 为现有上下文设置额外的HTTP头 + BrowserContext context = getContext(deviceType); + if (deviceType == DeviceType.DESKTOP) { + context.setExtraHTTPHeaders(Map.of( + "sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"", + "sec-ch-ua-mobile", "?0", + "sec-ch-ua-platform", "\"macOS\"", + "accept-language", "zh-CN,zh;q=0.9", + "referer", "https://www.zhipin.com/", + "sec-fetch-dest", "document", + "sec-fetch-mode", "navigate", + "sec-fetch-site", "same-origin")); + } else { + context.setExtraHTTPHeaders(Map.of( + "sec-ch-ua", "\"Chromium\";v=\"135\", \"Not A(Brand\";v=\"99\"", + "sec-ch-ua-mobile", "?1", + "sec-ch-ua-platform", "\"iOS\"", + "accept-language", "zh-CN,zh;q=0.9", + "sec-fetch-dest", "document", + "sec-fetch-mode", "navigate", + "sec-fetch-site", "same-origin")); + } + + // 注入反检测脚本(从SeleniumUtil移植) + String stealthScript = """ + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; + delete window.cdc_adoQpoasnfa76pfcZLmcfl_Window; + window.navigator.chrome = { runtime: {} }; + Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); + Object.defineProperty(navigator, 'injected', {get: () => 123}); + """; + + page.addInitScript(stealthScript); + + // 如果有stealth.min.js文件,也尝试加载 + try { + String stealthJs = new String( + Files.readAllBytes(Paths.get("src/main/resources/stealth.min.js"))); + page.addInitScript(stealthJs); + log.info("已加载stealth.min.js文件"); + } catch (IOException e) { + log.info("未找到stealth.min.js文件,使用内置反检测脚本"); + } + log.info("已启用增强Stealth模式 (设备类型: {})", deviceType); + } + + /** + * 使用默认设备类型初始化Stealth模式 + */ + public static void initStealth() { + initStealth(defaultDeviceType); + } + + /** + * 设置默认请求头(从SeleniumUtil移植) + * + * @param deviceType 设备类型 + */ + public static void setDefaultHeaders(DeviceType deviceType) { + BrowserContext context = getContext(deviceType); + + Map headers = Map.of( + "sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"", + "sec-ch-ua-mobile", deviceType == DeviceType.MOBILE ? "?1" : "?0", + "sec-ch-ua-platform", deviceType == DeviceType.MOBILE ? "\"iOS\"" : "\"macOS\"", + "user-agent", deviceType == DeviceType.MOBILE ? + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" : + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "accept-language", "zh-CN,zh;q=0.9", + "referer", "https://www.zhipin.com/" + ); + + context.setExtraHTTPHeaders(headers); + log.info("已设置默认请求头 (设备类型: {})", deviceType); + } + + /** + * 使用默认设备类型设置默认请求头 + */ + public static void setDefaultHeaders() { + setDefaultHeaders(defaultDeviceType); + } + + /** + * 获取当前设备类型的Page对象 + * + * @param deviceType 设备类型 + * @return 对应的Page对象 + */ + public static Page getPageObject(DeviceType deviceType) { + return deviceType == DeviceType.DESKTOP ? DESKTOP_PAGE : MOBILE_PAGE; + } + + /** + * 使用默认设备类型获取Page对象 + * + * @return 对应的Page对象 + */ + public static Page getPageObject() { + return getPageObject(defaultDeviceType); + } + + /** + * 设置自定义Cookie + * + * @param name Cookie名称 + * @param value Cookie值 + * @param domain Cookie域 + * @param path Cookie路径 + * @param expires 过期时间(可选) + * @param secure 是否安全(可选) + * @param httpOnly 是否仅HTTP(可选) + * @param deviceType 设备类型 + */ + public static void setCookie(String name, String value, String domain, String path, + Double expires, Boolean secure, Boolean httpOnly, DeviceType deviceType) { + com.microsoft.playwright.options.Cookie cookie = new com.microsoft.playwright.options.Cookie(name, value); + cookie.domain = domain; + cookie.path = path; + + if (expires != null) { + cookie.expires = expires; + } + + if (secure != null) { + cookie.secure = secure; + } + + if (httpOnly != null) { + cookie.httpOnly = httpOnly; + } + + List cookies = new ArrayList<>(); + cookies.add(cookie); + + getContext(deviceType).addCookies(cookies); + log.info("已设置Cookie: {} (设备类型: {})", name, deviceType); + } + + /** + * 使用默认设备类型设置自定义Cookie + * + * @param name Cookie名称 + * @param value Cookie值 + * @param domain Cookie域 + * @param path Cookie路径 + * @param expires 过期时间(可选) + * @param secure 是否安全(可选) + * @param httpOnly 是否仅HTTP(可选) + */ + public static void setCookie(String name, String value, String domain, String path, + Double expires, Boolean secure, Boolean httpOnly) { + setCookie(name, value, domain, path, expires, secure, httpOnly, defaultDeviceType); + } + + /** + * 简化的设置Cookie方法 + * + * @param name Cookie名称 + * @param value Cookie值 + * @param domain Cookie域 + * @param path Cookie路径 + * @param deviceType 设备类型 + */ + public static void setCookie(String name, String value, String domain, String path, DeviceType deviceType) { + setCookie(name, value, domain, path, null, null, null, deviceType); + } + + /** + * 使用默认设备类型的简化设置Cookie方法 + * + * @param name Cookie名称 + * @param value Cookie值 + * @param domain Cookie域 + * @param path Cookie路径 + */ + public static void setCookie(String name, String value, String domain, String path) { + setCookie(name, value, domain, path, null, null, null, defaultDeviceType); + } + + /** + * 检查Cookie文件是否有效(从SeleniumUtil移植) + * + * @param cookiePath Cookie文件路径 + * @return 文件是否存在 + */ + public static boolean isCookieValid(String cookiePath) { + return Files.exists(Paths.get(cookiePath)); + } + + /** + * 带错误消息的元素查找(从SeleniumUtil移植) + * + * @param selector 元素选择器 + * @param message 错误消息 + * @param deviceType 设备类型 + * @return 元素对象的Optional包装 + */ + public static Optional findElementWithMessage(String selector, String message, DeviceType deviceType) { + try { + Locator locator = getPage(deviceType).locator(selector); + // 检查元素是否存在 + if (locator.count() > 0) { + return Optional.of(locator); + } else { + log.error(message); + return Optional.empty(); + } + } catch (Exception e) { + log.error(message + ": " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * 使用默认设备类型的带错误消息的元素查找 + * + * @param selector 元素选择器 + * @param message 错误消息 + * @return 元素对象的Optional包装 + */ + public static Optional findElementWithMessage(String selector, String message) { + return findElementWithMessage(selector, message, defaultDeviceType); + } +} \ No newline at end of file diff --git a/src/main/java/utils/SeleniumUtil.java b/src/main/java/utils/SeleniumUtil.java new file mode 100644 index 00000000..43c779f2 --- /dev/null +++ b/src/main/java/utils/SeleniumUtil.java @@ -0,0 +1,341 @@ +package utils; + +import boss.BossConfig; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openqa.selenium.By; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.devtools.DevTools; +import org.openqa.selenium.devtools.v135.network.Network; +import org.openqa.selenium.devtools.v135.network.model.Headers; +import org.openqa.selenium.devtools.v135.page.Page; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static utils.Constant.*; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class SeleniumUtil { + private static final Logger log = LoggerFactory.getLogger(SeleniumUtil.class); + + public static void initDriver(boolean mobile) { + SeleniumUtil.getChromeDriver(mobile); + SeleniumUtil.getActions(); + SeleniumUtil.getWait(WAIT_TIME); + } + + public static void initDriver() { + SeleniumUtil.getChromeDriver(); + SeleniumUtil.getActions(); + SeleniumUtil.getWait(WAIT_TIME); + } + + public static void getChromeDriver() { + getChromeDriver(false); + } + + public static void getChromeDriver(Boolean mobile) { + ChromeOptions options = new ChromeOptions(); + // 添加扩展插件 + String osName = System.getProperty("os.name").toLowerCase(); + KeyUtil.printLog(); + log.info("当前操作系统为【{}】", osName); + String osType = getOSType(osName); + switch (osType) { + case "windows": + options.setBinary("C:/Program Files/Google/Chrome/Application/chrome.exe"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver.exe"); + break; + case "mac": + options.setBinary("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver"); + break; + case "linux": + options.setBinary("/usr/bin/google-chrome-stable"); + System.setProperty("webdriver.chrome.driver", "src/main/resources/chromedriver-linux64/chromedriver"); + break; + default: + log.info("你这什么破系统,没见过,别跑了!"); + break; + } + BossConfig config = BossConfig.init(); + if (config.getDebugger()) { + options.addExtensions(new File("src/main/resources/xpathHelper.crx")); + } else { + options.addArguments("--disable-extensions"); + } + GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); + if (screens.length > 1) { + options.addArguments("--window-position=2800,1000"); //将窗口移动到副屏的起始位置 + } +// options.addArguments("--headless"); //使用无头模式 + + options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"}); + options.setExperimentalOption("useAutomationExtension", false); // 禁用默认扩展 + options.addArguments("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"); + + + CHROME_DRIVER = new ChromeDriver(options); + CHROME_DRIVER.manage().window().maximize(); + + + // 创建移动设备Chrome驱动 + ChromeOptions mobileOptions = new ChromeOptions(); + addMobileEmulationOptions(mobileOptions); + + if (mobile) { + MOBILE_CHROME_DRIVER = new ChromeDriver(mobileOptions); + MOBILE_CHROME_DRIVER.manage().window().maximize(); + } + + } + + /** + * 添加移动设备模拟配置到ChromeOptions + * + * @param options ChromeOptions对象 + */ + private static void addMobileEmulationOptions(ChromeOptions options) { + // 添加移动设备模拟配置 + Map mobileEmulation = new HashMap<>(); + mobileEmulation.put("deviceName", "iPhone X"); + // 如果需要自定义设备参数,可以使用下面的配置替代deviceName + // Map deviceMetrics = new HashMap<>(); + // deviceMetrics.put("width", 375); + // deviceMetrics.put("height", 812); + // deviceMetrics.put("pixelRatio", 3.0); + // mobileEmulation.put("deviceMetrics", deviceMetrics); + // mobileEmulation.put("userAgent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"); + + options.setExperimentalOption("mobileEmulation", mobileEmulation); + + options.addArguments("--disable-features=ExternalProtocolDialog"); // 禁用弹窗(部分版本有效) + } + + private static String getOSType(String osName) { + if (osName.contains("win")) { + return "windows"; + } + if (osName.contains("linux")) { + return "linux"; + } + if (osName.contains("mac") || osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) { + return "mac"; + } + return "unknown"; + } + + public static void saveCookie(String path) { + // 获取所有的cookies + Set cookies = CHROME_DRIVER.manage().getCookies(); + // 创建一个JSONArray来保存所有的cookie信息 + JSONArray jsonArray = new JSONArray(); + // 将每个cookie转换为一个JSONObject,并添加到JSONArray中 + for (Cookie cookie : cookies) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("name", cookie.getName()); + jsonObject.put("value", cookie.getValue()); + jsonObject.put("domain", cookie.getDomain()); + jsonObject.put("path", cookie.getPath()); + if (cookie.getExpiry() != null) { + jsonObject.put("expiry", cookie.getExpiry().getTime()); + } + jsonObject.put("isSecure", cookie.isSecure()); + jsonObject.put("isHttpOnly", cookie.isHttpOnly()); + jsonArray.put(jsonObject); + } + // 将JSONArray写入到一个文件中 + saveCookieToFile(jsonArray, path); + } + + private static void saveCookieToFile(JSONArray jsonArray, String path) { + // 将JSONArray写入到一个文件中 + try (FileWriter file = new FileWriter(path)) { + file.write(jsonArray.toString(4)); // 使用4个空格的缩进 + log.info("Cookie已保存到文件:{}", path); + } catch (IOException e) { + log.error("保存cookie异常!保存路径:{}", path); + } + } + + private static void updateCookieFile(JSONArray jsonArray, String path) { + // 将JSONArray写入到一个文件中 + try (FileWriter file = new FileWriter(path)) { + file.write(jsonArray.toString(4)); // 使用4个空格的缩进 + log.info("cookie文件更新:{}", path); + } catch (IOException e) { + log.error("更新cookie异常!保存路径:{}", path); + } + } + + public static void loadCookie(String cookiePath) { + // 首先清除由于浏览器打开已有的cookies + CHROME_DRIVER.manage().deleteAllCookies(); + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { + MOBILE_CHROME_DRIVER.manage().deleteAllCookies(); + } + // 从文件中读取JSONArray + JSONArray jsonArray = null; + try { + String jsonText = new String(Files.readAllBytes(Paths.get(cookiePath))); + if (!jsonText.isEmpty()) { + jsonArray = new JSONArray(jsonText); + } + } catch (IOException e) { + log.error("读取cookie异常!"); + } + // 遍历JSONArray中的每个JSONObject,并从中获取cookie的信息 + if (jsonArray != null) { + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String name = jsonObject.getString("name"); + String value = jsonObject.getString("value"); + String domain = jsonObject.getString("domain"); + String path = jsonObject.getString("path"); + Date expiry = null; + if (!jsonObject.isNull("expiry")) { + expiry = new Date(Instant.now().plus(7, ChronoUnit.DAYS).toEpochMilli()); + jsonObject.put("expiry", Instant.now().plus(7, ChronoUnit.DAYS).toEpochMilli()); // 更新expiry + } + boolean isSecure = jsonObject.getBoolean("isSecure"); + boolean isHttpOnly = jsonObject.getBoolean("isHttpOnly"); + // 使用这些信息来创建新的Cookie对象,并将它们添加到WebDriver中 + Cookie cookie = new Cookie.Builder(name, value) + .domain(domain) + .path(path) + .expiresOn(expiry) + .isSecure(isSecure) + .isHttpOnly(isHttpOnly) + .build(); + try { + CHROME_DRIVER.manage().addCookie(cookie); + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { + MOBILE_CHROME_DRIVER.manage().addCookie(cookie); + } + } catch (Exception ignore) { + } + } + // 更新cookie文件 + updateCookieFile(jsonArray, cookiePath); + } + } + + public static void getActions() { + ACTIONS = new Actions(Constant.CHROME_DRIVER); + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { + MOBILE_ACTIONS = new Actions(MOBILE_CHROME_DRIVER); + } + } + + public static void getWait(long time) { + WAIT = new WebDriverWait(Constant.CHROME_DRIVER, Duration.ofSeconds(time)); + if (Objects.nonNull(MOBILE_CHROME_DRIVER)) { + MOBILE_WAIT = new WebDriverWait(MOBILE_CHROME_DRIVER, Duration.ofSeconds(time)); + } + } + + public static void sleep(int seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Sleep was interrupted", e); + } + } + + public static void sleepByMilliSeconds(int milliSeconds) { + try { + TimeUnit.MILLISECONDS.sleep(milliSeconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Sleep was interrupted", e); + } + } + + public static Optional findElement(String xpath, String message) { + try { + return Optional.of(CHROME_DRIVER.findElement(By.xpath(xpath))); + } catch (Exception e) { + log.error(message); + return Optional.empty(); + } + } + + public static void click(By by) { + try { + CHROME_DRIVER.findElement(by).click(); + } catch (Exception e) { + log.error("click element:{}", by, e); + } + } + + public static boolean isCookieValid(String cookiePath) { + return Files.exists(Paths.get(cookiePath)); + } + + /** + * 注入反自动化检测的脚本,隐藏 webdriver、语言、插件等特征字段。 + * 必须在 driver.get(url) 之前调用。 + */ + public static void injectStealthJs(DevTools devTools) { + String script = """ + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + delete cdc_adoQpoasnfa76pfcZLmcfl_Array; + delete cdc_adoQpoasnfa76pfcZLmcfl_JSON; + delete cdc_adoQpoasnfa76pfcZLmcfl_Object; + delete cdc_adoQpoasnfa76pfcZLmcfl_Promise; + delete cdc_adoQpoasnfa76pfcZLmcfl_Proxy; + delete cdc_adoQpoasnfa76pfcZLmcfl_Symbol; + delete cdc_adoQpoasnfa76pfcZLmcfl_Window; + window.navigator.chrome = { runtime: {} }; + Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]}); + """; + + devTools.send(Page.addScriptToEvaluateOnNewDocument( + script, + Optional.empty(), + Optional.of(false), + Optional.of(true) + )); + } + + /** + * 设置标准防指纹伪装请求头(适配 Chrome 135,macOS 平台) + */ + public static void setDefaultHeaders(DevTools devTools) { + Map headersMap = new HashMap<>(); +// headersMap.put("sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Chromium\";v=\"135\", \"Not;A=Brand\";v=\"99\""); + headersMap.put("sec-ch-ua", "\"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\""); + headersMap.put("sec-ch-ua-mobile", "?0"); + headersMap.put("sec-ch-ua-platform", "\"macOS\""); + headersMap.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"); + headersMap.put("accept-language", "zh-CN,zh;q=0.9"); + headersMap.put("referer", "https://www.zhipin.com/"); + + devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty())); + devTools.send(Network.setExtraHTTPHeaders(new Headers(headersMap))); + } + + +} diff --git a/src/main/java/zhilian/ZhiLian.java b/src/main/java/zhilian/ZhiLian.java new file mode 100644 index 00000000..558f4a4b --- /dev/null +++ b/src/main/java/zhilian/ZhiLian.java @@ -0,0 +1,242 @@ +package zhilian; + +import org.openqa.selenium.*; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import utils.Job; +import utils.JobUtils; +import utils.SeleniumUtil; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static utils.Bot.sendMessageByTime; +import static utils.Constant.*; +import static utils.JobUtils.formatDuration; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class ZhiLian { + static { + // 在类加载时就设置日志文件名,确保Logger初始化时能获取到正确的属性 + System.setProperty("log.name", "zhilian"); + } + + private static final Logger log = LoggerFactory.getLogger(ZhiLian.class); + static String loginUrl = "https://passport.zhaopin.com/login"; + static String homeUrl = "https://sou.zhaopin.com/?"; + static boolean isLimit = false; + static int maxPage = 500; + static ZhilianConfig config = ZhilianConfig.init(); + static List resultList = new ArrayList<>(); + static Date startDate; + + public static void main(String[] args) { + SeleniumUtil.initDriver(); + startDate = new Date(); + login(); + config.getKeywords().forEach(keyword -> { + if (isLimit) { + return; + } + CHROME_DRIVER.get(getSearchUrl(keyword, 1)); + submitJobs(keyword); + + }); + log.info(resultList.isEmpty() ? "未投递新的岗位..." : "新投递公司如下:\n{}", resultList.stream().map(Object::toString).collect(Collectors.joining("\n"))); + printResult(); + } + + private static void printResult() { + String message = String.format("\n智联招聘投递完成,共投递%d个岗位,用时%s", resultList.size(), formatDuration(startDate, new Date())); + log.info(message); + sendMessageByTime(message); + resultList.clear(); + CHROME_DRIVER.close(); + CHROME_DRIVER.quit(); + + // 确保所有日志都被刷新到文件 + try { + Thread.sleep(1000); // 等待1秒确保日志写入完成 + // 强制刷新日志 - 使用正确的方法 + ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static String getSearchUrl(String keyword, int page) { + return homeUrl + + JobUtils.appendParam("jl", config.getCityCode()) + + JobUtils.appendParam("kw", keyword) + + JobUtils.appendParam("sl", config.getSalary()) + + "&p=" + page; + } + + private static void submitJobs(String keyword) { + if (isLimit) { + return; + } + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[contains(@class, 'joblist-box__item')]"))); + setMaxPages(); + for (int i = 1; i <= maxPage; i++) { + if (i != 1) { + CHROME_DRIVER.get(getSearchUrl(keyword, i)); + } + log.info("开始投递【{}】关键词,第【{}】页...", keyword, i); + // 等待岗位出现 + try { + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='positionlist']"))); + } catch (Exception ignore) { + CHROME_DRIVER.navigate().refresh(); + SeleniumUtil.sleep(1); + } + // 全选 + try { + WebElement allSelect = WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//i[@class='betch__checkall__checkbox']"))); + allSelect.click(); + } catch (Exception e) { + log.info("没有全选按钮,程序退出..."); + continue; + } + // 投递 + WebElement submit = WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//button[@class='betch__button']"))); + submit.click(); + if (checkIsLimit()) { + break; + } + SeleniumUtil.sleep(1); + // 切换到新的标签页 + ArrayList tabs = new ArrayList<>(CHROME_DRIVER.getWindowHandles()); + CHROME_DRIVER.switchTo().window(tabs.get(tabs.size() - 1)); + //关闭弹框 + try { + WebElement result = CHROME_DRIVER.findElement(By.xpath("//div[@class='deliver-dialog']")); + if (result.getText().contains("申请成功")) { + log.info("岗位申请成功!"); + } + } catch (Exception e) { + log.error("关闭投递弹框失败..."); + } + try { + WebElement close = CHROME_DRIVER.findElement(By.xpath("//img[@title='close-icon']")); + close.click(); + } catch (Exception e) { + if (checkIsLimit()) { + break; + } + } + try { + // 投递相似职位 + WebElement checkButton = CHROME_DRIVER.findElement(By.xpath("//div[contains(@class, 'applied-select-all')]//input")); + if (!checkButton.isSelected()) { + checkButton.click(); + } + List jobs = CHROME_DRIVER.findElements(By.xpath("//div[@class='recommend-job']")); + WebElement post = CHROME_DRIVER.findElement(By.xpath("//div[contains(@class, 'applied-select-all')]//button")); + post.click(); + printRecommendJobs(jobs); + log.info("相似职位投递成功!"); + } catch (NoSuchElementException e) { + log.error("没有匹配到相似职位..."); + } catch (Exception e) { + log.error("相似职位投递异常!!!"); + } + // 投完了关闭当前窗口并切换至第一个窗口 + CHROME_DRIVER.close(); + CHROME_DRIVER.switchTo().window(tabs.get(0)); + } + } + + private static boolean checkIsLimit() { + try { + SeleniumUtil.sleepByMilliSeconds(500); + WebElement result = CHROME_DRIVER.findElement(By.xpath("//div[@class='a-job-apply-workflow']")); + if (result.getText().contains("达到上限")) { + log.info("今日投递已达上限!"); + isLimit = true; + return true; + } + return false; + } catch (Exception e) { + return false; + } + } + + private static void setMaxPages() { + try { + // 到底部 + ACTIONS.keyDown(Keys.CONTROL).sendKeys(Keys.END).keyUp(Keys.CONTROL).perform(); + WebElement inputElement = CHROME_DRIVER.findElement(By.className("soupager__pagebox__goinp")); + inputElement.clear(); + inputElement.sendKeys("99999"); + //使用 JavaScript 获取输入元素的当前值 + JavascriptExecutor js = CHROME_DRIVER; + String modifiedValue = (String) js.executeScript("return arguments[0].value;", inputElement); + maxPage = Integer.parseInt(modifiedValue); + log.info("设置最大页数:{}", maxPage); + WebElement home = CHROME_DRIVER.findElement(By.xpath("//li[@class='listsort__item']")); + ACTIONS.moveToElement(home).perform(); + } catch (Exception ignore) { + StackTraceElement element = Thread.currentThread().getStackTrace()[1]; + log.info("setMaxPages@设置最大页数异常!({}:{})", element.getFileName(), element.getLineNumber()); + log.info("设置默认最大页数50,如有需要请自行调整..."); + maxPage = 50; + } + } + + private static void printRecommendJobs(List jobs) { + jobs.forEach(j -> { + String jobName = j.findElement(By.xpath(".//*[contains(@class, 'recommend-job__position')]")).getText(); + String salary = j.findElement(By.xpath(".//span[@class='recommend-job__demand__salary']")).getText(); + String years = j.findElement(By.xpath(".//span[@class='recommend-job__demand__experience']")).getText().replaceAll("\n", " "); + String education = j.findElement(By.xpath(".//span[@class='recommend-job__demand__educational']")).getText().replaceAll("\n", " "); + String companyName = j.findElement(By.xpath(".//*[contains(@class, 'recommend-job__cname')]")).getText(); + String companyTag = j.findElement(By.xpath(".//*[contains(@class, 'recommend-job__demand__cinfo')]")).getText().replaceAll("\n", " "); + Job job = new Job(); + job.setJobName(jobName); + job.setSalary(salary); + job.setCompanyTag(companyTag); + job.setCompanyName(companyName); + job.setJobInfo(years + "·" + education); + log.info("投递【{}】公司【{}】岗位,薪资【{}】,要求【{}·{}】,规模【{}】", companyName, jobName, salary, years, education, companyTag); + resultList.add(job); + }); + } + + private static void login() { + CHROME_DRIVER.get(loginUrl); + if (SeleniumUtil.isCookieValid("./src/main/java/zhilian/cookie.json")) { + SeleniumUtil.loadCookie("./src/main/java/zhilian/cookie.json"); + CHROME_DRIVER.navigate().refresh(); + SeleniumUtil.sleep(1); + } + if (isLoginRequired()) { + scanLogin(); + } + } + + private static void scanLogin() { + try { + WebElement button = CHROME_DRIVER.findElement(By.xpath("//div[@class='zppp-panel-normal-bar__img']")); + button.click(); + log.info("等待扫码登录中..."); + WAIT.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='zp-main__personal']"))); + log.info("扫码登录成功!"); + SeleniumUtil.saveCookie("./src/main/java/zhilian/cookie.json"); + } catch (Exception e) { + log.error("扫码登录异常!"); + System.exit(-1); + } + } + + private static boolean isLoginRequired() { + return !CHROME_DRIVER.getCurrentUrl().contains("i.zhaopin.com"); + } +} diff --git a/src/main/java/zhilian/ZhilianConfig.java b/src/main/java/zhilian/ZhilianConfig.java new file mode 100644 index 00000000..95594c06 --- /dev/null +++ b/src/main/java/zhilian/ZhilianConfig.java @@ -0,0 +1,42 @@ +package zhilian; + +import lombok.Data; +import lombok.SneakyThrows; +import utils.JobUtils; + +import java.util.List; +import java.util.Objects; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Data +public class ZhilianConfig { + /** + * 搜索关键词列表 + */ + private List keywords; + + /** + * 城市编码 + */ + private String cityCode; + + /** + * 薪资范围 + */ + private String salary; + + + @SneakyThrows + public static ZhilianConfig init() { + ZhilianConfig config = JobUtils.getConfig(ZhilianConfig.class); + // 转换城市编码 + config.setCityCode(ZhilianEnum.CityCode.forValue(config.getCityCode()).getCode()); + String salary = config.getSalary(); + config.setSalary(Objects.equals("不限", salary) ? "0" : salary); + return config; + } + +} diff --git a/src/main/java/zhilian/ZhilianEnum.java b/src/main/java/zhilian/ZhilianEnum.java new file mode 100644 index 00000000..2d939587 --- /dev/null +++ b/src/main/java/zhilian/ZhilianEnum.java @@ -0,0 +1,41 @@ +package zhilian; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +public class ZhilianEnum { + + @Getter + public enum CityCode { + NULL("不限", "0"), + BEIJING("北京", "530"), + SHANGHAI("上海", "538"), + GUANGZHOU("广州", "763"), + SHENZHEN("深圳", "765"), + WUHAN("武汉", "736"), + CHENGDU("成都", "801"); + + private final String name; + private final String code; + + CityCode(String name, String code) { + this.name = name; + this.code = code; + } + + @JsonCreator + public static CityCode forValue(String value) { + for (CityCode cityCode : CityCode.values()) { + if (cityCode.name.equals(value)) { + return cityCode; + } + } + return NULL; + } + } + +} diff --git a/src/main/java/zhilian/ZhilianScheduled.java b/src/main/java/zhilian/ZhilianScheduled.java new file mode 100644 index 00000000..ca3e1782 --- /dev/null +++ b/src/main/java/zhilian/ZhilianScheduled.java @@ -0,0 +1,29 @@ +package zhilian; + +import lombok.extern.slf4j.Slf4j; +import utils.JobUtils; +import utils.Platform; + +/** + * @author loks666 + * 项目链接: https://github.com/loks666/get_jobs + */ +@Slf4j +public class ZhilianScheduled { + + public static void main(String[] args) { + JobUtils.runScheduled(Platform.ZHILIAN); + } + + public static void postJobs() { + safeRun(() -> ZhiLian.main(null)); + } + + private static void safeRun(Runnable task) { + try { + task.run(); + } catch (Exception e) { + log.error("safeRun异常:{}", e.getMessage(), e); + } + } +} diff --git a/src/main/resources/.env_template b/src/main/resources/.env_template new file mode 100644 index 00000000..20c0b519 --- /dev/null +++ b/src/main/resources/.env_template @@ -0,0 +1,5 @@ +HOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key_here +BARK_URL=https://api.day.app/your_key_here +BASE_URL=https://api.ruyun.fun +API_KEY=sk-xxx +MODEL=gpt-4o-mini \ No newline at end of file diff --git a/src/main/resources/chromedriver.exe b/src/main/resources/chromedriver.exe new file mode 100644 index 00000000..1339b31c Binary files /dev/null and b/src/main/resources/chromedriver.exe differ diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml new file mode 100644 index 00000000..6a70ea89 --- /dev/null +++ b/src/main/resources/config.yaml @@ -0,0 +1,49 @@ +# 带[ ]括号的,就是多选,不带的就是单选,所有选项的 开对应true,关对应false,自行根据需要调整 +boss: + debugger: false # 开发者模式,默认为false即可 + sayHi: "您好,我有8年工作经验,还有AIGC大模型、Java,Python,Golang和运维的相关经验,希望应聘这个岗位,期待可以与您进一步沟通,谢谢!" #必须要关闭boss的自动打招呼 + keywords: [ "大模型","Python","Golang","Java" ] # 需要搜索的职位,会依次投递 + industry: [ "不限" ] # 公司行业,只能选三个,相关代码枚举的部分,如果需要其他的需要自己找 + cityCode: [ "上海" ] # 只列举了部分,如果没有的需要自己找:目前支持的:全国 北京 上海 杭州 广州 深圳 成都 天津 + experience: [ "5-10年" ] # 工作经验:"应届毕业生", "1年以下", "1-3年", "3-5年", "5-10年", "10年以上" + jobType: "不限" #求职类型:"全职", "兼职" + salary: "20-50K" # 薪资(单选):"3K以下", "3-5K", "5-10K", "10-20K", "20-50K", "50K以上" + degree: [ "不限" ] # 学历: "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士" + scale: [ "不限" ] # 公司规模:"0-20人", "20-99人", "100-499人", "500-999人", "1000-9999人", "10000人以上" + stage: [ "不限" ] # "未融资", "天使轮", "A轮", "B轮", "C轮", "D轮及以上", "已上市", "不需要融资" + expectedSalary: [ 15,25 ] #期望薪资,单位为K,第一个数字为最低薪资,第二个数字为最高薪资,只填一个数字默认为最低薪水 + waitTime: 10 #每投递一个岗位,等待几秒 + filterDeadHR: true # 是否过滤不活跃HR,该选项会过滤半年前活跃的HR + enableAI: false # 开启AI检测与自动生成打招呼语 + sendImgResume: false # 是否发送图片简历 + deadStatus: [ "2周内活跃","本月活跃","2月内活跃","半年前活跃" ] # 过滤掉HR状态 + +job51: + jobArea: [ "上海" ] #工作地区:目前只有【北京 成都 上海 广州 深圳】 + keywords: [ "java", "python", "go", "golang", "大模型", "软件工程师" ] #关键词:依次投递 + salary: [ "不限" ] #薪资范围:只能选5个【"2千以下", "2-3千", "3-4.5千", "4.5-6千", "6-8千", "0.8-1万", "1-1.5万", "1.5-2万", "2-3万", "3-4万", "4-5万", "5万以上"】 + +lagou: + keywords: [ "AI工程师","Java","Golang","Python" ] #搜索关键词 + cityCode: "上海" #拉勾城市名没有限制,直接填写即可 + salary: "不限" #薪资【"不限","2k以下", "2k-5k", "5k-10k", "10k-15k", "15k-25k", "25k-50k", "50k以上"】 + scale: [ "不限" ] #公司规模【"不限","少于15人", "15-50人", "50-150人", "150-500人", "500-2000人", "2000人以上"】 + gj: "在校/应届,3年及以下" + +liepin: + cityCode: "上海" # 目前支持的:全国 北京 上海 广州 深圳 成都 + keywords: [ "Java", "Python", "Golang", "大模型" ] + salary: "15$30" # 填 15$30 代表 15k-30k + pubTime: "30" # 发布时间,单位天 + +zhilian: + cityCode: "上海" + salary: "25001,35000" #薪资区间 + keywords: [ "AI", "Java", "Python", "Golang" ] + +ai: + introduce: "我熟练使用Java Python Golang语言进行开发,目前主要方向为AI开发,Java熟悉Spring Boot、Cloud生态体系,擅长MySQL、Oracle、PostgreSQL等关系型数据库以及MongoDB、Redis等非关系型数据库。熟悉Docker、Kubernetes等容器化技术,掌握WebSocket、Netty等通信协议,拥有即时通讯系统的开发经验。熟练使用MyBatis-Plus、Spring Data、Django ORM等ORM框架,熟练使用Python、Golang开发,具备机器学习、深度学习及大语言模型的开发与部署经验。此外,我熟悉前端开发,涉及Vue、React、Nginx配置及PHP框架应用" #这是喂给AI的提示词,主要介绍自己的优势 + prompt: "我目前在找工作,%s,我期望的的岗位方向是【%s】,目前我需要投递的岗位名称是【%s】,这个岗位的要求是【%s】,如果这个岗位和我的期望与经历基本符合,注意是基本符合,那么请帮我写一个给HR打招呼的文本发给我,如果这个岗位和我的期望经历完全不相干,直接返回false给我,注意只要返回我需要的内容即可,不要有其他的语气助词,重点要突出我和岗位的匹配度以及我的优势,我自己写的招呼语是:【%s】,你可以参照我自己写的根据岗位情况进行适当调整" #这是AI的提示词,可以自行修改 + +bot: + is_send: false #开启企业微信消息推送 diff --git a/src/main/resources/images/boss.png b/src/main/resources/images/boss.png new file mode 100644 index 00000000..a969f0ef Binary files /dev/null and b/src/main/resources/images/boss.png differ diff --git a/src/main/resources/images/driver.png b/src/main/resources/images/driver.png new file mode 100644 index 00000000..88b3d1a1 Binary files /dev/null and b/src/main/resources/images/driver.png differ diff --git a/src/main/resources/images/getCity.png b/src/main/resources/images/getCity.png new file mode 100644 index 00000000..e9270e12 Binary files /dev/null and b/src/main/resources/images/getCity.png differ diff --git a/src/main/resources/images/jdk17.png b/src/main/resources/images/jdk17.png new file mode 100644 index 00000000..188da07b Binary files /dev/null and b/src/main/resources/images/jdk17.png differ diff --git a/src/main/resources/images/liepin.png b/src/main/resources/images/liepin.png new file mode 100644 index 00000000..3f4f2bc6 Binary files /dev/null and b/src/main/resources/images/liepin.png differ diff --git a/src/main/resources/images/maven.png b/src/main/resources/images/maven.png new file mode 100644 index 00000000..5e6c5cbd Binary files /dev/null and b/src/main/resources/images/maven.png differ diff --git a/src/main/resources/images/myh3.png b/src/main/resources/images/myh3.png new file mode 100644 index 00000000..77dbe94d Binary files /dev/null and b/src/main/resources/images/myh3.png differ diff --git a/src/main/resources/images/pian1.png b/src/main/resources/images/pian1.png new file mode 100644 index 00000000..fddebfe8 Binary files /dev/null and b/src/main/resources/images/pian1.png differ diff --git a/src/main/resources/images/pian2.png b/src/main/resources/images/pian2.png new file mode 100644 index 00000000..0e27fdea Binary files /dev/null and b/src/main/resources/images/pian2.png differ diff --git a/src/main/resources/images/pian3.png b/src/main/resources/images/pian3.png new file mode 100644 index 00000000..739496a6 Binary files /dev/null and b/src/main/resources/images/pian3.png differ diff --git a/src/main/resources/images/pian4.png b/src/main/resources/images/pian4.png new file mode 100644 index 00000000..a54d78f8 Binary files /dev/null and b/src/main/resources/images/pian4.png differ diff --git a/src/main/resources/images/qq.jpg b/src/main/resources/images/qq.jpg new file mode 100644 index 00000000..e1966231 Binary files /dev/null and b/src/main/resources/images/qq.jpg differ diff --git a/src/main/resources/images/run.png b/src/main/resources/images/run.png new file mode 100644 index 00000000..9c02584c Binary files /dev/null and b/src/main/resources/images/run.png differ diff --git a/src/main/resources/images/run1.png b/src/main/resources/images/run1.png new file mode 100644 index 00000000..bb59f4ff Binary files /dev/null and b/src/main/resources/images/run1.png differ diff --git a/src/main/resources/images/temp/tmp.txt b/src/main/resources/images/temp/tmp.txt new file mode 100644 index 00000000..f179d142 --- /dev/null +++ b/src/main/resources/images/temp/tmp.txt @@ -0,0 +1,9 @@ +### 本项目黑名单: + +- QQ:813851861,昵称:the one day 马永豪,山东枣庄 + +
+ myh1 + myh2 + myh3 +
\ No newline at end of file diff --git a/src/main/resources/images/wechatPay.jpg b/src/main/resources/images/wechatPay.jpg new file mode 100644 index 00000000..09a3e486 Binary files /dev/null and b/src/main/resources/images/wechatPay.jpg differ diff --git a/src/main/resources/images/wgroup.jpg b/src/main/resources/images/wgroup.jpg new file mode 100644 index 00000000..9cc0734a Binary files /dev/null and b/src/main/resources/images/wgroup.jpg differ diff --git "a/src/main/resources/images/\351\252\227\345\255\220\347\275\221\347\253\231.png" "b/src/main/resources/images/\351\252\227\345\255\220\347\275\221\347\253\231.png" new file mode 100644 index 00000000..12701693 Binary files /dev/null and "b/src/main/resources/images/\351\252\227\345\255\220\347\275\221\347\253\231.png" differ diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..187c85c8 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + UTF-8 + ${log.pattern} + + + + + + UTF-8 + ${log.pattern} + + + ${log.file} + 30 + + + true + + + + + + + diff --git "a/src/main/resources/\350\256\270\346\204\277\345\242\231.xlsx" "b/src/main/resources/\350\256\270\346\204\277\345\242\231.xlsx" new file mode 100644 index 00000000..d1b87c73 Binary files /dev/null and "b/src/main/resources/\350\256\270\346\204\277\345\242\231.xlsx" differ diff --git "a/\350\256\270\346\204\277\345\242\231.xlsx" "b/\350\256\270\346\204\277\345\242\231.xlsx" new file mode 100644 index 00000000..a7304962 Binary files /dev/null and "b/\350\256\270\346\204\277\345\242\231.xlsx" differ