Предыстория

Моя команда занимается разработкой медицинского ПО: приложения для передачи направлений пациентов в системе здравоохранения Австралии.

Принцип заключается в следующем: если терапевту нужно направить пациента для предоставления вторичных услуг здравоохранения, например, в стационар или в специализированную клинику, он должен написать направление с информацией о пациенте, его истории и причине направления. В прошлом эти операции выполнялись по факсу; это явно устаревшая технология, к тому же многие направления отклонялись, потому что в них отсутствовала часть необходимой информации.

Система электронных направлений удобна для всех. Когда врач создаёт новое направление, мы автоматически извлекаем информацию из PMS (patient management software, системы управления пациентами, которой владеет наша компания) с данными пациента, этнической принадлежностью, индексом массы тела, принимаемыми лекарствами, медицинской историей и всем остальным, что необходимо для направления. В бланке направления есть валидируемая «форма специализации» с полями, относящимися к конкретной услуге, для которой выписывается направление.

Благодаря этому в направлении содержится вся необходимая информация. И, разумеется, в форме также есть большое текстовое поле для свободного текста (referral letter), в котором терапевт может объяснить, почему он решил выписать пациенту направление.

При отправке направления данные передаются в цифровом виде. Они преобразуются в один из нескольких форматов в зависимости от получателя. Некоторые получатели используют для приёма направлений наш продукт Referral Manager; в этом случае нам не нужно ничего преобразовывать, они просто получают доступ в веб-интерфейсе к тем данным, которые хранятся в базе данных. Однако обычно данные преобразуются в HL7 (старый текстовый формат файлов медицинской информации), CDA (XML-документ с отдельной таблицей стилей) или просто в PDF с человекочитаемой информацией. Благодаря этому данные совместимы со множеством различных электронных систем других компаний.

Также выполняется сохранение PDF-копии направления в PMS врача для ведения его собственной картотеки. Это позволяет ему просматривать всю историю пациента в PMS без необходимости поиска документов в куче приложений разных разработчиков.

Моя роль

Когда я работал в компании, моя должность находилась где-то между разработчиком и поддержкой. Я занимался обслуживанием системы и обеспечением её работоспособности.

Кого-то из читателей может удивить то, что подчистка старых файлов логов, чтобы на серверах не закончилось место, во многих компаниях-разработчиках ПО выполняется вручную. Можно подумать, что эту задачу легко автоматизировать, и что вы сами автоматизировали бы её на своих серверах. Да, это легко, если вы делаете это на собственных серверах, которые вы знаете, как свои пять пальцев, и где можете делать всё, что пожелаете. Однако в контексте нашей компании всё было немного сложнее.

Также я непосредственно работал с командой поддержки. Если у врачей всплывали какие-то баги в ПО или проблемы, слишком технические для решения службой техподдержки, их перенаправляли мне. Я изучал скриншоты и все логи ошибок, обсуждал с другими членами команды, правильное ли это поведение. Если это был баг, то я трассировал его по серверам, чтобы найти первопричину, и исследовал исходный код, чтобы понять, как возникло такое состояние. Далее я писал тикет Jira с подробным описанием бага и способом его устранения. Обычно устранением самих багов занимался не я, и меня это вполне устраивало.

Я программист, и время от времени вносил изменения в код, когда считал, что это оправдано, но обычно кодом занимались другие участники команды, а я оставался на своей маленькой роли.

Вы бы удивились, как много глупых, монотонных, шаблонных задач мне приходилось выполнять, чтобы всё работало без проблем. Мне нравилось, когда можно было при помощи интуиции докапываться до причин новых багов, но чаще всего ситуации повторялись многократно, и можно было найти вики-страницу с конкретным пошаговым списком решения проблемы. Приходилось много копипастить GUID направлений в шаблоны SQL.

Самый загадочный баг

Один из таких повторяющихся багов, требовавших ручного вмешательства, повторялся каждые 2-4 недели. Направление успешно отправлялось врачом и сохранялось в нашей базе данных, и на стороне врача всё выглядело нормально. Но при удалении направления из очереди и попытке отправить его получателю возникала ошибка преобразования в один из перечисленных выше форматов:

Illegal Character entity: expansion character (code 0x2) not a valid XML character

[Введён недопустимый символ: символ расширения (код 0x2) не является допустимым символом XML]

Что это вообще значит?

Когда я только пришёл на эту работу, для решения проблемы мне нужно было открыть соответствующую вики-страницу и выполнить инструкции с неё. По инструкциям нужно было запустить команду SQL, которая, по сути, находит и заменяет все вхождения \u0002 (последовательности из этих шести символов) пустой строкой, таким образом полностью удаляя недопустимый символ. Затем я добавлял направление обратно в очередь, и оно успешно обрабатывалось.

