💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
qr-sign-password-protected-pdf-python

מבוא

קיים תבנית בת שלושה שלבים שרוב הצוותים משתמשים בה כאשר מסמך שצריך לחתום עליו מתגלה כמוצפן: פענוח, חתימה על הטקסט הפשוט, והצפנה מחדש של התוצאה. זה עובד. המשמעות היא שלמספר מאות מילישניות קיימת עותק קריא של מסמך מוגן במכוון בתיקייה זמנית, ובצינור עבודה מבוקר החלון הזה הוא הממצא ולא החתימה.

חתימת PDF מוגן היא יכולת של GroupDocs.Signature עבור Python דרך .NET שמדלגת על שלושת השלבים הללו לחלוטין: הסיסמה פותחת את המקור במקום, החתימה מתבצעת, והפלט נכתב חזרה כמוגן. מאמר זה משווה את ארבעת דרכי הטיפול בסיסמה – שתי דרכים שעובדות ושתי דרכים שמכוונות לכשל – ומסביר את חוזה הכשל הספציפי לקישור הזה.

למה זה חשוב

טיפול בסיסמאות הוא המקום שבו צינורות המסמכים דולפים. לא דרך ספריית החתימה, בדרך כלל, אלא דרך התשתית שמסביב: קובץ זמני שהיה אמור להימחק, מטפל החריגות שבלע שגיאת סיסמה שגויה וניסה מחדש לנצח, העותק החתום שנמסר עם סיסמה שהמקבל מעולם לא נודע עליה.

שלושת המקרים נובעים מאותו גורם שורשי – הסיסמה מתייחסת כאל משהו שיש להסיר מהדרך במקום כחלק מהפעולה. LoadOptions ו‑SaveOptions מחזירים אותה לתוך הפעולה.

דרישות מוקדמות

Python 3 ו‑groupdocs-signature-net==26.1, בנוסף ל‑PDF עם סיסמת משתמש. ללא רישיון הספרייה פועלת במצב הערכה, שממשיך לחתום אך מוסיף טקסט משלה לדף.

התקנה

pip install groupdocs-signature-net==26.1

שיטה 1 – שמירת הסיסמה המקורית

ברירת המחדל, והקלה ביותר מבחינת קוד. הסיסמה נכנסת דרך LoadOptions, ואין כלל SaveOptions שעובר:

load_options = LoadOptions()
load_options.password = password
options = _build_qr_options(qr_text)
with signature.Signature(source_path, load_options) as sign:
    result = sign.sign(output_path, options)
    return len(result.succeeded)

היעדר SaveOptions הוא מה שמבצע את העבודה האמיתית כאן. use_original_password מוגדר כברירת מחדל ל‑True, ולכן GroupDocs מיישם מחדש את סיסמת המקור על הפלט החתום. אין רגע שבו קיימת גרסה בלתי מוגנת, על הדיסק או אחרת, ו‑len(result.succeeded) מדווח כמה חתימות נכתבו.

שיטה 2 – שינוי הסיסמה של העותק החתום

כאשר המסמך החתום מועבר לצד אחר, הצעד ההגיוני הוא לתת לעותק שלו אישור משלו ולהשאיר את המקור ללא שינוי:

save_options = SaveOptions()
save_options.password = new_password
save_options.use_original_password = False
with signature.Signature(source_path, load_options) as sign:
    result = sign.sign(output_path, options, save_options)
    return len(result.succeeded)

שתי השורות של SaveOptions נדרשות, וזה הפרט שכדאי לזכור: הגדרת password תוך השארת use_original_password בערך ברירת המחדל אינה משנה דבר שניתן לצפייה. הדגל מנצח, הפלט שומר על הסיסמה הישנה, ואתם מגלים זאת כאשר המקבל מדווח שהסיסמה שנשלחה אינה עובדת.

שיטה 3 ו‑4 – שני הכישלונות

מסמך מוצפן מגיב באופן שונה לחוסר סיסמה ולסיסמה שגויה, וההבדל ראוי לטיפול.

בלי LoadOptions כלל, הפתיחה נכשלת ולא נכתב דבר:

try:
    with signature.Signature(source_path) as sign:
        sign.sign(output_path, options)
    return ""
except RuntimeError as error:
    return proxy_error_name(error)

זה מחזיר PasswordRequiredException. אם מספקים סיסמה שגויה במקום זאת, הקוד הזה מחזיר IncorrectPasswordException. הראשון משמעותו לבקש מהמשתמש אישור; השני משמעותו שהאישור שברשותכם אינו עדכני. מטפל שלא מסוגל להבדיל ביניהם ימשיך לנסות סיסמה שלעולם לא תעבוד.

חוזה הכישלון, ולמה הקוד הברור נשבר

זה החלק שמגבה חצי יום אם אף אחד לא מזהיר אתכם. הקישור חושף PasswordRequiredException, IncorrectPasswordException ו‑GroupDocsSignatureException כשמות גלויים שאינם יורשים מ‑BaseException. כתבו את המטפל האינטואיטיבי:

except IncorrectPasswordException:
    ...

