Performing OCR on Japanese PDFs Using Tesseract and Pytesseract
Extracting Japanese text from a PDF with Tesseract OCR v4 and pytesseract, including the normalization step needed to clean up the output.
Tesseract OCR v4 and its Python wrapper pytesseract can extract Japanese text from PDFs.
The source text is Run, Melos! by Osamu Dazai, a work that is now in the public domain.
Requirements
Before we start, ensure the following libraries are installed:
Additionally, Tesseract OCR itself must be installed. In this note, it is set up using Docker. Please refer to the instructions on the official repository for more details.
Building
Writing Python Script
import refrom datetime import datetime
import pdf2imageimport pytesseract
def to_images(pdf_path: str, first_page: int = None, last_page: int = None) -> list: """ Convert a PDF to a PNG image.
Args: pdf_path (str): PDF path first_page (int): First page starting 1 to be converted last_page (int): Last page to be converted
Returns: list: List of image data """
print(f'Convert a PDF ({pdf_path}) to a png...') images = pdf2image.convert_from_path( pdf_path=pdf_path, fmt='png', first_page=first_page, last_page=last_page, ) print(f'A total of converted png images is {len(images)}.') return images
def to_string(image) -> str: """ OCR an image data.
Args: image: Image data
Returns: str: OCR processed characters """
print(f'Extract characters from an image...') return pytesseract.image_to_string(image, lang='jpn')
def normalize(target: str) -> str: """ Normalize result text.
Applying the following: - Remove new line. - Remove spaces between Japanese characters.
Args: target (str): Target text to be normalized
Returns: str: Normalized text """
result = re.sub('\n', '', target) result = re.sub('([あ-んア-ン一-鿐])\s+((?=[あ-んア-ン一-鿐]))', r'\1\2', result) return result
def save(result: str) -> str: """ Save the result text in a text file.
Args: result (str): Result text
Returns: str: Text file path """
path = 'result.txt' with open(path, 'w') as f: f.write(result) return path
def main() -> None: start = datetime.now() result = ''
images = to_images('run-melos.pdf', 1, 2) for image in images: result += to_string(image) result = normalize(result) path = save(result)
end = datetime.now() duration = end.timestamp() - start.timestamp()
print('----------------------------------------') print(f'Start: {start}') print(f'End: {end}') print(f'Duration: {int(duration)} seconds') print(f'Result file path: {path}') print('----------------------------------------')
if __name__ == '__main__': main()Creating Dockerfile
The run-melos.pdf on line 6 can be downloaded here.
FROM python:3.10
WORKDIR /usr/src/appCOPY app.py ./COPY requirements.txt ./COPY run-melos.pdf ./RUN apt update && apt install -y poppler-utils tesseract-ocr tesseract-ocr-jpn \ && pip install -r requirements.txt
CMD ["python", "app.py"]# CMD ["/bin/sh", "-c", "while :; do sleep 10; done"]Testing
The script converts the PDF to PNG image data with pdf2image, extracts characters from the images with Tesseract OCR and pytesseract, and saves the result to a text file:
NAME=pytesseract-sampledocker build -t $NAME .docker run --name $NAME $NAME
# You can see OCR processing result in `result.txt`.docker cp $NAME:/usr/src/app/result.txt ./less result.txt
# Clean updocker container rm $NAMEdocker image rm $NAMEResult
The result can be downloaded from:
Conclusion
Running a public-domain Japanese PDF through pdf2image and Tesseract in a Docker container extracted readable text that closely matched the original. The lang='jpn' argument to pytesseract.image_to_string is what activates Tesseract’s Japanese-trained model, but getting readable output also depends on the normalize step’s regex, which strips the whitespace Tesseract tends to insert between Japanese characters — without it, the extracted text reads as fragmented even when the underlying recognition is accurate. Comparing the result against the original text of Run, Melos! is worth doing directly rather than skimming, since misreads between visually similar kanji can slip past a quick check. Because this PDF is a clean, digitally rendered public-domain text rather than a scan, accuracy here represents close to a best case — lower-resolution or scanned source material would likely need image preprocessing before OCR to get comparable results.
Related posts
Monitoring OPC UA Variable Nodes with Python
Subscribing to OPC UA variable node changes with opcua-asyncio so the client reacts to pushed updates instead of polling.
Installing Python Packages Without Internet Access
This note describes how to install python packages without Internet connectivity.
Running Proxy.py as a Lightweight HTTP Proxy on EC2
Running Proxy.py on an EC2 instance and reaching it safely through an SSH tunnel, since the proxy has no authentication of its own.

Dependency Injection for Maintainable, Testable Code
Refactoring a tightly coupled TypeScript class into one that's testable with mocks, using Dependency Injection for the salary calculator, the system clock, and SES.
Spying on Mock Object Properties in Jasmine
Working around Jasmine's "already been spied upon" error when spying on a mocked object's property with Object.getOwnPropertyDescriptor.