Чем дольше я работал с этими системами, тем глубже я их понимал. У меня возникало всё больше и больше вопросов:

  • Что это за символ 0x2?

  • Где он находится в данных?

  • Почему он в них, как он туда попадает? Что его создаёт?

  • Должен ли он там находиться? Плохо ли то, что мы решаем проблему, удаляя его?

  • Почему это вызывает проблемы?

  • Почему это происходит с такой регулярностью (каждые 2-4 недели)?

Много месяцев меня вполне устраивало то, что я просто должен решать проблему для каждого поломанного направления, по одному за раз; но каждый раз эти вопросы всё больше меня тяготили.

Когда эта загадка стала невыносимой, я решил потратить время на более подробное изучение системы, и каждая моя попытка добавляла в список всё более странные вопросы.

Поломанные данные?

Возможно, этот странный символ возникает при извлечении из PMS поломанных данных. Такое случается нередко; время от времени я диагностирую в продакшене баг, возникающий из-за содержащихся в PMS недопустимых данных, которые невозможно ввести, например, лекарство без названия или вычисление индекса массы тела без веса. Такие данные невозможно ввести с нуля, поэтому мы не смогли бы воссоздать эти проблемы в нашем тестовом окружении, и я, честно говоря, не знаю, как они появляются в продакшен-системах PMS. Возможно, клиенты обновились с более старой версии, где валидация выполнялась менее тщательно? Как бы то ни было, мы аккуратно брали эти данные из PMS (вне зависимости от их валидности) и вводили их в нашу форму. Когда врач создавал направление, оно пыталось передать плохие данные, которые не пропускали наши валидации, из-за чего направление невозможно было передать в бэкенд. Возможно, этот символ 0x2 появлялся из-за загрузки поломанных данных?

Я внимательно изучил SQL-скрипт, удалявший эти \u0002, чтобы понять, какое поле он редактировал. Так бы я мог разобраться, что искать. Выяснилось, что он удалял все вхождения в ответе на форму специализации. Это означало, что в полях формы специализации, в том числе и тех, которые автоматически подтягивались из PMS, мог оказаться этот символ. Неплохо для начала.

Я открыл данные направления, чтобы понять, в каком поле формы специализации они встречаются, и у меня появилось ещё больше вопросов. Это было поле referral letter, информацию в которое вводит врач, оно не заполняется автоматически PMS. То есть символ 0x2 находится там, потому что его ввёл врач! Что? Почему?

Что это вообще такое?

Наверно, стоит объяснить, что это за символ 0x2, чтобы вы поняли, почему ввод врачом этого символа крайне маловероятен.

Компьютеры хранят всё в байтах. Каждая буква текста этой статьи представлена в компьютере в виде числа. Например, латинская «A» — это 65, и так далее, вплоть до 127. Этого места достаточно для хранения всех букв, символов и чисел английского языка, и остаётся ещё немного свободных позиций. Числа от 31 и ниже не нужны для отображаемых на экране символов. При проектировании этой системы какие-то учёные мужи решили поместить в позиции до 31 «управляющие символы». Это символы, невидимые для читающего текст человека. Они сообщают компьютеру, как размещать идущий после них текст.

Во времена телетайпов эти символы были очень важны. При выводе телетайпом текста на бумагу, без управляющих символов в потоке текста он не будет знать, когда переместиться на следующую строку. Управляющий символ 0xD сообщает, что нужно переместить печатающую головку обратно влево, после чего 0xA сообщает, что нужно выполнить подачу бумаги на одну строку, чтобы печать началась со следующей строки. Есть даже символы наподобие 0x7, сообщающие, что нужно издать звуковой сигнал, но если попробовать использовать его сегодня, компьютер, вероятно, печально пискнет.

Большинство этих символов сегодня необязательно, потому что у компьютеров есть гораздо более совершенные способы создания структуры и форматирования текста. Пожалуй, единственный символ, который мы встречаем — это 0xA, начинающий новую строку текста, но само значение его изменилось: компьютеры достаточно умны, чтобы понимать, когда текст становится слишком длинным, и могут переносить его на следующую строку без прямой команды. Но вы всё равно можете нажать на Enter, чтобы вставить 0xA для принудительного создания новой строки.