ו‑Python תזרוק TypeError: catching classes that do not inherit from BaseException is not allowed. השגיאה המקורית נעלמת, מוחלפת באחת שמצביעה על שורת ה‑except שלכם במקום על הסיסמה. כתבתי בדיוק את המטפל הזה בפעם הראשונה, והעשרים דקות שביליתי בקריאת ה‑TypeError הן הסיבה שהקטע הזה קיים.

מה שבא בפועל הוא RuntimeError שההודעה שלו מתחילה ב‑Proxy error(<Name>): . ניתוח הקידומת משחזר את הסיבה:

message = str(error)
marker = "Proxy error("
if not message.startswith(marker):
    return ""
start = len(marker)
end = message.find(")", start)
if end < 0:
    return ""
return message[start:end]

בצעו סניף על שם שהוחזר במקום על טקסט ההודעה, שמכיל נתיבי קבצים ומשתנה בין הרצות.

בדיקה לפני החתימה

קיימת דרך חמישית שכדאי לדעת עליה, והיא לא כותבת דבר כלל. פתיחת המסמך עם LoadOptions וקראת get_document_info מחזירה את הפורמט, מספר העמודים והגודל בעוד שהקובץ נשאר מוצפן על הדיסק:

with signature.Signature(source_path, load_options) as sign:
    info = sign.get_document_info()
    return info.file_type.file_format, info.page_count, info.size

שתי שימושים לכך. כאשר הסיסמה הגיעה מטופס משתמש, זה מאמת את האישור בקריאה זולה במקום באמצע אצווה של מאתים מסמכים. וכאשר צינור עבודה אינו מורשה לאחסן טקסט גלוי כלל, הוא עדיין מאפשר לצינור לדווח על מה שהוא מחזיק – ספירת עמודים ללוג ביקורת, גדלים למכסת – ללא פענוח כלשהו.

השוואת השיטות: מתי להשתמש בכל אחת

שיטה מתאים ביותר ל‑ יתרונות מרכזיים מגבלות
שמירת הסיסמה המקורית צינורות החותמים במקום אין SaveOptions, אין כתיבה ברורה המקבל צריך את סיסמת המקור
שינוי סיסמה בעת שמירה העברה לצד אחר המקור שומר את האישור שלו, העותק מקבל חדש שתי שורות SaveOptions, קל לשכוח אחת
ללא סיסמה (כשל) הוכחת החוזה בבדיקות נכשל בפתיחה, לא נכתב דבר אינה דרך חתימה
סיסמה שגויה (כשל) הבדלה בין אישור ישן שם חריגה ייחודי אינה דרך חתימה

האם קריאת הפלט בחזרה שווה את הקריאה הנוספת?

כן, משתי סיבות. פתיחה מחדש של הקובץ החתום עם QrCodeVerifyOptions מאשרת שהחתימה שרדה את השמירה, וכיוון שהפתיחה מחדש מחייבת לספק סיסמה, היא גם מאשרת שהפלט עדיין מוצפן. ספירה של אפס כמעט תמיד נובעת מבעיה ברישיון ולא מכשלת חתימה – קריאת sign תזרוק כאשר היא נכשלת באמת, ולכן שקט עם אפס תואמים מצביע על בנייה ללא רישיון.

מה העלות של המעבר

אין שינוי מבני. אם הקוד שלכם כבר מפענח לקובץ זמני, השינוי הוא מחיקת הצעד הזה, העברת הסיסמה ל‑LoadOptions, והסרת קריאת ההצפנה מחדש בסוף – בדרך כלל ירידה נטו במספר השורות. קריאת החתימה עצמה אינה משתנה, והפלט הוא PDF חתום ביט‑ל‑ביט עם אותה הגנה שהיה לו בתחילה.

המקום היחיד שבו צריך לשים לב הוא קוד הניקוי. צינור שמבוסס על פענוח‑חתימה‑הצפנה מחדש בדרך כלל כולל בלוק finally שמוחק את הקובץ הזמני, וברגע שהקובץ הזמני נעלם הבלוק מנסה למחוק נתיב שאיננו קיים יותר.

שיטות עבודה מומלצות

  • השאירו use_original_password ללא שינוי אלא אם אתם מחליפים בכוונה; ברירת המחדל היא הבטוחה.
  • נתחו את שם הפרוקסי פעם אחת, בעזרייה, ובצעו סניף עליו בכל מקום אחר.
  • אמתו סיסמה שסופקה על ידי משתמש עם get_document_info לפני תחילת אצווה, כך שאישור שגוי יעלה קריאה זולה במקום ריצה חצי‑גמורה.
  • אל תכתבו את הפלט החתום על נתיב המקור, כך שטעות תשאיר את המקור ניתן לשחזור.

סיכום

הסיסמה איננה מכשול שיש לעקוף לפני החתימה – היא ארגומנט של הפעולה. פתחו עם LoadOptions, קבעו את הגנת הפלט עם SaveOptions, נתחו את שם הפרוקסי כאשר משהו נכשל, ואמתו דרך הסיסמה לאחר מכן. הדוגמה מריצה את כל ארבעת הנתיבים בבת אחת, כך שההבדל ביניהם נצפה בפקודה אחת במקום בפסקה שלמה של אמון.

משאבים נוספים