Что же такое 0x2? Этот символ называется «начало текста». Есть также 0x3, «конец текста». Их можно использовать для ограничения определённых разделов текста. Это сложно объяснить без примера, но я не смог найти ни одного случая их применения в реальном мире, потому что, как уже было сказано, теперь у нас есть более совершенные способы форматирования, и 0x2 не используется. Медицинские технологии бывают довольно старыми; возможно, есть какая-то компьютерная система, до сих пор кодирующая данные подобным образом?

Если бы символ использовался именно так, то можно было бы ожидать встретить в referral letter соответствующий символ 0x3, обозначающий конец текста. Но его нет. В referral letter присутствует только 0x2. Очень странно.

Подведём итог: символ 0x2 — это «начало текста», но этот символ не передаёт своего семантического значения, не используется ни в одной из известных мне систем и даже не применяется в данном случае для обозначения раздела текста.

...Теперь вы можете понять, почему при исследовании этой проблемы у меня возникали всё более странные вопросы.

Почему каждые несколько недель?

Отчаянно пытаясь найти паттерн, я начал изучать сводку по всем направлениям, в которых возникала эта проблема. Я заметил, что эти направления обычно отправляли одни и те же врачи с одними и теми же пациентами. Это определённо указывало на наличие плохих данных в файле пациента в PMS. Но я уже исключил эту возможность, потому что символ встречался в referral letter.

Мне стало интересно, можно ли извлечь ещё какие-то сведения из этой сводки. Возможно, у некоторых врачей есть какое-то ПО или процессы, повышающие вероятность возникновения бага, и поэтому он не встречается у других врачей?

Я решил попробовать почитать текст referral letter с проблемными данными, и сразу же заметил что-то странное: в тексте присутствовали жёсткие переносы (hard wrap).

Обычно текст заполняет свободное пространство и переносится на следующую строку автоматически. Но в этом referral letter, в каждой строке встречался символ новой строки, как будто врач вводил текст в поле и нажимал на Enter, чтобы перейти на следующую строку.

Зачем он это делал? Я знаю, что многие врачи на «вы» с компьютерами; возможно, кто-то из них считает, что нужно контролировать строки самостоятельно, как на печатной машинке. Но ведь наверняка он замечал, даже случайно, что если написать слишком много текста, то он переносится на следующую строку?

Какой могла быть причина жёстких переносов в том тексте?

...Все мои исследования приводили лишь к новым вопросам.

Что означает наличие жёстких переносов?

Мысль о том, что одновременно так могли вести себя несколько слабо владеющих компьютером врачей, казалась мне маловероятной, но всё же... Я открыл ещё одно referral letter того же врача и прочитал его, ожидая увидеть те же самые паттерны нажатий на Enter, что могло бы подтвердить мою теорию.

Но я увидел точно такое же referral letter.

Тот же текст. Те же жёсткие переносы. Тот же недопустимый символ, который нужно было удалить.

Чтобы удостовериться, я выбрал произвольное валидное referral letter, и в его тексте не нашлось никаких символов новой строки, как и ожидалось. Возможно ли, что жёсткие переносы как-то коррелируют с символом 0x2?

Я пригляделся к жёстким переносам внимательнее, и заметил нечто подобное:

I'm referring Liam here because of consistent history of slow speech development. Physically, his well♦being is healthy, so likely needs special tutoring, could be related to psychological issues.

(Этот пример я придумал сам. Он не скопирован и не воспроизведён из реального направления.)

Во всех строках есть жёсткие переносы, за исключением строки с символом 0x2 (я обозначил его символом ромба). Символ 0x2 встретился там, где я бы ожидал символ новой строки. Как будто символ новой строки был повреждён и заменён чем-то недопустимым. Но как такое может происходить и почему это стабильно происходит во всех referral letter по конкретному пациенту, не влияя на остальную часть текста?

Я решил, что жёсткие переносы — лучшая моя зацепка, поэтому сосредоточился на анализе причин их возникновения. Возможно, врач сначала решил написать письмо в документе Word, а затем вставил его в форму, из-за чего и появились жёсткие переносы?... Нет, так как врач отправил несколько одинаковых писем, то, вероятно, скопипастил текст из какого-то более постоянного источника. Возможно, из файла PDF в медицинской карте пациента? Файлы PDF своеобразны, поэтому было бы логично, что при копировании PDF в концах строк добавляются символы новой строки. Я попробовал составить PDF в Adobe Acrobat, сохранить его и скопировать текст в «Блокнот». И я оказался прав! PDF добавил в текст жёсткие переносы в местах разрыва строк.

Из какого PDF врач мог скопировать текст? Так как письмо писалось врачом от первого лица и в нём излагались основания для направления пациента, единственный логичный вариант заключался в том, что он скопировал текст из referral letter. Врач создал направление, ввёл текст referral letter, отправил, PDF-копия referral letter сохранилась в его PMS, после чего он открыл PDF, скопировал текст и вставил его в новое referral letter!

Всё наконец-то начало обретать смысл. Я ощутил невероятный всплеск воодушевления, как будто через Матрицу заглянул в мозг врача и его глазами наблюдал то, что делал он.

Но как это связано с символом?

Исследовав несколько referral letter от других врачей, я заметил кое-что ещё. В местах, где встречался 0x2 вместо символа новой строки, похоже, строка должна была заканчиваться дефисом. В показанном выше примере «well-being» пишется с дефисом. Если это слово встречалось в конце строки, компьютер переносил бы его так:

development. Physically, his well- being is healthy, so likely needs

Дефис превращался в 0x2. Значит, если дефис встречается в конце строки PDF и переносится, то каким-то образом повреждается? Может, проблема в ПО, с помощью которого мы создаём отправляемые PDF, и в этом нишевом случае оно генерирует PDF неправильно?

Я попробовал воспроизвести действия врача. Открыл свою тестовую копию направлений, написал направление, в котором referral letter состояло из набора слов с дефисами, понадеявшись, что одно из них будет оказано разбито на разные строки при преобразовании в PDF. Мне понадобилось несколько попыток, но наконец удалось получить нужный PDF. Но при его просмотре в PMS он выглядел нормально, дефисы рендерились без ошибок.

Я создал новое направление и скопипастил текст из PDF в новое referral letter, поймав, наконец, требуемый текст. Одна из строк не имела жёсткого переноса. Посередине строки находился символ прямоугольника. А когда я отправил новое направление и посмотрел логи, то увидел ошибку:

Illegal Character entity: expansion character (code 0x2) not a valid XML character.

Но почему так происходит?

Наша библиотека записи PDF настолько поломана, что создавала PDF, в котором символ отображался правильно, но копировался странно? Наверно, это возможно. Я видел PDF книг с отсканированным и распознанным текстом, где при выделении и копировании копировался некорректный вывод OCR.

Я не знаю, как заниматься отладкой PDF, поэтому сделал то, что пришло в голову первым: открыл поломанный PDF в Firefox и при помощи devtools исследовал каждую строку текста. На строке с дефисом я изучил символы в конце, но они казались нормальными, и визуально, и в devtools. Хм. Не этого я ожидал. Чтобы проверить ещё раз, я снова попробовал скопировать текст из основного PDF, но на этот раз получил реальный дефис. Копирование PDF во второй раз не дало мне 0x2. Почему? Это ведь тот же PDF!

Я воспроизвёл свои шаги и снова скопировал из PDF, отправленного PMS. В буфере обмена оказался 0x2.

То есть в самом PDF всё нормально. Возможно, символ появляется из-за программ просмотра PDF?

Я попробовал скопировать этот текст во всех программах просмотра PDF, до которых смог дотянуться: Firefox, Chrome, Edge и Adobe Acrobat. (Я проверил и в Opera, но, разумеется, в ней используется та же программа, что и в Chrome. Удивительно, что в Edge она своя, хотя это в буквальном смысле Chrome.) В этих четырёх PDF-программах я наблюдал четыре различающихся поведения при копировании дефиса:

  • Копирование переноса строки и дефиса (отлично)

  • Копирование только дефиса (норм)

  • Ничего не копируется (хм...)

  • Копируется 0x2 (что???)

При копировании дефиса в конце строки с переносом текста в программе просмотра PDF Microsoft Edge в буфер обмена попадает 0x2. В большинстве Windows-систем эта программа используется для открытия файлов PDF по умолчанию. PMS использует PDF-программу Edge.

После этого я изложил свои открытия в тикете Jira и отправил его команде разработчиков. Теперь, когда мы знали, что 0x2 не имеет никакого смысла и что мы не можем контролировать его появление в текстовом поле, они предложили автоматически удалять 0x2 отовсюду. Я настоял, что код нужно изменить так, чтобы 0x2 заменялся на дефис.

Позже была выпущена новая версия, и проблема больше не возникала. Я решил эту загадку. Это была самая дурацкая кроличья нора, в которую я когда-либо нырял, и самый интересный баг, который я устранял на работе.

Комментарии (2)


  1. EugeneVRN
    06.08.2025 14:04

    В Австралии настолько ничего не происходит что из обычной ошибки форматирования текста создали целое расследование.


  1. Javian
    06.08.2025 14:04

    Он лишил кого-то работы.